@zappie/foundations 0.1.1 → 0.1.2

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/account/index.js +49 -0
  3. package/dist/account/index.js.map +7 -0
  4. package/dist/cart/index.js +168 -0
  5. package/dist/cart/index.js.map +7 -0
  6. package/dist/fulfilment/address-picker.js +168 -0
  7. package/dist/fulfilment/address-picker.js.map +7 -0
  8. package/dist/fulfilment/branch-picker.js +54 -0
  9. package/dist/fulfilment/branch-picker.js.map +7 -0
  10. package/dist/fulfilment/schedule.js +146 -0
  11. package/dist/fulfilment/schedule.js.map +7 -0
  12. package/dist/fulfilment/switch.js +54 -0
  13. package/dist/fulfilment/switch.js.map +7 -0
  14. package/dist/index.js +48 -2114
  15. package/dist/index.js.map +7 -1
  16. package/dist/internal/combobox.js +131 -0
  17. package/dist/internal/combobox.js.map +7 -0
  18. package/dist/internal/data-attrs.js +8 -0
  19. package/dist/internal/data-attrs.js.map +7 -0
  20. package/dist/internal/ids.js +10 -0
  21. package/dist/internal/ids.js.map +7 -0
  22. package/dist/{chunk-D3J7S33L.js → internal/slot.js} +13 -6
  23. package/dist/internal/slot.js.map +7 -0
  24. package/dist/internal/use-controllable-state.js +29 -0
  25. package/dist/internal/use-controllable-state.js.map +7 -0
  26. package/dist/item-form/index.js +13 -0
  27. package/dist/item-form/index.js.map +7 -0
  28. package/dist/item-form/option-group.js +268 -0
  29. package/dist/item-form/option-group.js.map +7 -0
  30. package/dist/item-form/options.js +30 -0
  31. package/dist/item-form/options.js.map +7 -0
  32. package/dist/item-form/quantity.js +131 -0
  33. package/dist/item-form/quantity.js.map +7 -0
  34. package/dist/item-form/root.js +151 -0
  35. package/dist/item-form/root.js.map +7 -0
  36. package/dist/media/image.js +30 -0
  37. package/dist/media/image.js.map +7 -0
  38. package/dist/money.js +20 -0
  39. package/dist/money.js.map +7 -0
  40. package/dist/next.js +8 -6
  41. package/dist/next.js.map +7 -1
  42. package/dist/otp/input.js +110 -0
  43. package/dist/otp/input.js.map +7 -0
  44. package/dist/overlay/engine.js +16 -8
  45. package/dist/overlay/engine.js.map +7 -1
  46. package/dist/{chunk-BY2WQGLO.js → overlay/history.js} +9 -4
  47. package/dist/overlay/history.js.map +7 -0
  48. package/dist/overlay/react.js +261 -0
  49. package/dist/overlay/react.js.map +7 -0
  50. package/dist/payment/methods.js +103 -0
  51. package/dist/payment/methods.js.map +7 -0
  52. package/dist/rich-text/banners.js +9 -0
  53. package/dist/rich-text/banners.js.map +7 -0
  54. package/dist/rich-text/browser.js +3 -3
  55. package/dist/rich-text/browser.js.map +7 -1
  56. package/dist/rich-text/core.js +75 -0
  57. package/dist/rich-text/core.js.map +7 -0
  58. package/dist/rich-text/index.js +3 -79
  59. package/dist/rich-text/index.js.map +7 -1
  60. package/dist/search/engine.js +93 -0
  61. package/dist/search/engine.js.map +7 -0
  62. package/dist/search/index.js +175 -0
  63. package/dist/search/index.js.map +7 -0
  64. package/dist/status/hours.js +177 -0
  65. package/dist/status/hours.js.map +7 -0
  66. package/dist/status/open-status.js +45 -0
  67. package/dist/status/open-status.js.map +7 -0
  68. package/dist/version.js +6 -0
  69. package/dist/version.js.map +7 -0
  70. package/package.json +3 -3
  71. package/dist/chunk-BY2WQGLO.js.map +0 -1
  72. package/dist/chunk-D3J7S33L.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,2119 +1,53 @@
1
1
  "use client";
