@capillarytech/creatives-library 8.0.292-alpha.10 → 8.0.292-alpha.12

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.292-alpha.10",
4
+ "version": "8.0.292-alpha.12",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -113,6 +113,8 @@ const HTMLEditor = forwardRef(({
113
113
  const mainEditorRef = useRef(null);
114
114
  const modalEditorRef = useRef(null);
115
115
  const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
116
+ // InApp only: last initialContent value we applied (by value) to avoid reverting user edits on parent re-render
117
+ const inAppLastAppliedInitialRef = useRef({ android: '', ios: '' });
116
118
 
117
119
  // Get the currently active editor ref based on fullscreen state
118
120
  const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
@@ -174,19 +176,22 @@ const HTMLEditor = forwardRef(({
174
176
  const contentToUpdate = typeof initialContent === 'string'
175
177
  ? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
176
178
  : initialContent;
177
- if (inAppContent.updateContent) {
178
- const activeDevice = inAppContent.activeDevice;
179
- const currentContent = inAppContent.getDeviceContent?.(activeDevice);
180
- // Use active device's value only; empty string is valid (don't fall back to other device)
181
- const newContent = activeDevice in contentToUpdate
182
- ? (contentToUpdate[activeDevice] ?? '')
183
- : (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
184
- if (currentContent !== newContent) {
185
- inAppContent.updateContent(newContent, true);
186
- }
187
- }
179
+ const newAndroid = contentToUpdate[DEVICE_TYPES.ANDROID] ?? '';
180
+ const newIos = contentToUpdate[DEVICE_TYPES.IOS] ?? '';
181
+ const last = inAppLastAppliedInitialRef.current;
182
+ // InApp: only sync when initialContent value actually changed (first load or template switch)
183
+ // so we don't revert user typing when parent re-renders with same content (new ref)
184
+ const valueChanged = newAndroid !== (last.android ?? '') || newIos !== (last.ios ?? '');
185
+ if (!valueChanged) return;
186
+ if (!inAppContent.updateContent) return;
187
+ const activeDevice = inAppContent.activeDevice;
188
+ const newContent = activeDevice in contentToUpdate
189
+ ? (contentToUpdate[activeDevice] ?? '')
190
+ : (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
191
+ inAppContent.updateContent(newContent, true);
192
+ inAppLastAppliedInitialRef.current = { android: newAndroid, ios: newIos };
188
193
  }
189
- }, [initialContent, isEmailVariant, isInAppVariant]);
194
+ }, [initialContent, isEmailVariant, isInAppVariant, emailContent, inAppContent]);
190
195
  // Handle context change for tag API calls
191
196
  // If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
192
197
  const handleContextChange = useCallback((contextData) => {
@@ -178,8 +178,6 @@
178
178
  // Pin toolbar to the right and reserve space so it does not overlap DeviceToggle (iOS tab must stay clickable)
179
179
  &.html-editor--library-mode {
180
180
  .html-editor__header {
181
- position: relative;
182
- padding-right: 14rem; // Reserve space for toolbar so DeviceToggle (flex: 1) doesn't extend under it
183
181
 
184
182
  .editor-toolbar,
185
183
  .editor-toolbar.editor-toolbar,
@@ -187,6 +185,7 @@
187
185
  position: absolute;
188
186
  top: 0;
189
187
  right: 0;
188
+ width: unset;
190
189
  }
191
190
  }
192
191
  }
@@ -95,52 +95,51 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
95
95
  const previousContentRef = useRef({ android: '', ios: '' });
96
96
 
97
97
  // Update content when initialContent prop changes (for edit mode)
98
- // This should only run when loading a template for editing, NOT during active editing
98
+ // Only update when: first load, empty->content transition, or template switch (value changed).
99
+ // Value-based comparison prevents reverting user edits when parent re-renders with same content (new ref).
99
100
  useEffect(() => {
100
101
  const newAndroidContent = initialContent?.[ANDROID];
101
102
  const newIosContent = initialContent?.[IOS];
103
+ const prevRef = previousContentRef.current;
102
104
 
103
- // Check if this is meaningful initialContent (has actual content)
104
105
  const hasMeaningfulContent = (newAndroidContent && newAndroidContent.trim() !== '')
105
106
  || (newIosContent && newIosContent.trim() !== '');
106
107
 
107
- // Check if we're transitioning from empty to meaningful content (library mode scenario)
108
- const wasEmpty = (!previousContentRef.current.android || previousContentRef.current.android.trim() === '')
109
- && (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
108
+ const wasEmpty = (!prevRef.android || prevRef.android.trim() === '')
109
+ && (!prevRef.ios || prevRef.ios.trim() === '');
110
110
  const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
111
111
 
112
- // Only update if:
113
- // 1. We haven't loaded initial content yet (first load), OR
114
- // 2. We're transitioning from empty to meaningful content (library mode data fetch)
115
- // This prevents the effect from overriding user edits during active editing
116
- // while still allowing content to load properly in library mode
117
- if (!initialContentLoadedRef.current || isTransitioningToContent) {
118
- if (hasMeaningfulContent) {
119
- // Mark as loaded to prevent future updates from overriding user edits
120
- initialContentLoadedRef.current = true;
121
-
122
- setDeviceContent((prev) => {
123
- let updated = false;
124
- const updatedContent = { ...prev };
125
-
126
- if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
127
- updatedContent[ANDROID] = newAndroidContent;
128
- updated = true;
129
- }
130
- if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
131
- updatedContent[IOS] = newIosContent;
132
- updated = true;
133
- }
112
+ // Value-based: did initialContent content actually change? (template switch)
113
+ const initialValueChanged = (newAndroidContent || '') !== (prevRef.android || '')
114
+ || (newIosContent || '') !== (prevRef.ios || '');
134
115
 
135
- return updated ? updatedContent : prev;
136
- });
116
+ const shouldSync = !initialContentLoadedRef.current
117
+ || isTransitioningToContent
118
+ || initialValueChanged;
137
119
 
138
- // Update previous content ref
139
- previousContentRef.current = {
140
- android: newAndroidContent || '',
141
- ios: newIosContent || '',
142
- };
143
- }
120
+ if (shouldSync && hasMeaningfulContent) {
121
+ initialContentLoadedRef.current = true;
122
+
123
+ setDeviceContent((prev) => {
124
+ let updated = false;
125
+ const updatedContent = { ...prev };
126
+
127
+ if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
128
+ updatedContent[ANDROID] = newAndroidContent;
129
+ updated = true;
130
+ }
131
+ if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
132
+ updatedContent[IOS] = newIosContent;
133
+ updated = true;
134
+ }
135
+
136
+ return updated ? updatedContent : prev;
137
+ });
138
+
139
+ previousContentRef.current = {
140
+ android: newAndroidContent || '',
141
+ ios: newIosContent || '',
142
+ };
144
143
  }
145
144
  }, [initialContent, ANDROID, IOS]);
146
145
 
@@ -1,70 +0,0 @@
1
- # DeviceToggle – Click flow and when clicks are blocked
2
-
3
- ## 1. Click flow (button → end)
4
-
5
- 1. **User clicks Android or iOS tab**
6
- - `CapButton` receives the click and runs
7
- - `onClick={() => handleDeviceClick(DEVICE_TYPES.ANDROID)}` or
8
- - `onClick={() => handleDeviceClick(DEVICE_TYPES.IOS)}`.
9
-
10
- 2. **`handleDeviceClick(device)`** (in `DeviceToggle/index.js`):
11
- - Only does something if **both**:
12
- - `onDeviceChange` is truthy (passed from parent), and
13
- - `device !== activeDevice` (clicked tab is not already active).
14
- - If both hold: calls `onDeviceChange(device)`.
15
-
16
- 3. **Parent wiring** (`HTMLEditor.js`):
17
- - `onDeviceChange={switchDevice}`
18
- - So `onDeviceChange` is the `switchDevice` from `useInAppContent`.
19
-
20
- 4. **`switchDevice(device)`** (`hooks/useInAppContent.js`):
21
- - `validDevices = [ANDROID, IOS]` (from `DEVICE_TYPES`: `'android'`, `'ios'`).
22
- - If `device !== activeDevice` and `validDevices.includes(device)` → `setActiveDevice(device)`.
23
- - If `device` is invalid → logs a warning and does nothing.
24
-
25
- 5. **Result**
26
- - `activeDevice` updates → DeviceToggle and preview re-render with the new device; no explicit “block” inside this flow.
27
-
28
- ---
29
-
30
- ## 2. When the click is effectively “blocked” or does nothing
31
-
32
- - **No handler**
33
- If the parent does not pass `onDeviceChange` (or passes `undefined`/`null`), `handleDeviceClick` never calls anything. The button still receives the click; there is no visual “disabled” in DeviceToggle.
34
-
35
- - **Same device**
36
- If the user clicks the already-active tab (`device === activeDevice`), `handleDeviceClick` does nothing by design.
37
-
38
- - **Invalid device**
39
- If `device` is not `'android'` or `'ios'` (e.g. wrong constant when using the repo as a library), `switchDevice` only warns and does not update state.
40
-
41
- - **Click never reaches the button (layout/CSS)**
42
- Another element is on top of the tab and captures the click (see below). This is the likely cause when “the iOS button is not clickable in edit mode” when using the repo as a library.
43
-
44
- ---
45
-
46
- ## 3. Why the iOS button can be unclickable in library (edit) mode
47
-
48
- In **library mode** (`isFullMode === false`), InApp uses a special layout:
49
-
50
- - **Header**: flex row with **DeviceToggle** first and **EditorToolbar** second.
51
- - **DeviceToggle** has `flex: 1` (takes remaining space).
52
- - **EditorToolbar** gets `position: absolute` (no `top`/`right`/`left`), so it is taken out of flow.
53
-
54
- Because the toolbar is out of flow, the only flex child in flow is the DeviceToggle, so it expands to the full width of the header. The absolutely positioned toolbar is then drawn at its “static” position, which is the **right side** of the header. So the toolbar sits **on top of the right part of the header**, which is exactly where the **iOS** tab and the “Keep content same” checkbox are. Clicks on the iOS tab are then hit by the toolbar first, so the **iOS button appears not clickable** even though the handler logic is fine.
55
-
56
- So the problem is **not** that the button is disabled or that the click handler is blocked; it’s that **the EditorToolbar overlaps the iOS tab in library mode** and steals the click.
57
-
58
- ---
59
-
60
- ## 4. Recommended fix (library mode)
61
-
62
- Ensure the toolbar does not overlap the DeviceToggle:
63
-
64
- - Either **remove** the library-mode override that sets `position: absolute` on the toolbar so the header stays in normal flex flow, **or**
65
- - If you keep `position: absolute` for the toolbar:
66
- - Give the header a **positioning context** (e.g. `position: relative`).
67
- - Pin the toolbar to the right (e.g. `right: 0; top: 0`).
68
- - Reserve space for the toolbar so DeviceToggle doesn’t extend under it (e.g. `padding-right` on the header or a fixed/min width for the toolbar and corresponding margin on the DeviceToggle).
69
-
70
- With that, the DeviceToggle (including the iOS tab) remains fully clickable in edit/library mode.