@stamprally/admin-ui 0.11.0 → 0.13.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,181 @@ 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 previousInitialRef = react.useRef(initialConfig);
30
+ const [history, setHistory] = react.useState({
31
+ past: [],
32
+ present: initialConfig,
33
+ future: []
34
+ });
35
+ const config = history.present;
36
+ react.useEffect(() => {
37
+ if (previousInitialRef.current === initialConfig) return;
38
+ previousInitialRef.current = initialConfig;
39
+ const isDirty = JSON.stringify(history.present) !== JSON.stringify(initialRef.current);
40
+ if (!isDirty) {
41
+ initialRef.current = initialConfig;
42
+ setHistory({ past: [], present: initialConfig, future: [] });
43
+ }
44
+ }, [history.present, initialConfig]);
45
+ const commit = react.useCallback(
46
+ (value) => {
47
+ setHistory((current) => {
48
+ const next = typeof value === "function" ? value(current.present) : value;
49
+ if (next === current.present) return current;
50
+ options.onChange?.(next);
51
+ return { past: [...current.past, current.present], present: next, future: [] };
52
+ });
53
+ },
54
+ [options.onChange]
55
+ );
56
+ const setConfig = commit;
57
+ const update = (patch) => commit((current) => ({ ...current, ...patch }));
58
+ const updateSpot = (spotId, patch) => commit((current) => ({
59
+ ...current,
60
+ spots: current.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
61
+ }));
62
+ const updateReward = (rewardId, patch) => commit((current) => ({
63
+ ...current,
64
+ rewards: current.rewards.map(
65
+ (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
66
+ )
67
+ }));
68
+ const addSpot = (spotData = {}) => commit((current) => {
69
+ const id = spotData.id ?? nextEntityId("spot", new Set(current.spots.map((spot) => spot.id)));
70
+ const base = newSpot(current.spots.length);
71
+ return {
72
+ ...current,
73
+ spots: [...current.spots, { ...base, ...spotData, id, orderIndex: current.spots.length }]
74
+ };
75
+ });
76
+ const removeSpot = (spotId) => commit((current) => ({
77
+ ...current,
78
+ spots: current.spots.filter((spot) => spot.id !== spotId).map((spot, index) => ({ ...spot, orderIndex: index }))
79
+ }));
80
+ const reorderSpots = (fromIndex, toIndex) => commit((current) => ({ ...current, spots: moveTo(current.spots, fromIndex, toIndex) }));
81
+ const reorderRewards = (fromIndex, toIndex) => commit((current) => ({ ...current, rewards: moveTo(current.rewards, fromIndex, toIndex) }));
82
+ const duplicateSpot = (spotId) => commit((current) => {
83
+ const source = current.spots.find((spot) => spot.id === spotId);
84
+ if (source === void 0) return current;
85
+ const id = nextEntityId("spot-copy", new Set(current.spots.map((spot) => spot.id)));
86
+ const copy = { ...structuredClone(source), id, orderIndex: current.spots.length };
87
+ return { ...current, spots: [...current.spots, copy] };
88
+ });
89
+ const addReward = (rewardData = {}) => commit((current) => {
90
+ const id = rewardData.id ?? nextEntityId("reward", new Set(current.rewards.map((item) => item.id)));
91
+ return {
92
+ ...current,
93
+ rewards: [...current.rewards, { ...newReward(current.rewards.length), ...rewardData, id }]
94
+ };
95
+ });
96
+ const removeReward = (rewardId) => commit((current) => ({
97
+ ...current,
98
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
99
+ }));
100
+ const duplicateReward = (rewardId) => commit((current) => {
101
+ const source = current.rewards.find((reward) => reward.id === rewardId);
102
+ if (source === void 0) return current;
103
+ const id = nextEntityId("reward-copy", new Set(current.rewards.map((item) => item.id)));
104
+ return { ...current, rewards: [...current.rewards, { ...structuredClone(source), id }] };
105
+ });
106
+ const addCondition = (spotId, nextCondition) => commit((current) => ({
107
+ ...current,
108
+ spots: current.spots.map(
109
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: [...spot.conditions, structuredClone(nextCondition)] }
110
+ )
111
+ }));
112
+ const removeCondition = (spotId, conditionIndex) => commit((current) => ({
113
+ ...current,
114
+ spots: current.spots.map(
115
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: spot.conditions.filter((_, index) => index !== conditionIndex) }
116
+ )
117
+ }));
118
+ const updateLocalized = (path, locale, value) => commit((current) => {
119
+ const parts = pathParts(path);
120
+ if (parts[0] === "spots" && parts.length >= 3) {
121
+ const requestedIndex = parts[1];
122
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.spots.findIndex((spot) => spot.id === requestedIndex);
123
+ const field2 = parts[2];
124
+ if (index < 0 || field2 === void 0 || current.spots[index] === void 0) return current;
125
+ return {
126
+ ...current,
127
+ spots: current.spots.map(
128
+ (spot, spotIndex) => spotIndex === index ? {
129
+ ...spot,
130
+ [field2]: localizedValue(spot[field2], locale, value)
131
+ } : spot
132
+ )
133
+ };
134
+ }
135
+ if (parts[0] === "rewards" && parts.length >= 3) {
136
+ const requestedIndex = parts[1];
137
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.rewards.findIndex((reward) => reward.id === requestedIndex);
138
+ const field2 = parts[2];
139
+ if (index < 0 || field2 === void 0 || current.rewards[index] === void 0)
140
+ return current;
141
+ return {
142
+ ...current,
143
+ rewards: current.rewards.map(
144
+ (reward, rewardIndex) => rewardIndex === index ? {
145
+ ...reward,
146
+ [field2]: localizedValue(reward[field2], locale, value)
147
+ } : reward
148
+ )
149
+ };
150
+ }
151
+ const field = parts[0];
152
+ if (field === void 0) return current;
153
+ return {
154
+ ...current,
155
+ [field]: localizedValue(current[field], locale, value)
156
+ };
157
+ });
158
+ const undo = () => setHistory((current) => {
159
+ const previous = current.past.at(-1);
160
+ if (previous === void 0) return current;
161
+ options.onChange?.(previous);
162
+ return {
163
+ past: current.past.slice(0, -1),
164
+ present: previous,
165
+ future: [current.present, ...current.future]
166
+ };
167
+ });
168
+ const redo = () => setHistory((current) => {
169
+ const next = current.future[0];
170
+ if (next === void 0) return current;
171
+ options.onChange?.(next);
172
+ return {
173
+ past: [...current.past, current.present],
174
+ present: next,
175
+ future: current.future.slice(1)
176
+ };
177
+ });
178
+ const resetConfig = (newConfig) => {
179
+ initialRef.current = newConfig;
180
+ previousInitialRef.current = newConfig;
181
+ setHistory({ past: [], present: newConfig, future: [] });
182
+ options.onChange?.(newConfig);
28
183
  };