2
- import { subscribeLocation, locationSnapshot, openOverlay, closeOverlay } from './chunk-BY2WQGLO.js';
3
- import { part, composeRefs, partId } from './chunk-D3J7S33L.js';
4
- import { forwardRef, createContext, useState, useEffect, useMemo, useContext, useRef, memo, useCallback, useId, useSyncExternalStore } from 'react';
5
- import { useCartCatalogue, useCart, useZappie, useStore, useMenu } from '@zappie/sdk/react';
6
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
- import { format, ZappieError, previewItem, ZappieCartError } from '@zappie/sdk';
8
- import { srcSet, resized } from '@zappie/sdk/template';
9
-
10
- // src/version.ts
11
- var PACKAGE_VERSION = "0.1.1" ;
12
- var AccountNotConfiguredError = class extends Error {
13
- constructor() {
14
- super("Account authentication requires SDK auth (Open Storefronts PRD 13)");
15
- this.code = "not_configured";
16
- this.name = "AccountNotConfiguredError";
17
- }
18
- };
19
- async function signIn(_options) {
20
- throw new AccountNotConfiguredError();
21
- }
22
- async function signOut(_options) {
23
- throw new AccountNotConfiguredError();
24
- }
25
- var SignIn = forwardRef(function SignIn2(props, ref) {
26
- return part(
27
- "div",
28
- props,
29
- { signIn, status: "not_configured" },
30
- { "data-state": "invalid" },
31
- ref
32
- );
33
- });
34
- var SignOut = forwardRef(function SignOut2(props, ref) {
35
- return part(
36
- "div",
37
- props,
38
- { signOut, status: "not_configured" },
39
- { "data-state": "invalid" },
40
- ref
41
- );
42
- });
43
- var CustomerPart = forwardRef(function Customer(props, ref) {
44
- return part(
45
- "div",
46
- props,
47
- { customer: null, status: "not_configured" },
48
- { "data-state": "invalid" },
49
- ref
50
- );
51
- });
52
- var Account = { SignIn, SignOut, Customer: CustomerPart };
53
-
54
- // src/internal/data-attrs.ts
55
- function flag(value) {
56
- return value ? "" : void 0;
57
- }
58
- function useControllableState({
59
- value,
60
- defaultValue,
61
- onChange
62
- }) {
63
- const [internal, setInternal] = useState(defaultValue);
64
- const current = value === void 0 ? internal : value;
65
- const latest = useRef({ current, onChange, controlled: value !== void 0 });
66
- latest.current = { current, onChange, controlled: value !== void 0 };
67
- const set = useCallback((next) => {
68
- const snapshot = latest.current;
69
- const resolved = typeof next === "function" ? next(snapshot.current) : next;
70
- if (Object.is(resolved, snapshot.current)) {
71
- return;
72
- }
73
- if (!snapshot.controlled) {
74
- latest.current.current = resolved;
75
- setInternal(resolved);
76
- }
77
- snapshot.onChange?.(resolved);
78
- }, []);
79
- return [current, set];
80
- }
81
- var Context = createContext(null);
82
- function useQuantity() {
83
- const context = useContext(Context);
84
- if (!context) {
85
- throw new Error("Quantity parts require Quantity.Root");
86
- }
87
- return context;
88
- }
89
- function Root({
90
- value,
91
- defaultValue,
92
- onValueChange,
93
- min = 1,
94
- max = 99,
95
- step = 1,
96
- disabled = false,
97
- ...props
98
- }) {
99
- if (!Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(step) || step <= 0 || min > max) {
100
- throw new RangeError(
101
- "Quantity requires finite ordered bounds and a positive step"
102
- );
103
- }
104
- const [current, set] = useControllableState({
105
- value,
106
- defaultValue: defaultValue ?? min,
107
- onChange: onValueChange
108
- });
109
- const setValue = useCallback(
110
- (next) => {
111
- if (!disabled && Number.isFinite(next)) {
112
- const last = min + Math.floor((max - min) / step) * step;
113
- set(
114
- Math.max(
115
- min,
116
- Math.min(last, min + Math.round((next - min) / step) * step)
117
- )
118
- );
119
- }
120
- },
121
- [min, max, step, disabled, set]
122
- );
123
- const state = useMemo(
124
- () => ({ value: current, setValue, min, max, step, disabled }),
125
- [current, setValue, min, max, step, disabled]
126
- );
127
- return /* @__PURE__ */ jsx(Context.Provider, { value: state, children: part("div", props, state, { "data-disabled": flag(disabled) }) });
128
- }
129
- function button(direction) {
130
- return forwardRef(
131
- function QuantityButton(props, ref) {
132
- const ctx = useQuantity();
133
- const disabled = ctx.disabled || (direction < 0 ? ctx.value <= ctx.min : ctx.value + ctx.step > ctx.max);
134
- return part(
135
- "button",
136
- props,
137
- ctx,
138
- {
139
- type: "button",
140
- disabled,
141
- "aria-disabled": disabled,
142
- "data-disabled": flag(disabled),
143
- onClick: () => ctx.setValue(ctx.value + direction * ctx.step)
144
- },
145
- ref
146
- );
147
- }
148
- );
149
- }
150
- var Value = forwardRef(
151
- function Value2(props, ref) {
152
- const ctx = useQuantity();
153
- return part(
154
- "input",
155
- props,
156
- ctx,
157
- {
158
- type: "text",
159
- inputMode: "numeric",
160
- role: "spinbutton",
161
- value: String(ctx.value),
162
- disabled: ctx.disabled,
163
- "aria-valuenow": ctx.value,
164
- "aria-valuemin": ctx.min,
165
- "aria-valuemax": ctx.max,
166
- onChange: (event) => {
167
- if (event.target.value.trim()) {
168
- ctx.setValue(Number(event.target.value));
169
- }
170
- },
171
- onKeyDown: (event) => {
172
- const next = {
173
- ArrowUp: ctx.value + ctx.step,
174
- ArrowDown: ctx.value - ctx.step,
175
- Home: ctx.min,
176
- End: ctx.max
177
- }[event.key];
178
- if (next !== void 0) {
179
- event.preventDefault();
180
- ctx.setValue(next);
181
- }
182
- }
183
- },
184
- ref
185
- );
186
- }
187
- );
188
- var Quantity = {
189
- Root,
190
- Value,
191
- Increment: button(1),
192
- Decrement: button(-1)
193
- };
194
- var Context2 = createContext(null);
195
- function useContextValue() {
196
- const value = useContext(Context2);
197
- if (!value) {
198
- throw new Error("Cart parts require Cart.Root");
199
- }
200
- return value;
201
- }
202
- function Root2(props) {
203
- const state = useCart();
204
- return /* @__PURE__ */ jsx(Context2.Provider, { value: state, children: part("div", props, state, {
205
- "data-state": !state.isHydrated ? "loading" : state.issues.some((issue) => issue.severity === "blocking") ? "invalid" : "valid"
206
- }) });
207
- }
208
- var Line = forwardRef(function Line2({ line, ...props }, ref) {
209
- const cart = useContextValue();
210
- const item = useCartCatalogue()?.items[line.itemId];
211
- const current = cart.lines.find((candidate) => candidate.id === line.id);
212
- if (!current) {
213
- return null;
214
- }
215
- const context = {
216
- line: current,
217
- item,
218
- edit: (patch) => cart.actions.update(line.id, patch),
219
- remove: () => cart.actions.remove(line.id)
220
- };
221
- return part(
222
- "div",
223
- props,
224
- context,
225
- {
226
- "data-zappie-cart-line": line.id,
227
- "data-invalid": cart.issues.some(
228
- (issue) => issue.lineId === line.id && issue.severity === "blocking"
229
- ) ? "" : void 0
230
- },
231
- ref
232
- );
233
- });
234
- function CartQuantity({
235
- lineId,
236
- ...props
237
- }) {
238
- const cart = useContextValue();
239
- const line = cart.lines.find((candidate) => candidate.id === lineId);
240
- const item = useCartCatalogue()?.items[line?.itemId ?? ""];
241
- if (!line) {
242
- return null;
243
- }
244
- const step = item?.quantity.step ?? 1;
245
- const min = Math.ceil((item?.quantity.min ?? 1) / step) * step;
246
- return /* @__PURE__ */ jsx(
247
- Quantity.Root,
248
- {
249
- ...props,
250
- value: line.quantity,
251
- min,
252
- max: Math.min(item?.quantity.max ?? 99, 99),
253
- step,
254
- onValueChange: (quantity) => cart.actions.update(lineId, { quantity })
255
- }
256
- );
257
- }
258
- var Remove = forwardRef(
259
- function Remove2({ lineId, ...props }, ref) {
260
- const cart = useContextValue();
261
- return part(
262
- "button",
263
- props,
264
- {},
265
- { type: "button", onClick: () => cart.actions.remove(lineId) },
266
- ref
267
- );
268
- }
269
- );
270
- var Empty = forwardRef(function Empty2(props, ref) {
271
- const cart = useContextValue();
272
- return cart.isHydrated && !cart.lines.length ? part("div", props, {}, {}, ref) : null;
273
- });
274
- var Issues = forwardRef(function Issues2(props, ref) {
275
- const cart = useContextValue();
276
- return part(
277
- "div",
278
- props,
279
- { issues: cart.issues, applyFix: cart.actions.applyFix },
280
- {
281
- "aria-live": "polite",
282
- "data-state": cart.issues.length ? "invalid" : "valid"
283
- },
284
- ref
285
- );
286
- });
287
- var Cart = {
288
- Root: Root2,
289
- Line,
290
- Quantity: CartQuantity,
291
- Remove,
292
- Empty,
293
- Issues
294
- };
295
- var Totals = forwardRef(
296
- function Totals2(props, ref) {
297
- const cart = useCart();
298
- const client = useZappie();
299
- const [failure, setFailure] = useState(null);
300
- const [expired, setExpired] = useState(false);
301
- const quote = "quote" in cart.state ? cart.state.quote : void 0;
302
- useEffect(
303
- () => client.on(
304
- "quote_failed",
305
- ({ code }) => setFailure(
306
- code === "network_error" || code === "timeout" ? "offline" : "stale"
307
- )
308
- ),
309
- [client]
310
- );
311
- useEffect(() => {
312
- if (cart.state.status === "quoting" || quote) {
313
- setFailure(null);
314
- }
315
- setExpired(Boolean(quote && Date.parse(quote.expiresAt) <= Date.now()));
316
- if (!quote) {
317
- return;
318
- }
319
- const timeout = setTimeout(
320
- () => setExpired(true),
321
- Math.max(
322
- 0,
323
- Math.min(2147483647, Date.parse(quote.expiresAt) - Date.now())
324
- )
325
- );
326
- return () => clearTimeout(timeout);
327
- }, [quote, cart.state.status]);
328
- const pending = cart.state.status === "ready" && !quote && cart.lines.length > 0;
329
- const status = failure === "offline" ? "offline" : cart.state.status === "quoting" || !cart.isHydrated ? "loading" : failure === "stale" || expired ? "stale" : pending ? "loading" : !quote ? "stale" : "ready";
330
- return part(
331
- "div",
332
- props,
333
- { quote, status },
334
- {
335
- "aria-live": "polite",
336
- "aria-busy": status === "loading",
337
- "data-state": status === "ready" ? "valid" : status === "offline" ? "stale" : status
338
- },
339
- ref
340
- );
341
- }
342
- );
343
- function usePartId(provided) {
344
- const id = useId();
345
- return provided ?? `zappie-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
346
- }
347
- function useCombobox(results, onSelect) {
348
- const id = usePartId();
349
- const [activeIndex, setActiveIndex] = useState(-1);
350
- const [isOpen, setOpen] = useState(false);
351
- const [listId, setListId] = useState(`${id}-list`);
352
- const [optionIds, setOptionIds] = useState({});
353
- useEffect(() => setActiveIndex(-1), [results.length]);
354
- const registerOption = (index, id2) => setOptionIds(
355
- (previous) => previous[index] === id2 ? previous : { ...previous, [index]: id2 }
356
- );
357
- const active = activeIndex < results.length ? activeIndex : -1;
358
- const optionId = (index) => optionIds[index] ?? `${id}-option-${index}`;
359
- const activeId = isOpen && active >= 0 ? optionId(active) : void 0;
360
- useEffect(() => {
361
- if (activeId) {
362
- document.getElementById(activeId)?.scrollIntoView?.({ block: "nearest" });
363
- }
364
- }, [activeId]);
365
- const select = (index) => {
366
- const item = results[index];
367
- if (item !== void 0) {
368
- onSelect(item);
369
- setOpen(false);
370
- setActiveIndex(-1);
371
- }
372
- };
373
- return {
374
- setListId,
375
- registerOption,
376
- activeIndex: active,
377
- setActiveIndex,
378
- isOpen,
379
- setOpen,
380
- select,
381
- inputProps: {
382
- role: "combobox",
383
- "aria-autocomplete": "list",
384
- "aria-expanded": isOpen,
385
- "aria-controls": listId,
386
- "aria-activedescendant": activeId,
387
- onKeyDown: (event) => {
388
- if (event.nativeEvent.isComposing || event.keyCode === 229) {
389
- return;
390
- }
391
- if (event.key === "Escape") {
392
- event.preventDefault();
393
- setOpen(false);
394
- setActiveIndex(-1);
395
- } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
396
- event.preventDefault();
397
- setOpen(true);
398
- setActiveIndex(
399
- results.length ? active < 0 ? event.key === "ArrowDown" ? 0 : results.length - 1 : (active + (event.key === "ArrowDown" ? 1 : -1) + results.length) % results.length : -1
400
- );
401
- } else if (event.key === "Enter" && isOpen && active >= 0) {
402
- event.preventDefault();
403
- select(active);
404
- }
405
- },
406
- onFocus: () => setOpen(true)
407
- },
408
- listProps: { id: listId, role: "listbox", hidden: !isOpen },
409
- getOptionProps: (index) => ({
410
- id: optionId(index),
411
- role: "option",
412
- "aria-selected": active === index,
413
- "data-selected": active === index ? "" : void 0,
414
- onMouseDown: (event) => event.preventDefault(),
415
- // Inside a drawer or sheet, the overlay engine starts a swipe on any
416
- // press that isn't a button, link or field, then captures the pointer,
417
- // so the option's click never fires on touch. Options aren't swipe handles.
418
- onPointerDown: (event) => event.stopPropagation(),
419
- onTouchStart: (event) => event.stopPropagation(),
420
- onClick: () => select(index)
421
- })
422
- };
423
- }
424
- var visuallyHidden = {
425
- position: "absolute",
426
- width: 1,
427
- height: 1,
428
- margin: -1,
429
- padding: 0,
430
- border: 0,
431
- overflow: "hidden",
432
- clip: "rect(0 0 0 0)",
433
- whiteSpace: "nowrap"
434
- };
435
- function countAnnouncement(count) {
436
- return count === 0 ? "No results" : `${count} result${count === 1 ? "" : "s"} available`;
437
- }
438
- function LiveRegion({ message }) {
439
- return /* @__PURE__ */ jsx(
440
- "span",
441
- {
442
- role: "status",
443
- "aria-live": "polite",
444
- "aria-atomic": "true",
445
- "data-zappie-live": "",
446
- style: visuallyHidden,
447
- children: message
448
- }
449
- );
450
- }
451
- function withLiveRegion(props, message) {
452
- if (props.asChild) {
453
- return props;
454
- }
455
- const children = props.children;
456
- return {
457
- ...props,
458
- children: (state) => /* @__PURE__ */ jsxs(Fragment, { children: [
459
- typeof children === "function" ? children(state) : children,
460
- /* @__PURE__ */ jsx(LiveRegion, { message })
461
- ] })
462
- };
463
- }
464
- var Context3 = createContext(null);
465
- function useAddress() {
466
- const ctx = useContext(Context3);
467
- if (!ctx) {
468
- throw new Error("Address parts require Address.Picker");
469
- }
470
- return ctx;
471
- }
472
- function Picker({
473
- getSuggestions,
474
- onSelect,
475
- query,
476
- defaultQuery = "",
477
- onQueryChange,
478
- debounceMs = 200,
479
- announcement = countAnnouncement,
480
- ...props
481
- }) {
482
- const [value, setValue] = useControllableState({
483
- value: query,
484
- defaultValue: defaultQuery,
485
- onChange: onQueryChange
486
- });
487
- const [response, setResponse] = useState({ query: "", suggestions: [], status: "ready" });
488
- const suggestions = response.query === value ? response.suggestions : [];
489
- const combo = useCombobox(suggestions, (suggestion) => {
490
- setValue(suggestion.label);
491
- onSelect(suggestion);
492
- });
493
- useEffect(() => {
494
- if (!value.trim()) {
495
- setResponse({ query: value, suggestions: [], status: "ready" });
496
- return;
497
- }
498
- let live = true;
499
- const abort = new AbortController();
500
- setResponse({ query: value, suggestions: [], status: "loading" });
501
- const timer = setTimeout(() => {
502
- void Promise.resolve().then(() => getSuggestions(value, abort.signal)).then(
503
- (items) => {
504
- if (live) {
505
- setResponse({
506
- query: value,
507
- suggestions: items,
508
- status: "ready"
509
- });
510
- }
511
- },
512
- (error) => {
513
- if (live) {
514
- setResponse({
515
- query: value,
516
- suggestions: [],
517
- status: "error",
518
- error
519
- });
520
- }
521
- }
522
- );
523
- }, debounceMs);
524
- return () => {
525
- live = false;
526
- abort.abort();
527
- clearTimeout(timer);
528
- };
529
- }, [value, getSuggestions, debounceMs]);
530
- const setQuery = (next) => {
531
- setValue(next);
532
- combo.setOpen(true);
533
- combo.setActiveIndex(-1);
534
- };
535
- const state = {
536
- ...combo,
537
- query: value,
538
- setQuery,
539
- suggestions,
540
- status: response.status,
541
- error: response.error
542
- };
543
- return /* @__PURE__ */ jsx(Context3.Provider, { value: state, children: part(
544
- "div",
545
- withLiveRegion(
546
- props,
547
- combo.isOpen && value.trim() && response.status === "ready" ? announcement(suggestions.length) : ""
548
- ),
549
- state,
550
- {
551
- "data-state": response.status === "error" ? "invalid" : response.status === "loading" ? "loading" : "valid",
552
- onBlur: (event) => {
553
- if (!event.currentTarget.contains(event.relatedTarget)) {
554
- combo.setOpen(false);
555
- }
556
- }
557
- }
558
- ) });
559
- }
560
- var Input = forwardRef(
561
- function Input2(props, ref) {
562
- const state = useAddress();
563
- return part(
564
- "input",
565
- props,
566
- state,
567
- {
568
- ...state.inputProps,
569
- type: "text",
570
- autoComplete: "street-address",
571
- "data-zappie-address-input": "",
572
- value: state.query,
573
- onChange: (event) => state.setQuery(event.target.value)
574
- },
575
- ref
576
- );
577
- }
578
- );
579
- var Suggestions = forwardRef(
580
- function Suggestions2(props, ref) {
581
- const state = useAddress();
582
- return part(
583
- "div",
584
- props,
585
- state,
586
- { ...state.listProps, "aria-busy": state.status === "loading" },
587
- composeRefs(ref, (node) => {
588
- if (node?.id && node.id !== state.listProps.id) {
589
- state.setListId(node.id);
590
- }
591
- })
592
- );
593
- }
594
- );
595
- var Suggestion = forwardRef(function Suggestion2({ index, ...props }, ref) {
596
- const state = useAddress();
597
- return part(
598
- "div",
599
- props,
600
- state,
601
- state.getOptionProps(index),
602
- composeRefs(ref, (node) => {
603
- if (node?.id && node.id !== state.getOptionProps(index).id) {
604
- state.registerOption(index, node.id);
605
- }
606
- })
607
- );
608
- });
609
- var Address = { Picker, Input, Suggestions, Suggestion };
610
-
611
- // src/status/hours.ts
612
- var DAY = 864e5;
613
- var formatters = /* @__PURE__ */ new Map();
614
- function formatter(timezone) {
615
- let value = formatters.get(timezone);
616
- if (!value) {
617
- value = new Intl.DateTimeFormat("en-CA", {
618
- timeZone: timezone,
619
- year: "numeric",
620
- month: "2-digit",
621
- day: "2-digit",
622
- hour: "2-digit",
623
- minute: "2-digit",
624
- second: "2-digit",
625
- hourCycle: "h23"
626
- });
627
- formatters.set(timezone, value);
628
- }
629
- return value;
630
- }
631
- function localParts(date, timezone) {
632
- const parts = Object.fromEntries(
633
- formatter(timezone).formatToParts(date).map(({ type, value }) => [type, value])
634
- );
635
- return {
636
- date: `${parts.year}-${parts.month}-${parts.day}`,
637
- hour: Number(parts.hour),
638
- minute: Number(parts.minute),
639
- second: Number(parts.second)
640
- };
641
- }
642
- function parseWallClock(value) {
643
- const match = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
644
- if (!match || Number(match[1]) > 23 || Number(match[2]) > 59 || Number(match[3] ?? 0) > 59) {
645
- return void 0;
646
- }
647
- const hour24 = Number(match[1]);
648
- return {
649
- hour24,
650
- hour12: hour24 % 12 || 12,
651
- minute: Number(match[2]),
652
- second: Number(match[3] ?? 0),
653
- period: hour24 >= 12 ? "PM" : "AM"
654
- };
655
- }
656
- function formatWallClock(value) {
657
- const time = parseWallClock(value);
658
- return time ? `${time.hour12}${time.minute ? `:${String(time.minute).padStart(2, "0")}` : ""} ${time.period}` : value;
659
- }
660
- function formatOpeningHours(times) {
661
- const names = [
662
- "Sunday",
663
- "Monday",
664
- "Tuesday",
665
- "Wednesday",
666
- "Thursday",
667
- "Friday",
668
- "Saturday"
669
- ];
670
- const groups = [];
671
- for (let day = 0; day < 7; day++) {
672
- const intervals = times.filter((time) => !time.disabled && time.day === day).sort((a, b) => a.opens.localeCompare(b.opens));
673
- if (!intervals.length) {
674
- continue;
675
- }
676
- const hoursLabel = intervals.map(
677
- (time) => `${formatWallClock(time.opens)} \u2013 ${formatWallClock(time.closes)}`
678
- ).join(", ");
679
- const last = groups[groups.length - 1];
680
- if (last && last.days[last.days.length - 1] === day - 1 && last.hoursLabel === hoursLabel) {
681
- last.days.push(day);
682
- last.dayLabel = `${names[last.days[0]]} \u2013 ${names[day]}`;
683
- } else {
684
- groups.push({ days: [day], dayLabel: names[day], hoursLabel });
685
- }
686
- }
687
- return groups;
688
- }
689
- function dateOffset(date, days) {
690
- return new Date(Date.parse(`${date}T00:00:00Z`) + days * DAY).toISOString().slice(0, 10);
691
- }
692
- function wallInstants(date, clock, timezone) {
693
- const time = parseWallClock(clock);
694
- if (!time) {
695
- return [];
696
- }
697
- const wall = Date.parse(`${date}T00:00:00Z`) + ((time.hour24 * 60 + time.minute) * 60 + time.second) * 1e3;
698
- const offsets = /* @__PURE__ */ new Set();
699
- for (const shift of [-DAY, 0, DAY]) {
700
- const sample = wall + shift;
701
- const local = localParts(new Date(sample), timezone);
702
- const asUTC = Date.parse(`${local.date}T00:00:00Z`) + ((local.hour * 60 + local.minute) * 60 + local.second) * 1e3;
703
- offsets.add(asUTC - sample);
704
- }
705
- const matches = [];
706
- for (const offset of offsets) {
707
- const instant = wall - offset;
708
- const local = localParts(new Date(instant), timezone);
709
- if (local.date === date && local.hour === time.hour24 && local.minute === time.minute && local.second === time.second) {
710
- matches.push(instant);
711
- }
712
- }
713
- return matches.sort((a, b) => a - b);
714
- }
715
- function openingIntervals(branch, timezone, now, days = 8) {
716
- const today = localParts(now, timezone).date;
717
- const intervals = [];
718
- for (let offset = -1; offset < days; offset++) {
719
- const date = dateOffset(today, offset);
720
- const day = (/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay();
721
- const special = branch.specialHours?.find((hours) => hours.date === date);
722
- const times = special ? special.closed || !special.opens || !special.closes ? [] : [{ opens: special.opens, closes: special.closes }] : branch.openingTimes.filter(
723
- (hours) => hours.day === day && !hours.disabled
724
- );
725
- for (const hours of times) {
726
- const starts = wallInstants(date, hours.opens, timezone);
727
- const overnight = hours.closes <= hours.opens;
728
- const endDate = overnight ? dateOffset(date, 1) : date;
729
- const ends = wallInstants(endDate, hours.closes, timezone);
730
- const start = starts[0];
731
- let end = ends[ends.length - 1];
732
- if (overnight && branch.specialHours?.some(
733
- (hours2) => hours2.date === endDate && hours2.closed
734
- )) {
735
- end = wallInstants(endDate, "00:00", timezone)[0];
736
- }
737
- if (start !== void 0 && end !== void 0 && start < end) {
738
- intervals.push({ start, end });
739
- }
740
- }
741
- }
742
- const closedUntil = branch.closedUntil ? Date.parse(branch.closedUntil) : -Infinity;
743
- const sorted = intervals.map(({ start, end }) => ({ start: Math.max(start, closedUntil), end })).filter(({ start, end }) => start < end).sort((a, b) => a.start - b.start);
744
- const merged = [];
745
- for (const interval of sorted) {
746
- const last = merged[merged.length - 1];
747
- if (last && interval.start <= last.end) {
748
- last.end = Math.max(last.end, interval.end);
749
- } else {
750
- merged.push({ ...interval });
751
- }
752
- }
753
- return merged;
754
- }
755
- function getOpenStatus(branch, timezone, now = /* @__PURE__ */ new Date()) {
756
- const intervals = openingIntervals(branch, timezone, now);
757
- const current = intervals.find(
758
- ({ start, end }) => start <= now.getTime() && end > now.getTime()
759
- );
760
- const next = current?.end ?? intervals.find(({ start }) => start > now.getTime())?.start;
761
- if (next === void 0) {
762
- return { isOpen: false, label: "Closed" };
763
- }
764
- const nextChange = new Date(next);
765
- const local = localParts(nextChange, timezone);
766
- const clock = formatWallClock(
767
- `${String(local.hour).padStart(2, "0")}:${String(local.minute).padStart(2, "0")}`
768
- ).toLowerCase().replace(" ", "");
769
- const today = localParts(now, timezone).date;
770
- const suffix = local.date === today ? "" : local.date === dateOffset(today, 1) ? " tomorrow" : ` ${new Intl.DateTimeFormat("en", { timeZone: timezone, weekday: "long" }).format(nextChange)}`;
771
- return {
772
- isOpen: Boolean(current),
773
- label: current ? `Open until ${clock}` : `Opens ${clock}${suffix}`,
774
- nextChange
775
- };
776
- }
777
-
778
- // src/fulfilment/branch-picker.tsx
779
- var Picker2 = forwardRef(function Picker3({ branches, value, onSelect, ...props }, ref) {
780
- const cart = useCart();
781
- const store = useStore();
782
- const [isLoading, setLoading] = useState(false);
783
- const [error, setError] = useState();
784
- const selected = branches.find(
785
- (branch) => branch.id === (value ?? cart.branchId)
786
- );
787
- const select = async (branch) => {
788
- if (isLoading || branch.id === cart.branchId || !branches.some((candidate) => candidate.id === branch.id)) {
789
- return;
790
- }
791
- setLoading(true);
792
- setError(void 0);
793
- try {
794
- await cart.actions.moveTo(branch.id);
795
- onSelect?.(branch);
796
- } catch (failure) {
797
- setError(failure);
798
- } finally {
799
- setLoading(false);
800
- }
801
- };
802
- return part(
803
- "div",
804
- props,
805
- {
806
- branches,
807
- selected,
808
- select,
809
- isLoading,
810
- error,
811
- isOpenNow: (branch) => getOpenStatus(branch, branch.timezone ?? store.data?.timezone ?? "UTC").isOpen
812
- },
813
- {
814
- role: "group",
815
- "aria-busy": isLoading,
816
- "data-state": isLoading ? "loading" : error ? "invalid" : "valid"
817
- },
818
- ref
819
- );
820
- });
821
- var Branch = { Picker: Picker2 };
822
- function scheduleSlots(branch, timezone, now, {
823
- intervalMinutes = 30,
824
- maxDays = 14,
825
- noticeMinutes = 0,
826
- menuSchedule
827
- } = {}) {
828
- if (!Number.isInteger(intervalMinutes) || intervalMinutes < 1 || !Number.isInteger(maxDays) || maxDays < 1 || maxDays > 90 || !Number.isFinite(noticeMinutes) || noticeMinutes < 0) {
829
- throw new RangeError(
830
- "Schedule requires a positive interval, 1\u201390 days and a non-negative notice"
831
- );
832
- }
833
- if (!branch.fulfilment.scheduling) {
834
- return [];
835
- }
836
- const preorder = menuSchedule?.preorder ? menuSchedule : void 0;
837
- const days = preorder?.preorderMaxDays && preorder.preorderMaxDays > 0 ? Math.min(maxDays, preorder.preorderMaxDays) : maxDays;
838
- const earliest = now.getTime() + Math.max(noticeMinutes, (preorder?.preorderNoticeHours ?? 0) * 60) * 6e4;
839
- const latest = now.getTime() + days * 864e5;
840
- const menuIntervals = menuSchedule?.openingTimes.length ? openingIntervals(
841
- {
842
- ...branch,
843
- openingTimes: menuSchedule.openingTimes,
844
- specialHours: []
845
- },
846
- timezone,
847
- now,
848
- days + 1
849
- ) : void 0;
850
- const slots = [];
851
- for (const { start, end } of openingIntervals(
852
- branch,
853
- timezone,
854
- now,
855
- days + 1
856
- )) {
857
- const first = start + Math.max(0, Math.ceil((earliest - start) / (intervalMinutes * 6e4))) * intervalMinutes * 6e4;
858
- for (let instant = first; instant < end && instant <= latest; instant += intervalMinutes * 6e4) {
859
- if (menuIntervals && !menuIntervals.some(
860
- (interval) => interval.start <= instant && interval.end > instant
861
- )) {
862
- continue;
863
- }
864
- const date = new Date(instant);
865
- slots.push({
866
- value: date.toISOString(),
867
- date: localParts(date, timezone).date,
868
- label: new Intl.DateTimeFormat("en", {
869
- timeZone: timezone,
870
- hour: "numeric",
871
- minute: "2-digit",
872
- timeZoneName: "shortOffset"
873
- }).format(date)
874
- });
875
- }
876
- }
877
- return slots;
878
- }
879
- var Schedule = forwardRef(function Schedule2({
880
- branch,
881
- timezone: suppliedTimezone,
882
- value,
883
- defaultValue = null,
884
- onValueChange,
885
- intervalMinutes,
886
- maxDays,
887
- noticeMinutes,
888
- ...props
889
- }, ref) {
890
- const store = useStore();
891
- const menu = useMenu(branch.id, { metadataOnly: true });
892
- const timezone = suppliedTimezone ?? branch.timezone ?? store.data?.timezone ?? "UTC";
893
- const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
894
- useEffect(() => {
895
- const timer = setTimeout(
896
- () => setNow(/* @__PURE__ */ new Date()),
897
- 6e4 - Date.now() % 6e4
898
- );
899
- return () => clearTimeout(timer);
900
- }, [now]);
901
- const [selection, setSelection] = useControllableState({
902
- value,
903
- defaultValue,
904
- onChange: onValueChange
905
- });
906
- const slots = useMemo(
907
- () => menu.isLoading || menu.error ? [] : scheduleSlots(branch, timezone, now, {
908
- intervalMinutes,
909
- maxDays,
910
- noticeMinutes,
911
- menuSchedule: menu.data?.schedule
912
- }),
913
- [
914
- branch,
915
- timezone,
916
- now,
917
- intervalMinutes,
918
- maxDays,
919
- noticeMinutes,
920
- menu.data?.schedule,
921
- menu.isLoading,
922
- menu.error
923
- ]
924
- );
925
- const days = Array.from(new Set(slots.map((slot) => slot.date))).map(
926
- (date) => ({ date, slots: slots.filter((slot) => slot.date === date) })
927
- );
928
- const canAsap = !menu.isLoading && !menu.error && !menu.data?.schedule?.preorder && getOpenStatus(branch, timezone, now).isOpen;
929
- const isValid = selection === null ? canAsap : slots.some((slot) => slot.value === selection);
930
- const setValue = (next) => {
931
- if (next === null ? canAsap : slots.some((slot) => slot.value === next)) {
932
- setSelection(next);
933
- }
934
- };
935
- return part(
936
- "div",
937
- props,
938
- {
939
- days,
940
- slots,
941
- value: selection,
942
- setValue,
943
- isAsap: selection === null,
944
- canAsap,
945
- isValid
946
- },
947
- {
948
- role: "group",
949
- "data-state": isValid ? "valid" : "invalid",
950
- "data-invalid": isValid ? void 0 : ""
951
- },
952
- ref
953
- );
954
- });
955
- var Switch = forwardRef(function Switch2({ value, defaultValue, onValueChange, ...props }, ref) {
956
- const cart = useCart();
957
- const store = useStore();
958
- const [selection, setSelection] = useControllableState({
959
- value,
960
- defaultValue: defaultValue ?? cart.fulfilment,
961
- onChange: onValueChange
962
- });
963
- const current = value === void 0 && defaultValue === void 0 ? cart.fulfilment : selection;
964
- const branch = store.data?.branches.find(
965
- (branch2) => branch2.id === (cart.branchId ?? current?.branchId)
966
- ) ?? store.data?.branches.find((branch2) => branch2.isDefault) ?? store.data?.branches[0];
967
- const available = branch?.fulfilment;
968
- const options = [];
969
- if (available?.pickup) {
970
- options.push("pickup");
971
- }
972
- if (available?.delivery) {
973
- options.push("delivery");
974
- }
975
- if (available?.dineIn) {
976
- options.push("dine_in");
977
- }
978
- const setValue = (next) => {
979
- if (!options.includes(next.type) || next.branchId !== branch?.id) {
980
- throw new ZappieError(
981
- "invalid_request",
982
- "Select an offered fulfilment mode for this branch"
983
- );
984
- }
985
- cart.actions.setFulfilment(next);
986
- setSelection(next);
987
- };
988
- return part(
989
- "div",
990
- props,
991
- { options, value: current, setValue },
992
- { role: "group" },
993
- ref
994
- );
995
- });
996
- var Fulfilment = { Switch };
997
- var ItemFormStateContext = createContext(null);
998
- function useItemForm() {
999
- const value = useContext(ItemFormStateContext);
1000
- if (!value) {
1001
- throw new Error("ItemForm parts require ItemForm.Root");
1002
- }
1003
- return value;
1004
- }
1005
- var GroupRegistry = createContext(
1006
- null
1007
- );
1008
- function Root3({
1009
- item,
1010
- value,
1011
- defaultValue,
1012
- onValueChange,
1013
- defaultQuantity,
1014
- onAdded,
1015
- onCommit,
1016
- ...props
1017
- }) {
1018
- const client = useZappie();
1019
- const menu = useCartCatalogue();
1020
- const [input, setInput] = useControllableState({
1021
- value,
1022
- defaultValue: {
1023
- ...defaultValue,
1024
- itemId: item.id,
1025
- quantity: defaultQuantity ?? defaultValue?.quantity ?? item.quantity.min
1026
- },
1027
- onChange: onValueChange
1028
- });
1029
- const registry = useRef(/* @__PURE__ */ new Map());
1030
- const [added, setAdded] = useState(false);
1031
- const [attempted, setAttempted] = useState(false);
1032
- const [failure, setFailure] = useState(null);
1033
- const normalized = useMemo(
1034
- () => ({ ...input, itemId: item.id }),
1035
- [input, item.id]
1036
- );
1037
- const preview = useMemo(
1038
- () => previewItem(item, normalized),
1039
- [item, normalized]
1040
- );
1041
- const validation = useMemo(
1042
- () => client.cart.validate(normalized),
1043
- [client, normalized, menu]
1044
- );
1045
- const issues = failure ?? (attempted && !validation.ok ? validation.issues : []);
1046
- const setValue = useCallback(
1047
- (next) => {
1048
- setAdded(false);
1049
- setFailure(null);
1050
- setInput({ ...next, itemId: item.id });
1051
- },
1052
- [setInput, item.id]
1053
- );
1054
- const setQuantity = useCallback(
1055
- (quantity) => setValue({ ...normalized, quantity }),
1056
- [setValue, normalized]
1057
- );
1058
- const focusIssue = useCallback((errors) => {
1059
- for (const [groupId, element] of registry.current) {
1060
- if (errors.some((issue) => issue.groupId === groupId)) {
1061
- element.focus();
1062
- break;
1063
- }
1064
- }
1065
- }, []);
1066
- const add = useCallback(() => {
1067
- const result = client.cart.validate(normalized);
1068
- if (!result.ok) {
1069
- setAttempted(true);
1070
- setFailure(result.issues);
1071
- focusIssue(result.issues);
1072
- return null;
1073
- }
1074
- try {
1075
- const line = onCommit ? onCommit(normalized) : client.cart.add(item, normalized);
1076
- setAttempted(false);
1077
- setAdded(true);
1078
- onAdded?.(line);
1079
- return line;
1080
- } catch (error) {
1081
- if (error instanceof ZappieCartError) {
1082
- setAttempted(true);
1083
- setFailure([...error.issues]);
1084
- focusIssue([...error.issues]);
1085
- return null;
1086
- }
1087
- throw error;
1088
- }
1089
- }, [client, normalized, item, onAdded, onCommit, focusIssue]);
1090
- const context = useMemo(
1091
- () => ({
1092
- item,
1093
- ...preview,
1094
- quantity: normalized.quantity ?? item.quantity.min,
1095
- setQuantity,
1096
- issues,
1097
- canAdd: validation.ok && !failure,
1098
- add,
1099
- status: added ? "added" : validation.ok ? "valid" : "incomplete",
1100
- value: normalized,
1101
- setValue
1102
- }),
1103
- [
1104
- item,
1105
- preview,
1106
- normalized,
1107
- setQuantity,
1108
- issues,
1109
- validation.ok,
1110
- failure,
1111
- add,
1112
- added,
1113
- setValue
1114
- ]
1115
- );
1116
- return /* @__PURE__ */ jsx(ItemFormStateContext.Provider, { value: context, children: /* @__PURE__ */ jsx(GroupRegistry.Provider, { value: registry.current, children: part("div", props, context, {
1117
- "data-zappie-item": item.id,
1118
- "data-state": issues.length ? "invalid" : "valid",
1119
- "data-sold-out": item.isAvailable ? void 0 : ""
1120
- }) }) });
1121
- }
1122
-
1123
- // src/item-form/options.tsx
1124
- var Options = forwardRef(
1125
- function Options2(props, ref) {
1126
- return part("div", props, useItemForm(), {}, ref);
1127
- }
1128
- );
1129
- var Add = forwardRef(
1130
- function Add2(props, ref) {
1131
- const context = useItemForm();
1132
- return part(
1133
- "button",
1134
- props,
1135
- context,
1136
- {
1137
- type: "button",
1138
- onClick: context.add,
1139
- "data-zappie-action": "add-to-cart"
1140
- },
1141
- ref
1142
- );
1143
- }
1144
- );
1145
- var Context4 = createContext(null);
1146
- function useGroup() {
1147
- const value = useContext(Context4);
1148
- if (!value) {
1149
- throw new Error("Choice and OptionGroup.Input require OptionGroup");
1150
- }
1151
- return value;
1152
- }
1153
- var Group = forwardRef(function OptionGroup({ group: supplied, ...props }, ref) {
1154
- const ctx = useItemForm();
1155
- const registry = useContext(GroupRegistry);
1156
- const group = ctx.groups.find((candidate) => candidate.id === supplied.id) ?? supplied;
1157
- const mine = ctx.value.choices?.filter((choice) => choice.groupId === group.id) ?? [];
1158
- const selected = mine.map((choice) => choice.choiceId);
1159
- const quantities = Object.fromEntries(
1160
- mine.map((choice) => [choice.choiceId, choice.quantity ?? 1])
1161
- );
1162
- const invalid = ctx.issues.some((issue) => issue.groupId === group.id);
1163
- const input = ctx.value.inputs?.[group.id] ?? "";
1164
- const latest = useRef({ ctx, group });
1165
- latest.current = { ctx, group };
1166
- const actions = useMemo(
1167
- () => ({
1168
- toggle: (id) => {
1169
- const { ctx: ctx2, group: group2 } = latest.current;
1170
- const choice = group2.choices.find((candidate) => candidate.id === id);
1171
- if (!choice?.isAvailable) {
1172
- return;
1173
- }
1174
- const previous = ctx2.value.choices ?? [];
1175
- const chosen = previous.some(
1176
- (candidate) => candidate.groupId === group2.id && candidate.choiceId === id
1177
- );
1178
- let choices = previous.filter(
1179
- (candidate) => candidate.groupId !== group2.id || group2.selection.max !== 1 && candidate.choiceId !== id
1180
- );
1181
- if (!chosen || group2.selection.max === 1 && group2.required) {
1182
- choices = [
1183
- ...choices,
1184
- {
1185
- groupId: group2.id,
1186
- choiceId: id,
1187
- quantity: choice.quantity?.min ?? 1
1188
- }
1189
- ];
1190
- }
1191
- const inputs = { ...ctx2.value.inputs };
1192
- delete inputs[group2.id];
1193
- ctx2.setValue({ ...ctx2.value, choices, inputs });
1194
- },
1195
- setQuantity: (id, quantity) => {
1196
- const { ctx: ctx2, group: group2 } = latest.current;
1197
- const choice = group2.choices.find((candidate) => candidate.id === id);
1198
- if (!choice?.isAvailable || !Number.isInteger(quantity)) {
1199
- return;
1200
- }
1201
- const others = (ctx2.value.choices ?? []).filter(
1202
- (candidate) => candidate.groupId !== group2.id || group2.selection.max !== 1 && candidate.choiceId !== id
1203
- );
1204
- if (quantity <= 0) {
1205
- ctx2.setValue({
1206
- ...ctx2.value,
1207
- choices: (ctx2.value.choices ?? []).filter(
1208
- (candidate) => candidate.groupId !== group2.id || candidate.choiceId !== id
1209
- )
1210
- });
1211
- return;
1212
- }
1213
- const min = choice.quantity?.min ?? 1;
1214
- const max = choice.quantity?.max ?? 99;
1215
- ctx2.setValue({
1216
- ...ctx2.value,
1217
- choices: [
1218
- ...others,
1219
- {
1220
- groupId: group2.id,
1221
- choiceId: id,
1222
- quantity: Math.min(max, Math.max(min, quantity))
1223
- }
1224
- ]
1225
- });
1226
- },
1227
- setInput: (value) => {
1228
- const { ctx: ctx2, group: group2 } = latest.current;
1229
- ctx2.setValue({
1230
- ...ctx2.value,
1231
- choices: (ctx2.value.choices ?? []).filter(
1232
- (choice) => choice.groupId !== group2.id
1233
- ),
1234
- inputs: {
1235
- ...ctx2.value.inputs,
1236
- [group2.id]: value.slice(0, group2.input?.maxLength ?? 500)
1237
- }
1238
- });
1239
- }
1240
- }),
1241
- []
1242
- );
1243
- const selectionKey = JSON.stringify(quantities);
1244
- const state = useMemo(
1245
- () => ({ group, selected, quantities, invalid, input, ...actions }),
1246
- [group, selectionKey, invalid, input, actions]
1247
- );
1248
- const single = group.kind === "select" && group.selection.max === 1;
1249
- return /* @__PURE__ */ jsx(Context4.Provider, { value: state, children: part(
1250
- "div",
1251
- props,
1252
- state,
1253
- {
1254
- role: single ? "radiogroup" : "group",
1255
- tabIndex: -1,
1256
- "aria-label": group.name,
1257
- "aria-required": single ? group.required : void 0,
1258
- "aria-invalid": invalid || void 0,
1259
- "data-invalid": flag(invalid),
1260
- "data-state": invalid ? "invalid" : "valid",
1261
- "data-zappie-option-group": group.id,
1262
- onKeyDown: (event) => {
1263
- if (!single || ![
1264
- "ArrowDown",
1265
- "ArrowRight",
1266
- "ArrowUp",
1267
- "ArrowLeft",
1268
- "Home",
1269
- "End"
1270
- ].includes(event.key)) {
1271
- return;
1272
- }
1273
- const choices = Array.from(
1274
- event.currentTarget.querySelectorAll(
1275
- '[role="radio"]:not([aria-disabled="true"])'
1276
- )
1277
- );
1278
- if (!choices.length) {
1279
- return;
1280
- }
1281
- const current = choices.indexOf(
1282
- document.activeElement
1283
- );
1284
- const index = event.key === "Home" ? 0 : event.key === "End" ? choices.length - 1 : (current + (event.key === "ArrowUp" || event.key === "ArrowLeft" ? -1 : 1) + choices.length) % choices.length;
1285
- event.preventDefault();
1286
- choices[index]?.focus();
1287
- choices[index]?.click();
1288
- }
1289
- },
1290
- composeRefs(ref, (node) => {
1291
- if (node) {
1292
- registry.set(group.id, node);
1293
- } else {
1294
- registry.delete(group.id);
1295
- }
1296
- })
1297
- ) });
1298
- });
1299
- var ChoiceView = memo(
1300
- forwardRef(function ChoiceView2({
1301
- choice,
1302
- selected,
1303
- single,
1304
- tabIndex,
1305
- quantity,
1306
- toggleChoice,
1307
- setChoiceQuantity,
1308
- ...props
1309
- }, ref) {
1310
- const toggle = () => toggleChoice(choice.id);
1311
- const state = {
1312
- choice,
1313
- selected,
1314
- disabled: !choice.isAvailable,
1315
- toggle,
1316
- quantity,
1317
- min: choice.quantity?.min ?? 1,
1318
- max: choice.quantity?.max ?? 99,
1319
- setQuantity: (next) => setChoiceQuantity(choice.id, next)
1320
- };
1321
- return part(
1322
- "button",
1323
- props,
1324
- state,
1325
- {
1326
- type: "button",
1327
- role: single ? "radio" : "checkbox",
1328
- tabIndex,
1329
- "aria-checked": selected,
1330
- "aria-disabled": !choice.isAvailable,
1331
- disabled: !choice.isAvailable,
1332
- "data-selected": flag(selected),
1333
- "data-sold-out": flag(!choice.isAvailable),
1334
- "data-disabled": flag(!choice.isAvailable),
1335
- "data-state": selected ? "checked" : "unchecked",
1336
- "data-zappie-option": choice.id,
1337
- onClick: toggle
1338
- },
1339
- ref
1340
- );
1341
- })
1342
- );
1343
- var Choice = forwardRef(function Choice2(props, ref) {
1344
- const ctx = useGroup();
1345
- const single = ctx.group.selection.max === 1;
1346
- const selected = ctx.selected.includes(props.choice.id);
1347
- const first = ctx.group.choices.find((choice) => choice.isAvailable);
1348
- const tabIndex = !single || selected || !ctx.selected.length && first?.id === props.choice.id ? 0 : -1;
1349
- return /* @__PURE__ */ jsx(
1350
- ChoiceView,
1351
- {
1352
- ...props,
1353
- ref,
1354
- selected,
1355
- single,
1356
- tabIndex,
1357
- quantity: ctx.quantities[props.choice.id] ?? 0,
1358
- toggleChoice: ctx.toggle,
1359
- setChoiceQuantity: ctx.setQuantity
1360
- }
1361
- );
1362
- });
1363
- var Input3 = forwardRef(
1364
- function Input4(props, ref) {
1365
- const ctx = useGroup();
1366
- const id = usePartId(props.id);
1367
- const toggle = ctx.group.kind === "toggle";
1368
- return part(
1369
- "input",
1370
- props,
1371
- ctx,
1372
- {
1373
- id,
1374
- type: toggle ? "checkbox" : ctx.group.kind === "select" ? "text" : ctx.group.kind,
1375
- value: toggle ? void 0 : ctx.input,
1376
- checked: toggle ? ctx.input === "true" : void 0,
1377
- required: ctx.group.required,
1378
- "aria-label": ctx.group.name,
1379
- "aria-invalid": ctx.invalid || void 0,
1380
- "data-invalid": flag(ctx.invalid),
1381
- maxLength: ctx.group.input?.maxLength ?? 500,
1382
- placeholder: ctx.group.input?.placeholder,
1383
- onChange: (event) => ctx.setInput(
1384
- toggle ? String(event.target.checked) : event.target.value
1385
- )
1386
- },
1387
- ref
1388
- );
1389
- }
1390
- );
1391
- var OptionGroup2 = Object.assign(Group, { Input: Input3 });
1392
-
1393
- // src/item-form/index.ts
1394
- var ItemForm = { Root: Root3, Options, Add };
1395
- function Image({
1396
- src,
1397
- sizes = "100vw",
1398
- priority = false,
1399
- quality,
1400
- mode = "resize",
1401
- alt,
1402
- ...props
1403
- }) {
1404
- const responsive = mode === "resize" && /^https?:\/\//.test(src);
1405
- return /* @__PURE__ */ jsx(
1406
- "img",
1407
- {
1408
- ...props,
1409
- alt,
1410
- src: responsive ? resized(src, 960, quality) : src,
1411
- ...responsive ? { srcSet: srcSet(src, quality), sizes } : {},
1412
- loading: priority ? "eager" : "lazy",
1413
- decoding: priority ? "sync" : "async",
1414
- ...priority ? { fetchPriority: "high" } : {}
1415
- }
1416
- );
1417
- }
1418
- var Money = forwardRef(function Money2({ value, formatOptions, ...props }, ref) {
1419
- const formatted = format.money(value, formatOptions);
1420
- return part(
1421
- "data",
1422
- { ...props, children: props.children ?? formatted },
1423
- { formatted },
1424
- { value: String(value.amountMinor) },
1425
- ref
1426
- );
1427
- });
1428
- function slotAttributes(index, length, value, disabled, update, focus) {
1429
- return {
1430
- type: "text",
1431
- inputMode: "numeric",
1432
- autoComplete: index === 0 ? "one-time-code" : "off",
1433
- "aria-label": `Digit ${index + 1} of ${length}`,
1434
- "data-otp-slot": index,
1435
- value: value[index] ?? "",
1436
- disabled,
1437
- onChange: (event) => update(index, event.target.value),
1438
- onPaste: (event) => {
1439
- event.preventDefault();
1440
- update(index, event.clipboardData.getData("text"));
1441
- },
1442
- onKeyDown: (event) => {
1443
- if (event.key === "Backspace") {
1444
- event.preventDefault();
1445
- const target = value[index] ? index : Math.max(0, index - 1);
1446
- update(target, "");
1447
- focus(target);
1448
- } else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
1449
- event.preventDefault();
1450
- focus(index + (event.key === "ArrowLeft" ? -1 : 1));
1451
- }
1452
- },
1453
- onFocus: (event) => event.currentTarget.select()
1454
- };
1455
- }
1456
- var Input5 = forwardRef(function Input6({
1457
- length = 6,
1458
- value,
1459
- defaultValue = "",
1460
- onValueChange,
1461
- onComplete,
1462
- disabled = false,
1463
- ...props
1464
- }, ref) {
1465
- if (!Number.isInteger(length) || length < 1 || length > 12) {
1466
- throw new RangeError("OTP length must be 1\u201312");
1467
- }
1468
- const [current, setValue] = useControllableState({
1469
- value,
1470
- defaultValue,
1471
- onChange: onValueChange
1472
- });
1473
- const element = useRef(null);
1474
- const previous = useRef("");
1475
- const callback = useRef(onComplete);
1476
- callback.current = onComplete;
1477
- useEffect(() => {
1478
- if (current.length === length && /^\d+$/.test(current) && previous.current !== current) {
1479
- callback.current?.(current);
1480
- }
1481
- previous.current = current;
1482
- }, [current, length]);
1483
- const focus = (index) => element.current?.querySelector(
1484
- `[data-otp-slot="${Math.max(0, Math.min(length - 1, index))}"]`
1485
- )?.focus();
1486
- const update = (index, text) => {
1487
- if (disabled) {
1488
- return;
1489
- }
1490
- const digits = text.replace(/\D/g, "");
1491
- const start = digits.length >= length ? 0 : index;
1492
- const next = (current.slice(0, start) + digits + current.slice(start + Math.max(1, digits.length))).slice(0, length);
1493
- setValue(next);
1494
- if (digits) {
1495
- focus(start + digits.length);
1496
- }
1497
- };
1498
- const adopt = useRef(update);
1499
- adopt.current = update;
1500
- useEffect(() => {
1501
- const typed = Array.from(
1502
- element.current?.querySelectorAll("[data-otp-slot]") ?? [],
1503
- (input) => input.value
1504
- ).join("");
1505
- if (typed && typed !== current) {
1506
- adopt.current(0, typed);
1507
- }
1508
- }, []);
1509
- const getSlotProps = (index) => slotAttributes(index, length, current, disabled, update, focus);
1510
- const slots = Array.from({ length }, (_, index) => current[index] ?? "");
1511
- return part(
1512
- "div",
1513
- {
1514
- ...props,
1515
- children: props.children ?? slots.map((_, index) => /* @__PURE__ */ jsx("input", { ...getSlotProps(index) }, index))
1516
- },
1517
- { value: current, slots, getSlotProps },
1518
- { role: "group", "data-disabled": disabled ? "" : void 0 },
1519
- composeRefs(ref, element)
1520
- );
1521
- });
1522
- var Otp = { Input: Input5 };
1523
- var engineModule;
1524
- var enginePromise;
1525
- function loadOverlayEngine() {
1526
- enginePromise ?? (enginePromise = import('./overlay/engine.js').then(
1527
- (module) => engineModule = module,
1528
- (error) => {
1529
- enginePromise = void 0;
1530
- throw error;
1531
- }
1532
- ));
1533
- return enginePromise;
1534
- }
1535
- function useOverlay(param, id, options) {
1536
- const search = useSyncExternalStore(
1537
- subscribeLocation,
1538
- locationSnapshot,
1539
- () => ""
1540
- );
1541
- const optionsRef = useRef(options);
1542
- optionsRef.current = options;
1543
- const open = useCallback(
1544
- (query) => openOverlay(param, id, query),
1545
- [param, id]
1546
- );
1547
- const close = useCallback(
1548
- () => closeOverlay(param, id, optionsRef.current),
1549
- [param, id]
1550
- );
1551
- return useMemo(
1552
- () => ({
1553
- isOpen: new URLSearchParams(search).get(param) === id,
1554
- open,
1555
- close
1556
- }),
1557
- [search, param, id, open, close]
1558
- );
1559
- }
1560
- var BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 };
1561
- function usePresentation(value) {
1562
- const key = typeof value === "string" ? value : JSON.stringify(value);
1563
- const resolve = useCallback(() => {
1564
- const parsed = key.startsWith("{") ? JSON.parse(key) : key;
1565
- if (typeof parsed === "string") {
1566
- return parsed;
1567
- }
1568
- let current = parsed.base;
1569
- for (const [name, width] of Object.entries(BREAKPOINTS)) {
1570
- const at = parsed[name];
1571
- if (at && window.matchMedia(`(min-width: ${width}px)`).matches) {
1572
- current = at;
1573
- }
1574
- }
1575
- return current;
1576
- }, [key]);
1577
- const subscribe = useCallback(
1578
- (onChange) => {
1579
- if (!key.startsWith("{")) {
1580
- return () => {
1581
- };
1582
- }
1583
- const queries = Object.values(BREAKPOINTS).map(
1584
- (width) => window.matchMedia(`(min-width: ${width}px)`)
1585
- );
1586
- queries.forEach((query) => query.addEventListener("change", onChange));
1587
- return () => queries.forEach(
1588
- (query) => query.removeEventListener("change", onChange)
1589
- );
1590
- },
1591
- [key]
1592
- );
1593
- return useSyncExternalStore(
1594
- subscribe,
1595
- resolve,
1596
- () => typeof value === "string" ? value : value.base
1597
- );
1598
- }
1599
- var Context5 = createContext(null);
1600
- var EngineContext = createContext(null);
1601
- function useContextValue2() {
1602
- const value = useContext(Context5);
1603
- if (!value) {
1604
- throw new Error("Overlay parts require Overlay.Root");
1605
- }
1606
- return value;
1607
- }
1608
- var prefetch = () => void loadOverlayEngine().catch(() => {
1609
- });
1610
- function Root4({
1611
- param,
1612
- id,
1613
- presentation = "dialog",
1614
- swipeDirection,
1615
- fallback,
1616
- omitParams,
1617
- navigation,
1618
- ...props
1619
- }) {
1620
- const queryOverlay = useOverlay(param, id, { fallback, omitParams });
1621
- const overlay = navigation ?? queryOverlay;
1622
- const resolved = usePresentation(presentation);
1623
- const generated = useId();
1624
- const [contentId, setContentId] = useState(`zappie-overlay-${generated}`);
1625
- const trigger = useRef(null);
1626
- useEffect(() => {
1627
- if (engineModule) {
1628
- return void 0;
1629
- }
1630
- if (typeof window.requestIdleCallback === "function") {
1631
- const handle2 = window.requestIdleCallback(prefetch);
1632
- return () => window.cancelIdleCallback(handle2);
1633
- }
1634
- const handle = window.setTimeout(prefetch, 1500);
1635
- return () => window.clearTimeout(handle);
1636
- }, []);
1637
- const context = useMemo(
1638
- () => ({
1639
- ...overlay,
1640
- presentation: resolved,
1641
- swipeDirection: swipeDirection ?? (resolved === "sheet" ? "right" : "down"),
1642
- contentId,
1643
- setContentId,
1644
- trigger
1645
- }),
1646
- [overlay, resolved, swipeDirection, contentId]
1647
- );
1648
- return /* @__PURE__ */ jsx(Context5.Provider, { value: context, children: part("div", props, overlay, {
1649
- "data-state": overlay.isOpen ? "open" : "closed",
1650
- "data-presentation": resolved
1651
- }) });
1652
- }
1653
- var Trigger = forwardRef(
1654
- function Trigger2(props, ref) {
1655
- const ctx = useContextValue2();
1656
- return part(
1657
- "button",
1658
- props,
1659
- ctx,
1660
- {
1661
- type: "button",
1662
- "aria-haspopup": "dialog",
1663
- "aria-expanded": ctx.isOpen,
1664
- "aria-controls": ctx.contentId,
1665
- "data-state": ctx.isOpen ? "open" : "closed",
1666
- onPointerEnter: prefetch,
1667
- onFocus: prefetch,
1668
- onClick: () => ctx.open()
1669
- },
1670
- composeRefs(ref, (node) => {
1671
- ctx.trigger.current = node;
1672
- })
1673
- );
1674
- }
1675
- );
1676
- var Content = forwardRef(function Content2({ onEscape, onInteractOutside, forceMount, container, ...props }, ref) {
1677
- const ctx = useContextValue2();
1678
- const [engine, setEngine] = useState(engineModule);
1679
- const [opened, setOpened] = useState(ctx.isOpen);
1680
- if (ctx.isOpen && !opened) {
1681
- setOpened(true);
1682
- }
1683
- const customId = partId(props);
1684
- const { setContentId } = ctx;
1685
- useEffect(() => {
1686
- if (customId) {
1687
- setContentId(customId);
1688
- }
1689
- }, [customId, setContentId]);
1690
- useEffect(() => {
1691
- if (engine || !(ctx.isOpen || forceMount)) {
1692
- return void 0;
1693
- }
1694
- let live = true;
1695
- void loadOverlayEngine().then(
1696
- (module) => {
1697
- if (live) {
1698
- setEngine(module);
1699
- }
1700
- },
1701
- () => {
1702
- }
1703
- );
1704
- return () => {
1705
- live = false;
1706
- };
1707
- }, [engine, ctx.isOpen, forceMount]);
1708
- if (!engine || !opened && !forceMount) {
1709
- return null;
1710
- }
1711
- return /* @__PURE__ */ jsx(EngineContext.Provider, { value: engine, children: /* @__PURE__ */ jsx(
1712
- engine.Surface,
1713
- {
1714
- open: ctx.isOpen,
1715
- presentation: ctx.presentation,
1716
- swipeDirection: ctx.swipeDirection,
1717
- contentId: ctx.contentId,
1718
- trigger: ctx.trigger,
1719
- onOpen: () => ctx.open(),
1720
- onClose: ctx.close,
1721
- onEscape,
1722
- onInteractOutside,
1723
- forceMount,
1724
- container,
1725
- state: ctx,
1726
- contentProps: props,
1727
- contentRef: ref
1728
- }
1729
- ) });
1730
- });
1731
- function useEngine() {
1732
- const engine = useContext(EngineContext);
1733
- if (!engine) {
1734
- throw new Error(
1735
- "Overlay.Title and Overlay.Close must be inside Overlay.Content"
1736
- );
1737
- }
1738
- return engine;
1739
- }
1740
- var Title = forwardRef(
1741
- function Title2(props, ref) {
1742
- const ctx = useContextValue2();
1743
- const { EngineTitle } = useEngine();
1744
- return /* @__PURE__ */ jsx(EngineTitle, { ...props, state: ctx, ref });
1745
- }
1746
- );
1747
- var Close = forwardRef(
1748
- function Close2(props, ref) {
1749
- const ctx = useContextValue2();
1750
- const { EngineClose } = useEngine();
1751
- return /* @__PURE__ */ jsx(EngineClose, { ...props, state: ctx, ref });
1752
- }
1753
- );
1754
- var Overlay = { Root: Root4, Trigger, Content, Title, Close };
1755
- var Context6 = createContext(null);
1756
- function Root5({
1757
- methods,
1758
- value,
1759
- defaultValue = null,
1760
- onValueChange,
1761
- ...props
1762
- }) {
1763
- const [selected, set] = useControllableState({
1764
- value,
1765
- defaultValue,
1766
- onChange: onValueChange
1767
- });
1768
- const normalized = methods.map(
1769
- (method) => typeof method === "string" ? { value: method } : method
1770
- );
1771
- const setValue = (next) => {
1772
- if (normalized.some((method) => method.value === next && !method.disabled)) {
1773
- set(next);
1774
- }
1775
- };
1776
- const state = { methods: normalized, value: selected, setValue };
1777
- return /* @__PURE__ */ jsx(Context6.Provider, { value: state, children: part("div", props, state, {
1778
- role: "radiogroup",
1779
- onKeyDown: (event) => {
1780
- if (![
1781
- "ArrowRight",
1782
- "ArrowDown",
1783
- "ArrowLeft",
1784
- "ArrowUp",
1785
- "Home",
1786
- "End"
1787
- ].includes(event.key)) {
1788
- return;
1789
- }
1790
- const buttons = Array.from(
1791
- event.currentTarget.querySelectorAll(
1792
- '[role="radio"]:not([aria-disabled="true"])'
1793
- )
1794
- );
1795
- if (!buttons.length) {
1796
- return;
1797
- }
1798
- event.preventDefault();
1799
- const current = buttons.indexOf(
1800
- document.activeElement
1801
- );
1802
- const index = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (current + (["ArrowLeft", "ArrowUp"].includes(event.key) ? -1 : 1) + buttons.length) % buttons.length;
1803
- buttons[index]?.focus();
1804
- buttons[index]?.click();
1805
- }
1806
- }) });
1807
- }
1808
- var Item = forwardRef(function Item2({ value, ...props }, ref) {
1809
- const ctx = useContext(Context6);
1810
- const id = usePartId();
1811
- if (!ctx) {
1812
- throw new Error("PaymentMethods.Item requires PaymentMethods.Root");
1813
- }
1814
- const method = ctx.methods.find((method2) => method2.value === value);
1815
- if (!method) {
1816
- return null;
1817
- }
1818
- const selected = ctx.value === value;
1819
- const first = ctx.methods.find((method2) => !method2.disabled)?.value;
1820
- return part(
1821
- "button",
1822
- props,
1823
- { ...method, selected },
1824
- {
1825
- id,
1826
- type: "button",
1827
- role: "radio",
1828
- "aria-checked": selected,
1829
- "aria-disabled": method.disabled || void 0,
1830
- "aria-description": method.reason,
1831
- disabled: method.disabled,
1832
- tabIndex: selected || !ctx.value && first === value ? 0 : -1,
1833
- "data-disabled": method.disabled ? "" : void 0,
1834
- "data-selected": selected ? "" : void 0,
1835
- "data-state": selected ? "checked" : "unchecked",
1836
- onClick: () => ctx.setValue(value)
1837
- },
1838
- ref
1839
- );
1840
- });
1841
- var PaymentMethods = { Root: Root5, Item };
1842
-
1843
- // src/search/engine.ts
1844
- function normalizeSearch(text) {
1845
- return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
1846
- }
1847
- function near(a, b) {
1848
- if (Math.abs(a.length - b.length) > 1) {
1849
- return false;
1850
- }
1851
- let i = 0;
1852
- let j = 0;
1853
- let edits = 0;
1854
- while (i < a.length && j < b.length) {
1855
- if (a[i] === b[j]) {
1856
- i++;
1857
- j++;
1858
- continue;
1859
- }
1860
- if (++edits > 1) {
1861
- return false;
1862
- }
1863
- if (a.length === b.length) {
1864
- if (a[i] === b[j + 1] && a[i + 1] === b[j]) {
1865
- i += 2;
1866
- j += 2;
1867
- } else {
1868
- i++;
1869
- j++;
1870
- }
1871
- } else if (a.length > b.length) {
1872
- i++;
1873
- } else {
1874
- j++;
1875
- }
1876
- }
1877
- return edits + (a.length - i) + (b.length - j) <= 1;
1878
- }
1879
- function createSearchIndex(menu) {
1880
- const entries = [
1881
- ...Object.values(menu.items).map((item) => ({
1882
- result: { type: "item", item },
1883
- fields: [
1884
- normalizeSearch(item.name),
1885
- normalizeSearch(item.tags.join(" ")),
1886
- normalizeSearch(item.description ?? "")
1887
- ]
1888
- })),
1889
- ...menu.categories.map((category) => ({
1890
- result: { type: "category", category },
1891
- fields: [
1892
- normalizeSearch(category.name),
1893
- [],
1894
- normalizeSearch(category.description ?? "")
1895
- ]
1896
- }))
1897
- ];
1898
- return (query) => {
1899
- const tokens = normalizeSearch(query);
1900
- if (!tokens.length) {
1901
- return [];
1902
- }
1903
- const results = [];
1904
- for (const entry of entries) {
1905
- let score = 0;
1906
- for (const token of tokens) {
1907
- let best = 0;
1908
- for (let field = 0; field < entry.fields.length; field++) {
1909
- const weight = [30, 20, 10][field];
1910
- for (const word of entry.fields[field]) {
1911
- if (word.startsWith(token)) {
1912
- best = Math.max(best, weight + (word === token ? 3 : 2));
1913
- } else if (token.length >= 4 && near(token, word)) {
1914
- best = Math.max(best, weight);
1915
- }
1916
- }
1917
- }
1918
- if (!best) {
1919
- score = 0;
1920
- break;
1921
- }
1922
- score += best;
1923
- }
1924
- if (score) {
1925
- results.push({ ...entry.result, score });
1926
- }
1927
- }
1928
- return results.sort((a, b) => b.score - a.score);
1929
- };
1930
- }
1931
- var Context7 = createContext(null);
1932
- function useSearch() {
1933
- const ctx = useContext(Context7);
1934
- if (!ctx) {
1935
- throw new Error("Search parts require Search.Root");
1936
- }
1937
- return ctx;
1938
- }
1939
- function Root6(props) {
1940
- if (props.source) {
1941
- return /* @__PURE__ */ jsx(ControlledRoot, { ...props, source: props.source });
1942
- }
1943
- return /* @__PURE__ */ jsx(UncontrolledRoot, { ...props });
1944
- }
1945
- function UncontrolledRoot(props) {
1946
- const cart = useCart();
1947
- const menu = useMenu(cart.branchId ?? void 0);
1948
- const [query, setQuery] = useControllableState({
1949
- value: props.query,
1950
- defaultValue: props.defaultQuery ?? "",
1951
- onChange: props.onQueryChange
1952
- });
1953
- const index = useMemo(
1954
- () => menu.data ? createSearchIndex(menu.data) : () => [],
1955
- [menu.data]
1956
- );
1957
- const results = useMemo(() => index(query), [index, query]);
1958
- return /* @__PURE__ */ jsx(
1959
- ControlledRoot,
1960
- {
1961
- ...props,
1962
- query,
1963
- onQueryChange: setQuery,
1964
- source: { results, isLoading: menu.isLoading, isStale: menu.isStale }
1965
- }
1966
- );
1967
- }
1968
- function ControlledRoot({
1969
- query,
1970
- defaultQuery = "",
1971
- onQueryChange,
1972
- onSelect = () => {
1973
- },
1974
- announcement = countAnnouncement,
1975
- source,
1976
- ...props
1977
- }) {
1978
- const client = useZappie();
1979
- const [value, setValue] = useControllableState({
1980
- value: query,
1981
- defaultValue: defaultQuery,
1982
- onChange: onQueryChange
1983
- });
1984
- const results = source.results;
1985
- const combo = useCombobox(results, onSelect);
1986
- const setQuery = (next) => {
1987
- setValue(next);
1988
- combo.setActiveIndex(-1);
1989
- combo.setOpen(true);
1990
- };
1991
- useEffect(() => {
1992
- if (!value.trim()) {
1993
- return;
1994
- }
1995
- const timer = setTimeout(
1996
- () => client._internal.emit("search", {
1997
- query: value,
1998
- resultCount: results.length
1999
- }),
2000
- 600
2001
- );
2002
- return () => clearTimeout(timer);
2003
- }, [client, value, results.length]);
2004
- const state = {
2005
- ...combo,
2006
- query: value,
2007
- setQuery,
2008
- results,
2009
- setComposing: (value2) => source.onCompositionChange?.(value2),
2010
- ...source.error ? { error: source.error } : {},
2011
- ...source.retry ? { retry: source.retry } : {}
2012
- };
2013
- return /* @__PURE__ */ jsx(Context7.Provider, { value: state, children: part(
2014
- "div",
2015
- withLiveRegion(
2016
- props,
2017
- combo.isOpen && value.trim() ? announcement(results.length) : ""
2018
- ),
2019
- state,
2020
- {
2021
- "data-state": source.error ? "error" : source.isLoading ? "loading" : source.isStale ? "stale" : "valid",
2022
- onBlur: (event) => {
2023
- if (!event.currentTarget.contains(event.relatedTarget)) {
2024
- combo.setOpen(false);
2025
- }
2026
- }
2027
- }
2028
- ) });
2029
- }
2030
- var Input7 = forwardRef(
2031
- function Input8(props, ref) {
2032
- const state = useSearch();
2033
- return part(
2034
- "input",
2035
- props,
2036
- state,
2037
- {
2038
- ...state.inputProps,
2039
- type: "search",
2040
- onCompositionStart: () => state.setComposing(true),
2041
- onCompositionEnd: () => state.setComposing(false),
2042
- value: state.query,
2043
- onChange: (event) => state.setQuery(event.target.value)
2044
- },
2045
- ref
2046
- );
2047
- }
2048
- );
2049
- var Results = forwardRef(
2050
- function Results2(props, ref) {
2051
- const state = useSearch();
2052
- return part(
2053
- "div",
2054
- props,
2055
- state,
2056
- state.listProps,
2057
- composeRefs(ref, (node) => {
2058
- if (node?.id && node.id !== state.listProps.id) {
2059
- state.setListId(node.id);
2060
- }
2061
- })
2062
- );
2063
- }
2064
- );
2065
- var Result = forwardRef(function Result2({ index, ...props }, ref) {
2066
- const state = useSearch();
2067
- return part(
2068
- "div",
2069
- props,
2070
- state,
2071
- state.getOptionProps(index),
2072
- composeRefs(ref, (node) => {
2073
- if (node?.id && node.id !== state.getOptionProps(index).id) {
2074
- state.registerOption(index, node.id);
2075
- }
2076
- })
2077
- );
2078
- });
2079
- var Search = { Root: Root6, Input: Input7, Results, Result };
2080
- function useOpenStatus(branch, timezone) {
2081
- const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
2082
- const state = getOpenStatus(branch, timezone, now);
2083
- const boundary = state.nextChange?.getTime();
2084
- useEffect(() => {
2085
- const delay = boundary !== void 0 ? boundary - Date.now() : 864e5;
2086
- const timeout = setTimeout(
2087
- () => setNow(/* @__PURE__ */ new Date()),
2088
- Math.max(1, Math.min(2147483647, delay))
2089
- );
2090
- const refresh = () => setNow(/* @__PURE__ */ new Date());
2091
- window.addEventListener("focus", refresh);
2092
- return () => {
2093
- clearTimeout(timeout);
2094
- window.removeEventListener("focus", refresh);
2095
- };
2096
- }, [branch, timezone, boundary]);
2097
- return state;
2098
- }
2099
- var OpenStatus = forwardRef(function OpenStatus2({ branch, timezone, ...props }, ref) {
2100
- const store = useStore();
2101
- const state = useOpenStatus(
2102
- branch,
2103
- timezone ?? branch.timezone ?? store.data?.timezone ?? "UTC"
2104
- );
2105
- return part(
2106
- "span",
2107
- props,
2108
- state,
2109
- { "data-state": state.isOpen ? "open" : "closed" },
2110
- ref
2111
- );
2112
- });
2113
2
 
2114
3
  // src/index.ts
4
+ import { PACKAGE_VERSION } from "./version.js";
5
+ import { Account, AccountNotConfiguredError } from "./account/index.js";
6
+ import { Cart, Totals } from "./cart/index.js";
7
+ import { Address } from "./fulfilment/address-picker.js";
8
+ import { Branch } from "./fulfilment/branch-picker.js";
9
+ import { Schedule, scheduleSlots } from "./fulfilment/schedule.js";
10
+ import { Fulfilment } from "./fulfilment/switch.js";
11
+ import { useControllableState } from "./internal/use-controllable-state.js";
12
+ export * from "./item-form/index.js";
13
+ import { Image } from "./media/image.js";
14
+ import { Money } from "./money.js";
15
+ import { Otp } from "./otp/input.js";
16
+ import { Overlay, useOverlay } from "./overlay/react.js";
17
+ import { PaymentMethods } from "./payment/methods.js";
18
+ import { createSearchIndex, Search } from "./search/index.js";
19
+ import {
20
+ formatOpeningHours,
21
+ formatWallClock,
22
+ getOpenStatus,
23
+ parseWallClock
24
+ } from "./status/hours.js";
25
+ import { OpenStatus } from "./status/open-status.js";
2115
26
  var FOUNDATIONS_VERSION = PACKAGE_VERSION;
2116
-
2117
- export { Account, AccountNotConfiguredError, Address, Branch, Cart, Choice, FOUNDATIONS_VERSION, Fulfilment, Image, ItemForm, Money, OpenStatus, OptionGroup2 as OptionGroup, Otp, Overlay, PaymentMethods, Quantity, Schedule, Search, Totals, createSearchIndex, formatOpeningHours, formatWallClock, getOpenStatus, parseWallClock, scheduleSlots, useControllableState, useOverlay };
27
+ export {
28
+ Account,
29
+ AccountNotConfiguredError,
30
+ Address,
31
+ Branch,
32
+ Cart,
33
+ FOUNDATIONS_VERSION,
34
+ Fulfilment,
35
+ Image,
36
+ Money,
37
+ OpenStatus,
38
+ Otp,
39
+ Overlay,
40
+ PaymentMethods,
41
+ Schedule,
42
+ Search,
43
+ Totals,
44
+ createSearchIndex,
45
+ formatOpeningHours,
46
+ formatWallClock,
47
+ getOpenStatus,
48
+ parseWallClock,
49
+ scheduleSlots,
50
+ useControllableState,
51
+ useOverlay
52
+ };
2118
53
  //# sourceMappingURL=index.js.map
2119
- //# sourceMappingURL=index.js.map