@stamprally/admin-ui 0.10.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,8 +1,251 @@
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
+ }
25
+ function useAdminRallyEditor(initialConfig, options = {}) {
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);
169
+ };
170
+ return {
171
+ config,
172
+ setConfig,
173
+ update,
174
+ updateSpot,
175
+ updateReward,
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
193
+ };
194
+ }
195
+ function useSpotEditor(spotId, options = {}) {
196
+ const [config, setConfig] = useState(
197
+ options.config ?? options.initialConfig
198
+ );
199
+ const commit = (next) => {
200
+ setConfig(next);
201
+ options.onChange?.(next);
202
+ };
203
+ const spot = config?.spots.find((item) => item.id === spotId);
204
+ return {
205
+ config,
206
+ spot,
207
+ setConfig,
208
+ update: (patch) => {
209
+ if (config === void 0 || spot === void 0) return;
210
+ commit({
211
+ ...config,
212
+ spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
213
+ });
214
+ },
215
+ remove: () => {
216
+ if (config === void 0 || spot === void 0) return;
217
+ commit({ ...config, spots: config.spots.filter((item) => item.id !== spotId) });
218
+ }
219
+ };
220
+ }
221
+ function useRewardEditor(rewardId, options = {}) {
222
+ const [config, setConfig] = useState(
223
+ options.config ?? options.initialConfig
224
+ );
225
+ const reward = config?.rewards.find((item) => item.id === rewardId);
226
+ const commit = (next) => {
227
+ setConfig(next);
228
+ options.onChange?.(next);
229
+ };
230
+ return {
231
+ config,
232
+ reward,
233
+ setConfig,
234
+ update: (patch) => {
235
+ if (config === void 0 || reward === void 0) return;
236
+ commit({
237
+ ...config,
238
+ rewards: config.rewards.map(
239
+ (item) => item.id === rewardId ? { ...item, ...patch } : item
240
+ )
241
+ });
242
+ },
243
+ remove: () => {
244
+ if (config === void 0 || reward === void 0) return;
245
+ commit({ ...config, rewards: config.rewards.filter((item) => item.id !== rewardId) });
246
+ }
247
+ };
248
+ }
6
249
  function text(dictionary, locale, key, fallback) {
7
250
  return dictionary?.[locale]?.[key] ?? fallback;
8
251
  }
@@ -28,6 +271,16 @@ function move(items, index, direction) {
28
271
  if (item !== void 0) next.splice(target, 0, item);
29
272
  return next;
30
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
+ }
31
284
  function ConditionEditor({
32
285
  condition: condition2,
33
286
  onChange,
@@ -156,6 +409,18 @@ function SpotItemForm({
156
409
  }
157
410
  )
158
411
  ] }),
412
+ ["description", "hint"].map((key) => /* @__PURE__ */ jsxs("label", { children: [
413
+ field(key, key),
414
+ /* @__PURE__ */ jsx(
415
+ "textarea",
416
+ {
417
+ value: resolveLocalizedText(spot[key] ?? "", activeLocale),
418
+ onChange: (event) => update({
419
+ [key]: updateLocalizedField(spot[key] ?? "", activeLocale, event.target.value)
420
+ })
421
+ }
422
+ )
423
+ ] }, key)),
159
424
  ["imageUrl", "iconUrl", "redirectUrlAfterClaim"].map((key) => /* @__PURE__ */ jsxs("label", { children: [
160
425
  field(key, key),
161
426
  /* @__PURE__ */ jsx(
@@ -178,39 +443,23 @@ function SpotItemForm({
178
443
  }
179
444
  )
180
445
  ] }),
181
- /* @__PURE__ */ jsxs("label", { children: [
182
- field("externalReferences", "External references"),
183
- /* @__PURE__ */ jsx(
184
- "textarea",
185
- {
186
- value: JSON.stringify(spot.externalReferences ?? [], null, 2),
187
- onChange: (event) => {
188
- try {
189
- const parsed = JSON.parse(event.target.value);
190
- if (Array.isArray(parsed)) update({ externalReferences: parsed });
191
- } catch {
192
- }
193
- }
194
- }
195
- )
196
- ] }),
197
- /* @__PURE__ */ jsxs("label", { children: [
198
- field("metadata", "Metadata"),
199
- /* @__PURE__ */ jsx(
200
- "textarea",
201
- {
202
- value: JSON.stringify(spot.metadata ?? {}, null, 2),
203
- onChange: (event) => {
204
- try {
205
- const parsed = JSON.parse(event.target.value);
206
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
207
- update({ metadata: parsed });
208
- } catch {
209
- }
210
- }
211
- }
212
- )
213
- ] }),
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
+ ),
214
463
  spot.conditions.map((item, index) => /* @__PURE__ */ jsx(
215
464
  ConditionEditor,
216
465
  {
@@ -241,6 +490,92 @@ function SpotItemForm({
241
490
  onRemove !== void 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
242
491
  ] });
243
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
+ }
244
579
  function RewardItemForm({
245
580
  reward,
246
581
  locale,
@@ -264,6 +599,23 @@ function RewardItemForm({
264
599
  }
265
600
  )
266
601
  ] }),
602
+ /* @__PURE__ */ jsxs("label", { children: [
603
+ field("description", "Description"),
604
+ /* @__PURE__ */ jsx(
605
+ "textarea",
606
+ {
607
+ value: resolveLocalizedText(reward.description ?? "", locale),
608
+ onChange: (event) => onChange({
609
+ ...reward,
610
+ description: updateLocalizedField(
611
+ reward.description ?? "",
612
+ locale,
613
+ event.target.value
614
+ )
615
+ })
616
+ }
617
+ )
618
+ ] }),
267
619
  /* @__PURE__ */ jsxs("label", { children: [
268
620
  field("requiredSpotCount", "Required spot count"),
269
621
  /* @__PURE__ */ jsx(
@@ -309,20 +661,36 @@ function RewardItemForm({
309
661
  }
310
662
  )
311
663
  ] }),
312
- /* @__PURE__ */ jsxs("label", { children: [
313
- 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
+ )),
314
685
  /* @__PURE__ */ jsx(
315
- "textarea",
686
+ "button",
316
687
  {
317
- value: JSON.stringify(reward.conditions ?? [], null, 2),
318
- onChange: (event) => {
319
- try {
320
- const parsed = JSON.parse(event.target.value);
321
- if (Array.isArray(parsed))
322
- onChange({ ...reward, conditions: parsed });
323
- } catch {
324
- }
325
- }
688
+ type: "button",
689
+ onClick: () => onChange({
690
+ ...reward,
691
+ conditions: [...reward.conditions ?? [], newUnlockCondition("stamp_count")]
692
+ }),
693
+ children: field("addUnlockCondition", "Add unlock condition")
326
694
  }
327
695
  )
328
696
  ] }),