29
184
  return {
30
185
  config,
@@ -32,17 +187,37 @@ function useAdminRallyEditor(initialConfig, options = {}) {
32
187
  update,
33
188
  updateSpot,
34
189
  updateReward,
35
- reset: () => setConfig(initialConfig),
36
- isDirty: config !== initialConfig
190
+ addSpot,
191
+ removeSpot,
192
+ reorderSpots: (fromIndex, toIndex) => reorderSpots(fromIndex, toIndex),
193
+ reorderRewards,
194
+ duplicateSpot,
195
+ addReward,
196
+ removeReward,
197
+ duplicateReward,
198
+ addCondition,
199
+ removeCondition,
200
+ updateLocalizedField: updateLocalized,
201
+ undo,
202
+ redo,
203
+ canUndo: history.past.length > 0,
204
+ canRedo: history.future.length > 0,
205
+ resetConfig,
206
+ reset: () => resetConfig(initialRef.current),
207
+ isDirty: config !== initialRef.current
37
208
  };
38
209
  }
39
210
  function useSpotEditor(spotId, options = {}) {
40
211
  const [config, setConfig] = react.useState(
41
212
  options.config ?? options.initialConfig
42
213
  );
43
- const commit = (next) => {
44
- setConfig(next);
45
- options.onChange?.(next);
214
+ const commit = (updateConfig) => {
215
+ setConfig((current) => {
216
+ if (current === void 0) return current;
217
+ const next = updateConfig(current);
218
+ options.onChange?.(next);
219
+ return next;
220
+ });
46
221
  };
47
222
  const spot = config?.spots.find((item) => item.id === spotId);
48
223
  return {
@@ -51,14 +226,17 @@ function useSpotEditor(spotId, options = {}) {
51
226
  setConfig,
52
227
  update: (patch) => {
53
228
  if (config === void 0 || spot === void 0) return;
54
- commit({
55
- ...config,
56
- spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
57
- });
229
+ commit((current) => ({
230
+ ...current,
231
+ spots: current.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
232
+ }));
58
233
  },
59
234
  remove: () => {
60
235
  if (config === void 0 || spot === void 0) return;
61
- commit({ ...config, spots: config.spots.filter((item) => item.id !== spotId) });
236
+ commit((current) => ({
237
+ ...current,
238
+ spots: current.spots.filter((item) => item.id !== spotId)
239
+ }));
62
240
  }
63
241
  };
64
242
  }
@@ -67,9 +245,13 @@ function useRewardEditor(rewardId, options = {}) {
67
245
  options.config ?? options.initialConfig
68
246
  );
69
247
  const reward = config?.rewards.find((item) => item.id === rewardId);
70
- const commit = (next) => {
71
- setConfig(next);
72
- options.onChange?.(next);
248
+ const commit = (updateConfig) => {
249
+ setConfig((current) => {
250
+ if (current === void 0) return current;
251
+ const next = updateConfig(current);
252
+ options.onChange?.(next);
253
+ return next;
254
+ });
73
255
  };
74
256
  return {
75
257
  config,
@@ -77,16 +259,19 @@ function useRewardEditor(rewardId, options = {}) {
77
259
  setConfig,
78
260
  update: (patch) => {
79
261
  if (config === void 0 || reward === void 0) return;
80
- commit({
81
- ...config,
82
- rewards: config.rewards.map(
262
+ commit((current) => ({
263
+ ...current,
264
+ rewards: current.rewards.map(
83
265
  (item) => item.id === rewardId ? { ...item, ...patch } : item
84
266
  )
85
- });
267
+ }));
86
268
  },
87
269
  remove: () => {
88
270
  if (config === void 0 || reward === void 0) return;
89
- commit({ ...config, rewards: config.rewards.filter((item) => item.id !== rewardId) });
271
+ commit((current) => ({
272
+ ...current,
273
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
274
+ }));
90
275
  }
91
276
  };
92
277
  }
@@ -115,6 +300,16 @@ function move(items, index, direction) {
115
300
  if (item !== void 0) next.splice(target, 0, item);
116
301
  return next;
117
302
  }
303
+ function moveTo(items, fromIndex, toIndex) {
304
+ if (fromIndex < 0 || fromIndex >= items.length || toIndex < 0 || toIndex >= items.length || fromIndex === toIndex)
305
+ return items;
306
+ const next = [...items];
307
+ const [item] = next.splice(fromIndex, 1);
308
+ if (item !== void 0) next.splice(toIndex, 0, item);
309
+ return next.map(
310
+ (entry, index) => typeof entry === "object" && entry !== null && "orderIndex" in entry ? { ...entry, orderIndex: index } : entry
311
+ );
312
+ }
118
313
  function ConditionEditor({
119
314
  condition: condition2,
120
315
  onChange,
@@ -277,39 +472,23 @@ function SpotItemForm({
277
472
  }
278
473
  )
279
474
  ] }),
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
- ] }),
475
+ /* @__PURE__ */ jsxRuntime.jsx(
476
+ ExternalReferencesEditor,
477
+ {
478
+ references: spot.externalReferences ?? [],
479
+ onChange: (externalReferences) => update({ externalReferences }),
480
+ label: field("externalReferences", "External references")
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsxRuntime.jsx(
484
+ MetadataSection,
485
+ {
486
+ name: `spot-${spot.id}`,
487
+ values: spot.metadata ?? {},
488
+ onChange: (metadata) => update({ metadata }),
489
+ label: field("metadata", "Metadata")
490
+ }
491
+ ),
313
492
  spot.conditions.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
314
493
  ConditionEditor,
315
494
  {
@@ -340,6 +519,92 @@ function SpotItemForm({
340
519
  onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
341
520
  ] });
342
521
  }
522
+ function newUnlockCondition(type) {
523
+ if (type === "stamp_count") return { type, count: 1 };
524
+ if (type === "stamps") return { type, stampIds: [] };
525
+ return { type, conditions: [] };
526
+ }
527
+ function RewardUnlockConditionEditor({
528
+ condition: unlock,
529
+ onChange,
530
+ onRemove
531
+ }) {
532
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "Unlock condition" }),
534
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
535
+ "Type",
536
+ /* @__PURE__ */ jsxRuntime.jsxs(
537
+ "select",
538
+ {
539
+ value: unlock.type,
540
+ onChange: (event) => onChange(newUnlockCondition(event.target.value)),
541
+ children: [
542
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "stamp_count", children: "Stamp count" }),
543
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "stamps", children: "Specific stamps" }),
544
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All conditions" }),
545
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "any", children: "Any condition" })
546
+ ]
547
+ }
548
+ )
549
+ ] }),
550
+ unlock.type === "stamp_count" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
551
+ "Count",
552
+ /* @__PURE__ */ jsxRuntime.jsx(
553
+ "input",
554
+ {
555
+ type: "number",
556
+ min: 0,
557
+ value: unlock.count,
558
+ onChange: (event) => onChange({ ...unlock, count: Number(event.target.value) })
559
+ }
560
+ )
561
+ ] }),
562
+ unlock.type === "stamps" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
563
+ "Stamp IDs",
564
+ /* @__PURE__ */ jsxRuntime.jsx(
565
+ "input",
566
+ {
567
+ value: unlock.stampIds.join(", "),
568
+ onChange: (event) => onChange({
569
+ ...unlock,
570
+ stampIds: event.target.value.split(",").map((value) => value.trim()).filter(Boolean)
571
+ })
572
+ }
573
+ )
574
+ ] }),
575
+ (unlock.type === "all" || unlock.type === "any") && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
576
+ unlock.conditions.map((child, index) => /* @__PURE__ */ jsxRuntime.jsx(
577
+ RewardUnlockConditionEditor,
578
+ {
579
+ condition: child,
580
+ onChange: (next) => onChange({
581
+ ...unlock,
582
+ conditions: unlock.conditions.map(
583
+ (current, childIndex) => childIndex === index ? next : current
584
+ )
585
+ }),
586
+ onRemove: () => onChange({
587
+ ...unlock,
588
+ conditions: unlock.conditions.filter((_, childIndex) => childIndex !== index)
589
+ })
590
+ },
591
+ `${unlock.type}-${JSON.stringify(child)}`
592
+ )),
593
+ /* @__PURE__ */ jsxRuntime.jsx(
594
+ "button",
595
+ {
596
+ type: "button",
597
+ onClick: () => onChange({
598
+ ...unlock,
599
+ conditions: [...unlock.conditions, newUnlockCondition("stamp_count")]
600
+ }),
601
+ children: "Add nested condition"
602
+ }
603
+ )
604
+ ] }),
605
+ onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: "Remove condition" })
606
+ ] });
607
+ }
343
608
  function RewardItemForm({
344
609
  reward,
345
610
  locale,
@@ -425,20 +690,36 @@ function RewardItemForm({
425
690
  }
426
691
  )
427
692
  ] }),
