@oxyhq/bloom 0.22.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 +31 -21
- 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 +31 -21
- 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.map +1 -1
- 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.map +1 -1
- 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 +18 -8
- package/src/content-panel/index.web.tsx +40 -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":"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","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
|
/**
|
|
@@ -62,37 +63,46 @@ const ContentPanelComponent = ({
|
|
|
62
63
|
showStickyFrame,
|
|
63
64
|
maskColor
|
|
64
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)();
|
|
65
69
|
const {
|
|
66
70
|
colors
|
|
67
71
|
} = (0, _useTheme.useTheme)();
|
|
68
72
|
if (!framed) {
|
|
69
73
|
// Full-bleed: a single flat surface. No rounding, no sticky overlays.
|
|
70
74
|
const fullBleedClass = ['flex-1', surfaceClassName ?? 'bg-card', contentClassName].filter(Boolean).join(' ');
|
|
71
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
})
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
84
|
const surfaceClass = ['flex-1', 'rounded-radius-28', surfaceClassName ?? 'bg-card'].join(' ');
|
|
78
85
|
const contentClass = ['flex-1', 'rounded-radius-28 web:overflow-x-clip', contentClassName].filter(Boolean).join(' ');
|
|
79
|
-
return /*#__PURE__*/(0, _jsxRuntime.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
})
|
|
96
106
|
});
|
|
97
107
|
};
|
|
98
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","maskColor","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";
|
|
@@ -57,37 +58,46 @@ const ContentPanelComponent = ({
|
|
|
57
58
|
showStickyFrame,
|
|
58
59
|
maskColor
|
|
59
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();
|
|
60
64
|
const {
|
|
61
65
|
colors
|
|
62
66
|
} = useTheme();
|
|
63
67
|
if (!framed) {
|
|
64
68
|
// Full-bleed: a single flat surface. No rounding, no sticky overlays.
|
|
65
69
|
const fullBleedClass = ['flex-1', surfaceClassName ?? 'bg-card', contentClassName].filter(Boolean).join(' ');
|
|
66
|
-
return /*#__PURE__*/_jsx(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
})
|
|
70
77
|
});
|
|
71
78
|
}
|
|
72
79
|
const surfaceClass = ['flex-1', 'rounded-radius-28', surfaceClassName ?? 'bg-card'].join(' ');
|
|
73
80
|
const contentClass = ['flex-1', 'rounded-radius-28 web:overflow-x-clip', contentClassName].filter(Boolean).join(' ');
|
|
74
|
-
return /*#__PURE__*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
})
|
|
91
101
|
});
|
|
92
102
|
};
|
|
93
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","maskColor","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":[]}
|
|
@@ -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"}
|
|
@@ -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"}
|
|
@@ -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"}
|
|
@@ -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).
|
|
@@ -65,6 +70,9 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
65
70
|
contentClassName,
|
|
66
71
|
contentStyle,
|
|
67
72
|
}) => {
|
|
73
|
+
// Dev-only invariant — a ContentPanel must never be nested inside another.
|
|
74
|
+
useContentPanelNestingGuard();
|
|
75
|
+
|
|
68
76
|
const surfaceClass = [
|
|
69
77
|
'flex-1',
|
|
70
78
|
'rounded-radius-28',
|
|
@@ -74,17 +82,19 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
74
82
|
const contentClass = ['flex-1', contentClassName].filter(Boolean).join(' ');
|
|
75
83
|
|
|
76
84
|
return (
|
|
77
|
-
<
|
|
78
|
-
{...({ className: surfaceClass } as Record<string, string>)}
|
|
79
|
-
style={surfaceStyle}
|
|
80
|
-
>
|
|
85
|
+
<ContentPanelNestingContext.Provider value={true}>
|
|
81
86
|
<View
|
|
82
|
-
{...({ className:
|
|
83
|
-
style={
|
|
87
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
88
|
+
style={surfaceStyle}
|
|
84
89
|
>
|
|
85
|
-
|
|
90
|
+
<View
|
|
91
|
+
{...({ className: contentClass } as Record<string, string>)}
|
|
92
|
+
style={contentStyle}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</View>
|
|
86
96
|
</View>
|
|
87
|
-
</
|
|
97
|
+
</ContentPanelNestingContext.Provider>
|
|
88
98
|
);
|
|
89
99
|
};
|
|
90
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;
|
|
@@ -79,6 +83,9 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
79
83
|
showStickyFrame,
|
|
80
84
|
maskColor,
|
|
81
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();
|
|
82
89
|
const { colors } = useTheme();
|
|
83
90
|
|
|
84
91
|
if (!framed) {
|
|
@@ -91,12 +98,14 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
91
98
|
.filter(Boolean)
|
|
92
99
|
.join(' ');
|
|
93
100
|
return (
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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>
|
|
100
109
|
);
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -106,37 +115,39 @@ const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
|
106
115
|
.join(' ');
|
|
107
116
|
|
|
108
117
|
return (
|
|
109
|
-
<
|
|
110
|
-
{...({ className: surfaceClass } as Record<string, string>)}
|
|
111
|
-
style={surfaceStyle}
|
|
112
|
-
>
|
|
113
|
-
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. */}
|
|
118
|
+
<ContentPanelNestingContext.Provider value={true}>
|
|
114
119
|
<View
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} as Record<string, string>)}
|
|
120
|
-
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}` }}
|
|
121
|
-
/>
|
|
122
|
-
{/* (2) Border-frame overlay — one continuous rounded border, above all. */}
|
|
123
|
-
{showStickyFrame !== false && (
|
|
120
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
121
|
+
style={surfaceStyle}
|
|
122
|
+
>
|
|
123
|
+
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. */}
|
|
124
124
|
<View
|
|
125
125
|
pointerEvents="none"
|
|
126
126
|
{...({
|
|
127
127
|
className:
|
|
128
|
-
'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)]',
|
|
129
129
|
} as Record<string, string>)}
|
|
130
|
+
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${maskColor ?? colors.background}` }}
|
|
130
131
|
/>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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>
|
|
138
149
|
</View>
|
|
139
|
-
</
|
|
150
|
+
</ContentPanelNestingContext.Provider>
|
|
140
151
|
);
|
|
141
152
|
};
|
|
142
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
|
+
}
|