@oxyhq/bloom 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/content-panel/index.js +12 -6
- package/lib/commonjs/content-panel/index.js.map +1 -1
- package/lib/commonjs/content-panel/index.web.js +33 -22
- package/lib/commonjs/content-panel/index.web.js.map +1 -1
- package/lib/commonjs/content-panel/nesting-context.js +39 -0
- package/lib/commonjs/content-panel/nesting-context.js.map +1 -0
- package/lib/module/content-panel/index.js +12 -6
- package/lib/module/content-panel/index.js.map +1 -1
- package/lib/module/content-panel/index.web.js +33 -22
- package/lib/module/content-panel/index.web.js.map +1 -1
- package/lib/module/content-panel/nesting-context.js +35 -0
- package/lib/module/content-panel/nesting-context.js.map +1 -0
- package/lib/typescript/commonjs/content-panel/index.d.ts +5 -0
- package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/index.web.d.ts +9 -0
- package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/nesting-context.d.ts +24 -0
- package/lib/typescript/commonjs/content-panel/nesting-context.d.ts.map +1 -0
- package/lib/typescript/module/content-panel/index.d.ts +5 -0
- package/lib/typescript/module/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/index.web.d.ts +9 -0
- package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/nesting-context.d.ts +24 -0
- package/lib/typescript/module/content-panel/nesting-context.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/ContentPanelNesting.test.tsx +52 -0
- package/src/content-panel/index.tsx +23 -8
- package/src/content-panel/index.web.tsx +50 -29
- package/src/content-panel/nesting-context.ts +35 -0
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.PANEL_TOP_INSET = exports.PANEL_BOTTOM_INSET = exports.GUTTER_MASK_SPREAD = exports.ContentPanel = void 0;
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
|
+
var _nestingContext = require("./nesting-context.js");
|
|
9
10
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
10
11
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
12
|
/**
|
|
@@ -47,15 +48,20 @@ const ContentPanelComponent = ({
|
|
|
47
48
|
contentClassName,
|
|
48
49
|
contentStyle
|
|
49
50
|
}) => {
|
|
51
|
+
// Dev-only invariant — a ContentPanel must never be nested inside another.
|
|
52
|
+
(0, _nestingContext.useContentPanelNestingGuard)();
|
|
50
53
|
const surfaceClass = ['flex-1', 'rounded-radius-28', 'overflow-hidden border border-border', surfaceClassName ?? 'bg-card'].join(' ');
|
|
51
54
|
const contentClass = ['flex-1', contentClassName].filter(Boolean).join(' ');
|
|
52
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
53
|
-
|
|
54
|
-
style: surfaceStyle,
|
|
55
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_nestingContext.ContentPanelNestingContext.Provider, {
|
|
56
|
+
value: true,
|
|
55
57
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
56
|
-
className:
|
|
57
|
-
style:
|
|
58
|
-
children:
|
|
58
|
+
className: surfaceClass,
|
|
59
|
+
style: surfaceStyle,
|
|
60
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
61
|
+
className: contentClass,
|
|
62
|
+
style: contentStyle,
|
|
63
|
+
children: children
|
|
64
|
+
})
|
|
59
65
|
})
|
|
60
66
|
});
|
|
61
67
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","GUTTER_MASK_SPREAD","exports","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","surfaceClassName","surfaceStyle","contentClassName","contentStyle","surfaceClass","join","contentClass","filter","Boolean","jsx","View","className","style","ContentPanel","memo","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.tsx"],"mappings":";;;;;;AAsBA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_nestingContext","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","GUTTER_MASK_SPREAD","exports","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","surfaceClassName","surfaceStyle","contentClassName","contentStyle","useContentPanelNestingGuard","surfaceClass","join","contentClass","filter","Boolean","jsx","ContentPanelNestingContext","Provider","value","View","className","style","ContentPanel","memo","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.tsx"],"mappings":";;;;;;AAsBA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AAEA,IAAAE,eAAA,GAAAF,OAAA;AAG2B,IAAAG,WAAA,GAAAH,OAAA;AAAA,SAAAD,wBAAAK,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAP,uBAAA,YAAAA,CAAAK,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,gBAAAP,CAAA,IAAAD,CAAA,gBAAAC,CAAA,OAAAa,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAC,CAAA,OAAAM,CAAA,IAAAD,CAAA,GAAAU,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAC,CAAA,OAAAM,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAP,CAAA,EAAAM,CAAA,IAAAC,CAAA,CAAAP,CAAA,IAAAD,CAAA,CAAAC,CAAA,WAAAO,CAAA,KAAAR,CAAA,EAAAC,CAAA;AA5B3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AASA;AACA;AACA;AACA;AACO,MAAMkB,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAAG,EAAE;AACpC;AACO,MAAME,eAAe,GAAAD,OAAA,CAAAC,eAAA,GAAG,CAAC;AAChC;AACO,MAAMC,kBAAkB,GAAAF,OAAA,CAAAE,kBAAA,GAAG,CAAC;AA2BnC,MAAMC,qBAAkD,GAAGA,CAAC;EAC1DC,QAAQ;EACRC,gBAAgB;EAChBC,YAAY;EACZC,gBAAgB;EAChBC;AACF,CAAC,KAAK;EACJ;EACA,IAAAC,2CAA2B,EAAC,CAAC;EAE7B,MAAMC,YAAY,GAAG,CACnB,QAAQ,EACR,mBAAmB,EACnB,sCAAsC,EACtCL,gBAAgB,IAAI,SAAS,CAC9B,CAACM,IAAI,CAAC,GAAG,CAAC;EACX,MAAMC,YAAY,GAAG,CAAC,QAAQ,EAAEL,gBAAgB,CAAC,CAACM,MAAM,CAACC,OAAO,CAAC,CAACH,IAAI,CAAC,GAAG,CAAC;EAE3E,oBACE,IAAAhC,WAAA,CAAAoC,GAAA,EAACrC,eAAA,CAAAsC,0BAA0B,CAACC,QAAQ;IAACC,KAAK,EAAE,IAAK;IAAAd,QAAA,eAC/C,IAAAzB,WAAA,CAAAoC,GAAA,EAACtC,YAAA,CAAA0C,IAAI;MACIC,SAAS,EAAEV,YAAY;MAC9BW,KAAK,EAAEf,YAAa;MAAAF,QAAA,eAEpB,IAAAzB,WAAA,CAAAoC,GAAA,EAACtC,YAAA,CAAA0C,IAAI;QACIC,SAAS,EAAER,YAAY;QAC9BS,KAAK,EAAEb,YAAa;QAAAJ,QAAA,EAEnBA;MAAQ,CACL;IAAC,CACH;EAAC,CAC4B,CAAC;AAE1C,CAAC;AAEM,MAAMkB,YAAY,GAAAtB,OAAA,CAAAsB,YAAA,gBAAG,IAAAC,WAAI,EAACpB,qBAAqB,CAAC;AACvDmB,YAAY,CAACE,WAAW,GAAG,cAAc","ignoreList":[]}
|
|
@@ -7,6 +7,7 @@ exports.PANEL_TOP_INSET = exports.PANEL_BOTTOM_INSET = exports.GUTTER_MASK_SPREA
|
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
9
|
var _useTheme = require("../theme/use-theme.js");
|
|
10
|
+
var _nestingContext = require("./nesting-context.js");
|
|
10
11
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
11
12
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
12
13
|
/**
|
|
@@ -59,39 +60,49 @@ const ContentPanelComponent = ({
|
|
|
59
60
|
surfaceStyle,
|
|
60
61
|
contentClassName,
|
|
61
62
|
contentStyle,
|
|
62
|
-
showStickyFrame
|
|
63
|
+
showStickyFrame,
|
|
64
|
+
maskColor
|
|
63
65
|
}) => {
|
|
66
|
+
// Dev-only invariant — must run unconditionally before the early `!framed`
|
|
67
|
+
// return so the hook order stays stable across renders (rules of hooks).
|
|
68
|
+
(0, _nestingContext.useContentPanelNestingGuard)();
|
|
64
69
|
const {
|
|
65
70
|
colors
|
|
66
71
|
} = (0, _useTheme.useTheme)();
|
|
67
72
|
if (!framed) {
|
|
68
73
|
// Full-bleed: a single flat surface. No rounding, no sticky overlays.
|
|
69
74
|
const fullBleedClass = ['flex-1', surfaceClassName ?? 'bg-card', contentClassName].filter(Boolean).join(' ');
|
|
70
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_nestingContext.ContentPanelNestingContext.Provider, {
|
|
76
|
+
value: true,
|
|
77
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
78
|
+
className: fullBleedClass,
|
|
79
|
+
style: [surfaceStyle, contentStyle],
|
|
80
|
+
children: children
|
|
81
|
+
})
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
const surfaceClass = ['flex-1', 'rounded-radius-28', surfaceClassName ?? 'bg-card'].join(' ');
|
|
77
85
|
const contentClass = ['flex-1', 'rounded-radius-28 web:overflow-x-clip', contentClassName].filter(Boolean).join(' ');
|
|
78
|
-
return /*#__PURE__*/(0, _jsxRuntime.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_nestingContext.ContentPanelNestingContext.Provider, {
|
|
87
|
+
value: true,
|
|
88
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
89
|
+
className: surfaceClass,
|
|
90
|
+
style: surfaceStyle,
|
|
91
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
92
|
+
pointerEvents: "none",
|
|
93
|
+
className: '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)]',
|
|
94
|
+
style: {
|
|
95
|
+
boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}`
|
|
96
|
+
}
|
|
97
|
+
}), showStickyFrame !== false && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
98
|
+
pointerEvents: "none",
|
|
99
|
+
className: '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)]'
|
|
100
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
101
|
+
className: contentClass,
|
|
102
|
+
style: contentStyle,
|
|
103
|
+
children: children
|
|
104
|
+
})]
|
|
105
|
+
})
|
|
95
106
|
});
|
|
96
107
|
};
|
|
97
108
|
const ContentPanel = exports.ContentPanel = /*#__PURE__*/(0, _react.memo)(ContentPanelComponent);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_useTheme","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","GUTTER_MASK_SPREAD","exports","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","framed","surfaceClassName","surfaceStyle","contentClassName","contentStyle","showStickyFrame","colors","useTheme","fullBleedClass","filter","Boolean","join","jsx","View","className","style","surfaceClass","contentClass","jsxs","pointerEvents","boxShadow","background","ContentPanel","memo","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.web.tsx"],"mappings":";;;;;;AAoCA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AAEA,IAAAE,SAAA,GAAAF,OAAA;
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_useTheme","_nestingContext","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","GUTTER_MASK_SPREAD","exports","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","framed","surfaceClassName","surfaceStyle","contentClassName","contentStyle","showStickyFrame","maskColor","useContentPanelNestingGuard","colors","useTheme","fullBleedClass","filter","Boolean","join","jsx","ContentPanelNestingContext","Provider","value","View","className","style","surfaceClass","contentClass","jsxs","pointerEvents","boxShadow","background","ContentPanel","memo","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.web.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ContentPanelNestingContext = void 0;
|
|
7
|
+
exports.useContentPanelNestingGuard = useContentPanelNestingGuard;
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
/**
|
|
10
|
+
* Tracks whether the current subtree is already inside a `ContentPanel`.
|
|
11
|
+
*
|
|
12
|
+
* Every `ContentPanel` variant (web + native) wraps its rendered tree in
|
|
13
|
+
* `<ContentPanelNestingContext.Provider value={true}>`, so any DESCENDANT
|
|
14
|
+
* `ContentPanel` reads `true` here and can reject the invalid nesting. Context
|
|
15
|
+
* only flows downward, so side-by-side sibling panels each read the default
|
|
16
|
+
* `false` at their own level — siblings are valid, only true parent→child
|
|
17
|
+
* nesting trips the guard.
|
|
18
|
+
*/
|
|
19
|
+
const ContentPanelNestingContext = exports.ContentPanelNestingContext = /*#__PURE__*/(0, _react.createContext)(false);
|
|
20
|
+
ContentPanelNestingContext.displayName = 'BloomContentPanelNestingContext';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dev-only invariant: a `ContentPanel` must NEVER be rendered inside another
|
|
24
|
+
* `ContentPanel`. Nesting two content surfaces is meaningless and breaks the
|
|
25
|
+
* sticky / 100dvh framed frame. Call this unconditionally at the top of every
|
|
26
|
+
* `ContentPanel` variant — before any early return — so the hook order stays
|
|
27
|
+
* stable (rules of hooks).
|
|
28
|
+
*
|
|
29
|
+
* The throw is guarded by `process.env.NODE_ENV !== 'production'`, so bundlers
|
|
30
|
+
* (Metro on RN, Vite/Rolldown on web) statically strip the check from
|
|
31
|
+
* production builds for zero runtime overhead.
|
|
32
|
+
*/
|
|
33
|
+
function useContentPanelNestingGuard() {
|
|
34
|
+
const isNested = (0, _react.useContext)(ContentPanelNestingContext);
|
|
35
|
+
if (process.env.NODE_ENV !== 'production' && isNested) {
|
|
36
|
+
throw new Error('ContentPanel cannot be nested inside another ContentPanel. ' + 'Render panels as siblings (e.g. side-by-side columns), not nested.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=nesting-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_react","require","ContentPanelNestingContext","exports","createContext","displayName","useContentPanelNestingGuard","isNested","useContext","process","env","NODE_ENV","Error"],"sourceRoot":"../../../src","sources":["content-panel/nesting-context.ts"],"mappings":";;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMC,0BAA0B,GAAAC,OAAA,CAAAD,0BAAA,gBAAG,IAAAE,oBAAa,EAAU,KAAK,CAAC;AACvEF,0BAA0B,CAACG,WAAW,GAAG,iCAAiC;;AAE1E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,2BAA2BA,CAAA,EAAS;EAClD,MAAMC,QAAQ,GAAG,IAAAC,iBAAU,EAACN,0BAA0B,CAAC;EACvD,IAAIO,OAAO,CAACC,GAAG,CAACC,QAAQ,KAAK,YAAY,IAAIJ,QAAQ,EAAE;IACrD,MAAM,IAAIK,KAAK,CACb,6DAA6D,GAC3D,oEACJ,CAAC;EACH;AACF","ignoreList":[]}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import React, { memo } from 'react';
|
|
26
26
|
import { View } from 'react-native';
|
|
27
|
+
import { ContentPanelNestingContext, useContentPanelNestingGuard } from "./nesting-context.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Width (in px) of the WEB sticky gutter-mask box-shadow ring. Exported for
|
|
@@ -42,15 +43,20 @@ const ContentPanelComponent = ({
|
|
|
42
43
|
contentClassName,
|
|
43
44
|
contentStyle
|
|
44
45
|
}) => {
|
|
46
|
+
// Dev-only invariant — a ContentPanel must never be nested inside another.
|
|
47
|
+
useContentPanelNestingGuard();
|
|
45
48
|
const surfaceClass = ['flex-1', 'rounded-radius-28', 'overflow-hidden border border-border', surfaceClassName ?? 'bg-card'].join(' ');
|
|
46
49
|
const contentClass = ['flex-1', contentClassName].filter(Boolean).join(' ');
|
|
47
|
-
return /*#__PURE__*/_jsx(
|
|
48
|
-
|
|
49
|
-
style: surfaceStyle,
|
|
50
|
+
return /*#__PURE__*/_jsx(ContentPanelNestingContext.Provider, {
|
|
51
|
+
value: true,
|
|
50
52
|
children: /*#__PURE__*/_jsx(View, {
|
|
51
|
-
className:
|
|
52
|
-
style:
|
|
53
|
-
children:
|
|
53
|
+
className: surfaceClass,
|
|
54
|
+
style: surfaceStyle,
|
|
55
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
56
|
+
className: contentClass,
|
|
57
|
+
style: contentStyle,
|
|
58
|
+
children: children
|
|
59
|
+
})
|
|
54
60
|
})
|
|
55
61
|
});
|
|
56
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","memo","View","jsx","_jsx","GUTTER_MASK_SPREAD","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","surfaceClassName","surfaceStyle","contentClassName","contentStyle","surfaceClass","join","contentClass","filter","Boolean","className","style","ContentPanel","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAOA,KAAK,IAAIC,IAAI,QAAQ,OAAO;AACnC,SAASC,IAAI,QAAwC,cAAc;;
|
|
1
|
+
{"version":3,"names":["React","memo","View","ContentPanelNestingContext","useContentPanelNestingGuard","jsx","_jsx","GUTTER_MASK_SPREAD","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","surfaceClassName","surfaceStyle","contentClassName","contentStyle","surfaceClass","join","contentClass","filter","Boolean","Provider","value","className","style","ContentPanel","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAOA,KAAK,IAAIC,IAAI,QAAQ,OAAO;AACnC,SAASC,IAAI,QAAwC,cAAc;AAEnE,SACEC,0BAA0B,EAC1BC,2BAA2B,QACtB,sBAAmB;;AAE1B;AACA;AACA;AACA;AAHA,SAAAC,GAAA,IAAAC,IAAA;AAIA,OAAO,MAAMC,kBAAkB,GAAG,EAAE;AACpC;AACA,OAAO,MAAMC,eAAe,GAAG,CAAC;AAChC;AACA,OAAO,MAAMC,kBAAkB,GAAG,CAAC;AA2BnC,MAAMC,qBAAkD,GAAGA,CAAC;EAC1DC,QAAQ;EACRC,gBAAgB;EAChBC,YAAY;EACZC,gBAAgB;EAChBC;AACF,CAAC,KAAK;EACJ;EACAX,2BAA2B,CAAC,CAAC;EAE7B,MAAMY,YAAY,GAAG,CACnB,QAAQ,EACR,mBAAmB,EACnB,sCAAsC,EACtCJ,gBAAgB,IAAI,SAAS,CAC9B,CAACK,IAAI,CAAC,GAAG,CAAC;EACX,MAAMC,YAAY,GAAG,CAAC,QAAQ,EAAEJ,gBAAgB,CAAC,CAACK,MAAM,CAACC,OAAO,CAAC,CAACH,IAAI,CAAC,GAAG,CAAC;EAE3E,oBACEX,IAAA,CAACH,0BAA0B,CAACkB,QAAQ;IAACC,KAAK,EAAE,IAAK;IAAAX,QAAA,eAC/CL,IAAA,CAACJ,IAAI;MACIqB,SAAS,EAAEP,YAAY;MAC9BQ,KAAK,EAAEX,YAAa;MAAAF,QAAA,eAEpBL,IAAA,CAACJ,IAAI;QACIqB,SAAS,EAAEL,YAAY;QAC9BM,KAAK,EAAET,YAAa;QAAAJ,QAAA,EAEnBA;MAAQ,CACL;IAAC,CACH;EAAC,CAC4B,CAAC;AAE1C,CAAC;AAED,OAAO,MAAMc,YAAY,gBAAGxB,IAAI,CAACS,qBAAqB,CAAC;AACvDe,YAAY,CAACC,WAAW,GAAG,cAAc","ignoreList":[]}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import React, { memo } from 'react';
|
|
40
40
|
import { View } from 'react-native';
|
|
41
41
|
import { useTheme } from "../theme/use-theme.js";
|
|
42
|
+
import { ContentPanelNestingContext, useContentPanelNestingGuard } from "./nesting-context.js";
|
|
42
43
|
|
|
43
44
|
/** Width (in px) of the sticky gutter-mask box-shadow ring. */
|
|
44
45
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -54,39 +55,49 @@ const ContentPanelComponent = ({
|
|
|
54
55
|
surfaceStyle,
|
|
55
56
|
contentClassName,
|
|
56
57
|
contentStyle,
|
|
57
|
-
showStickyFrame
|
|
58
|
+
showStickyFrame,
|
|
59
|
+
maskColor
|
|
58
60
|
}) => {
|
|
61
|
+
// Dev-only invariant — must run unconditionally before the early `!framed`
|
|
62
|
+
// return so the hook order stays stable across renders (rules of hooks).
|
|
63
|
+
useContentPanelNestingGuard();
|
|
59
64
|
const {
|
|
60
65
|
colors
|
|
61
66
|
} = useTheme();
|
|
62
67
|
if (!framed) {
|
|
63
68
|
// Full-bleed: a single flat surface. No rounding, no sticky overlays.
|
|
64
69
|
const fullBleedClass = ['flex-1', surfaceClassName ?? 'bg-card', contentClassName].filter(Boolean).join(' ');
|
|
65
|
-
return /*#__PURE__*/_jsx(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
return /*#__PURE__*/_jsx(ContentPanelNestingContext.Provider, {
|
|
71
|
+
value: true,
|
|
72
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
73
|
+
className: fullBleedClass,
|
|
74
|
+
style: [surfaceStyle, contentStyle],
|
|
75
|
+
children: children
|
|
76
|
+
})
|
|
69
77
|
});
|
|
70
78
|
}
|
|
71
79
|
const surfaceClass = ['flex-1', 'rounded-radius-28', surfaceClassName ?? 'bg-card'].join(' ');
|
|
72
80
|
const contentClass = ['flex-1', 'rounded-radius-28 web:overflow-x-clip', contentClassName].filter(Boolean).join(' ');
|
|
73
|
-
return /*#__PURE__*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
return /*#__PURE__*/_jsx(ContentPanelNestingContext.Provider, {
|
|
82
|
+
value: true,
|
|
83
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
84
|
+
className: surfaceClass,
|
|
85
|
+
style: surfaceStyle,
|
|
86
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
87
|
+
pointerEvents: "none",
|
|
88
|
+
className: '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)]',
|
|
89
|
+
style: {
|
|
90
|
+
boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}`
|
|
91
|
+
}
|
|
92
|
+
}), showStickyFrame !== false && /*#__PURE__*/_jsx(View, {
|
|
93
|
+
pointerEvents: "none",
|
|
94
|
+
className: '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)]'
|
|
95
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
96
|
+
className: contentClass,
|
|
97
|
+
style: contentStyle,
|
|
98
|
+
children: children
|
|
99
|
+
})]
|
|
100
|
+
})
|
|
90
101
|
});
|
|
91
102
|
};
|
|
92
103
|
export const ContentPanel = /*#__PURE__*/memo(ContentPanelComponent);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","memo","View","useTheme","jsx","_jsx","jsxs","_jsxs","GUTTER_MASK_SPREAD","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","framed","surfaceClassName","surfaceStyle","contentClassName","contentStyle","showStickyFrame","colors","fullBleedClass","filter","Boolean","join","className","style","surfaceClass","contentClass","pointerEvents","boxShadow","background","ContentPanel","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.web.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAOA,KAAK,IAAIC,IAAI,QAAQ,OAAO;AACnC,SAASC,IAAI,QAAwC,cAAc;AAEnE,SAASC,QAAQ,QAAQ,uBAAoB;;
|
|
1
|
+
{"version":3,"names":["React","memo","View","useTheme","ContentPanelNestingContext","useContentPanelNestingGuard","jsx","_jsx","jsxs","_jsxs","GUTTER_MASK_SPREAD","PANEL_TOP_INSET","PANEL_BOTTOM_INSET","ContentPanelComponent","children","framed","surfaceClassName","surfaceStyle","contentClassName","contentStyle","showStickyFrame","maskColor","colors","fullBleedClass","filter","Boolean","join","Provider","value","className","style","surfaceClass","contentClass","pointerEvents","boxShadow","background","ContentPanel","displayName"],"sourceRoot":"../../../src","sources":["content-panel/index.web.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAOA,KAAK,IAAIC,IAAI,QAAQ,OAAO;AACnC,SAASC,IAAI,QAAwC,cAAc;AAEnE,SAASC,QAAQ,QAAQ,uBAAoB;AAC7C,SACEC,0BAA0B,EAC1BC,2BAA2B,QACtB,sBAAmB;;AAE1B;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AACA,OAAO,MAAMC,kBAAkB,GAAG,EAAE;AACpC;AACA,OAAO,MAAMC,eAAe,GAAG,CAAC;AAChC;AACA,OAAO,MAAMC,kBAAkB,GAAG,CAAC;AAyBnC,MAAMC,qBAAkD,GAAGA,CAAC;EAC1DC,QAAQ;EACRC,MAAM;EACNC,gBAAgB;EAChBC,YAAY;EACZC,gBAAgB;EAChBC,YAAY;EACZC,eAAe;EACfC;AACF,CAAC,KAAK;EACJ;EACA;EACAhB,2BAA2B,CAAC,CAAC;EAC7B,MAAM;IAAEiB;EAAO,CAAC,GAAGnB,QAAQ,CAAC,CAAC;EAE7B,IAAI,CAACY,MAAM,EAAE;IACX;IACA,MAAMQ,cAAc,GAAG,CACrB,QAAQ,EACRP,gBAAgB,IAAI,SAAS,EAC7BE,gBAAgB,CACjB,CACEM,MAAM,CAACC,OAAO,CAAC,CACfC,IAAI,CAAC,GAAG,CAAC;IACZ,oBACEnB,IAAA,CAACH,0BAA0B,CAACuB,QAAQ;MAACC,KAAK,EAAE,IAAK;MAAAd,QAAA,eAC/CP,IAAA,CAACL,IAAI;QACI2B,SAAS,EAAEN,cAAc;QAChCO,KAAK,EAAE,CAACb,YAAY,EAAEE,YAAY,CAAE;QAAAL,QAAA,EAEnCA;MAAQ,CACL;IAAC,CAC4B,CAAC;EAE1C;EAEA,MAAMiB,YAAY,GAAG,CAAC,QAAQ,EAAE,mBAAmB,EAAEf,gBAAgB,IAAI,SAAS,CAAC,CAACU,IAAI,CAAC,GAAG,CAAC;EAC7F,MAAMM,YAAY,GAAG,CAAC,QAAQ,EAAE,uCAAuC,EAAEd,gBAAgB,CAAC,CACvFM,MAAM,CAACC,OAAO,CAAC,CACfC,IAAI,CAAC,GAAG,CAAC;EAEZ,oBACEnB,IAAA,CAACH,0BAA0B,CAACuB,QAAQ;IAACC,KAAK,EAAE,IAAK;IAAAd,QAAA,eAC/CL,KAAA,CAACP,IAAI;MACI2B,SAAS,EAAEE,YAAY;MAC9BD,KAAK,EAAEb,YAAa;MAAAH,QAAA,gBAGpBP,IAAA,CAACL,IAAI;QACH+B,aAAa,EAAC,MAAM;QAElBJ,SAAS,EACP,8IAA8I;QAElJC,KAAK,EAAE;UAAEI,SAAS,EAAE,SAASxB,kBAAkB,MAAMW,SAAS,IAAIC,MAAM,CAACa,UAAU;QAAG;MAAE,CACzF,CAAC,EAEDf,eAAe,KAAK,KAAK,iBACxBb,IAAA,CAACL,IAAI;QACH+B,aAAa,EAAC,MAAM;QAElBJ,SAAS,EACP;MAAyI,CAE9I,CACF,eAEDtB,IAAA,CAACL,IAAI;QACI2B,SAAS,EAAEG,YAAY;QAC9BF,KAAK,EAAEX,YAAa;QAAAL,QAAA,EAEnBA;MAAQ,CACL,CAAC;IAAA,CACH;EAAC,CAC4B,CAAC;AAE1C,CAAC;AAED,OAAO,MAAMsB,YAAY,gBAAGnC,IAAI,CAACY,qBAAqB,CAAC;AACvDuB,YAAY,CAACC,WAAW,GAAG,cAAc","ignoreList":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks whether the current subtree is already inside a `ContentPanel`.
|
|
7
|
+
*
|
|
8
|
+
* Every `ContentPanel` variant (web + native) wraps its rendered tree in
|
|
9
|
+
* `<ContentPanelNestingContext.Provider value={true}>`, so any DESCENDANT
|
|
10
|
+
* `ContentPanel` reads `true` here and can reject the invalid nesting. Context
|
|
11
|
+
* only flows downward, so side-by-side sibling panels each read the default
|
|
12
|
+
* `false` at their own level — siblings are valid, only true parent→child
|
|
13
|
+
* nesting trips the guard.
|
|
14
|
+
*/
|
|
15
|
+
export const ContentPanelNestingContext = /*#__PURE__*/createContext(false);
|
|
16
|
+
ContentPanelNestingContext.displayName = 'BloomContentPanelNestingContext';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Dev-only invariant: a `ContentPanel` must NEVER be rendered inside another
|
|
20
|
+
* `ContentPanel`. Nesting two content surfaces is meaningless and breaks the
|
|
21
|
+
* sticky / 100dvh framed frame. Call this unconditionally at the top of every
|
|
22
|
+
* `ContentPanel` variant — before any early return — so the hook order stays
|
|
23
|
+
* stable (rules of hooks).
|
|
24
|
+
*
|
|
25
|
+
* The throw is guarded by `process.env.NODE_ENV !== 'production'`, so bundlers
|
|
26
|
+
* (Metro on RN, Vite/Rolldown on web) statically strip the check from
|
|
27
|
+
* production builds for zero runtime overhead.
|
|
28
|
+
*/
|
|
29
|
+
export function useContentPanelNestingGuard() {
|
|
30
|
+
const isNested = useContext(ContentPanelNestingContext);
|
|
31
|
+
if (process.env.NODE_ENV !== 'production' && isNested) {
|
|
32
|
+
throw new Error('ContentPanel cannot be nested inside another ContentPanel. ' + 'Render panels as siblings (e.g. side-by-side columns), not nested.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=nesting-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["createContext","useContext","ContentPanelNestingContext","displayName","useContentPanelNestingGuard","isNested","process","env","NODE_ENV","Error"],"sourceRoot":"../../../src","sources":["content-panel/nesting-context.ts"],"mappings":";;AAAA,SAASA,aAAa,EAAEC,UAAU,QAAQ,OAAO;;AAEjD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,0BAA0B,gBAAGF,aAAa,CAAU,KAAK,CAAC;AACvEE,0BAA0B,CAACC,WAAW,GAAG,iCAAiC;;AAE1E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,2BAA2BA,CAAA,EAAS;EAClD,MAAMC,QAAQ,GAAGJ,UAAU,CAACC,0BAA0B,CAAC;EACvD,IAAII,OAAO,CAACC,GAAG,CAACC,QAAQ,KAAK,YAAY,IAAIH,QAAQ,EAAE;IACrD,MAAM,IAAII,KAAK,CACb,6DAA6D,GAC3D,oEACJ,CAAC;EACH;AACF","ignoreList":[]}
|
|
@@ -49,6 +49,11 @@ export interface ContentPanelProps {
|
|
|
49
49
|
* is always part of the surface).
|
|
50
50
|
*/
|
|
51
51
|
showStickyFrame?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Color of the WEB sticky bleed-mask gutter ring. Accepted for cross-platform
|
|
54
|
+
* API parity — no-op on native (there is no bleed-mask), like `framed`.
|
|
55
|
+
*/
|
|
56
|
+
maskColor?: string;
|
|
52
57
|
}
|
|
53
58
|
export declare const ContentPanel: React.NamedExoticComponent<ContentPanelProps>;
|
|
54
59
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAOpE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,wDAAwD;AACxD,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqCD,eAAO,MAAM,YAAY,+CAA8B,CAAC"}
|
|
@@ -54,6 +54,15 @@ export interface ContentPanelProps {
|
|
|
54
54
|
contentStyle?: StyleProp<ViewStyle>;
|
|
55
55
|
/** Render the sticky border frame (defaults to true when framed). */
|
|
56
56
|
showStickyFrame?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Color of the sticky bleed-mask gutter ring. The ring is meant to match the
|
|
59
|
+
* UNSCOPED outer gutter band (the app's background), so consumers that wrap
|
|
60
|
+
* `<ContentPanel>` in a `BloomColorScope` (e.g. a tinted per-profile color)
|
|
61
|
+
* should pass the unscoped background here — otherwise the internally-read
|
|
62
|
+
* scoped `background` token would tint the ring and leave a faint corner seam.
|
|
63
|
+
* Defaults to the current theme's `background` token.
|
|
64
|
+
*/
|
|
65
|
+
maskColor?: string;
|
|
57
66
|
}
|
|
58
67
|
export declare const ContentPanel: React.NamedExoticComponent<ContentPanelProps>;
|
|
59
68
|
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAQpE,+DAA+D;AAC/D,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,4DAA4D;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgFD,eAAO,MAAM,YAAY,+CAA8B,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks whether the current subtree is already inside a `ContentPanel`.
|
|
3
|
+
*
|
|
4
|
+
* Every `ContentPanel` variant (web + native) wraps its rendered tree in
|
|
5
|
+
* `<ContentPanelNestingContext.Provider value={true}>`, so any DESCENDANT
|
|
6
|
+
* `ContentPanel` reads `true` here and can reject the invalid nesting. Context
|
|
7
|
+
* only flows downward, so side-by-side sibling panels each read the default
|
|
8
|
+
* `false` at their own level — siblings are valid, only true parent→child
|
|
9
|
+
* nesting trips the guard.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ContentPanelNestingContext: import("react").Context<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Dev-only invariant: a `ContentPanel` must NEVER be rendered inside another
|
|
14
|
+
* `ContentPanel`. Nesting two content surfaces is meaningless and breaks the
|
|
15
|
+
* sticky / 100dvh framed frame. Call this unconditionally at the top of every
|
|
16
|
+
* `ContentPanel` variant — before any early return — so the hook order stays
|
|
17
|
+
* stable (rules of hooks).
|
|
18
|
+
*
|
|
19
|
+
* The throw is guarded by `process.env.NODE_ENV !== 'production'`, so bundlers
|
|
20
|
+
* (Metro on RN, Vite/Rolldown on web) statically strip the check from
|
|
21
|
+
* production builds for zero runtime overhead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useContentPanelNestingGuard(): void;
|
|
24
|
+
//# sourceMappingURL=nesting-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nesting-context.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/nesting-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,kCAAgC,CAAC;AAGxE;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAQlD"}
|
|
@@ -49,6 +49,11 @@ export interface ContentPanelProps {
|
|
|
49
49
|
* is always part of the surface).
|
|
50
50
|
*/
|
|
51
51
|
showStickyFrame?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Color of the WEB sticky bleed-mask gutter ring. Accepted for cross-platform
|
|
54
|
+
* API parity — no-op on native (there is no bleed-mask), like `framed`.
|
|
55
|
+
*/
|
|
56
|
+
maskColor?: string;
|
|
52
57
|
}
|
|
53
58
|
export declare const ContentPanel: React.NamedExoticComponent<ContentPanelProps>;
|
|
54
59
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAOpE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,wDAAwD;AACxD,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqCD,eAAO,MAAM,YAAY,+CAA8B,CAAC"}
|
|
@@ -54,6 +54,15 @@ export interface ContentPanelProps {
|
|
|
54
54
|
contentStyle?: StyleProp<ViewStyle>;
|
|
55
55
|
/** Render the sticky border frame (defaults to true when framed). */
|
|
56
56
|
showStickyFrame?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Color of the sticky bleed-mask gutter ring. The ring is meant to match the
|
|
59
|
+
* UNSCOPED outer gutter band (the app's background), so consumers that wrap
|
|
60
|
+
* `<ContentPanel>` in a `BloomColorScope` (e.g. a tinted per-profile color)
|
|
61
|
+
* should pass the unscoped background here — otherwise the internally-read
|
|
62
|
+
* scoped `background` token would tint the ring and leave a faint corner seam.
|
|
63
|
+
* Defaults to the current theme's `background` token.
|
|
64
|
+
*/
|
|
65
|
+
maskColor?: string;
|
|
57
66
|
}
|
|
58
67
|
export declare const ContentPanel: React.NamedExoticComponent<ContentPanelProps>;
|
|
59
68
|
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/index.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAQpE,+DAA+D;AAC/D,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,4DAA4D;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,qEAAqE;IACrE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgFD,eAAO,MAAM,YAAY,+CAA8B,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks whether the current subtree is already inside a `ContentPanel`.
|
|
3
|
+
*
|
|
4
|
+
* Every `ContentPanel` variant (web + native) wraps its rendered tree in
|
|
5
|
+
* `<ContentPanelNestingContext.Provider value={true}>`, so any DESCENDANT
|
|
6
|
+
* `ContentPanel` reads `true` here and can reject the invalid nesting. Context
|
|
7
|
+
* only flows downward, so side-by-side sibling panels each read the default
|
|
8
|
+
* `false` at their own level — siblings are valid, only true parent→child
|
|
9
|
+
* nesting trips the guard.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ContentPanelNestingContext: import("react").Context<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Dev-only invariant: a `ContentPanel` must NEVER be rendered inside another
|
|
14
|
+
* `ContentPanel`. Nesting two content surfaces is meaningless and breaks the
|
|
15
|
+
* sticky / 100dvh framed frame. Call this unconditionally at the top of every
|
|
16
|
+
* `ContentPanel` variant — before any early return — so the hook order stays
|
|
17
|
+
* stable (rules of hooks).
|
|
18
|
+
*
|
|
19
|
+
* The throw is guarded by `process.env.NODE_ENV !== 'production'`, so bundlers
|
|
20
|
+
* (Metro on RN, Vite/Rolldown on web) statically strip the check from
|
|
21
|
+
* production builds for zero runtime overhead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useContentPanelNestingGuard(): void;
|
|
24
|
+
//# sourceMappingURL=nesting-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nesting-context.d.ts","sourceRoot":"","sources":["../../../../src/content-panel/nesting-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,kCAAgC,CAAC;AAGxE;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAQlD"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { ContentPanel } from '../content-panel';
|
|
6
|
+
|
|
7
|
+
// Jest resolves `../content-panel` to the native variant (`index.tsx`), which
|
|
8
|
+
// does not read the theme — so these render without a BloomThemeProvider. The
|
|
9
|
+
// nesting guard lives in the shared `nesting-context` module exercised by both
|
|
10
|
+
// variants.
|
|
11
|
+
describe('ContentPanel nesting guard', () => {
|
|
12
|
+
it('renders a single (non-nested) panel', () => {
|
|
13
|
+
const { getByText } = render(
|
|
14
|
+
<ContentPanel framed>
|
|
15
|
+
<Text>content</Text>
|
|
16
|
+
</ContentPanel>,
|
|
17
|
+
);
|
|
18
|
+
expect(getByText('content')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders side-by-side sibling panels without tripping the guard', () => {
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<React.Fragment>
|
|
24
|
+
<ContentPanel framed>
|
|
25
|
+
<Text>left</Text>
|
|
26
|
+
</ContentPanel>
|
|
27
|
+
<ContentPanel framed>
|
|
28
|
+
<Text>right</Text>
|
|
29
|
+
</ContentPanel>
|
|
30
|
+
</React.Fragment>,
|
|
31
|
+
);
|
|
32
|
+
expect(getByText('left')).toBeTruthy();
|
|
33
|
+
expect(getByText('right')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('throws when a ContentPanel is nested inside another ContentPanel', () => {
|
|
37
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
38
|
+
expect(() =>
|
|
39
|
+
render(
|
|
40
|
+
<ContentPanel framed>
|
|
41
|
+
<ContentPanel framed>
|
|
42
|
+
<Text>nested</Text>
|
|
43
|
+
</ContentPanel>
|
|
44
|
+
</ContentPanel>,
|
|
45
|
+
),
|
|
46
|
+
).toThrow(
|
|
47
|
+
'ContentPanel cannot be nested inside another ContentPanel. ' +
|
|
48
|
+
'Render panels as siblings (e.g. side-by-side columns), not nested.',
|
|
49
|
+
);
|
|
50
|
+
consoleError.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
import React, { memo } from 'react';
|
|
24
24
|
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
25
25
|
|
|
26
|
+
import {
|
|
27
|
+
ContentPanelNestingContext,
|
|
28
|
+
useContentPanelNestingGuard,
|
|
29
|
+
} from './nesting-context';
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Width (in px) of the WEB sticky gutter-mask box-shadow ring. Exported for
|
|
28
33
|
* cross-platform API parity (it has no effect on native).
|
|
@@ -51,6 +56,11 @@ export interface ContentPanelProps {
|
|
|
51
56
|
* is always part of the surface).
|
|
52
57
|
*/
|
|
53
58
|
showStickyFrame?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Color of the WEB sticky bleed-mask gutter ring. Accepted for cross-platform
|
|
61
|
+
* API parity — no-op on native (there is no bleed-mask), like `framed`.
|
|
62
|
+
*/
|
|
63
|
+
maskColor?: string;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
@@ -60,6 +70,9 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
60
70
|
contentClassName,
|
|
61
71
|
contentStyle,
|
|
62
72
|
}) => {
|
|
73
|
+
// Dev-only invariant — a ContentPanel must never be nested inside another.
|
|
74
|
+
useContentPanelNestingGuard();
|
|
75
|
+
|
|
63
76
|
const surfaceClass = [
|
|
64
77
|
'flex-1',
|
|
65
78
|
'rounded-radius-28',
|
|
@@ -69,17 +82,19 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
69
82
|
const contentClass = ['flex-1', contentClassName].filter(Boolean).join(' ');
|
|
70
83
|
|
|
71
84
|
return (
|
|
72
|
-
<
|
|
73
|
-
{...({ className: surfaceClass } as Record<string, string>)}
|
|
74
|
-
style={surfaceStyle}
|
|
75
|
-
>
|
|
85
|
+
<ContentPanelNestingContext.Provider value={true}>
|
|
76
86
|
<View
|
|
77
|
-
{...({ className:
|
|
78
|
-
style={
|
|
87
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
88
|
+
style={surfaceStyle}
|
|
79
89
|
>
|
|
80
|
-
|
|
90
|
+
<View
|
|
91
|
+
{...({ className: contentClass } as Record<string, string>)}
|
|
92
|
+
style={contentStyle}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</View>
|
|
81
96
|
</View>
|
|
82
|
-
</
|
|
97
|
+
</ContentPanelNestingContext.Provider>
|
|
83
98
|
);
|
|
84
99
|
};
|
|
85
100
|
|
|
@@ -38,6 +38,10 @@ import React, { memo } from 'react';
|
|
|
38
38
|
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
39
39
|
|
|
40
40
|
import { useTheme } from '../theme/use-theme';
|
|
41
|
+
import {
|
|
42
|
+
ContentPanelNestingContext,
|
|
43
|
+
useContentPanelNestingGuard,
|
|
44
|
+
} from './nesting-context';
|
|
41
45
|
|
|
42
46
|
/** Width (in px) of the sticky gutter-mask box-shadow ring. */
|
|
43
47
|
export const GUTTER_MASK_SPREAD = 40;
|
|
@@ -58,6 +62,15 @@ export interface ContentPanelProps {
|
|
|
58
62
|
contentStyle?: StyleProp<ViewStyle>;
|
|
59
63
|
/** Render the sticky border frame (defaults to true when framed). */
|
|
60
64
|
showStickyFrame?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Color of the sticky bleed-mask gutter ring. The ring is meant to match the
|
|
67
|
+
* UNSCOPED outer gutter band (the app's background), so consumers that wrap
|
|
68
|
+
* `<ContentPanel>` in a `BloomColorScope` (e.g. a tinted per-profile color)
|
|
69
|
+
* should pass the unscoped background here — otherwise the internally-read
|
|
70
|
+
* scoped `background` token would tint the ring and leave a faint corner seam.
|
|
71
|
+
* Defaults to the current theme's `background` token.
|
|
72
|
+
*/
|
|
73
|
+
maskColor?: string;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
@@ -68,7 +81,11 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
68
81
|
contentClassName,
|
|
69
82
|
contentStyle,
|
|
70
83
|
showStickyFrame,
|
|
84
|
+
maskColor,
|
|
71
85
|
}) => {
|
|
86
|
+
// Dev-only invariant — must run unconditionally before the early `!framed`
|
|
87
|
+
// return so the hook order stays stable across renders (rules of hooks).
|
|
88
|
+
useContentPanelNestingGuard();
|
|
72
89
|
const { colors } = useTheme();
|
|
73
90
|
|
|
74
91
|
if (!framed) {
|
|
@@ -81,12 +98,14 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
81
98
|
.filter(Boolean)
|
|
82
99
|
.join(' ');
|
|
83
100
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
<ContentPanelNestingContext.Provider value={true}>
|
|
102
|
+
<View
|
|
103
|
+
{...({ className: fullBleedClass } as Record<string, string>)}
|
|
104
|
+
style={[surfaceStyle, contentStyle]}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</View>
|
|
108
|
+
</ContentPanelNestingContext.Provider>
|
|
90
109
|
);
|
|
91
110
|
}
|
|
92
111
|
|
|
@@ -96,37 +115,39 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
96
115
|
.join(' ');
|
|
97
116
|
|
|
98
117
|
return (
|
|
99
|
-
<
|
|
100
|
-
{...({ className: surfaceClass } as Record<string, string>)}
|
|
101
|
-
style={surfaceStyle}
|
|
102
|
-
>
|
|
103
|
-
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. */}
|
|
118
|
+
<ContentPanelNestingContext.Provider value={true}>
|
|
104
119
|
<View
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} as Record<string, string>)}
|
|
110
|
-
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${colors.background}` }}
|
|
111
|
-
/>
|
|
112
|
-
{/* (2) Border-frame overlay — one continuous rounded border, above all. */}
|
|
113
|
-
{showStickyFrame !== false && (
|
|
120
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
121
|
+
style={surfaceStyle}
|
|
122
|
+
>
|
|
123
|
+
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. */}
|
|
114
124
|
<View
|
|
115
125
|
pointerEvents="none"
|
|
116
126
|
{...({
|
|
117
127
|
className:
|
|
118
|
-
'web:sticky web:top-2 z-
|
|
128
|
+
'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)]',
|
|
119
129
|
} as Record<string, string>)}
|
|
130
|
+
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}` }}
|
|
120
131
|
/>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
{/* (2) Border-frame overlay — one continuous rounded border, above all. */}
|
|
133
|
+
{showStickyFrame !== false && (
|
|
134
|
+
<View
|
|
135
|
+
pointerEvents="none"
|
|
136
|
+
{...({
|
|
137
|
+
className:
|
|
138
|
+
'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)]',
|
|
139
|
+
} as Record<string, string>)}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
{/* Content wrapper — clipped to the rounded panel shape on web. */}
|
|
143
|
+
<View
|
|
144
|
+
{...({ className: contentClass } as Record<string, string>)}
|
|
145
|
+
style={contentStyle}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</View>
|
|
128
149
|
</View>
|
|
129
|
-
</
|
|
150
|
+
</ContentPanelNestingContext.Provider>
|
|
130
151
|
);
|
|
131
152
|
};
|
|
132
153
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks whether the current subtree is already inside a `ContentPanel`.
|
|
5
|
+
*
|
|
6
|
+
* Every `ContentPanel` variant (web + native) wraps its rendered tree in
|
|
7
|
+
* `<ContentPanelNestingContext.Provider value={true}>`, so any DESCENDANT
|
|
8
|
+
* `ContentPanel` reads `true` here and can reject the invalid nesting. Context
|
|
9
|
+
* only flows downward, so side-by-side sibling panels each read the default
|
|
10
|
+
* `false` at their own level — siblings are valid, only true parent→child
|
|
11
|
+
* nesting trips the guard.
|
|
12
|
+
*/
|
|
13
|
+
export const ContentPanelNestingContext = createContext<boolean>(false);
|
|
14
|
+
ContentPanelNestingContext.displayName = 'BloomContentPanelNestingContext';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dev-only invariant: a `ContentPanel` must NEVER be rendered inside another
|
|
18
|
+
* `ContentPanel`. Nesting two content surfaces is meaningless and breaks the
|
|
19
|
+
* sticky / 100dvh framed frame. Call this unconditionally at the top of every
|
|
20
|
+
* `ContentPanel` variant — before any early return — so the hook order stays
|
|
21
|
+
* stable (rules of hooks).
|
|
22
|
+
*
|
|
23
|
+
* The throw is guarded by `process.env.NODE_ENV !== 'production'`, so bundlers
|
|
24
|
+
* (Metro on RN, Vite/Rolldown on web) statically strip the check from
|
|
25
|
+
* production builds for zero runtime overhead.
|
|
26
|
+
*/
|
|
27
|
+
export function useContentPanelNestingGuard(): void {
|
|
28
|
+
const isNested = useContext(ContentPanelNestingContext);
|
|
29
|
+
if (process.env.NODE_ENV !== 'production' && isNested) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'ContentPanel cannot be nested inside another ContentPanel. ' +
|
|
32
|
+
'Render panels as siblings (e.g. side-by-side columns), not nested.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|