@stamprally/admin-ui 0.11.0 → 0.12.0

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/index.js CHANGED
@@ -1,28 +1,171 @@
1
- import { resolveLocalizedText, updateLocalizedField, safeParseAdminConfig } from '@stamprally/core';
2
- import { useState } from 'react';
1
+ import { resolveLocalizedText, updateLocalizedField, DEFAULT_SHEET_THEME, safeParseAdminConfig } from '@stamprally/core';
2
+ import { useRef, useState, useCallback } from 'react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/index.tsx
6
+ function nextEntityId(prefix, ids) {
7
+ let index = ids.size + 1;
8
+ let candidate = `${prefix}-${index}`;
9
+ while (ids.has(candidate)) {
10
+ index += 1;
11
+ candidate = `${prefix}-${index}`;
12
+ }
13
+ return candidate;
14
+ }
15
+ function pathParts(path) {
16
+ return path.replaceAll("[", ".").replaceAll("]", "").split(".").filter((part) => part !== "");
17
+ }
18
+ function localizedValue(value, locale, nextValue) {
19
+ return updateLocalizedField(
20
+ typeof value === "string" || typeof value === "object" && value !== null ? value : "",
21
+ locale,
22
+ nextValue
23
+ );
24
+ }
6
25
  function useAdminRallyEditor(initialConfig, options = {}) {
7
- const [config, setConfig] = useState(initialConfig);
8
- const update = (patch) => {
9
- setConfig((current) => {
10
- const next = { ...current, ...patch };
11
- options.onChange?.(next);
12
- return next;
13
- });
14
- };
15
- const updateSpot = (spotId, patch) => {
16
- update({
17
- spots: config.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
18
- });
19
- };
20
- const updateReward = (rewardId, patch) => {
21
- update({
22
- rewards: config.rewards.map(
23
- (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
24
- )
25
- });
26
+ const initialRef = useRef(initialConfig);
27
+ const [history, setHistory] = useState({
28
+ past: [],
29
+ present: initialConfig,
30
+ future: []
31
+ });
32
+ const config = history.present;
33
+ const commit = useCallback(
34
+ (value) => {
35
+ setHistory((current) => {
36
+ const next = typeof value === "function" ? value(current.present) : value;
37
+ if (next === current.present) return current;
38
+ options.onChange?.(next);
39
+ return { past: [...current.past, current.present], present: next, future: [] };
40
+ });
41
+ },
42
+ [options.onChange]
43
+ );
44
+ const setConfig = commit;
45
+ const update = (patch) => commit((current) => ({ ...current, ...patch }));
46
+ const updateSpot = (spotId, patch) => commit((current) => ({
47
+ ...current,
48
+ spots: current.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
49
+ }));
50
+ const updateReward = (rewardId, patch) => commit((current) => ({
51
+ ...current,
52
+ rewards: current.rewards.map(
53
+ (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
54
+ )
55
+ }));
56
+ const addSpot = (spotData = {}) => commit((current) => {
57
+ const id = spotData.id ?? nextEntityId("spot", new Set(current.spots.map((spot) => spot.id)));
58
+ const base = newSpot(current.spots.length);
59
+ return {
60
+ ...current,
61
+ spots: [...current.spots, { ...base, ...spotData, id, orderIndex: current.spots.length }]
62
+ };
63
+ });
64
+ const removeSpot = (spotId) => commit((current) => ({
65
+ ...current,
66
+ spots: current.spots.filter((spot) => spot.id !== spotId).map((spot, index) => ({ ...spot, orderIndex: index }))
67
+ }));
68
+ const reorderSpots = (fromIndex, toIndex) => commit((current) => ({ ...current, spots: moveTo(current.spots, fromIndex, toIndex) }));
69
+ const duplicateSpot = (spotId) => commit((current) => {
70
+ const source = current.spots.find((spot) => spot.id === spotId);
71
+ if (source === void 0) return current;
72
+ const id = nextEntityId("spot-copy", new Set(current.spots.map((spot) => spot.id)));
73
+ const copy = { ...structuredClone(source), id, orderIndex: current.spots.length };
74
+ return { ...current, spots: [...current.spots, copy] };
75
+ });
76
+ const addReward = (rewardData = {}) => commit((current) => {
77
+ const id = rewardData.id ?? nextEntityId("reward", new Set(current.rewards.map((item) => item.id)));
78
+ return {
79
+ ...current,
80
+ rewards: [...current.rewards, { ...newReward(current.rewards.length), ...rewardData, id }]
81
+ };
82
+ });
83
+ const removeReward = (rewardId) => commit((current) => ({
84
+ ...current,
85
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
86
+ }));
87
+ const duplicateReward = (rewardId) => commit((current) => {
88
+ const source = current.rewards.find((reward) => reward.id === rewardId);
89
+ if (source === void 0) return current;
90
+ const id = nextEntityId("reward-copy", new Set(current.rewards.map((item) => item.id)));
91
+ return { ...current, rewards: [...current.rewards, { ...structuredClone(source), id }] };
92
+ });
93
+ const addCondition = (spotId, nextCondition) => commit((current) => ({
94
+ ...current,
95
+ spots: current.spots.map(
96
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: [...spot.conditions, structuredClone(nextCondition)] }
97
+ )
98
+ }));
99
+ const removeCondition = (spotId, conditionIndex) => commit((current) => ({
100
+ ...current,
101
+ spots: current.spots.map(
102
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: spot.conditions.filter((_, index) => index !== conditionIndex) }
103
+ )
104
+ }));
105
+ const updateLocalized = (path, locale, value) => commit((current) => {
106
+ const parts = pathParts(path);
107
+ if (parts[0] === "spots" && parts.length >= 3) {
108
+ const requestedIndex = parts[1];
109
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.spots.findIndex((spot) => spot.id === requestedIndex);
110
+ const field2 = parts[2];
111
+ if (index < 0 || field2 === void 0 || current.spots[index] === void 0) return current;
112
+ return {
113
+ ...current,
114
+ spots: current.spots.map(
115
+ (spot, spotIndex) => spotIndex === index ? {
116
+ ...spot,
117
+ [field2]: localizedValue(spot[field2], locale, value)
118
+ } : spot
119
+ )
120
+ };
121
+ }
122
+ if (parts[0] === "rewards" && parts.length >= 3) {
123
+ const requestedIndex = parts[1];
124
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.rewards.findIndex((reward) => reward.id === requestedIndex);
125
+ const field2 = parts[2];
126
+ if (index < 0 || field2 === void 0 || current.rewards[index] === void 0)
127
+ return current;
128
+ return {
129
+ ...current,
130
+ rewards: current.rewards.map(
131
+ (reward, rewardIndex) => rewardIndex === index ? {
132
+ ...reward,
133
+ [field2]: localizedValue(reward[field2], locale, value)
134
+ } : reward
135
+ )
136
+ };
137
+ }
138
+ const field = parts[0];
139
+ if (field === void 0) return current;
140
+ return {
141
+ ...current,
142
+ [field]: localizedValue(current[field], locale, value)
143
+ };
144
+ });
145
+ const undo = () => setHistory((current) => {
146
+ const previous = current.past.at(-1);
147
+ if (previous === void 0) return current;
148
+ options.onChange?.(previous);
149
+ return {
150
+ past: current.past.slice(0, -1),
151
+ present: previous,
152
+ future: [current.present, ...current.future]
153
+ };
154
+ });
155
+ const redo = () => setHistory((current) => {
156
+ const next = current.future[0];
157
+ if (next === void 0) return current;
158
+ options.onChange?.(next);
159
+ return {
160
+ past: [...current.past, current.present],
161
+ present: next,
162
+ future: current.future.slice(1)
163
+ };
164
+ });
165
+ const resetConfig = (newConfig) => {
166
+ initialRef.current = newConfig;
167
+ setHistory({ past: [], present: newConfig, future: [] });
168
+ options.onChange?.(newConfig);
26
169
  };
27
170
  return {
28
171
  config,
@@ -30,8 +173,23 @@ function useAdminRallyEditor(initialConfig, options = {}) {
30
173
  update,
31
174
  updateSpot,
32
175
  updateReward,
33
- reset: () => setConfig(initialConfig),
34
- isDirty: config !== initialConfig
176
+ addSpot,
177
+ removeSpot,
178
+ reorderSpots: (fromIndex, toIndex) => reorderSpots(fromIndex, toIndex),
179
+ duplicateSpot,
180
+ addReward,
181
+ removeReward,
182
+ duplicateReward,
183
+ addCondition,
184
+ removeCondition,
185
+ updateLocalizedField: updateLocalized,
186
+ undo,
187
+ redo,
188
+ canUndo: history.past.length > 0,
189
+ canRedo: history.future.length > 0,
190
+ resetConfig,
191
+ reset: () => resetConfig(initialRef.current),
192
+ isDirty: config !== initialRef.current
35
193
  };
36
194
  }
37
195
  function useSpotEditor(spotId, options = {}) {
@@ -113,6 +271,16 @@ function move(items, index, direction) {
113
271
  if (item !== void 0) next.splice(target, 0, item);
114
272
  return next;
115
273
  }
274
+ function moveTo(items, fromIndex, toIndex) {
275
+ if (fromIndex < 0 || fromIndex >= items.length || toIndex < 0 || toIndex >= items.length || fromIndex === toIndex)
276
+ return items;
277
+ const next = [...items];
278
+ const [item] = next.splice(fromIndex, 1);
279
+ if (item !== void 0) next.splice(toIndex, 0, item);
280
+ return next.map(
281
+ (entry, index) => typeof entry === "object" && entry !== null && "orderIndex" in entry ? { ...entry, orderIndex: index } : entry
282
+ );
283
+ }
116
284
  function ConditionEditor({
117
285
  condition: condition2,
118
286
  onChange,
@@ -275,39 +443,23 @@ function SpotItemForm({
275
443
  }
276
444
  )
277
445
  ] }),
278
- /* @__PURE__ */ jsxs("label", { children: [
279
- field("externalReferences", "External references"),
280
- /* @__PURE__ */ jsx(
281
- "textarea",
282
- {
283
- value: JSON.stringify(spot.externalReferences ?? [], null, 2),
284
- onChange: (event) => {
285
- try {
286
- const parsed = JSON.parse(event.target.value);
287
- if (Array.isArray(parsed)) update({ externalReferences: parsed });
288
- } catch {
289
- }
290
- }
291
- }
292
- )
293
- ] }),
294
- /* @__PURE__ */ jsxs("label", { children: [
295
- field("metadata", "Metadata"),
296
- /* @__PURE__ */ jsx(
297
- "textarea",
298
- {
299
- value: JSON.stringify(spot.metadata ?? {}, null, 2),
300
- onChange: (event) => {
301
- try {
302
- const parsed = JSON.parse(event.target.value);
303
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
304
- update({ metadata: parsed });
305
- } catch {
306
- }
307
- }
308
- }
309
- )
310
- ] }),
446
+ /* @__PURE__ */ jsx(
447
+ ExternalReferencesEditor,
448
+ {
449
+ references: spot.externalReferences ?? [],
450
+ onChange: (externalReferences) => update({ externalReferences }),
451
+ label: field("externalReferences", "External references")
452
+ }
453
+ ),
454
+ /* @__PURE__ */ jsx(
455
+ MetadataSection,
456
+ {
457
+ name: `spot-${spot.id}`,
458
+ values: spot.metadata ?? {},
459
+ onChange: (metadata) => update({ metadata }),
460
+ label: field("metadata", "Metadata")
461
+ }
462
+ ),
311
463
  spot.conditions.map((item, index) => /* @__PURE__ */ jsx(
312
464
  ConditionEditor,
313
465
  {
@@ -338,6 +490,92 @@ function SpotItemForm({
338
490
  onRemove !== void 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
339
491
  ] });
340
492
  }
493
+ function newUnlockCondition(type) {
494
+ if (type === "stamp_count") return { type, count: 1 };
495
+ if (type === "stamps") return { type, stampIds: [] };
496
+ return { type, conditions: [] };
497
+ }
498
+ function RewardUnlockConditionEditor({
499
+ condition: unlock,
500
+ onChange,
501
+ onRemove
502
+ }) {
503
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
504
+ /* @__PURE__ */ jsx("legend", { children: "Unlock condition" }),
505
+ /* @__PURE__ */ jsxs("label", { children: [
506
+ "Type",
507
+ /* @__PURE__ */ jsxs(
508
+ "select",
509
+ {
510
+ value: unlock.type,
511
+ onChange: (event) => onChange(newUnlockCondition(event.target.value)),
512
+ children: [
513
+ /* @__PURE__ */ jsx("option", { value: "stamp_count", children: "Stamp count" }),
514
+ /* @__PURE__ */ jsx("option", { value: "stamps", children: "Specific stamps" }),
515
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All conditions" }),
516
+ /* @__PURE__ */ jsx("option", { value: "any", children: "Any condition" })
517
+ ]
518
+ }
519
+ )
520
+ ] }),
521
+ unlock.type === "stamp_count" && /* @__PURE__ */ jsxs("label", { children: [
522
+ "Count",
523
+ /* @__PURE__ */ jsx(
524
+ "input",
525
+ {
526
+ type: "number",
527
+ min: 0,
528
+ value: unlock.count,
529
+ onChange: (event) => onChange({ ...unlock, count: Number(event.target.value) })
530
+ }
531
+ )
532
+ ] }),
533
+ unlock.type === "stamps" && /* @__PURE__ */ jsxs("label", { children: [
534
+ "Stamp IDs",
535
+ /* @__PURE__ */ jsx(
536
+ "input",
537
+ {
538
+ value: unlock.stampIds.join(", "),
539
+ onChange: (event) => onChange({
540
+ ...unlock,
541
+ stampIds: event.target.value.split(",").map((value) => value.trim()).filter(Boolean)
542
+ })
543
+ }
544
+ )
545
+ ] }),
546
+ (unlock.type === "all" || unlock.type === "any") && /* @__PURE__ */ jsxs(Fragment, { children: [
547
+ unlock.conditions.map((child, index) => /* @__PURE__ */ jsx(
548
+ RewardUnlockConditionEditor,
549
+ {
550
+ condition: child,
551
+ onChange: (next) => onChange({
552
+ ...unlock,
553
+ conditions: unlock.conditions.map(
554
+ (current, childIndex) => childIndex === index ? next : current
555
+ )
556
+ }),
557
+ onRemove: () => onChange({
558
+ ...unlock,
559
+ conditions: unlock.conditions.filter((_, childIndex) => childIndex !== index)
560
+ })
561
+ },
562
+ `${unlock.type}-${JSON.stringify(child)}`
563
+ )),
564
+ /* @__PURE__ */ jsx(
565
+ "button",
566
+ {
567
+ type: "button",
568
+ onClick: () => onChange({
569
+ ...unlock,
570
+ conditions: [...unlock.conditions, newUnlockCondition("stamp_count")]
571
+ }),
572
+ children: "Add nested condition"
573
+ }
574
+ )
575
+ ] }),
576
+ onRemove !== void 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: "Remove condition" })
577
+ ] });
578
+ }
341
579
  function RewardItemForm({
342
580
  reward,
343
581
  locale,
@@ -423,20 +661,36 @@ function RewardItemForm({
423
661
  }
424
662
  )
425
663
  ] }),
