@developer_tribe/react-builder 1.2.42 → 1.2.44-test.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.
Files changed (131) hide show
  1. package/dist/build-components/BIcon/BIconProps.generated.d.ts +1 -1
  2. package/dist/build-components/Checkbox/Checkbox.d.ts +6 -0
  3. package/dist/build-components/Checkbox/CheckboxProps.generated.d.ts +67 -0
  4. package/dist/build-components/CountDown/CountDownProps.generated.d.ts +1 -1
  5. package/dist/build-components/FormCheckbox/FormCheckbox.d.ts +3 -0
  6. package/dist/build-components/FormCheckbox/FormCheckboxProps.generated.d.ts +69 -0
  7. package/dist/build-components/FormErrorText/FormErrorText.d.ts +3 -0
  8. package/dist/build-components/FormErrorText/FormErrorTextProps.generated.d.ts +61 -0
  9. package/dist/build-components/FormProvider/FormProvider.d.ts +11 -0
  10. package/dist/build-components/FormProvider/FormProviderProps.generated.d.ts +55 -0
  11. package/dist/build-components/FormSubmitButton/FormSubmitButton.d.ts +2 -0
  12. package/dist/build-components/FormSubmitButton/FormSubmitButtonProps.generated.d.ts +73 -0
  13. package/dist/build-components/GlobalProvider/GlobalContext.d.ts +28 -0
  14. package/dist/build-components/GlobalProvider/GlobalProvider.d.ts +5 -0
  15. package/dist/build-components/GlobalProvider/GlobalProviderProps.generated.d.ts +60 -0
  16. package/dist/build-components/GlobalProvider/globalProviderUtils.d.ts +37 -0
  17. package/dist/build-components/GlobalProvider/useGlobalNavigation.d.ts +19 -0
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +17 -10
  19. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -1
  20. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +2 -0
  21. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -1
  22. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -1
  23. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +1 -1
  24. package/dist/build-components/PaywallFooter/PaywallFooterProps.generated.d.ts +1 -1
  25. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +2 -0
  26. package/dist/build-components/PriceTag/PriceTagProps.generated.d.ts +1 -1
  27. package/dist/build-components/Pricing/PricingProps.generated.d.ts +1 -1
  28. package/dist/build-components/Promo/PromoProps.generated.d.ts +1 -1
  29. package/dist/build-components/SystemButton/SystemButton.d.ts +7 -0
  30. package/dist/build-components/SystemButton/SystemButtonProps.generated.d.ts +71 -0
  31. package/dist/build-components/SystemButton/usePlacementButtonEvents.d.ts +28 -0
  32. package/dist/build-components/TermsProvider/TermsProvider.d.ts +5 -0
  33. package/dist/build-components/TermsProvider/TermsProviderProps.generated.d.ts +55 -0
  34. package/dist/build-components/Text/TextProps.generated.d.ts +1 -1
  35. package/dist/build-components/WebView/WebView.d.ts +2 -0
  36. package/dist/build-components/WebView/WebViewProps.generated.d.ts +59 -0
  37. package/dist/build-components/index.d.ts +10 -1
  38. package/dist/build-components/patterns.generated.d.ts +5507 -1543
  39. package/dist/index.cjs.js +1 -1
  40. package/dist/index.cjs.js.map +1 -1
  41. package/dist/index.esm.js +1 -1
  42. package/dist/index.esm.js.map +1 -1
  43. package/dist/index.web.cjs.js +5 -5
  44. package/dist/index.web.cjs.js.map +1 -1
  45. package/dist/index.web.d.ts +1 -0
  46. package/dist/index.web.esm.js +4 -4
  47. package/dist/index.web.esm.js.map +1 -1
  48. package/dist/mockOS/context/MockOSContextBase.d.ts +3 -1
  49. package/dist/styles.css +1 -1
  50. package/dist/types/PreviewConfig.d.ts +1 -1
  51. package/package.json +2 -1
  52. package/src/RenderPage.tsx +4 -1
  53. package/src/assets/meta.json +1 -1
  54. package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
  55. package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
  56. package/src/assets/samples/getSamples.ts +7 -0
  57. package/src/assets/samples/global-onboard-flow.json +729 -0
  58. package/src/assets/samples/paywall-1.json +2 -2
  59. package/src/assets/samples/terms-and-privacy-no-form.json +108 -0
  60. package/src/assets/samples/terms-and-privacy.json +130 -0
  61. package/src/attributes-editor/FallbackLocalizationField.tsx +725 -250
  62. package/src/build-components/BIcon/BIconProps.generated.ts +1 -1
  63. package/src/build-components/Checkbox/Checkbox.tsx +165 -0
  64. package/src/build-components/Checkbox/CheckboxProps.generated.ts +84 -0
  65. package/src/build-components/Checkbox/pattern.json +83 -0
  66. package/src/build-components/CountDown/CountDownProps.generated.ts +1 -1
  67. package/src/build-components/FormCheckbox/FormCheckbox.tsx +106 -0
  68. package/src/build-components/FormCheckbox/FormCheckboxProps.generated.ts +86 -0
  69. package/src/build-components/FormCheckbox/pattern.json +39 -0
  70. package/src/build-components/FormErrorText/FormErrorText.tsx +34 -0
  71. package/src/build-components/FormErrorText/FormErrorTextProps.generated.ts +78 -0
  72. package/src/build-components/FormErrorText/pattern.json +21 -0
  73. package/src/build-components/FormProvider/FormProvider.tsx +131 -0
  74. package/src/build-components/FormProvider/FormProviderProps.generated.ts +72 -0
  75. package/src/build-components/FormProvider/pattern.json +33 -0
  76. package/src/build-components/FormSubmitButton/FormSubmitButton.tsx +49 -0
  77. package/src/build-components/FormSubmitButton/FormSubmitButtonProps.generated.ts +91 -0
  78. package/src/build-components/FormSubmitButton/pattern.json +33 -0
  79. package/src/build-components/GlobalProvider/GlobalContext.ts +48 -0
  80. package/src/build-components/GlobalProvider/GlobalProvider.tsx +191 -0
  81. package/src/build-components/GlobalProvider/GlobalProviderProps.generated.ts +78 -0
  82. package/src/build-components/GlobalProvider/globalProviderUtils.ts +163 -0
  83. package/src/build-components/GlobalProvider/pattern.json +55 -0
  84. package/src/build-components/GlobalProvider/useGlobalNavigation.ts +70 -0
  85. package/src/build-components/OnboardButton/OnboardButton.tsx +41 -36
  86. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +17 -10
  87. package/src/build-components/OnboardButton/pattern.json +5 -4
  88. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -1
  89. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +12 -0
  90. package/src/build-components/OnboardProvider/pattern.json +9 -1
  91. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -1
  92. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -1
  93. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +1 -1
  94. package/src/build-components/PaywallFooter/PaywallFooterProps.generated.ts +1 -1
  95. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +12 -0
  96. package/src/build-components/PaywallProvider/pattern.json +9 -1
  97. package/src/build-components/PriceTag/PriceTagProps.generated.ts +1 -1
  98. package/src/build-components/Pricing/PricingProps.generated.ts +1 -1
  99. package/src/build-components/Promo/PromoProps.generated.ts +1 -1
  100. package/src/build-components/RenderNode.generated.tsx +46 -1
  101. package/src/build-components/SystemButton/SystemButton.tsx +71 -0
  102. package/src/build-components/SystemButton/SystemButtonProps.generated.ts +89 -0
  103. package/src/build-components/SystemButton/pattern.json +61 -0
  104. package/src/build-components/SystemButton/usePlacementButtonEvents.ts +101 -0
  105. package/src/build-components/TermsProvider/TermsProvider.tsx +45 -0
  106. package/src/build-components/TermsProvider/TermsProviderProps.generated.ts +82 -0
  107. package/src/build-components/TermsProvider/pattern.json +35 -0
  108. package/src/build-components/Text/Text.tsx +5 -4
  109. package/src/build-components/Text/TextProps.generated.ts +1 -1
  110. package/src/build-components/Text/pattern.json +2 -1
  111. package/src/build-components/WebView/WebView.tsx +149 -0
  112. package/src/build-components/WebView/WebViewProps.generated.ts +76 -0
  113. package/src/build-components/WebView/pattern.json +71 -0
  114. package/src/build-components/index.ts +45 -0
  115. package/src/build-components/patterns.generated.ts +5712 -1559
  116. package/src/hooks/useSafeAreaViewStyle.ts +1 -11
  117. package/src/index.web.ts +3 -0
  118. package/src/mockOS/components/MockOSRouter.tsx +21 -0
  119. package/src/mockOS/context/MockOSContext.tsx +7 -0
  120. package/src/mockOS/context/MockOSContextBase.ts +4 -0
  121. package/src/styles/components/_checkbox.scss +19 -0
  122. package/src/styles/components/_global-provider.scss +131 -0
  123. package/src/styles/components/_webview.scss +52 -0
  124. package/src/styles/index.scss +4 -0
  125. package/src/types/PreviewConfig.ts +19 -0
  126. package/src/utils/analyseNodeByPatterns.ts +5 -2
  127. package/src/utils/extractViewStyle/extractViewStyle.ts +0 -1
  128. package/src/utils/projectColors.ts +4 -0
  129. package/src/utils/useMergedStyle.ts +1 -1
  130. package/src/.DS_Store +0 -0
  131. package/src/assets/.DS_Store +0 -0
