@zappie/kit 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @zappie/kit
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 576a319: The cart button's accessible name now starts with its visible text. It used to be "Open cart, 2 items" while the button showed "Cart" and the subtotal, so people who use voice control couldn't say what they saw. The name is now the visible text followed by a visually hidden count, for example "Cart ₦4,500, 2 items". The `cart-button` registry item is 1.1.1. If your tests find the button by the name "Open cart", find it by "Cart" instead. A cart button you built yourself needs an accessible name that includes its visible text to pass `zappie verify`.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 488e47d: A delivery checkout keeps its region and the rider's note.
14
+
15
+ - `@zappie/sdk`: `createCheckout().open()` sends the delivery note as `fulfilment.instructions`, the field the API reads. Before, it was sent inside the address, where the API ignored it, so the rider never saw the note.
16
+ - `@zappie/kit` and the starter: the checkout button's request now carries `delivery_region_id`, so a chosen or pre-filled delivery region reaches the payment session. The `checkout-button` registry item is 1.0.2.
17
+
3
18
  ## 0.1.3
4
19
 
5
20
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":"2026-10-10","items":[{"name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses."},{"name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map."},{"name":"cart-button","type":"component","version":"1.0.0","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal."},{"name":"cart-drawer","type":"component","version":"1.0.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer."},{"name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line."},{"name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling."},{"name":"checkout-button","type":"component","version":"1.0.1","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution."},{"name":"checkout-page","type":"block","version":"1.0.0","title":"Checkout page","description":"Collect contact and delivery details, resolve cart issues and continue to payment."},{"name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order."},{"name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose."},{"name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options."},{"name":"item-card","type":"component","version":"1.0.0","title":"Item card","description":"Show an item, its price, availability and cart feedback."},{"name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation."},{"name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework."},{"name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles."},{"name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes."},{"name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links."},{"name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals."},{"name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders."},{"name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts."},{"name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits."},{"name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart."},{"name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results."},{"name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls."},{"name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links."},{"name":"store-header","type":"component","version":"1.0.0","title":"Store header","description":"Show store details, opening status and fulfilment."}]}
1
+ {"version":"2026-10-10","items":[{"name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses."},{"name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map."},{"name":"cart-button","type":"component","version":"1.1.1","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal."},{"name":"cart-drawer","type":"component","version":"1.1.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer."},{"name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line."},{"name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling."},{"name":"checkout-button","type":"component","version":"1.0.2","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution."},{"name":"checkout-page","type":"block","version":"1.0.0","title":"Checkout page","description":"Collect contact and delivery details, resolve cart issues and continue to payment."},{"name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order."},{"name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose."},{"name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options."},{"name":"item-card","type":"component","version":"1.1.0","title":"Item card","description":"Show an item, its price, availability and cart feedback."},{"name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation."},{"name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework."},{"name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles."},{"name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes."},{"name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links."},{"name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals."},{"name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders."},{"name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts."},{"name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits."},{"name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart."},{"name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results."},{"name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls."},{"name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links."},{"name":"store-header","type":"component","version":"1.1.0","title":"Store header","description":"Show store details, opening status and fulfilment."}]}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-button","type":"component","version":"1.0.0","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money"],"files":[{"path":"cart-button.tsx","target":"components/zappie/cart-button.tsx","sha256":"082c691b672dc1c6e4838980a3d6d62b8cb7d8320ad061862bf02288b33dadd7","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { ShoppingBag } from \"lucide-react\";\nimport \"./cart-button.css\";\n\nexport type CartButtonProps = {\n onOpen: () => void;\n /** Called when the shopper reaches for the button, to start loading the cart's code. */\n onIntent?: () => void;\n variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({\n onOpen,\n onIntent,\n variant = \"floating\",\n}: CartButtonProps) {\n const cart = useCart();\n return (\n <button\n type=\"button\"\n className=\"z-kit-cart-button\"\n data-variant={variant}\n data-zappie-cart-trigger=\"\"\n data-zappie-overlay-trigger=\"bag\"\n aria-label={`Open cart, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n disabled={!cart.isHydrated}\n onClick={onOpen}\n onPointerEnter={onIntent}\n onPointerDown={onIntent}\n onFocus={onIntent}\n >\n <ShoppingBag aria-hidden=\"true\" />\n <span>Cart</span>\n <span aria-hidden=\"true\">{cart.count}</span>\n {cart.subtotal && <Money value={cart.subtotal} />}\n </button>\n );\n}\n"},{"path":"cart-button.css","target":"components/zappie/cart-button.css","sha256":"0e6879dbab4158f6fd1979696663cdc7c80621867a90acf8a4040987bf3e9216","content":".z-kit-cart-button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-cart-button[data-variant=\"floating\"] {\n position: fixed;\n right: var(--z-space-4);\n bottom: var(--z-space-4);\n border: 1px solid var(--z-border);\n}\n\n.z-kit-cart-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-button svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-button"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-button","type":"component","version":"1.1.1","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money"],"files":[{"path":"cart-button.tsx","target":"components/zappie/cart-button.tsx","sha256":"c8e0c73c1383d39637a00fa67c05b96ab99ffe8bac78b2677fc9671b0d47fd4e","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { ShoppingBag } from \"lucide-react\";\nimport \"./cart-button.css\";\n\nexport type CartButtonProps = {\n onOpen: () => void;\n /** Called when the shopper reaches for the button, to start loading the cart's code. */\n onIntent?: () => void;\n variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({\n onOpen,\n onIntent,\n variant = \"floating\",\n}: CartButtonProps) {\n const cart = useCart();\n return (\n <button\n type=\"button\"\n className=\"z-kit-cart-button\"\n data-variant={variant}\n data-zappie-cart-trigger=\"\"\n data-zappie-overlay-trigger=\"bag\"\n disabled={!cart.isHydrated}\n onClick={onOpen}\n onPointerEnter={onIntent}\n onPointerDown={onIntent}\n onFocus={onIntent}\n >\n <ShoppingBag aria-hidden=\"true\" />\n <span>Cart</span>\n <span aria-hidden=\"true\">{cart.count}</span>\n {cart.subtotal && <Money value={cart.subtotal} />}\n {/* The accessible name starts with the visible label, so voice control matches it (WCAG 2.5.3). */}\n <span className=\"z-kit-cart-button__hidden\">\n {`, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n </span>\n </button>\n );\n}\n"},{"path":"cart-button.css","target":"components/zappie/cart-button.css","sha256":"1ee35ea1b85c7d96c5901885822749068123bf96a088f210c86c1e76c197320a","content":".z-kit-cart-button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-cart-button[data-variant=\"floating\"] {\n position: fixed;\n right: var(--z-space-4);\n bottom: var(--z-space-4);\n border: 1px solid var(--z-border);\n}\n\n.z-kit-cart-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-button svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n\n.z-kit-cart-button__hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-button"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-button","type":"component","version":"1.1.1","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money"],"files":[{"path":"cart-button.tsx","target":"components/zappie/cart-button.tsx","sha256":"c8e0c73c1383d39637a00fa67c05b96ab99ffe8bac78b2677fc9671b0d47fd4e","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { ShoppingBag } from \"lucide-react\";\nimport \"./cart-button.css\";\n\nexport type CartButtonProps = {\n onOpen: () => void;\n /** Called when the shopper reaches for the button, to start loading the cart's code. */\n onIntent?: () => void;\n variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({\n onOpen,\n onIntent,\n variant = \"floating\",\n}: CartButtonProps) {\n const cart = useCart();\n return (\n <button\n type=\"button\"\n className=\"z-kit-cart-button\"\n data-variant={variant}\n data-zappie-cart-trigger=\"\"\n data-zappie-overlay-trigger=\"bag\"\n disabled={!cart.isHydrated}\n onClick={onOpen}\n onPointerEnter={onIntent}\n onPointerDown={onIntent}\n onFocus={onIntent}\n >\n <ShoppingBag aria-hidden=\"true\" />\n <span>Cart</span>\n <span aria-hidden=\"true\">{cart.count}</span>\n {cart.subtotal && <Money value={cart.subtotal} />}\n {/* The accessible name starts with the visible label, so voice control matches it (WCAG 2.5.3). */}\n <span className=\"z-kit-cart-button__hidden\">\n {`, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n </span>\n </button>\n );\n}\n"},{"path":"cart-button.css","target":"components/zappie/cart-button.css","sha256":"1ee35ea1b85c7d96c5901885822749068123bf96a088f210c86c1e76c197320a","content":".z-kit-cart-button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-cart-button[data-variant=\"floating\"] {\n position: fixed;\n right: var(--z-space-4);\n bottom: var(--z-space-4);\n border: 1px solid var(--z-border);\n}\n\n.z-kit-cart-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-button svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n\n.z-kit-cart-button__hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-button"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-drawer","type":"component","version":"1.0.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity","cart-line"],"files":[{"path":"cart-drawer.tsx","target":"components/zappie/cart-drawer.tsx","sha256":"30c5a562025ad95f1863531260e9156559d1f6df24ac60f8d5b92b9f65ce0ab1","content":"\"use client\";\n\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Cart, Overlay, Totals } from \"@zappie/foundations\";\nimport type { CartFix, CartIssue } from \"@zappie/sdk\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport {\n Fragment,\n useEffect,\n useId,\n useMemo,\n useRef,\n type ReactNode,\n} from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /**\n * Where the Checkout link goes, built with `buildStorefrontUrl()` from `@zappie/sdk/url`.\n * The drawer shows no Checkout link when this is not set.\n */\n checkoutHref?: string;\n};\n\nexport function CartDrawer({\n open,\n onOpenChange,\n checkoutHref,\n}: CartDrawerProps) {\n const countedOpen = useRef(false);\n useEffect(() => {\n if (open && !countedOpen.current) {\n countEvent(\"bag\");\n }\n countedOpen.current = open;\n }, [open]);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n return (\n <Overlay.Root\n param=\"cart\"\n id=\"drawer\"\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-cart-drawer\" data-zappie-overlay=\"bag\">\n <header>\n <Overlay.Title>Your cart</Overlay.Title>\n <Overlay.Close aria-label=\"Close cart\">×</Overlay.Close>\n </header>\n <Cart.Root className=\"z-kit-cart-drawer__body\">\n {(cart) => (\n <Fragment>\n <Cart.Empty>Your cart is empty.</Cart.Empty>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n <Cart.Issues>\n {({ issues, applyFix }) => (\n <CartIssueList issues={issues} onFix={applyFix} />\n )}\n </Cart.Issues>\n {cart.lines.length > 0 && (\n <CartTotals>\n {checkoutHref && (\n <CartCheckoutLink\n href={checkoutHref}\n blocked={cart.issues.some(\n (issue) => issue.severity === \"blocking\",\n )}\n />\n )}\n </CartTotals>\n )}\n </Fragment>\n )}\n </Cart.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\n// Only fixes the SDK applies itself; the others need an editor the drawer doesn't have.\nconst fixLabels: Partial<Record<CartFix[\"action\"], string>> = {\n remove_line: \"Remove item\",\n accept_price: \"Accept new price\",\n remove_promo: \"Remove promo\",\n set_quantity: \"Update quantity\",\n};\n\nexport function CartIssueList({\n issues,\n onFix,\n}: {\n issues: readonly CartIssue[];\n onFix: (issue: CartIssue, fix: CartFix) => void;\n}) {\n if (issues.length === 0) {\n return null;\n }\n return (\n <div className=\"z-kit-cart-drawer__issues\" role=\"alert\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.code}:${issue.lineId ?? \"cart\"}:${index}`}\n data-zappie-issue={issue.code}\n >\n <p>{issue.message}</p>\n {issue.fixes\n .filter((fix) => fixLabels[fix.action])\n .map((fix, fixIndex) => (\n <button\n key={`${fix.action}:${fixIndex}`}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => onFix(issue, fix)}\n >\n {fix.action === \"set_quantity\"\n ? `Set quantity to ${fix.quantity}`\n : fixLabels[fix.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * The link from the cart to checkout. While a blocking issue is open it is a disabled button that says why.\n */\nexport function CartCheckoutLink({\n href,\n blocked,\n}: {\n href: string;\n blocked: boolean;\n}) {\n const reason = useId();\n if (blocked) {\n return (\n <Fragment>\n <button\n type=\"button\"\n className=\"z-kit-cart-checkout\"\n disabled\n aria-describedby={reason}\n data-zappie-cart-checkout=\"\"\n >\n Checkout\n </button>\n <p id={reason} className=\"z-kit-cart-checkout__reason\">\n Fix the issues in your cart to check out.\n </p>\n </Fragment>\n );\n }\n return (\n <a className=\"z-kit-cart-checkout\" href={href} data-zappie-cart-checkout=\"\">\n Checkout\n </a>\n );\n}\n\nfunction CartTotals({ children }: { children?: ReactNode }) {\n return (\n <Totals className=\"z-kit-cart-drawer__totals\">\n {({ quote, status }) => (\n <div>\n {status === \"loading\" && <p role=\"status\">Updating total…</p>}\n {(status === \"stale\" || status === \"offline\") && (\n <p role=\"alert\">Total unavailable. Try again.</p>\n )}\n {quote && (\n <p data-zappie-total=\"\">\n <strong>Total</strong> <Money value={quote.total} />\n </p>\n )}\n {children}\n </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"cd93c005860b4cc6008c771fd5bd859b5b8fb5ff26801d41a30c1bac609e02ce","content":".z-kit-cart-drawer {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n width: 100%;\n max-height: 90vh;\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-drawer > header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n border-bottom: 1px solid var(--z-border);\n}\n\n.z-kit-cart-drawer__body {\n overflow-y: auto;\n padding: 0 var(--z-space-4) var(--z-space-4);\n}\n\n.z-kit-cart-drawer__issues {\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n border-inline-start: 3px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n}\n\n.z-kit-cart-drawer__totals {\n position: sticky;\n bottom: 0;\n display: block;\n padding: var(--z-space-4) 0;\n background: var(--z-surface);\n}\n\n.z-kit-cart-drawer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n@media (min-width: 64rem) {\n .z-kit-cart-drawer {\n width: min(var(--z-drawer-width), 100vw);\n height: 100vh;\n max-height: none;\n margin-left: auto;\n }\n}\n\n.z-kit-cart-checkout {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n border: 0;\n text-decoration: none;\n cursor: pointer;\n}\n\n.z-kit-cart-checkout:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-checkout:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-checkout__reason {\n margin: var(--z-space-2) 0 0;\n color: var(--z-text-muted);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body","--z-drawer-width","--z-space-2","--z-space-5","--z-text-md","--z-text-muted","--z-accent","--z-accent-contrast"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-drawer"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-drawer","type":"component","version":"1.1.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity","cart-line"],"files":[{"path":"cart-drawer.tsx","target":"components/zappie/cart-drawer.tsx","sha256":"30c5a562025ad95f1863531260e9156559d1f6df24ac60f8d5b92b9f65ce0ab1","content":"\"use client\";\n\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Cart, Overlay, Totals } from \"@zappie/foundations\";\nimport type { CartFix, CartIssue } from \"@zappie/sdk\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport {\n Fragment,\n useEffect,\n useId,\n useMemo,\n useRef,\n type ReactNode,\n} from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /**\n * Where the Checkout link goes, built with `buildStorefrontUrl()` from `@zappie/sdk/url`.\n * The drawer shows no Checkout link when this is not set.\n */\n checkoutHref?: string;\n};\n\nexport function CartDrawer({\n open,\n onOpenChange,\n checkoutHref,\n}: CartDrawerProps) {\n const countedOpen = useRef(false);\n useEffect(() => {\n if (open && !countedOpen.current) {\n countEvent(\"bag\");\n }\n countedOpen.current = open;\n }, [open]);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n return (\n <Overlay.Root\n param=\"cart\"\n id=\"drawer\"\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-cart-drawer\" data-zappie-overlay=\"bag\">\n <header>\n <Overlay.Title>Your cart</Overlay.Title>\n <Overlay.Close aria-label=\"Close cart\">×</Overlay.Close>\n </header>\n <Cart.Root className=\"z-kit-cart-drawer__body\">\n {(cart) => (\n <Fragment>\n <Cart.Empty>Your cart is empty.</Cart.Empty>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n <Cart.Issues>\n {({ issues, applyFix }) => (\n <CartIssueList issues={issues} onFix={applyFix} />\n )}\n </Cart.Issues>\n {cart.lines.length > 0 && (\n <CartTotals>\n {checkoutHref && (\n <CartCheckoutLink\n href={checkoutHref}\n blocked={cart.issues.some(\n (issue) => issue.severity === \"blocking\",\n )}\n />\n )}\n </CartTotals>\n )}\n </Fragment>\n )}\n </Cart.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\n// Only fixes the SDK applies itself; the others need an editor the drawer doesn't have.\nconst fixLabels: Partial<Record<CartFix[\"action\"], string>> = {\n remove_line: \"Remove item\",\n accept_price: \"Accept new price\",\n remove_promo: \"Remove promo\",\n set_quantity: \"Update quantity\",\n};\n\nexport function CartIssueList({\n issues,\n onFix,\n}: {\n issues: readonly CartIssue[];\n onFix: (issue: CartIssue, fix: CartFix) => void;\n}) {\n if (issues.length === 0) {\n return null;\n }\n return (\n <div className=\"z-kit-cart-drawer__issues\" role=\"alert\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.code}:${issue.lineId ?? \"cart\"}:${index}`}\n data-zappie-issue={issue.code}\n >\n <p>{issue.message}</p>\n {issue.fixes\n .filter((fix) => fixLabels[fix.action])\n .map((fix, fixIndex) => (\n <button\n key={`${fix.action}:${fixIndex}`}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => onFix(issue, fix)}\n >\n {fix.action === \"set_quantity\"\n ? `Set quantity to ${fix.quantity}`\n : fixLabels[fix.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * The link from the cart to checkout. While a blocking issue is open it is a disabled button that says why.\n */\nexport function CartCheckoutLink({\n href,\n blocked,\n}: {\n href: string;\n blocked: boolean;\n}) {\n const reason = useId();\n if (blocked) {\n return (\n <Fragment>\n <button\n type=\"button\"\n className=\"z-kit-cart-checkout\"\n disabled\n aria-describedby={reason}\n data-zappie-cart-checkout=\"\"\n >\n Checkout\n </button>\n <p id={reason} className=\"z-kit-cart-checkout__reason\">\n Fix the issues in your cart to check out.\n </p>\n </Fragment>\n );\n }\n return (\n <a className=\"z-kit-cart-checkout\" href={href} data-zappie-cart-checkout=\"\">\n Checkout\n </a>\n );\n}\n\nfunction CartTotals({ children }: { children?: ReactNode }) {\n return (\n <Totals className=\"z-kit-cart-drawer__totals\">\n {({ quote, status }) => (\n <div>\n {status === \"loading\" && <p role=\"status\">Updating total…</p>}\n {(status === \"stale\" || status === \"offline\") && (\n <p role=\"alert\">Total unavailable. Try again.</p>\n )}\n {quote && (\n <p data-zappie-total=\"\">\n <strong>Total</strong> <Money value={quote.total} />\n </p>\n )}\n {children}\n </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"cd93c005860b4cc6008c771fd5bd859b5b8fb5ff26801d41a30c1bac609e02ce","content":".z-kit-cart-drawer {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n width: 100%;\n max-height: 90vh;\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-drawer > header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n border-bottom: 1px solid var(--z-border);\n}\n\n.z-kit-cart-drawer__body {\n overflow-y: auto;\n padding: 0 var(--z-space-4) var(--z-space-4);\n}\n\n.z-kit-cart-drawer__issues {\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n border-inline-start: 3px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n}\n\n.z-kit-cart-drawer__totals {\n position: sticky;\n bottom: 0;\n display: block;\n padding: var(--z-space-4) 0;\n background: var(--z-surface);\n}\n\n.z-kit-cart-drawer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n@media (min-width: 64rem) {\n .z-kit-cart-drawer {\n width: min(var(--z-drawer-width), 100vw);\n height: 100vh;\n max-height: none;\n margin-left: auto;\n }\n}\n\n.z-kit-cart-checkout {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n border: 0;\n text-decoration: none;\n cursor: pointer;\n}\n\n.z-kit-cart-checkout:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-checkout:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-checkout__reason {\n margin: var(--z-space-2) 0 0;\n color: var(--z-text-muted);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body","--z-drawer-width","--z-space-2","--z-space-5","--z-text-md","--z-text-muted","--z-accent","--z-accent-contrast"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-drawer"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-drawer","type":"component","version":"1.0.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity","cart-line"],"files":[{"path":"cart-drawer.tsx","target":"components/zappie/cart-drawer.tsx","sha256":"30c5a562025ad95f1863531260e9156559d1f6df24ac60f8d5b92b9f65ce0ab1","content":"\"use client\";\n\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Cart, Overlay, Totals } from \"@zappie/foundations\";\nimport type { CartFix, CartIssue } from \"@zappie/sdk\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport {\n Fragment,\n useEffect,\n useId,\n useMemo,\n useRef,\n type ReactNode,\n} from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /**\n * Where the Checkout link goes, built with `buildStorefrontUrl()` from `@zappie/sdk/url`.\n * The drawer shows no Checkout link when this is not set.\n */\n checkoutHref?: string;\n};\n\nexport function CartDrawer({\n open,\n onOpenChange,\n checkoutHref,\n}: CartDrawerProps) {\n const countedOpen = useRef(false);\n useEffect(() => {\n if (open && !countedOpen.current) {\n countEvent(\"bag\");\n }\n countedOpen.current = open;\n }, [open]);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n return (\n <Overlay.Root\n param=\"cart\"\n id=\"drawer\"\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-cart-drawer\" data-zappie-overlay=\"bag\">\n <header>\n <Overlay.Title>Your cart</Overlay.Title>\n <Overlay.Close aria-label=\"Close cart\">×</Overlay.Close>\n </header>\n <Cart.Root className=\"z-kit-cart-drawer__body\">\n {(cart) => (\n <Fragment>\n <Cart.Empty>Your cart is empty.</Cart.Empty>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n <Cart.Issues>\n {({ issues, applyFix }) => (\n <CartIssueList issues={issues} onFix={applyFix} />\n )}\n </Cart.Issues>\n {cart.lines.length > 0 && (\n <CartTotals>\n {checkoutHref && (\n <CartCheckoutLink\n href={checkoutHref}\n blocked={cart.issues.some(\n (issue) => issue.severity === \"blocking\",\n )}\n />\n )}\n </CartTotals>\n )}\n </Fragment>\n )}\n </Cart.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\n// Only fixes the SDK applies itself; the others need an editor the drawer doesn't have.\nconst fixLabels: Partial<Record<CartFix[\"action\"], string>> = {\n remove_line: \"Remove item\",\n accept_price: \"Accept new price\",\n remove_promo: \"Remove promo\",\n set_quantity: \"Update quantity\",\n};\n\nexport function CartIssueList({\n issues,\n onFix,\n}: {\n issues: readonly CartIssue[];\n onFix: (issue: CartIssue, fix: CartFix) => void;\n}) {\n if (issues.length === 0) {\n return null;\n }\n return (\n <div className=\"z-kit-cart-drawer__issues\" role=\"alert\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.code}:${issue.lineId ?? \"cart\"}:${index}`}\n data-zappie-issue={issue.code}\n >\n <p>{issue.message}</p>\n {issue.fixes\n .filter((fix) => fixLabels[fix.action])\n .map((fix, fixIndex) => (\n <button\n key={`${fix.action}:${fixIndex}`}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => onFix(issue, fix)}\n >\n {fix.action === \"set_quantity\"\n ? `Set quantity to ${fix.quantity}`\n : fixLabels[fix.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * The link from the cart to checkout. While a blocking issue is open it is a disabled button that says why.\n */\nexport function CartCheckoutLink({\n href,\n blocked,\n}: {\n href: string;\n blocked: boolean;\n}) {\n const reason = useId();\n if (blocked) {\n return (\n <Fragment>\n <button\n type=\"button\"\n className=\"z-kit-cart-checkout\"\n disabled\n aria-describedby={reason}\n data-zappie-cart-checkout=\"\"\n >\n Checkout\n </button>\n <p id={reason} className=\"z-kit-cart-checkout__reason\">\n Fix the issues in your cart to check out.\n </p>\n </Fragment>\n );\n }\n return (\n <a className=\"z-kit-cart-checkout\" href={href} data-zappie-cart-checkout=\"\">\n Checkout\n </a>\n );\n}\n\nfunction CartTotals({ children }: { children?: ReactNode }) {\n return (\n <Totals className=\"z-kit-cart-drawer__totals\">\n {({ quote, status }) => (\n <div>\n {status === \"loading\" && <p role=\"status\">Updating total…</p>}\n {(status === \"stale\" || status === \"offline\") && (\n <p role=\"alert\">Total unavailable. Try again.</p>\n )}\n {quote && (\n <p data-zappie-total=\"\">\n <strong>Total</strong> <Money value={quote.total} />\n </p>\n )}\n {children}\n </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"cd93c005860b4cc6008c771fd5bd859b5b8fb5ff26801d41a30c1bac609e02ce","content":".z-kit-cart-drawer {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n width: 100%;\n max-height: 90vh;\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-drawer > header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n border-bottom: 1px solid var(--z-border);\n}\n\n.z-kit-cart-drawer__body {\n overflow-y: auto;\n padding: 0 var(--z-space-4) var(--z-space-4);\n}\n\n.z-kit-cart-drawer__issues {\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n border-inline-start: 3px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n}\n\n.z-kit-cart-drawer__totals {\n position: sticky;\n bottom: 0;\n display: block;\n padding: var(--z-space-4) 0;\n background: var(--z-surface);\n}\n\n.z-kit-cart-drawer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n@media (min-width: 64rem) {\n .z-kit-cart-drawer {\n width: min(var(--z-drawer-width), 100vw);\n height: 100vh;\n max-height: none;\n margin-left: auto;\n }\n}\n\n.z-kit-cart-checkout {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n border: 0;\n text-decoration: none;\n cursor: pointer;\n}\n\n.z-kit-cart-checkout:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-checkout:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-checkout__reason {\n margin: var(--z-space-2) 0 0;\n color: var(--z-text-muted);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body","--z-drawer-width","--z-space-2","--z-space-5","--z-text-md","--z-text-muted","--z-accent","--z-accent-contrast"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-drawer"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-drawer","type":"component","version":"1.1.0","title":"Cart drawer","description":"Show cart lines, issues and totals in a responsive drawer.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity","cart-line"],"files":[{"path":"cart-drawer.tsx","target":"components/zappie/cart-drawer.tsx","sha256":"30c5a562025ad95f1863531260e9156559d1f6df24ac60f8d5b92b9f65ce0ab1","content":"\"use client\";\n\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Cart, Overlay, Totals } from \"@zappie/foundations\";\nimport type { CartFix, CartIssue } from \"@zappie/sdk\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport {\n Fragment,\n useEffect,\n useId,\n useMemo,\n useRef,\n type ReactNode,\n} from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /**\n * Where the Checkout link goes, built with `buildStorefrontUrl()` from `@zappie/sdk/url`.\n * The drawer shows no Checkout link when this is not set.\n */\n checkoutHref?: string;\n};\n\nexport function CartDrawer({\n open,\n onOpenChange,\n checkoutHref,\n}: CartDrawerProps) {\n const countedOpen = useRef(false);\n useEffect(() => {\n if (open && !countedOpen.current) {\n countEvent(\"bag\");\n }\n countedOpen.current = open;\n }, [open]);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n return (\n <Overlay.Root\n param=\"cart\"\n id=\"drawer\"\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-cart-drawer\" data-zappie-overlay=\"bag\">\n <header>\n <Overlay.Title>Your cart</Overlay.Title>\n <Overlay.Close aria-label=\"Close cart\">×</Overlay.Close>\n </header>\n <Cart.Root className=\"z-kit-cart-drawer__body\">\n {(cart) => (\n <Fragment>\n <Cart.Empty>Your cart is empty.</Cart.Empty>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n <Cart.Issues>\n {({ issues, applyFix }) => (\n <CartIssueList issues={issues} onFix={applyFix} />\n )}\n </Cart.Issues>\n {cart.lines.length > 0 && (\n <CartTotals>\n {checkoutHref && (\n <CartCheckoutLink\n href={checkoutHref}\n blocked={cart.issues.some(\n (issue) => issue.severity === \"blocking\",\n )}\n />\n )}\n </CartTotals>\n )}\n </Fragment>\n )}\n </Cart.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\n// Only fixes the SDK applies itself; the others need an editor the drawer doesn't have.\nconst fixLabels: Partial<Record<CartFix[\"action\"], string>> = {\n remove_line: \"Remove item\",\n accept_price: \"Accept new price\",\n remove_promo: \"Remove promo\",\n set_quantity: \"Update quantity\",\n};\n\nexport function CartIssueList({\n issues,\n onFix,\n}: {\n issues: readonly CartIssue[];\n onFix: (issue: CartIssue, fix: CartFix) => void;\n}) {\n if (issues.length === 0) {\n return null;\n }\n return (\n <div className=\"z-kit-cart-drawer__issues\" role=\"alert\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.code}:${issue.lineId ?? \"cart\"}:${index}`}\n data-zappie-issue={issue.code}\n >\n <p>{issue.message}</p>\n {issue.fixes\n .filter((fix) => fixLabels[fix.action])\n .map((fix, fixIndex) => (\n <button\n key={`${fix.action}:${fixIndex}`}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => onFix(issue, fix)}\n >\n {fix.action === \"set_quantity\"\n ? `Set quantity to ${fix.quantity}`\n : fixLabels[fix.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * The link from the cart to checkout. While a blocking issue is open it is a disabled button that says why.\n */\nexport function CartCheckoutLink({\n href,\n blocked,\n}: {\n href: string;\n blocked: boolean;\n}) {\n const reason = useId();\n if (blocked) {\n return (\n <Fragment>\n <button\n type=\"button\"\n className=\"z-kit-cart-checkout\"\n disabled\n aria-describedby={reason}\n data-zappie-cart-checkout=\"\"\n >\n Checkout\n </button>\n <p id={reason} className=\"z-kit-cart-checkout__reason\">\n Fix the issues in your cart to check out.\n </p>\n </Fragment>\n );\n }\n return (\n <a className=\"z-kit-cart-checkout\" href={href} data-zappie-cart-checkout=\"\">\n Checkout\n </a>\n );\n}\n\nfunction CartTotals({ children }: { children?: ReactNode }) {\n return (\n <Totals className=\"z-kit-cart-drawer__totals\">\n {({ quote, status }) => (\n <div>\n {status === \"loading\" && <p role=\"status\">Updating total…</p>}\n {(status === \"stale\" || status === \"offline\") && (\n <p role=\"alert\">Total unavailable. Try again.</p>\n )}\n {quote && (\n <p data-zappie-total=\"\">\n <strong>Total</strong> <Money value={quote.total} />\n </p>\n )}\n {children}\n </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"cd93c005860b4cc6008c771fd5bd859b5b8fb5ff26801d41a30c1bac609e02ce","content":".z-kit-cart-drawer {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n width: 100%;\n max-height: 90vh;\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-drawer > header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n border-bottom: 1px solid var(--z-border);\n}\n\n.z-kit-cart-drawer__body {\n overflow-y: auto;\n padding: 0 var(--z-space-4) var(--z-space-4);\n}\n\n.z-kit-cart-drawer__issues {\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n border-inline-start: 3px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n}\n\n.z-kit-cart-drawer__totals {\n position: sticky;\n bottom: 0;\n display: block;\n padding: var(--z-space-4) 0;\n background: var(--z-surface);\n}\n\n.z-kit-cart-drawer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n@media (min-width: 64rem) {\n .z-kit-cart-drawer {\n width: min(var(--z-drawer-width), 100vw);\n height: 100vh;\n max-height: none;\n margin-left: auto;\n }\n}\n\n.z-kit-cart-checkout {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n margin-top: var(--z-space-3);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n border: 0;\n text-decoration: none;\n cursor: pointer;\n}\n\n.z-kit-cart-checkout:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-checkout:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-checkout__reason {\n margin: var(--z-space-2) 0 0;\n color: var(--z-text-muted);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body","--z-drawer-width","--z-space-2","--z-space-5","--z-text-md","--z-text-muted","--z-accent","--z-accent-contrast"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-drawer"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.1","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"95c20fe2f322ba5039adcd883d4db81eb64fe6f75d5569baea3f3c5223e57b6a","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => window.top?.location.assign(url),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: cart.actions.toCheckoutPayload(),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"acf50ea6b4123a137bf4e415231b7a3a92f3a5f3e83a988e9dc6ecd511fec4a0","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.2","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"95c20fe2f322ba5039adcd883d4db81eb64fe6f75d5569baea3f3c5223e57b6a","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => window.top?.location.assign(url),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: cart.actions.toCheckoutPayload(),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"cefb578068eac7d86f343d2ae5f7f9e2feda13bced3f05b365b78a71252d1bca","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.delivery_region_id !== undefined\n ? { delivery_region_id: value.delivery_region_id }\n : {}),\n ...(value.instructions !== undefined\n ? { instructions: value.instructions }\n : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.2","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"95c20fe2f322ba5039adcd883d4db81eb64fe6f75d5569baea3f3c5223e57b6a","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => window.top?.location.assign(url),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: cart.actions.toCheckoutPayload(),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"cefb578068eac7d86f343d2ae5f7f9e2feda13bced3f05b365b78a71252d1bca","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.delivery_region_id !== undefined\n ? { delivery_region_id: value.delivery_region_id }\n : {}),\n ...(value.instructions !== undefined\n ? { instructions: value.instructions }\n : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-card","type":"component","version":"1.0.0","title":"Item card","description":"Show an item, its price, availability and cart feedback.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money","quantity"],"files":[{"path":"item-card.tsx","target":"components/zappie/item-card.tsx","sha256":"859e3770d72434d89acb4fa9a17e9cd56ca5a37913fe14d31dd1c2717a711860","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart, useItem } from \"@zappie/sdk/react\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Check } from \"lucide-react\";\nimport \"./item-card.css\";\n\nexport type ItemCardProps = {\n itemId: string;\n branch?: string;\n variant?: \"grid\" | \"list\";\n};\n\nexport function ItemCard({ itemId, branch, variant = \"grid\" }: ItemCardProps) {\n const item = useItem(itemId, branch);\n const cart = useCart();\n if (item.isLoading) {\n return <article className=\"z-kit-item-card\">Loading item…</article>;\n }\n if (item.error || !item.data) {\n return (\n <article className=\"z-kit-item-card\" role=\"alert\">\n {item.error?.message ?? \"Item unavailable\"}\n </article>\n );\n }\n const value = item.data;\n const added = cart.lines.some((line) => line.itemId === itemId);\n const soldOut = !value.isAvailable;\n return (\n <article\n className=\"z-kit-item-card\"\n data-variant={variant}\n data-zappie-item={value.id}\n >\n {value.images[0] && (\n <img\n className=\"z-kit-item-card__image\"\n src={value.images[0]}\n alt={value.name}\n />\n )}\n <div className=\"z-kit-item-card__content\">\n <div>\n <h3>{value.name}</h3>\n <Money value={value.price} />\n </div>\n {soldOut && <span className=\"z-kit-item-card__sold-out\">Sold out</span>}\n <button\n type=\"button\"\n disabled={soldOut || !cart.isHydrated}\n data-zappie-action=\"add-to-cart\"\n onClick={() => {\n cart.actions.add(value);\n countEvent(\"add\");\n }}\n >\n {added && <Check aria-hidden=\"true\" />}\n {added ? \"Added\" : \"Add to cart\"}\n </button>\n </div>\n </article>\n );\n}\n"},{"path":"item-card.css","target":"components/zappie/item-card.css","sha256":"60424b10a12a225213b7f2ada864d38fb027e75156842a7a6d2ee2d23e9039be","content":".z-kit-item-card {\n display: grid;\n gap: var(--z-space-3);\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-lg);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 124px 1fr;\n align-items: start;\n}\n\n/* Each layout holds a fixed box for the photo before it loads, so the menu doesn't shift when it arrives. */\n.z-kit-item-card__image {\n display: block;\n width: 100%;\n max-height: 180px;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n}\n\n@media (min-width: 40rem) {\n .z-kit-item-card__image {\n max-height: 200px;\n }\n}\n\n.z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 124px;\n height: 124px;\n max-height: none;\n}\n\n@media (max-width: 375px) {\n .z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 70px 1fr;\n }\n\n .z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 70px;\n height: 70px;\n }\n}\n\n.z-kit-item-card__content {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n}\n\n.z-kit-item-card h3 {\n margin: 0 0 var(--z-space-1);\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n\n.z-kit-item-card__sold-out {\n color: var(--z-text);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-item-card button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-item-card button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-item-card button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-item-card button svg {\n width: var(--z-text-md);\n height: var(--z-text-md);\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-lg","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-danger","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display","--z-text-sm","--z-text-md","--z-text-lg","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-card"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-card","type":"component","version":"1.1.0","title":"Item card","description":"Show an item, its price, availability and cart feedback.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money","quantity"],"files":[{"path":"item-card.tsx","target":"components/zappie/item-card.tsx","sha256":"859e3770d72434d89acb4fa9a17e9cd56ca5a37913fe14d31dd1c2717a711860","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart, useItem } from \"@zappie/sdk/react\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Check } from \"lucide-react\";\nimport \"./item-card.css\";\n\nexport type ItemCardProps = {\n itemId: string;\n branch?: string;\n variant?: \"grid\" | \"list\";\n};\n\nexport function ItemCard({ itemId, branch, variant = \"grid\" }: ItemCardProps) {\n const item = useItem(itemId, branch);\n const cart = useCart();\n if (item.isLoading) {\n return <article className=\"z-kit-item-card\">Loading item…</article>;\n }\n if (item.error || !item.data) {\n return (\n <article className=\"z-kit-item-card\" role=\"alert\">\n {item.error?.message ?? \"Item unavailable\"}\n </article>\n );\n }\n const value = item.data;\n const added = cart.lines.some((line) => line.itemId === itemId);\n const soldOut = !value.isAvailable;\n return (\n <article\n className=\"z-kit-item-card\"\n data-variant={variant}\n data-zappie-item={value.id}\n >\n {value.images[0] && (\n <img\n className=\"z-kit-item-card__image\"\n src={value.images[0]}\n alt={value.name}\n />\n )}\n <div className=\"z-kit-item-card__content\">\n <div>\n <h3>{value.name}</h3>\n <Money value={value.price} />\n </div>\n {soldOut && <span className=\"z-kit-item-card__sold-out\">Sold out</span>}\n <button\n type=\"button\"\n disabled={soldOut || !cart.isHydrated}\n data-zappie-action=\"add-to-cart\"\n onClick={() => {\n cart.actions.add(value);\n countEvent(\"add\");\n }}\n >\n {added && <Check aria-hidden=\"true\" />}\n {added ? \"Added\" : \"Add to cart\"}\n </button>\n </div>\n </article>\n );\n}\n"},{"path":"item-card.css","target":"components/zappie/item-card.css","sha256":"60424b10a12a225213b7f2ada864d38fb027e75156842a7a6d2ee2d23e9039be","content":".z-kit-item-card {\n display: grid;\n gap: var(--z-space-3);\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-lg);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 124px 1fr;\n align-items: start;\n}\n\n/* Each layout holds a fixed box for the photo before it loads, so the menu doesn't shift when it arrives. */\n.z-kit-item-card__image {\n display: block;\n width: 100%;\n max-height: 180px;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n}\n\n@media (min-width: 40rem) {\n .z-kit-item-card__image {\n max-height: 200px;\n }\n}\n\n.z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 124px;\n height: 124px;\n max-height: none;\n}\n\n@media (max-width: 375px) {\n .z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 70px 1fr;\n }\n\n .z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 70px;\n height: 70px;\n }\n}\n\n.z-kit-item-card__content {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n}\n\n.z-kit-item-card h3 {\n margin: 0 0 var(--z-space-1);\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n\n.z-kit-item-card__sold-out {\n color: var(--z-text);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-item-card button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-item-card button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-item-card button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-item-card button svg {\n width: var(--z-text-md);\n height: var(--z-text-md);\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-lg","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-danger","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display","--z-text-sm","--z-text-md","--z-text-lg","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-card"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-card","type":"component","version":"1.0.0","title":"Item card","description":"Show an item, its price, availability and cart feedback.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money","quantity"],"files":[{"path":"item-card.tsx","target":"components/zappie/item-card.tsx","sha256":"859e3770d72434d89acb4fa9a17e9cd56ca5a37913fe14d31dd1c2717a711860","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart, useItem } from \"@zappie/sdk/react\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Check } from \"lucide-react\";\nimport \"./item-card.css\";\n\nexport type ItemCardProps = {\n itemId: string;\n branch?: string;\n variant?: \"grid\" | \"list\";\n};\n\nexport function ItemCard({ itemId, branch, variant = \"grid\" }: ItemCardProps) {\n const item = useItem(itemId, branch);\n const cart = useCart();\n if (item.isLoading) {\n return <article className=\"z-kit-item-card\">Loading item…</article>;\n }\n if (item.error || !item.data) {\n return (\n <article className=\"z-kit-item-card\" role=\"alert\">\n {item.error?.message ?? \"Item unavailable\"}\n </article>\n );\n }\n const value = item.data;\n const added = cart.lines.some((line) => line.itemId === itemId);\n const soldOut = !value.isAvailable;\n return (\n <article\n className=\"z-kit-item-card\"\n data-variant={variant}\n data-zappie-item={value.id}\n >\n {value.images[0] && (\n <img\n className=\"z-kit-item-card__image\"\n src={value.images[0]}\n alt={value.name}\n />\n )}\n <div className=\"z-kit-item-card__content\">\n <div>\n <h3>{value.name}</h3>\n <Money value={value.price} />\n </div>\n {soldOut && <span className=\"z-kit-item-card__sold-out\">Sold out</span>}\n <button\n type=\"button\"\n disabled={soldOut || !cart.isHydrated}\n data-zappie-action=\"add-to-cart\"\n onClick={() => {\n cart.actions.add(value);\n countEvent(\"add\");\n }}\n >\n {added && <Check aria-hidden=\"true\" />}\n {added ? \"Added\" : \"Add to cart\"}\n </button>\n </div>\n </article>\n );\n}\n"},{"path":"item-card.css","target":"components/zappie/item-card.css","sha256":"60424b10a12a225213b7f2ada864d38fb027e75156842a7a6d2ee2d23e9039be","content":".z-kit-item-card {\n display: grid;\n gap: var(--z-space-3);\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-lg);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 124px 1fr;\n align-items: start;\n}\n\n/* Each layout holds a fixed box for the photo before it loads, so the menu doesn't shift when it arrives. */\n.z-kit-item-card__image {\n display: block;\n width: 100%;\n max-height: 180px;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n}\n\n@media (min-width: 40rem) {\n .z-kit-item-card__image {\n max-height: 200px;\n }\n}\n\n.z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 124px;\n height: 124px;\n max-height: none;\n}\n\n@media (max-width: 375px) {\n .z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 70px 1fr;\n }\n\n .z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 70px;\n height: 70px;\n }\n}\n\n.z-kit-item-card__content {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n}\n\n.z-kit-item-card h3 {\n margin: 0 0 var(--z-space-1);\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n\n.z-kit-item-card__sold-out {\n color: var(--z-text);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-item-card button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-item-card button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-item-card button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-item-card button svg {\n width: var(--z-text-md);\n height: var(--z-text-md);\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-lg","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-danger","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display","--z-text-sm","--z-text-md","--z-text-lg","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-card"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-card","type":"component","version":"1.1.0","title":"Item card","description":"Show an item, its price, availability and cart feedback.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money","quantity"],"files":[{"path":"item-card.tsx","target":"components/zappie/item-card.tsx","sha256":"859e3770d72434d89acb4fa9a17e9cd56ca5a37913fe14d31dd1c2717a711860","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart, useItem } from \"@zappie/sdk/react\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Check } from \"lucide-react\";\nimport \"./item-card.css\";\n\nexport type ItemCardProps = {\n itemId: string;\n branch?: string;\n variant?: \"grid\" | \"list\";\n};\n\nexport function ItemCard({ itemId, branch, variant = \"grid\" }: ItemCardProps) {\n const item = useItem(itemId, branch);\n const cart = useCart();\n if (item.isLoading) {\n return <article className=\"z-kit-item-card\">Loading item…</article>;\n }\n if (item.error || !item.data) {\n return (\n <article className=\"z-kit-item-card\" role=\"alert\">\n {item.error?.message ?? \"Item unavailable\"}\n </article>\n );\n }\n const value = item.data;\n const added = cart.lines.some((line) => line.itemId === itemId);\n const soldOut = !value.isAvailable;\n return (\n <article\n className=\"z-kit-item-card\"\n data-variant={variant}\n data-zappie-item={value.id}\n >\n {value.images[0] && (\n <img\n className=\"z-kit-item-card__image\"\n src={value.images[0]}\n alt={value.name}\n />\n )}\n <div className=\"z-kit-item-card__content\">\n <div>\n <h3>{value.name}</h3>\n <Money value={value.price} />\n </div>\n {soldOut && <span className=\"z-kit-item-card__sold-out\">Sold out</span>}\n <button\n type=\"button\"\n disabled={soldOut || !cart.isHydrated}\n data-zappie-action=\"add-to-cart\"\n onClick={() => {\n cart.actions.add(value);\n countEvent(\"add\");\n }}\n >\n {added && <Check aria-hidden=\"true\" />}\n {added ? \"Added\" : \"Add to cart\"}\n </button>\n </div>\n </article>\n );\n}\n"},{"path":"item-card.css","target":"components/zappie/item-card.css","sha256":"60424b10a12a225213b7f2ada864d38fb027e75156842a7a6d2ee2d23e9039be","content":".z-kit-item-card {\n display: grid;\n gap: var(--z-space-3);\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-lg);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 124px 1fr;\n align-items: start;\n}\n\n/* Each layout holds a fixed box for the photo before it loads, so the menu doesn't shift when it arrives. */\n.z-kit-item-card__image {\n display: block;\n width: 100%;\n max-height: 180px;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n}\n\n@media (min-width: 40rem) {\n .z-kit-item-card__image {\n max-height: 200px;\n }\n}\n\n.z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 124px;\n height: 124px;\n max-height: none;\n}\n\n@media (max-width: 375px) {\n .z-kit-item-card[data-variant=\"list\"] {\n grid-template-columns: 70px 1fr;\n }\n\n .z-kit-item-card[data-variant=\"list\"] .z-kit-item-card__image {\n width: 70px;\n height: 70px;\n }\n}\n\n.z-kit-item-card__content {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding: var(--z-space-4);\n}\n\n.z-kit-item-card h3 {\n margin: 0 0 var(--z-space-1);\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n\n.z-kit-item-card__sold-out {\n color: var(--z-text);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-item-card button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-item-card button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-item-card button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-item-card button svg {\n width: var(--z-text-md);\n height: var(--z-text-md);\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-lg","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-danger","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display","--z-text-sm","--z-text-md","--z-text-lg","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-card"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-header","type":"component","version":"1.0.0","title":"Store header","description":"Show store details, opening status and fulfilment.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["open-status"],"files":[{"path":"store-header.tsx","target":"components/zappie/store-header.tsx","sha256":"6fa907b259d33232b80ff34efea83b7a79e783dd3e6741faf8952f26461cbfe3","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport { useCallback, useState } from \"react\";\nimport \"./store-header.css\";\n\nexport type StoreHeaderProps = { branch?: string };\n\nexport function StoreHeader({ branch }: StoreHeaderProps) {\n const store = useStore();\n const cart = useCart();\n if (store.isLoading) {\n return <header className=\"z-kit-store-header\">Loading store…</header>;\n }\n if (store.error || !store.data) {\n return (\n <header className=\"z-kit-store-header\" role=\"alert\">\n Store unavailable\n </header>\n );\n }\n const selected =\n store.data.branches.find(\n (item) => item.id === (branch ?? cart.branchId) || item.slug === branch,\n ) ??\n store.data.branches.find((item) => item.isDefault) ??\n store.data.branches[0];\n return (\n <header className=\"z-kit-store-header\">\n {store.data.brand.coverImageUrl && (\n <img\n className=\"z-kit-store-header__cover\"\n src={store.data.brand.coverImageUrl}\n alt=\"\"\n />\n )}\n <div className=\"z-kit-store-header__identity\">\n {store.data.brand.logoUrl && <Logo src={store.data.brand.logoUrl} />}\n <div>\n <h1>{store.data.name}</h1>\n {store.data.description && <p>{store.data.description}</p>}\n </div>\n </div>\n {selected && (\n <div className=\"z-kit-store-header__summary\">\n <OpenStatus\n branch={selected}\n timezone={selected.timezone ?? store.data.timezone}\n />\n <span>{fulfilment(selected.fulfilment)}</span>\n </div>\n )}\n </header>\n );\n}\n\n// A wide logo is contained so its ends show; a square or tall one covers its box.\nfunction Logo({ src }: { src: string }) {\n const [fit, setFit] = useState<\"contain\" | \"cover\">(\"contain\");\n // A logo loaded before hydration fires no onLoad for React, so the ref reads it too.\n const measure = useCallback((image: HTMLImageElement | null) => {\n if (image?.complete && image.naturalWidth > 0) {\n setFit(image.naturalWidth > image.naturalHeight ? \"contain\" : \"cover\");\n }\n }, []);\n return (\n <img\n ref={measure}\n src={src}\n alt=\"\"\n data-fit={fit}\n onLoad={(event) => measure(event.currentTarget)}\n />\n );\n}\n\nfunction fulfilment(value: {\n pickup: boolean;\n delivery: boolean;\n dineIn: boolean;\n}) {\n return [\n value.delivery ? \"Delivery\" : \"\",\n value.pickup ? \"Pickup\" : \"\",\n value.dineIn ? \"Dine in\" : \"\",\n ]\n .filter(Boolean)\n .join(\" · \");\n}\n"},{"path":"store-header.css","target":"components/zappie/store-header.css","sha256":"b7fad541ad0e9085e97918b1efcdfabd4241a3a5af248fcae61c2abe180f4507","content":".z-kit-store-header {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-header__cover {\n width: 100%;\n aspect-ratio: 12 / 5;\n max-height: 20rem;\n object-fit: cover;\n border-radius: var(--z-radius-lg);\n}\n.z-kit-store-header__identity,\n.z-kit-store-header__summary {\n display: flex;\n align-items: center;\n gap: var(--z-space-3);\n}\n.z-kit-store-header__identity img {\n width: var(--z-media-sm);\n height: var(--z-media-sm);\n object-fit: cover;\n border-radius: var(--z-radius-md);\n}\n.z-kit-store-header__identity img[data-fit=\"contain\"] {\n object-fit: contain;\n}\n.z-kit-store-header h1 {\n margin: 0;\n font-family: var(--z-font-display);\n}\n.z-kit-store-header p {\n color: var(--z-text-muted);\n}\n.z-kit-store-header__summary {\n justify-content: space-between;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-text","--z-text-muted","--z-radius-md","--z-radius-lg","--z-font-body","--z-font-display","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-header"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-header","type":"component","version":"1.1.0","title":"Store header","description":"Show store details, opening status and fulfilment.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["open-status"],"files":[{"path":"store-header.tsx","target":"components/zappie/store-header.tsx","sha256":"6fa907b259d33232b80ff34efea83b7a79e783dd3e6741faf8952f26461cbfe3","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport { useCallback, useState } from \"react\";\nimport \"./store-header.css\";\n\nexport type StoreHeaderProps = { branch?: string };\n\nexport function StoreHeader({ branch }: StoreHeaderProps) {\n const store = useStore();\n const cart = useCart();\n if (store.isLoading) {\n return <header className=\"z-kit-store-header\">Loading store…</header>;\n }\n if (store.error || !store.data) {\n return (\n <header className=\"z-kit-store-header\" role=\"alert\">\n Store unavailable\n </header>\n );\n }\n const selected =\n store.data.branches.find(\n (item) => item.id === (branch ?? cart.branchId) || item.slug === branch,\n ) ??\n store.data.branches.find((item) => item.isDefault) ??\n store.data.branches[0];\n return (\n <header className=\"z-kit-store-header\">\n {store.data.brand.coverImageUrl && (\n <img\n className=\"z-kit-store-header__cover\"\n src={store.data.brand.coverImageUrl}\n alt=\"\"\n />\n )}\n <div className=\"z-kit-store-header__identity\">\n {store.data.brand.logoUrl && <Logo src={store.data.brand.logoUrl} />}\n <div>\n <h1>{store.data.name}</h1>\n {store.data.description && <p>{store.data.description}</p>}\n </div>\n </div>\n {selected && (\n <div className=\"z-kit-store-header__summary\">\n <OpenStatus\n branch={selected}\n timezone={selected.timezone ?? store.data.timezone}\n />\n <span>{fulfilment(selected.fulfilment)}</span>\n </div>\n )}\n </header>\n );\n}\n\n// A wide logo is contained so its ends show; a square or tall one covers its box.\nfunction Logo({ src }: { src: string }) {\n const [fit, setFit] = useState<\"contain\" | \"cover\">(\"contain\");\n // A logo loaded before hydration fires no onLoad for React, so the ref reads it too.\n const measure = useCallback((image: HTMLImageElement | null) => {\n if (image?.complete && image.naturalWidth > 0) {\n setFit(image.naturalWidth > image.naturalHeight ? \"contain\" : \"cover\");\n }\n }, []);\n return (\n <img\n ref={measure}\n src={src}\n alt=\"\"\n data-fit={fit}\n onLoad={(event) => measure(event.currentTarget)}\n />\n );\n}\n\nfunction fulfilment(value: {\n pickup: boolean;\n delivery: boolean;\n dineIn: boolean;\n}) {\n return [\n value.delivery ? \"Delivery\" : \"\",\n value.pickup ? \"Pickup\" : \"\",\n value.dineIn ? \"Dine in\" : \"\",\n ]\n .filter(Boolean)\n .join(\" · \");\n}\n"},{"path":"store-header.css","target":"components/zappie/store-header.css","sha256":"b7fad541ad0e9085e97918b1efcdfabd4241a3a5af248fcae61c2abe180f4507","content":".z-kit-store-header {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-header__cover {\n width: 100%;\n aspect-ratio: 12 / 5;\n max-height: 20rem;\n object-fit: cover;\n border-radius: var(--z-radius-lg);\n}\n.z-kit-store-header__identity,\n.z-kit-store-header__summary {\n display: flex;\n align-items: center;\n gap: var(--z-space-3);\n}\n.z-kit-store-header__identity img {\n width: var(--z-media-sm);\n height: var(--z-media-sm);\n object-fit: cover;\n border-radius: var(--z-radius-md);\n}\n.z-kit-store-header__identity img[data-fit=\"contain\"] {\n object-fit: contain;\n}\n.z-kit-store-header h1 {\n margin: 0;\n font-family: var(--z-font-display);\n}\n.z-kit-store-header p {\n color: var(--z-text-muted);\n}\n.z-kit-store-header__summary {\n justify-content: space-between;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-text","--z-text-muted","--z-radius-md","--z-radius-lg","--z-font-body","--z-font-display","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-header"}
@@ -1 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-header","type":"component","version":"1.0.0","title":"Store header","description":"Show store details, opening status and fulfilment.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["open-status"],"files":[{"path":"store-header.tsx","target":"components/zappie/store-header.tsx","sha256":"6fa907b259d33232b80ff34efea83b7a79e783dd3e6741faf8952f26461cbfe3","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport { useCallback, useState } from \"react\";\nimport \"./store-header.css\";\n\nexport type StoreHeaderProps = { branch?: string };\n\nexport function StoreHeader({ branch }: StoreHeaderProps) {\n const store = useStore();\n const cart = useCart();\n if (store.isLoading) {\n return <header className=\"z-kit-store-header\">Loading store…</header>;\n }\n if (store.error || !store.data) {\n return (\n <header className=\"z-kit-store-header\" role=\"alert\">\n Store unavailable\n </header>\n );\n }\n const selected =\n store.data.branches.find(\n (item) => item.id === (branch ?? cart.branchId) || item.slug === branch,\n ) ??\n store.data.branches.find((item) => item.isDefault) ??\n store.data.branches[0];\n return (\n <header className=\"z-kit-store-header\">\n {store.data.brand.coverImageUrl && (\n <img\n className=\"z-kit-store-header__cover\"\n src={store.data.brand.coverImageUrl}\n alt=\"\"\n />\n )}\n <div className=\"z-kit-store-header__identity\">\n {store.data.brand.logoUrl && <Logo src={store.data.brand.logoUrl} />}\n <div>\n <h1>{store.data.name}</h1>\n {store.data.description && <p>{store.data.description}</p>}\n </div>\n </div>\n {selected && (\n <div className=\"z-kit-store-header__summary\">\n <OpenStatus\n branch={selected}\n timezone={selected.timezone ?? store.data.timezone}\n />\n <span>{fulfilment(selected.fulfilment)}</span>\n </div>\n )}\n </header>\n );\n}\n\n// A wide logo is contained so its ends show; a square or tall one covers its box.\nfunction Logo({ src }: { src: string }) {\n const [fit, setFit] = useState<\"contain\" | \"cover\">(\"contain\");\n // A logo loaded before hydration fires no onLoad for React, so the ref reads it too.\n const measure = useCallback((image: HTMLImageElement | null) => {\n if (image?.complete && image.naturalWidth > 0) {\n setFit(image.naturalWidth > image.naturalHeight ? \"contain\" : \"cover\");\n }\n }, []);\n return (\n <img\n ref={measure}\n src={src}\n alt=\"\"\n data-fit={fit}\n onLoad={(event) => measure(event.currentTarget)}\n />\n );\n}\n\nfunction fulfilment(value: {\n pickup: boolean;\n delivery: boolean;\n dineIn: boolean;\n}) {\n return [\n value.delivery ? \"Delivery\" : \"\",\n value.pickup ? \"Pickup\" : \"\",\n value.dineIn ? \"Dine in\" : \"\",\n ]\n .filter(Boolean)\n .join(\" · \");\n}\n"},{"path":"store-header.css","target":"components/zappie/store-header.css","sha256":"b7fad541ad0e9085e97918b1efcdfabd4241a3a5af248fcae61c2abe180f4507","content":".z-kit-store-header {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-header__cover {\n width: 100%;\n aspect-ratio: 12 / 5;\n max-height: 20rem;\n object-fit: cover;\n border-radius: var(--z-radius-lg);\n}\n.z-kit-store-header__identity,\n.z-kit-store-header__summary {\n display: flex;\n align-items: center;\n gap: var(--z-space-3);\n}\n.z-kit-store-header__identity img {\n width: var(--z-media-sm);\n height: var(--z-media-sm);\n object-fit: cover;\n border-radius: var(--z-radius-md);\n}\n.z-kit-store-header__identity img[data-fit=\"contain\"] {\n object-fit: contain;\n}\n.z-kit-store-header h1 {\n margin: 0;\n font-family: var(--z-font-display);\n}\n.z-kit-store-header p {\n color: var(--z-text-muted);\n}\n.z-kit-store-header__summary {\n justify-content: space-between;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-text","--z-text-muted","--z-radius-md","--z-radius-lg","--z-font-body","--z-font-display","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-header"}
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-header","type":"component","version":"1.1.0","title":"Store header","description":"Show store details, opening status and fulfilment.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["open-status"],"files":[{"path":"store-header.tsx","target":"components/zappie/store-header.tsx","sha256":"6fa907b259d33232b80ff34efea83b7a79e783dd3e6741faf8952f26461cbfe3","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport { useCallback, useState } from \"react\";\nimport \"./store-header.css\";\n\nexport type StoreHeaderProps = { branch?: string };\n\nexport function StoreHeader({ branch }: StoreHeaderProps) {\n const store = useStore();\n const cart = useCart();\n if (store.isLoading) {\n return <header className=\"z-kit-store-header\">Loading store…</header>;\n }\n if (store.error || !store.data) {\n return (\n <header className=\"z-kit-store-header\" role=\"alert\">\n Store unavailable\n </header>\n );\n }\n const selected =\n store.data.branches.find(\n (item) => item.id === (branch ?? cart.branchId) || item.slug === branch,\n ) ??\n store.data.branches.find((item) => item.isDefault) ??\n store.data.branches[0];\n return (\n <header className=\"z-kit-store-header\">\n {store.data.brand.coverImageUrl && (\n <img\n className=\"z-kit-store-header__cover\"\n src={store.data.brand.coverImageUrl}\n alt=\"\"\n />\n )}\n <div className=\"z-kit-store-header__identity\">\n {store.data.brand.logoUrl && <Logo src={store.data.brand.logoUrl} />}\n <div>\n <h1>{store.data.name}</h1>\n {store.data.description && <p>{store.data.description}</p>}\n </div>\n </div>\n {selected && (\n <div className=\"z-kit-store-header__summary\">\n <OpenStatus\n branch={selected}\n timezone={selected.timezone ?? store.data.timezone}\n />\n <span>{fulfilment(selected.fulfilment)}</span>\n </div>\n )}\n </header>\n );\n}\n\n// A wide logo is contained so its ends show; a square or tall one covers its box.\nfunction Logo({ src }: { src: string }) {\n const [fit, setFit] = useState<\"contain\" | \"cover\">(\"contain\");\n // A logo loaded before hydration fires no onLoad for React, so the ref reads it too.\n const measure = useCallback((image: HTMLImageElement | null) => {\n if (image?.complete && image.naturalWidth > 0) {\n setFit(image.naturalWidth > image.naturalHeight ? \"contain\" : \"cover\");\n }\n }, []);\n return (\n <img\n ref={measure}\n src={src}\n alt=\"\"\n data-fit={fit}\n onLoad={(event) => measure(event.currentTarget)}\n />\n );\n}\n\nfunction fulfilment(value: {\n pickup: boolean;\n delivery: boolean;\n dineIn: boolean;\n}) {\n return [\n value.delivery ? \"Delivery\" : \"\",\n value.pickup ? \"Pickup\" : \"\",\n value.dineIn ? \"Dine in\" : \"\",\n ]\n .filter(Boolean)\n .join(\" · \");\n}\n"},{"path":"store-header.css","target":"components/zappie/store-header.css","sha256":"b7fad541ad0e9085e97918b1efcdfabd4241a3a5af248fcae61c2abe180f4507","content":".z-kit-store-header {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-header__cover {\n width: 100%;\n aspect-ratio: 12 / 5;\n max-height: 20rem;\n object-fit: cover;\n border-radius: var(--z-radius-lg);\n}\n.z-kit-store-header__identity,\n.z-kit-store-header__summary {\n display: flex;\n align-items: center;\n gap: var(--z-space-3);\n}\n.z-kit-store-header__identity img {\n width: var(--z-media-sm);\n height: var(--z-media-sm);\n object-fit: cover;\n border-radius: var(--z-radius-md);\n}\n.z-kit-store-header__identity img[data-fit=\"contain\"] {\n object-fit: contain;\n}\n.z-kit-store-header h1 {\n margin: 0;\n font-family: var(--z-font-display);\n}\n.z-kit-store-header p {\n color: var(--z-text-muted);\n}\n.z-kit-store-header__summary {\n justify-content: space-between;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-text","--z-text-muted","--z-radius-md","--z-radius-lg","--z-font-body","--z-font-display","--z-media-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-header"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zappie/kit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "The registry of copy-in Zappie storefront components, installed with the zappie CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/kit"
10
10
  },
11
11
  "devDependencies": {
12
- "zod": "^4.6.5"
12
+ "zod": "^4.6.7"
13
13
  },
14
14
  "license": "MIT",
15
15
  "homepage": "https://github.com/zappie/storefront#readme",
@@ -1 +0,0 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-button","type":"component","version":"1.0.0","title":"Cart button","description":"Open the cart from a floating or inline button with count and subtotal.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":["money"],"files":[{"path":"cart-button.tsx","target":"components/zappie/cart-button.tsx","sha256":"082c691b672dc1c6e4838980a3d6d62b8cb7d8320ad061862bf02288b33dadd7","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { ShoppingBag } from \"lucide-react\";\nimport \"./cart-button.css\";\n\nexport type CartButtonProps = {\n onOpen: () => void;\n /** Called when the shopper reaches for the button, to start loading the cart's code. */\n onIntent?: () => void;\n variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({\n onOpen,\n onIntent,\n variant = \"floating\",\n}: CartButtonProps) {\n const cart = useCart();\n return (\n <button\n type=\"button\"\n className=\"z-kit-cart-button\"\n data-variant={variant}\n data-zappie-cart-trigger=\"\"\n data-zappie-overlay-trigger=\"bag\"\n aria-label={`Open cart, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n disabled={!cart.isHydrated}\n onClick={onOpen}\n onPointerEnter={onIntent}\n onPointerDown={onIntent}\n onFocus={onIntent}\n >\n <ShoppingBag aria-hidden=\"true\" />\n <span>Cart</span>\n <span aria-hidden=\"true\">{cart.count}</span>\n {cart.subtotal && <Money value={cart.subtotal} />}\n </button>\n );\n}\n"},{"path":"cart-button.css","target":"components/zappie/cart-button.css","sha256":"0e6879dbab4158f6fd1979696663cdc7c80621867a90acf8a4040987bf3e9216","content":".z-kit-cart-button {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-2);\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-cart-button[data-variant=\"floating\"] {\n position: fixed;\n right: var(--z-space-4);\n bottom: var(--z-space-4);\n border: 1px solid var(--z-border);\n}\n\n.z-kit-cart-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n\n.z-kit-cart-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-cart-button svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-button"}
@@ -1 +0,0 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.1","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-button.tsx","target":"components/zappie/checkout-button.tsx","sha256":"95c20fe2f322ba5039adcd883d4db81eb64fe6f75d5569baea3f3c5223e57b6a","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { CheckoutError, isContactPhoneValid } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { currentSource } from \"@zappie/sdk/visits\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createCheckoutAttempt } from \"./checkout-attempt\";\nimport \"./checkout-button.css\";\nimport { checkoutButtonState } from \"./checkout-state\";\n\nexport type CheckoutButtonProps = {\n checkout: CheckoutClient;\n branchId: string;\n returnUrl?: string;\n cancelUrl?: string;\n contactPhone?: string;\n attribution?: () => { source: string; campaign: string };\n onSignIn?: () => void;\n /** Show a recoverable error when opening payment fails. */\n onError?: (failure: unknown) => void;\n onContactPhoneRequired?: () => void;\n redirect?: (url: string) => void;\n};\n\nexport function CheckoutButton({\n checkout,\n branchId,\n returnUrl,\n cancelUrl,\n contactPhone = \"\",\n attribution = currentSource,\n onSignIn,\n onError,\n onContactPhoneRequired,\n redirect = (url) => window.top?.location.assign(url),\n}: CheckoutButtonProps) {\n const cart = useCart();\n const attempt = useMemo(() => createCheckoutAttempt(), []);\n const [busy, setBusy] = useState(false);\n const quoted = useRef(\"\");\n const quoteKey = JSON.stringify([\n cart.lines,\n cart.fulfilment,\n cart.promoCode,\n ]);\n const [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\" &&\n quoted.current !== quoteKey\n ) {\n quoted.current = quoteKey;\n void cart.actions.quote().catch((failure: unknown) => onError?.(failure));\n }\n }, [\n cart.actions,\n cart.isHydrated,\n cart.lines.length,\n cart.state.status,\n quoteKey,\n onError,\n ]);\n const state = checkoutButtonState(\n cart.state,\n cart.fulfilment,\n isContactPhoneValid(contactPhone),\n busy,\n forced,\n );\n const activate = async () => {\n if (state.status === \"sign_in\") {\n onSignIn?.();\n return;\n }\n if (state.status === \"phone\") {\n onContactPhoneRequired?.();\n return;\n }\n if (state.status === \"retry\") {\n try {\n await cart.actions.quote({ force: true });\n } catch (failure) {\n onError?.(failure);\n }\n return;\n }\n if (state.status !== \"pay\") {\n return;\n }\n setBusy(true);\n try {\n const checkoutAttempt = attempt({\n payload: cart.actions.toCheckoutPayload(),\n branchId,\n contactPhone,\n returnUrl: returnUrl ?? `${location.origin}/checkout/return`,\n ...((cancelUrl ?? location.href)\n ? { cancelUrl: cancelUrl ?? location.href }\n : {}),\n attribution: attribution(),\n });\n const session = await checkout.open(\n checkoutAttempt.request,\n checkoutAttempt.key,\n );\n redirect(session.url);\n } catch (failure) {\n if (\n failure instanceof CheckoutError &&\n failure.apiCode === \"customer_required\"\n ) {\n setForced(\"sign_in\");\n onSignIn?.();\n return;\n }\n if (onError) {\n onError(failure);\n return;\n }\n throw failure;\n } finally {\n setBusy(false);\n }\n };\n return (\n <button\n type=\"button\"\n className=\"z-kit-checkout-button\"\n data-state={state.status}\n data-zappie-checkout=\"\"\n disabled={!state.enabled}\n onClick={() => void activate()}\n >\n {state.label}\n </button>\n );\n}\n"},{"path":"checkout-attempt.ts","target":"components/zappie/checkout-attempt.ts","sha256":"acf50ea6b4123a137bf4e415231b7a3a92f3a5f3e83a988e9dc6ecd511fec4a0","content":"import type { CartModule } from \"@zappie/sdk\";\nimport { checkoutFulfilment, type CheckoutRequest } from \"@zappie/sdk/checkout\";\n\ntype Payload = ReturnType<CartModule[\"toCheckoutPayload\"]>;\ntype Attribution = { source: string; campaign: string };\nexport type CheckoutAttempt = { key: string; request: CheckoutRequest };\n\nfunction fulfilment(payload: Payload) {\n const value = checkoutFulfilment(payload.fulfilment);\n if (value.type === \"pickup\") {\n return {\n type: \"pickup\" as const,\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n if (value.type === \"delivery\") {\n return {\n type: \"delivery\" as const,\n ...(value.address !== undefined ? { address: value.address } : {}),\n ...(value.scheduled_for !== undefined\n ? { scheduled_for: value.scheduled_for }\n : {}),\n };\n }\n throw new Error(\"Dine-in checkout is unavailable\");\n}\n\nexport function createCheckoutAttempt() {\n let current: { digest: string; attempt: CheckoutAttempt } | undefined;\n return ({\n payload,\n branchId,\n contactPhone,\n returnUrl,\n cancelUrl,\n attribution,\n }: {\n payload: Payload;\n branchId: string;\n contactPhone: string;\n returnUrl: string;\n cancelUrl?: string;\n attribution: Attribution;\n }) => {\n const normalizedPhone = contactPhone.trim().replace(/\\s+/g, \"\");\n const digest = JSON.stringify({ payload, contactPhone: normalizedPhone });\n if (current?.digest === digest) {\n return current.attempt;\n }\n const request: CheckoutRequest = {\n branch_id: branchId,\n menu_version: payload.menu_version,\n lines: payload.lines,\n fulfilment: fulfilment(payload),\n ...(normalizedPhone ? { contact_phone: normalizedPhone } : {}),\n ...(payload.promo_code ? { promo_code: payload.promo_code } : {}),\n ...(attribution.source ? { source: attribution.source } : {}),\n ...(attribution.campaign ? { campaign: attribution.campaign } : {}),\n return_url: returnUrl,\n ...(cancelUrl ? { cancel_url: cancelUrl } : {}),\n };\n current = { digest, attempt: { key: crypto.randomUUID(), request } };\n return current.attempt;\n };\n}\n"},{"path":"checkout-state.ts","target":"components/zappie/checkout-state.ts","sha256":"e8e2dc8f88d513c124d44516e3d18012d4fd7e2c069e31454d4426dd6c2d4e43","content":"import type { CartFulfilment, CartState } from \"@zappie/sdk\";\nimport {\n payReadiness,\n type PayReadiness,\n type PayReadinessQuoteState,\n} from \"@zappie/sdk/checkout\";\n\nexport type CheckoutButtonStatus = PayReadiness | \"busy\";\nconst labels: Record<CheckoutButtonStatus, string> = {\n address: \"Add a delivery address\",\n quoting: \"Updating total…\",\n retry: \"Update totals to continue\",\n choose_area: \"Choose a delivery area\",\n fix_issues: \"Review your cart\",\n sign_in: \"Sign in to continue\",\n phone: \"Check your phone number\",\n pay: \"Continue to payment\",\n busy: \"Please wait…\",\n};\n\nfunction quoteState(state: CartState): PayReadinessQuoteState {\n if (state.status === \"quoting\") {\n return { status: \"loading\" };\n }\n if (state.status === \"quoted\") {\n return { status: \"ready\", quote: state.quote };\n }\n if (state.status === \"needs_attention\") {\n return state.quote\n ? { status: \"ready\", quote: state.quote }\n : { status: \"error\" };\n }\n return { status: \"idle\" };\n}\n\nexport function checkoutButtonState(\n state: CartState,\n fulfilment: CartFulfilment | null,\n contactPhoneValid: boolean,\n busy: boolean,\n forced?: \"sign_in\",\n) {\n let status: CheckoutButtonStatus;\n if (busy) {\n status = \"busy\";\n } else if (forced) {\n status = forced;\n } else {\n const addressValid =\n fulfilment?.type !== \"delivery\" ||\n Boolean(\n fulfilment.address.line1 ||\n fulfilment.address.placeId ||\n fulfilment.address.coordinates,\n );\n status = payReadiness(quoteState(state), contactPhoneValid, addressValid);\n if (status === \"pay\" && state.status === \"needs_attention\") {\n status = \"fix_issues\";\n }\n }\n return {\n status,\n label: labels[status],\n enabled:\n status === \"pay\" ||\n status === \"sign_in\" ||\n status === \"phone\" ||\n status === \"retry\",\n };\n}\n"},{"path":"checkout-button.css","target":"components/zappie/checkout-button.css","sha256":"9e6bb8afe1bb1e5cd55ed458e435d90b4b3d546fb4276d3fd9432d022844ca22","content":".z-kit-checkout-button {\n width: 100%;\n border: 0;\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3) var(--z-space-5);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n cursor: pointer;\n}\n\n.z-kit-checkout-button:disabled {\n background: var(--z-border);\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-checkout-button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-radius-md","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-button"}