428
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
429
- field("unlockConditions", "Unlock conditions"),
693
+ /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
694
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: field("unlockConditions", "Unlock conditions") }),
695
+ (reward.conditions ?? []).map((unlock, index) => /* @__PURE__ */ jsxRuntime.jsx(
696
+ RewardUnlockConditionEditor,
697
+ {
698
+ condition: unlock,
699
+ onChange: (next) => onChange({
700
+ ...reward,
701
+ conditions: (reward.conditions ?? []).map(
702
+ (current, conditionIndex) => conditionIndex === index ? next : current
703
+ )
704
+ }),
705
+ onRemove: () => onChange({
706
+ ...reward,
707
+ conditions: (reward.conditions ?? []).filter(
708
+ (_, conditionIndex) => conditionIndex !== index
709
+ )
710
+ })
711
+ },
712
+ `${reward.id}-unlock-${JSON.stringify(unlock)}`
713
+ )),
430
714
  /* @__PURE__ */ jsxRuntime.jsx(
431
- "textarea",
715
+ "button",
432
716
  {
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
- }
717
+ type: "button",
718
+ onClick: () => onChange({
719
+ ...reward,
720
+ conditions: [...reward.conditions ?? [], newUnlockCondition("stamp_count")]
721
+ }),
722
+ children: field("addUnlockCondition", "Add unlock condition")
442
723
  }
