@zappie/theme-base 0.1.0 → 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 (129) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +26 -53
  3. package/dist/address.js +99 -2
  4. package/dist/address.js.map +7 -1
  5. package/dist/cart/address-sheet.js +143 -0
  6. package/dist/cart/address-sheet.js.map +7 -0
  7. package/dist/cart/cart-issues.js +72 -0
  8. package/dist/cart/cart-issues.js.map +7 -0
  9. package/dist/cart/cart-line-groups.js +34 -0
  10. package/dist/cart/cart-line-groups.js.map +7 -0
  11. package/dist/{chunk-FB5DRCU7.js → cart/cart-line.js} +9 -70
  12. package/dist/cart/cart-line.js.map +7 -0
  13. package/dist/cart/cart-lines.js +27 -0
  14. package/dist/cart/cart-lines.js.map +7 -0
  15. package/dist/cart/cart-schedule.js +24 -0
  16. package/dist/cart/cart-schedule.js.map +7 -0
  17. package/dist/cart/cart-sheet.js +35 -0
  18. package/dist/cart/cart-sheet.js.map +7 -0
  19. package/dist/cart/checkout-cta.js +81 -0
  20. package/dist/cart/checkout-cta.js.map +7 -0
  21. package/dist/cart/checkout-state.js +74 -0
  22. package/dist/cart/checkout-state.js.map +7 -0
  23. package/dist/cart/clear-cart.js +44 -0
  24. package/dist/cart/clear-cart.js.map +7 -0
  25. package/dist/cart/customer-details.js +70 -0
  26. package/dist/cart/customer-details.js.map +7 -0
  27. package/dist/cart/delivery-distance.js +22 -0
  28. package/dist/cart/delivery-distance.js.map +7 -0
  29. package/dist/cart/empty-cart.js +14 -0
  30. package/dist/cart/empty-cart.js.map +7 -0
  31. package/dist/cart/fulfilment-panel.js +83 -0
  32. package/dist/cart/fulfilment-panel.js.map +7 -0
  33. package/dist/cart/line-editor.js +35 -0
  34. package/dist/cart/line-editor.js.map +7 -0
  35. package/dist/cart/line-options.js +96 -0
  36. package/dist/cart/line-options.js.map +7 -0
  37. package/dist/cart/loading.js +17 -0
  38. package/dist/cart/loading.js.map +7 -0
  39. package/dist/cart/promo-sheet.js +103 -0
  40. package/dist/cart/promo-sheet.js.map +7 -0
  41. package/dist/cart/region-picker.js +40 -0
  42. package/dist/cart/region-picker.js.map +7 -0
  43. package/dist/cart/rewards-sheet.js +86 -0
  44. package/dist/cart/rewards-sheet.js.map +7 -0
  45. package/dist/cart/schedule-sheet.js +87 -0
  46. package/dist/cart/schedule-sheet.js.map +7 -0
  47. package/dist/{chunk-JFEI2OUZ.js → cart/theme-class.js} +7 -5
  48. package/dist/cart/theme-class.js.map +7 -0
  49. package/dist/cart/totals.js +81 -0
  50. package/dist/cart/totals.js.map +7 -0
  51. package/dist/cart/use-address-selection.js +130 -0
  52. package/dist/cart/use-address-selection.js.map +7 -0
  53. package/dist/cart.js +21 -1069
  54. package/dist/cart.js.map +7 -1
  55. package/dist/catalogue/catalogue-cart.js +25 -0
  56. package/dist/catalogue/catalogue-cart.js.map +7 -0
  57. package/dist/catalogue/catalogue-header.js +88 -0
  58. package/dist/catalogue/catalogue-header.js.map +7 -0
  59. package/dist/catalogue/catalogue-pages.js +68 -0
  60. package/dist/catalogue/catalogue-pages.js.map +7 -0
  61. package/dist/catalogue/catalogue-search.js +89 -0
  62. package/dist/catalogue/catalogue-search.js.map +7 -0
  63. package/dist/catalogue/category-navigation.js +98 -0
  64. package/dist/catalogue/category-navigation.js.map +7 -0
  65. package/dist/catalogue/category-section.js +113 -0
  66. package/dist/catalogue/category-section.js.map +7 -0
  67. package/dist/catalogue/info-banners.js +148 -0
  68. package/dist/catalogue/info-banners.js.map +7 -0
  69. package/dist/catalogue/item-tile.js +55 -0
  70. package/dist/catalogue/item-tile.js.map +7 -0
  71. package/dist/catalogue/loader.js +136 -0
  72. package/dist/catalogue/loader.js.map +7 -0
  73. package/dist/{chunk-USSLDBDY.js → catalogue/presentation.js} +12 -9
  74. package/dist/catalogue/presentation.js.map +7 -0
  75. package/dist/catalogue/saved-catalogue-sync.js +115 -0
  76. package/dist/catalogue/saved-catalogue-sync.js.map +7 -0
  77. package/dist/catalogue/types.js +1 -0
  78. package/dist/catalogue/types.js.map +7 -0
  79. package/dist/catalogue/use-catalogue-data.js +88 -0
  80. package/dist/catalogue/use-catalogue-data.js.map +7 -0
  81. package/dist/catalogue/use-category-scroll.js +115 -0
  82. package/dist/catalogue/use-category-scroll.js.map +7 -0
  83. package/dist/catalogue/use-item-opener.js +46 -0
  84. package/dist/catalogue/use-item-opener.js.map +7 -0
  85. package/dist/catalogue/use-search-source.js +132 -0
  86. package/dist/catalogue/use-search-source.js.map +7 -0
  87. package/dist/catalogue.js +26 -1216
  88. package/dist/catalogue.js.map +7 -1
  89. package/dist/checkout.js +149 -2
  90. package/dist/checkout.js.map +7 -1
  91. package/dist/host-bridge.js +22 -2
  92. package/dist/host-bridge.js.map +7 -1
  93. package/dist/index.js +3 -3
  94. package/dist/index.js.map +7 -1
  95. package/dist/item/configurator-commit.js +15 -0
  96. package/dist/item/configurator-commit.js.map +7 -0
  97. package/dist/item/configurator-draft.js +63 -0
  98. package/dist/item/configurator-draft.js.map +7 -0
  99. package/dist/item/configurator.js +135 -0
  100. package/dist/item/configurator.js.map +7 -0
  101. package/dist/item/note.js +65 -0
  102. package/dist/item/note.js.map +7 -0
  103. package/dist/item/variant-status.js +23 -0
  104. package/dist/item/variant-status.js.map +7 -0
  105. package/dist/item-frame.js +60 -3
  106. package/dist/item-frame.js.map +7 -1
  107. package/dist/item.js +11 -11
  108. package/dist/item.js.map +7 -1
  109. package/dist/navigation.js +39 -2
  110. package/dist/navigation.js.map +7 -1
  111. package/dist/url-navigation-state.js +61 -0
  112. package/dist/url-navigation-state.js.map +7 -0
  113. package/dist/{chunk-ILOCSCZL.js → url-navigation.js} +12 -89
  114. package/dist/url-navigation.js.map +7 -0
  115. package/package.json +8 -7
  116. package/dist/chunk-2MV6KU3R.js +0 -352
  117. package/dist/chunk-2MV6KU3R.js.map +0 -1
  118. package/dist/chunk-FB5DRCU7.js.map +0 -1
  119. package/dist/chunk-IG63ONID.js +0 -139
  120. package/dist/chunk-IG63ONID.js.map +0 -1
  121. package/dist/chunk-ILOCSCZL.js.map +0 -1
  122. package/dist/chunk-JFEI2OUZ.js.map +0 -1
  123. package/dist/chunk-NTJONY3N.js +0 -95
  124. package/dist/chunk-NTJONY3N.js.map +0 -1
  125. package/dist/chunk-QWST2TA2.js +0 -22
  126. package/dist/chunk-QWST2TA2.js.map +0 -1
  127. package/dist/chunk-USSLDBDY.js.map +0 -1
  128. package/dist/chunk-VIAJ27YC.js +0 -59
  129. package/dist/chunk-VIAJ27YC.js.map +0 -1
