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