443
724
  )
444
725
  ] }),
@@ -498,6 +779,257 @@ function RewardItemForm({
498
779
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
499
780
  ] });
500
781
  }
782
+ function ThemeEditor({
783
+ theme,
784
+ onChange,
785
+ locale,
786
+ dictionary
787
+ }) {
788
+ const activeLocale = locale ?? "en";
789
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
790
+ const update = (key, value) => onChange({ ...theme, [key]: value });
791
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
792
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: field("theme", "Theme") }),
793
+ [
794
+ "primaryColor",
795
+ "backgroundColor",
796
+ "cardBackgroundColor",
797
+ "textColor",
798
+ "backgroundImageUrl",
799
+ "completedStampColor"
800
+ ].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
801
+ field(key, key),
802
+ /* @__PURE__ */ jsxRuntime.jsx(
803
+ "input",
804
+ {
805
+ type: key.toLowerCase().includes("color") ? "color" : "url",
806
+ value: theme[key] ?? "",
807
+ onChange: (event) => update(key, event.target.value)
808
+ }
809
+ )
810
+ ] }, key)),
811
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
812
+ field("slotShape", "Slot shape"),
813
+ /* @__PURE__ */ jsxRuntime.jsx(
814
+ "select",
815
+ {
816
+ value: theme.slotShape,
817
+ onChange: (event) => update("slotShape", event.target.value),
818
+ children: ["circle", "square", "rounded"].map((shape) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: shape, children: field(`slotShape.${shape}`, shape) }, shape))
819
+ }
820
+ )
821
+ ] }),
822
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
823
+ field("gridColumns", "Grid columns"),
824
+ /* @__PURE__ */ jsxRuntime.jsx(
825
+ "input",
826
+ {
827
+ type: "number",
828
+ min: 1,
829
+ max: 12,
830
+ value: theme.gridColumns,
831
+ onChange: (event) => update("gridColumns", Number(event.target.value))
832
+ }
833
+ )
834
+ ] }),
835
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
836
+ field("fontFamily", "Font family"),
837
+ /* @__PURE__ */ jsxRuntime.jsx(
838
+ "select",
839
+ {
840
+ value: theme.fontFamily ?? "system-ui",
841
+ onChange: (event) => update("fontFamily", event.target.value),
842
+ children: ["system-ui", "serif", "rounded-sans", "monospace", "handwritten"].map(
843
+ (font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, children: font }, font)
844
+ )
845
+ }
846
+ )
847
+ ] }),
848
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
849
+ field("unclaimedOpacity", "Unclaimed opacity"),
850
+ /* @__PURE__ */ jsxRuntime.jsx(
851
+ "input",
852
+ {
853
+ type: "number",
854
+ min: 0,
855
+ max: 1,
856
+ step: 0.05,
857
+ value: theme.unclaimedOpacity ?? 1,
858
+ onChange: (event) => update("unclaimedOpacity", Number(event.target.value))
859
+ }
860
+ )
861
+ ] })
862
+ ] });
863
+ }
864
+ function metadataValue(value) {
865
+ return typeof value === "string" ? value : JSON.stringify(value);
866
+ }
867
+ function ExternalReferencesEditor({
868
+ references,
869
+ onChange,
870
+ label = "External references"
871
+ }) {
872
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
873
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: label }),
874
+ references.map((reference) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
875
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
876
+ "Type",
877
+ /* @__PURE__ */ jsxRuntime.jsx(
878
+ "input",
879
+ {
880
+ value: reference.type,
881
+ onChange: (event) => onChange(
882
+ references.map(
883
+ (current) => current === reference ? { ...current, type: event.target.value } : current
884
+ )
885
+ )
886
+ }
887
+ )
888
+ ] }),
889
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
890
+ "ID",
891
+ /* @__PURE__ */ jsxRuntime.jsx(
892
+ "input",
893
+ {
894
+ value: reference.id,
895
+ onChange: (event) => onChange(
896
+ references.map(
897
+ (current) => current === reference ? { ...current, id: event.target.value } : current
898
+ )
899
+ )
900
+ }
901
+ )
902
+ ] }),
903
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
904
+ "URL",
905
+ /* @__PURE__ */ jsxRuntime.jsx(
906
+ "input",
907
+ {
908
+ type: "url",
909
+ value: reference.url ?? "",
910
+ onChange: (event) => onChange(
911
+ references.map(
912
+ (current) => current === reference ? { ...current, url: event.target.value } : current
913
+ )
914
+ )
915
+ }
916
+ )
917
+ ] }),
918
+ /* @__PURE__ */ jsxRuntime.jsx(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ onClick: () => onChange(references.filter((current) => current !== reference)),
923
+ children: "Remove reference"
924
+ }
925
+ )
926
+ ] }, `${reference.type}-${reference.id}-${reference.url ?? ""}`)),
927
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => onChange([...references, { type: "", id: "" }]), children: "Add reference" })
928
+ ] });
929
+ }
930
+ function MetadataSection({
931
+ name,
932
+ values,
933
+ onChange,
934
+ label
935
+ }) {
936
+ const entries = Object.entries(values);
937
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
938
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: label }),
939
+ entries.map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
940
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
941
+ "Key",
942
+ /* @__PURE__ */ jsxRuntime.jsx(
943
+ "input",
944
+ {
945
+ value: key,
946
+ onChange: (event) => {
947
+ const next = { ...values };
948
+ delete next[key];
949
+ next[event.target.value] = value;
950
+ onChange(next);
951
+ }
952
+ }
953
+ )
954
+ ] }),
955
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
956
+ "Value",
957
+ /* @__PURE__ */ jsxRuntime.jsx(
958
+ "input",
959
+ {
960
+ value: metadataValue(value),
961
+ onChange: (event) => {
962
+ let nextValue = event.target.value;
963
+ try {
964
+ nextValue = JSON.parse(event.target.value);
965
+ } catch {
966
+ }
967
+ onChange({ ...values, [key]: nextValue });
968
+ }
969
+ }
970
+ )
971
+ ] }),
972
+ /* @__PURE__ */ jsxRuntime.jsx(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ onClick: () => {
977
+ const next = { ...values };
978
+ delete next[key];
979
+ onChange(next);
980
+ },
981
+ children: "Remove"
982
+ }
983
+ )
984
+ ] }, `${name}-${key}`)),
985
+ /* @__PURE__ */ jsxRuntime.jsx(
986
+ "button",
987
+ {
988
+ type: "button",
989
+ onClick: () => {
990
+ let index = entries.length + 1;
991
+ let key = `key${index}`;
992
+ while (key in values) {
993
+ index += 1;
994
+ key = `key${index}`;
995
+ }
996
+ onChange({ ...values, [key]: "" });
997
+ },
998
+ children: "Add field"
999
+ }
1000
+ )
1001
+ ] });
1002
+ }
1003
+ function MetadataEditor({
1004
+ publicMetadata = {},
1005
+ serverMetadata = {},
1006
+ onPublicMetadataChange,
1007
+ onServerMetadataChange,
1008
+ locale,
1009
+ dictionary
1010
+ }) {
1011
+ const activeLocale = locale ?? "en";
1012
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": text(dictionary, activeLocale, "metadata", "Metadata"), children: [
1013
+ /* @__PURE__ */ jsxRuntime.jsx(
1014
+ MetadataSection,
1015
+ {
1016
+ name: "public",
1017
+ values: publicMetadata,
1018
+ onChange: onPublicMetadataChange,
1019
+ label: "Public metadata"
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsxRuntime.jsx(
1023
+ MetadataSection,
1024
+ {
1025
+ name: "server",
1026
+ values: serverMetadata,
1027
+ onChange: onServerMetadataChange,
1028
+ label: "Server metadata"
1029
+ }
1030
+ )
1031
+ ] });
1032
+ }
501
1033
  function AdminRallyEditor({ config, onChange, locale, dictionary }) {
502
1034
  const activeLocale = locale ?? "en";
503
1035
  const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
@@ -531,45 +1063,36 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
531
1063
  }
