@stamprally/ui 0.3.0 → 0.5.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.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RewardItem, RewardState, SupportedLocale, ConsumeResult, SpotItem, StampRallyState, VerificationContext, LocalizedText, RallyConfig, StampRallyProgress, SheetTheme, SlotShape } from '@stamprally/core';
2
+ import { SupportedLocale, RewardItem, RewardState, LocaleDictionary, ConsumeResult, SpotItem, StampRallyState, VerificationContext, LocalizedText, RallyConfig, StampRallyProgress, SheetTheme, SlotShape } from '@stamprally/core';
3
+ export { LocaleDictionary } from '@stamprally/core';
3
4
 
4
5
  interface StampPresentation {
5
6
  readonly label?: string;
@@ -7,8 +8,8 @@ interface StampPresentation {
7
8
  readonly ink?: "vermilion" | "indigo";
8
9
  readonly channel?: "instant" | "qr" | "nfc" | "geo";
9
10
  }
10
- interface StampSlotProps {
11
- readonly stamp: SpotItem;
11
+ interface StampSlotProps<TLocale extends string = SupportedLocale> {
12
+ readonly stamp: SpotItem<TLocale>;
12
13
  readonly record?: StampRallyState["records"][number] | undefined;
13
14
  readonly isNext?: boolean;
14
15
  readonly slotNumber?: number;
@@ -16,55 +17,61 @@ interface StampSlotProps {
16
17
  readonly isAnimating?: boolean;
17
18
  readonly disabled?: boolean;
18
19
  readonly slotShape?: SlotShape;
19
- readonly locale?: SupportedLocale;
20
+ readonly locale?: TLocale;
21
+ readonly dictionary?: LocaleDictionary<TLocale>;
20
22
  readonly statusText?: string;
21
23
  readonly onSelect?: () => void;
22
24
  }
23
- declare function StampSlot({ stamp, record, isNext, slotNumber, presentation, isAnimating, disabled, slotShape, locale, statusText, onSelect, }: StampSlotProps): react.JSX.Element;
24
- interface StampSheetProps {
25
- readonly title?: LocalizedText;
26
- readonly config: RallyConfig;
25
+ declare function StampSlot<TLocale extends string = SupportedLocale>({ stamp, record, isNext, slotNumber, presentation, isAnimating, disabled, slotShape, locale, statusText, onSelect, }: StampSlotProps<TLocale>): react.JSX.Element;
26
+ interface StampSheetProps<TLocale extends string = SupportedLocale> {
27
+ readonly title?: LocalizedText<TLocale>;
28
+ readonly config: RallyConfig<TLocale>;
27
29
  readonly state?: StampRallyState | null;
28
30
  readonly progress?: StampRallyProgress;
29
31
  readonly presentations?: Readonly<Record<string, StampPresentation>>;
30
32
  readonly animatedStampId?: string | null;
31
33
  readonly disabled?: boolean;
32
- readonly locale?: SupportedLocale;
34
+ readonly locale?: TLocale;
35
+ readonly dictionary?: LocaleDictionary<TLocale>;
33
36
  readonly theme?: SheetTheme;
34
37
  readonly onStampSelect?: (stampId: string) => void;
35
38
  }
36
- declare function StampSheet({ title, config, state, progress, presentations, animatedStampId, disabled, locale, theme, onStampSelect, }: StampSheetProps): react.JSX.Element;
39
+ declare function StampSheet<TLocale extends string = SupportedLocale>({ title, config, state, progress, presentations, animatedStampId, disabled, locale, dictionary, theme, onStampSelect, }: StampSheetProps<TLocale>): react.JSX.Element;
37
40
  interface AcquisitionFeedback {
38
41
  readonly ok: boolean;
39
42
  readonly message: string;
40
43
  }
41
- interface StampModalProps {
44
+ interface StampModalProps<TLocale extends string = SupportedLocale> {
42
45
  readonly open: boolean;
43
- readonly stamp: SpotItem | null;
46
+ readonly stamp: SpotItem<TLocale> | null;
44
47
  readonly record?: StampRallyState["records"][number] | undefined;
45
48
  readonly presentation?: StampPresentation | null;
46
49
  readonly requiredStampName?: string | null;
47
50
  readonly isAvailable?: boolean;
48
51
  readonly isPending?: boolean;
49
52
  readonly isCompleted?: boolean;
50
- readonly locale?: SupportedLocale;
53
+ readonly locale?: TLocale;
54
+ readonly dictionary?: LocaleDictionary<TLocale>;
51
55
  readonly onClose: () => void;
52
56
  readonly onAcquire?: (stampId: string, context: VerificationContext) => Promise<AcquisitionFeedback>;
53
57
  readonly onCheckIn?: (stampId: string, context: VerificationContext) => Promise<AcquisitionFeedback>;
54
58
  readonly onNotify?: (feedback: AcquisitionFeedback) => void;
55
59
  }
56
- declare function StampModal({ open, stamp, record, isAvailable, isPending, locale, onClose, onAcquire, onCheckIn, onNotify, }: StampModalProps): react.JSX.Element | null;
57
- interface RewardPanelProps {
58
- readonly rewards: ReadonlyArray<RewardItem>;
60
+ declare function StampModal<TLocale extends string = SupportedLocale>({ open, stamp, record, isAvailable, isPending, locale, dictionary, onClose, onAcquire, onCheckIn, onNotify, }: StampModalProps<TLocale>): react.JSX.Element | null;
61
+ interface RewardPanelProps<TLocale extends string = SupportedLocale> {
62
+ readonly rewards: ReadonlyArray<RewardItem<TLocale>>;
59
63
  readonly states: ReadonlyArray<RewardState>;
60
- readonly locale?: SupportedLocale;
64
+ readonly locale?: TLocale;
65
+ readonly dictionary?: LocaleDictionary<TLocale>;
61
66
  readonly isPending?: boolean;
67
+ readonly open?: boolean;
68
+ readonly onClose?: () => void;
62
69
  readonly onRedeem: (rewardId: string, options?: {
63
70
  readonly passcode?: string;
64
71
  readonly staffId?: string;
65
72
  }) => Promise<ConsumeResult>;
66
73
  readonly onNotify?: (ok: boolean, message: string) => void;
67
74
  }
68
- declare function RewardPanel({ rewards, states, locale, isPending, onRedeem, onNotify, }: RewardPanelProps): react.JSX.Element;
75
+ declare function RewardPanel<TLocale extends string = SupportedLocale>({ rewards, states, locale, dictionary, isPending, open, onClose, onRedeem, onNotify, }: RewardPanelProps<TLocale>): react.JSX.Element | null;
69
76
 
70
77
  export { type AcquisitionFeedback, RewardPanel, type RewardPanelProps, StampModal, type StampModalProps, type StampPresentation, StampSheet, type StampSheetProps, StampSlot, type StampSlotProps };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RewardItem, RewardState, SupportedLocale, ConsumeResult, SpotItem, StampRallyState, VerificationContext, LocalizedText, RallyConfig, StampRallyProgress, SheetTheme, SlotShape } from '@stamprally/core';
2
+ import { SupportedLocale, RewardItem, RewardState, LocaleDictionary, ConsumeResult, SpotItem, StampRallyState, VerificationContext, LocalizedText, RallyConfig, StampRallyProgress, SheetTheme, SlotShape } from '@stamprally/core';
3
+ export { LocaleDictionary } from '@stamprally/core';
3
4
 
4
5
  interface StampPresentation {
5
6
  readonly label?: string;
@@ -7,8 +8,8 @@ interface StampPresentation {
7
8
  readonly ink?: "vermilion" | "indigo";
8
9
  readonly channel?: "instant" | "qr" | "nfc" | "geo";
9
10
  }
10
- interface StampSlotProps {
11
- readonly stamp: SpotItem;
11
+ interface StampSlotProps<TLocale extends string = SupportedLocale> {
12
+ readonly stamp: SpotItem<TLocale>;
12
13
  readonly record?: StampRallyState["records"][number] | undefined;
13
14
  readonly isNext?: boolean;
14
15
  readonly slotNumber?: number;
@@ -16,55 +17,61 @@ interface StampSlotProps {
16
17
  readonly isAnimating?: boolean;
17
18
  readonly disabled?: boolean;
18
19
  readonly slotShape?: SlotShape;
19
- readonly locale?: SupportedLocale;
20
+ readonly locale?: TLocale;
21
+ readonly dictionary?: LocaleDictionary<TLocale>;
20
22
  readonly statusText?: string;
21
23
  readonly onSelect?: () => void;
22
24
  }
23
- declare function StampSlot({ stamp, record, isNext, slotNumber, presentation, isAnimating, disabled, slotShape, locale, statusText, onSelect, }: StampSlotProps): react.JSX.Element;
24
- interface StampSheetProps {
25
- readonly title?: LocalizedText;
26
- readonly config: RallyConfig;
25
+ declare function StampSlot<TLocale extends string = SupportedLocale>({ stamp, record, isNext, slotNumber, presentation, isAnimating, disabled, slotShape, locale, statusText, onSelect, }: StampSlotProps<TLocale>): react.JSX.Element;
26
+ interface StampSheetProps<TLocale extends string = SupportedLocale> {
27
+ readonly title?: LocalizedText<TLocale>;
28
+ readonly config: RallyConfig<TLocale>;
27
29
  readonly state?: StampRallyState | null;
28
30
  readonly progress?: StampRallyProgress;
29
31
  readonly presentations?: Readonly<Record<string, StampPresentation>>;
30
32
  readonly animatedStampId?: string | null;
31
33
  readonly disabled?: boolean;
32
- readonly locale?: SupportedLocale;
34
+ readonly locale?: TLocale;
35
+ readonly dictionary?: LocaleDictionary<TLocale>;
33
36
  readonly theme?: SheetTheme;
34
37
  readonly onStampSelect?: (stampId: string) => void;
35
38
  }
36
- declare function StampSheet({ title, config, state, progress, presentations, animatedStampId, disabled, locale, theme, onStampSelect, }: StampSheetProps): react.JSX.Element;
39
+ declare function StampSheet<TLocale extends string = SupportedLocale>({ title, config, state, progress, presentations, animatedStampId, disabled, locale, dictionary, theme, onStampSelect, }: StampSheetProps<TLocale>): react.JSX.Element;
37
40
  interface AcquisitionFeedback {
38
41
  readonly ok: boolean;
39
42
  readonly message: string;
40
43
  }
41
- interface StampModalProps {
44
+ interface StampModalProps<TLocale extends string = SupportedLocale> {
42
45
  readonly open: boolean;
43
- readonly stamp: SpotItem | null;
46
+ readonly stamp: SpotItem<TLocale> | null;
44
47
  readonly record?: StampRallyState["records"][number] | undefined;
45
48
  readonly presentation?: StampPresentation | null;
46
49
  readonly requiredStampName?: string | null;
47
50
  readonly isAvailable?: boolean;
48
51
  readonly isPending?: boolean;
49
52
  readonly isCompleted?: boolean;
50
- readonly locale?: SupportedLocale;
53
+ readonly locale?: TLocale;
54
+ readonly dictionary?: LocaleDictionary<TLocale>;
51
55
  readonly onClose: () => void;
52
56
  readonly onAcquire?: (stampId: string, context: VerificationContext) => Promise<AcquisitionFeedback>;
53
57
  readonly onCheckIn?: (stampId: string, context: VerificationContext) => Promise<AcquisitionFeedback>;
54
58
  readonly onNotify?: (feedback: AcquisitionFeedback) => void;
55
59
  }
56
- declare function StampModal({ open, stamp, record, isAvailable, isPending, locale, onClose, onAcquire, onCheckIn, onNotify, }: StampModalProps): react.JSX.Element | null;
57
- interface RewardPanelProps {
58
- readonly rewards: ReadonlyArray<RewardItem>;
60
+ declare function StampModal<TLocale extends string = SupportedLocale>({ open, stamp, record, isAvailable, isPending, locale, dictionary, onClose, onAcquire, onCheckIn, onNotify, }: StampModalProps<TLocale>): react.JSX.Element | null;
61
+ interface RewardPanelProps<TLocale extends string = SupportedLocale> {
62
+ readonly rewards: ReadonlyArray<RewardItem<TLocale>>;
59
63
  readonly states: ReadonlyArray<RewardState>;
60
- readonly locale?: SupportedLocale;
64
+ readonly locale?: TLocale;
65
+ readonly dictionary?: LocaleDictionary<TLocale>;
61
66
  readonly isPending?: boolean;
67
+ readonly open?: boolean;
68
+ readonly onClose?: () => void;
62
69
  readonly onRedeem: (rewardId: string, options?: {
63
70
  readonly passcode?: string;
64
71
  readonly staffId?: string;
65
72
  }) => Promise<ConsumeResult>;
66
73
  readonly onNotify?: (ok: boolean, message: string) => void;
67
74
  }
68
- declare function RewardPanel({ rewards, states, locale, isPending, onRedeem, onNotify, }: RewardPanelProps): react.JSX.Element;
75
+ declare function RewardPanel<TLocale extends string = SupportedLocale>({ rewards, states, locale, dictionary, isPending, open, onClose, onRedeem, onNotify, }: RewardPanelProps<TLocale>): react.JSX.Element | null;
69
76
 
70
77
  export { type AcquisitionFeedback, RewardPanel, type RewardPanelProps, StampModal, type StampModalProps, type StampPresentation, StampSheet, type StampSheetProps, StampSlot, type StampSlotProps };
package/dist/index.js CHANGED
@@ -1,8 +1,48 @@
1
1
  import { resolveLocalizedText, DEFAULT_SHEET_THEME } from '@stamprally/core';
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useRef } from 'react';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
 
5
5
  // src/index.tsx
6
+ function useFocusTrap(active, onClose, closeDisabled = false) {
7
+ const containerRef = useRef(null);
8
+ useEffect(() => {
9
+ if (!active) return;
10
+ const container = containerRef.current;
11
+ if (container === null) return;
12
+ const focusableSelector = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
13
+ const focusable = () => Array.from(container.querySelectorAll(focusableSelector));
14
+ const first = focusable()[0];
15
+ first?.focus();
16
+ const handleKeyDown = (event) => {
17
+ if (event.key === "Escape") {
18
+ if (!closeDisabled) {
19
+ event.preventDefault();
20
+ onClose?.();
21
+ }
22
+ return;
23
+ }
24
+ if (event.key !== "Tab") return;
25
+ const elements = focusable();
26
+ if (elements.length === 0) {
27
+ event.preventDefault();
28
+ container.focus();
29
+ return;
30
+ }
31
+ const firstElement = elements[0];
32
+ const lastElement = elements[elements.length - 1];
33
+ if (event.shiftKey && document.activeElement === firstElement) {
34
+ event.preventDefault();
35
+ lastElement?.focus();
36
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
37
+ event.preventDefault();
38
+ firstElement?.focus();
39
+ }
40
+ };
41
+ document.addEventListener("keydown", handleKeyDown);
42
+ return () => document.removeEventListener("keydown", handleKeyDown);
43
+ }, [active, closeDisabled, onClose]);
44
+ return containerRef;
45
+ }
6
46
  var FONT_STACKS = {
7
47
  "system-ui": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
8
48
  serif: 'Georgia, "Times New Roman", "Yu Mincho", serif',
@@ -41,6 +81,7 @@ function StampSlot({
41
81
  disabled,
42
82
  onClick: onSelect,
43
83
  "aria-label": `#${String(slotNumber).padStart(2, "0")} ${name}${statusSeparator}${resolvedStatus}`,
84
+ "aria-disabled": disabled,
44
85
  "data-status": status,
45
86
  "data-shape": slotShape,
46
87
  className: `stamp-slot stamp-slot--${status} stamp-slot--${presentation.ink ?? "vermilion"} stamp-slot--shape-${slotShape}${isAnimating ? " stamp-slot--animating" : ""}`,
@@ -84,6 +125,7 @@ function StampSheet({
84
125
  animatedStampId = null,
85
126
  disabled = false,
86
127
  locale = "ja",
128
+ dictionary,
87
129
  theme = config.theme ?? DEFAULT_SHEET_THEME,
88
130
  onStampSelect
89
131
  }) {
@@ -108,18 +150,12 @@ function StampSheet({
108
150
  "--stamprally-text": `var(--stamprally-text-override, ${theme.textColor})`,
109
151
  "--stamprally-columns": String(theme.gridColumns)
110
152
  };
111
- const messages = locale === "en" ? {
112
- available: "available",
113
- locked: "locked",
114
- stamped: "stamped",
115
- remaining: (value) => `${value} spots remaining`,
116
- completed: "RALLY COMPLETED"
117
- } : {
118
- available: "\u53D6\u5F97\u53EF\u80FD",
119
- locked: "\u9806\u5E8F\u5F85\u3061",
120
- stamped: "\u53D6\u5F97\u6E08\u307F",
121
- remaining: (value) => `\u3042\u3068${value}\u7B87\u6240`,
122
- completed: "\u30E9\u30EA\u30FC\u9054\u6210"
153
+ const messages = {
154
+ available: dictionary?.[locale]?.available ?? (locale === "en" ? "available" : "\u53D6\u5F97\u53EF\u80FD"),
155
+ locked: dictionary?.[locale]?.locked ?? (locale === "en" ? "locked" : "\u9806\u5E8F\u5F85\u3061"),
156
+ stamped: dictionary?.[locale]?.stamped ?? (locale === "en" ? "stamped" : "\u53D6\u5F97\u6E08\u307F"),
157
+ remaining: (value) => dictionary?.[locale]?.remaining?.replace("{count}", String(value)) ?? (locale === "en" ? `${value} spots remaining` : `\u3042\u3068${value}\u7B87\u6240`),
158
+ completed: dictionary?.[locale]?.completed ?? (locale === "en" ? "RALLY COMPLETED" : "\u30E9\u30EA\u30FC\u9054\u6210")
123
159
  };
124
160
  return /* @__PURE__ */ jsxs(
125
161
  "section",
@@ -130,7 +166,7 @@ function StampSheet({
130
166
  children: [
131
167
  /* @__PURE__ */ jsxs("header", { className: "stamp-sheet__header", children: [
132
168
  /* @__PURE__ */ jsx("h2", { children: resolveLocalizedText(title, locale) || config.id }),
133
- /* @__PURE__ */ jsxs("span", { className: "stamp-sheet__score", "aria-live": "polite", children: [
169
+ /* @__PURE__ */ jsxs("span", { className: "stamp-sheet__score", role: "status", "aria-live": "polite", children: [
134
170
  count,
135
171
  " / ",
136
172
  total
@@ -175,13 +211,14 @@ function StampSheet({
175
211
  disabled,
176
212
  slotShape: theme.slotShape,
177
213
  locale,
214
+ ...dictionary === void 0 ? {} : { dictionary },
178
215
  statusText: recordForStamp === void 0 ? next.has(stamp.id) ? messages.available : messages.locked : messages.stamped,
179
216
  onSelect: () => onStampSelect?.(stamp.id)
180
217
  },
181
218
  stamp.id
182
219
  );
183
220
  }) }),
184
- percentage >= 100 && /* @__PURE__ */ jsx("div", { className: "stamp-sheet__complete-mark", children: "COMPLETE!!" }),
221
+ percentage >= 100 && /* @__PURE__ */ jsx("div", { className: "stamp-sheet__complete-mark", role: "status", "aria-live": "polite", children: "COMPLETE!!" }),
185
222
  percentage >= 100 && /* @__PURE__ */ jsx("div", { className: "stamp-sheet__complete-message", children: messages.completed })
186
223
  ]
187
224
  }
@@ -194,12 +231,14 @@ function StampModal({
194
231
  isAvailable = true,
195
232
  isPending = false,
196
233
  locale = "ja",
234
+ dictionary,
197
235
  onClose,
198
236
  onAcquire,
199
237
  onCheckIn,
200
238
  onNotify
201
239
  }) {
202
240
  const [token, setToken] = useState("");
241
+ const modalRef = useFocusTrap(open && stamp !== null, onClose, isPending);
203
242
  useEffect(() => {
204
243
  if (!open) setToken("");
205
244
  }, [open]);
@@ -213,6 +252,7 @@ function StampModal({
213
252
  if (feedback.ok) onClose();
214
253
  };
215
254
  const name = resolveLocalizedText(stamp.name, locale);
255
+ const messages = dictionary?.[locale] ?? {};
216
256
  return /* @__PURE__ */ jsx(
217
257
  "div",
218
258
  {
@@ -220,10 +260,20 @@ function StampModal({
220
260
  "aria-modal": "true",
221
261
  "aria-labelledby": "stamprally-modal-title",
222
262
  className: "stamprally-modal",
223
- children: /* @__PURE__ */ jsxs("div", { className: "stamprally-modal__panel", children: [
224
- /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Close", onClick: onClose, disabled: isPending, children: "\xD7" }),
263
+ children: /* @__PURE__ */ jsxs("div", { className: "stamprally-modal__panel", ref: modalRef, tabIndex: -1, children: [
264
+ /* @__PURE__ */ jsx(
265
+ "button",
266
+ {
267
+ type: "button",
268
+ "aria-label": "Close stamp details",
269
+ "aria-disabled": isPending,
270
+ onClick: onClose,
271
+ disabled: isPending,
272
+ children: "\xD7"
273
+ }
274
+ ),
225
275
  /* @__PURE__ */ jsx("h2", { id: "stamprally-modal-title", children: name }),
226
- record !== void 0 ? /* @__PURE__ */ jsx("p", { children: "Already claimed." }) : /* @__PURE__ */ jsx("p", { children: resolveLocalizedText(stamp.description, locale) }),
276
+ record !== void 0 ? /* @__PURE__ */ jsx("p", { children: messages.alreadyClaimed ?? "Already claimed." }) : /* @__PURE__ */ jsx("p", { children: resolveLocalizedText(stamp.description, locale) }),
227
277
  stamp.condition.type === "token" ? /* @__PURE__ */ jsxs(
228
278
  "form",
229
279
  {
@@ -233,19 +283,31 @@ function StampModal({
233
283
  },
234
284
  children: [
235
285
  /* @__PURE__ */ jsxs("label", { children: [
236
- "Passcode ",
286
+ messages.passcode ?? "Passcode",
287
+ " ",
237
288
  /* @__PURE__ */ jsx("input", { value: token, onChange: (event) => setToken(event.target.value) })
238
289
  ] }),
239
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: !isAvailable || isPending, children: "Claim stamp" })
290
+ /* @__PURE__ */ jsx(
291
+ "button",
292
+ {
293
+ type: "submit",
294
+ "aria-label": "Claim stamp",
295
+ "aria-disabled": !isAvailable || isPending,
296
+ disabled: !isAvailable || isPending,
297
+ children: messages.claimStamp ?? "Claim stamp"
298
+ }
299
+ )
240
300
  ]
241
301
  }
242
302
  ) : /* @__PURE__ */ jsx(
243
303
  "button",
244
304
  {
245
305
  type: "button",
306
+ "aria-label": "Claim stamp",
307
+ "aria-disabled": !isAvailable || isPending,
246
308
  onClick: () => void submit({ type: "instant" }),
247
309
  disabled: !isAvailable || isPending,
248
- children: "Claim stamp"
310
+ children: messages.claimStamp ?? "Claim stamp"
249
311
  }
250
312
  )
251
313
  ] })
@@ -256,67 +318,110 @@ function RewardPanel({
256
318
  rewards,
257
319
  states,
258
320
  locale = "ja",
321
+ dictionary,
259
322
  isPending = false,
323
+ open = true,
324
+ onClose,
260
325
  onRedeem,
261
326
  onNotify
262
327
  }) {
263
328
  const [passcodes, setPasscodes] = useState({});
264
- return /* @__PURE__ */ jsxs("section", { "aria-labelledby": "stamprally-rewards-title", className: "stamprally-rewards", children: [
265
- /* @__PURE__ */ jsx("h2", { id: "stamprally-rewards-title", children: "Rewards" }),
266
- rewards.map((reward) => {
267
- const state = states.find((item) => item.rewardId === reward.id);
268
- const available = state?.status === "AVAILABLE";
269
- const redeem = async (options) => {
270
- const result = await onRedeem(reward.id, options);
271
- onNotify?.(result.ok, result.ok ? "Reward claimed." : result.error.code);
272
- };
273
- return /* @__PURE__ */ jsxs(
274
- "article",
275
- {
276
- className: `stamprally-reward stamprally-reward--${state?.status ?? "LOCKED"}`,
277
- children: [
278
- /* @__PURE__ */ jsx("h3", { children: resolveLocalizedText(reward.title, locale) }),
279
- /* @__PURE__ */ jsx("p", { children: resolveLocalizedText(reward.description, locale) }),
280
- /* @__PURE__ */ jsx("span", { children: state?.status ?? "LOCKED" }),
281
- reward.redemptionMethod === "staff_passcode" && /* @__PURE__ */ jsxs(
282
- "form",
283
- {
284
- onSubmit: (event) => {
285
- event.preventDefault();
286
- void redeem({ passcode: passcodes[reward.id] ?? "" });
287
- },
288
- children: [
289
- /* @__PURE__ */ jsxs("label", { children: [
290
- "Staff passcode",
291
- " ",
292
- /* @__PURE__ */ jsx(
293
- "input",
294
- {
295
- type: "password",
296
- value: passcodes[reward.id] ?? "",
297
- onChange: (event) => setPasscodes((current) => ({ ...current, [reward.id]: event.target.value }))
298
- }
299
- )
300
- ] }),
301
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: !available || isPending, children: "Redeem" })
302
- ]
303
- }
304
- ),
305
- reward.redemptionMethod !== "staff_passcode" && /* @__PURE__ */ jsx(
306
- "button",
307
- {
308
- type: "button",
309
- disabled: !available || isPending,
310
- onClick: () => void redeem(),
311
- children: "Redeem"
312
- }
313
- )
314
- ]
315
- },
316
- reward.id
317
- );
318
- })
319
- ] });
329
+ const panelRef = useFocusTrap(open && onClose !== void 0, onClose, isPending);
330
+ if (!open) return null;
331
+ return /* @__PURE__ */ jsxs(
332
+ "section",
333
+ {
334
+ ref: panelRef,
335
+ role: "dialog",
336
+ "aria-labelledby": "stamprally-rewards-title",
337
+ "aria-modal": "true",
338
+ className: "stamprally-rewards",
339
+ tabIndex: -1,
340
+ children: [
341
+ /* @__PURE__ */ jsxs("header", { className: "stamprally-rewards__header", children: [
342
+ /* @__PURE__ */ jsx("h2", { id: "stamprally-rewards-title", children: dictionary?.[locale]?.rewards ?? "Rewards" }),
343
+ onClose !== void 0 && /* @__PURE__ */ jsx(
344
+ "button",
345
+ {
346
+ type: "button",
347
+ "aria-label": "Close rewards",
348
+ "aria-disabled": isPending,
349
+ disabled: isPending,
350
+ onClick: onClose,
351
+ children: "\xD7"
352
+ }
353
+ )
354
+ ] }),
355
+ rewards.map((reward) => {
356
+ const state = states.find((item) => item.rewardId === reward.id);
357
+ const available = state?.status === "AVAILABLE";
358
+ const redeem = async (options) => {
359
+ const result = await onRedeem(reward.id, options);
360
+ onNotify?.(
361
+ result.ok,
362
+ result.ok ? dictionary?.[locale]?.rewardClaimed ?? "Reward claimed." : result.error.code
363
+ );
364
+ };
365
+ return /* @__PURE__ */ jsxs(
366
+ "article",
367
+ {
368
+ className: `stamprally-reward stamprally-reward--${state?.status ?? "LOCKED"}`,
369
+ children: [
370
+ /* @__PURE__ */ jsx("h3", { children: resolveLocalizedText(reward.title, locale) }),
371
+ /* @__PURE__ */ jsx("p", { children: resolveLocalizedText(reward.description, locale) }),
372
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", children: state?.status ?? "LOCKED" }),
373
+ reward.redemptionMethod === "staff_passcode" && /* @__PURE__ */ jsxs(
374
+ "form",
375
+ {
376
+ onSubmit: (event) => {
377
+ event.preventDefault();
378
+ void redeem({ passcode: passcodes[reward.id] ?? "" });
379
+ },
380
+ children: [
381
+ /* @__PURE__ */ jsxs("label", { children: [
382
+ dictionary?.[locale]?.staffPasscode ?? "Staff passcode",
383
+ " ",
384
+ /* @__PURE__ */ jsx(
385
+ "input",
386
+ {
387
+ type: "password",
388
+ value: passcodes[reward.id] ?? "",
389
+ onChange: (event) => setPasscodes((current) => ({ ...current, [reward.id]: event.target.value }))
390
+ }
391
+ )
392
+ ] }),
393
+ /* @__PURE__ */ jsx(
394
+ "button",
395
+ {
396
+ type: "submit",
397
+ "aria-label": `Redeem ${resolveLocalizedText(reward.title, locale)}`,
398
+ "aria-disabled": !available || isPending,
399
+ disabled: !available || isPending,
400
+ children: dictionary?.[locale]?.redeem ?? "Redeem"
401
+ }
402
+ )
403
+ ]
404
+ }
405
+ ),
406
+ reward.redemptionMethod !== "staff_passcode" && /* @__PURE__ */ jsx(
407
+ "button",
408
+ {
409
+ type: "button",
410
+ "aria-label": `Redeem ${resolveLocalizedText(reward.title, locale)}`,
411
+ "aria-disabled": !available || isPending,
412
+ disabled: !available || isPending,
413
+ onClick: () => void redeem(),
414
+ children: dictionary?.[locale]?.redeem ?? "Redeem"
415
+ }
416
+ )
417
+ ]
418
+ },
419
+ reward.id
420
+ );
421
+ })
422
+ ]
423
+ }
424
+ );
320
425
  }
321
426
 
322
427
  export { RewardPanel, StampModal, StampSheet, StampSlot };