@zappie/kit 0.1.1 → 0.1.3

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,19 @@
1
1
  # @zappie/kit
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 45d1cd9: The item card holds a fixed box for its photo before the photo loads, so the menu no longer shifts when photos arrive. The grid layout shows a square photo across the card's width. The list layout shows a 124 px square, or 70 px on screens 375 px wide or narrower. Photos fill the box and are cropped to it, as on the default storefront.
8
+ - 3d0d559: The store header fits the business logo the way the default storefront does: a wide logo is contained so its ends show, and a square or tall one covers its box. The cover photo keeps a 12 by 5 box while it loads, so the header no longer grows when it arrives.
9
+ - 6595b0e: The cart drawer links to checkout. Pass `checkoutHref` and the drawer shows a Checkout link once the cart has lines. While a blocking issue is open the link is disabled and says why. The starter's cart page shows the same link, so shoppers reach checkout without typing its address.
10
+
11
+ ## 0.1.2
12
+
13
+ ### Patch Changes
14
+
15
+ - e55ac2f: Add account, order-list, checkout and payment-return blocks, plus consent controls for optional tracking. Expose the consent choice without loading tracking tags.
16
+
3
17
  ## 0.1.1
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -25,3 +25,22 @@ The kit is at `0.x`. Components carry their own versions, and `zappie diff` show
25
25
  ## License
26
26
 
27
27
  MIT