426
- /* @__PURE__ */ jsxs("label", { children: [
427
- field("unlockConditions", "Unlock conditions"),
664
+ /* @__PURE__ */ jsxs("fieldset", { children: [
665
+ /* @__PURE__ */ jsx("legend", { children: field("unlockConditions", "Unlock conditions") }),
666
+ (reward.conditions ?? []).map((unlock, index) => /* @__PURE__ */ jsx(
667
+ RewardUnlockConditionEditor,
668
+ {
669
+ condition: unlock,
670
+ onChange: (next) => onChange({
671
+ ...reward,
672
+ conditions: (reward.conditions ?? []).map(
673
+ (current, conditionIndex) => conditionIndex === index ? next : current
674
+ )
675
+ }),
676
+ onRemove: () => onChange({
677
+ ...reward,
678
+ conditions: (reward.conditions ?? []).filter(
679
+ (_, conditionIndex) => conditionIndex !== index
680
+ )
681
+ })
682
+ },
683
+ `${reward.id}-unlock-${JSON.stringify(unlock)}`
684
+ )),
428
685
  /* @__PURE__ */ jsx(
429
- "textarea",
686
+ "button",
430
687
  {
431
- value: JSON.stringify(reward.conditions ?? [], null, 2),
432
- onChange: (event) => {
433
- try {
434
- const parsed = JSON.parse(event.target.value);
435
- if (Array.isArray(parsed))
436
- onChange({ ...reward, conditions: parsed });
437
- } catch {
438
- }
439
- }
688
+ type: "button",
689
+ onClick: () => onChange({
690
+ ...reward,
691
+ conditions: [...reward.conditions ?? [], newUnlockCondition("stamp_count")]
692
+ }),
693
+ children: field("addUnlockCondition", "Add unlock condition")
440
694
  }
441
695
  )
442
696
  ] }),
@@ -496,6 +750,257 @@ function RewardItemForm({
496
750
  /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
497
751
  ] });
498
752
  }
753
+ function ThemeEditor({
754
+ theme,
755
+ onChange,
756
+ locale,
757
+ dictionary
758
+ }) {
759
+ const activeLocale = locale ?? "en";
760
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
761
+ const update = (key, value) => onChange({ ...theme, [key]: value });
762
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
763
+ /* @__PURE__ */ jsx("legend", { children: field("theme", "Theme") }),
764
+ [
765
+ "primaryColor",
766
+ "backgroundColor",
767
+ "cardBackgroundColor",
768
+ "textColor",
769
+ "backgroundImageUrl",
770
+ "completedStampColor"
771
+ ].map((key) => /* @__PURE__ */ jsxs("label", { children: [
772
+ field(key, key),
773
+ /* @__PURE__ */ jsx(
774
+ "input",
775
+ {
776
+ type: key.toLowerCase().includes("color") ? "color" : "url",
777
+ value: theme[key] ?? "",
778
+ onChange: (event) => update(key, event.target.value)
779
+ }
780
+ )
781
+ ] }, key)),
782
+ /* @__PURE__ */ jsxs("label", { children: [
783
+ field("slotShape", "Slot shape"),
784
+ /* @__PURE__ */ jsx(
785
+ "select",
786
+ {
787
+ value: theme.slotShape,
788
+ onChange: (event) => update("slotShape", event.target.value),
789
+ children: ["circle", "square", "rounded"].map((shape) => /* @__PURE__ */ jsx("option", { value: shape, children: field(`slotShape.${shape}`, shape) }, shape))
790
+ }
791
+ )
792
+ ] }),
793
+ /* @__PURE__ */ jsxs("label", { children: [
794
+ field("gridColumns", "Grid columns"),
795
+ /* @__PURE__ */ jsx(
796
+ "input",
797
+ {
798
+ type: "number",
799
+ min: 1,
800
+ max: 12,
801
+ value: theme.gridColumns,
802
+ onChange: (event) => update("gridColumns", Number(event.target.value))
803
+ }
804
+ )
805
+ ] }),
806
+ /* @__PURE__ */ jsxs("label", { children: [
807
+ field("fontFamily", "Font family"),
808
+ /* @__PURE__ */ jsx(
809
+ "select",
810
+ {
811
+ value: theme.fontFamily ?? "system-ui",
812
+ onChange: (event) => update("fontFamily", event.target.value),
813
+ children: ["system-ui", "serif", "rounded-sans", "monospace", "handwritten"].map(
814
+ (font) => /* @__PURE__ */ jsx("option", { value: font, children: font }, font)
815
+ )
816
+ }
817
+ )
818
+ ] }),
819
+ /* @__PURE__ */ jsxs("label", { children: [
820
+ field("unclaimedOpacity", "Unclaimed opacity"),
821
+ /* @__PURE__ */ jsx(
822
+ "input",
823
+ {
824
+ type: "number",
825
+ min: 0,
826
+ max: 1,
827
+ step: 0.05,
828
+ value: theme.unclaimedOpacity ?? 1,
829
+ onChange: (event) => update("unclaimedOpacity", Number(event.target.value))
830
+ }
831
+ )
832
+ ] })
833
+ ] });
834
+ }
835
+ function metadataValue(value) {
836
+ return typeof value === "string" ? value : JSON.stringify(value);
837
+ }
838
+ function ExternalReferencesEditor({
839
+ references,
840
+ onChange,
841
+ label = "External references"
842
+ }) {
843
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
844
+ /* @__PURE__ */ jsx("legend", { children: label }),
845
+ references.map((reference) => /* @__PURE__ */ jsxs("div", { children: [
846
+ /* @__PURE__ */ jsxs("label", { children: [
847
+ "Type",
848
+ /* @__PURE__ */ jsx(
849
+ "input",
850
+ {
851
+ value: reference.type,
852
+ onChange: (event) => onChange(
853
+ references.map(
854
+ (current) => current === reference ? { ...current, type: event.target.value } : current
855
+ )
856
+ )
857
+ }
858
+ )
859
+ ] }),
860
+ /* @__PURE__ */ jsxs("label", { children: [
861
+ "ID",
862
+ /* @__PURE__ */ jsx(
863
+ "input",
864
+ {
865
+ value: reference.id,
866
+ onChange: (event) => onChange(
867
+ references.map(
868
+ (current) => current === reference ? { ...current, id: event.target.value } : current
869
+ )
870
+ )
871
+ }
872
+ )
873
+ ] }),
874
+ /* @__PURE__ */ jsxs("label", { children: [
875
+ "URL",
876
+ /* @__PURE__ */ jsx(
877
+ "input",
878
+ {
879
+ type: "url",
880
+ value: reference.url ?? "",
881
+ onChange: (event) => onChange(
882
+ references.map(
883
+ (current) => current === reference ? { ...current, url: event.target.value } : current
884
+ )
885
+ )
886
+ }
887
+ )
888
+ ] }),
889
+ /* @__PURE__ */ jsx(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ onClick: () => onChange(references.filter((current) => current !== reference)),
894
+ children: "Remove reference"
895
+ }
896
+ )
897
+ ] }, `${reference.type}-${reference.id}-${reference.url ?? ""}`)),
898
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onChange([...references, { type: "", id: "" }]), children: "Add reference" })
899
+ ] });
900
+ }
901
+ function MetadataSection({
902
+ name,
903
+ values,
904
+ onChange,
905
+ label
906
+ }) {
907
+ const entries = Object.entries(values);
908
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
909
+ /* @__PURE__ */ jsx("legend", { children: label }),
910
+ entries.map(([key, value]) => /* @__PURE__ */ jsxs("div", { children: [
911
+ /* @__PURE__ */ jsxs("label", { children: [
912
+ "Key",
913
+ /* @__PURE__ */ jsx(
914
+ "input",
915
+ {
916
+ value: key,
917
+ onChange: (event) => {
918
+ const next = { ...values };
919
+ delete next[key];
920
+ next[event.target.value] = value;
921
+ onChange(next);
922
+ }
923
+ }
924
+ )
925
+ ] }),
926
+ /* @__PURE__ */ jsxs("label", { children: [
927
+ "Value",
928
+ /* @__PURE__ */ jsx(
929
+ "input",
930
+ {
931
+ value: metadataValue(value),
932
+ onChange: (event) => {
933
+ let nextValue = event.target.value;
934
+ try {
935
+ nextValue = JSON.parse(event.target.value);
936
+ } catch {
937
+ }
938
+ onChange({ ...values, [key]: nextValue });
939
+ }
940
+ }
941
+ )
942
+ ] }),
943
+ /* @__PURE__ */ jsx(
944
+ "button",
945
+ {
946
+ type: "button",
947
+ onClick: () => {
948
+ const next = { ...values };
949
+ delete next[key];
950
+ onChange(next);
951
+ },
952
+ children: "Remove"
953
+ }
954
+ )
955
+ ] }, `${name}-${key}`)),
956
+ /* @__PURE__ */ jsx(
957
+ "button",
958
+ {
959
+ type: "button",
960
+ onClick: () => {
961
+ let index = entries.length + 1;
962
+ let key = `key${index}`;
963
+ while (key in values) {
964
+ index += 1;
965
+ key = `key${index}`;
966
+ }
967
+ onChange({ ...values, [key]: "" });
968
+ },
969
+ children: "Add field"
970
+ }
971
+ )
972
+ ] });
973
+ }
974
+ function MetadataEditor({
975
+ publicMetadata = {},
976
+ serverMetadata = {},
977
+ onPublicMetadataChange,
978
+ onServerMetadataChange,
979
+ locale,
980
+ dictionary
981
+ }) {
982
+ const activeLocale = locale ?? "en";
983
+ return /* @__PURE__ */ jsxs("section", { "aria-label": text(dictionary, activeLocale, "metadata", "Metadata"), children: [
984
+ /* @__PURE__ */ jsx(
985
+ MetadataSection,
986
+ {
987
+ name: "public",
988
+ values: publicMetadata,
989
+ onChange: onPublicMetadataChange,
990
+ label: "Public metadata"
991
+ }
992
+ ),
993
+ /* @__PURE__ */ jsx(
994
+ MetadataSection,
995
+ {
996
+ name: "server",
997
+ values: serverMetadata,
998
+ onChange: onServerMetadataChange,
999
+ label: "Server metadata"
1000
+ }
1001
+ )
1002
+ ] });
1003
+ }
499
1004
  function AdminRallyEditor({ config, onChange, locale, dictionary }) {
500
1005
  const activeLocale = locale ?? "en";
501
1006
  const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
@@ -529,45 +1034,36 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
529
1034
  }
