@dotcms/react 26.9.18-1 → 26.9.23-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -308,23 +308,70 @@ The layout renders the pre-rendered slot node for a contentlet if one exists, ot
308
308
 
309
309
  #### Component Mapping
310
310
 
311
- The `DotCMSLayoutBody` component uses a `components` prop to map content type variable names to React components. This allows you to render different components for different content types. Example:
311
+ The `DotCMSLayoutBody` component uses a `components` prop to map content type variable names to React components. This allows you to render different components for different content types.
312
312
 
313
- ```typescript
314
- const DYNAMIC_COMPONENTS = {
315
- Blog: MyBlogCard,
316
- Product: DotCMSProductComponent
313
+ **Load the mapped components dynamically.** A static map makes every component reachable from the client entry, so a visitor downloads all of them on every route — even the ones that page never renders. Wrapping each in a dynamic import gives each component its own chunk, fetched only when a page actually contains that content type.
314
+
315
+ In Next.js, use `next/dynamic`:
316
+
317
+ ```tsx
318
+ import dynamic from 'next/dynamic';
319
+
320
+ import { CustomNoComponent } from './Empty';
321
+
322
+ export const pageComponents = {
323
+ Blog: dynamic(() => import('./MyBlogCard')),
324
+ Product: dynamic(() => import('./DotCMSProductComponent')),
325
+ // The fallback for unmapped content types stays eager: it should render without
326
+ // waiting on a network round-trip, and it is small.
327
+ CustomNoComponent
328
+ };
329
+ ```
330
+
331
+ Anywhere else — Astro, Vite, plain React — use `React.lazy`:
332
+
333
+ ```tsx
334
+ import { lazy } from 'react';
335
+
336
+ export const pageComponents = {
337
+ Blog: lazy(() => import('./MyBlogCard')),
338
+ Product: lazy(() => import('./DotCMSProductComponent'))
317
339
  };
