@oxy.so/bloom 7.3.0 → 7.5.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/docs/adoption-matrix.mdx +9 -9
- package/docs/appearance.mdx +1 -1
- package/docs/card.mdx +14 -0
- package/docs/content-panel.mdx +17 -0
- package/docs/layout.mdx +33 -0
- package/docs/mail-thread.mdx +9 -1
- package/docs/page-footer.mdx +13 -0
- package/docs/page-header.mdx +7 -0
- package/lib/commonjs/card/Card.js +6 -2
- package/lib/commonjs/card/Card.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.js +10 -7
- package/lib/commonjs/content-panel/ContentPanel.js.map +1 -1
- package/lib/commonjs/content-panel/ContentPanel.web.js +12 -7
- package/lib/commonjs/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +21 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/layout/ScrollArea.js +41 -0
- package/lib/commonjs/layout/ScrollArea.js.map +1 -0
- package/lib/commonjs/layout/index.js +21 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/screen-scope.js +6 -2
- package/lib/commonjs/layout/screen-scope.js.map +1 -1
- package/lib/commonjs/layout/scroll-metrics.js +50 -0
- package/lib/commonjs/layout/scroll-metrics.js.map +1 -0
- package/lib/commonjs/layout/scroll-offset.js +5 -1
- package/lib/commonjs/layout/scroll-offset.js.map +1 -1
- package/lib/commonjs/layout/use-scroll-metrics-binding.js +54 -0
- package/lib/commonjs/layout/use-scroll-metrics-binding.js.map +1 -0
- package/lib/commonjs/mail-thread/MailMessage.js +1 -1
- package/lib/commonjs/mail-thread/MailMessage.js.map +1 -1
- package/lib/commonjs/mail-thread/MailThread.js +3 -4
- package/lib/commonjs/mail-thread/MailThread.js.map +1 -1
- package/lib/commonjs/mail-thread/shared.js +3 -3
- package/lib/commonjs/mail-thread/shared.js.map +1 -1
- package/lib/commonjs/page-footer/PageFooter.js +10 -2
- package/lib/commonjs/page-footer/PageFooter.js.map +1 -1
- package/lib/commonjs/page-header/PageHeader.js +4 -2
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/module/card/Card.js +6 -2
- package/lib/module/card/Card.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.js +11 -8
- package/lib/module/content-panel/ContentPanel.js.map +1 -1
- package/lib/module/content-panel/ContentPanel.web.js +13 -8
- package/lib/module/content-panel/ContentPanel.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/layout/ScrollArea.js +36 -0
- package/lib/module/layout/ScrollArea.js.map +1 -0
- package/lib/module/layout/index.js +3 -0
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/screen-scope.js +6 -2
- package/lib/module/layout/screen-scope.js.map +1 -1
- package/lib/module/layout/scroll-metrics.js +42 -0
- package/lib/module/layout/scroll-metrics.js.map +1 -0
- package/lib/module/layout/scroll-offset.js +5 -1
- package/lib/module/layout/scroll-offset.js.map +1 -1
- package/lib/module/layout/use-scroll-metrics-binding.js +51 -0
- package/lib/module/layout/use-scroll-metrics-binding.js.map +1 -0
- package/lib/module/mail-thread/MailMessage.js +1 -1
- package/lib/module/mail-thread/MailMessage.js.map +1 -1
- package/lib/module/mail-thread/MailThread.js +1 -2
- package/lib/module/mail-thread/MailThread.js.map +1 -1
- package/lib/module/mail-thread/shared.js +3 -3
- package/lib/module/mail-thread/shared.js.map +1 -1
- package/lib/module/page-footer/PageFooter.js +10 -2
- package/lib/module/page-footer/PageFooter.js.map +1 -1
- package/lib/module/page-header/PageHeader.js +4 -2
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/typescript/commonjs/card/Card.d.ts.map +1 -1
- package/lib/typescript/commonjs/card/types.d.ts +5 -5
- package/lib/typescript/commonjs/card/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/types.d.ts +7 -0
- package/lib/typescript/commonjs/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/ScrollArea.d.ts +11 -0
- package/lib/typescript/commonjs/layout/ScrollArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +5 -0
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/scroll-metrics.d.ts +15 -0
- package/lib/typescript/commonjs/layout/scroll-metrics.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/use-scroll-metrics-binding.d.ts +11 -0
- package/lib/typescript/commonjs/layout/use-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/commonjs/mail-thread/MailThread.d.ts.map +1 -1
- package/lib/typescript/commonjs/mail-thread/shared.d.ts +2 -2
- package/lib/typescript/commonjs/mail-thread/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/note-card/shared.d.ts +1 -1
- package/lib/typescript/commonjs/note-card/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-footer/PageFooter.d.ts +1 -1
- package/lib/typescript/commonjs/page-footer/PageFooter.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-footer/types.d.ts +8 -2
- package/lib/typescript/commonjs/page-footer/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/types.d.ts +3 -4
- package/lib/typescript/commonjs/page-header/types.d.ts.map +1 -1
- package/lib/typescript/module/card/Card.d.ts.map +1 -1
- package/lib/typescript/module/card/types.d.ts +5 -5
- package/lib/typescript/module/card/types.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/ContentPanel.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/types.d.ts +7 -0
- package/lib/typescript/module/content-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/layout/ScrollArea.d.ts +11 -0
- package/lib/typescript/module/layout/ScrollArea.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +5 -0
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -1
- package/lib/typescript/module/layout/scroll-metrics.d.ts +15 -0
- package/lib/typescript/module/layout/scroll-metrics.d.ts.map +1 -0
- package/lib/typescript/module/layout/scroll-offset.d.ts.map +1 -1
- package/lib/typescript/module/layout/use-scroll-metrics-binding.d.ts +11 -0
- package/lib/typescript/module/layout/use-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/module/mail-thread/MailThread.d.ts.map +1 -1
- package/lib/typescript/module/mail-thread/shared.d.ts +2 -2
- package/lib/typescript/module/mail-thread/shared.d.ts.map +1 -1
- package/lib/typescript/module/note-card/shared.d.ts +1 -1
- package/lib/typescript/module/note-card/shared.d.ts.map +1 -1
- package/lib/typescript/module/page-footer/PageFooter.d.ts +1 -1
- package/lib/typescript/module/page-footer/PageFooter.d.ts.map +1 -1
- package/lib/typescript/module/page-footer/types.d.ts +8 -2
- package/lib/typescript/module/page-footer/types.d.ts.map +1 -1
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/page-header/types.d.ts +3 -4
- package/lib/typescript/module/page-header/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/card/Card.tsx +6 -2
- package/src/card/types.ts +5 -5
- package/src/content-panel/ContentPanel.tsx +13 -10
- package/src/content-panel/ContentPanel.web.tsx +13 -9
- package/src/content-panel/types.ts +7 -0
- package/src/index.ts +4 -1
- package/src/index.web.ts +4 -1
- package/src/layout/ScrollArea.tsx +25 -0
- package/src/layout/index.ts +6 -0
- package/src/layout/screen-scope.tsx +2 -1
- package/src/layout/scroll-metrics.tsx +35 -0
- package/src/layout/scroll-offset.ts +5 -1
- package/src/layout/use-scroll-metrics-binding.ts +37 -0
- package/src/mail-thread/MailMessage.tsx +1 -1
- package/src/mail-thread/MailThread.tsx +0 -2
- package/src/mail-thread/shared.ts +3 -3
- package/src/note-card/shared.ts +1 -1
- package/src/page-footer/PageFooter.tsx +11 -4
- package/src/page-footer/types.ts +8 -2
- package/src/page-header/PageHeader.tsx +4 -2
- package/src/page-header/types.ts +3 -4
|
@@ -38,7 +38,7 @@ import { StyleSheet, useWindowDimensions } from 'react-native';
|
|
|
38
38
|
import { useOptionalPanelChrome, usePanelShape } from '../styles/panel-chrome';
|
|
39
39
|
import { surfaceStyle as resolveShapeStyle } from '../shapes/surface-style';
|
|
40
40
|
import { StyledView } from '../styles/styled-primitives';
|
|
41
|
-
import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill } from '../styles/surface-levels';
|
|
41
|
+
import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill, useSurfaceLevelValue } from '../styles/surface-levels';
|
|
42
42
|
import {
|
|
43
43
|
ContentPanelNestingContext,
|
|
44
44
|
useContentPanelNestingGuard,
|
|
@@ -80,6 +80,7 @@ const RESPONSIVE_CONTENT: Record<ContentPanelFramedBreakpoint, string> = {
|
|
|
80
80
|
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
81
81
|
children,
|
|
82
82
|
errorBoundary = false,
|
|
83
|
+
appearance = 'solid',
|
|
83
84
|
framed,
|
|
84
85
|
framedFrom = 768,
|
|
85
86
|
fill = false,
|
|
@@ -100,8 +101,10 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
100
101
|
const defaultFill = rawFill;
|
|
101
102
|
const { width } = useWindowDimensions();
|
|
102
103
|
const parentFill = useOptionalSurfaceFill();
|
|
103
|
-
const
|
|
104
|
-
const
|
|
104
|
+
const parentLevel = useSurfaceLevelValue();
|
|
105
|
+
const isPlain = appearance === 'plain';
|
|
106
|
+
const paintsSurface = !isPlain && Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom) && chrome !== 'none';
|
|
107
|
+
const publishedFill = isPlain ? parentFill : rawFill && paintsSurface ? resolveSurfaceMaterial({ fill: rawFill, parentFill: parentFill! }).publishedFill : rawFill;
|
|
105
108
|
const radius = StyleSheet.flatten(surfaceStyle)?.borderRadius ?? panelShape.radius;
|
|
106
109
|
const isFramed = framed ?? width >= framedFrom;
|
|
107
110
|
const geometry = { ...resolveShapeStyle({ curve: panelShape.curve }), borderRadius: isFramed ? radius : 0 };
|
|
@@ -113,18 +116,18 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
113
116
|
// (the breakpoint bundle lives whole in `RESPONSIVE_SURFACE`; `framed ===
|
|
114
117
|
// true`/`false` ignore `framedFrom` for fixed always/never framing).
|
|
115
118
|
const surfaceBase =
|
|
116
|
-
chrome === 'none'
|
|
119
|
+
isPlain || chrome === 'none'
|
|
117
120
|
? 'flex-1'
|
|
118
121
|
: framed === undefined
|
|
119
122
|
? `flex-1 ${RESPONSIVE_SURFACE[framedFrom]}`
|
|
120
123
|
: framed
|
|
121
124
|
? 'flex-1 border border-border'
|
|
122
125
|
: 'flex-1';
|
|
123
|
-
const surfaceClass = [surfaceBase, fill ? 'min-h-0' : '', surfaceClassName ?? (paintsSurface ? 'bg-transparent' : 'bg-card')].filter(Boolean).join(' ');
|
|
126
|
+
const surfaceClass = [surfaceBase, fill ? 'min-h-0' : '', surfaceClassName ?? (isPlain || paintsSurface ? 'bg-transparent' : 'bg-card')].filter(Boolean).join(' ');
|
|
124
127
|
// Native has no overlays: the surface itself carries the edge. `none` drops
|
|
125
128
|
// both, `border` keeps the class-drawn hairline, `elevated` adds the lift.
|
|
126
129
|
const chromeStyle =
|
|
127
|
-
!isFramed || chrome === 'none' || chrome === 'border'
|
|
130
|
+
!isFramed || isPlain || chrome === 'none' || chrome === 'border'
|
|
128
131
|
? null
|
|
129
132
|
: shadow || panelChrome
|
|
130
133
|
? { boxShadow: shadow ?? panelChrome?.shadow }
|
|
@@ -149,14 +152,14 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
149
152
|
geometry,
|
|
150
153
|
chromeStyle,
|
|
151
154
|
surfaceStyle,
|
|
152
|
-
paintsSurface ? { backgroundColor: 'transparent' } : null,
|
|
155
|
+
isPlain || paintsSurface ? { backgroundColor: 'transparent' } : null,
|
|
153
156
|
]}
|
|
154
157
|
>
|
|
155
158
|
{paintsSurface ? <SurfacePaint fill={defaultFill!} radius={radius} shape={{ curve: panelShape.curve }} /> : null}
|
|
156
159
|
<StyledView testID="content-panel-content" className={contentClass} style={[geometry, contentStyle]}>
|
|
157
|
-
{/*
|
|
158
|
-
|
|
159
|
-
<SurfaceLevelProvider level={1} fill={publishedFill}>
|
|
160
|
+
{/* Solid panels publish their painted surface. Plain panels preserve
|
|
161
|
+
the enclosing surface so descendant scrims follow its real fill. */}
|
|
162
|
+
<SurfaceLevelProvider level={isPlain ? parentLevel : 1} fill={publishedFill}>
|
|
160
163
|
{errorBoundary === false ? children : <PanelErrorBoundary {...(errorBoundary === true ? {} : errorBoundary)}>{children}</PanelErrorBoundary>}
|
|
161
164
|
</SurfaceLevelProvider>
|
|
162
165
|
</StyledView>
|
|
@@ -97,7 +97,7 @@ import { surfaceStyle as resolveShapeStyle } from '../shapes/surface-style';
|
|
|
97
97
|
import { StyledView } from '../styles/styled-primitives';
|
|
98
98
|
|
|
99
99
|
import { useOptionalPanelChrome, usePanelShape } from '../styles/panel-chrome';
|
|
100
|
-
import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill } from '../styles/surface-levels';
|
|
100
|
+
import { SurfaceLevelProvider, surfaceFillVars, useOptionalSurfaceFill, useSurfaceLevelValue } from '../styles/surface-levels';
|
|
101
101
|
import { useTheme } from '../theme/use-theme';
|
|
102
102
|
import {
|
|
103
103
|
ContentPanelNestingContext,
|
|
@@ -173,6 +173,7 @@ const PANEL_INSET_CSS = `
|
|
|
173
173
|
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
174
174
|
children,
|
|
175
175
|
errorBoundary = false,
|
|
176
|
+
appearance = 'solid',
|
|
176
177
|
framed,
|
|
177
178
|
framedFrom = 768,
|
|
178
179
|
fill = false,
|
|
@@ -211,8 +212,10 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
211
212
|
const defaultFill = rawFill;
|
|
212
213
|
const { width } = useWindowDimensions();
|
|
213
214
|
const parentFill = useOptionalSurfaceFill();
|
|
214
|
-
const
|
|
215
|
-
const
|
|
215
|
+
const parentLevel = useSurfaceLevelValue();
|
|
216
|
+
const isPlain = appearance === 'plain';
|
|
217
|
+
const paintsSurface = !isPlain && Boolean(defaultFill) && parentFill !== undefined && (framed ?? width >= framedFrom) && chrome !== 'none';
|
|
218
|
+
const publishedFill = isPlain ? parentFill : rawFill && paintsSurface ? resolveSurfaceMaterial({ fill: rawFill, parentFill: parentFill! }).publishedFill : rawFill;
|
|
216
219
|
const materialStyle = useSurfaceMaterial('[data-bloom-panel-material]', 'bloom-content-panel-material', defaultFill ?? 'transparent');
|
|
217
220
|
|
|
218
221
|
// Tri-state: `undefined` → responsive (md:-gated), `true` → always framed,
|
|
@@ -257,7 +260,7 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
257
260
|
const gridStackClass = boundToPanel
|
|
258
261
|
? 'web:grid web:[grid-template-columns:minmax(0,1fr)] web:[grid-template-rows:minmax(0,1fr)]'
|
|
259
262
|
: '';
|
|
260
|
-
const surfaceClass = [surfaceBase, fillSurface, gridStackClass, surfaceClassName ?? (paintsSurface ? 'bg-transparent' : 'bg-card')].filter(Boolean).join(' ');
|
|
263
|
+
const surfaceClass = [surfaceBase, fillSurface, gridStackClass, surfaceClassName ?? (isPlain || paintsSurface ? 'bg-transparent' : 'bg-card')].filter(Boolean).join(' ');
|
|
261
264
|
const contentClass = [contentBase, fillContent, boundToPanel ? 'web:[grid-area:1/1]' : '', contentClassName]
|
|
262
265
|
.filter(Boolean)
|
|
263
266
|
.join(' ');
|
|
@@ -313,6 +316,7 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
313
316
|
framed === true ? { borderRadius: radius } : framed === false ? { borderRadius: 0 } : null,
|
|
314
317
|
surfaceStyle,
|
|
315
318
|
paintsSurface ? materialStyle : null,
|
|
319
|
+
isPlain ? { backgroundColor: 'transparent' } : null,
|
|
316
320
|
]}
|
|
317
321
|
>
|
|
318
322
|
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. Not
|
|
@@ -331,12 +335,12 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
331
335
|
? `web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
|
|
332
336
|
: 'web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-[var(--bloom-panel-radius)] web:[margin-bottom:calc(-100dvh+16px)]'
|
|
333
337
|
}
|
|
334
|
-
style={{ ...panelStyle, ...insetStyle, boxShadow: `0 0 0 ${maskSpread}px ${maskColor ?? colors.background}` }}
|
|
338
|
+
style={{ ...panelStyle, ...insetStyle, boxShadow: `0 0 0 ${maskSpread}px ${maskColor ?? (isPlain ? parentFill : undefined) ?? colors.background}` }}
|
|
335
339
|
/>
|
|
336
340
|
)}
|
|
337
341
|
{/* (2) Border-frame overlay — one continuous rounded border, above all.
|
|
338
342
|
Same visibility gating as the bleed-mask. */}
|
|
339
|
-
{showOverlays && showStickyFrame !== false && chrome !== 'none' && (
|
|
343
|
+
{showOverlays && showStickyFrame !== false && !isPlain && chrome !== 'none' && (
|
|
340
344
|
<StyledView
|
|
341
345
|
key="border-frame"
|
|
342
346
|
testID="content-panel-border-frame"
|
|
@@ -361,9 +365,9 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
361
365
|
scroll/virtualizer + refetch on a breakpoint cross). Clipped to the
|
|
362
366
|
rounded panel shape on web when framed. */}
|
|
363
367
|
<StyledView key="content" testID="content-panel-content" className={contentClass} style={[curveStyle, framed === true ? { borderRadius: radius } : framed === false ? { borderRadius: 0 } : null, contentStyle]}>
|
|
364
|
-
{/*
|
|
365
|
-
|
|
366
|
-
<SurfaceLevelProvider level={1} fill={publishedFill}>
|
|
368
|
+
{/* Solid panels publish their painted surface. Plain panels preserve
|
|
369
|
+
the enclosing surface so descendant scrims follow its real fill. */}
|
|
370
|
+
<SurfaceLevelProvider level={isPlain ? parentLevel : 1} fill={publishedFill}>
|
|
367
371
|
{errorBoundary === false ? children : <PanelErrorBoundary {...(errorBoundary === true ? {} : errorBoundary)}>{children}</PanelErrorBoundary>}
|
|
368
372
|
</SurfaceLevelProvider>
|
|
369
373
|
</StyledView>
|
|
@@ -18,6 +18,13 @@ export type ContentPanelChrome = 'elevated' | 'border' | 'none';
|
|
|
18
18
|
|
|
19
19
|
export interface ContentPanelProps {
|
|
20
20
|
children: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* `solid` (default) paints a panel surface. `plain` keeps layout, framing and
|
|
23
|
+
* clipping while leaving the enclosing surface visible, without material,
|
|
24
|
+
* border or shadow. Descendants inherit the parent's surface level and fill.
|
|
25
|
+
* In plain mode surface background overrides and surfaceColor are ignored.
|
|
26
|
+
*/
|
|
27
|
+
appearance?: 'solid' | 'plain';
|
|
21
28
|
/** Opt in to local themed error recovery. Split panes already provide their own boundaries. */
|
|
22
29
|
errorBoundary?: boolean | PanelErrorBoundaryOptions;
|
|
23
30
|
/**
|
package/src/index.ts
CHANGED
|
@@ -169,13 +169,16 @@ export {
|
|
|
169
169
|
useClaimTopEdge,
|
|
170
170
|
useTopEdgeInset,
|
|
171
171
|
ScrollOffsetProvider,
|
|
172
|
+
ScrollMetricsProvider,
|
|
173
|
+
useScrollMetricsBinding,
|
|
174
|
+
ScrollArea,
|
|
172
175
|
useScrollOffset,
|
|
173
176
|
ScreenScope,
|
|
174
177
|
HeaderDockProvider,
|
|
175
178
|
useHeaderDockInset,
|
|
176
179
|
StickySection,
|
|
177
180
|
} from './layout';
|
|
178
|
-
export type { StickySectionProps } from './layout';
|
|
181
|
+
export type { StickySectionProps, ScrollMetricsBindingOptions, ScrollAreaProps } from './layout';
|
|
179
182
|
export { Fab } from './fab';
|
|
180
183
|
export type { FabProps, FabSize } from './fab';
|
|
181
184
|
export { FrostedIconButton } from './frosted-icon-button';
|
package/src/index.web.ts
CHANGED
|
@@ -174,13 +174,16 @@ export {
|
|
|
174
174
|
useClaimTopEdge,
|
|
175
175
|
useTopEdgeInset,
|
|
176
176
|
ScrollOffsetProvider,
|
|
177
|
+
ScrollMetricsProvider,
|
|
178
|
+
useScrollMetricsBinding,
|
|
179
|
+
ScrollArea,
|
|
177
180
|
useScrollOffset,
|
|
178
181
|
ScreenScope,
|
|
179
182
|
HeaderDockProvider,
|
|
180
183
|
useHeaderDockInset,
|
|
181
184
|
StickySection,
|
|
182
185
|
} from './layout';
|
|
183
|
-
export type { StickySectionProps } from './layout';
|
|
186
|
+
export type { StickySectionProps, ScrollMetricsBindingOptions, ScrollAreaProps } from './layout';
|
|
184
187
|
export { Fab } from './fab/index.web';
|
|
185
188
|
export type { FabProps, FabSize } from './fab/index.web';
|
|
186
189
|
export { FrostedIconButton } from './frosted-icon-button/index.web';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { forwardRef, type ComponentType, type Ref } from 'react';
|
|
2
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
3
|
+
import { styled } from 'react-native-css';
|
|
4
|
+
import { useScrollMetricsBinding } from './use-scroll-metrics-binding';
|
|
5
|
+
|
|
6
|
+
export interface ScrollAreaProps extends ScrollViewProps {
|
|
7
|
+
/** Utilities for the scrolling viewport, resolved by Bloom's own interop. */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Utilities for the inner content container. */
|
|
10
|
+
contentContainerClassName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Narrow only the styled mapping input to avoid ScrollViewProps' recursive
|
|
14
|
+
// dot-path union. The public component retains the complete ScrollView contract.
|
|
15
|
+
const ScrollAreaBase: ComponentType<Pick<ScrollViewProps, 'style' | 'contentContainerStyle'>> = ScrollView;
|
|
16
|
+
const StyledScrollView: ComponentType<ScrollAreaProps & { ref?: Ref<ScrollView> }> = styled(ScrollAreaBase, {
|
|
17
|
+
className: 'style',
|
|
18
|
+
contentContainerClassName: 'contentContainerStyle',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** A ScrollView that publishes its measurements to surrounding page chrome. */
|
|
22
|
+
export const ScrollArea = forwardRef<ScrollView, ScrollAreaProps>(function ScrollArea({ onScroll, onLayout, onContentSizeChange, scrollEventThrottle, ...props }, ref) {
|
|
23
|
+
const binding = useScrollMetricsBinding({ onScroll, onLayout, onContentSizeChange });
|
|
24
|
+
return <StyledScrollView {...props} {...binding} scrollEventThrottle={scrollEventThrottle ?? binding.scrollEventThrottle} ref={ref} />;
|
|
25
|
+
});
|
package/src/layout/index.ts
CHANGED
|
@@ -18,3 +18,9 @@ export { ScreenScope } from './screen-scope';
|
|
|
18
18
|
export { HeaderDockProvider, useHeaderDockInset } from './header-dock';
|
|
19
19
|
export { StickySection } from './StickySection';
|
|
20
20
|
export type { StickySectionProps } from './StickySection';
|
|
21
|
+
|
|
22
|
+
export { ScrollMetricsProvider } from './scroll-metrics';
|
|
23
|
+
export { useScrollMetricsBinding } from './use-scroll-metrics-binding';
|
|
24
|
+
export type { ScrollMetricsBindingOptions } from './use-scroll-metrics-binding';
|
|
25
|
+
export { ScrollArea } from './ScrollArea';
|
|
26
|
+
export type { ScrollAreaProps } from './ScrollArea';
|
|
@@ -39,6 +39,7 @@ import { ScreenContext, ScreenNavigationContext } from '../screen/context';
|
|
|
39
39
|
import type { PropsWithChildren } from 'react';
|
|
40
40
|
|
|
41
41
|
import { BottomEdgeProvider } from './bottom-edge';
|
|
42
|
+
import { ScrollMetricsContext } from './scroll-metrics';
|
|
42
43
|
import { ScrollOffsetProvider } from './scroll-offset';
|
|
43
44
|
import { TopEdgeProvider } from './top-edge';
|
|
44
45
|
|
|
@@ -49,7 +50,7 @@ export function ScreenScope({ children }: PropsWithChildren) {
|
|
|
49
50
|
<TopEdgeProvider>
|
|
50
51
|
<BottomEdgeProvider>
|
|
51
52
|
{/* null means no scroller has claimed this scope yet. */}
|
|
52
|
-
<ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider>
|
|
53
|
+
<ScrollMetricsContext.Provider value={null}><ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider></ScrollMetricsContext.Provider>
|
|
53
54
|
</BottomEdgeProvider>
|
|
54
55
|
</TopEdgeProvider>
|
|
55
56
|
</ScreenNavigationContext.Provider>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
|
|
2
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { ScrollOffsetProvider } from './scroll-offset';
|
|
4
|
+
|
|
5
|
+
export interface ScrollMetrics {
|
|
6
|
+
scrollY: SharedValue<number>;
|
|
7
|
+
contentHeight: SharedValue<number>;
|
|
8
|
+
viewportHeight: SharedValue<number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Internal scope boundary; overlay screens must not follow the page beneath them. */
|
|
12
|
+
export const ScrollMetricsContext = createContext<ScrollMetrics | null>(null);
|
|
13
|
+
|
|
14
|
+
/** Pair one scroller with its surrounding header/footer without adding a layout box. */
|
|
15
|
+
export function ScrollMetricsProvider({ children }: PropsWithChildren) {
|
|
16
|
+
const scrollY = useSharedValue(0);
|
|
17
|
+
const contentHeight = useSharedValue(-1);
|
|
18
|
+
const viewportHeight = useSharedValue(0);
|
|
19
|
+
const value = useMemo(() => ({ scrollY, contentHeight, viewportHeight }), [scrollY, contentHeight, viewportHeight]);
|
|
20
|
+
return <ScrollMetricsContext.Provider value={value}>
|
|
21
|
+
<ScrollOffsetProvider value={scrollY}>{children}</ScrollOffsetProvider>
|
|
22
|
+
</ScrollMetricsContext.Provider>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useScrollMetricsValue(): ScrollMetrics | null {
|
|
26
|
+
return useContext(ScrollMetricsContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Unmeasured and non-scrollable content has no hidden bottom edge. */
|
|
30
|
+
export function remainingScrollDistance(metrics: ScrollMetrics | null): number {
|
|
31
|
+
'worklet';
|
|
32
|
+
if (!metrics || metrics.viewportHeight.value <= 0 || metrics.contentHeight.value < 0) return 0;
|
|
33
|
+
const max = Math.max(0, metrics.contentHeight.value - metrics.viewportHeight.value);
|
|
34
|
+
return Math.max(0, max - Math.max(0, metrics.scrollY.value));
|
|
35
|
+
}
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* one scroll container has moved. The container is the screen's, and the chrome
|
|
9
9
|
* is usually not inside it, so the number has to travel.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Prefer ScrollMetricsProvider with ScrollArea when the footer also needs to
|
|
12
|
+
* know the remaining content. This lower-level offset-only provider remains
|
|
13
|
+
* useful for an existing animated scroll owner.
|
|
14
|
+
*
|
|
15
|
+
* Originally it travelled as a prop, and the default when the prop was omitted
|
|
12
16
|
* was `window.scrollY` on web and "nothing moves" on native. That default is
|
|
13
17
|
* right for exactly one layout — a web page whose document IS the scroller —
|
|
14
18
|
* and silently wrong for the two that a Bloom app usually has: a desktop panel
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { ScrollViewProps } from 'react-native';
|
|
3
|
+
import { useScrollMetricsValue } from './scroll-metrics';
|
|
4
|
+
|
|
5
|
+
/** Events accepted by ordinary RN ScrollView, FlatList and SectionList. */
|
|
6
|
+
export type ScrollMetricsBindingOptions = Pick<ScrollViewProps, 'onScroll' | 'onLayout' | 'onContentSizeChange'>;
|
|
7
|
+
|
|
8
|
+
/** Bind one scrolling owner to the nearest ScrollMetricsProvider. No React updates per frame. */
|
|
9
|
+
export function useScrollMetricsBinding({ onScroll, onLayout, onContentSizeChange }: ScrollMetricsBindingOptions = {}) {
|
|
10
|
+
const metrics = useScrollMetricsValue();
|
|
11
|
+
const handleScroll = useCallback<NonNullable<ScrollViewProps['onScroll']>>(event => {
|
|
12
|
+
if (metrics) {
|
|
13
|
+
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
14
|
+
if (Number.isFinite(contentSize.height)) metrics.contentHeight.value = Math.max(0, contentSize.height);
|
|
15
|
+
if (Number.isFinite(layoutMeasurement.height)) metrics.viewportHeight.value = Math.max(0, layoutMeasurement.height);
|
|
16
|
+
const max = Math.max(0, metrics.contentHeight.value - metrics.viewportHeight.value);
|
|
17
|
+
if (Number.isFinite(contentOffset.y)) metrics.scrollY.value = Math.min(max, Math.max(0, contentOffset.y));
|
|
18
|
+
}
|
|
19
|
+
onScroll?.(event);
|
|
20
|
+
}, [metrics, onScroll]);
|
|
21
|
+
const handleLayout = useCallback<NonNullable<ScrollViewProps['onLayout']>>(event => {
|
|
22
|
+
const height = event.nativeEvent.layout.height;
|
|
23
|
+
if (metrics && Number.isFinite(height)) {
|
|
24
|
+
metrics.viewportHeight.value = Math.max(0, height);
|
|
25
|
+
if (metrics.contentHeight.value >= 0) metrics.scrollY.value = Math.min(metrics.scrollY.value, Math.max(0, metrics.contentHeight.value - height));
|
|
26
|
+
}
|
|
27
|
+
onLayout?.(event);
|
|
28
|
+
}, [metrics, onLayout]);
|
|
29
|
+
const handleContentSizeChange = useCallback<NonNullable<ScrollViewProps['onContentSizeChange']>>((width, height) => {
|
|
30
|
+
if (metrics && Number.isFinite(height)) {
|
|
31
|
+
metrics.contentHeight.value = Math.max(0, height);
|
|
32
|
+
metrics.scrollY.value = Math.min(metrics.scrollY.value, Math.max(0, height - metrics.viewportHeight.value));
|
|
33
|
+
}
|
|
34
|
+
onContentSizeChange?.(width, height);
|
|
35
|
+
}, [metrics, onContentSizeChange]);
|
|
36
|
+
return { onScroll: handleScroll, onLayout: handleLayout, onContentSizeChange: handleContentSizeChange, scrollEventThrottle: 16 };
|
|
37
|
+
}
|
|
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { Button } from '../button';
|
|
5
|
-
import { Divider } from '../divider';
|
|
6
5
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
7
6
|
import { MailLabelChips, MailStar } from '../mail-list/parts';
|
|
8
7
|
import { mailStrings, resolveMailPaint, visibleLabels } from '../mail-list/shared';
|
|
@@ -145,7 +144,6 @@ export function MailThread({
|
|
|
145
144
|
/>
|
|
146
145
|
</View>
|
|
147
146
|
) : null}
|
|
148
|
-
<Divider />
|
|
149
147
|
<View testID={testID ? `${testID}-messages` : undefined} style={{ paddingTop: space.xs, paddingBottom: space.xs, gap: messageGap }}>
|
|
150
148
|
{entries.map((entry) =>
|
|
151
149
|
entry.kind === 'message' ? (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
|
-
import {
|
|
2
|
+
import { SURFACE_SHAPES } from '../design-tokens/shapes';
|
|
3
3
|
|
|
4
4
|
import { MAIL_THREAD_MESSAGES } from './messages';
|
|
5
5
|
import type { MailThreadMessage, MailAddress, MailThreadStrings } from './types';
|
|
@@ -44,8 +44,8 @@ export const MAIL_THREAD_GEOMETRY = {
|
|
|
44
44
|
glyph: 16,
|
|
45
45
|
/** The star and the overflow trigger. A touch target, so 44. */
|
|
46
46
|
action: 44,
|
|
47
|
-
/**
|
|
48
|
-
radius:
|
|
47
|
+
/** Default radius; rendered messages inherit BloomScope.panelRadius via Card. */
|
|
48
|
+
radius: SURFACE_SHAPES.sidebar.radius,
|
|
49
49
|
} as const;
|
|
50
50
|
|
|
51
51
|
/**
|
package/src/note-card/shared.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface NoteCardGeometry {
|
|
|
40
40
|
padding: number;
|
|
41
41
|
gap: number;
|
|
42
42
|
/** A rung of the `RADIUS` scale — `Card` takes the rung, the focus ring takes its px. */
|
|
43
|
-
radius: CardRadius
|
|
43
|
+
radius: Exclude<CardRadius, 'panel'>;
|
|
44
44
|
excerptLines: number;
|
|
45
45
|
titleLines: number;
|
|
46
46
|
maxTags: number;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { memo, useContext, useEffect, useId, useState } from 'react';
|
|
2
2
|
import { Platform, StyleSheet, View, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
|
4
|
+
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
5
|
+
import { remainingScrollDistance, useScrollMetricsValue } from '../layout/scroll-metrics';
|
|
4
6
|
import { BloomScope } from '../appearance';
|
|
5
7
|
import { useBottomEdgeInset } from '../layout/bottom-edge';
|
|
6
8
|
import { EdgeScrim, SCRIM_TAIL_RATIO } from '../page-header/EdgeScrim';
|
|
@@ -15,9 +17,14 @@ const BAR_HEIGHT = 56;
|
|
|
15
17
|
|
|
16
18
|
/** Page-local controls floating over a bounded scroller, above app navigation. */
|
|
17
19
|
function PageFooterComponent({
|
|
18
|
-
children, actions, bottomInset, safeArea, scrim = 'always', scrimColor, style, testID,
|
|
20
|
+
children, actions, bottomInset, safeArea, scrim = 'always', scrimColor, scrollThreshold = 20, style, testID,
|
|
19
21
|
}: PageFooterProps) {
|
|
20
22
|
const theme = useTheme();
|
|
23
|
+
const metrics = useScrollMetricsValue();
|
|
24
|
+
const threshold = Math.max(1, Number.isFinite(scrollThreshold) ? scrollThreshold : 20);
|
|
25
|
+
const scrimStyle = useAnimatedStyle(() => ({
|
|
26
|
+
opacity: scrim === 'auto' ? Math.min(1, remainingScrollDistance(metrics) / threshold) : 1,
|
|
27
|
+
}), [metrics, scrim, threshold]);
|
|
21
28
|
const fill = useSurfaceFill() ?? theme.colors.background;
|
|
22
29
|
const occupied = useBottomEdgeInset();
|
|
23
30
|
const rawBottom = bottomInset ?? occupied;
|
|
@@ -51,13 +58,13 @@ function PageFooterComponent({
|
|
|
51
58
|
style={[styles.container, { bottom, paddingBottom: padBottom }, style]}
|
|
52
59
|
>
|
|
53
60
|
{scrim !== 'none' ? (
|
|
54
|
-
<View
|
|
61
|
+
<Animated.View
|
|
55
62
|
pointerEvents="none"
|
|
56
|
-
style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }]}
|
|
63
|
+
style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }, scrimStyle]}
|
|
57
64
|
testID={testID ? `${testID}-scrim` : undefined}
|
|
58
65
|
>
|
|
59
66
|
<EdgeScrim edge="bottom" color={scrimColor ?? fill} testID={testID ? `${testID}-scrim-gradient` : undefined} />
|
|
60
|
-
</View>
|
|
67
|
+
</Animated.View>
|
|
61
68
|
) : null}
|
|
62
69
|
<View
|
|
63
70
|
onLayout={onLayout}
|
package/src/page-footer/types.ts
CHANGED
|
@@ -17,8 +17,14 @@ export interface PageFooterProps {
|
|
|
17
17
|
* true on native, false on web. Disable when the containing shell owns it.
|
|
18
18
|
*/
|
|
19
19
|
safeArea?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Mirrored PageHeader gradient. Default always preserves existing behavior.
|
|
22
|
+
* auto follows ScrollMetricsProvider: hidden at the bottom or for short /
|
|
23
|
+
* unmeasured content, fading in over scrollThreshold of remaining content.
|
|
24
|
+
*/
|
|
25
|
+
scrim?: 'auto' | 'always' | 'none';
|
|
26
|
+
/** Distance from the bottom over which the automatic scrim fades. Default 20. */
|
|
27
|
+
scrollThreshold?: number;
|
|
22
28
|
/** Opaque gradient color; defaults to the containing surface's fill. */
|
|
23
29
|
scrimColor?: string;
|
|
24
30
|
style?: StyleProp<ViewStyle>;
|
|
@@ -24,6 +24,7 @@ import { BUTTON_SHADOW } from '../button/shared';
|
|
|
24
24
|
import { BloomScope } from "../appearance";
|
|
25
25
|
import { GlassIsland } from '../glass';
|
|
26
26
|
import { RiArrowLeftLine } from '../icons/remix/RiArrowLeftLine';
|
|
27
|
+
import { useScrollMetricsValue } from '../layout/scroll-metrics';
|
|
27
28
|
import { useClaimTopEdge, useScrollOffset } from '../layout';
|
|
28
29
|
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
29
30
|
import { WEB_POSITION_STICKY, WEB_SURFACE_STICKY_TOP, type WebCssStyle } from '../styles/web-view-style';
|
|
@@ -186,14 +187,15 @@ function PageHeaderComponent({
|
|
|
186
187
|
|
|
187
188
|
// ── The scroll owner ──────────────────────────────────────────────────────
|
|
188
189
|
//
|
|
189
|
-
//
|
|
190
|
+
// Explicit offset, dock, local metrics, Screen, legacy offset, then the web document.
|
|
190
191
|
// The context step is the one that matters for a Bloom composition: without
|
|
191
192
|
// it a header inside a scrolling panel followed `window.scrollY`, which on a
|
|
192
193
|
// desktop shell never moves, so the header looked deliberately inert.
|
|
193
194
|
const screen = useScreenContext();
|
|
194
195
|
const contextScrollY = useScrollOffset();
|
|
196
|
+
const metrics = useScrollMetricsValue();
|
|
195
197
|
const internalScrollY = useSharedValue(0);
|
|
196
|
-
const scrollY = externalScrollY ?? dock?.scrollY ?? screen?.scrollY ?? contextScrollY ?? internalScrollY;
|
|
198
|
+
const scrollY = externalScrollY ?? dock?.scrollY ?? metrics?.scrollY ?? screen?.scrollY ?? contextScrollY ?? internalScrollY;
|
|
197
199
|
const followsWindow = !externalScrollY && !dock && !screen && !contextScrollY;
|
|
198
200
|
|
|
199
201
|
useEffect(() => {
|
package/src/page-header/types.ts
CHANGED
|
@@ -142,10 +142,9 @@ export interface PageHeaderProps {
|
|
|
142
142
|
/**
|
|
143
143
|
* The scroll offset driving the scrim, the separator and the shadow.
|
|
144
144
|
*
|
|
145
|
-
* Resolution order: this prop,
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* On native with none of the three, the header stays at rest.
|
|
145
|
+
* Resolution order: this prop, HeaderDockProvider, ScrollMetricsProvider,
|
|
146
|
+
* the enclosing Screen, then ScrollOffsetProvider. On web with no local
|
|
147
|
+
* owner it follows window.scrollY; on native it stays at rest.
|
|
149
148
|
*/
|
|
150
149
|
scrollY?: SharedValue<number>;
|
|
151
150
|
/** The offset (px) over which the scrim, separator and shadow fade in. Default 20. */
|