@oxyhq/bloom 0.35.6 → 0.35.8
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/content-panel/index.js +50 -6
- package/lib/commonjs/content-panel/index.js.map +1 -1
- package/lib/commonjs/content-panel/index.web.js +93 -12
- package/lib/commonjs/content-panel/index.web.js.map +1 -1
- package/lib/module/content-panel/index.js +50 -6
- package/lib/module/content-panel/index.js.map +1 -1
- package/lib/module/content-panel/index.web.js +93 -12
- package/lib/module/content-panel/index.web.js.map +1 -1
- package/lib/typescript/commonjs/content-panel/index.d.ts +39 -8
- package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/index.web.d.ts +49 -6
- package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/index.d.ts +39 -8
- package/lib/typescript/module/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/index.web.d.ts +49 -6
- package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/ContentPanelNesting.test.tsx +74 -0
- package/src/content-panel/index.tsx +69 -14
- package/src/content-panel/index.web.tsx +130 -22
|
@@ -23,16 +23,33 @@
|
|
|
23
23
|
* content rather than displacing it). They render BEFORE the content wrapper so
|
|
24
24
|
* z-index — not DOM order — decides layering.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Framing is tri-state via the `framed` prop:
|
|
27
|
+
*
|
|
28
|
+
* - `undefined` (DEFAULT) → RESPONSIVE, driven purely by NativeWind: full-bleed
|
|
29
|
+
* below the `framedFrom` breakpoint, framed at/above it. The breakpoint is
|
|
30
|
+
* configurable per consumer (`framedFrom`, default `768` / Tailwind `md`); the
|
|
31
|
+
* rounding is gated by that breakpoint's `min-*:` variant and both overlays
|
|
32
|
+
* are always rendered but carry the matching `max-*:hidden` literal
|
|
33
|
+
* (`display:none` below the breakpoint), so the breakpoint is decided entirely
|
|
34
|
+
* in CSS — no consumer JS breakpoint hook.
|
|
35
|
+
* - `false` → NEVER framed (full-bleed at every size): a single flat surface,
|
|
36
|
+
* no rounding, no overlays.
|
|
37
|
+
* - `true` → ALWAYS framed (rounded + overlays at every size).
|
|
38
|
+
*
|
|
39
|
+
* The overlays are gated by VISIBILITY (a `max-*:hidden` literal for the chosen
|
|
40
|
+
* breakpoint) rather than by conditional mounting for the responsive case, so
|
|
41
|
+
* crossing the breakpoint never remounts them. The content wrapper likewise
|
|
42
|
+
* keeps a stable `key` so `{children}` reconciles in place across the breakpoint
|
|
43
|
+
* instead of remounting (which would reset feed scroll / the virtualizer +
|
|
44
|
+
* refetch).
|
|
28
45
|
*
|
|
29
46
|
* Native bundlers use `./index.tsx` (a plain rounded surface); web bundlers
|
|
30
47
|
* select this file via the `"browser"` export condition in `package.json`.
|
|
31
48
|
*
|
|
32
49
|
* Styling is NativeWind-className-first; the literal class strings below MUST
|
|
33
50
|
* stay literal so a consumer's Tailwind content-scan over `lib/**` picks them up
|
|
34
|
-
* (no dynamic concatenation of the arbitrary `web:[…]` / `rounded-radius-28`
|
|
35
|
-
* parts).
|
|
51
|
+
* (no dynamic concatenation of the arbitrary `web:[…]` / `rounded-radius-28` /
|
|
52
|
+
* `md:` parts — whole class strings are selected per mode instead).
|
|
36
53
|
*/
|
|
37
54
|
import React, { memo } from 'react';
|
|
38
55
|
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
@@ -50,10 +67,37 @@ export const PANEL_TOP_INSET = 8;
|
|
|
50
67
|
/** Bottom sticky inset (px) of the framed panel chrome. */
|
|
51
68
|
export const PANEL_BOTTOM_INSET = 8;
|
|
52
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Viewport width (px) at which a RESPONSIVE `ContentPanel` (`framed` undefined)
|
|
72
|
+
* switches from full-bleed to framed. Each value maps to a PRE-SHIPPED literal
|
|
73
|
+
* Tailwind class bundle — `768` (default) uses the named `md:`/`max-md:` screens
|
|
74
|
+
* (byte-identical to prior behavior), `640` uses `sm:`/`max-sm:`, `1024` uses
|
|
75
|
+
* `lg:`/`max-lg:`, and `500` uses the arbitrary `min-[500px]:`/`max-[500px]:`
|
|
76
|
+
* variants (there is no named screen at 500px). The breakpoint tokens are never
|
|
77
|
+
* built at runtime, so a consumer's Tailwind content-scan over `lib/**` resolves
|
|
78
|
+
* them verbatim.
|
|
79
|
+
*/
|
|
80
|
+
export type ContentPanelFramedBreakpoint = 500 | 640 | 768 | 1024;
|
|
81
|
+
|
|
53
82
|
export interface ContentPanelProps {
|
|
54
83
|
children: React.ReactNode;
|
|
55
|
-
/**
|
|
56
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Framing mode. Tri-state, resolved purely with NativeWind — no consumer
|
|
86
|
+
* breakpoint hook needed:
|
|
87
|
+
* - `undefined` (DEFAULT) → responsive: full-bleed below the `framedFrom`
|
|
88
|
+
* breakpoint, framed at/above it (rounded corners + sticky bleed-mask/border
|
|
89
|
+
* overlays).
|
|
90
|
+
* - `false` → never framed (full-bleed at every size).
|
|
91
|
+
* - `true` → always framed (rounded + overlays at every size).
|
|
92
|
+
*/
|
|
93
|
+
framed?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Viewport width at which the RESPONSIVE panel switches from full-bleed to
|
|
96
|
+
* framed. Only meaningful when `framed` is `undefined` (responsive) — ignored
|
|
97
|
+
* when `framed` is `true` (always framed) or `false` (never framed). Defaults
|
|
98
|
+
* to `768` (Tailwind `md`), reproducing the prior fixed behavior exactly.
|
|
99
|
+
*/
|
|
100
|
+
framedFrom?: ContentPanelFramedBreakpoint;
|
|
57
101
|
/** Override the surface background utility (defaults to `bg-card`). */
|
|
58
102
|
surfaceClassName?: string;
|
|
59
103
|
surfaceStyle?: StyleProp<ViewStyle>;
|
|
@@ -73,9 +117,50 @@ export interface ContentPanelProps {
|
|
|
73
117
|
maskColor?: string;
|
|
74
118
|
}
|
|
75
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Per-breakpoint literal Tailwind variant tokens for the WEB responsive mode.
|
|
122
|
+
* Each field is a WHOLE literal class string (never assembled from parts at
|
|
123
|
+
* runtime) so the consumer's content-scan over `lib/**` picks up every
|
|
124
|
+
* arbitrary/`min-*`/`max-*`/`web:` token verbatim. Selecting a bundle by
|
|
125
|
+
* `framedFrom` value is the only runtime decision:
|
|
126
|
+
* - `surface` → framing rounding on the surface at/above the breakpoint.
|
|
127
|
+
* - `content` → the same rounding + a web-only horizontal clip on the
|
|
128
|
+
* inner content wrapper (`web:<bp>:overflow-x-clip`, platform-first-then-
|
|
129
|
+
* breakpoint — matching the consumer convention `web:sm:flex` in `@mercaria/ui`).
|
|
130
|
+
* - `overlayHidden` → `display:none` for the sticky overlays BELOW the breakpoint.
|
|
131
|
+
* `min-[500px]:` (≥500) and `max-[500px]:` (<500) are exactly complementary
|
|
132
|
+
* (meet at 500, no overlap/gap), mirroring the named `md:`/`max-md:` pair.
|
|
133
|
+
*/
|
|
134
|
+
const RESPONSIVE_WEB: Record<
|
|
135
|
+
ContentPanelFramedBreakpoint,
|
|
136
|
+
{ surface: string; content: string; overlayHidden: string }
|
|
137
|
+
> = {
|
|
138
|
+
500: {
|
|
139
|
+
surface: 'min-[500px]:rounded-radius-28',
|
|
140
|
+
content: 'min-[500px]:rounded-radius-28 web:min-[500px]:overflow-x-clip',
|
|
141
|
+
overlayHidden: 'max-[500px]:hidden',
|
|
142
|
+
},
|
|
143
|
+
640: {
|
|
144
|
+
surface: 'sm:rounded-radius-28',
|
|
145
|
+
content: 'sm:rounded-radius-28 web:sm:overflow-x-clip',
|
|
146
|
+
overlayHidden: 'max-sm:hidden',
|
|
147
|
+
},
|
|
148
|
+
768: {
|
|
149
|
+
surface: 'md:rounded-radius-28',
|
|
150
|
+
content: 'md:rounded-radius-28 web:md:overflow-x-clip',
|
|
151
|
+
overlayHidden: 'max-md:hidden',
|
|
152
|
+
},
|
|
153
|
+
1024: {
|
|
154
|
+
surface: 'lg:rounded-radius-28',
|
|
155
|
+
content: 'lg:rounded-radius-28 web:lg:overflow-x-clip',
|
|
156
|
+
overlayHidden: 'max-lg:hidden',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
76
160
|
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
77
161
|
children,
|
|
78
162
|
framed,
|
|
163
|
+
framedFrom = 768,
|
|
79
164
|
surfaceClassName,
|
|
80
165
|
surfaceStyle,
|
|
81
166
|
contentClassName,
|
|
@@ -83,17 +168,35 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
83
168
|
showStickyFrame,
|
|
84
169
|
maskColor,
|
|
85
170
|
}) => {
|
|
86
|
-
// Dev-only invariant — must run unconditionally before
|
|
87
|
-
//
|
|
171
|
+
// Dev-only invariant — must run unconditionally (before deriving any
|
|
172
|
+
// mode-specific branch) so the hook order stays stable (rules of hooks).
|
|
88
173
|
useContentPanelNestingGuard();
|
|
89
174
|
const { colors } = useTheme();
|
|
90
175
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
176
|
+
// Tri-state: `undefined` → responsive (md:-gated), `true` → always framed,
|
|
177
|
+
// `false` → never framed (full-bleed).
|
|
178
|
+
const responsive = framed === undefined;
|
|
179
|
+
const showOverlays = framed !== false;
|
|
180
|
+
// Responsive mode selects a pre-shipped literal breakpoint bundle by value;
|
|
181
|
+
// `framed === true`/`false` ignore `framedFrom` (fixed always/never framing).
|
|
182
|
+
const bp = RESPONSIVE_WEB[framedFrom];
|
|
183
|
+
|
|
184
|
+
// Whole literal class strings selected per mode (the Tailwind content-scan
|
|
185
|
+
// over `lib/**` requires each arbitrary/`min-*`/`max-*`/`web:` token stay
|
|
186
|
+
// literal — the breakpoint tokens live whole in `RESPONSIVE_WEB` above and are
|
|
187
|
+
// only spliced in, never assembled from parts).
|
|
188
|
+
const surfaceBase = responsive
|
|
189
|
+
? `flex-1 ${bp.surface}`
|
|
190
|
+
: framed
|
|
191
|
+
? 'flex-1 rounded-radius-28'
|
|
192
|
+
: 'flex-1';
|
|
193
|
+
const contentBase = responsive
|
|
194
|
+
? `flex-1 ${bp.content}`
|
|
195
|
+
: framed
|
|
196
|
+
? 'flex-1 rounded-radius-28 web:overflow-x-clip'
|
|
197
|
+
: 'flex-1';
|
|
198
|
+
const surfaceClass = [surfaceBase, surfaceClassName ?? 'bg-card'].join(' ');
|
|
199
|
+
const contentClass = [contentBase, contentClassName].filter(Boolean).join(' ');
|
|
97
200
|
|
|
98
201
|
return (
|
|
99
202
|
<ContentPanelNestingContext.Provider value={true}>
|
|
@@ -101,26 +204,31 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
101
204
|
{...({ className: surfaceClass } as Record<string, string>)}
|
|
102
205
|
style={surfaceStyle}
|
|
103
206
|
>
|
|
104
|
-
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome.
|
|
105
|
-
|
|
207
|
+
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. Not
|
|
208
|
+
rendered when never-framed; `max-md:hidden` (display:none <md) when
|
|
209
|
+
responsive, so the breakpoint is decided in CSS, not by remounting. */}
|
|
210
|
+
{showOverlays && (
|
|
106
211
|
<View
|
|
107
212
|
key="bleed-mask"
|
|
108
213
|
pointerEvents="none"
|
|
109
214
|
{...({
|
|
110
|
-
className:
|
|
111
|
-
|
|
215
|
+
className: responsive
|
|
216
|
+
? `web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-radius-28 ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)] web:[clip-path:inset(-12px)]`
|
|
217
|
+
: 'web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-radius-28 web:[margin-bottom:calc(-100dvh+16px)] web:[clip-path:inset(-12px)]',
|
|
112
218
|
} as Record<string, string>)}
|
|
113
219
|
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}` }}
|
|
114
220
|
/>
|
|
115
221
|
)}
|
|
116
|
-
{/* (2) Border-frame overlay — one continuous rounded border, above all.
|
|
117
|
-
|
|
222
|
+
{/* (2) Border-frame overlay — one continuous rounded border, above all.
|
|
223
|
+
Same visibility gating as the bleed-mask. */}
|
|
224
|
+
{showOverlays && showStickyFrame !== false && (
|
|
118
225
|
<View
|
|
119
226
|
key="border-frame"
|
|
120
227
|
pointerEvents="none"
|
|
121
228
|
{...({
|
|
122
|
-
className:
|
|
123
|
-
|
|
229
|
+
className: responsive
|
|
230
|
+
? `web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-radius-28 border border-border ${bp.overlayHidden} web:[margin-bottom:calc(-100dvh+16px)]`
|
|
231
|
+
: 'web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-radius-28 border border-border web:[margin-bottom:calc(-100dvh+16px)]',
|
|
124
232
|
} as Record<string, string>)}
|
|
125
233
|
/>
|
|
126
234
|
)}
|