@developer_tribe/react-builder 1.2.40 → 1.2.41
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/dist/attributes-editor/FallbackLocalizationField.d.ts +6 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +2 -2
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.d.ts +8 -0
- package/dist/index.web.esm.js +2 -2
- package/dist/index.web.esm.js.map +1 -1
- package/dist/product-base/types.d.ts +3 -0
- package/dist/store.d.ts +25 -0
- package/dist/types/PreviewConfig.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/public/bin.js +0 -0
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
- package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
- package/src/assets/samples/paywall-1.json +1 -1
- package/src/assets/samples/paywall-2.json +2 -2
- package/src/assets/samples/paywall-app-delete-offer.json +2 -2
- package/src/assets/samples/paywall-app-open-offer.json +2 -2
- package/src/assets/samples/paywall-back-offer.json +1 -1
- package/src/assets/samples/paywall-notification-offer.json +1 -1
- package/src/attributes-editor/AttributesEditorView.tsx +17 -6
- package/src/attributes-editor/FallbackLocalizationField.tsx +384 -0
- package/src/components/BottomBar.tsx +135 -31
- package/src/hooks/useLocalize.ts +3 -1
- package/src/index.web.ts +19 -0
- package/src/modals/InspectModal.tsx +112 -4
- package/src/product-base/buildPaywallLocalizationParams.ts +3 -0
- package/src/product-base/types.ts +3 -0
- package/src/store.ts +39 -0
- package/src/types/PreviewConfig.ts +6 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { useRenderStore } from '../store';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Self-contained fallback localization field for react-builder.
|
|
6
|
+
*
|
|
7
|
+
* Uses the localizationApiConfig from the store to:
|
|
8
|
+
* 1. Pre-load ALL keys on first open (per_page=100, auto-paginate)
|
|
9
|
+
* 2. Filter locally — no subsequent API calls for search
|
|
10
|
+
* 3. Works independently of the host app
|
|
11
|
+
*
|
|
12
|
+
* API: GET {forgeUrl}/api/manage/localization/{platform}?per_page=100&page=N&app_id=X
|
|
13
|
+
* Header: X-Authorization: {forgeToken}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type LocalizationRecord = {
|
|
17
|
+
key: string;
|
|
18
|
+
translations: Record<string, { value: string | null }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FallbackLocalizationFieldProps = {
|
|
22
|
+
value: string;
|
|
23
|
+
onChange: (v: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function FallbackLocalizationField({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
}: FallbackLocalizationFieldProps) {
|
|
30
|
+
const config = useRenderStore((s) => s.localizationApiConfig);
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
const [query, setQuery] = useState('');
|
|
33
|
+
const [allRecords, setAllRecords] = useState<LocalizationRecord[]>([]);
|
|
34
|
+
const [filteredRecords, setFilteredRecords] = useState<LocalizationRecord[]>(
|
|
35
|
+
[],
|
|
36
|
+
);
|
|
37
|
+
const [preloadDone, setPreloadDone] = useState(false);
|
|
38
|
+
const [loading, setLoading] = useState(false);
|
|
39
|
+
const preloadVersionRef = useRef(0);
|
|
40
|
+
|
|
41
|
+
const canSearch = !!config?.forgeUrl && !!config?.forgeToken;
|
|
42
|
+
const platform = 'ios'; // default platform for fallback
|
|
43
|
+
|
|
44
|
+
// --- Forge API helpers ---
|
|
45
|
+
const buildUrl = useCallback(
|
|
46
|
+
(page: number) => {
|
|
47
|
+
if (!config) return '';
|
|
48
|
+
const base = `${config.forgeUrl.replace(/\/$/, '')}/api/manage/localization/${platform}`;
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
per_page: '1000',
|
|
51
|
+
page: String(page),
|
|
52
|
+
});
|
|
53
|
+
if (config.forgeAppId) {
|
|
54
|
+
params.append('app_id', String(config.forgeAppId));
|
|
55
|
+
}
|
|
56
|
+
return `${base}?${params.toString()}`;
|
|
57
|
+
},
|
|
58
|
+
[config],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const fetchPage = useCallback(
|
|
62
|
+
async (page: number) => {
|
|
63
|
+
const url = buildUrl(page);
|
|
64
|
+
const res = await fetch(url, {
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
Accept: 'application/json',
|
|
68
|
+
'X-Authorization': config!.forgeToken,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) return { data: [] as LocalizationRecord[], hasMore: false };
|
|
72
|
+
const json = await res.json();
|
|
73
|
+
const records: LocalizationRecord[] = json.data ?? [];
|
|
74
|
+
const pagination = json.pagination;
|
|
75
|
+
const hasMore =
|
|
76
|
+
pagination && pagination.current_page < pagination.last_page;
|
|
77
|
+
return { data: records, hasMore };
|
|
78
|
+
},
|
|
79
|
+
[buildUrl, config],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// --- Pre-load all records on first dialog open ---
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!open || !canSearch || preloadDone) return;
|
|
85
|
+
|
|
86
|
+
const version = ++preloadVersionRef.current;
|
|
87
|
+
setLoading(true);
|
|
88
|
+
|
|
89
|
+
const loadAll = async () => {
|
|
90
|
+
const all: LocalizationRecord[] = [];
|
|
91
|
+
let page = 1;
|
|
92
|
+
let hasMore = true;
|
|
93
|
+
while (hasMore) {
|
|
94
|
+
const result = await fetchPage(page);
|
|
95
|
+
if (version !== preloadVersionRef.current) return;
|
|
96
|
+
all.push(...result.data);
|
|
97
|
+
hasMore = result.hasMore;
|
|
98
|
+
page++;
|
|
99
|
+
}
|
|
100
|
+
if (version === preloadVersionRef.current) {
|
|
101
|
+
setAllRecords(all);
|
|
102
|
+
setFilteredRecords(all);
|
|
103
|
+
setPreloadDone(true);
|
|
104
|
+
setLoading(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
void loadAll();
|
|
109
|
+
}, [open, canSearch, preloadDone, fetchPage]);
|
|
110
|
+
|
|
111
|
+
// --- Local filter ---
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!preloadDone) return;
|
|
114
|
+
const q = query.trim().toLowerCase();
|
|
115
|
+
if (!q) {
|
|
116
|
+
setFilteredRecords(allRecords);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
setFilteredRecords(
|
|
120
|
+
allRecords.filter((r) => {
|
|
121
|
+
if (r.key.toLowerCase().includes(q)) return true;
|
|
122
|
+
return Object.values(r.translations).some(
|
|
123
|
+
(t) => t?.value && t.value.toLowerCase().includes(q),
|
|
124
|
+
);
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}, [query, allRecords, preloadDone]);
|
|
128
|
+
|
|
129
|
+
const invalidate = () => {
|
|
130
|
+
setPreloadDone(false);
|
|
131
|
+
setAllRecords([]);
|
|
132
|
+
setFilteredRecords([]);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<div style={styles.row}>
|
|
138
|
+
<input
|
|
139
|
+
type="text"
|
|
140
|
+
className="attributes-editor__text-input"
|
|
141
|
+
value={value ?? ''}
|
|
142
|
+
onChange={(e) => onChange(e.target.value)}
|
|
143
|
+
style={{ flex: 1 }}
|
|
144
|
+
/>
|
|
145
|
+
{canSearch ? (
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setOpen(true)}
|
|
149
|
+
title="Browse localization keys"
|
|
150
|
+
style={styles.browseBtn}
|
|
151
|
+
>
|
|
152
|
+
🔍
|
|
153
|
+
</button>
|
|
154
|
+
) : (
|
|
155
|
+
<span title="Localization API yapılandırılmadı" style={styles.dot} />
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
{open && (
|
|
159
|
+
<div onClick={() => setOpen(false)} style={styles.overlay}>
|
|
160
|
+
<div onClick={(e) => e.stopPropagation()} style={styles.modal}>
|
|
161
|
+
{/* Header */}
|
|
162
|
+
<div style={styles.header}>
|
|
163
|
+
<span style={{ fontWeight: 600, fontSize: 14 }}>
|
|
164
|
+
Localization Anahtarı Seç
|
|
165
|
+
</span>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => setOpen(false)}
|
|
169
|
+
style={styles.closeBtn}
|
|
170
|
+
>
|
|
171
|
+
✕
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{/* Search */}
|
|
175
|
+
<div style={{ padding: '10px 16px 6px' }}>
|
|
176
|
+
<input
|
|
177
|
+
autoFocus
|
|
178
|
+
type="text"
|
|
179
|
+
value={query}
|
|
180
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
181
|
+
placeholder="Anahtar veya çeviri ara…"
|
|
182
|
+
style={styles.searchInput}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
{/* Info */}
|
|
186
|
+
{preloadDone && (
|
|
187
|
+
<div style={styles.infoBar}>
|
|
188
|
+
{filteredRecords.length} / {allRecords.length} anahtar
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
{/* Results */}
|
|
192
|
+
<div style={styles.resultsList}>
|
|
193
|
+
{loading && <div style={styles.emptyMsg}>Yükleniyor…</div>}
|
|
194
|
+
{!loading && preloadDone && filteredRecords.length === 0 && (
|
|
195
|
+
<div style={styles.emptyMsg}>Sonuç bulunamadı</div>
|
|
196
|
+
)}
|
|
197
|
+
{filteredRecords.map((entry) => {
|
|
198
|
+
const isSelected = entry.key === value;
|
|
199
|
+
const translations = Object.entries(entry.translations);
|
|
200
|
+
return (
|
|
201
|
+
<button
|
|
202
|
+
key={entry.key}
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => {
|
|
205
|
+
onChange(entry.key);
|
|
206
|
+
setOpen(false);
|
|
207
|
+
}}
|
|
208
|
+
style={{
|
|
209
|
+
...styles.resultItem,
|
|
210
|
+
background: isSelected ? '#333' : 'transparent',
|
|
211
|
+
border: isSelected
|
|
212
|
+
? '1px solid #6366f1'
|
|
213
|
+
: '1px solid #333',
|
|
214
|
+
}}
|
|
215
|
+
onMouseEnter={(e) => {
|
|
216
|
+
if (!isSelected)
|
|
217
|
+
e.currentTarget.style.background = '#2a2a2a';
|
|
218
|
+
}}
|
|
219
|
+
onMouseLeave={(e) => {
|
|
220
|
+
if (!isSelected)
|
|
221
|
+
e.currentTarget.style.background = 'transparent';
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<div
|
|
225
|
+
style={{
|
|
226
|
+
fontWeight: 600,
|
|
227
|
+
fontSize: 12,
|
|
228
|
+
color: '#e0e0e0',
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{entry.key}
|
|
232
|
+
</div>
|
|
233
|
+
{translations.slice(0, 3).map(([lang, cell]) => (
|
|
234
|
+
<div key={lang} style={styles.translationRow}>
|
|
235
|
+
<span style={styles.langBadge}>{lang}</span>
|
|
236
|
+
<span style={styles.translationText}>
|
|
237
|
+
{cell?.value && cell.value.length > 80
|
|
238
|
+
? cell.value.slice(0, 80) + '…'
|
|
239
|
+
: cell?.value || (
|
|
240
|
+
<span style={{ color: '#ef4444' }}>Eksik</span>
|
|
241
|
+
)}
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
))}
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
257
|
+
row: {
|
|
258
|
+
display: 'flex',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
gap: 4,
|
|
261
|
+
},
|
|
262
|
+
browseBtn: {
|
|
263
|
+
width: 28,
|
|
264
|
+
height: 28,
|
|
265
|
+
display: 'flex',
|
|
266
|
+
alignItems: 'center',
|
|
267
|
+
justifyContent: 'center',
|
|
268
|
+
border: '1px solid hsl(0 0% 40%)',
|
|
269
|
+
borderRadius: 4,
|
|
270
|
+
background: '#2a2a2a',
|
|
271
|
+
color: '#fff',
|
|
272
|
+
cursor: 'pointer',
|
|
273
|
+
fontSize: 13,
|
|
274
|
+
flexShrink: 0,
|
|
275
|
+
},
|
|
276
|
+
dot: {
|
|
277
|
+
display: 'inline-block',
|
|
278
|
+
width: 8,
|
|
279
|
+
height: 8,
|
|
280
|
+
borderRadius: '50%',
|
|
281
|
+
backgroundColor: '#ef4444',
|
|
282
|
+
flexShrink: 0,
|
|
283
|
+
cursor: 'help',
|
|
284
|
+
},
|
|
285
|
+
overlay: {
|
|
286
|
+
position: 'fixed',
|
|
287
|
+
inset: 0,
|
|
288
|
+
background: 'rgba(0,0,0,0.6)',
|
|
289
|
+
zIndex: 9999,
|
|
290
|
+
display: 'flex',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
justifyContent: 'center',
|
|
293
|
+
},
|
|
294
|
+
modal: {
|
|
295
|
+
background: '#1e1e1e',
|
|
296
|
+
borderRadius: 10,
|
|
297
|
+
width: 460,
|
|
298
|
+
maxHeight: '75vh',
|
|
299
|
+
display: 'flex',
|
|
300
|
+
flexDirection: 'column',
|
|
301
|
+
boxShadow: '0 12px 40px rgba(0,0,0,0.5)',
|
|
302
|
+
color: '#e0e0e0',
|
|
303
|
+
border: '1px solid #333',
|
|
304
|
+
overflow: 'hidden',
|
|
305
|
+
},
|
|
306
|
+
header: {
|
|
307
|
+
display: 'flex',
|
|
308
|
+
justifyContent: 'space-between',
|
|
309
|
+
alignItems: 'center',
|
|
310
|
+
padding: '14px 18px',
|
|
311
|
+
borderBottom: '1px solid #333',
|
|
312
|
+
},
|
|
313
|
+
closeBtn: {
|
|
314
|
+
background: 'none',
|
|
315
|
+
border: 'none',
|
|
316
|
+
cursor: 'pointer',
|
|
317
|
+
fontSize: 18,
|
|
318
|
+
color: '#999',
|
|
319
|
+
padding: 4,
|
|
320
|
+
},
|
|
321
|
+
searchInput: {
|
|
322
|
+
width: '100%',
|
|
323
|
+
padding: '8px 10px',
|
|
324
|
+
border: '1px solid #444',
|
|
325
|
+
borderRadius: 6,
|
|
326
|
+
background: '#2a2a2a',
|
|
327
|
+
color: '#e0e0e0',
|
|
328
|
+
fontSize: 13,
|
|
329
|
+
outline: 'none',
|
|
330
|
+
boxSizing: 'border-box' as const,
|
|
331
|
+
},
|
|
332
|
+
infoBar: {
|
|
333
|
+
padding: '2px 18px 4px',
|
|
334
|
+
fontSize: 11,
|
|
335
|
+
color: '#666',
|
|
336
|
+
textAlign: 'right' as const,
|
|
337
|
+
},
|
|
338
|
+
resultsList: {
|
|
339
|
+
overflowY: 'auto' as const,
|
|
340
|
+
flex: 1,
|
|
341
|
+
padding: '6px 10px 10px',
|
|
342
|
+
},
|
|
343
|
+
emptyMsg: {
|
|
344
|
+
padding: 20,
|
|
345
|
+
textAlign: 'center' as const,
|
|
346
|
+
color: '#888',
|
|
347
|
+
fontSize: 13,
|
|
348
|
+
},
|
|
349
|
+
resultItem: {
|
|
350
|
+
display: 'block',
|
|
351
|
+
width: '100%',
|
|
352
|
+
textAlign: 'left' as const,
|
|
353
|
+
borderRadius: 6,
|
|
354
|
+
padding: '8px 10px',
|
|
355
|
+
cursor: 'pointer',
|
|
356
|
+
marginBottom: 3,
|
|
357
|
+
transition: 'background 0.15s',
|
|
358
|
+
},
|
|
359
|
+
translationRow: {
|
|
360
|
+
display: 'flex',
|
|
361
|
+
alignItems: 'center',
|
|
362
|
+
gap: 6,
|
|
363
|
+
marginTop: 3,
|
|
364
|
+
fontSize: 11,
|
|
365
|
+
},
|
|
366
|
+
langBadge: {
|
|
367
|
+
display: 'inline-block',
|
|
368
|
+
background: '#333',
|
|
369
|
+
border: '1px solid #444',
|
|
370
|
+
borderRadius: 3,
|
|
371
|
+
padding: '1px 5px',
|
|
372
|
+
fontSize: 10,
|
|
373
|
+
fontWeight: 600,
|
|
374
|
+
color: '#aaa',
|
|
375
|
+
flexShrink: 0,
|
|
376
|
+
},
|
|
377
|
+
translationText: {
|
|
378
|
+
color: '#999',
|
|
379
|
+
overflow: 'hidden',
|
|
380
|
+
whiteSpace: 'nowrap' as const,
|
|
381
|
+
textOverflow: 'ellipsis',
|
|
382
|
+
flex: 1,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { Icon } from './Icon.generated';
|
|
3
3
|
import type { IconsType } from '../types/Icons.generated';
|
|
4
4
|
import { useRenderStore } from '../store';
|
|
@@ -29,7 +29,6 @@ export function BottomBar({
|
|
|
29
29
|
setData,
|
|
30
30
|
project,
|
|
31
31
|
}: BottomBarProps) {
|
|
32
|
-
const rtlIcon: IconsType = 'translate';
|
|
33
32
|
const magicCursorIcon: IconsType = 'magicpen';
|
|
34
33
|
const debugIcon: IconsType = 'speedometer-03';
|
|
35
34
|
const localizationIcon: IconsType = 'globe-01';
|
|
@@ -45,8 +44,8 @@ export function BottomBar({
|
|
|
45
44
|
setDefaultLanguage,
|
|
46
45
|
previewMode,
|
|
47
46
|
setPreviewMode,
|
|
48
|
-
isRtl,
|
|
49
47
|
setIsRtl,
|
|
48
|
+
languageColumns,
|
|
50
49
|
} = useRenderStore((s) => ({
|
|
51
50
|
localization: s.localization,
|
|
52
51
|
setLocalization: s.setLocalization,
|
|
@@ -56,24 +55,55 @@ export function BottomBar({
|
|
|
56
55
|
setDefaultLanguage: s.setDefaultLanguage,
|
|
57
56
|
previewMode: s.previewMode,
|
|
58
57
|
setPreviewMode: s.setPreviewMode,
|
|
59
|
-
isRtl: s.isRtl,
|
|
60
58
|
setIsRtl: s.setIsRtl,
|
|
59
|
+
languageColumns: s.languageColumns,
|
|
61
60
|
}));
|
|
62
61
|
|
|
63
62
|
const [isDebugOpen, setIsDebugOpen] = useState(false);
|
|
64
63
|
const [isLocalizationOpen, setIsLocalizationOpen] = useState(false);
|
|
65
64
|
const [isInspectOpen, setIsInspectOpen] = useState(false);
|
|
66
65
|
const [isPromptManagerOpen, setIsPromptManagerOpen] = useState(false);
|
|
66
|
+
const [langDropdownOpen, setLangDropdownOpen] = useState(false);
|
|
67
|
+
const langTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
68
|
+
|
|
69
|
+
// Use languageColumns from API, fallback to localization object keys
|
|
70
|
+
const availableLanguages = useMemo(() => {
|
|
71
|
+
if (languageColumns.length > 0) return languageColumns;
|
|
72
|
+
const locKeys = Object.keys(localization ?? {});
|
|
73
|
+
return locKeys.map((code) => ({
|
|
74
|
+
id: 0,
|
|
75
|
+
code: code.toUpperCase(),
|
|
76
|
+
title: code,
|
|
77
|
+
iso_code: code.toUpperCase(),
|
|
78
|
+
is_right_alignment: false,
|
|
79
|
+
}));
|
|
80
|
+
}, [languageColumns, localization]);
|
|
67
81
|
|
|
68
|
-
const languages = useMemo(() => ['en', 'tr', 'ar'], []);
|
|
69
82
|
const activeLanguage = defaultLanguage;
|
|
70
83
|
|
|
84
|
+
const handleSelectLanguage = useCallback(
|
|
85
|
+
(lang: { code: string; is_right_alignment: boolean }) => {
|
|
86
|
+
const code = lang.code.toLowerCase();
|
|
87
|
+
setDefaultLanguage(code);
|
|
88
|
+
setIsRtl(lang.is_right_alignment);
|
|
89
|
+
setLangDropdownOpen(false);
|
|
90
|
+
},
|
|
91
|
+
[setDefaultLanguage, setIsRtl],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const handleLangMouseEnter = () => {
|
|
95
|
+
if (langTimeoutRef.current) clearTimeout(langTimeoutRef.current);
|
|
96
|
+
setLangDropdownOpen(true);
|
|
97
|
+
};
|
|
98
|
+
const handleLangMouseLeave = () => {
|
|
99
|
+
langTimeoutRef.current = setTimeout(() => setLangDropdownOpen(false), 200);
|
|
100
|
+
};
|
|
101
|
+
|
|
71
102
|
const handleLocalicationChange = (next: Localication) => {
|
|
72
103
|
setLocalization(next);
|
|
73
104
|
};
|
|
74
105
|
|
|
75
106
|
const themeIsActive = theme === 'dark';
|
|
76
|
-
const rtlIsActive = isRtl;
|
|
77
107
|
const previewIsActive = previewMode;
|
|
78
108
|
const themeIcon: IconsType = themeIsActive ? 'moon-bold' : 'sun';
|
|
79
109
|
|
|
@@ -90,19 +120,6 @@ export function BottomBar({
|
|
|
90
120
|
<Icon iconType={themeIcon} size={20} color="currentColor" alt="" />
|
|
91
121
|
</button>
|
|
92
122
|
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
95
|
-
className={`rb-bottom-bar__button rb-bottom-bar__button--rtl${
|
|
96
|
-
rtlIsActive ? ' is-active' : ''
|
|
97
|
-
}`}
|
|
98
|
-
aria-label="RTL"
|
|
99
|
-
aria-pressed={rtlIsActive}
|
|
100
|
-
onClick={() => setIsRtl(!isRtl)}
|
|
101
|
-
>
|
|
102
|
-
<Icon iconType={rtlIcon} size={18} color="currentColor" alt="" />
|
|
103
|
-
<span className="rb-bottom-bar__rtl-text">RTL</span>
|
|
104
|
-
</button>
|
|
105
|
-
|
|
106
123
|
<button
|
|
107
124
|
type="button"
|
|
108
125
|
className={`rb-bottom-bar__button rb-bottom-bar__button--preview${
|
|
@@ -171,19 +188,106 @@ export function BottomBar({
|
|
|
171
188
|
|
|
172
189
|
<div className="rb-bottom-bar__spacer" />
|
|
173
190
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
{/* Language selector with hover-up dropdown */}
|
|
192
|
+
<div
|
|
193
|
+
className="rb-bottom-bar__lang-selector"
|
|
194
|
+
onMouseEnter={handleLangMouseEnter}
|
|
195
|
+
onMouseLeave={handleLangMouseLeave}
|
|
196
|
+
style={{ position: 'relative' }}
|
|
197
|
+
>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className="rb-bottom-bar__lang is-active"
|
|
201
|
+
style={{
|
|
202
|
+
display: 'flex',
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
gap: 4,
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
{activeLanguage.toUpperCase()}
|
|
208
|
+
<span style={{ fontSize: 8, opacity: 0.6 }}>▲</span>
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
{langDropdownOpen && availableLanguages.length > 0 && (
|
|
212
|
+
<div
|
|
213
|
+
className="rb-bottom-bar__lang-dropdown"
|
|
214
|
+
style={{
|
|
215
|
+
position: 'absolute',
|
|
216
|
+
bottom: '100%',
|
|
217
|
+
right: 0,
|
|
218
|
+
marginBottom: 4,
|
|
219
|
+
background: 'var(--rb-bg-secondary, #2a2a2a)',
|
|
220
|
+
border: '1px solid var(--rb-border, #444)',
|
|
221
|
+
borderRadius: 6,
|
|
222
|
+
padding: '4px 0',
|
|
223
|
+
minWidth: 150,
|
|
224
|
+
maxHeight: 260,
|
|
225
|
+
overflowY: 'auto',
|
|
226
|
+
boxShadow: '0 -4px 16px rgba(0,0,0,0.3)',
|
|
227
|
+
zIndex: 100,
|
|
228
|
+
}}
|
|
183
229
|
>
|
|
184
|
-
{
|
|
185
|
-
|
|
186
|
-
|
|
230
|
+
{availableLanguages.map((lang) => {
|
|
231
|
+
const isActive =
|
|
232
|
+
lang.code.toLowerCase() === activeLanguage.toLowerCase();
|
|
233
|
+
return (
|
|
234
|
+
<button
|
|
235
|
+
key={lang.code}
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => handleSelectLanguage(lang)}
|
|
238
|
+
style={{
|
|
239
|
+
display: 'flex',
|
|
240
|
+
alignItems: 'center',
|
|
241
|
+
gap: 6,
|
|
242
|
+
width: '100%',
|
|
243
|
+
padding: '5px 10px',
|
|
244
|
+
border: 'none',
|
|
245
|
+
background: isActive
|
|
246
|
+
? 'var(--rb-accent, #6366f1)'
|
|
247
|
+
: 'transparent',
|
|
248
|
+
color: isActive
|
|
249
|
+
? '#fff'
|
|
250
|
+
: 'var(--rb-text-primary, #e0e0e0)',
|
|
251
|
+
cursor: 'pointer',
|
|
252
|
+
fontSize: 12,
|
|
253
|
+
textAlign: 'left',
|
|
254
|
+
}}
|
|
255
|
+
onMouseEnter={(e) => {
|
|
256
|
+
if (!isActive)
|
|
257
|
+
e.currentTarget.style.background =
|
|
258
|
+
'var(--rb-bg-hover, #333)';
|
|
259
|
+
}}
|
|
260
|
+
onMouseLeave={(e) => {
|
|
261
|
+
if (!isActive)
|
|
262
|
+
e.currentTarget.style.background = 'transparent';
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
<span
|
|
266
|
+
style={{
|
|
267
|
+
fontWeight: 600,
|
|
268
|
+
width: 24,
|
|
269
|
+
flexShrink: 0,
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
{lang.code}
|
|
273
|
+
</span>
|
|
274
|
+
<span style={{ opacity: 0.7, flex: 1 }}>{lang.title}</span>
|
|
275
|
+
{lang.is_right_alignment && (
|
|
276
|
+
<span
|
|
277
|
+
style={{
|
|
278
|
+
fontSize: 9,
|
|
279
|
+
opacity: 0.5,
|
|
280
|
+
flexShrink: 0,
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
RTL
|
|
284
|
+
</span>
|
|
285
|
+
)}
|
|
286
|
+
</button>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
187
291
|
</div>
|
|
188
292
|
</div>
|
|
189
293
|
|
package/src/hooks/useLocalize.ts
CHANGED
|
@@ -23,7 +23,9 @@ export function useLocalize(options?: {
|
|
|
23
23
|
|
|
24
24
|
return useCallback(
|
|
25
25
|
(keyOrText: string) => {
|
|
26
|
-
const
|
|
26
|
+
const langMap = localization?.[defaultLanguage];
|
|
27
|
+
const raw = langMap?.[keyOrText] ?? keyOrText;
|
|
28
|
+
|
|
27
29
|
return replaceLocalizationParams(raw, params);
|
|
28
30
|
},
|
|
29
31
|
[defaultLanguage, localization, params],
|
package/src/index.web.ts
CHANGED
|
@@ -11,3 +11,22 @@ export * from './index';
|
|
|
11
11
|
export * from './build-components';
|
|
12
12
|
export { default as useNode } from './build-components/useNode';
|
|
13
13
|
export type { EventObjectGenerated } from './build-components/OnboardButton/OnboardButtonProps.generated';
|
|
14
|
+
|
|
15
|
+
// Host-app API: let parent apps inject a custom "Text" field renderer.
|
|
16
|
+
import { useRenderStore } from './store';
|
|
17
|
+
import type { ComponentType } from 'react';
|
|
18
|
+
import type { LocalizationApiConfig, LanguageColumn } from './store';
|
|
19
|
+
|
|
20
|
+
export function setBuilderStringChildrenField(
|
|
21
|
+
comp?: ComponentType<{ value: string; onChange: (v: string) => void }>,
|
|
22
|
+
) {
|
|
23
|
+
useRenderStore.getState().setRenderStringChildrenField(comp);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setBuilderLocalizationConfig(config?: LocalizationApiConfig) {
|
|
27
|
+
useRenderStore.getState().setLocalizationApiConfig(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setBuilderLanguageColumns(cols: LanguageColumn[]) {
|
|
31
|
+
useRenderStore.getState().setLanguageColumns(cols);
|
|
32
|
+
}
|