530
1035
  )
531
1036
  ] }),
1037
+ /* @__PURE__ */ jsx(
1038
+ ThemeEditor,
1039
+ {
1040
+ theme: config.theme ?? DEFAULT_SHEET_THEME,
1041
+ locale: activeLocale,
1042
+ ...dictionary === void 0 ? {} : { dictionary },
1043
+ onChange: (theme) => update({ theme })
1044
+ }
1045
+ ),
532
1046
  /* @__PURE__ */ jsxs("label", { children: [
533
- field("theme", "Theme (JSON)"),
1047
+ field("serverEndpoint", "Server endpoint"),
534
1048
  /* @__PURE__ */ jsx(
535
- "textarea",
1049
+ "input",
536
1050
  {
537
- "aria-label": field("theme", "Theme (JSON)"),
538
- value: JSON.stringify(config.theme ?? {}, null, 2),
539
- onChange: (event) => {
540
- try {
541
- const parsed = JSON.parse(event.target.value);
542
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
543
- update({ theme: parsed });
544
- } catch {
545
- }
546
- }
1051
+ value: config.serverEndpoint ?? "",
1052
+ onChange: (event) => update({ serverEndpoint: event.target.value })
547
1053
  }
548
1054
  )
549
1055
  ] }),
550
- ["serverEndpoint", "publicMetadata", "serverMetadata"].map((key) => /* @__PURE__ */ jsxs("label", { children: [
551
- field(key, key),
552
- /* @__PURE__ */ jsx(
553
- "textarea",
554
- {
555
- value: key === "serverEndpoint" ? config.serverEndpoint ?? "" : JSON.stringify(config[key] ?? {}, null, 2),
556
- onChange: (event) => {
557
- if (key === "serverEndpoint") {
558
- update({ serverEndpoint: event.target.value });
559
- return;
560
- }
561
- try {
562
- const parsed = JSON.parse(event.target.value);
563
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
564
- update({ [key]: parsed });
565
- } catch {
566
- }
567
- }
568
- }
569
- )
570
- ] }, key)),
1056
+ /* @__PURE__ */ jsx(
1057
+ MetadataEditor,
1058
+ {
1059
+ publicMetadata: config.publicMetadata ?? config.metadata ?? {},
1060
+ serverMetadata: config.serverMetadata ?? {},
1061
+ locale: activeLocale,
1062
+ ...dictionary === void 0 ? {} : { dictionary },
1063
+ onPublicMetadataChange: (metadata) => update({ publicMetadata: metadata }),
1064
+ onServerMetadataChange: (metadata) => update({ serverMetadata: metadata })
1065
+ }
1066
+ ),
571
1067
  /* @__PURE__ */ jsx(
572
1068
  "button",
573
1069
  {
@@ -700,6 +1196,6 @@ function JsonConfigIO({ config, onImport, locale, dictionary }) {
700
1196
  ] });
701
1197
  }
702
1198
 
703
- export { AdminRallyEditor, ConditionEditor, GeneralSettingsForm, JsonConfigIO, RallyEditor, SpotItemForm, useAdminRallyEditor, useRewardEditor, useSpotEditor };
1199
+ export { AdminRallyEditor, ConditionEditor, ExternalReferencesEditor, GeneralSettingsForm, JsonConfigIO, MetadataEditor, RallyEditor, RewardItemForm, RewardUnlockConditionEditor, SpotItemForm, ThemeEditor, useAdminRallyEditor, useRewardEditor, useSpotEditor };
704
1200
  //# sourceMappingURL=index.js.map
705
1201
  //# sourceMappingURL=index.js.map