28
+
29
+ ## Account and checkout pages
30
+
31
+ Install `checkout-page`, `account-page`, `orders-page`, `checkout-return` and
32
+ `consent` with `zappie add`. Supply public SDK clients and your own route links.
33
+ Account and order pages use the same customer client as `CustomerProvider`.
34
+ Checkout option fixes call your item editor with the cart line and option group.
35
+ Payment-method selection stays on hosted checkout, whose session request does
36
+ not accept a selected method.
37
+
38
+ Pass the return route's `session` query parameter to `CheckoutReturn`. It clears
39
+ the cart only after checkout confirms a completed order. It links to your order
40
+ route, where you can render `OrderPage`.
41
+
42
+ Give `Consent` a store-specific, versioned storage key and a privacy-policy URL.
43
+ Use its `onChange` callback to read the stored choice and later changes, including
44
+ withdrawal. The item loads no tracking tags and reads no integration IDs. Merchant
45
+ pixels will use `analytics.forward` when it becomes available. Direct tracking
46
+ scripts remain disallowed.
@@ -1 +1 @@
1
- {"version":"2026-10-10","items":[{"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.0","title":"Checkout button","description":"Open checkout with quote checks, safe retries and visit attribution."},{"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":"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.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."}]}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button"],"files":[{"path":"account-page.css","target":"components/zappie/account-page.css","sha256":"6bfc66e08b88126e9f59eeb4c58c872629efe7a5d7f31a15176e707ebc9ba8c7","content":".z-kit-account-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-account-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-account-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-account-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-account-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"account-page.tsx","target":"components/zappie/account-page.tsx","sha256":"844ed8a5fb5df3353120ab4fca0d7c0786ab55f76b2d5c604b0d39f1fe983511","content":"\"use client\";\n\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport type {\n CustomerClient,\n CustomerProfile,\n SavedAddress,\n} from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./account-page.css\";\n\n/** Show your profile and let you save or remove delivery addresses. */\nexport function AccountPage({\n customer,\n ordersHref = \"/orders\",\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n ordersHref?: string;\n}) {\n const auth = useAuth();\n return (\n <section className=\"z-kit-account-page\" aria-label=\"Your account\">\n <h2>Your account</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Account key={auth.customer.id} customer={customer} />\n ) : (\n <p>Sign in to see your profile and saved addresses.</p>\n )}\n <SignInButton />\n <a href={ordersHref}>Your orders</a>\n </section>\n );\n}\n\nfunction Account({ customer }: { customer: CustomerClient }) {\n const [profile, setProfile] = useState<CustomerProfile>();\n const [addresses, setAddresses] = useState<SavedAddress[]>([]);\n const [loading, setLoading] = useState(true);\n const [attempt, setAttempt] = useState(0);\n const [error, setError] = useState(\"\");\n const [busy, setBusy] = useState(false);\n useEffect(() => {\n let active = true;\n setLoading(true);\n setError(\"\");\n void Promise.all([customer.profile(), customer.addresses()])\n .then(\n ([next, saved]) => {\n if (active) {\n setProfile(next);\n setAddresses(saved);\n }\n },\n () => {\n if (active) {\n setError(\"We could not load your account. Try again.\");\n }\n },\n )\n .finally(() => {\n if (active) {\n setLoading(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const remove = async (id: string) => {\n setBusy(true);\n setError(\"\");\n try {\n await customer.removeAddress(id);\n setAddresses((current) => current.filter((address) => address.id !== id));\n } catch {\n setError(\"We could not remove that address. Try again.\");\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {loading && <p role=\"status\">Loading your account…</p>}\n {error && (\n <div role=\"alert\">\n <p>{error}</p>\n <button\n type=\"button\"\n onClick={() => setAttempt((value) => value + 1)}\n >\n Reload account\n </button>\n </div>\n )}\n {profile && (\n <div>\n <p>\n {[profile.given_name, profile.family_name]\n .filter(Boolean)\n .join(\" \")}\n </p>\n <p>{profile.phone_masked}</p>\n </div>\n )}\n <h3>Saved addresses</h3>\n {!loading && addresses.length === 0 && (\n <p>You have no saved addresses.</p>\n )}\n <ul>\n {addresses.map((address) => (\n <li key={address.id}>\n <p>\n {address.label && `${address.label}: `}\n {address.line1}\n </p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void remove(address.id)}\n aria-label={`Remove ${address.line1}`}\n >\n Remove\n </button>\n </li>\n ))}\n </ul>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n const form = event.currentTarget;\n const data = new FormData(form);\n const line1 = String(data.get(\"line1\") ?? \"\").trim();\n if (!line1 || busy) {\n return;\n }\n setBusy(true);\n setError(\"\");\n void customer\n .addAddress({\n line1,\n label: \"other\",\n instructions: String(data.get(\"instructions\") ?? \"\").trim(),\n })\n .then(\n (address) => {\n setAddresses((current) => [...current, address]);\n form.reset();\n },\n () => setError(\"We could not save that address. Try again.\"),\n )\n .finally(() => setBusy(false));\n }}\n >\n <label>\n Street and address\n <input name=\"line1\" autoComplete=\"street-address\" required />\n </label>\n <label>\n Delivery instructions\n <input name=\"instructions\" maxLength={200} />\n </label>\n <button type=\"submit\" disabled={busy || loading}>\n {busy ? \"Saving…\" : \"Save address\"}\n </button>\n </form>\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/account-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"account-page","type":"block","version":"1.0.0","title":"Account page","description":"Show your profile and manage saved addresses.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button"],"files":[{"path":"account-page.css","target":"components/zappie/account-page.css","sha256":"6bfc66e08b88126e9f59eeb4c58c872629efe7a5d7f31a15176e707ebc9ba8c7","content":".z-kit-account-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-account-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-account-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-account-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-account-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"account-page.tsx","target":"components/zappie/account-page.tsx","sha256":"844ed8a5fb5df3353120ab4fca0d7c0786ab55f76b2d5c604b0d39f1fe983511","content":"\"use client\";\n\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport type {\n CustomerClient,\n CustomerProfile,\n SavedAddress,\n} from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./account-page.css\";\n\n/** Show your profile and let you save or remove delivery addresses. */\nexport function AccountPage({\n customer,\n ordersHref = \"/orders\",\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n ordersHref?: string;\n}) {\n const auth = useAuth();\n return (\n <section className=\"z-kit-account-page\" aria-label=\"Your account\">\n <h2>Your account</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Account key={auth.customer.id} customer={customer} />\n ) : (\n <p>Sign in to see your profile and saved addresses.</p>\n )}\n <SignInButton />\n <a href={ordersHref}>Your orders</a>\n </section>\n );\n}\n\nfunction Account({ customer }: { customer: CustomerClient }) {\n const [profile, setProfile] = useState<CustomerProfile>();\n const [addresses, setAddresses] = useState<SavedAddress[]>([]);\n const [loading, setLoading] = useState(true);\n const [attempt, setAttempt] = useState(0);\n const [error, setError] = useState(\"\");\n const [busy, setBusy] = useState(false);\n useEffect(() => {\n let active = true;\n setLoading(true);\n setError(\"\");\n void Promise.all([customer.profile(), customer.addresses()])\n .then(\n ([next, saved]) => {\n if (active) {\n setProfile(next);\n setAddresses(saved);\n }\n },\n () => {\n if (active) {\n setError(\"We could not load your account. Try again.\");\n }\n },\n )\n .finally(() => {\n if (active) {\n setLoading(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const remove = async (id: string) => {\n setBusy(true);\n setError(\"\");\n try {\n await customer.removeAddress(id);\n setAddresses((current) => current.filter((address) => address.id !== id));\n } catch {\n setError(\"We could not remove that address. Try again.\");\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {loading && <p role=\"status\">Loading your account…</p>}\n {error && (\n <div role=\"alert\">\n <p>{error}</p>\n <button\n type=\"button\"\n onClick={() => setAttempt((value) => value + 1)}\n >\n Reload account\n </button>\n </div>\n )}\n {profile && (\n <div>\n <p>\n {[profile.given_name, profile.family_name]\n .filter(Boolean)\n .join(\" \")}\n </p>\n <p>{profile.phone_masked}</p>\n </div>\n )}\n <h3>Saved addresses</h3>\n {!loading && addresses.length === 0 && (\n <p>You have no saved addresses.</p>\n )}\n <ul>\n {addresses.map((address) => (\n <li key={address.id}>\n <p>\n {address.label && `${address.label}: `}\n {address.line1}\n </p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => void remove(address.id)}\n aria-label={`Remove ${address.line1}`}\n >\n Remove\n </button>\n </li>\n ))}\n </ul>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n const form = event.currentTarget;\n const data = new FormData(form);\n const line1 = String(data.get(\"line1\") ?? \"\").trim();\n if (!line1 || busy) {\n return;\n }\n setBusy(true);\n setError(\"\");\n void customer\n .addAddress({\n line1,\n label: \"other\",\n instructions: String(data.get(\"instructions\") ?? \"\").trim(),\n })\n .then(\n (address) => {\n setAddresses((current) => [...current, address]);\n form.reset();\n },\n () => setError(\"We could not save that address. Try again.\"),\n )\n .finally(() => setBusy(false));\n }}\n >\n <label>\n Street and address\n <input name=\"line1\" autoComplete=\"street-address\" required />\n </label>\n <label>\n Delivery instructions\n <input name=\"instructions\" maxLength={200} />\n </label>\n <button type=\"submit\" disabled={busy || loading}>\n {busy ? \"Saving…\" : \"Save address\"}\n </button>\n </form>\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/account-page"}
@@ -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":"ee8a10d8f2f61549fe332fff56647336f5fb16927253d0177e649bdd652678f9","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 variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({ onOpen, variant = \"floating\" }: 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 aria-label={`Open cart, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n disabled={!cart.isHydrated}\n onClick={onOpen}\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.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 +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":"ee8a10d8f2f61549fe332fff56647336f5fb16927253d0177e649bdd652678f9","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 variant?: \"floating\" | \"inline\";\n};\n\nexport function CartButton({ onOpen, variant = \"floating\" }: 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 aria-label={`Open cart, ${cart.count} ${cart.count === 1 ? \"item\" : \"items\"}`}\n disabled={!cart.isHydrated}\n onClick={onOpen}\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.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 +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":"097295c0dd4212ce32aefca7a79120c81cac42c912f0d6cce85ce49c1d7ec155","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 { Fragment, useEffect, useMemo, useRef } from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n};\n\nexport function CartDrawer({ open, onOpenChange }: 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\">\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 && <CartTotals />}\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\nfunction CartTotals() {\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 </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"57f9c0663f66de931d78a6eeda91d30b434b853638541f2a0c92c763858adbe2","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"}],"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"],"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.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 +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":"097295c0dd4212ce32aefca7a79120c81cac42c912f0d6cce85ce49c1d7ec155","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 { Fragment, useEffect, useMemo, useRef } from \"react\";\nimport \"./cart-drawer.css\";\n\nexport type CartDrawerProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n};\n\nexport function CartDrawer({ open, onOpenChange }: 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\">\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 && <CartTotals />}\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\nfunction CartTotals() {\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 </div>\n )}\n </Totals>\n );\n}\n"},{"path":"cart-drawer.css","target":"components/zappie/cart-drawer.css","sha256":"57f9c0663f66de931d78a6eeda91d30b434b853638541f2a0c92c763858adbe2","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"}],"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"],"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.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 +1 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.0","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":"034bf092d4a1911b5dc3790f25f1b468004aadce772173abcaaab60518365669","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, 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 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 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 [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\"\n ) {\n void cart.actions.quote();\n }\n }, [cart.actions, cart.isHydrated, cart.lines.length, cart.state.status]);\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 await cart.actions.quote({ force: true });\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 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.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"}
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","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.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["address-picker","fulfilment-switch","schedule-picker","checkout-button","cart-line","money"],"files":[{"path":"checkout-page.css","target":"components/zappie/checkout-page.css","sha256":"445916aa2a1f93ca05d843fac73e28659072055ab2541ae53a70ffd773a1db03","content":".z-kit-checkout-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-checkout-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-checkout-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-checkout-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-checkout-page ul {\n list-style: none;\n padding: 0;\n}\n.z-kit-checkout-page dl > div {\n display: flex;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n.z-kit-checkout-page dd {\n margin: 0;\n}\n"},{"path":"checkout-page.tsx","target":"components/zappie/checkout-page.tsx","sha256":"c6073493b5b4bf8e07bea5faabd5f4e12cf6c9f70b744af0da7a957db89e0390","content":"\"use client\";\n\nimport { AddressPicker } from \"@/components/zappie/address-picker\";\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { CheckoutButton } from \"@/components/zappie/checkout-button\";\nimport { FulfilmentSwitch } from \"@/components/zappie/fulfilment-switch\";\nimport { Money } from \"@/components/zappie/money\";\nimport { SchedulePicker } from \"@/components/zappie/schedule-picker\";\nimport { Cart } from \"@zappie/foundations\";\nimport type { Branch, CartFix, CartIssue } from \"@zappie/sdk\";\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { useId, useRef, useState } from \"react\";\nimport \"./checkout-page.css\";\n\nconst labels: 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 change_address: \"Change address\",\n choose_region: \"Choose delivery area\",\n schedule: \"Choose a time\",\n switch_fulfilment: \"Change fulfilment\",\n choose_option: \"Choose an option\",\n choose_other_option: \"Change an option\",\n};\n\n/** Collect checkout details and send the current cart to hosted payment. */\nexport function CheckoutPage({\n branch,\n checkout,\n places,\n onSignIn,\n onEditItem,\n returnUrl,\n cancelUrl,\n shopHref = \"/\",\n}: {\n branch: Branch;\n checkout: CheckoutClient;\n places: PlacesClient;\n onSignIn: () => void;\n /** Open your item editor for an option fix, keeping the line's current selections. */\n onEditItem: (lineId: string, groupId: string) => void;\n returnUrl?: string;\n cancelUrl?: string;\n shopHref?: string;\n}) {\n const cart = useCart();\n const [phone, setPhone] = useState(\"\");\n const [deliveryRequested, requestDelivery] = useState(false);\n const [error, setError] = useState(\"\");\n const phoneField = useRef<HTMLInputElement>(null);\n const addressSection = useRef<HTMLDivElement>(null);\n const regionField = useRef<HTMLSelectElement>(null);\n const scheduleSection = useRef<HTMLDetailsElement>(null);\n const fulfilmentSection = useRef<HTMLDivElement>(null);\n const regionId = useId();\n const fulfilment = cart.fulfilment;\n const address =\n fulfilment?.type === \"delivery\" ? fulfilment.address : undefined;\n const quote = \"quote\" in cart.state ? cart.state.quote : undefined;\n const fix = (issue: CartIssue, action: CartFix) => {\n setError(\"\");\n try {\n switch (action.action) {\n case \"choose_region\":\n if (regionField.current) {\n regionField.current.focus();\n } else {\n setError(\n \"Delivery areas could not load. Try pickup or contact the store.\",\n );\n }\n return;\n case \"change_address\":\n requestDelivery(true);\n addressSection.current?.focus();\n return;\n case \"schedule\":\n if (scheduleSection.current) {\n scheduleSection.current.open = true;\n scheduleSection.current.focus();\n }\n return;\n case \"switch_fulfilment\":\n fulfilmentSection.current?.focus();\n return;\n case \"choose_option\":\n case \"choose_other_option\":\n if (issue.lineId) {\n onEditItem(issue.lineId, action.groupId);\n }\n return;\n default:\n cart.actions.applyFix(issue, action);\n }\n } catch {\n setError(\"We could not apply that change. Try again.\");\n }\n };\n if (!cart.isHydrated) {\n return <p role=\"status\">Loading your cart…</p>;\n }\n if (cart.lines.length === 0) {\n return (\n <section className=\"z-kit-checkout-page\">\n <h2>Your cart is empty</h2>\n <a href={shopHref}>Keep shopping</a>\n </section>\n );\n }\n return (\n <section className=\"z-kit-checkout-page\" aria-label=\"Checkout\">\n <h2>Checkout</h2>\n <label>\n Contact phone\n <input\n ref={phoneField}\n type=\"tel\"\n autoComplete=\"tel\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n />\n </label>\n <div ref={fulfilmentSection} tabIndex={-1} aria-label=\"Choose fulfilment\">\n <FulfilmentSwitch\n branchId={branch.id}\n {...(address ? { deliveryAddress: address } : {})}\n deliveryRequested={deliveryRequested}\n onDeliveryRequest={() => requestDelivery(true)}\n onChange={() => requestDelivery(false)}\n />\n </div>\n {(fulfilment?.type === \"delivery\" || deliveryRequested) && (\n <div ref={addressSection} tabIndex={-1} aria-label=\"Choose address\">\n {address && <p>{address.line1}</p>}\n <AddressPicker\n places={places}\n branchId={branch.id}\n onChange={(next) => {\n cart.actions.setFulfilment({\n type: \"delivery\",\n branchId: branch.id,\n address: next,\n });\n requestDelivery(false);\n }}\n />\n {fulfilment?.type === \"delivery\" && (\n <>\n {(branch.deliveryRegions?.length ?? 0) > 0 && (\n <label htmlFor={regionId}>\n Delivery area\n <select\n id={regionId}\n ref={regionField}\n value={fulfilment.deliveryRegionId ?? \"\"}\n onChange={(event) => {\n const { deliveryRegionId: _old, ...rest } = fulfilment;\n cart.actions.setFulfilment(\n event.target.value\n ? { ...rest, deliveryRegionId: event.target.value }\n : rest,\n );\n }}\n >\n <option value=\"\">Choose an area</option>\n {branch.deliveryRegions?.map((region) => (\n <option key={region.id} value={region.id}>\n {region.name}\n </option>\n ))}\n </select>\n </label>\n )}\n <label>\n Delivery instructions\n <input\n maxLength={200}\n value={address?.instructions ?? \"\"}\n onChange={(event) =>\n cart.actions.setFulfilment({\n ...fulfilment,\n address: {\n ...fulfilment.address,\n instructions: event.target.value,\n },\n })\n }\n />\n </label>\n </>\n )}\n </div>\n )}\n <details\n ref={scheduleSection}\n tabIndex={-1}\n aria-label=\"Choose order time\"\n >\n <summary>\n Order time:{\" \"}\n {fulfilment &&\n fulfilment.type !== \"dine_in\" &&\n fulfilment.scheduledFor\n ? fulfilment.scheduledFor\n : \"As soon as possible\"}\n </summary>\n <SchedulePicker branch={branch} />\n </details>\n <h3>Your order</h3>\n <Cart.Root>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n </Cart.Root>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n cart.actions.setPromoCode(\n String(\n new FormData(event.currentTarget).get(\"promo\") ?? \"\",\n ).trim() || null,\n );\n }}\n >\n <label>\n Promo code\n <input name=\"promo\" defaultValue={cart.promoCode ?? \"\"} />\n </label>\n <button type=\"submit\">Apply promo</button>\n </form>\n <div aria-live=\"polite\">\n {cart.issues.map((issue, index) => (\n <div key={`${issue.code}:${index}`} data-zappie-issue={issue.code}>\n <p>{issue.message}</p>\n {issue.fixes.map((action, index) => (\n <button\n key={index}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => fix(issue, action)}\n >\n {labels[action.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n {error && <p role=\"alert\">{error}</p>}\n {cart.state.status === \"quoting\" && (\n <p role=\"status\">Updating your total…</p>\n )}\n {quote && (\n <dl>\n <div>\n <dt>Subtotal</dt>\n <dd>\n <Money value={quote.subtotal} />\n </dd>\n </div>\n {quote.delivery && (\n <div>\n <dt>Delivery</dt>\n <dd>\n <Money value={quote.delivery} />\n </dd>\n </div>\n )}\n {quote.fees.map((fee, index) => (\n <div key={index}>\n <dt>{fee.label}</dt>\n <dd>\n <Money value={fee.amount} />\n </dd>\n </div>\n ))}\n {quote.discounts.map((discount, index) => (\n <div key={index}>\n <dt>{discount.label}</dt>\n <dd>\n <Money value={discount.amount} />\n </dd>\n </div>\n ))}\n <div data-zappie-total=\"\">\n <dt>Total</dt>\n <dd>\n <Money value={quote.total} />\n </dd>\n </div>\n </dl>\n )}\n <p>Choose your payment method on the payment page.</p>\n {fulfilment ? (\n <CheckoutButton\n checkout={checkout}\n branchId={branch.id}\n contactPhone={phone}\n onSignIn={onSignIn}\n onError={() => setError(\"Checkout could not update. Try again.\")}\n onContactPhoneRequired={() => phoneField.current?.focus()}\n {...(returnUrl ? { returnUrl } : {})}\n {...(cancelUrl ? { cancelUrl } : {})}\n />\n ) : (\n <p>Choose pickup or delivery to continue.</p>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","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.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["address-picker","fulfilment-switch","schedule-picker","checkout-button","cart-line","money"],"files":[{"path":"checkout-page.css","target":"components/zappie/checkout-page.css","sha256":"445916aa2a1f93ca05d843fac73e28659072055ab2541ae53a70ffd773a1db03","content":".z-kit-checkout-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-checkout-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-checkout-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-checkout-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-checkout-page ul {\n list-style: none;\n padding: 0;\n}\n.z-kit-checkout-page dl > div {\n display: flex;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n.z-kit-checkout-page dd {\n margin: 0;\n}\n"},{"path":"checkout-page.tsx","target":"components/zappie/checkout-page.tsx","sha256":"c6073493b5b4bf8e07bea5faabd5f4e12cf6c9f70b744af0da7a957db89e0390","content":"\"use client\";\n\nimport { AddressPicker } from \"@/components/zappie/address-picker\";\nimport { CartLine } from \"@/components/zappie/cart-line\";\nimport { CheckoutButton } from \"@/components/zappie/checkout-button\";\nimport { FulfilmentSwitch } from \"@/components/zappie/fulfilment-switch\";\nimport { Money } from \"@/components/zappie/money\";\nimport { SchedulePicker } from \"@/components/zappie/schedule-picker\";\nimport { Cart } from \"@zappie/foundations\";\nimport type { Branch, CartFix, CartIssue } from \"@zappie/sdk\";\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { useId, useRef, useState } from \"react\";\nimport \"./checkout-page.css\";\n\nconst labels: 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 change_address: \"Change address\",\n choose_region: \"Choose delivery area\",\n schedule: \"Choose a time\",\n switch_fulfilment: \"Change fulfilment\",\n choose_option: \"Choose an option\",\n choose_other_option: \"Change an option\",\n};\n\n/** Collect checkout details and send the current cart to hosted payment. */\nexport function CheckoutPage({\n branch,\n checkout,\n places,\n onSignIn,\n onEditItem,\n returnUrl,\n cancelUrl,\n shopHref = \"/\",\n}: {\n branch: Branch;\n checkout: CheckoutClient;\n places: PlacesClient;\n onSignIn: () => void;\n /** Open your item editor for an option fix, keeping the line's current selections. */\n onEditItem: (lineId: string, groupId: string) => void;\n returnUrl?: string;\n cancelUrl?: string;\n shopHref?: string;\n}) {\n const cart = useCart();\n const [phone, setPhone] = useState(\"\");\n const [deliveryRequested, requestDelivery] = useState(false);\n const [error, setError] = useState(\"\");\n const phoneField = useRef<HTMLInputElement>(null);\n const addressSection = useRef<HTMLDivElement>(null);\n const regionField = useRef<HTMLSelectElement>(null);\n const scheduleSection = useRef<HTMLDetailsElement>(null);\n const fulfilmentSection = useRef<HTMLDivElement>(null);\n const regionId = useId();\n const fulfilment = cart.fulfilment;\n const address =\n fulfilment?.type === \"delivery\" ? fulfilment.address : undefined;\n const quote = \"quote\" in cart.state ? cart.state.quote : undefined;\n const fix = (issue: CartIssue, action: CartFix) => {\n setError(\"\");\n try {\n switch (action.action) {\n case \"choose_region\":\n if (regionField.current) {\n regionField.current.focus();\n } else {\n setError(\n \"Delivery areas could not load. Try pickup or contact the store.\",\n );\n }\n return;\n case \"change_address\":\n requestDelivery(true);\n addressSection.current?.focus();\n return;\n case \"schedule\":\n if (scheduleSection.current) {\n scheduleSection.current.open = true;\n scheduleSection.current.focus();\n }\n return;\n case \"switch_fulfilment\":\n fulfilmentSection.current?.focus();\n return;\n case \"choose_option\":\n case \"choose_other_option\":\n if (issue.lineId) {\n onEditItem(issue.lineId, action.groupId);\n }\n return;\n default:\n cart.actions.applyFix(issue, action);\n }\n } catch {\n setError(\"We could not apply that change. Try again.\");\n }\n };\n if (!cart.isHydrated) {\n return <p role=\"status\">Loading your cart…</p>;\n }\n if (cart.lines.length === 0) {\n return (\n <section className=\"z-kit-checkout-page\">\n <h2>Your cart is empty</h2>\n <a href={shopHref}>Keep shopping</a>\n </section>\n );\n }\n return (\n <section className=\"z-kit-checkout-page\" aria-label=\"Checkout\">\n <h2>Checkout</h2>\n <label>\n Contact phone\n <input\n ref={phoneField}\n type=\"tel\"\n autoComplete=\"tel\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n />\n </label>\n <div ref={fulfilmentSection} tabIndex={-1} aria-label=\"Choose fulfilment\">\n <FulfilmentSwitch\n branchId={branch.id}\n {...(address ? { deliveryAddress: address } : {})}\n deliveryRequested={deliveryRequested}\n onDeliveryRequest={() => requestDelivery(true)}\n onChange={() => requestDelivery(false)}\n />\n </div>\n {(fulfilment?.type === \"delivery\" || deliveryRequested) && (\n <div ref={addressSection} tabIndex={-1} aria-label=\"Choose address\">\n {address && <p>{address.line1}</p>}\n <AddressPicker\n places={places}\n branchId={branch.id}\n onChange={(next) => {\n cart.actions.setFulfilment({\n type: \"delivery\",\n branchId: branch.id,\n address: next,\n });\n requestDelivery(false);\n }}\n />\n {fulfilment?.type === \"delivery\" && (\n <>\n {(branch.deliveryRegions?.length ?? 0) > 0 && (\n <label htmlFor={regionId}>\n Delivery area\n <select\n id={regionId}\n ref={regionField}\n value={fulfilment.deliveryRegionId ?? \"\"}\n onChange={(event) => {\n const { deliveryRegionId: _old, ...rest } = fulfilment;\n cart.actions.setFulfilment(\n event.target.value\n ? { ...rest, deliveryRegionId: event.target.value }\n : rest,\n );\n }}\n >\n <option value=\"\">Choose an area</option>\n {branch.deliveryRegions?.map((region) => (\n <option key={region.id} value={region.id}>\n {region.name}\n </option>\n ))}\n </select>\n </label>\n )}\n <label>\n Delivery instructions\n <input\n maxLength={200}\n value={address?.instructions ?? \"\"}\n onChange={(event) =>\n cart.actions.setFulfilment({\n ...fulfilment,\n address: {\n ...fulfilment.address,\n instructions: event.target.value,\n },\n })\n }\n />\n </label>\n </>\n )}\n </div>\n )}\n <details\n ref={scheduleSection}\n tabIndex={-1}\n aria-label=\"Choose order time\"\n >\n <summary>\n Order time:{\" \"}\n {fulfilment &&\n fulfilment.type !== \"dine_in\" &&\n fulfilment.scheduledFor\n ? fulfilment.scheduledFor\n : \"As soon as possible\"}\n </summary>\n <SchedulePicker branch={branch} />\n </details>\n <h3>Your order</h3>\n <Cart.Root>\n {cart.lines.map((line) => (\n <CartLine key={line.id} line={line} />\n ))}\n </Cart.Root>\n <form\n onSubmit={(event) => {\n event.preventDefault();\n cart.actions.setPromoCode(\n String(\n new FormData(event.currentTarget).get(\"promo\") ?? \"\",\n ).trim() || null,\n );\n }}\n >\n <label>\n Promo code\n <input name=\"promo\" defaultValue={cart.promoCode ?? \"\"} />\n </label>\n <button type=\"submit\">Apply promo</button>\n </form>\n <div aria-live=\"polite\">\n {cart.issues.map((issue, index) => (\n <div key={`${issue.code}:${index}`} data-zappie-issue={issue.code}>\n <p>{issue.message}</p>\n {issue.fixes.map((action, index) => (\n <button\n key={index}\n type=\"button\"\n data-zappie-issue-fix=\"\"\n onClick={() => fix(issue, action)}\n >\n {labels[action.action]}\n </button>\n ))}\n </div>\n ))}\n </div>\n {error && <p role=\"alert\">{error}</p>}\n {cart.state.status === \"quoting\" && (\n <p role=\"status\">Updating your total…</p>\n )}\n {quote && (\n <dl>\n <div>\n <dt>Subtotal</dt>\n <dd>\n <Money value={quote.subtotal} />\n </dd>\n </div>\n {quote.delivery && (\n <div>\n <dt>Delivery</dt>\n <dd>\n <Money value={quote.delivery} />\n </dd>\n </div>\n )}\n {quote.fees.map((fee, index) => (\n <div key={index}>\n <dt>{fee.label}</dt>\n <dd>\n <Money value={fee.amount} />\n </dd>\n </div>\n ))}\n {quote.discounts.map((discount, index) => (\n <div key={index}>\n <dt>{discount.label}</dt>\n <dd>\n <Money value={discount.amount} />\n </dd>\n </div>\n ))}\n <div data-zappie-total=\"\">\n <dt>Total</dt>\n <dd>\n <Money value={quote.total} />\n </dd>\n </div>\n </dl>\n )}\n <p>Choose your payment method on the payment page.</p>\n {fulfilment ? (\n <CheckoutButton\n checkout={checkout}\n branchId={branch.id}\n contactPhone={phone}\n onSignIn={onSignIn}\n onError={() => setError(\"Checkout could not update. Try again.\")}\n onContactPhoneRequired={() => phoneField.current?.focus()}\n {...(returnUrl ? { returnUrl } : {})}\n {...(cancelUrl ? { cancelUrl } : {})}\n />\n ) : (\n <p>Choose pickup or delivery to continue.</p>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-return.css","target":"components/zappie/checkout-return.css","sha256":"fc5678407d5800623d2dab20de0b48a0bf7542b257c755d9fc186bd9fece5616","content":".z-kit-checkout-return {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-checkout-return :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-checkout-return :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-checkout-return :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-checkout-return ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"checkout-return.tsx","target":"components/zappie/checkout-return.tsx","sha256":"86d494e3599ca20bdf02d686638550e436e6bb5328d070b30edfa156ae28fb81","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./checkout-return.css\";\n\n/** Confirm a checkout session and clear the cart only after an order completes. */\nexport function CheckoutReturn({\n checkout,\n sessionId,\n orderHref = (id) => `/orders/${encodeURIComponent(id)}`,\n checkoutHref = \"/checkout\",\n ordersHref = \"/orders\",\n shopHref = \"/\",\n}: {\n checkout: CheckoutClient;\n /** Read the session query parameter in your router and pass it here. */\n sessionId?: string;\n orderHref?: (id: string) => string;\n checkoutHref?: string;\n ordersHref?: string;\n shopHref?: string;\n}) {\n const cart = useCart();\n const [result, setResult] = useState<{\n session: string;\n state: \"completed\" | \"ended\" | \"unknown\";\n orderId?: string;\n }>();\n useEffect(() => {\n if (!sessionId) {\n return;\n }\n const controller = new AbortController();\n void checkout\n .awaitCompletion(sessionId, { signal: controller.signal })\n .then(\n (session) => {\n if (controller.signal.aborted) {\n return;\n }\n if (session.status === \"completed\") {\n cart.actions.clear();\n setResult({\n session: sessionId,\n state: \"completed\",\n orderId: session.order_id,\n });\n } else {\n setResult({ session: sessionId, state: \"ended\" });\n }\n },\n () => {\n if (!controller.signal.aborted) {\n setResult({ session: sessionId, state: \"unknown\" });\n }\n },\n );\n return () => controller.abort();\n }, [checkout, sessionId, cart.actions]);\n const current = result?.session === sessionId ? result : undefined;\n return (\n <section className=\"z-kit-checkout-return\" aria-live=\"polite\">\n {!sessionId ? (\n <>\n <h2>Nothing to confirm</h2>\n <p>This page opens after a payment.</p>\n <a href={shopHref}>Keep shopping</a>\n </>\n ) : current?.state === \"completed\" && current.orderId ? (\n <>\n <h2>Order placed</h2>\n <a href={orderHref(current.orderId)}>View your order</a>\n </>\n ) : current?.state === \"ended\" ? (\n <>\n <h2>That payment ended</h2>\n <p>\n Your cart is still here. Check your orders if you made a payment.\n </p>\n <a href={checkoutHref}>Back to checkout</a>\n </>\n ) : current?.state === \"unknown\" ? (\n <>\n <h2>We could not confirm it here</h2>\n <p>If you paid, check your orders.</p>\n <a href={ordersHref}>Your orders</a>\n </>\n ) : (\n <>\n <h2>Confirming your payment</h2>\n <p role=\"status\">This can take a few seconds.</p>\n </>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-return"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-return","type":"block","version":"1.0.0","title":"Checkout return","description":"Confirm payment and link to the completed order.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"checkout-return.css","target":"components/zappie/checkout-return.css","sha256":"fc5678407d5800623d2dab20de0b48a0bf7542b257c755d9fc186bd9fece5616","content":".z-kit-checkout-return {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-checkout-return :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-checkout-return :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-checkout-return :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-checkout-return ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"checkout-return.tsx","target":"components/zappie/checkout-return.tsx","sha256":"86d494e3599ca20bdf02d686638550e436e6bb5328d070b30edfa156ae28fb81","content":"\"use client\";\n\nimport type { CheckoutClient } from \"@zappie/sdk/checkout\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./checkout-return.css\";\n\n/** Confirm a checkout session and clear the cart only after an order completes. */\nexport function CheckoutReturn({\n checkout,\n sessionId,\n orderHref = (id) => `/orders/${encodeURIComponent(id)}`,\n checkoutHref = \"/checkout\",\n ordersHref = \"/orders\",\n shopHref = \"/\",\n}: {\n checkout: CheckoutClient;\n /** Read the session query parameter in your router and pass it here. */\n sessionId?: string;\n orderHref?: (id: string) => string;\n checkoutHref?: string;\n ordersHref?: string;\n shopHref?: string;\n}) {\n const cart = useCart();\n const [result, setResult] = useState<{\n session: string;\n state: \"completed\" | \"ended\" | \"unknown\";\n orderId?: string;\n }>();\n useEffect(() => {\n if (!sessionId) {\n return;\n }\n const controller = new AbortController();\n void checkout\n .awaitCompletion(sessionId, { signal: controller.signal })\n .then(\n (session) => {\n if (controller.signal.aborted) {\n return;\n }\n if (session.status === \"completed\") {\n cart.actions.clear();\n setResult({\n session: sessionId,\n state: \"completed\",\n orderId: session.order_id,\n });\n } else {\n setResult({ session: sessionId, state: \"ended\" });\n }\n },\n () => {\n if (!controller.signal.aborted) {\n setResult({ session: sessionId, state: \"unknown\" });\n }\n },\n );\n return () => controller.abort();\n }, [checkout, sessionId, cart.actions]);\n const current = result?.session === sessionId ? result : undefined;\n return (\n <section className=\"z-kit-checkout-return\" aria-live=\"polite\">\n {!sessionId ? (\n <>\n <h2>Nothing to confirm</h2>\n <p>This page opens after a payment.</p>\n <a href={shopHref}>Keep shopping</a>\n </>\n ) : current?.state === \"completed\" && current.orderId ? (\n <>\n <h2>Order placed</h2>\n <a href={orderHref(current.orderId)}>View your order</a>\n </>\n ) : current?.state === \"ended\" ? (\n <>\n <h2>That payment ended</h2>\n <p>\n Your cart is still here. Check your orders if you made a payment.\n </p>\n <a href={checkoutHref}>Back to checkout</a>\n </>\n ) : current?.state === \"unknown\" ? (\n <>\n <h2>We could not confirm it here</h2>\n <p>If you paid, check your orders.</p>\n <a href={ordersHref}>Your orders</a>\n </>\n ) : (\n <>\n <h2>Confirming your payment</h2>\n <p role=\"status\">This can take a few seconds.</p>\n </>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/checkout-return"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"consent.css","target":"components/zappie/consent.css","sha256":"2c3687d7561528c54135d8518fc775987eb7cc882af00aaad69b427a9a145405","content":".z-kit-consent {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-consent :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-consent :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-consent :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-consent ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"consent.tsx","target":"components/zappie/consent.tsx","sha256":"6247ceff308d5f0cbcb77f7feac22038375be321663056a1bc4cc8a8fad342f1","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport \"./consent.css\";\n\n/** The shopper's choice for optional tracking, or unknown before they choose. */\nexport type ConsentChoice = \"unknown\" | \"allowed\" | \"declined\";\n\n/** Save a privacy choice and let the shopper withdraw it. This component loads no tracking tags. */\nexport function Consent({\n storageKey,\n privacyHref,\n onChange,\n}: {\n /** Use a key unique to this store and consent policy version. */\n storageKey: string;\n privacyHref: string;\n /** Receive the stored choice after hydration and each change, including withdrawal. */\n onChange?: (choice: ConsentChoice) => void;\n}) {\n const [saved, setSaved] = useState<{ key: string; choice: ConsentChoice }>({\n key: \"\",\n choice: \"unknown\",\n });\n const choice = saved.key === storageKey ? saved.choice : \"unknown\";\n const [storageError, setStorageError] = useState(false);\n useEffect(() => {\n let next: ConsentChoice = \"unknown\";\n try {\n const value = localStorage.getItem(storageKey);\n if (value === \"allowed\" || value === \"declined\") {\n next = value;\n }\n } catch {\n // An unreadable choice grants no consent.\n }\n setStorageError(false);\n setSaved({ key: storageKey, choice: next });\n }, [storageKey]);\n useEffect(() => {\n if (saved.key === storageKey) {\n onChange?.(saved.choice);\n }\n }, [saved, storageKey, onChange]);\n const choose = (next: \"allowed\" | \"declined\") => {\n setStorageError(false);\n try {\n localStorage.setItem(storageKey, next);\n } catch {\n setStorageError(true);\n if (next === \"allowed\") {\n return;\n }\n try {\n localStorage.removeItem(storageKey);\n } catch {\n // Withdrawal still applies to the current page through onChange.\n }\n }\n setSaved({ key: storageKey, choice: next });\n };\n return (\n <section className=\"z-kit-consent\" aria-label=\"Privacy choices\">\n <h2>Privacy choices</h2>\n <p>\n {choice === \"unknown\"\n ? \"Allow optional tracking to help us understand how you use this store?\"\n : choice === \"allowed\"\n ? \"Optional tracking is allowed.\"\n : \"Optional tracking is off.\"}\n </p>\n <a href={privacyHref}>Read the privacy policy</a>\n {choice !== \"allowed\" && (\n <button type=\"button\" onClick={() => choose(\"allowed\")}>\n Allow tracking\n </button>\n )}\n {choice !== \"declined\" && (\n <button type=\"button\" onClick={() => choose(\"declined\")}>\n {choice === \"allowed\" ? \"Withdraw consent\" : \"Decline tracking\"}\n </button>\n )}\n {storageError && (\n <p role=\"alert\">\n We could not save your privacy choice. Please try again.\n </p>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/consent"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"consent","type":"component","version":"1.0.0","title":"Consent","description":"Save your privacy choice and withdraw consent when you choose.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"consent.css","target":"components/zappie/consent.css","sha256":"2c3687d7561528c54135d8518fc775987eb7cc882af00aaad69b427a9a145405","content":".z-kit-consent {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-consent :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-consent :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-consent :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-consent ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"consent.tsx","target":"components/zappie/consent.tsx","sha256":"6247ceff308d5f0cbcb77f7feac22038375be321663056a1bc4cc8a8fad342f1","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport \"./consent.css\";\n\n/** The shopper's choice for optional tracking, or unknown before they choose. */\nexport type ConsentChoice = \"unknown\" | \"allowed\" | \"declined\";\n\n/** Save a privacy choice and let the shopper withdraw it. This component loads no tracking tags. */\nexport function Consent({\n storageKey,\n privacyHref,\n onChange,\n}: {\n /** Use a key unique to this store and consent policy version. */\n storageKey: string;\n privacyHref: string;\n /** Receive the stored choice after hydration and each change, including withdrawal. */\n onChange?: (choice: ConsentChoice) => void;\n}) {\n const [saved, setSaved] = useState<{ key: string; choice: ConsentChoice }>({\n key: \"\",\n choice: \"unknown\",\n });\n const choice = saved.key === storageKey ? saved.choice : \"unknown\";\n const [storageError, setStorageError] = useState(false);\n useEffect(() => {\n let next: ConsentChoice = \"unknown\";\n try {\n const value = localStorage.getItem(storageKey);\n if (value === \"allowed\" || value === \"declined\") {\n next = value;\n }\n } catch {\n // An unreadable choice grants no consent.\n }\n setStorageError(false);\n setSaved({ key: storageKey, choice: next });\n }, [storageKey]);\n useEffect(() => {\n if (saved.key === storageKey) {\n onChange?.(saved.choice);\n }\n }, [saved, storageKey, onChange]);\n const choose = (next: \"allowed\" | \"declined\") => {\n setStorageError(false);\n try {\n localStorage.setItem(storageKey, next);\n } catch {\n setStorageError(true);\n if (next === \"allowed\") {\n return;\n }\n try {\n localStorage.removeItem(storageKey);\n } catch {\n // Withdrawal still applies to the current page through onChange.\n }\n }\n setSaved({ key: storageKey, choice: next });\n };\n return (\n <section className=\"z-kit-consent\" aria-label=\"Privacy choices\">\n <h2>Privacy choices</h2>\n <p>\n {choice === \"unknown\"\n ? \"Allow optional tracking to help us understand how you use this store?\"\n : choice === \"allowed\"\n ? \"Optional tracking is allowed.\"\n : \"Optional tracking is off.\"}\n </p>\n <a href={privacyHref}>Read the privacy policy</a>\n {choice !== \"allowed\" && (\n <button type=\"button\" onClick={() => choose(\"allowed\")}>\n Allow tracking\n </button>\n )}\n {choice !== \"declined\" && (\n <button type=\"button\" onClick={() => choose(\"declined\")}>\n {choice === \"allowed\" ? \"Withdraw consent\" : \"Decline tracking\"}\n </button>\n )}\n {storageError && (\n <p role=\"alert\">\n We could not save your privacy choice. Please try again.\n </p>\n )}\n </section>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/consent"}
@@ -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":"dfab55753284002fccc66e365f3ee0f13e683df20ad9e77c8d80da9533ad338c","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: var(--z-media-sm) 1fr;\n align-items: stretch;\n}\n\n.z-kit-item-card__image {\n width: 100%;\n height: 100%;\n min-height: var(--z-media-sm);\n object-fit: cover;\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.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 +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":"dfab55753284002fccc66e365f3ee0f13e683df20ad9e77c8d80da9533ad338c","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: var(--z-media-sm) 1fr;\n align-items: stretch;\n}\n\n.z-kit-item-card__image {\n width: 100%;\n height: 100%;\n min-height: var(--z-media-sm);\n object-fit: cover;\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.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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button","money"],"files":[{"path":"orders-page.css","target":"components/zappie/orders-page.css","sha256":"ccae7e1f5882baab31f2bef3223e664b7dc0394418306adba44373962e638df6","content":".z-kit-orders-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-orders-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-orders-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-orders-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-orders-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"orders-page.tsx","target":"components/zappie/orders-page.tsx","sha256":"48d06171c7856ac2fb3ec855e9760ab77291f43e943a9072eb26aee3977f5602","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport { money } from \"@zappie/sdk\";\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./orders-page.css\";\n\n/** List your orders and load older orders a page at a time. */\nexport function OrdersPage({\n customer,\n orderHref = (id) => `/orders/${encodeURIComponent(id)}`,\n shopHref = \"/\",\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n /** Link to a route that renders the kit OrderPage or OrderStatus. */\n orderHref?: (id: string) => string;\n shopHref?: string;\n}) {\n const auth = useAuth();\n return (\n <section className=\"z-kit-orders-page\" aria-label=\"Your orders\">\n <h2>Your orders</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Orders\n key={auth.customer.id}\n customer={customer}\n orderHref={orderHref}\n />\n ) : (\n <SignInButton />\n )}\n <a href={shopHref}>Keep shopping</a>\n </section>\n );\n}\n\nfunction Orders({\n customer,\n orderHref,\n}: {\n customer: CustomerClient;\n orderHref: (id: string) => string;\n}) {\n const [orders, setOrders] = useState<CustomerOrder[]>([]);\n const [cursor, setCursor] = useState<string | null>();\n const [busy, setBusy] = useState(true);\n const [error, setError] = useState(false);\n const [attempt, setAttempt] = useState(0);\n useEffect(() => {\n let active = true;\n setBusy(true);\n setError(false);\n void customer\n .orders()\n .then(\n (page) => {\n if (active) {\n setOrders(page.orders);\n setCursor(page.next_cursor);\n }\n },\n () => {\n if (active) {\n setError(true);\n }\n },\n )\n .finally(() => {\n if (active) {\n setBusy(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const more = async () => {\n if (!cursor || busy) {\n return;\n }\n setBusy(true);\n setError(false);\n try {\n const page = await customer.orders({ cursor });\n setOrders((current) => [\n ...new Map(\n [...current, ...page.orders].map((order) => [order.id, order]),\n ).values(),\n ]);\n setCursor(page.next_cursor);\n } catch {\n setError(true);\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {busy && <p role=\"status\">Loading your orders…</p>}\n {error && (\n <div role=\"alert\">\n <p>We could not load your orders.</p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() =>\n cursor ? void more() : setAttempt((value) => value + 1)\n }\n >\n Try again\n </button>\n </div>\n )}\n {!busy && !error && orders.length === 0 && <p>No orders yet.</p>}\n <ul>\n {orders.map((order) => (\n <li key={order.id}>\n <a href={orderHref(order.id)}>\n <span>\n Order {order.id} · {order.status.replaceAll(\"_\", \" \")}\n </span>\n <time dateTime={order.placed_at}>\n {new Date(order.placed_at).toLocaleDateString()}\n </time>\n <Money\n value={money(order.total.amount_minor, order.total.currency)}\n />\n </a>\n </li>\n ))}\n </ul>\n {cursor && (\n <button type=\"button\" disabled={busy} onClick={() => void more()}>\n Older orders\n </button>\n )}\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/orders-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"orders-page","type":"block","version":"1.0.0","title":"Orders page","description":"Browse your orders and load older orders.","requires":{"@zappie/sdk":"^0.1.4","@zappie/foundations":"^0.1.2","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["sign-in-button","money"],"files":[{"path":"orders-page.css","target":"components/zappie/orders-page.css","sha256":"ccae7e1f5882baab31f2bef3223e664b7dc0394418306adba44373962e638df6","content":".z-kit-orders-page {\n display: grid;\n gap: var(--z-space-4);\n color: var(--z-text);\n font-family: var(--z-font-body);\n max-width: 48rem;\n}\n.z-kit-orders-page :where(form, label, li, a) {\n display: grid;\n gap: var(--z-space-2);\n}\n.z-kit-orders-page :where(input, button) {\n font: inherit;\n color: var(--z-text);\n background: var(--z-surface);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-sm);\n padding: var(--z-space-3);\n min-width: 0;\n}\n.z-kit-orders-page :where(input, button, a):focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n.z-kit-orders-page ul {\n list-style: none;\n padding: 0;\n}\n"},{"path":"orders-page.tsx","target":"components/zappie/orders-page.tsx","sha256":"48d06171c7856ac2fb3ec855e9760ab77291f43e943a9072eb26aee3977f5602","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { SignInButton } from \"@/components/zappie/sign-in-button\";\nimport { money } from \"@zappie/sdk\";\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport { useEffect, useState } from \"react\";\nimport \"./orders-page.css\";\n\n/** List your orders and load older orders a page at a time. */\nexport function OrdersPage({\n customer,\n orderHref = (id) => `/orders/${encodeURIComponent(id)}`,\n shopHref = \"/\",\n}: {\n /** Use the same client as your CustomerProvider. */\n customer: CustomerClient;\n /** Link to a route that renders the kit OrderPage or OrderStatus. */\n orderHref?: (id: string) => string;\n shopHref?: string;\n}) {\n const auth = useAuth();\n return (\n <section className=\"z-kit-orders-page\" aria-label=\"Your orders\">\n <h2>Your orders</h2>\n {auth.state === \"unknown\" ? (\n <p role=\"status\">Checking your session…</p>\n ) : auth.customer ? (\n <Orders\n key={auth.customer.id}\n customer={customer}\n orderHref={orderHref}\n />\n ) : (\n <SignInButton />\n )}\n <a href={shopHref}>Keep shopping</a>\n </section>\n );\n}\n\nfunction Orders({\n customer,\n orderHref,\n}: {\n customer: CustomerClient;\n orderHref: (id: string) => string;\n}) {\n const [orders, setOrders] = useState<CustomerOrder[]>([]);\n const [cursor, setCursor] = useState<string | null>();\n const [busy, setBusy] = useState(true);\n const [error, setError] = useState(false);\n const [attempt, setAttempt] = useState(0);\n useEffect(() => {\n let active = true;\n setBusy(true);\n setError(false);\n void customer\n .orders()\n .then(\n (page) => {\n if (active) {\n setOrders(page.orders);\n setCursor(page.next_cursor);\n }\n },\n () => {\n if (active) {\n setError(true);\n }\n },\n )\n .finally(() => {\n if (active) {\n setBusy(false);\n }\n });\n return () => {\n active = false;\n };\n }, [customer, attempt]);\n const more = async () => {\n if (!cursor || busy) {\n return;\n }\n setBusy(true);\n setError(false);\n try {\n const page = await customer.orders({ cursor });\n setOrders((current) => [\n ...new Map(\n [...current, ...page.orders].map((order) => [order.id, order]),\n ).values(),\n ]);\n setCursor(page.next_cursor);\n } catch {\n setError(true);\n } finally {\n setBusy(false);\n }\n };\n return (\n <div>\n {busy && <p role=\"status\">Loading your orders…</p>}\n {error && (\n <div role=\"alert\">\n <p>We could not load your orders.</p>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() =>\n cursor ? void more() : setAttempt((value) => value + 1)\n }\n >\n Try again\n </button>\n </div>\n )}\n {!busy && !error && orders.length === 0 && <p>No orders yet.</p>}\n <ul>\n {orders.map((order) => (\n <li key={order.id}>\n <a href={orderHref(order.id)}>\n <span>\n Order {order.id} · {order.status.replaceAll(\"_\", \" \")}\n </span>\n <time dateTime={order.placed_at}>\n {new Date(order.placed_at).toLocaleDateString()}\n </time>\n <Money\n value={money(order.total.amount_minor, order.total.currency)}\n />\n </a>\n </li>\n ))}\n </ul>\n {cursor && (\n <button type=\"button\" disabled={busy} onClick={() => void more()}>\n Older orders\n </button>\n )}\n </div>\n );\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-text","--z-surface","--z-border","--z-radius-sm","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/orders-page"}
@@ -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":"75d7817a581836260773e3f4a2f9906f41cb3d015c2a7fc8fb73575deb91a4a1","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/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 && (\n <img src={store.data.brand.logoUrl} alt=\"\" />\n )}\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\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":"5d8a57cac0287330b8d35e49bcb1887aaafab13ccbe1a524f5ba3befa3c41d03","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 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 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.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 +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":"75d7817a581836260773e3f4a2f9906f41cb3d015c2a7fc8fb73575deb91a4a1","content":"\"use client\";\n\nimport { OpenStatus } from \"@/components/zappie/open-status\";\nimport { useCart, useStore } from \"@zappie/sdk/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 && (\n <img src={store.data.brand.logoUrl} alt=\"\" />\n )}\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\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":"5d8a57cac0287330b8d35e49bcb1887aaafab13ccbe1a524f5ba3befa3c41d03","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 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 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.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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zappie/kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "The registry of copy-in Zappie storefront components, installed with the zappie CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"checkout-button","type":"component","version":"1.0.0","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":"034bf092d4a1911b5dc3790f25f1b468004aadce772173abcaaab60518365669","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, 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 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 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 [forced, setForced] = useState<\"sign_in\" | undefined>();\n useEffect(() => {\n if (\n cart.isHydrated &&\n cart.lines.length > 0 &&\n cart.state.status === \"ready\"\n ) {\n void cart.actions.quote();\n }\n }, [cart.actions, cart.isHydrated, cart.lines.length, cart.state.status]);\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 await cart.actions.quote({ force: true });\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 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"}