@developer_tribe/react-builder 1.2.42 → 1.2.43
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/build-components/BIcon/BIconProps.generated.d.ts +1 -1
- package/dist/build-components/CountDown/CountDownProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -1
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallFooter/PaywallFooterProps.generated.d.ts +1 -1
- package/dist/build-components/PriceTag/PriceTagProps.generated.d.ts +1 -1
- package/dist/build-components/Pricing/PricingProps.generated.d.ts +1 -1
- package/dist/build-components/Promo/PromoProps.generated.d.ts +1 -1
- package/dist/build-components/Text/TextProps.generated.d.ts +1 -1
- package/dist/build-components/patterns.generated.d.ts +22 -11
- 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 +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +4 -4
- package/dist/index.web.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/RenderPage.tsx +4 -1
- 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 +2 -2
- package/src/attributes-editor/FallbackLocalizationField.tsx +725 -250
- package/src/build-components/BIcon/BIconProps.generated.ts +1 -1
- package/src/build-components/CountDown/CountDownProps.generated.ts +1 -1
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -1
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -1
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -1
- package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +1 -1
- package/src/build-components/PaywallFooter/PaywallFooterProps.generated.ts +1 -1
- package/src/build-components/PriceTag/PriceTagProps.generated.ts +1 -1
- package/src/build-components/Pricing/PricingProps.generated.ts +1 -1
- package/src/build-components/Promo/PromoProps.generated.ts +1 -1
- package/src/build-components/Text/Text.tsx +5 -4
- package/src/build-components/Text/TextProps.generated.ts +1 -1
- package/src/build-components/Text/pattern.json +2 -1
- package/src/build-components/patterns.generated.ts +22 -11
- package/src/hooks/useSafeAreaViewStyle.ts +1 -11
- package/src/utils/extractViewStyle/extractViewStyle.ts +0 -1
- package/src/utils/useMergedStyle.ts +1 -1
|
@@ -1,251 +1,579 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { useRenderStore } from '../store';
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
|
-
* Self-contained
|
|
11
|
+
* Self-contained localization key selector for react-builder.
|
|
6
12
|
*
|
|
7
|
-
*
|
|
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
|
|
13
|
+
* Mirrors TribeHub LocalizationKeySelector UX without external deps.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Auto-fetches records on mount (for compact preview + translations)
|
|
17
|
+
* - Compact preview: selected key + EN/TR translations below it
|
|
18
|
+
* - Modal: platform toggle, search, result cards with translation previews
|
|
19
|
+
* - Create new key form in modal
|
|
20
|
+
* - Module-level cache (survives re-renders); ↻ refresh button
|
|
14
21
|
*/
|
|
15
22
|
|
|
23
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
16
25
|
type LocalizationRecord = {
|
|
17
26
|
key: string;
|
|
27
|
+
description?: string | null;
|
|
18
28
|
translations: Record<string, { value: string | null }>;
|
|
19
29
|
};
|
|
20
30
|
|
|
31
|
+
type LanguageColumn = { code: string; title: string };
|
|
32
|
+
type Platform = 'ios' | 'android';
|
|
21
33
|
type FallbackLocalizationFieldProps = {
|
|
22
34
|
value: string;
|
|
23
35
|
onChange: (v: string) => void;
|
|
24
36
|
};
|
|
25
37
|
|
|
38
|
+
// ─── Module-level cache ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
type CacheEntry = {
|
|
41
|
+
records: LocalizationRecord[];
|
|
42
|
+
languages: LanguageColumn[];
|
|
43
|
+
};
|
|
44
|
+
const cache: Record<string, CacheEntry> = {};
|
|
45
|
+
const rk = (url: string, appId: unknown, p: Platform) =>
|
|
46
|
+
`${url}|${appId ?? ''}|${p}`;
|
|
47
|
+
const lk = (url: string, appId: unknown) => `lang|${url}|${appId ?? ''}`;
|
|
48
|
+
|
|
49
|
+
// ─── Component ───────────────────────────────────────────────────
|
|
50
|
+
|
|
26
51
|
export function FallbackLocalizationField({
|
|
27
52
|
value,
|
|
28
53
|
onChange,
|
|
29
54
|
}: FallbackLocalizationFieldProps) {
|
|
30
55
|
const config = useRenderStore((s) => s.localizationApiConfig);
|
|
56
|
+
const storeLanguageColumns = useRenderStore((s) => s.languageColumns);
|
|
57
|
+
|
|
31
58
|
const [open, setOpen] = useState(false);
|
|
32
59
|
const [query, setQuery] = useState('');
|
|
60
|
+
const [platform, setPlatform] = useState<Platform>('ios');
|
|
33
61
|
const [allRecords, setAllRecords] = useState<LocalizationRecord[]>([]);
|
|
34
|
-
const [filteredRecords, setFilteredRecords] = useState<LocalizationRecord[]>(
|
|
35
|
-
[],
|
|
36
|
-
);
|
|
37
|
-
const [preloadDone, setPreloadDone] = useState(false);
|
|
38
62
|
const [loading, setLoading] = useState(false);
|
|
39
|
-
const
|
|
63
|
+
const [languages, setLanguages] = useState<LanguageColumn[]>([]);
|
|
64
|
+
const [dataReady, setDataReady] = useState(false);
|
|
65
|
+
const fetchVer = useRef(0);
|
|
66
|
+
|
|
67
|
+
// Create-new state
|
|
68
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
69
|
+
const [newKey, setNewKey] = useState('');
|
|
70
|
+
const [newEN, setNewEN] = useState('');
|
|
71
|
+
const [newTR, setNewTR] = useState('');
|
|
72
|
+
const [creating, setCreating] = useState(false);
|
|
73
|
+
const [createError, setCreateError] = useState<string | null>(null);
|
|
40
74
|
|
|
41
75
|
const canSearch = !!config?.forgeUrl && !!config?.forgeToken;
|
|
42
|
-
const
|
|
76
|
+
const displayLangs =
|
|
77
|
+
languages.length > 0
|
|
78
|
+
? languages
|
|
79
|
+
: ((storeLanguageColumns as LanguageColumn[]) ?? []);
|
|
43
80
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
// ─── API helpers ───────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const forgeHeaders = useCallback(() => {
|
|
84
|
+
if (!config) return {};
|
|
85
|
+
return {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
Accept: 'application/json',
|
|
88
|
+
'X-Authorization': config.forgeToken,
|
|
89
|
+
};
|
|
90
|
+
}, [config]);
|
|
91
|
+
|
|
92
|
+
const doFetchRecords = useCallback(
|
|
93
|
+
async (force = false) => {
|
|
94
|
+
if (!config || !canSearch) return;
|
|
95
|
+
const key = rk(config.forgeUrl, config.forgeAppId, platform);
|
|
96
|
+
if (!force && cache[key]?.records?.length) {
|
|
97
|
+
setAllRecords(cache[key].records);
|
|
98
|
+
setDataReady(true);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const version = ++fetchVer.current;
|
|
103
|
+
setLoading(true);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const all: LocalizationRecord[] = [];
|
|
107
|
+
let page = 1;
|
|
108
|
+
let hasMore = true;
|
|
109
|
+
while (hasMore) {
|
|
110
|
+
const base = `${config.forgeUrl.replace(/\/$/, '')}/api/manage/localization/${platform}`;
|
|
111
|
+
const params = new URLSearchParams({
|
|
112
|
+
per_page: '1000',
|
|
113
|
+
page: String(page),
|
|
114
|
+
});
|
|
115
|
+
if (config.forgeAppId)
|
|
116
|
+
params.append('app_id', String(config.forgeAppId));
|
|
117
|
+
const res = await fetch(`${base}?${params}`, {
|
|
118
|
+
headers: forgeHeaders() as HeadersInit,
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok || version !== fetchVer.current) break;
|
|
121
|
+
const json = await res.json();
|
|
122
|
+
all.push(...(json.data ?? []));
|
|
123
|
+
const pg = json.pagination;
|
|
124
|
+
hasMore = pg && pg.current_page < pg.last_page;
|
|
125
|
+
page++;
|
|
126
|
+
}
|
|
127
|
+
if (version === fetchVer.current) {
|
|
128
|
+
setAllRecords(all);
|
|
129
|
+
setDataReady(true);
|
|
130
|
+
cache[key] = { ...cache[key], records: all };
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
/* silent */
|
|
134
|
+
} finally {
|
|
135
|
+
if (version === fetchVer.current) setLoading(false);
|
|
55
136
|
}
|
|
56
|
-
return `${base}?${params.toString()}`;
|
|
57
137
|
},
|
|
58
|
-
[config],
|
|
138
|
+
[config, canSearch, platform, forgeHeaders],
|
|
59
139
|
);
|
|
60
140
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
141
|
+
const doFetchLanguages = useCallback(async () => {
|
|
142
|
+
if (!config || !canSearch) return;
|
|
143
|
+
const key = lk(config.forgeUrl, config.forgeAppId);
|
|
144
|
+
if (cache[key]?.languages?.length) {
|
|
145
|
+
setLanguages(cache[key].languages);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const url = `${config.forgeUrl.replace(/\/$/, '')}/api/manage/localization-languages`;
|
|
150
|
+
const params = new URLSearchParams();
|
|
151
|
+
if (config.forgeAppId) params.append('app_id', String(config.forgeAppId));
|
|
152
|
+
const res = await fetch(`${url}?${params}`, {
|
|
153
|
+
headers: forgeHeaders() as HeadersInit,
|
|
70
154
|
});
|
|
71
|
-
if (!res.ok) return
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
155
|
+
if (!res.ok) return;
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
const langs: LanguageColumn[] = Array.isArray(data) ? data : [];
|
|
158
|
+
setLanguages(langs);
|
|
159
|
+
cache[key] = {
|
|
160
|
+
...cache[key],
|
|
161
|
+
languages: langs,
|
|
162
|
+
records: cache[key]?.records ?? [],
|
|
163
|
+
};
|
|
164
|
+
} catch {
|
|
165
|
+
/* silent */
|
|
166
|
+
}
|
|
167
|
+
}, [config, canSearch, forgeHeaders]);
|
|
168
|
+
|
|
169
|
+
// ─── Auto-fetch on mount (for compact preview) ────────────────
|
|
81
170
|
|
|
82
|
-
// --- Pre-load all records on first dialog open ---
|
|
83
171
|
useEffect(() => {
|
|
84
|
-
if (!
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
};
|
|
172
|
+
if (!canSearch) return;
|
|
173
|
+
void doFetchLanguages();
|
|
174
|
+
void doFetchRecords();
|
|
175
|
+
}, [canSearch, doFetchRecords, doFetchLanguages]);
|
|
107
176
|
|
|
108
|
-
|
|
109
|
-
}, [open, canSearch, preloadDone, fetchPage]);
|
|
177
|
+
// ─── Local filter ──────────────────────────────────────────────
|
|
110
178
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!preloadDone) return;
|
|
179
|
+
const filteredRecords = useMemo(() => {
|
|
180
|
+
if (!dataReady) return [];
|
|
114
181
|
const q = query.trim().toLowerCase();
|
|
115
|
-
if (!q)
|
|
116
|
-
|
|
117
|
-
return;
|
|
182
|
+
if (!q) return allRecords;
|
|
183
|
+
return allRecords.filter((r) => {
|
|
184
|
+
if (r.key.toLowerCase().includes(q)) return true;
|
|
185
|
+
return Object.values(r.translations).some(
|
|
186
|
+
(t) => t?.value && t.value.toLowerCase().includes(q),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
}, [query, allRecords, dataReady]);
|
|
190
|
+
|
|
191
|
+
// ─── Selected record for preview ──────────────────────────────
|
|
192
|
+
|
|
193
|
+
const selectedRecord = useMemo(() => {
|
|
194
|
+
if (!value) return null;
|
|
195
|
+
return allRecords.find((r) => r.key === value) ?? null;
|
|
196
|
+
}, [value, allRecords]);
|
|
197
|
+
|
|
198
|
+
const getTranslation = (record: LocalizationRecord | null, code: string) => {
|
|
199
|
+
if (!record) return '';
|
|
200
|
+
return record.translations[code.toUpperCase()]?.value?.trim() || '';
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const primaryLang = displayLangs[0] ?? { code: 'EN', title: 'English' };
|
|
204
|
+
const secondaryLang = displayLangs[1] ?? { code: 'TR', title: 'Türkçe' };
|
|
205
|
+
const primaryVal = getTranslation(selectedRecord, primaryLang.code);
|
|
206
|
+
const secondaryVal = getTranslation(selectedRecord, secondaryLang.code);
|
|
207
|
+
|
|
208
|
+
// ─── Handlers ──────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
const handleSelect = (record: LocalizationRecord) => {
|
|
211
|
+
onChange(record.key);
|
|
212
|
+
setOpen(false);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
onChange('');
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const handlePlatformChange = (p: Platform) => {
|
|
221
|
+
if (p === platform) return;
|
|
222
|
+
setPlatform(p);
|
|
223
|
+
setDataReady(false);
|
|
224
|
+
setAllRecords([]);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleRefresh = () => {
|
|
228
|
+
if (config) {
|
|
229
|
+
delete cache[rk(config.forgeUrl, config.forgeAppId, platform)];
|
|
230
|
+
delete cache[lk(config.forgeUrl, config.forgeAppId)];
|
|
118
231
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
232
|
+
setDataReady(false);
|
|
233
|
+
setAllRecords([]);
|
|
234
|
+
setLanguages([]);
|
|
235
|
+
void doFetchLanguages();
|
|
236
|
+
void doFetchRecords(true);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const handleCreate = async () => {
|
|
240
|
+
if (!config || !canSearch || !newKey.trim()) return;
|
|
241
|
+
setCreating(true);
|
|
242
|
+
setCreateError(null);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const translations: Record<string, { value: string }> = {};
|
|
246
|
+
if (newEN.trim()) translations['EN'] = { value: newEN.trim() };
|
|
247
|
+
if (newTR.trim()) translations['TR'] = { value: newTR.trim() };
|
|
248
|
+
|
|
249
|
+
const body = { key: newKey.trim(), translations };
|
|
250
|
+
const base = `${config.forgeUrl.replace(/\/$/, '')}/api/manage/localization/${platform}`;
|
|
251
|
+
const params = new URLSearchParams();
|
|
252
|
+
if (config.forgeAppId) params.append('app_id', String(config.forgeAppId));
|
|
253
|
+
|
|
254
|
+
const res = await fetch(`${base}?${params}`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: forgeHeaders() as HeadersInit,
|
|
257
|
+
body: JSON.stringify(body),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
const errData = await res.json().catch(() => ({}));
|
|
262
|
+
throw new Error(
|
|
263
|
+
(errData as { message?: string }).message || `HTTP ${res.status}`,
|
|
124
264
|
);
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
}, [query, allRecords, preloadDone]);
|
|
265
|
+
}
|
|
128
266
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
267
|
+
// Select the new key
|
|
268
|
+
onChange(newKey.trim());
|
|
269
|
+
setShowCreate(false);
|
|
270
|
+
setNewKey('');
|
|
271
|
+
setNewEN('');
|
|
272
|
+
setNewTR('');
|
|
273
|
+
// Invalidate cache and refresh
|
|
274
|
+
handleRefresh();
|
|
275
|
+
setOpen(false);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
setCreateError(err instanceof Error ? err.message : 'Oluşturulamadı');
|
|
278
|
+
} finally {
|
|
279
|
+
setCreating(false);
|
|
280
|
+
}
|
|
133
281
|
};
|
|
134
282
|
|
|
283
|
+
const resolveMissingCount = (record: LocalizationRecord) =>
|
|
284
|
+
displayLangs.filter((lang) => {
|
|
285
|
+
const cell = record.translations[lang.code];
|
|
286
|
+
return !cell?.value || !cell.value.trim();
|
|
287
|
+
}).length;
|
|
288
|
+
|
|
289
|
+
// ─── Render ────────────────────────────────────────────────────
|
|
290
|
+
|
|
135
291
|
return (
|
|
136
292
|
<>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
293
|
+
{/* === Compact Preview === */}
|
|
294
|
+
{value ? (
|
|
295
|
+
<div
|
|
296
|
+
role="button"
|
|
297
|
+
tabIndex={0}
|
|
298
|
+
onClick={() => canSearch && setOpen(true)}
|
|
299
|
+
onKeyDown={(e) => e.key === 'Enter' && canSearch && setOpen(true)}
|
|
300
|
+
style={S.compactCard}
|
|
301
|
+
>
|
|
302
|
+
<div style={S.compactHeader}>
|
|
303
|
+
<span style={S.compactLabel}>Text</span>
|
|
304
|
+
<span style={S.badgeSmall}>{platform.toUpperCase()}</span>
|
|
305
|
+
<span
|
|
306
|
+
role="button"
|
|
307
|
+
tabIndex={0}
|
|
308
|
+
onClick={handleClear}
|
|
309
|
+
onKeyDown={(e) => {
|
|
310
|
+
if (e.key === 'Enter') {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
onChange('');
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
title="Temizle"
|
|
316
|
+
style={S.clearBtn}
|
|
317
|
+
>
|
|
318
|
+
✕
|
|
319
|
+
</span>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div style={S.keyRow}>
|
|
323
|
+
<span style={S.keyText}>{value}</span>
|
|
324
|
+
<span style={{ fontSize: 12, color: '#555' }}>🔍</span>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* EN translation */}
|
|
328
|
+
<div style={S.transRow}>
|
|
329
|
+
<span style={S.langBadge}>
|
|
330
|
+
{primaryLang.title || primaryLang.code}
|
|
331
|
+
</span>
|
|
332
|
+
<span style={primaryVal ? S.transVal : S.transMissing}>
|
|
333
|
+
{primaryVal || 'Eksik'}
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* TR translation */}
|
|
338
|
+
<div style={S.transRow}>
|
|
339
|
+
<span style={S.langBadge}>
|
|
340
|
+
{secondaryLang.title || secondaryLang.code}
|
|
341
|
+
</span>
|
|
342
|
+
<span style={secondaryVal ? S.transVal : S.transMissing}>
|
|
343
|
+
{secondaryVal || 'Eksik'}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
) : (
|
|
348
|
+
<div style={S.emptyWrap}>
|
|
349
|
+
<span style={S.compactLabel}>Text</span>
|
|
350
|
+
<div
|
|
351
|
+
role="button"
|
|
352
|
+
tabIndex={0}
|
|
353
|
+
onClick={() => canSearch && setOpen(true)}
|
|
354
|
+
onKeyDown={(e) => e.key === 'Enter' && canSearch && setOpen(true)}
|
|
355
|
+
style={S.emptyBtn}
|
|
151
356
|
>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
357
|
+
<span style={{ opacity: 0.5 }}>
|
|
358
|
+
Localization anahtarı seçilmedi
|
|
359
|
+
</span>
|
|
360
|
+
{canSearch && <span>🔍</span>}
|
|
361
|
+
</div>
|
|
362
|
+
{!canSearch && (
|
|
363
|
+
<div style={S.configWarn}>Localization API yapılandırılmadı</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* === Modal === */}
|
|
158
369
|
{open && (
|
|
159
|
-
<div onClick={() => setOpen(false)} style={
|
|
160
|
-
<div onClick={(e) => e.stopPropagation()} style={
|
|
370
|
+
<div onClick={() => setOpen(false)} style={S.overlay}>
|
|
371
|
+
<div onClick={(e) => e.stopPropagation()} style={S.modal}>
|
|
161
372
|
{/* Header */}
|
|
162
|
-
<div style={
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
373
|
+
<div style={S.modalHeader}>
|
|
374
|
+
<div>
|
|
375
|
+
<div style={{ fontWeight: 600, fontSize: 15 }}>
|
|
376
|
+
Localization Anahtarı Seç
|
|
377
|
+
</div>
|
|
378
|
+
<div style={{ fontSize: 12, color: '#888', marginTop: 2 }}>
|
|
379
|
+
Arama, seçim veya yeni anahtar oluşturma.
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
<div
|
|
383
|
+
style={{
|
|
384
|
+
display: 'flex',
|
|
385
|
+
gap: 8,
|
|
386
|
+
alignItems: 'center',
|
|
387
|
+
flexShrink: 0,
|
|
388
|
+
}}
|
|
170
389
|
>
|
|
171
|
-
|
|
172
|
-
|
|
390
|
+
<span
|
|
391
|
+
role="button"
|
|
392
|
+
tabIndex={0}
|
|
393
|
+
onClick={handleRefresh}
|
|
394
|
+
title="Yenile"
|
|
395
|
+
style={S.refreshBtn}
|
|
396
|
+
>
|
|
397
|
+
↻
|
|
398
|
+
</span>
|
|
399
|
+
<span
|
|
400
|
+
role="button"
|
|
401
|
+
tabIndex={0}
|
|
402
|
+
onClick={() => setOpen(false)}
|
|
403
|
+
style={S.closeBtn}
|
|
404
|
+
>
|
|
405
|
+
✕
|
|
406
|
+
</span>
|
|
407
|
+
</div>
|
|
173
408
|
</div>
|
|
174
|
-
|
|
175
|
-
|
|
409
|
+
|
|
410
|
+
{/* Platform toggle */}
|
|
411
|
+
<div style={S.platformBar}>
|
|
412
|
+
{(['ios', 'android'] as Platform[]).map((p) => (
|
|
413
|
+
<div
|
|
414
|
+
key={p}
|
|
415
|
+
role="button"
|
|
416
|
+
tabIndex={0}
|
|
417
|
+
onClick={() => handlePlatformChange(p)}
|
|
418
|
+
style={{ ...S.pBtn, ...(platform === p ? S.pBtnActive : {}) }}
|
|
419
|
+
>
|
|
420
|
+
{p === 'ios' ? 'iOS' : 'Android'}
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
{/* Search + Create toggle */}
|
|
426
|
+
<div style={{ padding: '10px 16px 8px', display: 'flex', gap: 8 }}>
|
|
176
427
|
<input
|
|
177
428
|
autoFocus
|
|
178
429
|
type="text"
|
|
179
430
|
value={query}
|
|
180
431
|
onChange={(e) => setQuery(e.target.value)}
|
|
181
432
|
placeholder="Anahtar veya çeviri ara…"
|
|
182
|
-
style={
|
|
433
|
+
style={{ ...S.searchInput, flex: 1 }}
|
|
183
434
|
/>
|
|
435
|
+
<div
|
|
436
|
+
role="button"
|
|
437
|
+
tabIndex={0}
|
|
438
|
+
onClick={() => setShowCreate(!showCreate)}
|
|
439
|
+
style={S.createToggle}
|
|
440
|
+
>
|
|
441
|
+
{showCreate ? '← Ara' : '+ Yeni'}
|
|
442
|
+
</div>
|
|
184
443
|
</div>
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
444
|
+
|
|
445
|
+
{/* === Create form === */}
|
|
446
|
+
{showCreate ? (
|
|
447
|
+
<div style={S.createForm}>
|
|
448
|
+
<div style={S.createField}>
|
|
449
|
+
<label style={S.createLabel}>Anahtar</label>
|
|
450
|
+
<input
|
|
451
|
+
type="text"
|
|
452
|
+
value={newKey}
|
|
453
|
+
onChange={(e) => setNewKey(e.target.value)}
|
|
454
|
+
placeholder="örn: faq.category.general"
|
|
455
|
+
style={S.searchInput}
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
<div style={S.createField}>
|
|
459
|
+
<label style={S.createLabel}>English</label>
|
|
460
|
+
<input
|
|
461
|
+
type="text"
|
|
462
|
+
value={newEN}
|
|
463
|
+
onChange={(e) => setNewEN(e.target.value)}
|
|
464
|
+
placeholder="English translation"
|
|
465
|
+
style={S.searchInput}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
<div style={S.createField}>
|
|
469
|
+
<label style={S.createLabel}>Türkçe</label>
|
|
470
|
+
<input
|
|
471
|
+
type="text"
|
|
472
|
+
value={newTR}
|
|
473
|
+
onChange={(e) => setNewTR(e.target.value)}
|
|
474
|
+
placeholder="Türkçe çeviri"
|
|
475
|
+
style={S.searchInput}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
{createError && <div style={S.errorMsg}>{createError}</div>}
|
|
479
|
+
<div
|
|
480
|
+
role="button"
|
|
481
|
+
tabIndex={0}
|
|
482
|
+
onClick={handleCreate}
|
|
483
|
+
style={{
|
|
484
|
+
...S.saveBtn,
|
|
485
|
+
opacity: creating || !newKey.trim() ? 0.5 : 1,
|
|
486
|
+
pointerEvents: creating || !newKey.trim() ? 'none' : 'auto',
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
{creating ? 'Kaydediliyor…' : 'Oluştur ve Seç'}
|
|
490
|
+
</div>
|
|
189
491
|
</div>
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
492
|
+
) : (
|
|
493
|
+
<>
|
|
494
|
+
{/* Info bar */}
|
|
495
|
+
{dataReady && (
|
|
496
|
+
<div style={S.infoBar}>
|
|
497
|
+
{filteredRecords.length} / {allRecords.length} anahtar
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{/* Results */}
|
|
502
|
+
<div style={S.resultsList}>
|
|
503
|
+
{loading && <div style={S.emptyMsg}>Yükleniyor…</div>}
|
|
504
|
+
{!loading && dataReady && filteredRecords.length === 0 && (
|
|
505
|
+
<div style={S.emptyMsg}>Kayıt bulunamadı.</div>
|
|
506
|
+
)}
|
|
507
|
+
{filteredRecords.map((entry) => {
|
|
508
|
+
const isSelected = entry.key === value;
|
|
509
|
+
const missingCount = resolveMissingCount(entry);
|
|
510
|
+
const pvEN = getTranslation(entry, primaryLang.code);
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<div
|
|
514
|
+
key={entry.key}
|
|
515
|
+
role="button"
|
|
516
|
+
tabIndex={0}
|
|
517
|
+
onClick={() => handleSelect(entry)}
|
|
518
|
+
onKeyDown={(e) =>
|
|
519
|
+
e.key === 'Enter' && handleSelect(entry)
|
|
520
|
+
}
|
|
521
|
+
style={{
|
|
522
|
+
...S.resultCard,
|
|
523
|
+
...(isSelected ? S.resultCardActive : {}),
|
|
524
|
+
}}
|
|
525
|
+
onMouseEnter={(e) => {
|
|
526
|
+
if (!isSelected)
|
|
527
|
+
e.currentTarget.style.background = '#272730';
|
|
528
|
+
}}
|
|
529
|
+
onMouseLeave={(e) => {
|
|
530
|
+
if (!isSelected)
|
|
531
|
+
e.currentTarget.style.background = isSelected
|
|
532
|
+
? 'rgba(99,102,241,0.12)'
|
|
533
|
+
: 'transparent';
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
<div style={S.resultKeyRow}>
|
|
537
|
+
<span style={S.resultKey}>{entry.key}</span>
|
|
538
|
+
<div
|
|
539
|
+
style={{
|
|
540
|
+
display: 'flex',
|
|
541
|
+
gap: 5,
|
|
542
|
+
flexShrink: 0,
|
|
543
|
+
alignItems: 'center',
|
|
544
|
+
}}
|
|
545
|
+
>
|
|
546
|
+
<span style={S.badgeSmall}>
|
|
547
|
+
{platform.toUpperCase()}
|
|
548
|
+
</span>
|
|
549
|
+
{missingCount > 0 && (
|
|
550
|
+
<span style={S.missingBadge}>
|
|
551
|
+
Eksik {missingCount}
|
|
552
|
+
</span>
|
|
553
|
+
)}
|
|
554
|
+
{isSelected && (
|
|
555
|
+
<span style={S.selectedBadge}>✔</span>
|
|
556
|
+
)}
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
<div style={S.transRow}>
|
|
560
|
+
<span style={S.langBadge}>
|
|
561
|
+
{primaryLang.title || primaryLang.code}
|
|
562
|
+
</span>
|
|
563
|
+
<span style={pvEN ? S.transVal : S.transMissing}>
|
|
564
|
+
{pvEN
|
|
565
|
+
? pvEN.length > 100
|
|
566
|
+
? pvEN.slice(0, 100) + '…'
|
|
567
|
+
: pvEN
|
|
568
|
+
: 'Eksik'}
|
|
569
|
+
</span>
|
|
570
|
+
</div>
|
|
243
571
|
</div>
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
572
|
+
);
|
|
573
|
+
})}
|
|
574
|
+
</div>
|
|
575
|
+
</>
|
|
576
|
+
)}
|
|
249
577
|
</div>
|
|
250
578
|
</div>
|
|
251
579
|
)}
|
|
@@ -253,37 +581,85 @@ export function FallbackLocalizationField({
|
|
|
253
581
|
);
|
|
254
582
|
}
|
|
255
583
|
|
|
256
|
-
|
|
257
|
-
|
|
584
|
+
// ─── Styles ──────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
const S: Record<string, React.CSSProperties> = {
|
|
587
|
+
compactCard: {
|
|
588
|
+
display: 'block',
|
|
589
|
+
width: '100%',
|
|
590
|
+
textAlign: 'left' as const,
|
|
591
|
+
background: 'transparent',
|
|
592
|
+
border: '1px solid hsl(0 0% 30%)',
|
|
593
|
+
borderRadius: 8,
|
|
594
|
+
padding: '8px 10px',
|
|
595
|
+
cursor: 'pointer',
|
|
596
|
+
},
|
|
597
|
+
compactHeader: {
|
|
258
598
|
display: 'flex',
|
|
259
599
|
alignItems: 'center',
|
|
260
|
-
gap:
|
|
600
|
+
gap: 6,
|
|
601
|
+
marginBottom: 4,
|
|
602
|
+
},
|
|
603
|
+
compactLabel: {
|
|
604
|
+
fontSize: 10,
|
|
605
|
+
fontWeight: 600,
|
|
606
|
+
textTransform: 'uppercase' as const,
|
|
607
|
+
color: '#888',
|
|
608
|
+
letterSpacing: '0.05em',
|
|
609
|
+
},
|
|
610
|
+
clearBtn: {
|
|
611
|
+
cursor: 'pointer',
|
|
612
|
+
fontSize: 12,
|
|
613
|
+
color: '#666',
|
|
614
|
+
padding: '0 2px',
|
|
615
|
+
marginLeft: 'auto',
|
|
616
|
+
userSelect: 'none' as const,
|
|
261
617
|
},
|
|
262
|
-
|
|
263
|
-
width: 28,
|
|
264
|
-
height: 28,
|
|
618
|
+
keyRow: {
|
|
265
619
|
display: 'flex',
|
|
266
620
|
alignItems: 'center',
|
|
267
|
-
justifyContent: '
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
background: '#2a2a2a',
|
|
271
|
-
color: '#fff',
|
|
272
|
-
cursor: 'pointer',
|
|
273
|
-
fontSize: 13,
|
|
274
|
-
flexShrink: 0,
|
|
621
|
+
justifyContent: 'space-between',
|
|
622
|
+
gap: 8,
|
|
623
|
+
marginBottom: 4,
|
|
275
624
|
},
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
cursor: 'help',
|
|
625
|
+
keyText: {
|
|
626
|
+
fontSize: 12,
|
|
627
|
+
color: '#ccc',
|
|
628
|
+
overflow: 'hidden',
|
|
629
|
+
textOverflow: 'ellipsis',
|
|
630
|
+
whiteSpace: 'nowrap' as const,
|
|
631
|
+
flex: 1,
|
|
284
632
|
},
|
|
633
|
+
transRow: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 },
|
|
634
|
+
transVal: {
|
|
635
|
+
fontSize: 11,
|
|
636
|
+
color: '#bbb',
|
|
637
|
+
overflow: 'hidden',
|
|
638
|
+
whiteSpace: 'nowrap' as const,
|
|
639
|
+
textOverflow: 'ellipsis',
|
|
640
|
+
flex: 1,
|
|
641
|
+
},
|
|
642
|
+
transMissing: { fontSize: 11, color: '#ef4444', flex: 1 },
|
|
643
|
+
|
|
644
|
+
emptyWrap: { display: 'flex', flexDirection: 'column' as const, gap: 4 },
|
|
645
|
+
emptyBtn: {
|
|
646
|
+
display: 'flex',
|
|
647
|
+
alignItems: 'center',
|
|
648
|
+
justifyContent: 'space-between',
|
|
649
|
+
width: '100%',
|
|
650
|
+
padding: '7px 10px',
|
|
651
|
+
border: '1px solid hsl(0 0% 30%)',
|
|
652
|
+
borderRadius: 6,
|
|
653
|
+
background: 'transparent',
|
|
654
|
+
color: '#ccc',
|
|
655
|
+
fontSize: 12,
|
|
656
|
+
cursor: 'pointer',
|
|
657
|
+
textAlign: 'left' as const,
|
|
658
|
+
},
|
|
659
|
+
configWarn: { fontSize: 10, color: '#ef4444', marginTop: 2 },
|
|
660
|
+
|
|
285
661
|
overlay: {
|
|
286
|
-
position: 'fixed',
|
|
662
|
+
position: 'fixed' as const,
|
|
287
663
|
inset: 0,
|
|
288
664
|
background: 'rgba(0,0,0,0.6)',
|
|
289
665
|
zIndex: 9999,
|
|
@@ -292,93 +668,192 @@ const styles: Record<string, React.CSSProperties> = {
|
|
|
292
668
|
justifyContent: 'center',
|
|
293
669
|
},
|
|
294
670
|
modal: {
|
|
295
|
-
background: '#
|
|
296
|
-
borderRadius:
|
|
297
|
-
width:
|
|
298
|
-
maxHeight: '
|
|
671
|
+
background: '#1a1a22',
|
|
672
|
+
borderRadius: 12,
|
|
673
|
+
width: 560,
|
|
674
|
+
maxHeight: '80vh',
|
|
299
675
|
display: 'flex',
|
|
300
|
-
flexDirection: 'column',
|
|
301
|
-
boxShadow: '0
|
|
676
|
+
flexDirection: 'column' as const,
|
|
677
|
+
boxShadow: '0 16px 48px rgba(0,0,0,0.6)',
|
|
302
678
|
color: '#e0e0e0',
|
|
303
|
-
border: '1px solid #
|
|
679
|
+
border: '1px solid #2a2a35',
|
|
304
680
|
overflow: 'hidden',
|
|
305
681
|
},
|
|
306
|
-
|
|
682
|
+
modalHeader: {
|
|
307
683
|
display: 'flex',
|
|
308
684
|
justifyContent: 'space-between',
|
|
309
|
-
alignItems: '
|
|
310
|
-
padding: '
|
|
311
|
-
borderBottom: '1px solid #
|
|
685
|
+
alignItems: 'flex-start',
|
|
686
|
+
padding: '16px 18px',
|
|
687
|
+
borderBottom: '1px solid #2a2a35',
|
|
688
|
+
gap: 12,
|
|
312
689
|
},
|
|
313
690
|
closeBtn: {
|
|
314
|
-
background: 'none',
|
|
315
|
-
border: 'none',
|
|
316
691
|
cursor: 'pointer',
|
|
317
692
|
fontSize: 18,
|
|
318
693
|
color: '#999',
|
|
319
694
|
padding: 4,
|
|
695
|
+
userSelect: 'none' as const,
|
|
696
|
+
},
|
|
697
|
+
refreshBtn: {
|
|
698
|
+
cursor: 'pointer',
|
|
699
|
+
fontSize: 18,
|
|
700
|
+
color: '#6366f1',
|
|
701
|
+
padding: 4,
|
|
702
|
+
userSelect: 'none' as const,
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
platformBar: {
|
|
706
|
+
display: 'flex',
|
|
707
|
+
gap: 8,
|
|
708
|
+
padding: '10px 16px 8px',
|
|
709
|
+
borderBottom: '1px solid #2a2a35',
|
|
710
|
+
},
|
|
711
|
+
pBtn: {
|
|
712
|
+
padding: '5px 14px',
|
|
713
|
+
borderRadius: 6,
|
|
714
|
+
border: '1px solid #3a3a45',
|
|
715
|
+
background: 'transparent',
|
|
716
|
+
color: '#999',
|
|
717
|
+
fontSize: 12,
|
|
718
|
+
fontWeight: 600,
|
|
719
|
+
cursor: 'pointer',
|
|
720
|
+
userSelect: 'none' as const,
|
|
320
721
|
},
|
|
722
|
+
pBtnActive: { background: '#6366f1', borderColor: '#6366f1', color: '#fff' },
|
|
723
|
+
|
|
321
724
|
searchInput: {
|
|
322
725
|
width: '100%',
|
|
323
|
-
padding: '8px
|
|
324
|
-
border: '1px solid #
|
|
726
|
+
padding: '8px 12px',
|
|
727
|
+
border: '1px solid #3a3a45',
|
|
325
728
|
borderRadius: 6,
|
|
326
|
-
background: '#
|
|
729
|
+
background: '#22222a',
|
|
327
730
|
color: '#e0e0e0',
|
|
328
731
|
fontSize: 13,
|
|
329
732
|
outline: 'none',
|
|
330
733
|
boxSizing: 'border-box' as const,
|
|
331
734
|
},
|
|
735
|
+
createToggle: {
|
|
736
|
+
padding: '6px 14px',
|
|
737
|
+
borderRadius: 6,
|
|
738
|
+
border: '1px solid #6366f1',
|
|
739
|
+
background: 'transparent',
|
|
740
|
+
color: '#6366f1',
|
|
741
|
+
fontSize: 12,
|
|
742
|
+
fontWeight: 600,
|
|
743
|
+
cursor: 'pointer',
|
|
744
|
+
whiteSpace: 'nowrap' as const,
|
|
745
|
+
userSelect: 'none' as const,
|
|
746
|
+
},
|
|
747
|
+
|
|
332
748
|
infoBar: {
|
|
333
|
-
padding: '2px 18px
|
|
749
|
+
padding: '2px 18px 6px',
|
|
334
750
|
fontSize: 11,
|
|
335
|
-
color: '#
|
|
751
|
+
color: '#555',
|
|
336
752
|
textAlign: 'right' as const,
|
|
337
753
|
},
|
|
754
|
+
|
|
338
755
|
resultsList: {
|
|
339
756
|
overflowY: 'auto' as const,
|
|
340
757
|
flex: 1,
|
|
341
|
-
padding: '
|
|
758
|
+
padding: '4px 10px 10px',
|
|
342
759
|
},
|
|
343
760
|
emptyMsg: {
|
|
344
|
-
padding:
|
|
761
|
+
padding: 28,
|
|
345
762
|
textAlign: 'center' as const,
|
|
346
|
-
color: '#
|
|
763
|
+
color: '#666',
|
|
347
764
|
fontSize: 13,
|
|
348
765
|
},
|
|
349
|
-
|
|
766
|
+
resultCard: {
|
|
350
767
|
display: 'block',
|
|
351
768
|
width: '100%',
|
|
352
769
|
textAlign: 'left' as const,
|
|
353
|
-
borderRadius:
|
|
354
|
-
padding: '
|
|
770
|
+
borderRadius: 8,
|
|
771
|
+
padding: '10px 12px',
|
|
355
772
|
cursor: 'pointer',
|
|
356
|
-
marginBottom:
|
|
357
|
-
|
|
773
|
+
marginBottom: 4,
|
|
774
|
+
border: '1px solid #2a2a35',
|
|
775
|
+
background: 'transparent',
|
|
358
776
|
},
|
|
359
|
-
|
|
777
|
+
resultCardActive: {
|
|
778
|
+
background: 'rgba(99,102,241,0.12)',
|
|
779
|
+
borderColor: '#6366f1',
|
|
780
|
+
},
|
|
781
|
+
resultKeyRow: {
|
|
360
782
|
display: 'flex',
|
|
361
783
|
alignItems: 'center',
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
fontSize: 11,
|
|
784
|
+
justifyContent: 'space-between',
|
|
785
|
+
gap: 8,
|
|
365
786
|
},
|
|
787
|
+
resultKey: {
|
|
788
|
+
fontWeight: 600,
|
|
789
|
+
fontSize: 13,
|
|
790
|
+
color: '#e0e0e0',
|
|
791
|
+
overflow: 'hidden',
|
|
792
|
+
textOverflow: 'ellipsis',
|
|
793
|
+
whiteSpace: 'nowrap' as const,
|
|
794
|
+
flex: 1,
|
|
795
|
+
},
|
|
796
|
+
|
|
366
797
|
langBadge: {
|
|
367
798
|
display: 'inline-block',
|
|
368
|
-
background: '#
|
|
369
|
-
border: '1px solid #
|
|
370
|
-
borderRadius:
|
|
371
|
-
padding: '1px
|
|
799
|
+
background: '#2a2a35',
|
|
800
|
+
border: '1px solid #3a3a45',
|
|
801
|
+
borderRadius: 4,
|
|
802
|
+
padding: '1px 6px',
|
|
372
803
|
fontSize: 10,
|
|
373
804
|
fontWeight: 600,
|
|
374
805
|
color: '#aaa',
|
|
375
806
|
flexShrink: 0,
|
|
376
807
|
},
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
808
|
+
badgeSmall: {
|
|
809
|
+
display: 'inline-block',
|
|
810
|
+
background: '#2a2a35',
|
|
811
|
+
border: '1px solid #3a3a45',
|
|
812
|
+
borderRadius: 4,
|
|
813
|
+
padding: '1px 5px',
|
|
814
|
+
fontSize: 9,
|
|
815
|
+
fontWeight: 600,
|
|
816
|
+
color: '#888',
|
|
817
|
+
textTransform: 'uppercase' as const,
|
|
818
|
+
},
|
|
819
|
+
missingBadge: {
|
|
820
|
+
display: 'inline-block',
|
|
821
|
+
background: 'rgba(239,68,68,0.15)',
|
|
822
|
+
border: '1px solid rgba(239,68,68,0.3)',
|
|
823
|
+
borderRadius: 4,
|
|
824
|
+
padding: '1px 6px',
|
|
825
|
+
fontSize: 10,
|
|
826
|
+
fontWeight: 600,
|
|
827
|
+
color: '#ef4444',
|
|
828
|
+
},
|
|
829
|
+
selectedBadge: { fontSize: 10, color: '#6366f1', fontWeight: 600 },
|
|
830
|
+
|
|
831
|
+
/* Create form */
|
|
832
|
+
createForm: {
|
|
833
|
+
padding: '12px 16px',
|
|
834
|
+
display: 'flex',
|
|
835
|
+
flexDirection: 'column' as const,
|
|
836
|
+
gap: 10,
|
|
837
|
+
},
|
|
838
|
+
createField: { display: 'flex', flexDirection: 'column' as const, gap: 4 },
|
|
839
|
+
createLabel: {
|
|
840
|
+
fontSize: 11,
|
|
841
|
+
fontWeight: 600,
|
|
842
|
+
color: '#888',
|
|
843
|
+
textTransform: 'uppercase' as const,
|
|
844
|
+
},
|
|
845
|
+
errorMsg: { fontSize: 12, color: '#ef4444', padding: '4px 0' },
|
|
846
|
+
saveBtn: {
|
|
847
|
+
padding: '8px 16px',
|
|
848
|
+
borderRadius: 6,
|
|
849
|
+
background: '#6366f1',
|
|
850
|
+
color: '#fff',
|
|
851
|
+
fontSize: 13,
|
|
852
|
+
fontWeight: 600,
|
|
853
|
+
cursor: 'pointer',
|
|
854
|
+
textAlign: 'center' as const,
|
|
855
|
+
userSelect: 'none' as const,
|
|
856
|
+
border: 'none',
|
|
857
|
+
marginTop: 4,
|
|
383
858
|
},
|
|
384
859
|
};
|