@@ -343,6 +711,16 @@ function RewardItemForm({
343
711
  }
344
712
  )
345
713
  ] }, key)),
714
+ /* @__PURE__ */ jsxs("label", { children: [
715
+ field("digitalContentUrl", "Digital content URL"),
716
+ /* @__PURE__ */ jsx(
717
+ "input",
718
+ {
719
+ value: reward.digitalContentUrl ?? "",
720
+ onChange: (event) => onChange({ ...reward, digitalContentUrl: event.target.value })
721
+ }
722
+ )
723
+ ] }),
346
724
  /* @__PURE__ */ jsxs("label", { children: [
347
725
  field("staffPasscode", "Staff passcode"),
348
726
  /* @__PURE__ */ jsx(
@@ -372,6 +750,257 @@ function RewardItemForm({
372
750
  /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
373
751
  ] });
374
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
+ }
375
1004
  function AdminRallyEditor({ config, onChange, locale, dictionary }) {
376
1005
  const activeLocale = locale ?? "en";
377
1006
  const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
@@ -389,6 +1018,52 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
389
1018
  }
390
1019
  )
391
1020
  ] }),
1021
+ /* @__PURE__ */ jsxs("label", { children: [
1022
+ field("description", "Description"),
1023
+ /* @__PURE__ */ jsx(
1024
+ "textarea",
1025
+ {
1026
+ value: resolveLocalizedText(config.description ?? "", activeLocale),
1027
+ onChange: (event) => update({
1028
+ description: updateLocalizedField(
1029
+ config.description ?? "",
1030
+ activeLocale,
1031
+ event.target.value
1032
+ )
1033
+ })
1034
+ }
1035
+ )
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
+ ),
1046
+ /* @__PURE__ */ jsxs("label", { children: [
1047
+ field("serverEndpoint", "Server endpoint"),
1048
+ /* @__PURE__ */ jsx(
1049
+ "input",
1050
+ {
1051
+ value: config.serverEndpoint ?? "",
1052
+ onChange: (event) => update({ serverEndpoint: event.target.value })
1053
+ }
1054
+ )
1055
+ ] }),
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
+ ),
392
1067
  /* @__PURE__ */ jsx(
393
1068
  "button",
394
1069
  {
@@ -521,6 +1196,6 @@ function JsonConfigIO({ config, onImport, locale, dictionary }) {
521
1196
  ] });
522
1197
  }
523
1198
 
524
- export { AdminRallyEditor, ConditionEditor, GeneralSettingsForm, JsonConfigIO, RallyEditor, SpotItemForm };
1199
+ export { AdminRallyEditor, ConditionEditor, ExternalReferencesEditor, GeneralSettingsForm, JsonConfigIO, MetadataEditor, RallyEditor, RewardItemForm, RewardUnlockConditionEditor, SpotItemForm, ThemeEditor, useAdminRallyEditor, useRewardEditor, useSpotEditor };
525
1200
  //# sourceMappingURL=index.js.map
526
1201
  //# sourceMappingURL=index.js.map