@zappie/theme-base 0.0.0-stage → 0.1.1

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 (50) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -2
  4. package/dist/address.d.ts +32 -0
  5. package/dist/address.js +4 -0
  6. package/dist/address.js.map +1 -0
  7. package/dist/cart.d.ts +12 -0
  8. package/dist/cart.js +1140 -0
  9. package/dist/cart.js.map +1 -0
  10. package/dist/catalogue.d.ts +60 -0
  11. package/dist/catalogue.js +1389 -0
  12. package/dist/catalogue.js.map +1 -0
  13. package/dist/checkout.d.ts +36 -0
  14. package/dist/checkout.js +4 -0
  15. package/dist/checkout.js.map +1 -0
  16. package/dist/chunk-2MV6KU3R.js +352 -0
  17. package/dist/chunk-2MV6KU3R.js.map +1 -0
  18. package/dist/chunk-FB5DRCU7.js +146 -0
  19. package/dist/chunk-FB5DRCU7.js.map +1 -0
  20. package/dist/chunk-IG63ONID.js +139 -0
  21. package/dist/chunk-IG63ONID.js.map +1 -0
  22. package/dist/chunk-ILOCSCZL.js +217 -0
  23. package/dist/chunk-ILOCSCZL.js.map +1 -0
  24. package/dist/chunk-JFEI2OUZ.js +13 -0
  25. package/dist/chunk-JFEI2OUZ.js.map +1 -0
  26. package/dist/chunk-NTJONY3N.js +95 -0
  27. package/dist/chunk-NTJONY3N.js.map +1 -0
  28. package/dist/chunk-QWST2TA2.js +22 -0
  29. package/dist/chunk-QWST2TA2.js.map +1 -0
  30. package/dist/chunk-USSLDBDY.js +48 -0
  31. package/dist/chunk-USSLDBDY.js.map +1 -0
  32. package/dist/chunk-VIAJ27YC.js +59 -0
  33. package/dist/chunk-VIAJ27YC.js.map +1 -0
  34. package/dist/host-bridge.d.ts +100 -0
  35. package/dist/host-bridge.js +4 -0
  36. package/dist/host-bridge.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +6 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/item-frame.d.ts +20 -0
  41. package/dist/item-frame.js +5 -0
  42. package/dist/item-frame.js.map +1 -0
  43. package/dist/item.d.ts +20 -0
  44. package/dist/item.js +153 -0
  45. package/dist/item.js.map +1 -0
  46. package/dist/navigation.d.ts +64 -0
  47. package/dist/navigation.js +4 -0
  48. package/dist/navigation.js.map +1 -0
  49. package/dist/styles.css +842 -0
  50. package/package.json +74 -3