532
1064
  )
533
1065
  ] }),
1066
+ /* @__PURE__ */ jsxRuntime.jsx(
1067
+ ThemeEditor,
1068
+ {
1069
+ theme: config.theme ?? core.DEFAULT_SHEET_THEME,
1070
+ locale: activeLocale,
1071
+ ...dictionary === void 0 ? {} : { dictionary },
1072
+ onChange: (theme) => update({ theme })
1073
+ }
1074
+ ),
534
1075
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
535
- field("theme", "Theme (JSON)"),
1076
+ field("serverEndpoint", "Server endpoint"),
536
1077
  /* @__PURE__ */ jsxRuntime.jsx(
537
- "textarea",
1078
+ "input",
538
1079
  {
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
- }
1080
+ value: config.serverEndpoint ?? "",
1081
+ onChange: (event) => update({ serverEndpoint: event.target.value })
549
1082
  }
550
1083
  )
551
1084
  ] }),
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)),
1085
+ /* @__PURE__ */ jsxRuntime.jsx(
1086
+ MetadataEditor,
1087
+ {
1088
+ publicMetadata: config.publicMetadata ?? config.metadata ?? {},
1089
+ serverMetadata: config.serverMetadata ?? {},
1090
+ locale: activeLocale,
1091
+ ...dictionary === void 0 ? {} : { dictionary },
1092
+ onPublicMetadataChange: (metadata) => update({ publicMetadata: metadata }),
1093
+ onServerMetadataChange: (metadata) => update({ serverMetadata: metadata })
1094
+ }
1095
+ ),
573
1096
  /* @__PURE__ */ jsxRuntime.jsx(
574
1097
  "button",
575
1098
  {
@@ -618,21 +1141,40 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
618
1141
  children: field("addReward", "Add reward")
619
1142
  }
620
1143
  ),
