@developer_tribe/react-builder 1.2.32 → 1.2.34
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/assets/prompt-scheme-onboard.generated.d.ts +1 -0
- package/dist/assets/prompt-scheme-paywall.generated.d.ts +1 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +3 -3
- package/dist/build-components/patterns.generated.d.ts +52 -52
- package/dist/components/BuilderProvider.d.ts +2 -4
- 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 +6 -6
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +6 -6
- package/dist/index.web.esm.js.map +1 -1
- package/dist/modals/PromptManagerModal.d.ts +9 -0
- package/dist/modals/index.d.ts +1 -0
- package/dist/styles.css +1 -1
- package/dist/utils/nodeXml.d.ts +11 -0
- package/package.json +5 -1
- package/scripts/prebuild/assets/prompt_scheme.md +77 -0
- package/scripts/prebuild/generate-prompt-schemes.js +464 -0
- package/scripts/prebuild/prebuild.js +4 -0
- package/src/RenderPage.tsx +6 -6
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +4 -0
- package/src/assets/prompt-scheme-paywall.generated.ts +4 -0
- package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -1
- package/src/build-components/BIcon/pattern.json +1 -3
- package/src/build-components/BackgroundImage/pattern.json +2 -10
- package/src/build-components/Button/pattern.json +1 -3
- package/src/build-components/Carousel/pattern.json +2 -8
- package/src/build-components/CarouselDots/CarouselDots.tsx +1 -1
- package/src/build-components/CarouselProvider/pattern.json +1 -4
- package/src/build-components/CountDown/pattern.json +1 -3
- package/src/build-components/Image/pattern.json +2 -9
- package/src/build-components/Main/pattern.json +1 -3
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +1 -1
- package/src/build-components/NavigationBarColor/pattern.json +1 -3
- package/src/build-components/Onboard/pattern.json +2 -6
- package/src/build-components/OnboardButton/OnboardButton.tsx +1 -1
- package/src/build-components/OnboardButton/pattern.json +3 -14
- package/src/build-components/OnboardButtons/pattern.json +4 -15
- package/src/build-components/OnboardDot/OnboardDot.tsx +1 -1
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +3 -3
- package/src/build-components/OnboardDot/pattern.json +15 -16
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
- package/src/build-components/OnboardFooter/pattern.json +15 -19
- package/src/build-components/OnboardItem/pattern.json +3 -11
- package/src/build-components/OnboardProvider/pattern.json +2 -8
- package/src/build-components/OnboardSubtitle/pattern.json +1 -4
- package/src/build-components/OnboardTitle/pattern.json +1 -4
- package/src/build-components/PaywallBackground/pattern.json +1 -3
- package/src/build-components/PaywallCloseButton/pattern.json +1 -3
- package/src/build-components/PaywallOptions/pattern.json +1 -3
- package/src/build-components/PaywallProvider/pattern.json +1 -3
- package/src/build-components/PaywallSubscribeButton/pattern.json +1 -3
- package/src/build-components/PriceTag/pattern.json +2 -8
- package/src/build-components/Pricing/pattern.json +1 -3
- package/src/build-components/Promo/pattern.json +1 -3
- package/src/build-components/Separator/pattern.json +1 -3
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +1 -1
- package/src/build-components/StatusBarColor/pattern.json +1 -3
- package/src/build-components/Text/pattern.json +1 -3
- package/src/build-components/View/pattern.json +4 -16
- package/src/build-components/patterns.generated.ts +52 -52
- package/src/components/BottomBar.tsx +28 -1
- package/src/components/BuilderProvider.tsx +5 -14
- package/src/hooks/useLocalize.ts +1 -1
- package/src/modals/MockableFeatureModal.tsx +552 -5
- package/src/modals/Modal.tsx +7 -1
- package/src/modals/PromptManagerModal.tsx +270 -0
- package/src/modals/index.ts +1 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/modals/_prompt-manager-modal.scss +95 -0
- package/src/utils/nodeXml.ts +196 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
2
|
import Modal from './Modal';
|
|
3
3
|
import { useRenderStore } from '../store';
|
|
4
4
|
import { ProductEditModal } from './ProductEditModal';
|
|
@@ -9,6 +9,13 @@ import type {
|
|
|
9
9
|
PaywallBenefits,
|
|
10
10
|
PaywallBenefitValue,
|
|
11
11
|
} from '../paywall/types/benefits';
|
|
12
|
+
import type { Localication } from '../types/PreviewConfig';
|
|
13
|
+
import { defaultLocalization, mergeLocalization } from '../types/PreviewConfig';
|
|
14
|
+
import type { ProjectColors } from '../types/Project';
|
|
15
|
+
import {
|
|
16
|
+
defaultProjectColors,
|
|
17
|
+
mergeProjectColors,
|
|
18
|
+
} from '../utils/projectColors';
|
|
12
19
|
|
|
13
20
|
type MockableFeatureModalProps = {
|
|
14
21
|
featureKey: string;
|
|
@@ -32,6 +39,10 @@ export function MockableFeatureModal({
|
|
|
32
39
|
upsertBenefit,
|
|
33
40
|
removeBenefit,
|
|
34
41
|
renameBenefit,
|
|
42
|
+
setLocalization,
|
|
43
|
+
localization,
|
|
44
|
+
setProjectColors,
|
|
45
|
+
projectColors,
|
|
35
46
|
} = useRenderStore((s) => ({
|
|
36
47
|
products: s.products,
|
|
37
48
|
addProduct: s.addProduct,
|
|
@@ -45,6 +56,10 @@ export function MockableFeatureModal({
|
|
|
45
56
|
upsertBenefit: s.upsertBenefit,
|
|
46
57
|
removeBenefit: s.removeBenefit,
|
|
47
58
|
renameBenefit: s.renameBenefit,
|
|
59
|
+
setLocalization: s.setLocalization,
|
|
60
|
+
localization: s.localization,
|
|
61
|
+
setProjectColors: s.setProjectColors,
|
|
62
|
+
projectColors: s.projectColors,
|
|
48
63
|
}));
|
|
49
64
|
|
|
50
65
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
@@ -54,6 +69,106 @@ export function MockableFeatureModal({
|
|
|
54
69
|
);
|
|
55
70
|
const [showBenefitPresets, setShowBenefitPresets] = useState(false);
|
|
56
71
|
|
|
72
|
+
// ─── Import refs ─────────────────────────────────────────────────────────
|
|
73
|
+
const csvInputRef = useRef<HTMLInputElement>(null);
|
|
74
|
+
const colorsInputRef = useRef<HTMLInputElement>(null);
|
|
75
|
+
const genericInputRef = useRef<HTMLInputElement>(null);
|
|
76
|
+
|
|
77
|
+
/** Parse CSV with `en` and `tr` columns into a Localication map. */
|
|
78
|
+
const handleLocalizationCsvImport = (
|
|
79
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
80
|
+
) => {
|
|
81
|
+
const file = e.target.files?.[0];
|
|
82
|
+
if (!file) return;
|
|
83
|
+
const reader = new FileReader();
|
|
84
|
+
reader.onload = (event) => {
|
|
85
|
+
try {
|
|
86
|
+
const text = event.target?.result as string;
|
|
87
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
88
|
+
if (lines.length < 2) {
|
|
89
|
+
alert('CSV must have a header row and at least one data row.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const headers = lines[0]!.split(',').map((h) => h.trim().toLowerCase());
|
|
93
|
+
const keyIdx = headers.indexOf('key');
|
|
94
|
+
const enIdx = headers.indexOf('en');
|
|
95
|
+
const trIdx = headers.indexOf('tr');
|
|
96
|
+
if (keyIdx === -1 || (enIdx === -1 && trIdx === -1)) {
|
|
97
|
+
alert(
|
|
98
|
+
'CSV must have a "key" column and at least one of "en" or "tr" columns.',
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const localization: Localication = {};
|
|
103
|
+
for (let i = 1; i < lines.length; i++) {
|
|
104
|
+
const cols = lines[i]!.split(',').map((c) => c.trim());
|
|
105
|
+
const key = cols[keyIdx]?.trim();
|
|
106
|
+
if (!key) continue;
|
|
107
|
+
const entry: Record<string, string> = {};
|
|
108
|
+
if (enIdx !== -1 && cols[enIdx] !== undefined)
|
|
109
|
+
entry['en'] = cols[enIdx]!;
|
|
110
|
+
if (trIdx !== -1 && cols[trIdx] !== undefined)
|
|
111
|
+
entry['tr'] = cols[trIdx]!;
|
|
112
|
+
localization[key] = entry;
|
|
113
|
+
}
|
|
114
|
+
setLocalization(localization);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
alert('Failed to parse CSV: ' + String(err));
|
|
117
|
+
} finally {
|
|
118
|
+
e.target.value = '';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
reader.readAsText(file);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Parse JSON file and call setProjectColors. */
|
|
125
|
+
const handleColorsJsonImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
126
|
+
const file = e.target.files?.[0];
|
|
127
|
+
if (!file) return;
|
|
128
|
+
const reader = new FileReader();
|
|
129
|
+
reader.onload = (event) => {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(
|
|
132
|
+
event.target?.result as string,
|
|
133
|
+
) as ProjectColors;
|
|
134
|
+
setProjectColors(parsed);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
alert('Failed to parse JSON: ' + String(err));
|
|
137
|
+
} finally {
|
|
138
|
+
e.target.value = '';
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
reader.readAsText(file);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Generic JSON import — sets products or benefits based on featureKey. */
|
|
145
|
+
const handleGenericJsonImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
146
|
+
const file = e.target.files?.[0];
|
|
147
|
+
if (!file) return;
|
|
148
|
+
const reader = new FileReader();
|
|
149
|
+
reader.onload = (event) => {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(event.target?.result as string);
|
|
152
|
+
if (featureKey === 'products' && Array.isArray(parsed)) {
|
|
153
|
+
setProducts(parsed);
|
|
154
|
+
} else if (
|
|
155
|
+
featureKey === 'benefits' &&
|
|
156
|
+
typeof parsed === 'object' &&
|
|
157
|
+
!Array.isArray(parsed)
|
|
158
|
+
) {
|
|
159
|
+
setBenefits(parsed as PaywallBenefits);
|
|
160
|
+
} else {
|
|
161
|
+
alert('Imported JSON does not match the expected format.');
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
alert('Failed to parse JSON: ' + String(err));
|
|
165
|
+
} finally {
|
|
166
|
+
e.target.value = '';
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
reader.readAsText(file);
|
|
170
|
+
};
|
|
171
|
+
|
|
57
172
|
const benefitEntries = Object.entries(
|
|
58
173
|
benefits && typeof benefits === 'object' && !Array.isArray(benefits)
|
|
59
174
|
? (benefits as PaywallBenefits)
|
|
@@ -89,7 +204,23 @@ export function MockableFeatureModal({
|
|
|
89
204
|
</button>
|
|
90
205
|
</div>
|
|
91
206
|
<div className="mockable-feature-modal__body">
|
|
92
|
-
{featureKey === '
|
|
207
|
+
{featureKey === 'localization' ? (
|
|
208
|
+
<LocalizationPanel
|
|
209
|
+
localization={localization}
|
|
210
|
+
defaultLoc={defaultLocalization}
|
|
211
|
+
csvInputRef={csvInputRef}
|
|
212
|
+
onImportClick={() => csvInputRef.current?.click()}
|
|
213
|
+
onChange={handleLocalizationCsvImport}
|
|
214
|
+
/>
|
|
215
|
+
) : featureKey === 'colors' ? (
|
|
216
|
+
<ColorsPanel
|
|
217
|
+
projectColors={projectColors}
|
|
218
|
+
defaultColors={defaultProjectColors}
|
|
219
|
+
colorsInputRef={colorsInputRef}
|
|
220
|
+
onImportClick={() => colorsInputRef.current?.click()}
|
|
221
|
+
onChange={handleColorsJsonImport}
|
|
222
|
+
/>
|
|
223
|
+
) : featureKey === 'products' ? (
|
|
93
224
|
<>
|
|
94
225
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
95
226
|
<button
|
|
@@ -222,9 +353,27 @@ export function MockableFeatureModal({
|
|
|
222
353
|
</div>
|
|
223
354
|
</>
|
|
224
355
|
) : (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
356
|
+
<>
|
|
357
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
className="editor-button"
|
|
361
|
+
onClick={() => genericInputRef.current?.click()}
|
|
362
|
+
>
|
|
363
|
+
Import JSON
|
|
364
|
+
</button>
|
|
365
|
+
<input
|
|
366
|
+
ref={genericInputRef}
|
|
367
|
+
type="file"
|
|
368
|
+
accept=".json,application/json"
|
|
369
|
+
style={{ display: 'none' }}
|
|
370
|
+
onChange={handleGenericJsonImport}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
<p style={{ margin: '8px 0 0', opacity: 0.7, fontSize: 12 }}>
|
|
374
|
+
No mock UI yet for "{featureKey}" — import via JSON.
|
|
375
|
+
</p>
|
|
376
|
+
</>
|
|
228
377
|
)}
|
|
229
378
|
</div>
|
|
230
379
|
</Modal>
|
|
@@ -290,3 +439,401 @@ export function MockableFeatureModal({
|
|
|
290
439
|
}
|
|
291
440
|
|
|
292
441
|
export default MockableFeatureModal;
|
|
442
|
+
|
|
443
|
+
// ─── Source badge ─────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
type Source = 'default' | 'custom' | 'merged';
|
|
446
|
+
|
|
447
|
+
const SOURCE_STYLE: Record<Source, React.CSSProperties> = {
|
|
448
|
+
default: {
|
|
449
|
+
fontSize: 10,
|
|
450
|
+
padding: '1px 5px',
|
|
451
|
+
borderRadius: 3,
|
|
452
|
+
background: '#3a3d4a',
|
|
453
|
+
color: '#9ca3af',
|
|
454
|
+
fontWeight: 500,
|
|
455
|
+
whiteSpace: 'nowrap',
|
|
456
|
+
},
|
|
457
|
+
custom: {
|
|
458
|
+
fontSize: 10,
|
|
459
|
+
padding: '1px 5px',
|
|
460
|
+
borderRadius: 3,
|
|
461
|
+
background: '#14532d',
|
|
462
|
+
color: '#86efac',
|
|
463
|
+
fontWeight: 500,
|
|
464
|
+
whiteSpace: 'nowrap',
|
|
465
|
+
},
|
|
466
|
+
merged: {
|
|
467
|
+
fontSize: 10,
|
|
468
|
+
padding: '1px 5px',
|
|
469
|
+
borderRadius: 3,
|
|
470
|
+
background: '#1e3a5f',
|
|
471
|
+
color: '#93c5fd',
|
|
472
|
+
fontWeight: 500,
|
|
473
|
+
whiteSpace: 'nowrap',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
function SourceBadge({ source }: { source: Source }) {
|
|
478
|
+
return <span style={SOURCE_STYLE[source]}>{source}</span>;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ─── LocalizationPanel ───────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
type LocalizationPanelProps = {
|
|
484
|
+
localization: import('../types/PreviewConfig').Localication;
|
|
485
|
+
defaultLoc: import('../types/PreviewConfig').Localication;
|
|
486
|
+
csvInputRef: React.RefObject<HTMLInputElement>;
|
|
487
|
+
onImportClick: () => void;
|
|
488
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
function LocalizationPanel({
|
|
492
|
+
localization,
|
|
493
|
+
defaultLoc,
|
|
494
|
+
csvInputRef,
|
|
495
|
+
onImportClick,
|
|
496
|
+
onChange,
|
|
497
|
+
}: LocalizationPanelProps) {
|
|
498
|
+
const merged = mergeLocalization(defaultLoc, localization);
|
|
499
|
+
// Collect all langs and all keys
|
|
500
|
+
const langs = Array.from(
|
|
501
|
+
new Set([...Object.keys(defaultLoc), ...Object.keys(localization)]),
|
|
502
|
+
).sort();
|
|
503
|
+
const allKeys = Array.from(
|
|
504
|
+
new Set(
|
|
505
|
+
langs.flatMap((l) => [
|
|
506
|
+
...Object.keys(defaultLoc[l] ?? {}),
|
|
507
|
+
...Object.keys(localization[l] ?? {}),
|
|
508
|
+
]),
|
|
509
|
+
),
|
|
510
|
+
).sort();
|
|
511
|
+
|
|
512
|
+
const hasCustom =
|
|
513
|
+
localization &&
|
|
514
|
+
Object.keys(localization).some(
|
|
515
|
+
(l) => Object.keys(localization[l] ?? {}).length > 0,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<>
|
|
520
|
+
{/* toolbar */}
|
|
521
|
+
<div
|
|
522
|
+
style={{
|
|
523
|
+
display: 'flex',
|
|
524
|
+
gap: 8,
|
|
525
|
+
alignItems: 'center',
|
|
526
|
+
flexWrap: 'wrap',
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
<button type="button" className="editor-button" onClick={onImportClick}>
|
|
530
|
+
Import CSV
|
|
531
|
+
</button>
|
|
532
|
+
<input
|
|
533
|
+
ref={csvInputRef}
|
|
534
|
+
type="file"
|
|
535
|
+
accept=".csv,text/csv"
|
|
536
|
+
style={{ display: 'none' }}
|
|
537
|
+
onChange={onChange}
|
|
538
|
+
/>
|
|
539
|
+
<span style={{ fontSize: 11, opacity: 0.6 }}>key, en, tr columns</span>
|
|
540
|
+
<span style={{ marginLeft: 'auto', fontSize: 11, opacity: 0.5 }}>
|
|
541
|
+
{hasCustom ? 'custom overrides active' : 'using defaults'}
|
|
542
|
+
</span>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
{/* legend */}
|
|
546
|
+
<div
|
|
547
|
+
style={{ display: 'flex', gap: 8, margin: '8px 0 4px', fontSize: 11 }}
|
|
548
|
+
>
|
|
549
|
+
<SourceBadge source="default" /> default only
|
|
550
|
+
<SourceBadge source="custom" /> custom override
|
|
551
|
+
<SourceBadge source="merged" /> both (custom wins)
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{/* table */}
|
|
555
|
+
<div style={{ overflowX: 'auto', marginTop: 4 }}>
|
|
556
|
+
<table
|
|
557
|
+
style={{
|
|
558
|
+
borderCollapse: 'collapse',
|
|
559
|
+
fontSize: 11,
|
|
560
|
+
width: '100%',
|
|
561
|
+
tableLayout: 'fixed',
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
<thead>
|
|
565
|
+
<tr>
|
|
566
|
+
<th
|
|
567
|
+
style={{
|
|
568
|
+
textAlign: 'left',
|
|
569
|
+
padding: '4px 6px',
|
|
570
|
+
borderBottom: '1px solid #333',
|
|
571
|
+
width: 200,
|
|
572
|
+
fontWeight: 600,
|
|
573
|
+
}}
|
|
574
|
+
>
|
|
575
|
+
key
|
|
576
|
+
</th>
|
|
577
|
+
{langs.map((lang) => (
|
|
578
|
+
<th
|
|
579
|
+
key={lang}
|
|
580
|
+
style={{
|
|
581
|
+
textAlign: 'left',
|
|
582
|
+
padding: '4px 6px',
|
|
583
|
+
borderBottom: '1px solid #333',
|
|
584
|
+
fontWeight: 600,
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
{lang}
|
|
588
|
+
</th>
|
|
589
|
+
))}
|
|
590
|
+
</tr>
|
|
591
|
+
</thead>
|
|
592
|
+
<tbody>
|
|
593
|
+
{allKeys.map((key) => (
|
|
594
|
+
<tr key={key} style={{ borderBottom: '1px solid #222' }}>
|
|
595
|
+
<td
|
|
596
|
+
style={{
|
|
597
|
+
padding: '3px 6px',
|
|
598
|
+
fontFamily: 'monospace',
|
|
599
|
+
wordBreak: 'break-all',
|
|
600
|
+
opacity: 0.8,
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
{key}
|
|
604
|
+
</td>
|
|
605
|
+
{langs.map((lang) => {
|
|
606
|
+
const inDefault = (defaultLoc[lang] ?? {})[key] !== undefined;
|
|
607
|
+
const inCustom =
|
|
608
|
+
(localization[lang] ?? {})[key] !== undefined;
|
|
609
|
+
const val = (merged[lang] ?? {})[key] ?? '';
|
|
610
|
+
let src: Source = 'default';
|
|
611
|
+
if (inDefault && inCustom) src = 'merged';
|
|
612
|
+
else if (inCustom) src = 'custom';
|
|
613
|
+
return (
|
|
614
|
+
<td
|
|
615
|
+
key={lang}
|
|
616
|
+
style={{ padding: '3px 6px', verticalAlign: 'top' }}
|
|
617
|
+
>
|
|
618
|
+
<div
|
|
619
|
+
style={{
|
|
620
|
+
display: 'flex',
|
|
621
|
+
gap: 4,
|
|
622
|
+
alignItems: 'flex-start',
|
|
623
|
+
}}
|
|
624
|
+
>
|
|
625
|
+
<SourceBadge source={src} />
|
|
626
|
+
<span
|
|
627
|
+
style={{ wordBreak: 'break-word', opacity: 0.85 }}
|
|
628
|
+
>
|
|
629
|
+
{val}
|
|
630
|
+
</span>
|
|
631
|
+
</div>
|
|
632
|
+
</td>
|
|
633
|
+
);
|
|
634
|
+
})}
|
|
635
|
+
</tr>
|
|
636
|
+
))}
|
|
637
|
+
</tbody>
|
|
638
|
+
</table>
|
|
639
|
+
</div>
|
|
640
|
+
</>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ─── ColorsPanel ─────────────────────────────────────────────────────────────
|
|
645
|
+
|
|
646
|
+
type ColorsPanelProps = {
|
|
647
|
+
projectColors?: import('../types/Project').ProjectColors;
|
|
648
|
+
defaultColors: import('../types/Project').ProjectColors;
|
|
649
|
+
colorsInputRef: React.RefObject<HTMLInputElement>;
|
|
650
|
+
onImportClick: () => void;
|
|
651
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
function ColorsPanel({
|
|
655
|
+
projectColors,
|
|
656
|
+
defaultColors,
|
|
657
|
+
colorsInputRef,
|
|
658
|
+
onImportClick,
|
|
659
|
+
onChange,
|
|
660
|
+
}: ColorsPanelProps) {
|
|
661
|
+
const merged = projectColors
|
|
662
|
+
? mergeProjectColors(defaultColors, projectColors)
|
|
663
|
+
: defaultColors;
|
|
664
|
+
|
|
665
|
+
const hasCustom = !!projectColors;
|
|
666
|
+
|
|
667
|
+
/** Build rows for a flat token map comparing default vs custom. */
|
|
668
|
+
function buildRows(
|
|
669
|
+
defaultMap: Record<string, string> = {},
|
|
670
|
+
customMap: Record<string, string> = {},
|
|
671
|
+
mergedMap: Record<string, string> = {},
|
|
672
|
+
) {
|
|
673
|
+
const keys = Array.from(
|
|
674
|
+
new Set([...Object.keys(defaultMap), ...Object.keys(customMap)]),
|
|
675
|
+
).sort();
|
|
676
|
+
return keys.map((k) => {
|
|
677
|
+
const inDefault = k in defaultMap;
|
|
678
|
+
const inCustom = k in (customMap ?? {});
|
|
679
|
+
let src: Source = 'default';
|
|
680
|
+
if (inDefault && inCustom) src = 'merged';
|
|
681
|
+
else if (inCustom) src = 'custom';
|
|
682
|
+
return { key: k, value: mergedMap[k] ?? '', src };
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const staticDefault = defaultColors.STATIC_COLORS ?? {};
|
|
687
|
+
const staticCustom = projectColors?.STATIC_COLORS ?? {};
|
|
688
|
+
const staticMerged = merged.STATIC_COLORS ?? {};
|
|
689
|
+
const staticRows = buildRows(staticDefault, staticCustom, staticMerged);
|
|
690
|
+
|
|
691
|
+
const themeNames = Array.from(
|
|
692
|
+
new Set([
|
|
693
|
+
...Object.keys(defaultColors.THEME_COLORS ?? {}),
|
|
694
|
+
...Object.keys(projectColors?.THEME_COLORS ?? {}),
|
|
695
|
+
]),
|
|
696
|
+
).sort();
|
|
697
|
+
|
|
698
|
+
function ColorRow({
|
|
699
|
+
k,
|
|
700
|
+
value,
|
|
701
|
+
src,
|
|
702
|
+
}: {
|
|
703
|
+
k: string;
|
|
704
|
+
value: string;
|
|
705
|
+
src: Source;
|
|
706
|
+
}) {
|
|
707
|
+
return (
|
|
708
|
+
<tr style={{ borderBottom: '1px solid #222' }}>
|
|
709
|
+
<td
|
|
710
|
+
style={{
|
|
711
|
+
padding: '3px 8px',
|
|
712
|
+
fontFamily: 'monospace',
|
|
713
|
+
fontSize: 11,
|
|
714
|
+
opacity: 0.8,
|
|
715
|
+
}}
|
|
716
|
+
>
|
|
717
|
+
{k}
|
|
718
|
+
</td>
|
|
719
|
+
<td style={{ padding: '3px 8px' }}>
|
|
720
|
+
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
721
|
+
<span
|
|
722
|
+
style={{
|
|
723
|
+
display: 'inline-block',
|
|
724
|
+
width: 14,
|
|
725
|
+
height: 14,
|
|
726
|
+
borderRadius: 3,
|
|
727
|
+
background: value,
|
|
728
|
+
border: '1px solid #555',
|
|
729
|
+
flexShrink: 0,
|
|
730
|
+
}}
|
|
731
|
+
/>
|
|
732
|
+
<span style={{ fontFamily: 'monospace', fontSize: 11 }}>
|
|
733
|
+
{value}
|
|
734
|
+
</span>
|
|
735
|
+
</div>
|
|
736
|
+
</td>
|
|
737
|
+
<td style={{ padding: '3px 8px' }}>
|
|
738
|
+
<SourceBadge source={src} />
|
|
739
|
+
</td>
|
|
740
|
+
</tr>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function SectionTable({
|
|
745
|
+
rows,
|
|
746
|
+
}: {
|
|
747
|
+
rows: Array<{ key: string; value: string; src: Source }>;
|
|
748
|
+
}) {
|
|
749
|
+
return (
|
|
750
|
+
<table
|
|
751
|
+
style={{ borderCollapse: 'collapse', width: '100%', fontSize: 11 }}
|
|
752
|
+
>
|
|
753
|
+
<thead>
|
|
754
|
+
<tr>
|
|
755
|
+
{['token', 'value', 'source'].map((h) => (
|
|
756
|
+
<th
|
|
757
|
+
key={h}
|
|
758
|
+
style={{
|
|
759
|
+
textAlign: 'left',
|
|
760
|
+
padding: '3px 8px',
|
|
761
|
+
borderBottom: '1px solid #333',
|
|
762
|
+
fontWeight: 600,
|
|
763
|
+
}}
|
|
764
|
+
>
|
|
765
|
+
{h}
|
|
766
|
+
</th>
|
|
767
|
+
))}
|
|
768
|
+
</tr>
|
|
769
|
+
</thead>
|
|
770
|
+
<tbody>
|
|
771
|
+
{rows.map((r) => (
|
|
772
|
+
<ColorRow key={r.key} k={r.key} value={r.value} src={r.src} />
|
|
773
|
+
))}
|
|
774
|
+
</tbody>
|
|
775
|
+
</table>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return (
|
|
780
|
+
<>
|
|
781
|
+
{/* toolbar */}
|
|
782
|
+
<div
|
|
783
|
+
style={{
|
|
784
|
+
display: 'flex',
|
|
785
|
+
gap: 8,
|
|
786
|
+
alignItems: 'center',
|
|
787
|
+
flexWrap: 'wrap',
|
|
788
|
+
}}
|
|
789
|
+
>
|
|
790
|
+
<button type="button" className="editor-button" onClick={onImportClick}>
|
|
791
|
+
Import JSON
|
|
792
|
+
</button>
|
|
793
|
+
<input
|
|
794
|
+
ref={colorsInputRef}
|
|
795
|
+
type="file"
|
|
796
|
+
accept=".json,application/json"
|
|
797
|
+
style={{ display: 'none' }}
|
|
798
|
+
onChange={onChange}
|
|
799
|
+
/>
|
|
800
|
+
<span style={{ marginLeft: 'auto', fontSize: 11, opacity: 0.5 }}>
|
|
801
|
+
{hasCustom ? 'custom overrides active' : 'using defaults'}
|
|
802
|
+
</span>
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
{/* legend */}
|
|
806
|
+
<div
|
|
807
|
+
style={{ display: 'flex', gap: 8, margin: '8px 0 4px', fontSize: 11 }}
|
|
808
|
+
>
|
|
809
|
+
<SourceBadge source="default" /> default only
|
|
810
|
+
<SourceBadge source="custom" /> custom override
|
|
811
|
+
<SourceBadge source="merged" /> both (custom wins)
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
{/* STATIC */}
|
|
815
|
+
<div style={{ fontWeight: 600, fontSize: 12, margin: '10px 0 4px' }}>
|
|
816
|
+
STATIC_COLORS
|
|
817
|
+
</div>
|
|
818
|
+
<SectionTable rows={staticRows} />
|
|
819
|
+
|
|
820
|
+
{/* THEME */}
|
|
821
|
+
{themeNames.map((theme) => {
|
|
822
|
+
const themeDefault = defaultColors.THEME_COLORS?.[theme] ?? {};
|
|
823
|
+
const themeCustom = projectColors?.THEME_COLORS?.[theme] ?? {};
|
|
824
|
+
const themeMerged = merged.THEME_COLORS?.[theme] ?? {};
|
|
825
|
+
const rows = buildRows(themeDefault, themeCustom, themeMerged);
|
|
826
|
+
return (
|
|
827
|
+
<div key={theme}>
|
|
828
|
+
<div
|
|
829
|
+
style={{ fontWeight: 600, fontSize: 12, margin: '10px 0 4px' }}
|
|
830
|
+
>
|
|
831
|
+
THEME_COLORS / {theme}
|
|
832
|
+
</div>
|
|
833
|
+
<SectionTable rows={rows} />
|
|
834
|
+
</div>
|
|
835
|
+
);
|
|
836
|
+
})}
|
|
837
|
+
</>
|
|
838
|
+
);
|
|
839
|
+
}
|
package/src/modals/Modal.tsx
CHANGED
|
@@ -43,7 +43,13 @@ export function Modal({
|
|
|
43
43
|
>
|
|
44
44
|
<div
|
|
45
45
|
className="modal__overlay"
|
|
46
|
-
onClick={
|
|
46
|
+
onClick={
|
|
47
|
+
closeOnOverlayClick
|
|
48
|
+
? (e) => {
|
|
49
|
+
if (e.target === e.currentTarget) onClose();
|
|
50
|
+
}
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
47
53
|
/>
|
|
48
54
|
<div
|
|
49
55
|
className={`modal__content${contentClassName ? ` ${contentClassName}` : ''}`}
|