@@ -0,0 +1,1389 @@
1
+ "use client";
2
+ import { Description, ItemImage } from './chunk-USSLDBDY.js';
3
+ import { ClearCart, CartLine } from './chunk-FB5DRCU7.js';
4
+ import './chunk-2MV6KU3R.js';
5
+ import { useBaseHostBridge } from './chunk-QWST2TA2.js';
6
+ import { ThemeClassProvider } from './chunk-JFEI2OUZ.js';
7
+ import { useCatalogueNavigation } from './chunk-ILOCSCZL.js';
8
+ import { useOverlay, createSearchIndex, OpenStatus, Search, Overlay, Cart } from '@zappie/foundations';
9
+ import { useZappie, useStore, useCart, useCartCatalogue } from '@zappie/sdk/react';
10
+ import { useState, useCallback, useMemo, useEffect, useRef, Fragment } from 'react';
11
+ import { jsx, jsxs } from 'react/jsx-runtime';
12
+ import { countEvent } from '@zappie/sdk/visits';
13
+ import { createSanitizeRichText, splitInfoBanner } from '@zappie/foundations/rich-text';
14
+ import { parseRichText } from '@zappie/foundations/rich-text/browser';
15
+
16
+ function CatalogueCart({
17
+ action,
18
+ count,
19
+ subtotal,
20
+ client,
21
+ onOpen
22
+ }) {
23
+ if (count === 0) {
24
+ return null;
25
+ }
26
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: "z-base-catalogue-cart", onClick: onOpen, children: [
27
+ action,
28
+ " Order \xB7 ",
29
+ count,
30
+ " items \xB7",
31
+ " ",
32
+ subtotal ? client.format.money(subtotal) : "Calculating\u2026"
33
+ ] });
34
+ }
35
+ function CatalogueHeader({
36
+ branch,
37
+ storeName,
38
+ menus,
39
+ selectedMenuId,
40
+ cartLines,
41
+ cartOpen,
42
+ fulfilmentMode,
43
+ layout,
44
+ slots,
45
+ onOpenCart,
46
+ onOpenBranchDetails,
47
+ onSelectMenu,
48
+ onOpenBranchFinder,
49
+ onSwitchFulfilment,
50
+ onSetLayout
51
+ }) {
52
+ const [canShare, setCanShare] = useState(false);
53
+ useEffect(() => setCanShare(typeof navigator.share === "function"), []);
54
+ const ToolbarAction = slots?.ToolbarAction;
55
+ return /* @__PURE__ */ jsxs("header", { children: [
56
+ /* @__PURE__ */ jsx("h2", { tabIndex: -1, id: "base-catalogue-heading", children: "Menu" }),
57
+ /* @__PURE__ */ jsx(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ "aria-label": `Open cart${cartLines.length ? `, ${cartLines.length} items` : ""}`,
62
+ onClick: onOpenCart,
63
+ children: "Cart"
64
+ }
65
+ ),
66
+ branch && /* @__PURE__ */ jsx(
67
+ "button",
68
+ {
69
+ type: "button",
70
+ onClick: onOpenBranchDetails,
71
+ "aria-label": "View opening hours",
72
+ children: /* @__PURE__ */ jsx(OpenStatus, { branch, children: ({ isOpen, label }) => branch.isOpen === isOpen ? label : branch.isOpen ? "Open" : "Temporarily closed" })
73
+ }
74
+ ),
75
+ selectedMenuId && /* @__PURE__ */ jsxs("label", { children: [
76
+ "Menu",
77
+ /* @__PURE__ */ jsx(
78
+ "select",
79
+ {
80
+ value: selectedMenuId,
81
+ onChange: (event) => onSelectMenu(event.target.value),
82
+ children: menus?.map((menu) => /* @__PURE__ */ jsxs("option", { value: menu.id, children: [
83
+ menu.name,
84
+ " (",
85
+ cartLines.filter((line) => line.menuId === menu.id).reduce((sum, line) => sum + line.quantity, 0),
86
+ " ",
87
+ "added)"
88
+ ] }, menu.id))
89
+ }
90
+ )
91
+ ] }),
92
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onOpenBranchFinder, children: "Change branch" }),
93
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onSwitchFulfilment, children: fulfilmentMode === "pickup" ? "Delivery" : "Pickup" }),
94
+ canShare && /* @__PURE__ */ jsx(
95
+ "button",
96
+ {
97
+ type: "button",
98
+ onClick: () => {
99
+ if (navigator.share) {
100
+ void navigator.share({
101
+ url: window.location.href,
102
+ ...storeName ? { title: storeName } : {}
103
+ }).catch(() => {
104
+ });
105
+ }
106
+ },
107
+ children: "Share menu"
108
+ }
109
+ ),
110
+ ToolbarAction && /* @__PURE__ */ jsx(ToolbarAction, { layout, setLayout: onSetLayout }),
111
+ cartLines.length > 0 && !cartOpen && /* @__PURE__ */ jsx(ClearCart, {})
112
+ ] });
113
+ }
114
+ function CatalogueSearch({
115
+ search,
116
+ open,
117
+ themeClass,
118
+ onOpen,
119
+ onSelectItem,
120
+ onSelectCategory
121
+ }) {
122
+ const content = /* @__PURE__ */ jsxs(
123
+ Search.Root,
124
+ {
125
+ source: search.source,
126
+ query: search.query,
127
+ onQueryChange: search.setQuery,
128
+ onSelect: (result) => {
129
+ if (result.type === "item") {
130
+ onSelectItem(result.item);
131
+ } else {
132
+ onSelectCategory(result.category.id);
133
+ }
134
+ },
135
+ children: [
136
+ /* @__PURE__ */ jsx(
137
+ Search.Input,
138
+ {
139
+ "aria-label": "Search menu",
140
+ placeholder: "Search menu",
141
+ onFocus: onOpen
142
+ }
143
+ ),
144
+ search.query && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => search.setQuery(""), children: "Clear search" }),
145
+ open && search.query && /* @__PURE__ */ jsx(Search.Results, { children: ({ results, error, retry }) => /* @__PURE__ */ jsxs(Fragment, { children: [
146
+ search.source.isLoading && /* @__PURE__ */ jsx("p", { role: "status", children: "Searching\u2026" }),
147
+ error && /* @__PURE__ */ jsxs("p", { role: "alert", children: [
148
+ error,
149
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: retry, children: "Retry search" })
150
+ ] }),
151
+ !search.source.isLoading && !error && !results.length && /* @__PURE__ */ jsx("p", { children: "No results" }),
152
+ search.hasMore && /* @__PURE__ */ jsx(
153
+ "button",
154
+ {
155
+ type: "button",
156
+ disabled: search.source.isLoading,
157
+ onClick: search.loadMore,
158
+ children: "More search results"
159
+ }
160
+ ),
161
+ results.map((result, index) => /* @__PURE__ */ jsx(
162
+ Search.Result,
163
+ {
164
+ index,
165
+ children: result.type === "item" ? /* @__PURE__ */ jsxs(Fragment, { children: [
166
+ result.item.name,
167
+ !result.item.isAvailable && /* @__PURE__ */ jsx("span", { children: result.item.stockLabel ?? "Unavailable" })
168
+ ] }) : result.category.name
169
+ },
170
+ result.type === "item" ? result.item.id : result.category.id
171
+ ))
172
+ ] }) })
173
+ ]
174
+ }
175
+ );
176
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
177
+ /* @__PURE__ */ jsx("div", { className: "z-base-search-desktop", children: content }),
178
+ /* @__PURE__ */ jsxs(Overlay.Root, { param: "catalogue_search", id: "search", presentation: "sheet", children: [
179
+ /* @__PURE__ */ jsx(Overlay.Trigger, { className: "z-base-search-mobile", children: "Search menu" }),
180
+ /* @__PURE__ */ jsxs(
181
+ Overlay.Content,
182
+ {
183
+ "data-zappie-theme": "base",
184
+ className: ["z-base-cart z-base-editor", themeClass].filter(Boolean).join(" "),
185
+ children: [
186
+ /* @__PURE__ */ jsx(Overlay.Title, { children: "Search menu" }),
187
+ /* @__PURE__ */ jsx(Overlay.Close, { children: "Close search" }),
188
+ content
189
+ ]
190
+ }
191
+ )
192
+ ] })
193
+ ] });
194
+ }
195
+ function CategoryNavigation({
196
+ categories,
197
+ active,
198
+ themeClass,
199
+ slots,
200
+ onSelect,
201
+ onFocusCategory
202
+ }) {
203
+ const overlay = useOverlay("catalogue_categories", "categories");
204
+ const [filter, setFilter] = useState("");
205
+ const [pending, setPending] = useState();
206
+ const CategoryTab = slots?.CategoryTab;
207
+ useEffect(() => {
208
+ if (pending && !overlay.isOpen) {
209
+ const frame = requestAnimationFrame(() => {
210
+ setPending(void 0);
211
+ onSelect(pending);
212
+ onFocusCategory(pending);
213
+ });
214
+ return () => cancelAnimationFrame(frame);
215
+ }
216
+ }, [pending, overlay.isOpen, onSelect, onFocusCategory]);
217
+ const filtered = categories.filter(
218
+ (category) => category.name.toLowerCase().includes(filter.toLowerCase())
219
+ );
220
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
221
+ /* @__PURE__ */ jsxs(
222
+ Overlay.Root,
223
+ {
224
+ param: "catalogue_categories",
225
+ id: "categories",
226
+ presentation: { base: "sheet", lg: "dialog" },
227
+ children: [
228
+ /* @__PURE__ */ jsx(Overlay.Trigger, { children: "Browse categories" }),
229
+ /* @__PURE__ */ jsxs(
230
+ Overlay.Content,
231
+ {
232
+ "data-zappie-theme": "base",
233
+ className: ["z-base-cart z-base-editor", themeClass].filter(Boolean).join(" "),
234
+ children: [
235
+ /* @__PURE__ */ jsx(Overlay.Title, { children: "Browse categories" }),
236
+ /* @__PURE__ */ jsx(Overlay.Close, { children: "Close categories" }),
237
+ /* @__PURE__ */ jsx(
238
+ "input",
239
+ {
240
+ "aria-label": "Filter categories",
241
+ value: filter,
242
+ onChange: (event) => setFilter(event.target.value)
243
+ }
244
+ ),
245
+ filtered.map((category) => /* @__PURE__ */ jsx(
246
+ "button",
247
+ {
248
+ type: "button",
249
+ onClick: () => {
250
+ setPending(category.id);
251
+ overlay.close();
252
+ },
253
+ children: category.name
254
+ },
255
+ category.id
256
+ )),
257
+ filtered.length === 0 && /* @__PURE__ */ jsx("p", { children: "No matching categories" })
258
+ ]
259
+ }
260
+ )
261
+ ]
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Categories", className: "z-base-category-nav", children: categories.map((category) => /* @__PURE__ */ jsx(
265
+ "a",
266
+ {
267
+ href: `#base-category-${category.id}`,
268
+ "aria-current": active === category.id ? "true" : void 0,
269
+ onClick: (event) => {
270
+ event.preventDefault();
271
+ onSelect(category.id);
272
+ },
273
+ children: CategoryTab ? /* @__PURE__ */ jsx(
274
+ CategoryTab,
275
+ {
276
+ category,
277
+ active: active === category.id
278
+ }
279
+ ) : category.name
280
+ },
281
+ category.id
282
+ )) })
283
+ ] });
284
+ }
285
+ function BaseItemTile({
286
+ item,
287
+ lines,
288
+ href,
289
+ layout,
290
+ priority,
291
+ client,
292
+ onOpen
293
+ }) {
294
+ const price = item.price;
295
+ return /* @__PURE__ */ jsxs(
296
+ "a",
297
+ {
298
+ href,
299
+ className: "z-base-item-tile",
300
+ onClick: (event) => {
301
+ event.preventDefault();
302
+ event.currentTarget.focus({ preventScroll: true });
303
+ onOpen();
304
+ },
305
+ children: [
306
+ /* @__PURE__ */ jsx(
307
+ ItemImage,
308
+ {
309
+ src: item.images[0],
310
+ sizes: layout === "LIST" ? "100vw" : "(min-width: 768px) 33vw, 50vw",
311
+ priority
312
+ }
313
+ ),
314
+ /* @__PURE__ */ jsx("span", { className: "z-base-item-name", children: item.name }),
315
+ item.description && /* @__PURE__ */ jsx(Description, { value: item.description }),
316
+ item.promo && /* @__PURE__ */ jsx("span", { children: item.promo.name }),
317
+ item.compareAtPrice && /* @__PURE__ */ jsx("del", { children: client.format.money(item.compareAtPrice) }),
318
+ price && /* @__PURE__ */ jsxs("span", { className: "z-base-amount", children: [
319
+ item.variants.some(
320
+ (variant) => variant.price.amountMinor !== price.amountMinor
321
+ ) ? "From " : "",
322
+ client.format.money(price)
323
+ ] }),
324
+ (item.stockLabel || !item.isAvailable) && /* @__PURE__ */ jsx("span", { children: item.stockLabel ?? "Unavailable" }),
325
+ lines.map((line) => /* @__PURE__ */ jsxs("span", { children: [
326
+ line.quantity,
327
+ " added"
328
+ ] }, line.id))
329
+ ]
330
+ }
331
+ );
332
+ }
333
+ function CategorySection({
334
+ category,
335
+ categoryIndex,
336
+ catalogue,
337
+ menuId,
338
+ layout,
339
+ slots,
340
+ sections,
341
+ cartOpen,
342
+ client,
343
+ busy,
344
+ hasMore,
345
+ itemHref,
346
+ onOpen,
347
+ onLoadMore
348
+ }) {
349
+ const CategoryHeader = slots?.CategoryHeader;
350
+ const Tile = slots?.Tile;
351
+ const columns = slots?.layout?.columns;
352
+ const baseColumns = typeof columns === "object" ? columns.base : columns;
353
+ const mediumColumns = typeof columns === "object" ? columns.medium : columns;
354
+ const largeColumns = typeof columns === "object" ? columns.large : columns;
355
+ return /* @__PURE__ */ jsxs(
356
+ "section",
357
+ {
358
+ id: `base-category-${category.id}`,
359
+ className: "z-base-category",
360
+ "data-unloaded": category.itemIds.length === 0,
361
+ ref: (node) => {
362
+ if (node) {
363
+ sections.current.set(category.id, node);
364
+ } else {
365
+ sections.current.delete(category.id);
366
+ }
367
+ },
368
+ children: [
369
+ /* @__PURE__ */ jsx("h3", { tabIndex: -1, children: CategoryHeader ? /* @__PURE__ */ jsx(CategoryHeader, { category }) : category.name }),
370
+ category.description && /* @__PURE__ */ jsx(Description, { value: category.description }),
371
+ /* @__PURE__ */ jsx(
372
+ Cart.Root,
373
+ {
374
+ className: "z-base-item-list",
375
+ "data-layout": layout,
376
+ "data-columns": baseColumns,
377
+ "data-columns-medium": mediumColumns,
378
+ "data-columns-large": largeColumns,
379
+ "data-density": slots?.layout?.density,
380
+ children: category.itemIds.map((id, index) => {
381
+ const item = catalogue.items[id];
382
+ if (!item) {
383
+ return null;
384
+ }
385
+ const lines = client.cart.lines.filter((line) => line.itemId === id);
386
+ const href = itemHref(item.id, menuId);
387
+ return /* @__PURE__ */ jsxs("div", { children: [
388
+ Tile ? /* @__PURE__ */ jsx(
389
+ Tile,
390
+ {
391
+ item,
392
+ ...menuId !== void 0 ? { menuId } : {},
393
+ href,
394
+ quantityInCart: lines.reduce(
395
+ (sum, line) => sum + line.quantity,
396
+ 0
397
+ ),
398
+ onAdd: () => {
399
+ try {
400
+ client.cart.add(item.id, {
401
+ ...menuId !== void 0 ? { menuId } : {}
402
+ });
403
+ countEvent("add");
404
+ } catch {
405
+ onOpen(item);
406
+ }
407
+ },
408
+ onOpen: () => onOpen(item)
409
+ }
410
+ ) : /* @__PURE__ */ jsx(
411
+ BaseItemTile,
412
+ {
413
+ item,
414
+ lines,
415
+ href,
416
+ layout,
417
+ priority: categoryIndex === 0 && index < 4,
418
+ client,
419
+ onOpen: () => onOpen(item)
420
+ }
421
+ ),
422
+ !cartOpen && lines.map((line) => /* @__PURE__ */ jsx(CartLine, { line }, line.id))
423
+ ] }, id);
424
+ })
425
+ }
426
+ ),
427
+ hasMore && /* @__PURE__ */ jsxs("button", { type: "button", disabled: busy, onClick: onLoadMore, children: [
428
+ "Load more ",
429
+ category.name
430
+ ] })
431
+ ]
432
+ }
433
+ );
434
+ }
435
+ var sanitize = createSanitizeRichText(parseRichText);
436
+ function InfoBanners({ value }) {
437
+ const [banners, setBanners] = useState([]);
438
+ const [index, setIndex] = useState(0);
439
+ const [expanded, setExpanded] = useState({});
440
+ const [paused, setPaused] = useState(false);
441
+ const viewport = useRef(null);
442
+ const interacted = useRef(false);
443
+ useEffect(() => {
444
+ setBanners(
445
+ splitInfoBanner(value).map(sanitize).filter((banner) => banner.replace(/<[^>]*>/g, "").trim())
446
+ );
447
+ setIndex(0);
448
+ setExpanded({});
449
+ interacted.current = false;
450
+ }, [value]);
451
+ function select(next, interaction = true) {
452
+ interacted.current || (interacted.current = interaction);
453
+ setExpanded({});
454
+ viewport.current?.children[next]?.scrollIntoView({
455
+ block: "nearest",
456
+ inline: "start",
457
+ behavior: "instant"
458
+ });
459
+ setIndex(next);
460
+ }
461
+ const isExpanded = Object.values(expanded).some(Boolean);
462
+ useEffect(() => {
463
+ if (paused || isExpanded || banners.length < 2) {
464
+ return;
465
+ }
466
+ const interval = setInterval(() => {
467
+ const node = viewport.current;
468
+ if (!node || interacted.current || window.matchMedia("(prefers-reduced-motion: reduce)").matches || node.scrollWidth <= node.clientWidth) {
469
+ return;
470
+ }
471
+ select((index + 1) % banners.length, false);
472
+ }, 5e3);
473
+ return () => clearInterval(interval);
474
+ }, [index, paused, isExpanded, banners.length]);
475
+ if (!banners.length) {
476
+ return null;
477
+ }
478
+ return /* @__PURE__ */ jsxs(
479
+ "section",
480
+ {
481
+ "aria-label": "Menu information",
482
+ onMouseEnter: () => setPaused(true),
483
+ onMouseLeave: () => setPaused(false),
484
+ onFocus: () => setPaused(true),
485
+ onBlur: () => setPaused(false),
486
+ children: [
487
+ /* @__PURE__ */ jsx(
488
+ "div",
489
+ {
490
+ className: "z-base-info-banners",
491
+ ref: viewport,
492
+ onPointerDown: () => {
493
+ interacted.current = true;
494
+ },
495
+ onScroll: () => {
496
+ const node = viewport.current;
497
+ if (node?.children[0]) {
498
+ setIndex(
499
+ Math.round(
500
+ node.scrollLeft / node.children[0].getBoundingClientRect().width
501
+ )
502
+ );
503
+ }
504
+ },
505
+ children: banners.map((banner, position) => /* @__PURE__ */ jsxs("article", { children: [
506
+ /* @__PURE__ */ jsx(
507
+ "div",
508
+ {
509
+ className: "z-base-info-copy",
510
+ "data-expanded": Boolean(expanded[position]),
511
+ dangerouslySetInnerHTML: { __html: banner }
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsxs(
515
+ "button",
516
+ {
517
+ type: "button",
518
+ "aria-expanded": Boolean(expanded[position]),
519
+ onClick: () => setExpanded((current) => ({
520
+ ...current,
521
+ [position]: !current[position]
522
+ })),
523
+ children: [
524
+ "Show ",
525
+ expanded[position] ? "less" : "more"
526
+ ]
527
+ }
528
+ )
529
+ ] }, banner + position))
530
+ }
531
+ ),
532
+ banners.length > 1 && /* @__PURE__ */ jsxs("div", { children: [
533
+ /* @__PURE__ */ jsx(
534
+ "button",
535
+ {
536
+ type: "button",
537
+ "aria-label": "Previous information",
538
+ disabled: index === 0,
539
+ onClick: () => select(index - 1),
540
+ children: "Previous"
541
+ }
542
+ ),
543
+ /* @__PURE__ */ jsx(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ "aria-label": "Next information",
548
+ disabled: index >= banners.length - 1,
549
+ onClick: () => select(index + 1),
550
+ children: "Next"
551
+ }
552
+ ),
553
+ banners.map((_, position) => /* @__PURE__ */ jsx(
554
+ "button",
555
+ {
556
+ type: "button",
557
+ "aria-label": `Go to banner ${position + 1}`,
558
+ "aria-current": index === position ? "true" : void 0,
559
+ onClick: () => select(position),
560
+ children: position + 1
561
+ },
562
+ position
563
+ ))
564
+ ] })
565
+ ]
566
+ }
567
+ );
568
+ }
569
+
570
+ // src/catalogue/catalogue-pages.ts
571
+ var pageKey = (menuId, categoryId) => `${menuId}:${categoryId}`;
572
+ function pageState(nextCursor) {
573
+ return { ...nextCursor ? { next: nextCursor } : {}, done: !nextCursor };
574
+ }
575
+ function mergePage(menu, page) {
576
+ return {
577
+ ...menu,
578
+ items: { ...menu.items, ...page.menu.items },
579
+ categories: menu.categories.map((category) => {
580
+ const incoming = page.menu.categories.find(
581
+ (value) => value.id === category.id
582
+ );
583
+ return incoming ? {
584
+ ...category,
585
+ itemIds: [.../* @__PURE__ */ new Set([...category.itemIds, ...incoming.itemIds])]
586
+ } : category;
587
+ }),
588
+ completeness: "partial"
589
+ };
590
+ }
591
+ function readSavedPages(client, menuId, branchId, saved, signal) {
592
+ return Promise.all(
593
+ saved.map(async (entry) => ({
594
+ ...entry,
595
+ page: await client.menu.page(menuId, entry.categoryId, {
596
+ branch: branchId,
597
+ signal,
598
+ ...entry.cursor ? { cursor: entry.cursor } : {}
599
+ })
600
+ }))
601
+ );
602
+ }
603
+ function replaceRefreshedCategories(menu, refreshed) {
604
+ const fresh = /* @__PURE__ */ new Map();
605
+ let next = menu;
606
+ for (const { categoryId, page } of refreshed) {
607
+ const ids = page.menu.categories.find((c) => c.id === categoryId)?.itemIds;
608
+ fresh.set(categoryId, [...fresh.get(categoryId) ?? [], ...ids ?? []]);
609
+ next = { ...next, items: { ...next.items, ...page.menu.items } };
610
+ }
611
+ return {
612
+ ...next,
613
+ categories: next.categories.map((category) => {
614
+ const ids = fresh.get(category.id);
615
+ return ids ? { ...category, itemIds: ids } : category;
616
+ })
617
+ };
618
+ }
619
+ function replaceSavedPages(saved, refreshed) {
620
+ for (const page of refreshed) {
621
+ const index = saved.findIndex(
622
+ (entry) => entry.categoryId === page.categoryId && entry.cursor === page.cursor
623
+ );
624
+ if (index >= 0) {
625
+ saved[index] = page;
626
+ }
627
+ }
628
+ }
629
+
630
+ // src/catalogue/saved-catalogue-sync.ts
631
+ async function loadResources(client, scope) {
632
+ const [persistence, selected] = await Promise.all([
633
+ import('@zappie/sdk/persist/catalogue'),
634
+ import('@zappie/sdk/transport/selected-menu')
635
+ ]);
636
+ return {
637
+ saved: persistence.createSavedCatalogue(client, scope),
638
+ selected: selected.createSelectedMenuReader(client)
639
+ };
640
+ }
641
+ function createSavedCatalogueSync({
642
+ client,
643
+ state,
644
+ scope,
645
+ revision,
646
+ publish
647
+ }) {
648
+ let resources;
649
+ function snapshot() {
650
+ return {
651
+ scope,
652
+ menu: state.menu,
653
+ coverage: { menuIds: [scope.menuId], complete: state.complete },
654
+ pages: [...state.savedPages],
655
+ etag: state.menu.version,
656
+ savedAt: Date.now(),
657
+ ...revision ? { revision } : {}
658
+ };
659
+ }
660
+ async function save() {
661
+ if (await resources.saved.publish(snapshot()).catch(() => true)) {
662
+ state.abort.signal.throwIfAborted();
663
+ publish?.(state.menu);
664
+ }
665
+ }
666
+ async function refresh() {
667
+ if (state.complete) {
668
+ const response = await resources.selected.read(scope, {
669
+ signal: state.abort.signal
670
+ });
671
+ state.abort.signal.throwIfAborted();
672
+ if ("notModified" in response) {
673
+ return;
674
+ }
675
+ state.menu = response.menu;
676
+ await save();
677
+ return;
678
+ }
679
+ const refreshed = await readSavedPages(
680
+ client,
681
+ scope.menuId,
682
+ scope.branchId,
683
+ state.savedPages,
684
+ state.abort.signal
685
+ );
686
+ state.abort.signal.throwIfAborted();
687
+ state.menu = replaceRefreshedCategories(state.menu, refreshed);
688
+ for (const { categoryId, page } of refreshed) {
689
+ state.pages.set(
690
+ pageKey(scope.menuId, categoryId),
691
+ pageState(page.nextCursor)
692
+ );
693
+ }
694
+ replaceSavedPages(state.savedPages, refreshed);
695
+ await save();
696
+ }
697
+ return {
698
+ get resources() {
699
+ return resources;
700
+ },
701
+ /** Shows the current menu now; saving runs behind it and never fails a read. */
702
+ emit() {
703
+ state.abort.signal.throwIfAborted();
704
+ publish?.(state.menu);
705
+ void resources?.saved.publish(snapshot()).catch(() => {
706
+ });
707
+ },
708
+ async restore() {
709
+ resources = await loadResources(client, scope);
710
+ const value = await resources.saved.restore();
711
+ state.abort.signal.throwIfAborted();
712
+ if (!value) {
713
+ return false;
714
+ }
715
+ state.menu = value.menu;
716
+ state.complete = value.coverage.complete;
717
+ state.mode = state.complete ? "eager" : "progressive";
718
+ state.savedPages.push(...value.pages);
719
+ for (const saved of value.pages) {
720
+ state.pages.set(
721
+ pageKey(scope.menuId, saved.categoryId),
722
+ pageState(saved.page.nextCursor)
723
+ );
724
+ }
725
+ publish?.(state.menu);
726
+ if (!revision || value.revision !== revision) {
727
+ void refresh().catch(() => {
728
+ });
729
+ }
730
+ return true;
731
+ }
732
+ };
733
+ }
734
+
735
+ // src/catalogue/loader.ts
736
+ function createCatalogueLoader(client, metadata, resolve, publish, orderContext = "delivery", revision) {
737
+ const state = {
738
+ menu: metadata,
739
+ mode: resolve(metadata),
740
+ complete: false,
741
+ abort: new AbortController(),
742
+ pages: /* @__PURE__ */ new Map(),
743
+ savedPages: []
744
+ };
745
+ const scope = {
746
+ store: client.config.store,
747
+ branchId: metadata.branchId,
748
+ orderContext,
749
+ menuId: metadata.menus?.[0]?.id ?? metadata.id
750
+ };
751
+ const saved = createSavedCatalogueSync({
752
+ client,
753
+ state,
754
+ scope,
755
+ revision,
756
+ publish
757
+ });
758
+ const pending = /* @__PURE__ */ new Map();
759
+ let eager;
760
+ let speculative;
761
+ function readEager(signal = state.abort.signal) {
762
+ eager ?? (eager = saved.resources.selected.read(scope, { signal }).then(async (response) => {
763
+ signal.throwIfAborted();
764
+ if ("notModified" in response) {
765
+ throw new Error("Unexpected conditional selected-menu response");
766
+ }
767
+ state.complete = true;
768
+ state.menu = response.menu;
769
+ saved.emit();
770
+ return state.menu;
771
+ }).catch((failure) => {
772
+ eager = void 0;
773
+ throw failure;
774
+ }));
775
+ return eager;
776
+ }
777
+ function speculate() {
778
+ if (eager) {
779
+ return;
780
+ }
781
+ speculative = new AbortController();
782
+ const cancel = () => speculative?.abort();
783
+ state.abort.signal.addEventListener("abort", cancel, { once: true });
784
+ void readEager(speculative.signal).catch(() => {
785
+ });
786
+ }
787
+ async function readPage(menuId, categoryId, key, cursor) {
788
+ const page = await client.menu.page(menuId, categoryId, {
789
+ branch: metadata.branchId,
790
+ signal: state.abort.signal,
791
+ ...cursor ? { cursor } : {}
792
+ });
793
+ state.abort.signal.throwIfAborted();
794
+ if (!state.complete) {
795
+ state.menu = mergePage(state.menu, page);
796
+ }
797
+ state.savedPages.push({ categoryId, ...cursor ? { cursor } : {}, page });
798
+ saved.emit();
799
+ state.pages.set(key, pageState(page.nextCursor));
800
+ if (state.mode === null) {
801
+ state.mode = resolve(metadata, page.menu) ?? "progressive";
802
+ if (state.mode !== "eager") {
803
+ speculative?.abort();
804
+ eager = void 0;
805
+ }
806
+ }
807
+ return state.menu;
808
+ }
809
+ const initialized = saved.restore().catch(() => false);
810
+ return {
811
+ get value() {
812
+ return state.menu;
813
+ },
814
+ get mode() {
815
+ return state.mode;
816
+ },
817
+ hasMore(menuId, categoryId) {
818
+ return state.mode !== "eager" && !state.pages.get(pageKey(menuId, categoryId))?.done;
819
+ },
820
+ /** True when reaching this category would neither read nor change. */
821
+ isLoaded(menuId, categoryId) {
822
+ return state.complete || state.mode !== "eager" && state.pages.has(pageKey(menuId, categoryId));
823
+ },
824
+ async reach(menuId, categoryId, more = false) {
825
+ const key = pageKey(menuId, categoryId);
826
+ const warm = await initialized;
827
+ if (state.complete || warm && state.pages.has(key) && !more) {
828
+ return state.menu;
829
+ }
830
+ if (state.mode === "eager") {
831
+ return readEager();
832
+ }
833
+ const existing = pending.get(key);
834
+ if (existing) {
835
+ return existing;
836
+ }
837
+ const loaded = state.pages.get(key);
838
+ if (loaded && (!more || loaded.done)) {
839
+ return state.menu;
840
+ }
841
+ if (state.mode === null) {
842
+ speculate();
843
+ }
844
+ const request = readPage(menuId, categoryId, key, loaded?.next);
845
+ pending.set(key, request);
846
+ try {
847
+ return await request;
848
+ } finally {
849
+ pending.delete(key);
850
+ }
851
+ },
852
+ /** Aborts in-flight reads; `resume()` makes the loader usable again. */
853
+ dispose() {
854
+ state.abort.abort();
855
+ },
856
+ resume() {
857
+ if (state.abort.signal.aborted) {
858
+ state.abort = new AbortController();
859
+ }
860
+ }
861
+ };
862
+ }
863
+
864
+ // src/catalogue/use-catalogue-data.ts
865
+ function useCatalogueData({
866
+ client,
867
+ initialMenu,
868
+ selectedMenuId,
869
+ fulfilmentMode,
870
+ resolveStrategy,
871
+ revision
872
+ }) {
873
+ const [catalogue, setCatalogue] = useState(initialMenu);
874
+ const show = useCallback(
875
+ (value) => setCatalogue({
876
+ ...value,
877
+ ...initialMenu.menus ? { menus: initialMenu.menus } : {},
878
+ categories: initialMenu.categories.map(
879
+ (category) => value.categories.find((loaded) => loaded.id === category.id) ?? category
880
+ )
881
+ }),
882
+ [initialMenu]
883
+ );
884
+ const loader = useMemo(() => {
885
+ const selected = initialMenu.menus?.find(
886
+ (source2) => source2.id === selectedMenuId
887
+ );
888
+ const scoped = {
889
+ ...initialMenu,
890
+ ...selected ? { menus: [selected] } : {},
891
+ categories: initialMenu.categories.filter(
892
+ (category) => !selected || selected.categoryIds.includes(category.id)
893
+ )
894
+ };
895
+ return createCatalogueLoader(
896
+ client,
897
+ scoped,
898
+ resolveStrategy,
899
+ show,
900
+ fulfilmentMode,
901
+ revision
902
+ );
903
+ }, [
904
+ client,
905
+ initialMenu,
906
+ selectedMenuId,
907
+ fulfilmentMode,
908
+ resolveStrategy,
909
+ revision,
910
+ show
911
+ ]);
912
+ useEffect(() => {
913
+ loader.resume();
914
+ return () => loader.dispose();
915
+ }, [loader]);
916
+ const source = catalogue.menus?.find((value) => value.id === selectedMenuId) ?? catalogue.menus?.[0];
917
+ const categories = useMemo(
918
+ () => catalogue.categories.filter(
919
+ (category) => source?.categoryIds.includes(category.id)
920
+ ),
921
+ [catalogue.categories, source]
922
+ );
923
+ const [error, setError] = useState();
924
+ const [busy, setBusy] = useState(false);
925
+ const reach = useCallback(
926
+ async (categoryId, more = false) => {
927
+ if (!source || !more && loader.isLoaded(source.id, categoryId)) {
928
+ return;
929
+ }
930
+ setBusy(true);
931
+ setError(void 0);
932
+ try {
933
+ show(await loader.reach(source.id, categoryId, more));
934
+ } catch (failure) {
935
+ if (!(failure instanceof Error) || failure.name !== "AbortError") {
936
+ setError("Catalogue could not load. Try again.");
937
+ }
938
+ } finally {
939
+ setBusy(false);
940
+ }
941
+ },
942
+ [loader, source, show]
943
+ );
944
+ return { catalogue, source, categories, loader, error, busy, reach };
945
+ }
946
+ function useCategoryScroll({
947
+ categories,
948
+ selectedCategoryId,
949
+ itemOpen,
950
+ menuId,
951
+ menuLayout,
952
+ reach,
953
+ onSelectCategory
954
+ }) {
955
+ const [active, setActive] = useState(selectedCategoryId ?? categories[0]?.id);
956
+ const activeRef = useRef(active);
957
+ activeRef.current = active;
958
+ const sections = useRef(/* @__PURE__ */ new Map());
959
+ const wanted = useRef(void 0);
960
+ const reading = useRef(false);
961
+ const reachLatest = useCallback(
962
+ (categoryId) => {
963
+ wanted.current = categoryId;
964
+ if (reading.current) {
965
+ return;
966
+ }
967
+ reading.current = true;
968
+ void (async () => {
969
+ try {
970
+ while (wanted.current) {
971
+ const next = wanted.current;
972
+ wanted.current = void 0;
973
+ await reach(next);
974
+ }
975
+ } finally {
976
+ reading.current = false;
977
+ }
978
+ })();
979
+ },
980
+ [reach]
981
+ );
982
+ useEffect(() => {
983
+ if (itemOpen) {
984
+ return;
985
+ }
986
+ const id = selectedCategoryId ?? categories[0]?.id;
987
+ if (id) {
988
+ setActive(id);
989
+ void reach(id);
990
+ }
991
+ }, [menuId, selectedCategoryId, itemOpen, reach, categories]);
992
+ useEffect(() => {
993
+ if (itemOpen) {
994
+ return;
995
+ }
996
+ let frame = 0;
997
+ const select = () => {
998
+ frame = 0;
999
+ let id = categories[0]?.id;
1000
+ for (const category of categories) {
1001
+ const top = sections.current.get(category.id)?.getBoundingClientRect().top;
1002
+ if (top === void 0) {
1003
+ continue;
1004
+ }
1005
+ if (top > 200) {
1006
+ break;
1007
+ }
1008
+ id = category.id;
1009
+ }
1010
+ if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) {
1011
+ id = categories.at(-1)?.id ?? id;
1012
+ }
1013
+ if (id && id !== activeRef.current) {
1014
+ setActive(id);
1015
+ reachLatest(id);
1016
+ }
1017
+ };
1018
+ const onScroll = () => {
1019
+ if (!frame) {
1020
+ frame = requestAnimationFrame(select);
1021
+ }
1022
+ };
1023
+ window.addEventListener("scroll", onScroll, { passive: true });
1024
+ return () => {
1025
+ window.removeEventListener("scroll", onScroll);
1026
+ cancelAnimationFrame(frame);
1027
+ };
1028
+ }, [categories, itemOpen, reachLatest]);
1029
+ const selectCategory = useCallback(
1030
+ (id) => {
1031
+ setActive(id);
1032
+ onSelectCategory(id);
1033
+ sections.current.get(id)?.scrollIntoView({ block: "start" });
1034
+ void reach(id);
1035
+ },
1036
+ [onSelectCategory, reach]
1037
+ );
1038
+ const previousLayout = useRef(menuLayout);
1039
+ useEffect(() => {
1040
+ if (previousLayout.current !== menuLayout && active) {
1041
+ sections.current.get(active)?.scrollIntoView({ block: "start" });
1042
+ }
1043
+ previousLayout.current = menuLayout;
1044
+ }, [menuLayout, active]);
1045
+ const focusCategory = useCallback((id) => {
1046
+ sections.current.get(id)?.querySelector("h3")?.focus();
1047
+ }, []);
1048
+ return {
1049
+ active,
1050
+ sections,
1051
+ selectCategory,
1052
+ focusCategory
1053
+ };
1054
+ }
1055
+ function useItemOpener({
1056
+ navigation,
1057
+ source,
1058
+ clearSearch
1059
+ }) {
1060
+ const searchOverlay = useOverlay("catalogue_search", "search");
1061
+ const [pendingItem, setPendingItem] = useState();
1062
+ const open = useCallback(
1063
+ async (item) => {
1064
+ if (searchOverlay.isOpen) {
1065
+ setPendingItem(item);
1066
+ searchOverlay.close();
1067
+ return;
1068
+ }
1069
+ const placement = item.placements?.find((value) => value.menuId === source?.id) ?? item.placements?.[0];
1070
+ const menuId = placement?.menuId ?? source?.id;
1071
+ if (menuId) {
1072
+ clearSearch();
1073
+ await navigation.openItem({
1074
+ itemId: item.id,
1075
+ item,
1076
+ menuId,
1077
+ ...placement ? { categoryId: placement.categoryId } : {}
1078
+ });
1079
+ }
1080
+ },
1081
+ [clearSearch, navigation, searchOverlay, source]
1082
+ );
1083
+ useEffect(() => {
1084
+ if (pendingItem && !searchOverlay.isOpen) {
1085
+ const frame = requestAnimationFrame(() => {
1086
+ setPendingItem(void 0);
1087
+ void open(pendingItem);
1088
+ });
1089
+ return () => cancelAnimationFrame(frame);
1090
+ }
1091
+ }, [pendingItem, searchOverlay.isOpen, open]);
1092
+ return open;
1093
+ }
1094
+ function useSearchSource(menu, enabled) {
1095
+ const client = useZappie();
1096
+ const [query, setQuery] = useState("");
1097
+ const [composing, setComposing] = useState(false);
1098
+ const [attempt, setAttempt] = useState(0);
1099
+ const [cursor, setCursor] = useState();
1100
+ const [remote, setRemote] = useState({ query: "" });
1101
+ const index = useMemo(() => createSearchIndex(menu), [menu]);
1102
+ const local = useMemo(() => index(query), [index, query]);
1103
+ const hasLocalItems = local.some((result) => result.type === "item");
1104
+ useEffect(() => {
1105
+ if (!enabled || composing || !query.trim() || hasLocalItems) {
1106
+ return;
1107
+ }
1108
+ const abort = new AbortController();
1109
+ const timer = setTimeout(() => {
1110
+ setRemote((previous) => ({
1111
+ ...cursor && previous.query === query ? previous : {},
1112
+ query,
1113
+ loading: true
1114
+ }));
1115
+ void client.menu.search(query, {
1116
+ branch: menu.branchId,
1117
+ signal: abort.signal,
1118
+ ...cursor ? { cursor } : {}
1119
+ }).then(
1120
+ (page) => {
1121
+ if (!abort.signal.aborted) {
1122
+ setRemote((previous) => {
1123
+ if (!cursor || previous.query !== query || !previous.page) {
1124
+ return { query, page };
1125
+ }
1126
+ return {
1127
+ query,
1128
+ page: {
1129
+ ...page,
1130
+ menu: {
1131
+ ...page.menu,
1132
+ items: {
1133
+ ...previous.page.menu.items,
1134
+ ...page.menu.items
1135
+ },
1136
+ categories: [
1137
+ ...new Map(
1138
+ [
1139
+ ...previous.page.menu.categories,
1140
+ ...page.menu.categories
1141
+ ].map((category) => [category.id, category])
1142
+ ).values()
1143
+ ]
1144
+ }
1145
+ }
1146
+ };
1147
+ });
1148
+ }
1149
+ },
1150
+ () => {
1151
+ if (!abort.signal.aborted) {
1152
+ setRemote((previous) => ({
1153
+ ...cursor && previous.query === query ? previous : {},
1154
+ query,
1155
+ loading: false,
1156
+ error: "Search could not load. Try again."
1157
+ }));
1158
+ }
1159
+ }
1160
+ );
1161
+ }, 500);
1162
+ return () => {
1163
+ clearTimeout(timer);
1164
+ abort.abort();
1165
+ };
1166
+ }, [
1167
+ client,
1168
+ menu.branchId,
1169
+ query,
1170
+ enabled,
1171
+ composing,
1172
+ hasLocalItems,
1173
+ attempt,
1174
+ cursor
1175
+ ]);
1176
+ const current = !composing && remote.query === query && !hasLocalItems ? remote : void 0;
1177
+ const results = useMemo(() => {
1178
+ const values = [...local];
1179
+ for (const item of Object.values(current?.page?.menu.items ?? {})) {
1180
+ if (!values.some(
1181
+ (result) => result.type === "item" && result.item.id === item.id
1182
+ )) {
1183
+ values.push({ type: "item", item, score: 0 });
1184
+ }
1185
+ }
1186
+ for (const category of current?.page?.menu.categories ?? []) {
1187
+ if (category.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) && !values.some(
1188
+ (result) => result.type === "category" && result.category.id === category.id
1189
+ )) {
1190
+ values.push({ type: "category", category, score: 0 });
1191
+ }
1192
+ }
1193
+ return values;
1194
+ }, [local, current?.page, query]);
1195
+ return {
1196
+ query,
1197
+ setQuery: (value) => {
1198
+ setCursor(void 0);
1199
+ setQuery(value);
1200
+ },
1201
+ hasMore: Boolean(current?.page?.nextCursor),
1202
+ loadMore: () => {
1203
+ if (!current?.loading && current?.page?.nextCursor) {
1204
+ setCursor(current.page.nextCursor);
1205
+ }
1206
+ },
1207
+ source: {
1208
+ results,
1209
+ isLoading: current?.loading ?? false,
1210
+ ...current?.error ? { error: current.error } : {},
1211
+ retry: () => setAttempt((value) => value + 1),
1212
+ onCompositionChange: setComposing
1213
+ }
1214
+ };
1215
+ }
1216
+ function Catalogue({
1217
+ initialMenu,
1218
+ resolveStrategy,
1219
+ themeClass,
1220
+ orderAction = "View",
1221
+ menuNotice,
1222
+ revision,
1223
+ slots
1224
+ }) {
1225
+ const client = useZappie();
1226
+ const fulfilmentMode = useBaseHostBridge().fulfilmentMode === "pickup" ? "pickup" : "delivery";
1227
+ const navigation = useCatalogueNavigation();
1228
+ const store = useStore();
1229
+ const cart = useCart();
1230
+ const cartOverlay = useOverlay("modal_id", "review-menu");
1231
+ useCartCatalogue();
1232
+ const selectedMenuId = navigation.location.menuId ?? initialMenu.menus?.[0]?.id ?? initialMenu.id;
1233
+ const data = useCatalogueData({
1234
+ client,
1235
+ initialMenu,
1236
+ selectedMenuId,
1237
+ fulfilmentMode,
1238
+ resolveStrategy,
1239
+ ...revision !== void 0 ? { revision } : {}
1240
+ });
1241
+ const layout = navigation.location.menuLayout ?? "GRID";
1242
+ const [searchOpen, setSearchOpen] = useState(false);
1243
+ const search = useSearchSource(data.catalogue, searchOpen);
1244
+ const clearSearch = useCallback(() => {
1245
+ setSearchOpen(false);
1246
+ search.setQuery("");
1247
+ }, [search.setQuery]);
1248
+ const openItem = useItemOpener({
1249
+ navigation,
1250
+ ...data.source !== void 0 ? { source: data.source } : {},
1251
+ clearSearch
1252
+ });
1253
+ const categories = useCategoryScroll({
1254
+ categories: data.categories,
1255
+ ...navigation.location.categoryId !== void 0 ? { selectedCategoryId: navigation.location.categoryId } : {},
1256
+ itemOpen: Boolean(navigation.location.item),
1257
+ ...data.source?.id !== void 0 ? { menuId: data.source.id } : {},
1258
+ ...navigation.location.menuLayout !== void 0 ? { menuLayout: navigation.location.menuLayout } : {},
1259
+ reach: data.reach,
1260
+ onSelectCategory: (categoryId) => navigation.selectCategory(categoryId)
1261
+ });
1262
+ const branch = store.data?.branches.find(
1263
+ (value) => value.id === data.catalogue.branchId
1264
+ );
1265
+ const BeforeList = slots?.BeforeList;
1266
+ const AfterList = slots?.AfterList;
1267
+ return /* @__PURE__ */ jsx(ThemeClassProvider, { value: themeClass, children: /* @__PURE__ */ jsxs(
1268
+ "section",
1269
+ {
1270
+ className: ["z-base-catalogue", themeClass].filter(Boolean).join(" "),
1271
+ "data-zappie-theme": "base",
1272
+ "aria-label": "Catalogue",
1273
+ children: [
1274
+ /* @__PURE__ */ jsx(
1275
+ CatalogueHeader,
1276
+ {
1277
+ ...branch !== void 0 ? { branch } : {},
1278
+ ...store.data?.name !== void 0 ? { storeName: store.data.name } : {},
1279
+ ...data.catalogue.menus !== void 0 ? { menus: data.catalogue.menus } : {},
1280
+ ...data.source?.id !== void 0 ? { selectedMenuId: data.source.id } : {},
1281
+ cartLines: cart.lines,
1282
+ cartOpen: cartOverlay.isOpen,
1283
+ fulfilmentMode,
1284
+ layout,
1285
+ ...slots !== void 0 ? { slots } : {},
1286
+ onOpenCart: () => void navigation.openCart(),
1287
+ onOpenBranchDetails: () => void navigation.openBranchDetails?.(),
1288
+ onSelectMenu: (menuId) => navigation.selectMenu(menuId),
1289
+ onOpenBranchFinder: () => void navigation.openBranchFinder(),
1290
+ onSwitchFulfilment: () => {
1291
+ void navigation.switchFulfilment(
1292
+ fulfilmentMode === "pickup" ? "delivery" : "pickup"
1293
+ );
1294
+ },
1295
+ onSetLayout: (value) => navigation.setLayout(value)
1296
+ }
1297
+ ),
1298
+ menuNotice && /* @__PURE__ */ jsx("p", { role: "status", children: menuNotice }),
1299
+ /* @__PURE__ */ jsx(
1300
+ InfoBanners,
1301
+ {
1302
+ ...data.source?.infoBanner ? { value: data.source.infoBanner } : {}
1303
+ }
1304
+ ),
1305
+ BeforeList && /* @__PURE__ */ jsx(BeforeList, { menu: data.catalogue }),
1306
+ /* @__PURE__ */ jsx(
1307
+ CatalogueSearch,
1308
+ {
1309
+ search,
1310
+ open: searchOpen,
1311
+ ...themeClass !== void 0 ? { themeClass } : {},
1312
+ onOpen: () => setSearchOpen(true),
1313
+ onSelectItem: (item) => void openItem(item),
1314
+ onSelectCategory: categories.selectCategory
1315
+ }
1316
+ ),
1317
+ /* @__PURE__ */ jsx(
1318
+ CategoryNavigation,
1319
+ {
1320
+ categories: data.categories,
1321
+ ...categories.active !== void 0 ? { active: categories.active } : {},
1322
+ ...themeClass !== void 0 ? { themeClass } : {},
1323
+ ...slots !== void 0 ? { slots } : {},
1324
+ onSelect: categories.selectCategory,
1325
+ onFocusCategory: categories.focusCategory
1326
+ }
1327
+ ),
1328
+ data.error && /* @__PURE__ */ jsxs("p", { role: "alert", children: [
1329
+ data.error,
1330
+ /* @__PURE__ */ jsx(
1331
+ "button",
1332
+ {
1333
+ onClick: () => {
1334
+ if (categories.active) {
1335
+ void data.reach(categories.active);
1336
+ }
1337
+ },
1338
+ children: "Retry catalogue"
1339
+ }
1340
+ )
1341
+ ] }),
1342
+ !data.categories.length && /* @__PURE__ */ jsx("p", { children: "No items available" }),
1343
+ data.categories.map((category, index) => /* @__PURE__ */ jsx(
1344
+ CategorySection,
1345
+ {
1346
+ category,
1347
+ categoryIndex: index,
1348
+ catalogue: data.catalogue,
1349
+ ...data.source?.id !== void 0 ? { menuId: data.source.id } : {},
1350
+ layout,
1351
+ ...slots !== void 0 ? { slots } : {},
1352
+ sections: categories.sections,
1353
+ cartOpen: cartOverlay.isOpen,
1354
+ client,
1355
+ busy: data.busy,
1356
+ hasMore: Boolean(
1357
+ data.source && data.loader.hasMore(data.source.id, category.id)
1358
+ ),
1359
+ itemHref: (itemId, menuId) => navigation.hrefFor({
1360
+ type: "item",
1361
+ itemId,
1362
+ ...menuId !== void 0 ? { menuId } : {},
1363
+ categoryId: category.id
1364
+ }),
1365
+ onOpen: (item) => void openItem(item),
1366
+ onLoadMore: () => void data.reach(category.id, true)
1367
+ },
1368
+ category.id
1369
+ )),
1370
+ AfterList && /* @__PURE__ */ jsx(AfterList, { menu: data.catalogue }),
1371
+ data.busy && /* @__PURE__ */ jsx("p", { role: "status", children: "Loading products\u2026" }),
1372
+ /* @__PURE__ */ jsx(
1373
+ CatalogueCart,
1374
+ {
1375
+ action: orderAction,
1376
+ count: cart.lines.reduce((sum, line) => sum + line.quantity, 0),
1377
+ ...cart.subtotal ? { subtotal: cart.subtotal } : {},
1378
+ client,
1379
+ onOpen: () => void navigation.openCart()
1380
+ }
1381
+ )
1382
+ ]
1383
+ }
1384
+ ) });
1385
+ }
1386
+
1387
+ export { Catalogue };
1388
+ //# sourceMappingURL=catalogue.js.map
1389
+ //# sourceMappingURL=catalogue.js.map