restaurant-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +488 -0
  3. package/dist/RestaurantSiteRoutes.js +23 -0
  4. package/dist/admin/AdminActionMenu.js +29 -0
  5. package/dist/admin/AdminAuthPanel.js +53 -0
  6. package/dist/admin/AdminBackupsDialog.js +54 -0
  7. package/dist/admin/AdminDraftProvider.js +10 -0
  8. package/dist/admin/AdminGroupsPanel.js +68 -0
  9. package/dist/admin/AdminItemDrawer.js +264 -0
  10. package/dist/admin/AdminMenuCard.js +63 -0
  11. package/dist/admin/AdminMenuEditor.module.css.js +209 -0
  12. package/dist/admin/AdminMenuGroupTabs.js +43 -0
  13. package/dist/admin/AdminMenuIconPicker.js +39 -0
  14. package/dist/admin/AdminRoutes.js +13 -0
  15. package/dist/admin/AdminSectionEditor.js +104 -0
  16. package/dist/admin/AdminTestimonialsPanel.js +214 -0
  17. package/dist/admin/AdminTestimonialsPanel.module.css.js +29 -0
  18. package/dist/admin/adminDraftContext.js +5 -0
  19. package/dist/admin/adminEditorUtils.js +47 -0
  20. package/dist/admin/adminMenuCommands.js +83 -0
  21. package/dist/admin/useAdminDraft.js +14 -0
  22. package/dist/admin/useAdminMenuDraft.js +226 -0
  23. package/dist/cart/CartDrawer.js +87 -0
  24. package/dist/cart/CartDrawer.module.css.js +47 -0
  25. package/dist/cart/CartProvider.js +31 -0
  26. package/dist/cart/cartContext.js +5 -0
  27. package/dist/cart/cartOrder.js +34 -0
  28. package/dist/cart/cartReducer.js +57 -0
  29. package/dist/cart/useCart.js +10 -0
  30. package/dist/components/Brand.js +16 -0
  31. package/dist/components/Brand.module.css.js +15 -0
  32. package/dist/components/Footer.js +19 -0
  33. package/dist/components/Footer.module.css.js +11 -0
  34. package/dist/components/Header.js +34 -0
  35. package/dist/components/Header.module.css.js +19 -0
  36. package/dist/components/Hero.js +23 -0
  37. package/dist/components/Hero.module.css.js +15 -0
  38. package/dist/components/InstagramIcon.js +27 -0
  39. package/dist/components/LocationsSection.js +34 -0
  40. package/dist/components/LocationsSection.module.css.js +17 -0
  41. package/dist/components/Marquee.js +10 -0
  42. package/dist/components/Marquee.module.css.js +9 -0
  43. package/dist/components/OrderIcon.js +11 -0
  44. package/dist/components/RatingSection.js +32 -0
  45. package/dist/components/RatingSection.module.css.js +19 -0
  46. package/dist/components/TestimonialsSection.js +73 -0
  47. package/dist/components/TestimonialsSection.module.css.js +37 -0
  48. package/dist/components/WhatsAppIcon.js +13 -0
  49. package/dist/config/RestaurantKitProvider.js +26 -0
  50. package/dist/config/siteConfigContext.js +23 -0
  51. package/dist/config/useOrdering.js +12 -0
  52. package/dist/index.js +92 -0
  53. package/dist/menu/localMenuApi.js +43 -0
  54. package/dist/menu/menuApi.js +121 -0
  55. package/dist/menu/menuDraftStorage.js +35 -0
  56. package/dist/menu/menuIcons.js +24 -0
  57. package/dist/menu/menuMutations.js +99 -0
  58. package/dist/menu/menuOrdering.js +94 -0
  59. package/dist/menu/menuSchema.js +4 -0
  60. package/dist/menu/priceFormat.js +39 -0
  61. package/dist/menu/useMenuData.js +38 -0
  62. package/dist/menu/validateMenu.js +63 -0
  63. package/dist/menu-browser/MenuBrowser.js +54 -0
  64. package/dist/menu-browser/MenuBrowser.module.css.js +153 -0
  65. package/dist/menu-browser/MenuCard.js +106 -0
  66. package/dist/menu-browser/MenuCartDrawer.js +18 -0
  67. package/dist/menu-browser/MenuCategoryBlock.js +45 -0
  68. package/dist/menu-browser/MenuItemSheet.js +132 -0
  69. package/dist/menu-browser/MenuLightbox.js +33 -0
  70. package/dist/menu-browser/MenuRow.js +43 -0
  71. package/dist/menu-browser/MenuSection.js +10 -0
  72. package/dist/menu-browser/menuItemDisplay.js +19 -0
  73. package/dist/menu-browser/useMediaQuery.js +19 -0
  74. package/dist/menu-browser/useMenuBrowserState.js +82 -0
  75. package/dist/menu-browser/useParkedSheet.js +32 -0
  76. package/dist/pages/AdminMenuEditor.js +291 -0
  77. package/dist/pages/AdminPreview.js +43 -0
  78. package/dist/pages/AdminPreview.module.css.js +9 -0
  79. package/dist/pages/MenuSite.js +40 -0
  80. package/dist/restaurant-kit.css +1 -0
  81. package/dist/testimonials/downscaleImage.js +14 -0
  82. package/dist/testimonials/testimonialMutations.js +52 -0
  83. package/dist/types/RestaurantSiteRoutes.d.ts +11 -0
  84. package/dist/types/admin/AdminActionMenu.d.ts +20 -0
  85. package/dist/types/admin/AdminAuthPanel.d.ts +13 -0
  86. package/dist/types/admin/AdminBackupsDialog.d.ts +9 -0
  87. package/dist/types/admin/AdminDraftProvider.d.ts +12 -0
  88. package/dist/types/admin/AdminGroupsPanel.d.ts +14 -0
  89. package/dist/types/admin/AdminItemDrawer.d.ts +16 -0
  90. package/dist/types/admin/AdminMenuCard.d.ts +10 -0
  91. package/dist/types/admin/AdminMenuGroupTabs.d.ts +11 -0
  92. package/dist/types/admin/AdminMenuIconPicker.d.ts +8 -0
  93. package/dist/types/admin/AdminRoutes.d.ts +5 -0
  94. package/dist/types/admin/AdminSectionEditor.d.ts +20 -0
  95. package/dist/types/admin/AdminTestimonialsPanel.d.ts +7 -0
  96. package/dist/types/admin/adminDraftContext.d.ts +2 -0
  97. package/dist/types/admin/adminEditorUtils.d.ts +22 -0
  98. package/dist/types/admin/adminMenuCommands.d.ts +73 -0
  99. package/dist/types/admin/useAdminDraft.d.ts +5 -0
  100. package/dist/types/admin/useAdminMenuDraft.d.ts +41 -0
  101. package/dist/types/cart/CartDrawer.d.ts +7 -0
  102. package/dist/types/cart/CartProvider.d.ts +4 -0
  103. package/dist/types/cart/cartContext.d.ts +2 -0
  104. package/dist/types/cart/cartOrder.d.ts +9 -0
  105. package/dist/types/cart/cartReducer.d.ts +36 -0
  106. package/dist/types/cart/cartTypes.d.ts +37 -0
  107. package/dist/types/cart/useCart.d.ts +1 -0
  108. package/dist/types/components/Brand.d.ts +5 -0
  109. package/dist/types/components/Footer.d.ts +1 -0
  110. package/dist/types/components/Header.d.ts +1 -0
  111. package/dist/types/components/Hero.d.ts +1 -0
  112. package/dist/types/components/InstagramIcon.d.ts +6 -0
  113. package/dist/types/components/LocationsSection.d.ts +1 -0
  114. package/dist/types/components/Marquee.d.ts +1 -0
  115. package/dist/types/components/OrderIcon.d.ts +4 -0
  116. package/dist/types/components/RatingSection.d.ts +1 -0
  117. package/dist/types/components/TestimonialsSection.d.ts +2 -0
  118. package/dist/types/components/WhatsAppIcon.d.ts +5 -0
  119. package/dist/types/config/RestaurantKitProvider.d.ts +24 -0
  120. package/dist/types/config/siteConfigContext.d.ts +28 -0
  121. package/dist/types/config/siteTypes.d.ts +50 -0
  122. package/dist/types/config/useOrdering.d.ts +16 -0
  123. package/dist/types/index.d.ts +38 -0
  124. package/dist/types/menu/localMenuApi.d.ts +9 -0
  125. package/dist/types/menu/menuApi.d.ts +68 -0
  126. package/dist/types/menu/menuDraftStorage.d.ts +5 -0
  127. package/dist/types/menu/menuIcons.d.ts +7 -0
  128. package/dist/types/menu/menuMutations.d.ts +13 -0
  129. package/dist/types/menu/menuOrdering.d.ts +38 -0
  130. package/dist/types/menu/menuSchema.d.ts +68 -0
  131. package/dist/types/menu/priceFormat.d.ts +8 -0
  132. package/dist/types/menu/useMenuData.d.ts +12 -0
  133. package/dist/types/menu/validateMenu.d.ts +16 -0
  134. package/dist/types/menu-browser/MenuBrowser.d.ts +6 -0
  135. package/dist/types/menu-browser/MenuCard.d.ts +7 -0
  136. package/dist/types/menu-browser/MenuCartDrawer.d.ts +5 -0
  137. package/dist/types/menu-browser/MenuCategoryBlock.d.ts +11 -0
  138. package/dist/types/menu-browser/MenuItemSheet.d.ts +10 -0
  139. package/dist/types/menu-browser/MenuLightbox.d.ts +9 -0
  140. package/dist/types/menu-browser/MenuRow.d.ts +7 -0
  141. package/dist/types/menu-browser/MenuSection.d.ts +1 -0
  142. package/dist/types/menu-browser/menuItemDisplay.d.ts +5 -0
  143. package/dist/types/menu-browser/useMediaQuery.d.ts +1 -0
  144. package/dist/types/menu-browser/useMenuBrowserState.d.ts +17 -0
  145. package/dist/types/menu-browser/useParkedSheet.d.ts +13 -0
  146. package/dist/types/pages/AdminMenuEditor.d.ts +1 -0
  147. package/dist/types/pages/AdminPreview.d.ts +1 -0
  148. package/dist/types/pages/MenuSite.d.ts +6 -0
  149. package/dist/types/testimonials/downscaleImage.d.ts +6 -0
  150. package/dist/types/testimonials/testimonialMutations.d.ts +9 -0
  151. package/dist/types/utils.d.ts +1 -0
  152. package/dist/types/worker/auth.d.ts +97 -0
  153. package/dist/types/worker/http.d.ts +17 -0
  154. package/dist/types/worker/index.d.ts +16 -0
  155. package/dist/types/worker/menuStorage.d.ts +94 -0
  156. package/dist/utils.js +6 -0
  157. package/dist/worker/auth.js +183 -0
  158. package/dist/worker/http.js +65 -0
  159. package/dist/worker/menuStorage.js +95 -0
  160. package/dist/worker.js +131 -0
  161. package/package.json +105 -0
  162. package/scripts/bootstrap-site.mjs +125 -0
  163. package/scripts/hash-admin-password.mjs +11 -0
  164. package/scripts/lib/password.mjs +72 -0