621
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
622
- RewardItemForm,
623
- {
624
- reward,
625
- locale: activeLocale,
626
- ...dictionary === void 0 ? {} : { dictionary },
627
- onChange: (next) => update({
628
- rewards: config.rewards.map(
629
- (current) => current.id === reward.id ? next : current
630
- )
631
- }),
632
- onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
633
- },
634
- reward.id
635
- )) })
1144
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1145
+ /* @__PURE__ */ jsxRuntime.jsx(
1146
+ RewardItemForm,
1147
+ {
1148
+ reward,
1149
+ locale: activeLocale,
1150
+ ...dictionary === void 0 ? {} : { dictionary },
1151
+ onChange: (next) => update({
1152
+ rewards: config.rewards.map(
1153
+ (current) => current.id === reward.id ? next : current
1154
+ )
1155
+ }),
1156
+ onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
1157
+ }
1158
+ ),
1159
+ /* @__PURE__ */ jsxRuntime.jsx(
1160
+ "button",
1161
+ {
1162
+ type: "button",
1163
+ disabled: index === 0,
1164
+ onClick: () => update({ rewards: move(config.rewards, index, -1) }),
1165
+ children: field("moveUp", "Move up")
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ jsxRuntime.jsx(
1169
+ "button",
1170
+ {
1171
+ type: "button",
1172
+ disabled: index === config.rewards.length - 1,
1173
+ onClick: () => update({ rewards: move(config.rewards, index, 1) }),
1174
+ children: field("moveDown", "Move down")
1175
+ }
1176
+ )
1177
+ ] }, reward.id)) })
636
1178
  ] });
637
1179
  }
638
1180
  var RallyEditor = AdminRallyEditor;
@@ -704,10 +1246,15 @@ function JsonConfigIO({ config, onImport, locale, dictionary }) {
704
1246
 
705
1247
  exports.AdminRallyEditor = AdminRallyEditor;
706
1248
  exports.ConditionEditor = ConditionEditor;
1249
+ exports.ExternalReferencesEditor = ExternalReferencesEditor;
707
1250
  exports.GeneralSettingsForm = GeneralSettingsForm;
708
1251
  exports.JsonConfigIO = JsonConfigIO;
1252
+ exports.MetadataEditor = MetadataEditor;
709
1253
  exports.RallyEditor = RallyEditor;
1254
+ exports.RewardItemForm = RewardItemForm;
1255
+ exports.RewardUnlockConditionEditor = RewardUnlockConditionEditor;
710
1256
  exports.SpotItemForm = SpotItemForm;
1257
+ exports.ThemeEditor = ThemeEditor;
711
1258
  exports.useAdminRallyEditor = useAdminRallyEditor;
712
1259
  exports.useRewardEditor = useRewardEditor;
713
1260
  exports.useSpotEditor = useSpotEditor;