318
340
  ```
319
341
 
342
+ `next/dynamic` brings its own Suspense boundary. `React.lazy` does not, but you do not need to add one: `DotCMSLayoutBody` wraps every contentlet in a Suspense boundary, so a lazy component can be mapped directly.
343
+
320
344
  - Keys (e.g., `Blog`, `Product`): Match your [content type variable names](https://dev.dotcms.com/docs/content-types#VariableNames) in dotCMS
321
- - Values: Dynamic imports of your React components that render each content type
322
- - Supports lazy loading through dynamic imports
323
- - Components must be standalone or declared in a module
345
+ - Values: any React component type — including the result of `next/dynamic` or `React.lazy`
324
346
 
325
347
  > [!TIP]
326
348
  > Always use the exact content type variable name from dotCMS as the key. You can find this in the Content Types section of your dotCMS admin panel.
327
349
 
350
+ > [!WARNING]
351
+ > Avoid re-exporting your content-type components from a barrel (`export * from './Banner'`). Anything importing that barrel makes every component statically reachable, which puts them all back in the initial bundle no matter how the map loads them.
352
+
353
+ #### Migrating an existing app to dynamic mapping
354
+
355
+ Earlier versions of our examples showed a static component map, so an app built from them ships **every** mapped component on **every** route. If you have dozens of content types, that is the single biggest thing you can fix — and upgrading the SDK does not fix it for you, because the map lives in your code.
356
+
357
+ Measured on the Next.js example with 135 mapped content types, one realistic component each:
358
+
359
+ | Component map | Initial route JS | Mapped components in the initial bundle |
360
+ | --- | --- | --- |
361
+ | Static imports | 892.7 KB raw / 250.4 KB gzip | **135 of 135** |
362
+ | `next/dynamic` | 792.6 KB raw / 238.1 KB gzip | **0 of 135** |
363
+
364
+ Every mapped component leaves the initial route and is fetched only when a page contains that content type. How much weight that removes depends on your components: the 100 KB above is what 135 modest ones cost, and real component libraries are usually heavier.
365
+
366
+ To migrate:
367
+
368
+ 1. Wrap each entry in the map with `next/dynamic` (or `React.lazy` outside Next.js). Keep the unmatched-type fallback eager.
369
+ 2. Delete any barrel that re-exports your content-type components, and import them directly where you need them elsewhere. A single `export * from './Banner'` re-exports every component from one module and undoes the whole change.
370
+ 3. Do the same for the `customRenderers` map you pass to `DotCMSBlockEditorRenderer` if it maps more than a handful of components.
371
+ 4. Rebuild and confirm. The quickest check is to map a component no page uses, give it a unique string, and grep the production client chunks for that string — it should appear only in its own chunk. `examples/scripts/check-initial-bundle.mjs` in this repository does exactly that for the Next.js and Astro examples and can be pointed at your build.
372
+
373
+ `next/dynamic` keeps server rendering on by default, so this does not change what the crawler sees.
374
+
328
375
 
329
376
  ### DotCMSEditableText
330
377
 
@@ -1,8 +1,15 @@
1
- import { styleInject } from '../../../../_virtual/_style-inject.esm.js';
2
-
3
- var css = "._col-start-1_1myqa_1 {\n grid-column-start: 1;\n}\n\n._col-start-2_1myqa_5 {\n grid-column-start: 2;\n}\n\n._col-start-3_1myqa_9 {\n grid-column-start: 3;\n}\n\n._col-start-4_1myqa_13 {\n grid-column-start: 4;\n}\n\n._col-start-5_1myqa_17 {\n grid-column-start: 5;\n}\n\n._col-start-6_1myqa_21 {\n grid-column-start: 6;\n}\n\n._col-start-7_1myqa_25 {\n grid-column-start: 7;\n}\n\n._col-start-8_1myqa_29 {\n grid-column-start: 8;\n}\n\n._col-start-9_1myqa_33 {\n grid-column-start: 9;\n}\n\n._col-start-10_1myqa_37 {\n grid-column-start: 10;\n}\n\n._col-start-11_1myqa_41 {\n grid-column-start: 11;\n}\n\n._col-start-12_1myqa_45 {\n grid-column-start: 12;\n}\n\n._col-end-1_1myqa_49 {\n grid-column-end: 1;\n}\n\n._col-end-2_1myqa_53 {\n grid-column-end: 2;\n}\n\n._col-end-3_1myqa_57 {\n grid-column-end: 3;\n}\n\n._col-end-4_1myqa_61 {\n grid-column-end: 4;\n}\n\n._col-end-5_1myqa_65 {\n grid-column-end: 5;\n}\n\n._col-end-6_1myqa_69 {\n grid-column-end: 6;\n}\n\n._col-end-7_1myqa_73 {\n grid-column-end: 7;\n}\n\n._col-end-8_1myqa_77 {\n grid-column-end: 8;\n}\n\n._col-end-9_1myqa_81 {\n grid-column-end: 9;\n}\n\n._col-end-10_1myqa_85 {\n grid-column-end: 10;\n}\n\n._col-end-11_1myqa_89 {\n grid-column-end: 11;\n}\n\n._col-end-12_1myqa_93 {\n grid-column-end: 12;\n}\n\n._col-end-13_1myqa_97 {\n grid-column-end: 13;\n}\n";
4
-
5
- styleInject(css);
6
- var styles = {"col-start-1":"_col-start-1_1myqa_1","col-start-2":"_col-start-2_1myqa_5","col-start-3":"_col-start-3_1myqa_9","col-start-4":"_col-start-4_1myqa_13","col-start-5":"_col-start-5_1myqa_17","col-start-6":"_col-start-6_1myqa_21","col-start-7":"_col-start-7_1myqa_25","col-start-8":"_col-start-8_1myqa_29","col-start-9":"_col-start-9_1myqa_33","col-start-10":"_col-start-10_1myqa_37","col-start-11":"_col-start-11_1myqa_41","col-start-12":"_col-start-12_1myqa_45","col-end-1":"_col-end-1_1myqa_49","col-end-2":"_col-end-2_1myqa_53","col-end-3":"_col-end-3_1myqa_57","col-end-4":"_col-end-4_1myqa_61","col-end-5":"_col-end-5_1myqa_65","col-end-6":"_col-end-6_1myqa_69","col-end-7":"_col-end-7_1myqa_73","col-end-8":"_col-end-8_1myqa_77","col-end-9":"_col-end-9_1myqa_81","col-end-10":"_col-end-10_1myqa_85","col-end-11":"_col-end-11_1myqa_89","col-end-12":"_col-end-12_1myqa_93","col-end-13":"_col-end-13_1myqa_97"};
1
+ var css_248z = ".Column-module_col-start-1__ZjXIT {\n grid-column-start: 1;\n}\n\n.Column-module_col-start-2__pKXk4 {\n grid-column-start: 2;\n}\n\n.Column-module_col-start-3__9jDkG {\n grid-column-start: 3;\n}\n\n.Column-module_col-start-4__nKEgb {\n grid-column-start: 4;\n}\n\n.Column-module_col-start-5__ybSmQ {\n grid-column-start: 5;\n}\n\n.Column-module_col-start-6__oMag- {\n grid-column-start: 6;\n}\n\n.Column-module_col-start-7__B3Gyy {\n grid-column-start: 7;\n}\n\n.Column-module_col-start-8__jLDTT {\n grid-column-start: 8;\n}\n\n.Column-module_col-start-9__CtwzK {\n grid-column-start: 9;\n}\n\n.Column-module_col-start-10__7wGuq {\n grid-column-start: 10;\n}\n\n.Column-module_col-start-11__hFAC7 {\n grid-column-start: 11;\n}\n\n.Column-module_col-start-12__yl9H8 {\n grid-column-start: 12;\n}\n\n.Column-module_col-end-1__8NXpa {\n grid-column-end: 1;\n}\n\n.Column-module_col-end-2__PNIHf {\n grid-column-end: 2;\n}\n\n.Column-module_col-end-3__tYgN- {\n grid-column-end: 3;\n}\n\n.Column-module_col-end-4__kfbI3 {\n grid-column-end: 4;\n}\n\n.Column-module_col-end-5__E8Gsl {\n grid-column-end: 5;\n}\n\n.Column-module_col-end-6__SpIXw {\n grid-column-end: 6;\n}\n\n.Column-module_col-end-7__0nOh1 {\n grid-column-end: 7;\n}\n\n.Column-module_col-end-8__iOzUU {\n grid-column-end: 8;\n}\n\n.Column-module_col-end-9__xSt6O {\n grid-column-end: 9;\n}\n\n.Column-module_col-end-10__naTtw {\n grid-column-end: 10;\n}\n\n.Column-module_col-end-11__b6utR {\n grid-column-end: 11;\n}\n\n.Column-module_col-end-12__D18ty {\n grid-column-end: 12;\n}\n\n.Column-module_col-end-13__5rTLe {\n grid-column-end: 13;\n}\n";
2
+ var styles = {"col-start-1":"Column-module_col-start-1__ZjXIT","col-start-2":"Column-module_col-start-2__pKXk4","col-start-3":"Column-module_col-start-3__9jDkG","col-start-4":"Column-module_col-start-4__nKEgb","col-start-5":"Column-module_col-start-5__ybSmQ","col-start-6":"Column-module_col-start-6__oMag-","col-start-7":"Column-module_col-start-7__B3Gyy","col-start-8":"Column-module_col-start-8__jLDTT","col-start-9":"Column-module_col-start-9__CtwzK","col-start-10":"Column-module_col-start-10__7wGuq","col-start-11":"Column-module_col-start-11__hFAC7","col-start-12":"Column-module_col-start-12__yl9H8","col-end-1":"Column-module_col-end-1__8NXpa","col-end-2":"Column-module_col-end-2__PNIHf","col-end-3":"Column-module_col-end-3__tYgN-","col-end-4":"Column-module_col-end-4__kfbI3","col-end-5":"Column-module_col-end-5__E8Gsl","col-end-6":"Column-module_col-end-6__SpIXw","col-end-7":"Column-module_col-end-7__0nOh1","col-end-8":"Column-module_col-end-8__iOzUU","col-end-9":"Column-module_col-end-9__xSt6O","col-end-10":"Column-module_col-end-10__naTtw","col-end-11":"Column-module_col-end-11__b6utR","col-end-12":"Column-module_col-end-12__D18ty","col-end-13":"Column-module_col-end-13__5rTLe"};
3
+ (function () {
4
+ if (typeof document === 'undefined') return;
5
+ var existing = document.head.querySelectorAll('style[data-dotcms-style]');
6
+ for (var i = 0; i < existing.length; i++) {
7
+ if (existing[i].textContent === css_248z) return;
8
+ }
9
+ var style = document.createElement('style');
10
+ style.setAttribute('data-dotcms-style', '');
11
+ style.appendChild(document.createTextNode(css_248z));
12
+ document.head.appendChild(style);
13
+ })();
7
14
 
8
15
  export { styles as default };
@@ -1,11 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, Fragment } from 'react/jsx-runtime';
3
- import { useRef, useMemo, useContext } from 'react';
3
+ import { useRef, useContext, useMemo, Suspense } from 'react';
4
4
  import { getDotContentletAttributes, getAnalyticsContentletAttributes, CUSTOM_NO_COMPONENT } from '@dotcms/uve/internal';
5
5
  import { DotCMSPageContext } from '../../contexts/DotCMSPageContext.esm.js';
6
6
  import { useCheckVisibleContent } from '../../hooks/useCheckVisibleContent.esm.js';
7
- import { useIsAnalyticsActive } from '../../hooks/useIsAnalyticsActive.esm.js';
8
- import { useIsDevMode } from '../../hooks/useIsDevMode.esm.js';
9
7
  import { FallbackComponent } from '../FallbackComponent/FallbackComponent.esm.js';
10
8
 
11
9
  /**
@@ -34,9 +32,16 @@ function Contentlet({
34
32
  container
35
33
  }) {
36
34
  const ref = useRef(null);
37
- const isDevMode = useIsDevMode();
38
- const isAnalyticsActive = useIsAnalyticsActive();
39
- const haveContent = useCheckVisibleContent(ref);
35
+ // Both flags are resolved once per layout tree by DotCMSPageProvider. Reading them from
36
+ // context keeps this component free of its own UVE lookup and analytics listener, which
37
+ // previously ran once per contentlet on the page.
38
+ const {
39
+ isDevMode,
40
+ isAnalyticsActive
41
+ } = useContext(DotCMSPageContext);
42
+ // The measurement only feeds the editor's empty-contentlet placeholder, so skip the
43
+ // forced layout entirely outside development mode.
44
+ const haveContent = useCheckVisibleContent(ref, isDevMode);
40
45
  const style = useMemo(() => isDevMode ? {
41
46
  minHeight: haveContent ? undefined : '4rem'
42
47
  } : {}, [isDevMode, haveContent]);
@@ -58,8 +63,11 @@ function Contentlet({
58
63
  className: CONTENTLET_CLASS,
59
64
  ref: ref,
60
65
  style: style,
61
- children: jsx(CustomComponent, {
62
- contentlet: contentlet
66
+ children: jsx(Suspense, {
67
+ fallback: null,
68
+ children: jsx(CustomComponent, {
69
+ contentlet: contentlet
70
+ })
63
71
  })
64
72
  }));
65
73
  }
@@ -1,12 +1,17 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { Editor } from '@tinymce/tinymce-react';
3
- import { useRef, useState, useEffect } from 'react';
2
+ import { useRef, useState, useEffect, Suspense, lazy } from 'react';
4
3
  import { UVE_MODE, DotCMSUVEAction } from '@dotcms/types';
5
4
  import { __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal';
6
5
  import { getUVEState, sendMessageToUVE } from '@dotcms/uve';
7
6
  import { __TINYMCE_PATH_ON_DOTCMS__ } from '@dotcms/uve/internal';
8
- import { TINYMCE_CONFIG } from './utils.esm.js';
9
7
 
8
+ /**
9
+ * TinyMCE is only ever rendered once the UVE is in edit mode, so the integration is split
10
+ * into its own chunk and fetched at that point. Importing it statically put the whole editor
11
+ * — and the TinyMCE React wrapper — into every consumer bundle, including live-mode pages
12
+ * that can never show it.
13
+ */
14
+ const TinyMCEEditor = /*#__PURE__*/lazy(() => import('./TinyMCEEditor.esm.js'));
10
15
  /**
11
16
  * Allows inline edit content pulled from dotCMS API using TinyMCE editor
12
17
  *
@@ -171,14 +176,20 @@ function DotCMSEditableText({
171
176
  outline: '2px solid #006ce7',
172
177
  borderRadius: '4px'
173
178
  },
174
- children: jsx(Editor, {
175
- tinymceScriptSrc: scriptSrc,
176
- inline: true,
177
- onInit: (_, editor) => editorRef.current = editor,
178
- init: TINYMCE_CONFIG[mode],
179
- initialValue: content,
180
- onMouseDown: onMouseDown,
181
- onFocusOut: onFocusOut
179
+ children: jsx(Suspense, {
180
+ fallback: jsx("span", {
181
+ dangerouslySetInnerHTML: {
182
+ __html: content
183
+ }
184
+ }),
185
+ children: jsx(TinyMCEEditor, {
186
+ scriptSrc: scriptSrc,
187
+ mode: mode,
188
+ initialValue: content,
189
+ onEditorInit: editor => editorRef.current = editor,
190
+ onMouseDown: onMouseDown,
191
+ onFocusOut: onFocusOut
192
+ })
182
193
  })
183
194
  });
184
195
  }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { Editor } from '@tinymce/tinymce-react';
4
+ import { TINYMCE_CONFIG } from './utils.esm.js';
5
+
6
+ /**
7
+ * @internal
8
+ *
9
+ * Thin wrapper around `@tinymce/tinymce-react`, kept in its own module so it is the *only*
10
+ * place that imports the TinyMCE integration. `DotCMSEditableText` pulls it in with a dynamic
11
+ * import once the UVE actually enters edit mode, so live-mode consumers never download it.
12
+ *
13
+ * Do not import this module statically from anywhere else — doing so puts TinyMCE back in
14
+ * the main bundle and undoes the split.
15
+ */
16
+ function TinyMCEEditor({
17
+ scriptSrc,
18
+ mode,
19
+ initialValue,
20
+ onEditorInit,
21
+ onMouseDown,
22
+ onFocusOut
23
+ }) {
24
+ return jsx(Editor, {
25
+ tinymceScriptSrc: scriptSrc,
26
+ inline: true,
27
+ onInit: (_, editor) => onEditorInit(editor),
28
+ init: TINYMCE_CONFIG[mode],
29
+ initialValue: initialValue,
30
+ onMouseDown: onMouseDown,
31
+ onFocusOut: onFocusOut
32
+ });
33
+ }
34
+
35
+ export { TinyMCEEditor, TinyMCEEditor as default };
@@ -20,11 +20,18 @@ import { Row } from '../Row/Row.esm.js';
20
20
  * @returns {JSX.Element} The rendered DotCMS page body or an error message if the layout body is missing.
21
21
  *
22
22
  */
23
+ /**
24
+ * Hoisted so the defaults keep a stable identity across renders. As inline `= {}` defaults
25
+ * they produced a fresh object on every render, which invalidated the page context's useMemo
26
+ * and re-rendered every container and contentlet in the tree.
27
+ */
28
+ const NO_COMPONENTS = {};
29
+ const NO_SLOTS = {};
23
30
  const DotCMSLayoutBody = ({
24
31
  page,
25
- components: _components = {},
32
+ components: _components = NO_COMPONENTS,
26
33
  mode: _mode = 'production',
27
- slots: _slots = {}
34
+ slots: _slots = NO_SLOTS
28
35
  }) => {
29
36
  var _page$layout;
30
37
  const dotCMSPageBody = page == null || (_page$layout = page.layout) == null ? void 0 : _page$layout.body;
@@ -1,6 +1,9 @@
1
1
  "use client";
2
2
  import { jsx } from 'react/jsx-runtime';
3
+ import { useMemo } from 'react';
3
4
  import { DotCMSPageContext } from '../../contexts/DotCMSPageContext.esm.js';
5
+ import { useIsAnalyticsActive } from '../../hooks/useIsAnalyticsActive.esm.js';
6
+ import { useResolvedDevMode } from '../../hooks/useIsDevMode.esm.js';
4
7
 
5
8
  /**
6
9
  * @internal
@@ -8,6 +11,11 @@ import { DotCMSPageContext } from '../../contexts/DotCMSPageContext.esm.js';
8
11
  * Client boundary that provides the DotCMS page context to the layout tree.
9
12
  * Keeping this separate from DotCMSLayoutBody allows the layout to remain
10
13
  * a server component while only the context provider runs on the client.
14
+ *
15
+ * Development mode and the Analytics-active flag are resolved here, once per layout tree,
16
+ * and shared through the context. Resolving them per contentlet meant one
17
+ * `dotcms:analytics:ready` window listener and one UVE-state lookup for every piece of
18
+ * content on the page.
11
19
  */
12
20
  function DotCMSPageProvider({
13
21
  page,
@@ -16,13 +24,18 @@ function DotCMSPageProvider({
16
24
  slots,
17
25
  children
18
26
  }) {
27
+ const isDevMode = useResolvedDevMode(mode);
28
+ const isAnalyticsActive = useIsAnalyticsActive();
29
+ const value = useMemo(() => ({
30
+ pageAsset: page,
31
+ userComponents: components,
32
+ mode,
33
+ slots,
34
+ isDevMode,
35
+ isAnalyticsActive
36
+ }), [page, components, mode, slots, isDevMode, isAnalyticsActive]);
19
37
  return jsx(DotCMSPageContext.Provider, {
20
- value: {
21
- pageAsset: page,
22
- userComponents: components,
23
- mode,
24
- slots
25
- },
38
+ value: value,
26
39
  children: children
27
40
  });
28
41
  }
@@ -1,8 +1,15 @@
1
- import { styleInject } from '../../../../_virtual/_style-inject.esm.js';
2
-
3
- var css = "._row_1e5l5_1 {\n display: grid;\n grid-template-columns: repeat(12, 1fr);\n gap: 1rem;\n}\n";
4
-
5
- styleInject(css);
6
- var styles = {"row":"_row_1e5l5_1"};
1
+ var css_248z = ".Row-module_row__fJF2K {\n display: grid;\n grid-template-columns: repeat(12, 1fr);\n gap: 1rem;\n}\n";
2
+ var styles = {"row":"Row-module_row__fJF2K"};
3
+ (function () {
4
+ if (typeof document === 'undefined') return;
5
+ var existing = document.head.querySelectorAll('style[data-dotcms-style]');
6
+ for (var i = 0; i < existing.length; i++) {
7
+ if (existing[i].textContent === css_248z) return;
8
+ }
9
+ var style = document.createElement('style');
10
+ style.setAttribute('data-dotcms-style', '');
11
+ style.appendChild(document.createTextNode(css_248z));
12
+ document.head.appendChild(style);
13
+ })();
7
14
 
8
15
  export { styles as default };
@@ -10,7 +10,9 @@ const DotCMSPageContext = /*#__PURE__*/createContext({
10
10
  pageAsset: undefined,
11
11
  mode: 'production',
12
12
  userComponents: {},
13
- slots: {}
13
+ slots: {},
14
+ isDevMode: false,
15
+ isAnalyticsActive: false
14
16
  });
15
17
 
16
18
  export { DotCMSPageContext };
@@ -4,7 +4,13 @@ import { useState, useLayoutEffect } from 'react';
4
4
  * @internal
5
5
  * A custom React hook that checks whether a referenced HTMLDivElement has visible content based on its height.
6
6
  *
7
+ * The measurement is only used to reserve space for the editor's empty-contentlet placeholder,
8
+ * so it is gated behind `enabled`. `getBoundingClientRect()` forces a synchronous layout, and
9
+ * running it from a layout effect on every contentlet of a page is expensive — in production
10
+ * the result was measured and then discarded.
11
+ *
7
12
  * @param {RefObject<HTMLDivElement>} ref - A React ref object pointing to an HTMLDivElement.
13
+ * @param {boolean} [enabled=true] - When false the element is never measured and the hook returns false.
8
14
  * @returns {boolean} - Returns true if the element's height is greater than zero (indicating visible content), otherwise false.
9
15
  *
10
16
  * @example
@@ -22,9 +28,12 @@ import { useState, useLayoutEffect } from 'react';
22
28
  * );
23
29
  * }
24
30
  */
25
- const useCheckVisibleContent = ref => {
31
+ const useCheckVisibleContent = (ref, enabled = true) => {
26
32
  const [haveContent, setHaveContent] = useState(false);
27
33
  useLayoutEffect(() => {
34
+ if (!enabled) {
35
+ return;
36
+ }
28
37
  if (!ref.current) {
29
38
  setHaveContent(false);
30
39
  return;
@@ -33,7 +42,7 @@ const useCheckVisibleContent = ref => {
33
42
  height
34
43
  } = ref.current.getBoundingClientRect();
35
44
  setHaveContent(height > 0);
36
- }, [ref]);
45
+ }, [ref, enabled]);
37
46
  return haveContent;
38
47
  };
39
48
 
@@ -1,4 +1,5 @@
1
- import { useContext, useState, useEffect } from 'react';
1
+ "use client";
2
+ import { useState, useEffect, useContext } from 'react';
2
3
  import { UVE_MODE } from '@dotcms/types';
3
4
  import { getUVEState } from '@dotcms/uve';
4
5
  import { DEVELOPMENT_MODE } from '@dotcms/uve/internal';
@@ -6,30 +7,60 @@ import { DotCMSPageContext } from '../contexts/DotCMSPageContext.esm.js';
6
7
 
7
8
  /**
8
9
  * @internal
10
+ *
11
+ * Resolve whether we are rendering in "development" mode — i.e. whether editor metadata
12
+ * (`data-dot-*` attributes, empty-state placeholders, fallback components) should be emitted.
13
+ * Inside the UVE it follows the UVE state (dev when the mode is EDIT); otherwise it follows
14
+ * the renderer `mode` passed to `DotCMSLayoutBody`.
15
+ *
16
+ * @param mode the renderer mode from the page context
17
+ * @returns `true` when editor metadata should be emitted
18
+ */
19
+ const resolveDevMode = mode => {
20
+ var _getUVEState;
21
+ const uveMode = (_getUVEState = getUVEState()) == null ? void 0 : _getUVEState.mode;
22
+ if (uveMode) {
23
+ return uveMode === UVE_MODE.EDIT;
24
+ }
25
+ return mode === DEVELOPMENT_MODE;
26
+ };
27
+ /**
28
+ * @internal
29
+ *
30
+ * Owns the development-mode state for one layout tree. Called once by `DotCMSPageProvider`;
31
+ * the result travels down through the page context.
32
+ *
33
+ * `getUVEState()` reads the browser, so it is deliberately resolved in an effect rather than
34
+ * during render: on the server it would always report "not in the editor", and resolving it
35
+ * synchronously on the client would make the first render disagree with the server-rendered
36
+ * markup and trip React's hydration check inside the UVE.
37
+ *
38
+ * @param mode the renderer mode passed to `DotCMSLayoutBody`
39
+ * @returns `true` when editor metadata should be emitted
40
+ */
41
+ const useResolvedDevMode = mode => {
42
+ const [isDevMode, setIsDevMode] = useState(mode === DEVELOPMENT_MODE);
43
+ useEffect(() => {
44
+ setIsDevMode(resolveDevMode(mode));
45
+ }, [mode]);
46
+ return isDevMode;
47
+ };
48
+ /**
49
+ * @internal
50
+ *
9
51
  * A React hook that determines if the current environment is in development mode.
10
52
  *
11
- * The hook returns `true` if either:
12
- * - The application is running inside the DotCMS editor (as determined by `getUVEState()`).
53
+ * The value is resolved once per layout tree by `DotCMSPageProvider` and shared through the
54
+ * page context, so a page with many containers and contentlets resolves it a single time
55
+ * instead of once per component.
13
56
  *
14
57
  * @returns {boolean} - `true` if in development mode or inside the editor; otherwise, `false`.
15
58
  */
16
59
  const useIsDevMode = () => {
17
60
  const {
18
- mode
61
+ isDevMode
19
62
  } = useContext(DotCMSPageContext);
20
- const [isDevMode, setIsDevMode] = useState(mode === 'development');
21
- useEffect(() => {
22
- var _getUVEState;
23
- // Inside UVE we rely on the UVE state to determine if we are in development mode
24
- if ((_getUVEState = getUVEState()) != null && _getUVEState.mode) {
25
- var _getUVEState2;
26
- const isUVEInEditor = ((_getUVEState2 = getUVEState()) == null ? void 0 : _getUVEState2.mode) === UVE_MODE.EDIT;
27
- setIsDevMode(isUVEInEditor);
28
- return;
29
- }
30
- setIsDevMode(mode === DEVELOPMENT_MODE);
31
- }, [mode]);
32
63
  return isDevMode;
33
64
  };
34
65
 
35
- export { useIsDevMode };
66
+ export { resolveDevMode, useIsDevMode, useResolvedDevMode };
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@dotcms/react",
3
- "version": "26.9.18-1",
3
+ "version": "26.9.23-1",
4
4
  "peerDependencies": {
5
5
  "react": ">=18",
6
6
  "react-dom": ">=18",
7
- "@dotcms/uve": "26.9.18-1",
8
- "@dotcms/client": "26.9.18-1"
7
+ "@dotcms/uve": "26.9.23-1",
8
+ "@dotcms/client": "26.9.23-1",
9
+ "@dotcms/types": "26.9.23-1"
9
10
  },
10
11
  "dependencies": {
11
12
  "@tinymce/tinymce-react": "6.2.1"
12
13
  },
13
14
  "devDependencies": {
14
- "@dotcms/types": "26.9.18-1"
15
+ "@dotcms/types": "26.9.23-1"
15
16
  },
16
17
  "description": "Official React Components library to render a dotCMS page.",
17
18
  "repository": {
@@ -30,15 +31,15 @@
30
31
  "exports": {
31
32
  "./package.json": "./package.json",
32
33
  ".": {
34
+ "types": "./index.d.ts",
33
35
  "react-server": "./index.server.esm.js",
34
- "import": "./index.esm.js",
35
- "types": "./index.d.ts"
36
+ "import": "./index.esm.js"
36
37
  }
37
38
  },
38
39
  "typesVersions": {
39
40
  "*": {
40
41
  ".": [
41
- "./src/index.d.ts"
42
+ "./index.d.ts"
42
43
  ]
43
44
  }
44
45
  },
@@ -48,8 +49,12 @@
48
49
  "url": "https://github.com/dotCMS/core/issues"
49
50
  },
50
51
  "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/react/README.md",
52
+ "sideEffects": [
53
+ "**/*.css",
54
+ "**/*.css.esm.js"
55
+ ],
51
56
  "module": "./index.esm.js",
52
57
  "type": "module",
53
58
  "main": "./index.esm.js",
54
59
  "types": "./index.d.ts"
55
- }
60
+ }
@@ -0,0 +1,35 @@
1
+ import { Editor } from '@tinymce/tinymce-react';
2
+ import { DOT_EDITABLE_TEXT_MODE } from './utils';
3
+ /**
4
+ * @internal
5
+ *
6
+ * The TinyMCE editor instance handed back by `onInit`.
7
+ */
8
+ export type TinyMCEEditorInstance = NonNullable<Editor['editor']>;
9
+ /**
10
+ * @internal
11
+ */
12
+ export interface TinyMCEEditorProps {
13
+ /** URL of the TinyMCE script served by the dotCMS host. */
14
+ scriptSrc: string;
15
+ /** Toolbar preset to load: `plain`, `minimal` or `full`. */
16
+ mode: DOT_EDITABLE_TEXT_MODE;
17
+ /** Field content to seed the editor with. */
18
+ initialValue: string;
19
+ /** Called once the editor is ready, with the live editor instance. */
20
+ onEditorInit: (editor: TinyMCEEditorInstance) => void;
21
+ onMouseDown: (event: MouseEvent) => void;
22
+ onFocusOut: () => void;
23
+ }
24
+ /**
25
+ * @internal
26
+ *
27
+ * Thin wrapper around `@tinymce/tinymce-react`, kept in its own module so it is the *only*
28
+ * place that imports the TinyMCE integration. `DotCMSEditableText` pulls it in with a dynamic
29
+ * import once the UVE actually enters edit mode, so live-mode consumers never download it.
30
+ *
31
+ * Do not import this module statically from anywhere else — doing so puts TinyMCE back in
32
+ * the main bundle and undoes the split.
33
+ */
34
+ export declare function TinyMCEEditor({ scriptSrc, mode, initialValue, onEditorInit, onMouseDown, onFocusOut }: Readonly<TinyMCEEditorProps>): import("react/jsx-runtime").JSX.Element;
35
+ export default TinyMCEEditor;
@@ -1,5 +1,5 @@
1
- import { IAllProps } from '@tinymce/tinymce-react';
2
1
  import { DotCMSBasicContentlet } from '@dotcms/types';
2
+ import type { IAllProps } from '@tinymce/tinymce-react';
3
3
  export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text';
4
4
  export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain';
5
5
  export interface DotCMSEditableTextProps<T extends DotCMSBasicContentlet> {
@@ -29,21 +29,4 @@ export interface DotCMSLayoutBodyProps<TContentlet extends DotCMSBasicContentlet
29
29
  */
30
30
  slots?: Record<string, ReactNode>;
31
31
  }
32
- /**
33
- * DotCMSLayoutBody component renders the layout body for a DotCMS page.
34
- *
35
- * It utilizes the dotCMS page asset's layout body to render the page body.
36
- * If the layout body does not exist, it renders an error message in the mode is `development`.
37
- *
38
- * @public
39
- * @component
40
- * @param {Object} props - Component properties.
41
- * @param {DotCMSPageAsset} props.page - The DotCMS page asset containing the layout information.
42
- * @param {Record<string, React.ComponentType<DotCMSContentlet>>} [props.components] - mapping of custom components for content rendering.
43
- * @param {DotCMSPageRendererMode} [props.mode='production'] - The renderer mode; defaults to 'production'. Alternate modes might trigger different behaviors.
44
- * @param {Record<string, ReactNode>} [props.slots] - Pre-rendered server component nodes keyed by contentlet identifier.
45
- *
46
- * @returns {JSX.Element} The rendered DotCMS page body or an error message if the layout body is missing.
47
- *
48
- */
49
32
  export declare const DotCMSLayoutBody: ({ page, components, mode, slots }: DotCMSLayoutBodyProps) => import("react/jsx-runtime").JSX.Element;
@@ -13,6 +13,11 @@ interface DotCMSPageProviderProps {
13
13
  * Client boundary that provides the DotCMS page context to the layout tree.
14
14
  * Keeping this separate from DotCMSLayoutBody allows the layout to remain
15
15
  * a server component while only the context provider runs on the client.
16
+ *
17
+ * Development mode and the Analytics-active flag are resolved here, once per layout tree,
18
+ * and shared through the context. Resolving them per contentlet meant one
19
+ * `dotcms:analytics:ready` window listener and one UVE-state lookup for every piece of
20
+ * content on the page.
16
21
  */
17
22
  export declare function DotCMSPageProvider({ page, components, mode, slots, children }: DotCMSPageProviderProps): import("react/jsx-runtime").JSX.Element;
18
23
  export {};
@@ -19,6 +19,17 @@ export interface DotCMSPageContextProps {
19
19
  mode: DotCMSPageRendererMode;
20
20
  userComponents: Record<string, React.ComponentType<DotCMSBasicContentlet>>;
21
21
  slots?: Record<string, ReactNode>;
22
+ /**
23
+ * Whether editor metadata (`data-dot-*` attributes, placeholders, fallbacks) should be
24
+ * emitted. Resolved once at the layout root and shared with the whole tree so it isn't
25
+ * recomputed by every container and contentlet.
26
+ */
27
+ isDevMode: boolean;
28
+ /**
29
+ * Whether dotCMS Analytics is active. Resolved once at the layout root — a single
30
+ * `dotcms:analytics:ready` listener for the tree instead of one per contentlet.
31
+ */
32
+ isAnalyticsActive: boolean;
22
33
  }
23
34
  /**
24
35
  * The `PageContext` is a React context that provides access to the DotCMS page context.
@@ -3,7 +3,13 @@ import { RefObject } from 'react';
3
3
  * @internal
4
4
  * A custom React hook that checks whether a referenced HTMLDivElement has visible content based on its height.
5
5
  *
6
+ * The measurement is only used to reserve space for the editor's empty-contentlet placeholder,
7
+ * so it is gated behind `enabled`. `getBoundingClientRect()` forces a synchronous layout, and
8
+ * running it from a layout effect on every contentlet of a page is expensive — in production
9
+ * the result was measured and then discarded.
10
+ *
6
11
  * @param {RefObject<HTMLDivElement>} ref - A React ref object pointing to an HTMLDivElement.
12
+ * @param {boolean} [enabled=true] - When false the element is never measured and the hook returns false.
7
13
  * @returns {boolean} - Returns true if the element's height is greater than zero (indicating visible content), otherwise false.
8
14
  *
9
15
  * @example
@@ -21,4 +27,4 @@ import { RefObject } from 'react';
21
27
  * );
22
28
  * }
23
29
  */
24
- export declare const useCheckVisibleContent: (ref: RefObject<HTMLDivElement>) => boolean;
30
+ export declare const useCheckVisibleContent: (ref: RefObject<HTMLDivElement>, enabled?: boolean) => boolean;
@@ -1,9 +1,39 @@
1
+ import { DotCMSPageRendererMode } from '@dotcms/types';
1
2
  /**
2
3
  * @internal
4
+ *
5
+ * Resolve whether we are rendering in "development" mode — i.e. whether editor metadata
6
+ * (`data-dot-*` attributes, empty-state placeholders, fallback components) should be emitted.
7
+ * Inside the UVE it follows the UVE state (dev when the mode is EDIT); otherwise it follows
8
+ * the renderer `mode` passed to `DotCMSLayoutBody`.
9
+ *
10
+ * @param mode the renderer mode from the page context
11
+ * @returns `true` when editor metadata should be emitted
12
+ */
13
+ export declare const resolveDevMode: (mode: DotCMSPageRendererMode | undefined) => boolean;
14
+ /**
15
+ * @internal
16
+ *
17
+ * Owns the development-mode state for one layout tree. Called once by `DotCMSPageProvider`;
18
+ * the result travels down through the page context.
19
+ *
20
+ * `getUVEState()` reads the browser, so it is deliberately resolved in an effect rather than
21
+ * during render: on the server it would always report "not in the editor", and resolving it
22
+ * synchronously on the client would make the first render disagree with the server-rendered
23
+ * markup and trip React's hydration check inside the UVE.
24
+ *
25
+ * @param mode the renderer mode passed to `DotCMSLayoutBody`
26
+ * @returns `true` when editor metadata should be emitted
27
+ */
28
+ export declare const useResolvedDevMode: (mode: DotCMSPageRendererMode | undefined) => boolean;
29
+ /**
30
+ * @internal
31
+ *
3
32
  * A React hook that determines if the current environment is in development mode.
4
33
  *
5
- * The hook returns `true` if either:
6
- * - The application is running inside the DotCMS editor (as determined by `getUVEState()`).
34
+ * The value is resolved once per layout tree by `DotCMSPageProvider` and shared through the
35
+ * page context, so a page with many containers and contentlets resolves it a single time
36
+ * instead of once per component.
7
37
  *
8
38
  * @returns {boolean} - `true` if in development mode or inside the editor; otherwise, `false`.
9
39
  */
@@ -1,4 +1,4 @@
1
- import { createDotCMSClient } from '@dotcms/client';
1
+ import type { createDotCMSClient } from '@dotcms/client';
2
2
  import { DotCMSAISearchContentletData, DotCMSAISearchParams, DotCMSAISearchResponse, DotCMSBasicContentlet, DotCMSEntityStatus } from '@dotcms/types';
3
3
  /**
4
4
  * Return type of the AI Search context
@@ -1,30 +0,0 @@
1
- function styleInject(css, ref) {
2
- if (ref === void 0) ref = {};
3
- var insertAt = ref.insertAt;
4
-
5
- if (typeof document === 'undefined') {
6
- return;
7
- }
8
-
9
- var head = document.head || document.getElementsByTagName('head')[0];
10
- var style = document.createElement('style');
11
- style.type = 'text/css';
12
-
13
- if (insertAt === 'top') {
14
- if (head.firstChild) {
15
- head.insertBefore(style, head.firstChild);
16
- } else {
17
- head.appendChild(style);
18
- }
19
- } else {
20
- head.appendChild(style);
21
- }
22
-
23
- if (style.styleSheet) {
24
- style.styleSheet.cssText = css;
25
- } else {
26
- style.appendChild(document.createTextNode(css));
27
- }
28
- }
29
-
30
- export { styleInject as default, styleInject };