@@ -0,0 +1,16 @@
1
+ export type ResolvedOrdering = {
2
+ /** False = browse-only menu: no add-to-cart, no cart drawer. */
3
+ readonly cartEnabled: boolean;
4
+ readonly isWhatsApp: boolean;
5
+ /** Where the header / hero / item-sheet order buttons go. Empty when there is nowhere to send people. */
6
+ readonly href: string;
7
+ readonly label: string;
8
+ readonly shortLabel: string;
9
+ /** External mode only: the ordering app's logo. */
10
+ readonly iconSrc?: string;
11
+ };
12
+ /**
13
+ * Resolves `config.ordering`. Reads the context directly instead of `useSiteConfig()` so the
14
+ * menu components stay renderable without a provider (they fall back to the WhatsApp cart).
15
+ */
16
+ export declare function useOrdering(): ResolvedOrdering;
@@ -0,0 +1,38 @@
1
+ import "./styles/global.css";
2
+ export { RestaurantSite } from "./RestaurantSiteRoutes";
3
+ export { RestaurantKitProvider } from "./config/RestaurantKitProvider";
4
+ export { useMenuApi, useRestaurantKit, useSeedMenu, useSiteConfig, type RestaurantKitApiOptions, type RestaurantKitValue } from "./config/siteConfigContext";
5
+ export type { OrderingConfig, RestaurantSiteConfig } from "./config/siteTypes";
6
+ export { useOrdering, type ResolvedOrdering } from "./config/useOrdering";
7
+ export { MenuSite } from "./pages/MenuSite";
8
+ export { AdminMenuEditor } from "./pages/AdminMenuEditor";
9
+ export { AdminPreview } from "./pages/AdminPreview";
10
+ export { Brand } from "./components/Brand";
11
+ export { Footer } from "./components/Footer";
12
+ export { Header } from "./components/Header";
13
+ export { Hero } from "./components/Hero";
14
+ export { LocationsSection } from "./components/LocationsSection";
15
+ export { Marquee } from "./components/Marquee";
16
+ export { RatingSection } from "./components/RatingSection";
17
+ export { TestimonialsSection } from "./components/TestimonialsSection";
18
+ export { InstagramIcon } from "./components/InstagramIcon";
19
+ export { WhatsAppIcon } from "./components/WhatsAppIcon";
20
+ export { MenuBrowser } from "./menu-browser/MenuBrowser";
21
+ export { MenuSection } from "./menu-browser/MenuSection";
22
+ export { MenuCartDrawer } from "./menu-browser/MenuCartDrawer";
23
+ export { CartProvider } from "./cart/CartProvider";
24
+ export { useCart } from "./cart/useCart";
25
+ export { buildWhatsAppMessage, buildWhatsAppOrderUrl, cartSelectionKey, formatVariationName, type CartOrderConfig } from "./cart/cartOrder";
26
+ export type { CartItem, CartSelection, CartState, CartContextValue } from "./cart/cartTypes";
27
+ export { MenuDataProvider, useMenuData, type MenuDataStatus, type MenuDataValue } from "./menu/useMenuData";
28
+ export { createMenuApiClient, MenuApiError, type AdminAuthStrategy, type AdminSession, type MenuApiClient, type MenuBackup, type RestoreDraftSource, type MenuApiClientOptions, type MenuImageUpload, type PublishResult, } from "./menu/menuApi";
29
+ export { createLocalMenuApiClient, shouldUseLocalMenuApi } from "./menu/localMenuApi";
30
+ export { clearMenuDraft, menuDraftStorageKey, readMenuDraft, writeMenuDraft } from "./menu/menuDraftStorage";
31
+ export { menuIcons, menuIconOptions } from "./menu/menuIcons";
32
+ export { formatPrice, formatPriceTotal, priceFromInput, priceInputValue, type PriceCurrency } from "./menu/priceFormat";
33
+ export { menuIconIds, type DynamicMenu, type MenuCategory, type MenuGroup, type MenuIcon, type MenuItem, type MenuSize, type Testimonial } from "./menu/menuSchema";
34
+ export { assertValidMenu, validateMenu, type MenuValidationIssue } from "./menu/validateMenu";
35
+ export { useAdminMenuDraft, type AdminMenuDraft, type UseAdminMenuDraftOptions } from "./admin/useAdminMenuDraft";
36
+ export { AdminDraftProvider } from "./admin/AdminDraftProvider";
37
+ export { useAdminDraft, useAdminDraftContext } from "./admin/useAdminDraft";
38
+ export { applyAdminMenuCommand, getAdminMenuCommandRejection, type AdminMenuCommand, type AdminMenuCommandRejection } from "./admin/adminMenuCommands";
@@ -0,0 +1,9 @@
1
+ import type { MenuApiClient } from "./menuApi";
2
+ import type { DynamicMenu } from "./menuSchema";
3
+ /** Browser-only adapter for localhost: drafts persist to localStorage under `storageKey`. */
4
+ export declare function createLocalMenuApiClient(initialMenu: DynamicMenu, storageKey: string): MenuApiClient;
5
+ /**
6
+ * localhost / 127.0.0.1 → browser-local adapter (no Worker needed), unless an API base URL is
7
+ * configured, which opts the dev server into a real Worker (e.g. `wrangler dev` on :8787).
8
+ */
9
+ export declare function shouldUseLocalMenuApi(apiBaseUrl?: string): boolean;
@@ -0,0 +1,68 @@
1
+ import type { DynamicMenu } from "./menuSchema";
2
+ type Fetcher = typeof fetch;
3
+ export type MenuImageUpload = {
4
+ readonly key: string;
5
+ readonly url: string;
6
+ };
7
+ export type PublishResult = {
8
+ readonly ok: true;
9
+ readonly backupKey: string | null;
10
+ };
11
+ /** "local" is the browser-only dev client; the Worker reports password | access | none. */
12
+ export type AdminAuthStrategy = "password" | "access" | "none" | "local";
13
+ export type AdminSession = {
14
+ readonly authenticated: boolean;
15
+ readonly strategy: AdminAuthStrategy;
16
+ readonly user: string | null;
17
+ };
18
+ export type DraftMenuRecord = {
19
+ readonly menu: DynamicMenu;
20
+ /** Server etag of the draft, or null when unknown (local adapter, published fallback). */
21
+ readonly etag: string | null;
22
+ };
23
+ export type SaveDraftOptions = {
24
+ /** Send the etag from the last load/save; the server answers 412 if the draft changed meanwhile. */
25
+ readonly ifMatch?: string | null;
26
+ };
27
+ export type MenuBackup = {
28
+ readonly key: string;
29
+ readonly publishedAt: string;
30
+ readonly size: number;
31
+ };
32
+ export type RestoreDraftSource = {
33
+ readonly kind: "published";
34
+ } | {
35
+ readonly kind: "backup";
36
+ readonly key: string;
37
+ };
38
+ export type MenuApiClient = {
39
+ readonly getPublishedMenu: () => Promise<DynamicMenu>;
40
+ readonly getDraftMenu: () => Promise<DynamicMenu>;
41
+ readonly getDraft: () => Promise<DraftMenuRecord>;
42
+ readonly getAdminSignInUrl: (returnTo?: string) => string;
43
+ readonly getAdminSession: () => Promise<AdminSession>;
44
+ readonly signInWithPassword: (password: string) => Promise<AdminSession>;
45
+ readonly signOut: () => Promise<void>;
46
+ readonly saveDraftMenu: (menu: DynamicMenu, options?: SaveDraftOptions) => Promise<{
47
+ readonly etag: string | null;
48
+ }>;
49
+ readonly publishDraftMenu: () => Promise<PublishResult>;
50
+ readonly listMenuBackups: () => Promise<readonly MenuBackup[]>;
51
+ /** Replaces the remote draft with the published menu or a backup. Reload the draft afterwards. */
52
+ readonly restoreDraft: (source: RestoreDraftSource) => Promise<{
53
+ readonly etag: string | null;
54
+ }>;
55
+ readonly uploadMenuImage: (file: File) => Promise<MenuImageUpload>;
56
+ readonly deleteMenuImage: (key: string) => Promise<void>;
57
+ };
58
+ export type MenuApiClientOptions = {
59
+ readonly apiBaseUrl?: string;
60
+ readonly publicMenuUrl?: string;
61
+ readonly fetcher?: Fetcher;
62
+ };
63
+ export declare class MenuApiError extends Error {
64
+ readonly status: number;
65
+ constructor(message: string, status: number);
66
+ }
67
+ export declare function createMenuApiClient(options?: MenuApiClientOptions): MenuApiClient;
68
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { DynamicMenu } from "./menuSchema";
2
+ export declare function menuDraftStorageKey(restaurantId: string): string;
3
+ export declare function readMenuDraft(storageKey: string): DynamicMenu | null;
4
+ export declare function writeMenuDraft(menu: DynamicMenu, storageKey: string): void;
5
+ export declare function clearMenuDraft(storageKey: string): void;
@@ -0,0 +1,7 @@
1
+ import { Utensils } from "lucide-react";
2
+ import type { MenuIcon } from "./menuSchema";
3
+ export declare const menuIcons: Record<MenuIcon, typeof Utensils>;
4
+ export declare const menuIconOptions: readonly {
5
+ readonly id: MenuIcon;
6
+ readonly label: string;
7
+ }[];
@@ -0,0 +1,13 @@
1
+ import type { DynamicMenu, MenuCategory, MenuGroup, MenuItem, MenuSize } from "./menuSchema";
2
+ export declare function addMenuGroup(menu: DynamicMenu, group: MenuGroup): DynamicMenu;
3
+ export declare function updateMenuGroup(menu: DynamicMenu, groupId: string, patch: Partial<MenuGroup>): DynamicMenu;
4
+ export declare function deleteMenuGroup(menu: DynamicMenu, groupId: string): DynamicMenu;
5
+ export declare function addMenuCategory(menu: DynamicMenu, category: MenuCategory): DynamicMenu;
6
+ export declare function updateMenuCategory(menu: DynamicMenu, categoryId: string, patch: Partial<MenuCategory>): DynamicMenu;
7
+ export declare function deleteMenuCategory(menu: DynamicMenu, categoryId: string): DynamicMenu;
8
+ export declare function addMenuItem(menu: DynamicMenu, item: MenuItem): DynamicMenu;
9
+ export declare function updateMenuItem(menu: DynamicMenu, itemId: string, patch: Partial<MenuItem>): DynamicMenu;
10
+ export declare function deleteMenuItem(menu: DynamicMenu, itemId: string): DynamicMenu;
11
+ export declare function addMenuSize(menu: DynamicMenu, itemId: string, size: MenuSize): DynamicMenu;
12
+ export declare function updateMenuSize(menu: DynamicMenu, itemId: string, sizeId: string, patch: Partial<MenuSize>): DynamicMenu;
13
+ export declare function deleteMenuSize(menu: DynamicMenu, itemId: string, sizeId: string): DynamicMenu;
@@ -0,0 +1,38 @@
1
+ import type { DynamicMenu, MenuCategory, MenuGroup, MenuItem } from "./menuSchema";
2
+ type OrderedEntity = {
3
+ readonly order: number;
4
+ };
5
+ export type OrderedMenuSection = {
6
+ readonly section: MenuCategory;
7
+ readonly items: readonly MenuItem[];
8
+ };
9
+ export type OrderedMenuGroup = {
10
+ readonly group: MenuGroup;
11
+ readonly sections: readonly OrderedMenuSection[];
12
+ };
13
+ export type OrderedMenu = {
14
+ readonly groups: readonly OrderedMenuGroup[];
15
+ readonly sections: readonly OrderedMenuSection[];
16
+ };
17
+ type OrderedMenuOptions = {
18
+ readonly includeHidden?: boolean;
19
+ readonly includeEmptySections?: boolean;
20
+ };
21
+ export declare function sortByOrder<T extends OrderedEntity>(items: readonly T[]): T[];
22
+ export declare function normalizeOrder<T extends OrderedEntity>(items: readonly T[]): (T & {
23
+ order: number;
24
+ })[];
25
+ /**
26
+ * Re-numbers categories in render order (group order, then section order). Categories whose
27
+ * group no longer resolves are kept at the end untouched, so a dangling `groupId` in seed or
28
+ * remote data is never silently deleted by an unrelated edit.
29
+ */
30
+ export declare function renumberSections(menu: DynamicMenu): DynamicMenu;
31
+ export declare function getOrderedMenu(menu: DynamicMenu, options?: OrderedMenuOptions): OrderedMenu;
32
+ export declare function moveGroup(menu: DynamicMenu, groupId: string, direction: -1 | 1): DynamicMenu;
33
+ export declare function moveGroupBefore(menu: DynamicMenu, groupId: string, targetGroupId: string): DynamicMenu;
34
+ export declare function setGroupOrder(menu: DynamicMenu, orderedGroupIds: readonly string[]): DynamicMenu;
35
+ export declare function moveSection(menu: DynamicMenu, sectionId: string, direction: -1 | 1): DynamicMenu;
36
+ export declare function moveSectionBefore(menu: DynamicMenu, sectionId: string, targetSectionId: string): DynamicMenu;
37
+ export declare function moveItemBefore(menu: DynamicMenu, itemId: string, targetItemId: string): DynamicMenu;
38
+ export {};
@@ -0,0 +1,68 @@
1
+ export declare const menuIconIds: readonly ["plate", "burger", "wrap", "chicken", "platter", "fries", "kids", "drink"];
2
+ export type MenuIcon = (typeof menuIconIds)[number];
3
+ export type MenuGroup = {
4
+ readonly id: string;
5
+ readonly label: string;
6
+ readonly icon: MenuIcon;
7
+ readonly order: number;
8
+ readonly isVisible: boolean;
9
+ };
10
+ export type MenuCategory = {
11
+ readonly id: string;
12
+ readonly groupId: string;
13
+ readonly title: string;
14
+ readonly order: number;
15
+ readonly isVisible: boolean;
16
+ };
17
+ export type MenuSize = {
18
+ readonly id: string;
19
+ readonly label: string;
20
+ readonly price: string;
21
+ readonly order: number;
22
+ };
23
+ export type MenuItem = {
24
+ readonly id: string;
25
+ readonly categoryId: string;
26
+ readonly title: string;
27
+ readonly description: string;
28
+ readonly image: string;
29
+ readonly imageKey?: string;
30
+ readonly order: number;
31
+ readonly isVisible: boolean;
32
+ readonly pricingMode: "single" | "sizes";
33
+ readonly price: string;
34
+ readonly sizes: readonly MenuSize[];
35
+ readonly removableIngredients?: readonly string[];
36
+ };
37
+ /** A customer review the owner copied in (typically from Google Maps), optionally with the customer's photo. */
38
+ export type Testimonial = {
39
+ readonly id: string;
40
+ readonly author: string;
41
+ /** Whole stars, 1–5. */
42
+ readonly rating: number;
43
+ readonly text: string;
44
+ /** Static path, URL, or an uploaded image URL (then `imageKey` is its R2 key). */
45
+ readonly image?: string;
46
+ readonly imageKey?: string;
47
+ /** Link to the original review. */
48
+ readonly sourceUrl?: string;
49
+ readonly order: number;
50
+ readonly isVisible: boolean;
51
+ };
52
+ export type DynamicMenu = {
53
+ readonly schemaVersion: 1;
54
+ readonly updatedAt: string;
55
+ readonly restaurant: {
56
+ readonly id: string;
57
+ readonly name: string;
58
+ readonly tagline: string;
59
+ readonly phone: string;
60
+ readonly whatsapp: string;
61
+ readonly address: string;
62
+ };
63
+ readonly groups: readonly MenuGroup[];
64
+ readonly categories: readonly MenuCategory[];
65
+ readonly items: readonly MenuItem[];
66
+ /** Optional. The public Testimonials section only renders when at least one is visible. */
67
+ readonly testimonials?: readonly Testimonial[];
68
+ };
@@ -0,0 +1,8 @@
1
+ export type PriceCurrency = "USD" | "LBP";
2
+ export declare function priceInputValue(price: string, fallbackCurrency?: PriceCurrency): {
3
+ readonly amount: string;
4
+ readonly currency: PriceCurrency;
5
+ };
6
+ export declare function priceFromInput(amount: string, currency: PriceCurrency): string;
7
+ export declare function formatPrice(price: string): string;
8
+ export declare function formatPriceTotal(price: string, quantity: number): string;
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DynamicMenu } from "./menuSchema";
3
+ export type MenuDataStatus = "local" | "loading" | "remote" | "fallback" | "draft";
4
+ export type MenuDataValue = {
5
+ readonly menu: DynamicMenu;
6
+ readonly status: MenuDataStatus;
7
+ };
8
+ export declare function MenuDataProvider({ children, value }: {
9
+ readonly children: ReactNode;
10
+ readonly value: MenuDataValue;
11
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<MenuDataValue | null>>;
12
+ export declare function useMenuData(): MenuDataValue;
@@ -0,0 +1,16 @@
1
+ import { type DynamicMenu } from "./menuSchema";
2
+ export type MenuValidationIssue = {
3
+ /** Dotted path to the offending value, e.g. `items[3].categoryId`. */
4
+ readonly path: string;
5
+ readonly message: string;
6
+ };
7
+ /**
8
+ * Checks a menu's shape and cross-references. The runtime tolerates orphans (a category whose
9
+ * group is gone is kept at the end, an item whose category is gone is hidden); call this on seed
10
+ * data in a test or build step so those mistakes fail loudly instead.
11
+ *
12
+ * Returns every issue found; an empty array means the menu is valid.
13
+ */
14
+ export declare function validateMenu(value: unknown): MenuValidationIssue[];
15
+ /** Throws with every issue listed when the menu is invalid; narrows `value` otherwise. */
16
+ export declare function assertValidMenu(value: unknown): asserts value is DynamicMenu;
@@ -0,0 +1,6 @@
1
+ import type { DynamicMenu } from "../menu/menuSchema";
2
+ type MenuBrowserProps = {
3
+ menu: DynamicMenu;
4
+ };
5
+ export declare function MenuBrowser({ menu }: MenuBrowserProps): import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { MenuItem } from "../menu/menuSchema";
2
+ type MenuCardProps = {
3
+ item: MenuItem;
4
+ onOpen?: () => void;
5
+ };
6
+ export declare function MenuCard({ item, onOpen }: MenuCardProps): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CartDrawer wired to the menu: each cart line gets an Edit action that
3
+ * opens the item sheet prefilled with the line and replaces it on save.
4
+ */
5
+ export declare function MenuCartDrawer(): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import type { MenuCategory, MenuItem } from "../menu/menuSchema";
2
+ type MenuCategoryBlockProps = {
3
+ category: MenuCategory;
4
+ groupId: string;
5
+ groupLabel: string;
6
+ isOpen: boolean;
7
+ items: readonly MenuItem[];
8
+ onToggle: () => void;
9
+ };
10
+ export declare function MenuCategoryBlock({ category, groupId, groupLabel, isOpen, items, onToggle }: MenuCategoryBlockProps): import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { CartItem } from "../cart/cartTypes";
2
+ import type { MenuItem } from "../menu/menuSchema";
3
+ type MenuItemSheetProps = {
4
+ item: MenuItem;
5
+ /** Cart line to edit: prefills the form and replaces the line on save instead of adding. */
6
+ editLine?: CartItem;
7
+ onClose: () => void;
8
+ };
9
+ export declare function MenuItemSheet({ item, editLine, onClose }: MenuItemSheetProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,9 @@
1
+ type MenuLightboxProps = {
2
+ addLabel?: string;
3
+ alt: string;
4
+ imageSrc: string;
5
+ onAdd?: () => void;
6
+ onClose: () => void;
7
+ };
8
+ export declare function MenuLightbox({ addLabel, alt, imageSrc, onAdd, onClose }: MenuLightboxProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { MenuItem } from "../menu/menuSchema";
2
+ type MenuRowProps = {
3
+ item: MenuItem;
4
+ onOpen: () => void;
5
+ };
6
+ export declare function MenuRow({ item, onOpen }: MenuRowProps): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1 @@
1
+ export declare function MenuSection(): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import type { MenuItem, MenuSize } from "../menu/menuSchema";
2
+ export { formatPrice } from "../menu/priceFormat";
3
+ export declare function variationLabel(size: MenuSize): string;
4
+ export declare function sortedSizes(item: MenuItem): MenuSize[];
5
+ export declare function imageSrc(image: string): string;
@@ -0,0 +1 @@
1
+ export declare function useMediaQuery(query: string): boolean;
@@ -0,0 +1,17 @@
1
+ import type { DynamicMenu } from "../menu/menuSchema";
2
+ export declare function useMenuBrowserState(menu: DynamicMenu): {
3
+ activeGroup: string;
4
+ catalogRef: import("react").RefObject<HTMLDivElement | null>;
5
+ controlStripRef: import("react").RefObject<HTMLDivElement | null>;
6
+ groupLabels: Map<string, string>;
7
+ handleGroupClick: (groupId: string) => void;
8
+ isTabsStuck: boolean;
9
+ openCategories: readonly string[];
10
+ tabsRef: import("react").RefObject<HTMLDivElement | null>;
11
+ toggleCategory: (categoryId: string) => void;
12
+ visibleCategories: {
13
+ category: import("..").MenuCategory;
14
+ items: readonly import("..").MenuItem[];
15
+ }[];
16
+ visibleGroups: import("..").MenuGroup[];
17
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * State for a sheet/dialog that the browser back button should close.
3
+ * Opening parks a history entry; every close routes through history.back()
4
+ * so popping the entry is the single close path (pushing from the open
5
+ * handler, never an effect, keeps StrictMode's double effects harmless).
6
+ * Each parked entry is tagged with a token so only this hook's own entry is
7
+ * ever popped, and unmounting while open pops it instead of leaving it behind.
8
+ */
9
+ export declare function useParkedSheet<T>(onClosed?: () => void): {
10
+ value: T | null;
11
+ open: (next: T) => void;
12
+ close: () => void;
13
+ };
@@ -0,0 +1 @@
1
+ export declare function AdminMenuEditor(): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function AdminPreview(): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { type MenuDataValue } from "../menu/useMenuData";
2
+ type MenuSiteProps = {
3
+ readonly menuData?: MenuDataValue;
4
+ };
5
+ export declare function MenuSite({ menuData }: MenuSiteProps): import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Customer photos come straight off a phone. Shrink them to a web-sized JPEG before upload so they
3
+ * stay far below the Worker's 5 MB limit (and fit localStorage in local-dev mode).
4
+ * Falls back to the original file when the browser cannot decode it.
5
+ */
6
+ export declare function downscaleImage(file: File): Promise<File>;
@@ -0,0 +1,9 @@
1
+ import type { DynamicMenu, Testimonial } from "../menu/menuSchema";
2
+ export declare function clampRating(rating: number): number;
3
+ export declare function sortedTestimonials(menu: DynamicMenu): readonly Testimonial[];
4
+ export declare function visibleTestimonials(menu: DynamicMenu): readonly Testimonial[];
5
+ export declare function createTestimonialDraft(): Testimonial;
6
+ export declare function addTestimonial(menu: DynamicMenu, testimonial: Testimonial): DynamicMenu;
7
+ export declare function updateTestimonial(menu: DynamicMenu, testimonialId: string, patch: Partial<Testimonial>): DynamicMenu;
8
+ export declare function deleteTestimonial(menu: DynamicMenu, testimonialId: string): DynamicMenu;
9
+ export declare function moveTestimonial(menu: DynamicMenu, testimonialId: string, direction: -1 | 1): DynamicMenu;
@@ -0,0 +1 @@
1
+ export declare function cx(...classes: Array<string | false | null | undefined>): string;
@@ -0,0 +1,97 @@
1
+ import { type HttpEnv } from "./http";
2
+ /**
3
+ * Admin authentication for the menu API.
4
+ *
5
+ * Two strategies:
6
+ * - "password": a single owner password (PBKDF2 hash in ADMIN_PASSWORD_HASH) exchanged for
7
+ * an HMAC-signed, HttpOnly session cookie. No Cloudflare Access / Zero Trust required.
8
+ * - "access": legacy Cloudflare Access; trusts the cf-access-authenticated-user-email header
9
+ * against ADMIN_EMAILS. Only safe when Access fronts every route to this Worker.
10
+ *
11
+ * The strategy is picked from env: ADMIN_AUTH wins when set, otherwise ADMIN_PASSWORD_HASH
12
+ * implies "password", otherwise a non-empty ADMIN_EMAILS implies "access", otherwise "none"
13
+ * (every admin request is denied).
14
+ */
15
+ export type AdminAuthStrategy = "password" | "access" | "none";
16
+ export type RateLimiter = {
17
+ limit(options: {
18
+ readonly key: string;
19
+ }): Promise<{
20
+ readonly success: boolean;
21
+ }>;
22
+ };
23
+ export type AuthEnv = HttpEnv & {
24
+ readonly ADMIN_AUTH?: string;
25
+ readonly ADMIN_PASSWORD_HASH?: string;
26
+ readonly ADMIN_SESSION_SECRET?: string;
27
+ readonly ADMIN_SESSION_TTL_HOURS?: string;
28
+ readonly LOGIN_RATE_LIMITER?: RateLimiter;
29
+ };
30
+ export type AdminIdentity = {
31
+ readonly user: string;
32
+ readonly strategy: AdminAuthStrategy;
33
+ };
34
+ export type AdminSessionInfo = {
35
+ readonly authenticated: boolean;
36
+ readonly strategy: AdminAuthStrategy;
37
+ readonly user: string | null;
38
+ };
39
+ export declare const SESSION_COOKIE_NAME = "rk_admin_session";
40
+ export declare const PASSWORD_HASH_ALGORITHM = "pbkdf2-sha256";
41
+ /** Workers Free plan caps PBKDF2 at 100k iterations; keep the default there so login fits the CPU budget. */
42
+ export declare const DEFAULT_PBKDF2_ITERATIONS = 100000;
43
+ export declare function resolveAdminAuthStrategy(env: AuthEnv): AdminAuthStrategy;
44
+ export declare function getAllowedAdminEmails(env: HttpEnv): string[];
45
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
46
+ export declare function base64UrlDecode(value: string): Uint8Array<ArrayBuffer>;
47
+ export declare function timingSafeEqual(left: Uint8Array, right: Uint8Array): boolean;
48
+ /** Produces `pbkdf2-sha256$<iterations>$<salt>$<hash>` for ADMIN_PASSWORD_HASH. */
49
+ export declare function hashPassword(password: string, iterations?: number): Promise<string>;
50
+ export declare function parsePasswordHash(encoded: string): {
51
+ iterations: number;
52
+ salt: Uint8Array<ArrayBuffer>;
53
+ hash: Uint8Array<ArrayBuffer>;
54
+ } | null;
55
+ export declare function verifyPassword(password: string, encodedHash: string): Promise<boolean>;
56
+ type SessionPayload = {
57
+ readonly sub: string;
58
+ readonly iat: number;
59
+ readonly exp: number;
60
+ };
61
+ export declare function createSessionToken(secret: string, subject: string, ttlSeconds: number, now?: number): Promise<string>;
62
+ export declare function verifySessionToken(secret: string, token: string, now?: number): Promise<SessionPayload | null>;
63
+ export declare function readCookie(request: Request, name: string): string | null;
64
+ export declare function buildSessionCookie(request: Request, token: string, maxAgeSeconds: number): string;
65
+ export declare function buildClearedSessionCookie(request: Request): string;
66
+ /**
67
+ * Browsers always attach an Origin header to POST/PUT/DELETE, so a present Origin that is
68
+ * neither the request's own origin nor an allowed one is a cross-site request. Non-browser
69
+ * clients (no Origin) carry no ambient cookie, so they are not a CSRF vector.
70
+ */
71
+ export declare function isTrustedOrigin(request: Request, env: HttpEnv): boolean;
72
+ export declare function getAccessIdentity(request: Request, env: HttpEnv): AdminIdentity | null;
73
+ /**
74
+ * Session signing key = session secret + current password hash. Rotating either one
75
+ * invalidates every outstanding session, so a password change is also a global sign-out.
76
+ */
77
+ export declare function getSessionSigningKey(env: AuthEnv): string | null;
78
+ export declare function getPasswordSessionIdentity(request: Request, env: AuthEnv): Promise<AdminIdentity | null>;
79
+ export declare function getAdminIdentity(request: Request, env: AuthEnv): Promise<AdminIdentity | null>;
80
+ export declare function getAdminSessionInfo(request: Request, env: AuthEnv): Promise<AdminSessionInfo>;
81
+ export type LoginResult = {
82
+ readonly status: "ok";
83
+ readonly user: string;
84
+ readonly setCookie: string;
85
+ } | {
86
+ readonly status: "wrong-password";
87
+ } | {
88
+ readonly status: "rate-limited";
89
+ } | {
90
+ readonly status: "bad-request";
91
+ readonly message: string;
92
+ } | {
93
+ readonly status: "misconfigured";
94
+ readonly message: string;
95
+ };
96
+ export declare function loginWithPassword(request: Request, env: AuthEnv): Promise<LoginResult>;
97
+ export {};
@@ -0,0 +1,17 @@
1
+ export type HttpEnv = {
2
+ /** Comma-separated Cloudflare Access emails. Only used by the "access" auth strategy. */
3
+ readonly ADMIN_EMAILS?: string;
4
+ readonly ALLOWED_ORIGINS: string;
5
+ };
6
+ /** Error carrying an HTTP status so the top-level handler can answer 4xx instead of 500. */
7
+ export declare class RequestError extends Error {
8
+ readonly status: number;
9
+ constructor(message: string, status: number);
10
+ }
11
+ export declare function getAllowedOrigins(env: HttpEnv): string[];
12
+ export declare function getCorsHeaders(request: Request, env: HttpEnv): Record<string, string>;
13
+ export declare function getSafeReturnTo(request: Request, env: HttpEnv): string;
14
+ export declare function redirectResponse(location: string, corsHeaders: Record<string, string>): Response;
15
+ export declare function jsonResponse(body: unknown, init?: ResponseInit, corsHeaders?: Record<string, string>): Response;
16
+ /** Cloudflare Access header check. Kept for the "access" strategy; see auth.ts for the full picture. */
17
+ export declare function isAdminRequest(request: Request, env: HttpEnv): boolean;
@@ -0,0 +1,16 @@
1
+ import { type AuthEnv } from "./auth";
2
+ import { type R2Bucket } from "./menuStorage";
3
+ type AssetsBinding = {
4
+ fetch(request: Request): Promise<Response>;
5
+ };
6
+ export type Env = AuthEnv & {
7
+ readonly MENU_BUCKET: R2Bucket;
8
+ readonly RESTAURANT_ID: string;
9
+ /** Present when the site is deployed as one Worker with static assets (`[assets]` in wrangler.toml). */
10
+ readonly ASSETS?: AssetsBinding;
11
+ };
12
+ declare const _default: {
13
+ fetch(request: Request, env: Env): Promise<Response>;
14
+ };
15
+ export default _default;
16
+ export { hashPassword, verifyPassword, resolveAdminAuthStrategy, type AdminAuthStrategy, type AuthEnv } from "./auth";