@@ -1,251 +1,579 @@
1
- import React, { useState, useCallback, useRef, useEffect } from '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 fallback localization field for react-builder.
11
+ * Self-contained localization key selector for react-builder.
6
12
  *
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
13
+ * Mirrors TribeHub LocalizationKeySelector UX without external deps.
11
14
  *
12
- * API: GET {forgeUrl}/api/manage/localization/{platform}?per_page=100&page=N&app_id=X
13
- * Header: X-Authorization: {forgeToken}
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 preloadVersionRef = useRef(0);
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 platform = 'ios'; // default platform for fallback
76
+ const displayLangs =
77
+ languages.length > 0
78
+ ? languages
79
+ : ((storeLanguageColumns as LanguageColumn[]) ?? []);
43
80
 
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));
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 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
- },
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 { 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
- );
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 (!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
- };
172
+ if (!canSearch) return;
173
+ void doFetchLanguages();
174
+ void doFetchRecords();
175
+ }, [canSearch, doFetchRecords, doFetchLanguages]);
107
176
 
108
- void loadAll();
109
- }, [open, canSearch, preloadDone, fetchPage]);
177
+ // ─── Local filter ──────────────────────────────────────────────
110
178
 
111
- // --- Local filter ---
112
- useEffect(() => {
113
- if (!preloadDone) return;
179
+ const filteredRecords = useMemo(() => {
180
+ if (!dataReady) return [];
114
181
  const q = query.trim().toLowerCase();
115
- if (!q) {
116
- setFilteredRecords(allRecords);
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
- 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),
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
- const invalidate = () => {
130
- setPreloadDone(false);
131
- setAllRecords([]);
132
- setFilteredRecords([]);
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
- <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}
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
- </button>
154
- ) : (
155
- <span title="Localization API yapılandırılmadı" style={styles.dot} />
156
- )}
157
- </div>
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={styles.overlay}>
160
- <div onClick={(e) => e.stopPropagation()} style={styles.modal}>
370
+ <div onClick={() => setOpen(false)} style={S.overlay}>
371
+ <div onClick={(e) => e.stopPropagation()} style={S.modal}>
161
372
  {/* 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}
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
- </button>
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
- {/* Search */}
175
- <div style={{ padding: '10px 16px 6px' }}>
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={styles.searchInput}
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
- {/* Info */}
186
- {preloadDone && (
187
- <div style={styles.infoBar}>
188
- {filteredRecords.length} / {allRecords.length} anahtar
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
- {/* 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>
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
- </button>
246
- );
247
- })}
248
- </div>
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
- const styles: Record<string, React.CSSProperties> = {
257
- row: {
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: 4,
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
- browseBtn: {
263
- width: 28,
264
- height: 28,
618
+ keyRow: {
265
619
  display: 'flex',
266
620
  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,
621
+ justifyContent: 'space-between',
622
+ gap: 8,
623
+ marginBottom: 4,
275
624
  },
276
- dot: {
277
- display: 'inline-block',
278
- width: 8,
279
- height: 8,
280
- borderRadius: '50%',
281
- backgroundColor: '#ef4444',
282
- flexShrink: 0,
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: '#1e1e1e',
296
- borderRadius: 10,
297
- width: 460,
298
- maxHeight: '75vh',
671
+ background: '#1a1a22',
672
+ borderRadius: 12,
673
+ width: 560,
674
+ maxHeight: '80vh',
299
675
  display: 'flex',
300
- flexDirection: 'column',
301
- boxShadow: '0 12px 40px rgba(0,0,0,0.5)',
676
+ flexDirection: 'column' as const,
677
+ boxShadow: '0 16px 48px rgba(0,0,0,0.6)',
302
678
  color: '#e0e0e0',
303
- border: '1px solid #333',
679
+ border: '1px solid #2a2a35',
304
680
  overflow: 'hidden',
305
681
  },
306
- header: {
682
+ modalHeader: {
307
683
  display: 'flex',
308
684
  justifyContent: 'space-between',
309
- alignItems: 'center',
310
- padding: '14px 18px',
311
- borderBottom: '1px solid #333',
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 10px',
324
- border: '1px solid #444',
726
+ padding: '8px 12px',
727
+ border: '1px solid #3a3a45',
325
728
  borderRadius: 6,
326
- background: '#2a2a2a',
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 4px',
749
+ padding: '2px 18px 6px',
334
750
  fontSize: 11,
335
- color: '#666',
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: '6px 10px 10px',
758
+ padding: '4px 10px 10px',
342
759
  },
343
760
  emptyMsg: {
344
- padding: 20,
761
+ padding: 28,
345
762
  textAlign: 'center' as const,
346
- color: '#888',
763
+ color: '#666',
347
764
  fontSize: 13,
348
765
  },
349
- resultItem: {
766
+ resultCard: {
350
767
  display: 'block',
351
768
  width: '100%',
352
769
  textAlign: 'left' as const,
353
- borderRadius: 6,
354
- padding: '8px 10px',
770
+ borderRadius: 8,
771
+ padding: '10px 12px',
355
772
  cursor: 'pointer',
356
- marginBottom: 3,
357
- transition: 'background 0.15s',
773
+ marginBottom: 4,
774
+ border: '1px solid #2a2a35',
775
+ background: 'transparent',
358
776
  },
359
- translationRow: {
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
- gap: 6,
363
- marginTop: 3,
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: '#333',
369
- border: '1px solid #444',
370
- borderRadius: 3,
371
- padding: '1px 5px',
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
- translationText: {
378
- color: '#999',
379
- overflow: 'hidden',
380
- whiteSpace: 'nowrap' as const,
381
- textOverflow: 'ellipsis',
382
- flex: 1,
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
  };