@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.
Files changed (79) hide show
  1. package/dist/assets/prompt-scheme-onboard.generated.d.ts +1 -0
  2. package/dist/assets/prompt-scheme-paywall.generated.d.ts +1 -0
  3. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +3 -3
  4. package/dist/build-components/patterns.generated.d.ts +52 -52
  5. package/dist/components/BuilderProvider.d.ts +2 -4
  6. package/dist/index.cjs.js +1 -1
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.esm.js +1 -1
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.web.cjs.js +6 -6
  11. package/dist/index.web.cjs.js.map +1 -1
  12. package/dist/index.web.esm.js +6 -6
  13. package/dist/index.web.esm.js.map +1 -1
  14. package/dist/modals/PromptManagerModal.d.ts +9 -0
  15. package/dist/modals/index.d.ts +1 -0
  16. package/dist/styles.css +1 -1
  17. package/dist/utils/nodeXml.d.ts +11 -0
  18. package/package.json +5 -1
  19. package/scripts/prebuild/assets/prompt_scheme.md +77 -0
  20. package/scripts/prebuild/generate-prompt-schemes.js +464 -0
  21. package/scripts/prebuild/prebuild.js +4 -0
  22. package/src/RenderPage.tsx +6 -6
  23. package/src/assets/meta.json +1 -1
  24. package/src/assets/prompt-scheme-onboard.generated.ts +4 -0
  25. package/src/assets/prompt-scheme-paywall.generated.ts +4 -0
  26. package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -1
  27. package/src/attribute-analyser/style/native/useExtractTextStyle.ts +1 -1
  28. package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -1
  29. package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -1
  30. package/src/attribute-analyser/style/web/useExtractTextStyle.ts +1 -1
  31. package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -1
  32. package/src/build-components/BIcon/pattern.json +1 -3
  33. package/src/build-components/BackgroundImage/pattern.json +2 -10
  34. package/src/build-components/Button/pattern.json +1 -3
  35. package/src/build-components/Carousel/pattern.json +2 -8
  36. package/src/build-components/CarouselDots/CarouselDots.tsx +1 -1
  37. package/src/build-components/CarouselProvider/pattern.json +1 -4
  38. package/src/build-components/CountDown/pattern.json +1 -3
  39. package/src/build-components/Image/pattern.json +2 -9
  40. package/src/build-components/Main/pattern.json +1 -3
  41. package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +1 -1
  42. package/src/build-components/NavigationBarColor/pattern.json +1 -3
  43. package/src/build-components/Onboard/pattern.json +2 -6
  44. package/src/build-components/OnboardButton/OnboardButton.tsx +1 -1
  45. package/src/build-components/OnboardButton/pattern.json +3 -14
  46. package/src/build-components/OnboardButtons/pattern.json +4 -15
  47. package/src/build-components/OnboardDot/OnboardDot.tsx +1 -1
  48. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +3 -3
  49. package/src/build-components/OnboardDot/pattern.json +15 -16
  50. package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
  51. package/src/build-components/OnboardFooter/pattern.json +15 -19
  52. package/src/build-components/OnboardItem/pattern.json +3 -11
  53. package/src/build-components/OnboardProvider/pattern.json +2 -8
  54. package/src/build-components/OnboardSubtitle/pattern.json +1 -4
  55. package/src/build-components/OnboardTitle/pattern.json +1 -4
  56. package/src/build-components/PaywallBackground/pattern.json +1 -3
  57. package/src/build-components/PaywallCloseButton/pattern.json +1 -3
  58. package/src/build-components/PaywallOptions/pattern.json +1 -3
  59. package/src/build-components/PaywallProvider/pattern.json +1 -3
  60. package/src/build-components/PaywallSubscribeButton/pattern.json +1 -3
  61. package/src/build-components/PriceTag/pattern.json +2 -8
  62. package/src/build-components/Pricing/pattern.json +1 -3
  63. package/src/build-components/Promo/pattern.json +1 -3
  64. package/src/build-components/Separator/pattern.json +1 -3
  65. package/src/build-components/StatusBarColor/StatusBarColor.tsx +1 -1
  66. package/src/build-components/StatusBarColor/pattern.json +1 -3
  67. package/src/build-components/Text/pattern.json +1 -3
  68. package/src/build-components/View/pattern.json +4 -16
  69. package/src/build-components/patterns.generated.ts +52 -52
  70. package/src/components/BottomBar.tsx +28 -1
  71. package/src/components/BuilderProvider.tsx +5 -14
  72. package/src/hooks/useLocalize.ts +1 -1
  73. package/src/modals/MockableFeatureModal.tsx +552 -5
  74. package/src/modals/Modal.tsx +7 -1
  75. package/src/modals/PromptManagerModal.tsx +270 -0
  76. package/src/modals/index.ts +1 -0
  77. package/src/styles/index.scss +1 -0
  78. package/src/styles/modals/_prompt-manager-modal.scss +95 -0
  79. 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 === 'products' ? (
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
- <p style={{ margin: 0, opacity: 0.7 }}>
226
- No mock UI yet for “{featureKey}”.
227
- </p>
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
+ }
@@ -43,7 +43,13 @@ export function Modal({
43
43
  >
44
44
  <div
45
45
  className="modal__overlay"
46
- onClick={closeOnOverlayClick ? onClose : undefined}
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}` : ''}`}