@@ -0,0 +1,98 @@
1
+ // src/catalogue/category-navigation.tsx
2
+ import { Overlay, useOverlay } from "@zappie/foundations";
3
+ import { Fragment, useEffect, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ function CategoryNavigation({
6
+ categories,
7
+ active,
8
+ themeClass,
9
+ slots,
10
+ onSelect,
11
+ onFocusCategory
12
+ }) {
13
+ const overlay = useOverlay("catalogue_categories", "categories");
14
+ const [filter, setFilter] = useState("");
15
+ const [pending, setPending] = useState();
16
+ const CategoryTab = slots?.CategoryTab;
17
+ useEffect(() => {
18
+ if (pending && !overlay.isOpen) {
19
+ const frame = requestAnimationFrame(() => {
20
+ setPending(void 0);
21
+ onSelect(pending);
22
+ onFocusCategory(pending);
23
+ });
24
+ return () => cancelAnimationFrame(frame);
25
+ }
26
+ }, [pending, overlay.isOpen, onSelect, onFocusCategory]);
27
+ const filtered = categories.filter(
28
+ (category) => category.name.toLowerCase().includes(filter.toLowerCase())
29
+ );
30
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
31
+ /* @__PURE__ */ jsxs(
32
+ Overlay.Root,
33
+ {
34
+ param: "catalogue_categories",
35
+ id: "categories",
36
+ presentation: { base: "sheet", lg: "dialog" },
37
+ children: [
38
+ /* @__PURE__ */ jsx(Overlay.Trigger, { children: "Browse categories" }),
39
+ /* @__PURE__ */ jsxs(
40
+ Overlay.Content,
41
+ {
42
+ "data-zappie-theme": "base",
43
+ className: ["z-base-cart z-base-editor", themeClass].filter(Boolean).join(" "),
44
+ children: [
45
+ /* @__PURE__ */ jsx(Overlay.Title, { children: "Browse categories" }),
46
+ /* @__PURE__ */ jsx(Overlay.Close, { children: "Close categories" }),
47
+ /* @__PURE__ */ jsx(
48
+ "input",
49
+ {
50
+ "aria-label": "Filter categories",
51
+ value: filter,
52
+ onChange: (event) => setFilter(event.target.value)
53
+ }
54
+ ),
55
+ filtered.map((category) => /* @__PURE__ */ jsx(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ onClick: () => {
60
+ setPending(category.id);
61
+ overlay.close();
62
+ },
63
+ children: category.name
64
+ },
65
+ category.id
66
+ )),
67
+ filtered.length === 0 && /* @__PURE__ */ jsx("p", { children: "No matching categories" })
68
+ ]
69
+ }
70
+ )
71
+ ]
72
+ }
73
+ ),
74
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Categories", className: "z-base-category-nav", children: categories.map((category) => /* @__PURE__ */ jsx(
75
+ "a",
76
+ {
77
+ href: `#base-category-${category.id}`,
78
+ "aria-current": active === category.id ? "true" : void 0,
79
+ onClick: (event) => {
80
+ event.preventDefault();
81
+ onSelect(category.id);
82
+ },
83
+ children: CategoryTab ? /* @__PURE__ */ jsx(
84
+ CategoryTab,
85
+ {
86
+ category,
87
+ active: active === category.id
88
+ }
89
+ ) : category.name
90
+ },
91
+ category.id
92
+ )) })
93
+ ] });
94
+ }
95
+ export {
96
+ CategoryNavigation
97
+ };
98
+ //# sourceMappingURL=category-navigation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/catalogue/category-navigation.tsx"],
4
+ "sourcesContent": ["import { Overlay, useOverlay } from \"@zappie/foundations\";\nimport type { Category } from \"@zappie/sdk\";\nimport { Fragment, useEffect, useState } from \"react\";\nimport type { CatalogueSlots } from \"./types\";\n\nexport function CategoryNavigation({\n categories,\n active,\n themeClass,\n slots,\n onSelect,\n onFocusCategory,\n}: {\n categories: readonly Category[];\n active?: string;\n themeClass?: string;\n slots?: CatalogueSlots;\n onSelect(categoryId: string): void;\n onFocusCategory(categoryId: string): void;\n}) {\n const overlay = useOverlay(\"catalogue_categories\", \"categories\");\n const [filter, setFilter] = useState(\"\");\n const [pending, setPending] = useState<string>();\n const CategoryTab = slots?.CategoryTab;\n useEffect(() => {\n if (pending && !overlay.isOpen) {\n const frame = requestAnimationFrame(() => {\n setPending(undefined);\n onSelect(pending);\n onFocusCategory(pending);\n });\n return () => cancelAnimationFrame(frame);\n }\n }, [pending, overlay.isOpen, onSelect, onFocusCategory]);\n const filtered = categories.filter((category) =>\n category.name.toLowerCase().includes(filter.toLowerCase()),\n );\n return (\n <Fragment>\n <Overlay.Root\n param=\"catalogue_categories\"\n id=\"categories\"\n presentation={{ base: \"sheet\", lg: \"dialog\" }}\n >\n <Overlay.Trigger>Browse categories</Overlay.Trigger>\n <Overlay.Content\n data-zappie-theme=\"base\"\n className={[\"z-base-cart z-base-editor\", themeClass]\n .filter(Boolean)\n .join(\" \")}\n >\n <Overlay.Title>Browse categories</Overlay.Title>\n <Overlay.Close>Close categories</Overlay.Close>\n <input\n aria-label=\"Filter categories\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n />\n {filtered.map((category) => (\n <button\n key={category.id}\n type=\"button\"\n onClick={() => {\n setPending(category.id);\n overlay.close();\n }}\n >\n {category.name}\n </button>\n ))}\n {filtered.length === 0 && <p>No matching categories</p>}\n </Overlay.Content>\n </Overlay.Root>\n <nav aria-label=\"Categories\" className=\"z-base-category-nav\">\n {categories.map((category) => (\n <a\n key={category.id}\n href={`#base-category-${category.id}`}\n aria-current={active === category.id ? \"true\" : undefined}\n onClick={(event) => {\n event.preventDefault();\n onSelect(category.id);\n }}\n >\n {CategoryTab ? (\n <CategoryTab\n category={category}\n active={active === category.id}\n />\n ) : (\n category.name\n )}\n </a>\n ))}\n </nav>\n </Fragment>\n );\n}\n"],
5
+ "mappings": ";AAAA,SAAS,SAAS,kBAAkB;AAEpC,SAAS,UAAU,WAAW,gBAAgB;AA0CtC,cACA,YADA;AAvCD,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,UAAU,WAAW,wBAAwB,YAAY;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAiB;AAC/C,QAAM,cAAc,OAAO;AAC3B,YAAU,MAAM;AACd,QAAI,WAAW,CAAC,QAAQ,QAAQ;AAC9B,YAAM,QAAQ,sBAAsB,MAAM;AACxC,mBAAW,MAAS;AACpB,iBAAS,OAAO;AAChB,wBAAgB,OAAO;AAAA,MACzB,CAAC;AACD,aAAO,MAAM,qBAAqB,KAAK;AAAA,IACzC;AAAA,EACF,GAAG,CAAC,SAAS,QAAQ,QAAQ,UAAU,eAAe,CAAC;AACvD,QAAM,WAAW,WAAW;AAAA,IAAO,CAAC,aAClC,SAAS,KAAK,YAAY,EAAE,SAAS,OAAO,YAAY,CAAC;AAAA,EAC3D;AACA,SACE,qBAAC,YACC;AAAA;AAAA,MAAC,QAAQ;AAAA,MAAR;AAAA,QACC,OAAM;AAAA,QACN,IAAG;AAAA,QACH,cAAc,EAAE,MAAM,SAAS,IAAI,SAAS;AAAA,QAE5C;AAAA,8BAAC,QAAQ,SAAR,EAAgB,+BAAiB;AAAA,UAClC;AAAA,YAAC,QAAQ;AAAA,YAAR;AAAA,cACC,qBAAkB;AAAA,cAClB,WAAW,CAAC,6BAA6B,UAAU,EAChD,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,cAEX;AAAA,oCAAC,QAAQ,OAAR,EAAc,+BAAiB;AAAA,gBAChC,oBAAC,QAAQ,OAAR,EAAc,8BAAgB;AAAA,gBAC/B;AAAA,kBAAC;AAAA;AAAA,oBACC,cAAW;AAAA,oBACX,OAAO;AAAA,oBACP,UAAU,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK;AAAA;AAAA,gBACnD;AAAA,gBACC,SAAS,IAAI,CAAC,aACb;AAAA,kBAAC;AAAA;AAAA,oBAEC,MAAK;AAAA,oBACL,SAAS,MAAM;AACb,iCAAW,SAAS,EAAE;AACtB,8BAAQ,MAAM;AAAA,oBAChB;AAAA,oBAEC,mBAAS;AAAA;AAAA,kBAPL,SAAS;AAAA,gBAQhB,CACD;AAAA,gBACA,SAAS,WAAW,KAAK,oBAAC,OAAE,oCAAsB;AAAA;AAAA;AAAA,UACrD;AAAA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,SAAI,cAAW,cAAa,WAAU,uBACpC,qBAAW,IAAI,CAAC,aACf;AAAA,MAAC;AAAA;AAAA,QAEC,MAAM,kBAAkB,SAAS,EAAE;AAAA,QACnC,gBAAc,WAAW,SAAS,KAAK,SAAS;AAAA,QAChD,SAAS,CAAC,UAAU;AAClB,gBAAM,eAAe;AACrB,mBAAS,SAAS,EAAE;AAAA,QACtB;AAAA,QAEC,wBACC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,QAAQ,WAAW,SAAS;AAAA;AAAA,QAC9B,IAEA,SAAS;AAAA;AAAA,MAdN,SAAS;AAAA,IAgBhB,CACD,GACH;AAAA,KACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,113 @@
1
+ // src/catalogue/category-section.tsx
2
+ import { Cart } from "@zappie/foundations";
3
+ import { countEvent } from "@zappie/sdk/visits";
4
+ import { CartLine } from "../cart/cart-line.js";
5
+ import { BaseItemTile } from "./item-tile.js";
6
+ import { Description } from "./presentation.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function CategorySection({
9
+ category,
10
+ categoryIndex,
11
+ catalogue,
12
+ menuId,
13
+ layout,
14
+ slots,
15
+ sections,
16
+ cartOpen,
17
+ client,
18
+ busy,
19
+ hasMore,
20
+ itemHref,
21
+ onOpen,
22
+ onLoadMore
23
+ }) {
24
+ const CategoryHeader = slots?.CategoryHeader;
25
+ const Tile = slots?.Tile;
26
+ const columns = slots?.layout?.columns;
27
+ const baseColumns = typeof columns === "object" ? columns.base : columns;
28
+ const mediumColumns = typeof columns === "object" ? columns.medium : columns;
29
+ const largeColumns = typeof columns === "object" ? columns.large : columns;
30
+ return /* @__PURE__ */ jsxs(
31
+ "section",
32
+ {
33
+ id: `base-category-${category.id}`,
34
+ className: "z-base-category",
35
+ "data-unloaded": category.itemIds.length === 0,
36
+ ref: (node) => {
37
+ if (node) {
38
+ sections.current.set(category.id, node);
39
+ } else {
40
+ sections.current.delete(category.id);
41
+ }
42
+ },
43
+ children: [
44
+ /* @__PURE__ */ jsx("h3", { tabIndex: -1, children: CategoryHeader ? /* @__PURE__ */ jsx(CategoryHeader, { category }) : category.name }),
45
+ category.description && /* @__PURE__ */ jsx(Description, { value: category.description }),
46
+ /* @__PURE__ */ jsx(
47
+ Cart.Root,
48
+ {
49
+ className: "z-base-item-list",
50
+ "data-layout": layout,
51
+ "data-columns": baseColumns,
52
+ "data-columns-medium": mediumColumns,
53
+ "data-columns-large": largeColumns,
54
+ "data-density": slots?.layout?.density,
55
+ children: category.itemIds.map((id, index) => {
56
+ const item = catalogue.items[id];
57
+ if (!item) {
58
+ return null;
59
+ }
60
+ const lines = client.cart.lines.filter((line) => line.itemId === id);
61
+ const href = itemHref(item.id, menuId);
62
+ return /* @__PURE__ */ jsxs("div", { children: [
63
+ Tile ? /* @__PURE__ */ jsx(
64
+ Tile,
65
+ {
66
+ item,
67
+ ...menuId !== void 0 ? { menuId } : {},
68
+ href,
69
+ quantityInCart: lines.reduce(
70
+ (sum, line) => sum + line.quantity,
71
+ 0
72
+ ),
73
+ onAdd: () => {
74
+ try {
75
+ client.cart.add(item.id, {
76
+ ...menuId !== void 0 ? { menuId } : {}
77
+ });
78
+ countEvent("add");
79
+ } catch {
80
+ onOpen(item);
81
+ }
82
+ },
83
+ onOpen: () => onOpen(item)
84
+ }
85
+ ) : /* @__PURE__ */ jsx(
86
+ BaseItemTile,
87
+ {
88
+ item,
89
+ lines,
90
+ href,
91
+ layout,
92
+ priority: categoryIndex === 0 && index < 4,
93
+ client,
94
+ onOpen: () => onOpen(item)
95
+ }
96
+ ),
97
+ !cartOpen && lines.map((line) => /* @__PURE__ */ jsx(CartLine, { line }, line.id))
98
+ ] }, id);
99
+ })
100
+ }
101
+ ),
102
+ hasMore && /* @__PURE__ */ jsxs("button", { type: "button", disabled: busy, onClick: onLoadMore, children: [
103
+ "Load more ",
104
+ category.name
105
+ ] })
106
+ ]
107
+ }
108
+ );
109
+ }
110
+ export {
111
+ CategorySection
112
+ };
113
+ //# sourceMappingURL=category-section.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/catalogue/category-section.tsx"],
4
+ "sourcesContent": ["import { Cart } from \"@zappie/foundations\";\nimport type { Category, Item, Menu, Zappie } from \"@zappie/sdk\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport type { MutableRefObject } from \"react\";\nimport { CartLine } from \"../cart/cart-line\";\nimport { BaseItemTile } from \"./item-tile\";\nimport { Description } from \"./presentation\";\nimport type { CatalogueLayoutMode, CatalogueSlots } from \"./types\";\n\nexport function CategorySection({\n category,\n categoryIndex,\n catalogue,\n menuId,\n layout,\n slots,\n sections,\n cartOpen,\n client,\n busy,\n hasMore,\n itemHref,\n onOpen,\n onLoadMore,\n}: {\n category: Category;\n categoryIndex: number;\n catalogue: Menu;\n menuId?: string;\n layout: CatalogueLayoutMode;\n slots?: CatalogueSlots;\n sections: MutableRefObject<Map<string, HTMLElement>>;\n cartOpen: boolean;\n client: Zappie;\n busy: boolean;\n hasMore: boolean;\n itemHref(itemId: string, menuId?: string): string;\n onOpen(item: Item): void;\n onLoadMore(): void;\n}) {\n const CategoryHeader = slots?.CategoryHeader;\n const Tile = slots?.Tile;\n const columns = slots?.layout?.columns;\n const baseColumns = typeof columns === \"object\" ? columns.base : columns;\n const mediumColumns = typeof columns === \"object\" ? columns.medium : columns;\n const largeColumns = typeof columns === \"object\" ? columns.large : columns;\n return (\n <section\n id={`base-category-${category.id}`}\n className=\"z-base-category\"\n data-unloaded={category.itemIds.length === 0}\n ref={(node) => {\n if (node) {\n sections.current.set(category.id, node);\n } else {\n sections.current.delete(category.id);\n }\n }}\n >\n <h3 tabIndex={-1}>\n {CategoryHeader ? (\n <CategoryHeader category={category} />\n ) : (\n category.name\n )}\n </h3>\n {category.description && <Description value={category.description} />}\n <Cart.Root\n className=\"z-base-item-list\"\n data-layout={layout}\n data-columns={baseColumns}\n data-columns-medium={mediumColumns}\n data-columns-large={largeColumns}\n data-density={slots?.layout?.density}\n >\n {category.itemIds.map((id, index) => {\n const item = catalogue.items[id];\n if (!item) {\n return null;\n }\n const lines = client.cart.lines.filter((line) => line.itemId === id);\n const href = itemHref(item.id, menuId);\n return (\n <div key={id}>\n {Tile ? (\n <Tile\n item={item}\n {...(menuId !== undefined ? { menuId } : {})}\n href={href}\n quantityInCart={lines.reduce(\n (sum, line) => sum + line.quantity,\n 0,\n )}\n onAdd={() => {\n // An item the cart refuses as-is (required choices, an\n // ambiguous variant) opens the item to be configured.\n try {\n client.cart.add(item.id, {\n ...(menuId !== undefined ? { menuId } : {}),\n });\n countEvent(\"add\");\n } catch {\n onOpen(item);\n }\n }}\n onOpen={() => onOpen(item)}\n />\n ) : (\n <BaseItemTile\n item={item}\n lines={lines}\n href={href}\n layout={layout}\n priority={categoryIndex === 0 && index < 4}\n client={client}\n onOpen={() => onOpen(item)}\n />\n )}\n {!cartOpen &&\n lines.map((line) => <CartLine key={line.id} line={line} />)}\n </div>\n );\n })}\n </Cart.Root>\n {hasMore && (\n <button type=\"button\" disabled={busy} onClick={onLoadMore}>\n Load more {category.name}\n </button>\n )}\n </section>\n );\n}\n"],
5
+ "mappings": ";AAAA,SAAS,YAAY;AAErB,SAAS,kBAAkB;AAE3B,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAuDlB,cAsBE,YAtBF;AApDH,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAeG;AACD,QAAM,iBAAiB,OAAO;AAC9B,QAAM,OAAO,OAAO;AACpB,QAAM,UAAU,OAAO,QAAQ;AAC/B,QAAM,cAAc,OAAO,YAAY,WAAW,QAAQ,OAAO;AACjE,QAAM,gBAAgB,OAAO,YAAY,WAAW,QAAQ,SAAS;AACrE,QAAM,eAAe,OAAO,YAAY,WAAW,QAAQ,QAAQ;AACnE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI,iBAAiB,SAAS,EAAE;AAAA,MAChC,WAAU;AAAA,MACV,iBAAe,SAAS,QAAQ,WAAW;AAAA,MAC3C,KAAK,CAAC,SAAS;AACb,YAAI,MAAM;AACR,mBAAS,QAAQ,IAAI,SAAS,IAAI,IAAI;AAAA,QACxC,OAAO;AACL,mBAAS,QAAQ,OAAO,SAAS,EAAE;AAAA,QACrC;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,QAAG,UAAU,IACX,2BACC,oBAAC,kBAAe,UAAoB,IAEpC,SAAS,MAEb;AAAA,QACC,SAAS,eAAe,oBAAC,eAAY,OAAO,SAAS,aAAa;AAAA,QACnE;AAAA,UAAC,KAAK;AAAA,UAAL;AAAA,YACC,WAAU;AAAA,YACV,eAAa;AAAA,YACb,gBAAc;AAAA,YACd,uBAAqB;AAAA,YACrB,sBAAoB;AAAA,YACpB,gBAAc,OAAO,QAAQ;AAAA,YAE5B,mBAAS,QAAQ,IAAI,CAAC,IAAI,UAAU;AACnC,oBAAM,OAAO,UAAU,MAAM,EAAE;AAC/B,kBAAI,CAAC,MAAM;AACT,uBAAO;AAAA,cACT;AACA,oBAAM,QAAQ,OAAO,KAAK,MAAM,OAAO,CAAC,SAAS,KAAK,WAAW,EAAE;AACnE,oBAAM,OAAO,SAAS,KAAK,IAAI,MAAM;AACrC,qBACE,qBAAC,SACE;AAAA,uBACC;AAAA,kBAAC;AAAA;AAAA,oBACC;AAAA,oBACC,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;AAAA,oBAC1C;AAAA,oBACA,gBAAgB,MAAM;AAAA,sBACpB,CAAC,KAAK,SAAS,MAAM,KAAK;AAAA,sBAC1B;AAAA,oBACF;AAAA,oBACA,OAAO,MAAM;AAGX,0BAAI;AACF,+BAAO,KAAK,IAAI,KAAK,IAAI;AAAA,0BACvB,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;AAAA,wBAC3C,CAAC;AACD,mCAAW,KAAK;AAAA,sBAClB,QAAQ;AACN,+BAAO,IAAI;AAAA,sBACb;AAAA,oBACF;AAAA,oBACA,QAAQ,MAAM,OAAO,IAAI;AAAA;AAAA,gBAC3B,IAEA;AAAA,kBAAC;AAAA;AAAA,oBACC;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA,UAAU,kBAAkB,KAAK,QAAQ;AAAA,oBACzC;AAAA,oBACA,QAAQ,MAAM,OAAO,IAAI;AAAA;AAAA,gBAC3B;AAAA,gBAED,CAAC,YACA,MAAM,IAAI,CAAC,SAAS,oBAAC,YAAuB,QAAT,KAAK,EAAgB,CAAE;AAAA,mBApCpD,EAqCV;AAAA,YAEJ,CAAC;AAAA;AAAA,QACH;AAAA,QACC,WACC,qBAAC,YAAO,MAAK,UAAS,UAAU,MAAM,SAAS,YAAY;AAAA;AAAA,UAC9C,SAAS;AAAA,WACtB;AAAA;AAAA;AAAA,EAEJ;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ // src/catalogue/info-banners.tsx
4
+ import {
5
+ createSanitizeRichText,
6
+ splitInfoBanner
7
+ } from "@zappie/foundations/rich-text";
8
+ import { parseRichText } from "@zappie/foundations/rich-text/browser";
9
+ import { useEffect, useRef, useState } from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ var sanitize = createSanitizeRichText(parseRichText);
12
+ function InfoBanners({ value }) {
13
+ const [banners, setBanners] = useState([]);
14
+ const [index, setIndex] = useState(0);
15
+ const [expanded, setExpanded] = useState({});
16
+ const [paused, setPaused] = useState(false);
17
+ const viewport = useRef(null);
18
+ const interacted = useRef(false);
19
+ useEffect(() => {
20
+ setBanners(
21
+ splitInfoBanner(value).map(sanitize).filter((banner) => banner.replace(/<[^>]*>/g, "").trim())
22
+ );
23
+ setIndex(0);
24
+ setExpanded({});
25
+ interacted.current = false;
26
+ }, [value]);
27
+ function select(next, interaction = true) {
28
+ interacted.current || (interacted.current = interaction);
29
+ setExpanded({});
30
+ viewport.current?.children[next]?.scrollIntoView({
31
+ block: "nearest",
32
+ inline: "start",
33
+ behavior: "instant"
34
+ });
35
+ setIndex(next);
36
+ }
37
+ const isExpanded = Object.values(expanded).some(Boolean);
38
+ useEffect(() => {
39
+ if (paused || isExpanded || banners.length < 2) {
40
+ return;
41
+ }
42
+ const interval = setInterval(() => {
43
+ const node = viewport.current;
44
+ if (!node || interacted.current || window.matchMedia("(prefers-reduced-motion: reduce)").matches || node.scrollWidth <= node.clientWidth) {
45
+ return;
46
+ }
47
+ select((index + 1) % banners.length, false);
48
+ }, 5e3);
49
+ return () => clearInterval(interval);
50
+ }, [index, paused, isExpanded, banners.length]);
51
+ if (!banners.length) {
52
+ return null;
53
+ }
54
+ return /* @__PURE__ */ jsxs(
55
+ "section",
56
+ {
57
+ "aria-label": "Menu information",
58
+ onMouseEnter: () => setPaused(true),
59
+ onMouseLeave: () => setPaused(false),
60
+ onFocus: () => setPaused(true),
61
+ onBlur: () => setPaused(false),
62
+ children: [
63
+ /* @__PURE__ */ jsx(
64
+ "div",
65
+ {
66
+ className: "z-base-info-banners",
67
+ ref: viewport,
68
+ onPointerDown: () => {
69
+ interacted.current = true;
70
+ },
71
+ onScroll: () => {
72
+ const node = viewport.current;
73
+ if (node?.children[0]) {
74
+ setIndex(
75
+ Math.round(
76
+ node.scrollLeft / node.children[0].getBoundingClientRect().width
77
+ )
78
+ );
79
+ }
80
+ },
81
+ children: banners.map((banner, position) => /* @__PURE__ */ jsxs("article", { children: [
82
+ /* @__PURE__ */ jsx(
83
+ "div",
84
+ {
85
+ className: "z-base-info-copy",
86
+ "data-expanded": Boolean(expanded[position]),
87
+ dangerouslySetInnerHTML: { __html: banner }
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsxs(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ "aria-expanded": Boolean(expanded[position]),
95
+ onClick: () => setExpanded((current) => ({
96
+ ...current,
97
+ [position]: !current[position]
98
+ })),
99
+ children: [
100
+ "Show ",
101
+ expanded[position] ? "less" : "more"
102
+ ]
103
+ }
104
+ )
105
+ ] }, banner + position))
106
+ }
107
+ ),
108
+ banners.length > 1 && /* @__PURE__ */ jsxs("div", { children: [
109
+ /* @__PURE__ */ jsx(
110
+ "button",
111
+ {
112
+ type: "button",
113
+ "aria-label": "Previous information",
114
+ disabled: index === 0,
115
+ onClick: () => select(index - 1),
116
+ children: "Previous"
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsx(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ "aria-label": "Next information",
124
+ disabled: index >= banners.length - 1,
125
+ onClick: () => select(index + 1),
126
+ children: "Next"
127
+ }
128
+ ),
129
+ banners.map((_, position) => /* @__PURE__ */ jsx(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ "aria-label": `Go to banner ${position + 1}`,
134
+ "aria-current": index === position ? "true" : void 0,
135
+ onClick: () => select(position),
136
+ children: position + 1
137
+ },
138
+ position
139
+ ))
140
+ ] })
141
+ ]
142
+ }
143
+ );
144
+ }
145
+ export {
146
+ InfoBanners
147
+ };
148
+ //# sourceMappingURL=info-banners.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/catalogue/info-banners.tsx"],
4
+ "sourcesContent": ["\"use client\";\nimport {\n createSanitizeRichText,\n splitInfoBanner,\n} from \"@zappie/foundations/rich-text\";\nimport { parseRichText } from \"@zappie/foundations/rich-text/browser\";\nimport { useEffect, useRef, useState } from \"react\";\nconst sanitize = createSanitizeRichText(parseRichText);\n/** Safe rich text with touch scrolling, explicit controls and pausable rotation. */\nexport function InfoBanners({ value }: { value?: string }) {\n const [banners, setBanners] = useState<string[]>([]);\n const [index, setIndex] = useState(0);\n const [expanded, setExpanded] = useState<Record<number, boolean>>({});\n const [paused, setPaused] = useState(false);\n const viewport = useRef<HTMLDivElement>(null);\n const interacted = useRef(false);\n useEffect(() => {\n setBanners(\n splitInfoBanner(value)\n .map(sanitize)\n .filter((banner) => banner.replace(/<[^>]*>/g, \"\").trim()),\n );\n setIndex(0);\n setExpanded({});\n interacted.current = false;\n }, [value]);\n function select(next: number, interaction = true) {\n interacted.current ||= interaction;\n setExpanded({});\n viewport.current?.children[next]?.scrollIntoView({\n block: \"nearest\",\n inline: \"start\",\n behavior: \"instant\",\n });\n setIndex(next);\n }\n const isExpanded = Object.values(expanded).some(Boolean);\n useEffect(() => {\n if (paused || isExpanded || banners.length < 2) {\n return;\n }\n const interval = setInterval(() => {\n const node = viewport.current;\n if (\n !node ||\n interacted.current ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ||\n node.scrollWidth <= node.clientWidth\n ) {\n return;\n }\n select((index + 1) % banners.length, false);\n }, 5000);\n return () => clearInterval(interval);\n }, [index, paused, isExpanded, banners.length]);\n if (!banners.length) {\n return null;\n }\n return (\n <section\n aria-label=\"Menu information\"\n onMouseEnter={() => setPaused(true)}\n onMouseLeave={() => setPaused(false)}\n onFocus={() => setPaused(true)}\n onBlur={() => setPaused(false)}\n >\n <div\n className=\"z-base-info-banners\"\n ref={viewport}\n onPointerDown={() => {\n interacted.current = true;\n }}\n onScroll={() => {\n const node = viewport.current;\n if (node?.children[0]) {\n setIndex(\n Math.round(\n node.scrollLeft /\n node.children[0].getBoundingClientRect().width,\n ),\n );\n }\n }}\n >\n {banners.map((banner, position) => (\n <article key={banner + position}>\n <div\n className=\"z-base-info-copy\"\n data-expanded={Boolean(expanded[position])}\n dangerouslySetInnerHTML={{ __html: banner }}\n />\n <button\n type=\"button\"\n aria-expanded={Boolean(expanded[position])}\n onClick={() =>\n setExpanded((current) => ({\n ...current,\n [position]: !current[position],\n }))\n }\n >\n Show {expanded[position] ? \"less\" : \"more\"}\n </button>\n </article>\n ))}\n </div>\n {banners.length > 1 && (\n <div>\n <button\n type=\"button\"\n aria-label=\"Previous information\"\n disabled={index === 0}\n onClick={() => select(index - 1)}\n >\n Previous\n </button>\n <button\n type=\"button\"\n aria-label=\"Next information\"\n disabled={index >= banners.length - 1}\n onClick={() => select(index + 1)}\n >\n Next\n </button>\n {banners.map((_, position) => (\n <button\n key={position}\n type=\"button\"\n aria-label={`Go to banner ${position + 1}`}\n aria-current={index === position ? \"true\" : undefined}\n onClick={() => select(position)}\n >\n {position + 1}\n </button>\n ))}\n </div>\n )}\n </section>\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,55 @@
1
+ // src/catalogue/item-tile.tsx
2
+ import { Description, ItemImage } from "./presentation.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ function BaseItemTile({
5
+ item,
6
+ lines,
7
+ href,
8
+ layout,
9
+ priority,
10
+ client,
11
+ onOpen
12
+ }) {
13
+ const price = item.price;
14
+ return /* @__PURE__ */ jsxs(
15
+ "a",
16
+ {
17
+ href,
18
+ className: "z-base-item-tile",
19
+ onClick: (event) => {
20
+ event.preventDefault();
21
+ event.currentTarget.focus({ preventScroll: true });
22
+ onOpen();
23
+ },
24
+ children: [
25
+ /* @__PURE__ */ jsx(
26
+ ItemImage,
27
+ {
28
+ src: item.images[0],
29
+ sizes: layout === "LIST" ? "100vw" : "(min-width: 768px) 33vw, 50vw",
30
+ priority
31
+ }
32
+ ),
33
+ /* @__PURE__ */ jsx("span", { className: "z-base-item-name", children: item.name }),
34
+ item.description && /* @__PURE__ */ jsx(Description, { value: item.description }),
35
+ item.promo && /* @__PURE__ */ jsx("span", { children: item.promo.name }),
36
+ item.compareAtPrice && /* @__PURE__ */ jsx("del", { children: client.format.money(item.compareAtPrice) }),
37
+ price && /* @__PURE__ */ jsxs("span", { className: "z-base-amount", children: [
38
+ item.variants.some(
39
+ (variant) => variant.price.amountMinor !== price.amountMinor
40
+ ) ? "From " : "",
41
+ client.format.money(price)
42
+ ] }),
43
+ (item.stockLabel || !item.isAvailable) && /* @__PURE__ */ jsx("span", { children: item.stockLabel ?? "Unavailable" }),
44
+ lines.map((line) => /* @__PURE__ */ jsxs("span", { children: [
45
+ line.quantity,
46
+ " added"
47
+ ] }, line.id))
48
+ ]
49
+ }
50
+ );
51
+ }
52
+ export {
53
+ BaseItemTile
54
+ };
55
+ //# sourceMappingURL=item-tile.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/catalogue/item-tile.tsx"],
4
+ "sourcesContent": ["import type { CartLine, Item, Zappie } from \"@zappie/sdk\";\nimport { Description, ItemImage } from \"./presentation\";\nimport type { CatalogueLayoutMode } from \"./types\";\n\nexport function BaseItemTile({\n item,\n lines,\n href,\n layout,\n priority,\n client,\n onOpen,\n}: {\n item: Item;\n lines: readonly CartLine[];\n href: string;\n layout: CatalogueLayoutMode;\n priority: boolean;\n client: Zappie;\n onOpen(): void;\n}) {\n const price = item.price;\n return (\n <a\n href={href}\n className=\"z-base-item-tile\"\n onClick={(event) => {\n event.preventDefault();\n event.currentTarget.focus({ preventScroll: true });\n onOpen();\n }}\n >\n <ItemImage\n src={item.images[0]}\n sizes={layout === \"LIST\" ? \"100vw\" : \"(min-width: 768px) 33vw, 50vw\"}\n priority={priority}\n />\n <span className=\"z-base-item-name\">{item.name}</span>\n {item.description && <Description value={item.description} />}\n {item.promo && <span>{item.promo.name}</span>}\n {item.compareAtPrice && (\n <del>{client.format.money(item.compareAtPrice)}</del>\n )}\n {price && (\n <span className=\"z-base-amount\">\n {item.variants.some(\n (variant) => variant.price.amountMinor !== price.amountMinor,\n )\n ? \"From \"\n : \"\"}\n {client.format.money(price)}\n </span>\n )}\n {(item.stockLabel || !item.isAvailable) && (\n <span>{item.stockLabel ?? \"Unavailable\"}</span>\n )}\n {lines.map((line) => (\n <span key={line.id}>{line.quantity} added</span>\n ))}\n </a>\n );\n}\n"],
5
+ "mappings": ";AACA,SAAS,aAAa,iBAAiB;AA+BjC,cAYE,YAZF;AA5BC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,QAAQ,KAAK;AACnB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,SAAS,CAAC,UAAU;AAClB,cAAM,eAAe;AACrB,cAAM,cAAc,MAAM,EAAE,eAAe,KAAK,CAAC;AACjD,eAAO;AAAA,MACT;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,KAAK,OAAO,CAAC;AAAA,YAClB,OAAO,WAAW,SAAS,UAAU;AAAA,YACrC;AAAA;AAAA,QACF;AAAA,QACA,oBAAC,UAAK,WAAU,oBAAoB,eAAK,MAAK;AAAA,QAC7C,KAAK,eAAe,oBAAC,eAAY,OAAO,KAAK,aAAa;AAAA,QAC1D,KAAK,SAAS,oBAAC,UAAM,eAAK,MAAM,MAAK;AAAA,QACrC,KAAK,kBACJ,oBAAC,SAAK,iBAAO,OAAO,MAAM,KAAK,cAAc,GAAE;AAAA,QAEhD,SACC,qBAAC,UAAK,WAAU,iBACb;AAAA,eAAK,SAAS;AAAA,YACb,CAAC,YAAY,QAAQ,MAAM,gBAAgB,MAAM;AAAA,UACnD,IACI,UACA;AAAA,UACH,OAAO,OAAO,MAAM,KAAK;AAAA,WAC5B;AAAA,SAEA,KAAK,cAAc,CAAC,KAAK,gBACzB,oBAAC,UAAM,eAAK,cAAc,eAAc;AAAA,QAEzC,MAAM,IAAI,CAAC,SACV,qBAAC,UAAoB;AAAA,eAAK;AAAA,UAAS;AAAA,aAAxB,KAAK,EAAyB,CAC1C;AAAA;AAAA;AAAA,EACH;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,136 @@
1
+ // src/catalogue/loader.ts
2
+ import { mergePage, pageKey, pageState } from "./catalogue-pages.js";
3
+ import {
4
+ createSavedCatalogueSync
5
+ } from "./saved-catalogue-sync.js";
6
+ function createCatalogueLoader(client, metadata, resolve, publish, orderContext = "delivery", revision) {
7
+ const state = {
8
+ menu: metadata,
9
+ mode: resolve(metadata),
10
+ complete: false,
11
+ abort: new AbortController(),
12
+ pages: /* @__PURE__ */ new Map(),
13
+ savedPages: []
14
+ };
15
+ const scope = {
16
+ store: client.config.store,
17
+ branchId: metadata.branchId,
18
+ orderContext,
19
+ menuId: metadata.menus?.[0]?.id ?? metadata.id
20
+ };
21
+ const saved = createSavedCatalogueSync({
22
+ client,
23
+ state,
24
+ scope,
25
+ revision,
26
+ publish
27
+ });
28
+ const pending = /* @__PURE__ */ new Map();
29
+ let eager;
30
+ let speculative;
31
+ function readEager(signal = state.abort.signal) {
32
+ eager ?? (eager = saved.resources.selected.read(scope, { signal }).then(async (response) => {
33
+ signal.throwIfAborted();
34
+ if ("notModified" in response) {
35
+ throw new Error("Unexpected conditional selected-menu response");
36
+ }
37
+ state.complete = true;
38
+ state.menu = response.menu;
39
+ saved.emit();
40
+ return state.menu;
41
+ }).catch((failure) => {
42
+ eager = void 0;
43
+ throw failure;
44
+ }));
45
+ return eager;
46
+ }
47
+ function speculate() {
48
+ if (eager) {
49
+ return;
50
+ }
51
+ speculative = new AbortController();
52
+ const cancel = () => speculative?.abort();
53
+ state.abort.signal.addEventListener("abort", cancel, { once: true });
54
+ void readEager(speculative.signal).catch(() => {
55
+ });
56
+ }
57
+ async function readPage(menuId, categoryId, key, cursor) {
58
+ const page = await client.menu.page(menuId, categoryId, {
59
+ branch: metadata.branchId,
60
+ signal: state.abort.signal,
61
+ ...cursor ? { cursor } : {}
62
+ });
63
+ state.abort.signal.throwIfAborted();
64
+ if (!state.complete) {
65
+ state.menu = mergePage(state.menu, page);
66
+ }
67
+ state.savedPages.push({ categoryId, ...cursor ? { cursor } : {}, page });
68
+ saved.emit();
69
+ state.pages.set(key, pageState(page.nextCursor));
70
+ if (state.mode === null) {
71
+ state.mode = resolve(metadata, page.menu) ?? "progressive";
72
+ if (state.mode !== "eager") {
73
+ speculative?.abort();
74
+ eager = void 0;
75
+ }
76
+ }
77
+ return state.menu;
78
+ }
79
+ const initialized = saved.restore().catch(() => false);
80
+ return {
81
+ get value() {
82
+ return state.menu;
83
+ },
84
+ get mode() {
85
+ return state.mode;
86
+ },
87
+ hasMore(menuId, categoryId) {
88
+ return state.mode !== "eager" && !state.pages.get(pageKey(menuId, categoryId))?.done;
89
+ },
90
+ /** True when reaching this category would neither read nor change. */
91
+ isLoaded(menuId, categoryId) {
92
+ return state.complete || state.mode !== "eager" && state.pages.has(pageKey(menuId, categoryId));
93
+ },
94
+ async reach(menuId, categoryId, more = false) {
95
+ const key = pageKey(menuId, categoryId);
96
+ const warm = await initialized;
97
+ if (state.complete || warm && state.pages.has(key) && !more) {
98
+ return state.menu;
99
+ }
100
+ if (state.mode === "eager") {
101
+ return readEager();
102
+ }
103
+ const existing = pending.get(key);
104
+ if (existing) {
105
+ return existing;
106
+ }
107
+ const loaded = state.pages.get(key);
108
+ if (loaded && (!more || loaded.done)) {
109
+ return state.menu;
110
+ }
111
+ if (state.mode === null) {
112
+ speculate();
113
+ }
114
+ const request = readPage(menuId, categoryId, key, loaded?.next);
115
+ pending.set(key, request);
116
+ try {
117
+ return await request;
118
+ } finally {
119
+ pending.delete(key);
120
+ }
121
+ },
122
+ /** Aborts in-flight reads; `resume()` makes the loader usable again. */
123
+ dispose() {
124
+ state.abort.abort();
125
+ },
126
+ resume() {
127
+ if (state.abort.signal.aborted) {
128
+ state.abort = new AbortController();
129
+ }
130
+ }
131
+ };
132
+ }
133
+ export {
134
+ createCatalogueLoader
135
+ };
136
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/catalogue/loader.ts"],
4
+ "sourcesContent": ["import type { Menu, Zappie } from \"@zappie/sdk\";\nimport { mergePage, pageKey, pageState } from \"./catalogue-pages\";\nimport {\n createSavedCatalogueSync,\n type LoaderState,\n} from \"./saved-catalogue-sync\";\nexport type CatalogueStrategy = (\n metadata: Menu,\n sample?: Menu,\n) => \"eager\" | \"progressive\" | null;\n/** Screen-local read state; never marks a progressive catalogue as complete. */\nexport function createCatalogueLoader(\n client: Zappie,\n metadata: Menu,\n resolve: CatalogueStrategy,\n publish?: (menu: Menu) => void,\n orderContext: \"delivery\" | \"pickup\" | \"dineIn\" = \"delivery\",\n /** Host catalogue revision; a saved copy at this revision is not refreshed. */\n revision?: string,\n) {\n const state: LoaderState = {\n menu: metadata,\n mode: resolve(metadata),\n complete: false,\n abort: new AbortController(),\n pages: new Map(),\n savedPages: [],\n };\n const scope = {\n store: client.config.store,\n branchId: metadata.branchId,\n orderContext,\n menuId: metadata.menus?.[0]?.id ?? metadata.id,\n };\n const saved = createSavedCatalogueSync({\n client,\n state,\n scope,\n revision,\n publish,\n });\n const pending = new Map<string, Promise<Menu>>();\n let eager: Promise<Menu> | undefined;\n // The complete read starts beside the size probe rather than after it, and\n // is cancelled if the probe's measurement rejects eager loading.\n let speculative: AbortController | undefined;\n function readEager(signal: AbortSignal = state.abort.signal) {\n eager ??= saved\n .resources!.selected.read(scope, { signal })\n .then(async (response) => {\n signal.throwIfAborted();\n if (\"notModified\" in response) {\n throw new Error(\"Unexpected conditional selected-menu response\");\n }\n state.complete = true;\n state.menu = response.menu;\n saved.emit();\n return state.menu;\n })\n .catch((failure: unknown) => {\n eager = undefined;\n throw failure;\n });\n return eager;\n }\n function speculate() {\n if (eager) {\n return;\n }\n speculative = new AbortController();\n const cancel = () => speculative?.abort();\n state.abort.signal.addEventListener(\"abort\", cancel, { once: true });\n void readEager(speculative.signal).catch(() => {});\n }\n async function readPage(\n menuId: string,\n categoryId: string,\n key: string,\n cursor: string | undefined,\n ) {\n const page = await client.menu.page(menuId, categoryId, {\n branch: metadata.branchId,\n signal: state.abort.signal,\n ...(cursor ? { cursor } : {}),\n });\n state.abort.signal.throwIfAborted();\n // The size probe is a real first page: it renders before any\n // complete read the measurement approves.\n if (!state.complete) {\n state.menu = mergePage(state.menu, page);\n }\n state.savedPages.push({ categoryId, ...(cursor ? { cursor } : {}), page });\n saved.emit();\n state.pages.set(key, pageState(page.nextCursor));\n if (state.mode === null) {\n state.mode = resolve(metadata, page.menu) ?? \"progressive\";\n if (state.mode !== \"eager\") {\n speculative?.abort();\n eager = undefined;\n }\n }\n return state.menu;\n }\n // Restoring starts with the loader, not at the first reach; any failure to\n // restore (no IndexedDB, a chunk error) falls back to the network.\n const initialized = saved.restore().catch(() => false);\n return {\n get value() {\n return state.menu;\n },\n get mode() {\n return state.mode;\n },\n hasMore(menuId: string, categoryId: string) {\n return (\n state.mode !== \"eager\" &&\n !state.pages.get(pageKey(menuId, categoryId))?.done\n );\n },\n /** True when reaching this category would neither read nor change. */\n isLoaded(menuId: string, categoryId: string) {\n return (\n state.complete ||\n (state.mode !== \"eager\" && state.pages.has(pageKey(menuId, categoryId)))\n );\n },\n async reach(\n menuId: string,\n categoryId: string,\n more = false,\n ): Promise<Menu> {\n const key = pageKey(menuId, categoryId);\n const warm = await initialized;\n if (state.complete || (warm && state.pages.has(key) && !more)) {\n return state.menu;\n }\n if (state.mode === \"eager\") {\n return readEager();\n }\n const existing = pending.get(key);\n if (existing) {\n return existing;\n }\n const loaded = state.pages.get(key);\n if (loaded && (!more || loaded.done)) {\n return state.menu;\n }\n if (state.mode === null) {\n speculate();\n }\n const request = readPage(menuId, categoryId, key, loaded?.next);\n pending.set(key, request);\n try {\n return await request;\n } finally {\n pending.delete(key);\n }\n },\n /** Aborts in-flight reads; `resume()` makes the loader usable again. */\n dispose() {\n state.abort.abort();\n },\n resume() {\n if (state.abort.signal.aborted) {\n state.abort = new AbortController();\n }\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }