@zappie/kit 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +26 -2
  4. package/dist/registry/v1/index.json +1 -0
  5. package/dist/registry/v1/items/address-picker.json +1 -0
  6. package/dist/registry/v1/items/address-picker@1.0.0.json +1 -0
  7. package/dist/registry/v1/items/cart-button.json +1 -0
  8. package/dist/registry/v1/items/cart-button@1.0.0.json +1 -0
  9. package/dist/registry/v1/items/cart-drawer.json +1 -0
  10. package/dist/registry/v1/items/cart-drawer@1.0.0.json +1 -0
  11. package/dist/registry/v1/items/cart-line.json +1 -0
  12. package/dist/registry/v1/items/cart-line@1.0.0.json +1 -0
  13. package/dist/registry/v1/items/category-nav.json +1 -0
  14. package/dist/registry/v1/items/category-nav@1.0.0.json +1 -0
  15. package/dist/registry/v1/items/checkout-button.json +1 -0
  16. package/dist/registry/v1/items/checkout-button@1.0.0.json +1 -0
  17. package/dist/registry/v1/items/fulfilment-switch.json +1 -0
  18. package/dist/registry/v1/items/fulfilment-switch@1.0.0.json +1 -0
  19. package/dist/registry/v1/items/item-card.json +1 -0
  20. package/dist/registry/v1/items/item-card@1.0.0.json +1 -0
  21. package/dist/registry/v1/items/item-sheet.json +1 -0
  22. package/dist/registry/v1/items/item-sheet@1.0.0.json +1 -0
  23. package/dist/registry/v1/items/menu-page.json +1 -0
  24. package/dist/registry/v1/items/menu-page@1.0.0.json +1 -0
  25. package/dist/registry/v1/items/money.json +1 -0
  26. package/dist/registry/v1/items/money@1.0.0.json +1 -0
  27. package/dist/registry/v1/items/open-status.json +1 -0
  28. package/dist/registry/v1/items/open-status@1.0.0.json +1 -0
  29. package/dist/registry/v1/items/order-page.json +1 -0
  30. package/dist/registry/v1/items/order-page@1.0.0.json +1 -0
  31. package/dist/registry/v1/items/order-status.json +1 -0
  32. package/dist/registry/v1/items/order-status@1.0.0.json +1 -0
  33. package/dist/registry/v1/items/provider.json +1 -0
  34. package/dist/registry/v1/items/provider@1.0.0.json +1 -0
  35. package/dist/registry/v1/items/quantity.json +1 -0
  36. package/dist/registry/v1/items/quantity@1.0.0.json +1 -0
  37. package/dist/registry/v1/items/schedule-picker.json +1 -0
  38. package/dist/registry/v1/items/schedule-picker@1.0.0.json +1 -0
  39. package/dist/registry/v1/items/search.json +1 -0
  40. package/dist/registry/v1/items/search@1.0.0.json +1 -0
  41. package/dist/registry/v1/items/sign-in-button.json +1 -0
  42. package/dist/registry/v1/items/sign-in-button@1.0.0.json +1 -0
  43. package/dist/registry/v1/items/store-footer.json +1 -0
  44. package/dist/registry/v1/items/store-footer@1.0.0.json +1 -0
  45. package/dist/registry/v1/items/store-header.json +1 -0
  46. package/dist/registry/v1/items/store-header@1.0.0.json +1 -0
  47. package/dist/registry/v1/schema/index.json +1 -0
  48. package/dist/registry/v1/schema/item.json +1 -0
  49. package/dist/registry/v1/schema/project.json +1 -0
  50. package/package.json +35 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @zappie/kit
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1e6f420: Each package now has a README written for people installing it from npm: what it does, how to install and use it, its entry points, and links to docs.zappie.ai. `@zappie/kit` gains its first README.
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - - `@zappie/tokens`: Publish the semantic storefront token contract, CSS aliases, validation and compilation tools.
14
+ - `@zappie/sdk`: Publish the framework-agnostic storefront client and its React, server, checkout, customer, places, visits and URL entries.
15
+ - `@zappie/foundations`: Publish accessible, unstyled React primitives for storefront catalogue, item, fulfilment and cart experiences.
16
+ - `@zappie/theme-base`: Publish the reusable base storefront theme and its catalogue, item and cart surfaces.
17
+ - `@zappie/kit`: Publish the built component registry consumed by the Zappie CLI.
18
+ - `zappie`: Publish the component CLI with `add`, `list` and `diff`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zappie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @zappie/kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ready-made, styled storefront components that you copy into your project and own: item cards, the cart drawer, checkout and sign-in buttons, address and schedule pickers, search, order status, store headers and footers, and whole page blocks. They're built on `@zappie/foundations`, `@zappie/sdk` and `@zappie/tokens`, so they follow your theme's `--z-*` tokens.
4
+
5
+ You don't install this package directly. Add components with the Zappie CLI:
6
+
7
+ ```sh
8
+ npx zappie list
9
+ npx zappie add cart-drawer
10
+ ```
11
+
12
+ `zappie add` copies the component and the components it needs into your project, then installs the npm packages they use. Use `zappie diff` to see and merge later updates.
13
+
14
+ This package holds the registry the CLI reads: one JSON file per component, with its source, version and dependencies. The same files are served at `https://registry.zappie.ai/v1`.
15
+
16
+ ## Docs
17
+
18
+ - [Kit components](https://docs.zappie.ai/kit)
19
+ - [CLI reference](https://docs.zappie.ai/cli)
20
+
21
+ ## Status
22
+
23
+ The kit is at `0.x`. Components carry their own versions, and `zappie diff` shows what changed. Each release is listed in `CHANGELOG.md`.
24
+
25
+ ## License
26
+
27
+ MIT
@@ -0,0 +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."}]}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":[],"files":[{"path":"address-picker.tsx","target":"components/zappie/address-picker.tsx","sha256":"6d7792f304e81efca61dc84fd107177f12a4f4151c33b9d58fe605ff949c3def","content":"\"use client\";\n\nimport { Address } from \"@zappie/foundations\";\nimport type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { MapPin } from \"lucide-react\";\nimport \"./address-picker.css\";\nimport { deliveryAddress } from \"./address-utils\";\nimport { useAddressPicker } from \"./use-address-picker\";\n\nexport type AddressPickerProps = {\n places: PlacesClient;\n branchId: string;\n onChange: (address: DeliveryAddress) => void;\n};\n\nexport function AddressPicker({\n places,\n branchId,\n onChange,\n}: AddressPickerProps) {\n const state = useAddressPicker(places, branchId, onChange);\n const coordinates = state.selected?.coordinates;\n return (\n <section className=\"z-kit-address-picker\" aria-label=\"Delivery address\">\n {typeof navigator !== \"undefined\" && navigator.geolocation && (\n <button type=\"button\" disabled={state.finding} onClick={state.locate}>\n <MapPin aria-hidden=\"true\" />\n {state.finding ? \"Finding your location…\" : \"Use my current location\"}\n </button>\n )}\n <Address.Picker\n query={state.query}\n onQueryChange={state.setQuery}\n getSuggestions={async (query) =>\n (await state.search.suggest(query)).map((suggestion) => ({\n id: suggestion.place_id,\n label: suggestion.text,\n address: { line1: suggestion.text },\n }))\n }\n onSelect={(suggestion) => {\n if (\"address\" in suggestion) {\n void state.search.select(suggestion.id).then((place) => {\n state.choose(deliveryAddress(place));\n });\n }\n }}\n >\n {({ suggestions, status }) => (\n <div>\n <Address.Input\n aria-label=\"Search for your address\"\n placeholder=\"Search for your address\"\n />\n <Address.Suggestions>\n {suggestions.map((suggestion, index) => (\n <Address.Suggestion key={suggestion.id} index={index}>\n {suggestion.label}\n </Address.Suggestion>\n ))}\n </Address.Suggestions>\n {status === \"loading\" && <p role=\"status\">Searching…</p>}\n </div>\n )}\n </Address.Picker>\n {state.error && <p role=\"alert\">{state.error}</p>}\n {coordinates && (\n <img\n src={places.staticMapUrl(coordinates.lat, coordinates.lng, branchId)}\n alt={`Map showing ${state.selected?.line1 ?? \"the selected address\"}`}\n />\n )}\n </section>\n );\n}\n"},{"path":"address-utils.ts","target":"components/zappie/address-utils.ts","sha256":"f46fb2676478d71d6b03d6fe98c9e5048bc2269169cedaa738503fcdf58d1a09","content":"import type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { Place } from \"@zappie/sdk/places\";\n\nexport function deliveryAddress(place: Place): DeliveryAddress {\n return {\n line1: place.line1,\n ...(place.city ? { city: place.city } : {}),\n placeId: place.place_id,\n coordinates: { lat: place.lat, lng: place.lng },\n };\n}\n\nexport function locationError(error: unknown): string {\n const position =\n typeof error === \"object\" && error !== null && \"code\" in error\n ? (error as { code: unknown })\n : undefined;\n if (position?.code === 1) {\n return \"Location access is off. Search for your address instead.\";\n }\n if (position?.code === 2 || position?.code === 3) {\n return \"We couldn't find your location. Search for your address instead.\";\n }\n return error instanceof Error\n ? error.message\n : \"We couldn't find your location. Search for your address instead.\";\n}\n"},{"path":"use-address-picker.ts","target":"components/zappie/use-address-picker.ts","sha256":"0c0f58ab270efcbc13b8a25ab8c5f6ec27ca4433551bd127396be7f4722a5da1","content":"\"use client\";\n\nimport type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { deliveryAddress, locationError } from \"./address-utils\";\n\nexport function useAddressPicker(\n places: PlacesClient,\n branchId: string,\n onChange: (address: DeliveryAddress) => void,\n) {\n const search = useMemo(\n () => places.createAddressSearch({ branchId }),\n [branchId, places],\n );\n const revision = useRef(0);\n const [query, setQueryState] = useState(\"\");\n const [selected, setSelected] = useState<DeliveryAddress | null>(null);\n const [finding, setFinding] = useState(false);\n const [error, setError] = useState<string | null>(null);\n useEffect(() => () => search.dispose(), [search]);\n const choose = useCallback(\n (address: DeliveryAddress) => {\n revision.current++;\n setSelected(address);\n setQueryState(address.line1);\n setError(null);\n onChange(address);\n },\n [onChange],\n );\n const setQuery = useCallback((value: string) => {\n revision.current++;\n setQueryState(value);\n setError(null);\n }, []);\n const locate = useCallback(() => {\n const current = ++revision.current;\n setFinding(true);\n setError(null);\n navigator.geolocation.getCurrentPosition(\n ({ coords }) => {\n void places.reverse(coords.latitude, coords.longitude).then(\n (place) => {\n if (revision.current === current) {\n choose(deliveryAddress(place));\n setFinding(false);\n }\n },\n (failure) => {\n if (revision.current === current) {\n setError(locationError(failure));\n setFinding(false);\n }\n },\n );\n },\n (failure) => {\n if (revision.current === current) {\n setError(locationError(failure));\n setFinding(false);\n }\n },\n { enableHighAccuracy: false, timeout: 10_000, maximumAge: 60_000 },\n );\n }, [choose, places]);\n return { search, query, setQuery, selected, choose, finding, error, locate };\n}\n"},{"path":"address-picker.css","target":"components/zappie/address-picker.css","sha256":"9b98ea36fcafbeded1415549750e94f62fb20cf3991d9439cb3b93334f9fee61","content":".z-kit-address-picker {\n display: grid;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-address-picker button,\n.z-kit-address-picker input {\n width: 100%;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n background: var(--z-surface);\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-address-picker button {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--z-space-2);\n cursor: pointer;\n}\n\n.z-kit-address-picker [role=\"listbox\"] {\n display: grid;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-surface-raised);\n}\n\n.z-kit-address-picker [role=\"option\"] {\n padding: var(--z-space-3);\n cursor: pointer;\n}\n\n.z-kit-address-picker [role=\"alert\"] {\n border-inline-start: 3px solid var(--z-danger);\n padding-inline-start: var(--z-space-2);\n color: var(--z-text);\n}\n\n.z-kit-address-picker img {\n display: block;\n width: 100%;\n height: auto;\n border-radius: var(--z-radius-md);\n}\n\n.z-kit-address-picker :focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/address-picker"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"address-picker","type":"component","version":"1.0.0","title":"Address picker","description":"Find delivery addresses and show them on a map.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":[],"files":[{"path":"address-picker.tsx","target":"components/zappie/address-picker.tsx","sha256":"6d7792f304e81efca61dc84fd107177f12a4f4151c33b9d58fe605ff949c3def","content":"\"use client\";\n\nimport { Address } from \"@zappie/foundations\";\nimport type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { MapPin } from \"lucide-react\";\nimport \"./address-picker.css\";\nimport { deliveryAddress } from \"./address-utils\";\nimport { useAddressPicker } from \"./use-address-picker\";\n\nexport type AddressPickerProps = {\n places: PlacesClient;\n branchId: string;\n onChange: (address: DeliveryAddress) => void;\n};\n\nexport function AddressPicker({\n places,\n branchId,\n onChange,\n}: AddressPickerProps) {\n const state = useAddressPicker(places, branchId, onChange);\n const coordinates = state.selected?.coordinates;\n return (\n <section className=\"z-kit-address-picker\" aria-label=\"Delivery address\">\n {typeof navigator !== \"undefined\" && navigator.geolocation && (\n <button type=\"button\" disabled={state.finding} onClick={state.locate}>\n <MapPin aria-hidden=\"true\" />\n {state.finding ? \"Finding your location…\" : \"Use my current location\"}\n </button>\n )}\n <Address.Picker\n query={state.query}\n onQueryChange={state.setQuery}\n getSuggestions={async (query) =>\n (await state.search.suggest(query)).map((suggestion) => ({\n id: suggestion.place_id,\n label: suggestion.text,\n address: { line1: suggestion.text },\n }))\n }\n onSelect={(suggestion) => {\n if (\"address\" in suggestion) {\n void state.search.select(suggestion.id).then((place) => {\n state.choose(deliveryAddress(place));\n });\n }\n }}\n >\n {({ suggestions, status }) => (\n <div>\n <Address.Input\n aria-label=\"Search for your address\"\n placeholder=\"Search for your address\"\n />\n <Address.Suggestions>\n {suggestions.map((suggestion, index) => (\n <Address.Suggestion key={suggestion.id} index={index}>\n {suggestion.label}\n </Address.Suggestion>\n ))}\n </Address.Suggestions>\n {status === \"loading\" && <p role=\"status\">Searching…</p>}\n </div>\n )}\n </Address.Picker>\n {state.error && <p role=\"alert\">{state.error}</p>}\n {coordinates && (\n <img\n src={places.staticMapUrl(coordinates.lat, coordinates.lng, branchId)}\n alt={`Map showing ${state.selected?.line1 ?? \"the selected address\"}`}\n />\n )}\n </section>\n );\n}\n"},{"path":"address-utils.ts","target":"components/zappie/address-utils.ts","sha256":"f46fb2676478d71d6b03d6fe98c9e5048bc2269169cedaa738503fcdf58d1a09","content":"import type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { Place } from \"@zappie/sdk/places\";\n\nexport function deliveryAddress(place: Place): DeliveryAddress {\n return {\n line1: place.line1,\n ...(place.city ? { city: place.city } : {}),\n placeId: place.place_id,\n coordinates: { lat: place.lat, lng: place.lng },\n };\n}\n\nexport function locationError(error: unknown): string {\n const position =\n typeof error === \"object\" && error !== null && \"code\" in error\n ? (error as { code: unknown })\n : undefined;\n if (position?.code === 1) {\n return \"Location access is off. Search for your address instead.\";\n }\n if (position?.code === 2 || position?.code === 3) {\n return \"We couldn't find your location. Search for your address instead.\";\n }\n return error instanceof Error\n ? error.message\n : \"We couldn't find your location. Search for your address instead.\";\n}\n"},{"path":"use-address-picker.ts","target":"components/zappie/use-address-picker.ts","sha256":"0c0f58ab270efcbc13b8a25ab8c5f6ec27ca4433551bd127396be7f4722a5da1","content":"\"use client\";\n\nimport type { DeliveryAddress } from \"@zappie/sdk\";\nimport type { PlacesClient } from \"@zappie/sdk/places\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { deliveryAddress, locationError } from \"./address-utils\";\n\nexport function useAddressPicker(\n places: PlacesClient,\n branchId: string,\n onChange: (address: DeliveryAddress) => void,\n) {\n const search = useMemo(\n () => places.createAddressSearch({ branchId }),\n [branchId, places],\n );\n const revision = useRef(0);\n const [query, setQueryState] = useState(\"\");\n const [selected, setSelected] = useState<DeliveryAddress | null>(null);\n const [finding, setFinding] = useState(false);\n const [error, setError] = useState<string | null>(null);\n useEffect(() => () => search.dispose(), [search]);\n const choose = useCallback(\n (address: DeliveryAddress) => {\n revision.current++;\n setSelected(address);\n setQueryState(address.line1);\n setError(null);\n onChange(address);\n },\n [onChange],\n );\n const setQuery = useCallback((value: string) => {\n revision.current++;\n setQueryState(value);\n setError(null);\n }, []);\n const locate = useCallback(() => {\n const current = ++revision.current;\n setFinding(true);\n setError(null);\n navigator.geolocation.getCurrentPosition(\n ({ coords }) => {\n void places.reverse(coords.latitude, coords.longitude).then(\n (place) => {\n if (revision.current === current) {\n choose(deliveryAddress(place));\n setFinding(false);\n }\n },\n (failure) => {\n if (revision.current === current) {\n setError(locationError(failure));\n setFinding(false);\n }\n },\n );\n },\n (failure) => {\n if (revision.current === current) {\n setError(locationError(failure));\n setFinding(false);\n }\n },\n { enableHighAccuracy: false, timeout: 10_000, maximumAge: 60_000 },\n );\n }, [choose, places]);\n return { search, query, setQuery, selected, choose, finding, error, locate };\n}\n"},{"path":"address-picker.css","target":"components/zappie/address-picker.css","sha256":"9b98ea36fcafbeded1415549750e94f62fb20cf3991d9439cb3b93334f9fee61","content":".z-kit-address-picker {\n display: grid;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-address-picker button,\n.z-kit-address-picker input {\n width: 100%;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n background: var(--z-surface);\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-address-picker button {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--z-space-2);\n cursor: pointer;\n}\n\n.z-kit-address-picker [role=\"listbox\"] {\n display: grid;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-surface-raised);\n}\n\n.z-kit-address-picker [role=\"option\"] {\n padding: var(--z-space-3);\n cursor: pointer;\n}\n\n.z-kit-address-picker [role=\"alert\"] {\n border-inline-start: 3px solid var(--z-danger);\n padding-inline-start: var(--z-space-2);\n color: var(--z-text);\n}\n\n.z-kit-address-picker img {\n display: block;\n width: 100%;\n height: auto;\n border-radius: var(--z-radius-md);\n}\n\n.z-kit-address-picker :focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-border","--z-radius-md","--z-surface","--z-surface-raised","--z-text","--z-danger","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/address-picker"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity"],"files":[{"path":"cart-line.tsx","target":"components/zappie/cart-line.tsx","sha256":"900ebfed4b257b70725f5c25fb43747b5e01cd3b4349869e30cc12dced20994d","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { Cart } from \"@zappie/foundations\";\nimport type { CartLine as Line } from \"@zappie/sdk\";\nimport { Fragment } from \"react\";\nimport \"./cart-line.css\";\n\nexport function CartLine({ line }: { line: Line }) {\n return (\n <Cart.Line line={line} className=\"z-kit-cart-line\">\n {({ item }) => (\n <Fragment>\n <div className=\"z-kit-cart-line__heading\">\n <h3>{line.name}</h3>\n <Money value={line.lineSubtotal} />\n </div>\n {line.choices.length > 0 && (\n <ul>\n {line.choices.map((selection) => {\n const group = item?.optionGroups.find(\n (candidate) => candidate.id === selection.groupId,\n );\n const choice = group?.choices.find(\n (candidate) => candidate.id === selection.choiceId,\n );\n return (\n <li key={`${selection.groupId}:${selection.choiceId}`}>\n {choice?.name ?? \"Unavailable option\"}\n {selection.quantity > 1 ? ` × ${selection.quantity}` : \"\"}\n </li>\n );\n })}\n </ul>\n )}\n {line.note && <p>{line.note}</p>}\n <div className=\"z-kit-cart-line__actions\">\n <Cart.Quantity lineId={line.id}>\n {({ value, min, max, step, setValue, disabled }) => (\n <Quantity\n value={value}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n label={`${line.name} quantity`}\n onValueChange={setValue}\n />\n )}\n </Cart.Quantity>\n <Cart.Remove lineId={line.id} aria-label={`Remove ${line.name}`}>\n Remove\n </Cart.Remove>\n </div>\n </Fragment>\n )}\n </Cart.Line>\n );\n}\n"},{"path":"cart-line.css","target":"components/zappie/cart-line.css","sha256":"d45d5205614402bc38152e6064e3a4f88972ec55adb12b514204db25e125a854","content":".z-kit-cart-line {\n display: grid;\n gap: var(--z-space-3);\n padding: var(--z-space-4) 0;\n border-bottom: 1px solid var(--z-border);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-line__heading,\n.z-kit-cart-line__actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n\n.z-kit-cart-line h3,\n.z-kit-cart-line p,\n.z-kit-cart-line ul {\n margin: 0;\n}\n\n.z-kit-cart-line ul {\n padding-left: var(--z-space-5);\n color: var(--z-text-muted);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-cart-line__actions > button {\n border: 0;\n padding: var(--z-space-2);\n background: transparent;\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-cart-line button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-text","--z-text-muted","--z-danger","--z-focus-ring","--z-font-body","--z-text-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-line"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"cart-line","type":"component","version":"1.0.0","title":"Cart line","description":"Show options and let shoppers change quantity or remove a line.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity"],"files":[{"path":"cart-line.tsx","target":"components/zappie/cart-line.tsx","sha256":"900ebfed4b257b70725f5c25fb43747b5e01cd3b4349869e30cc12dced20994d","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { Cart } from \"@zappie/foundations\";\nimport type { CartLine as Line } from \"@zappie/sdk\";\nimport { Fragment } from \"react\";\nimport \"./cart-line.css\";\n\nexport function CartLine({ line }: { line: Line }) {\n return (\n <Cart.Line line={line} className=\"z-kit-cart-line\">\n {({ item }) => (\n <Fragment>\n <div className=\"z-kit-cart-line__heading\">\n <h3>{line.name}</h3>\n <Money value={line.lineSubtotal} />\n </div>\n {line.choices.length > 0 && (\n <ul>\n {line.choices.map((selection) => {\n const group = item?.optionGroups.find(\n (candidate) => candidate.id === selection.groupId,\n );\n const choice = group?.choices.find(\n (candidate) => candidate.id === selection.choiceId,\n );\n return (\n <li key={`${selection.groupId}:${selection.choiceId}`}>\n {choice?.name ?? \"Unavailable option\"}\n {selection.quantity > 1 ? ` × ${selection.quantity}` : \"\"}\n </li>\n );\n })}\n </ul>\n )}\n {line.note && <p>{line.note}</p>}\n <div className=\"z-kit-cart-line__actions\">\n <Cart.Quantity lineId={line.id}>\n {({ value, min, max, step, setValue, disabled }) => (\n <Quantity\n value={value}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n label={`${line.name} quantity`}\n onValueChange={setValue}\n />\n )}\n </Cart.Quantity>\n <Cart.Remove lineId={line.id} aria-label={`Remove ${line.name}`}>\n Remove\n </Cart.Remove>\n </div>\n </Fragment>\n )}\n </Cart.Line>\n );\n}\n"},{"path":"cart-line.css","target":"components/zappie/cart-line.css","sha256":"d45d5205614402bc38152e6064e3a4f88972ec55adb12b514204db25e125a854","content":".z-kit-cart-line {\n display: grid;\n gap: var(--z-space-3);\n padding: var(--z-space-4) 0;\n border-bottom: 1px solid var(--z-border);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-cart-line__heading,\n.z-kit-cart-line__actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n\n.z-kit-cart-line h3,\n.z-kit-cart-line p,\n.z-kit-cart-line ul {\n margin: 0;\n}\n\n.z-kit-cart-line ul {\n padding-left: var(--z-space-5);\n color: var(--z-text-muted);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-cart-line__actions > button {\n border: 0;\n padding: var(--z-space-2);\n background: transparent;\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-cart-line button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-space-5","--z-border","--z-text","--z-text-muted","--z-danger","--z-focus-ring","--z-font-body","--z-text-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/cart-line"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"category-nav.tsx","target":"components/zappie/category-nav.tsx","sha256":"4f4c6af7036899a33ee109a01117880c9d54d1f7bc6bfc0154e74c180eba9857","content":"\"use client\";\n\nimport { useMenu } from \"@zappie/sdk/react\";\nimport { useEffect, useRef, useState, type KeyboardEvent } from \"react\";\nimport \"./category-nav.css\";\n\nexport type CategoryNavProps = {\n branch?: string;\n categoryId?: string;\n onCategoryChange?: (categoryId: string) => void;\n getCategoryId?: (categoryId: string) => string;\n};\n\nconst categoryElementId = (id: string) => `category-${id}`;\n\nexport function CategoryNav({\n branch,\n categoryId,\n onCategoryChange,\n getCategoryId = categoryElementId,\n}: CategoryNavProps) {\n const menu = useMenu(branch);\n const [observed, setObserved] = useState<string>();\n const links = useRef<HTMLElement>(null);\n const active = categoryId ?? observed ?? menu.data?.categories[0]?.id;\n useEffect(() => {\n if (!menu.data || typeof IntersectionObserver === \"undefined\") {\n return;\n }\n const ids = new Map<Element, string>();\n const observer = new IntersectionObserver(\n (entries) => {\n const visible = entries\n .filter((entry) => entry.isIntersecting)\n .sort(\n (left, right) => right.intersectionRatio - left.intersectionRatio,\n )[0];\n const id = visible ? ids.get(visible.target) : undefined;\n if (id) {\n setObserved(id);\n onCategoryChange?.(id);\n }\n },\n { rootMargin: \"-20% 0px -70%\", threshold: 0 },\n );\n for (const category of menu.data.categories) {\n const target = document.getElementById(getCategoryId(category.id));\n if (target) {\n ids.set(target, category.id);\n observer.observe(target);\n }\n }\n return () => observer.disconnect();\n }, [getCategoryId, menu.data, onCategoryChange]);\n const select = (id: string) => {\n setObserved(id);\n onCategoryChange?.(id);\n document.getElementById(getCategoryId(id))?.scrollIntoView({\n behavior: \"smooth\",\n block: \"start\",\n });\n };\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") {\n return;\n }\n const tabs = Array.from(links.current?.querySelectorAll(\"a\") ?? []);\n const current = tabs.indexOf(document.activeElement as HTMLAnchorElement);\n const direction = event.key === \"ArrowRight\" ? 1 : -1;\n const next = tabs[(current + direction + tabs.length) % tabs.length];\n if (next) {\n event.preventDefault();\n next.focus();\n next.click();\n }\n };\n if (menu.isLoading) {\n return <nav aria-label=\"Categories\">Loading categories…</nav>;\n }\n if (menu.error || !menu.data) {\n return <nav aria-label=\"Categories\">Categories unavailable</nav>;\n }\n return (\n <nav ref={links} aria-label=\"Categories\" className=\"z-kit-category-nav\">\n {menu.data.categories.map((category) => (\n <a\n key={category.id}\n href={`#${getCategoryId(category.id)}`}\n aria-current={active === category.id ? \"page\" : undefined}\n onKeyDown={onKeyDown}\n onClick={(event) => {\n event.preventDefault();\n select(category.id);\n }}\n >\n {category.name}\n </a>\n ))}\n </nav>\n );\n}\n"},{"path":"category-nav.css","target":"components/zappie/category-nav.css","sha256":"24598de9279d73a875016ea4c00743ba0e6c2a8db50cf699be19b388341517c7","content":".z-kit-category-nav {\n position: sticky;\n z-index: 1;\n top: 0;\n display: flex;\n gap: var(--z-space-2);\n overflow-x: auto;\n padding: var(--z-space-2) 0;\n background: var(--z-bg);\n font-family: var(--z-font-body);\n scrollbar-width: none;\n}\n\n.z-kit-category-nav a {\n flex: none;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n color: var(--z-text-muted);\n text-decoration: none;\n}\n\n.z-kit-category-nav a[aria-current=\"page\"] {\n border-color: var(--z-accent);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-category-nav a:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-4","--z-bg","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/category-nav"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"category-nav","type":"component","version":"1.0.0","title":"Category navigation","description":"Navigate categories with tabs that follow scrolling.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"category-nav.tsx","target":"components/zappie/category-nav.tsx","sha256":"4f4c6af7036899a33ee109a01117880c9d54d1f7bc6bfc0154e74c180eba9857","content":"\"use client\";\n\nimport { useMenu } from \"@zappie/sdk/react\";\nimport { useEffect, useRef, useState, type KeyboardEvent } from \"react\";\nimport \"./category-nav.css\";\n\nexport type CategoryNavProps = {\n branch?: string;\n categoryId?: string;\n onCategoryChange?: (categoryId: string) => void;\n getCategoryId?: (categoryId: string) => string;\n};\n\nconst categoryElementId = (id: string) => `category-${id}`;\n\nexport function CategoryNav({\n branch,\n categoryId,\n onCategoryChange,\n getCategoryId = categoryElementId,\n}: CategoryNavProps) {\n const menu = useMenu(branch);\n const [observed, setObserved] = useState<string>();\n const links = useRef<HTMLElement>(null);\n const active = categoryId ?? observed ?? menu.data?.categories[0]?.id;\n useEffect(() => {\n if (!menu.data || typeof IntersectionObserver === \"undefined\") {\n return;\n }\n const ids = new Map<Element, string>();\n const observer = new IntersectionObserver(\n (entries) => {\n const visible = entries\n .filter((entry) => entry.isIntersecting)\n .sort(\n (left, right) => right.intersectionRatio - left.intersectionRatio,\n )[0];\n const id = visible ? ids.get(visible.target) : undefined;\n if (id) {\n setObserved(id);\n onCategoryChange?.(id);\n }\n },\n { rootMargin: \"-20% 0px -70%\", threshold: 0 },\n );\n for (const category of menu.data.categories) {\n const target = document.getElementById(getCategoryId(category.id));\n if (target) {\n ids.set(target, category.id);\n observer.observe(target);\n }\n }\n return () => observer.disconnect();\n }, [getCategoryId, menu.data, onCategoryChange]);\n const select = (id: string) => {\n setObserved(id);\n onCategoryChange?.(id);\n document.getElementById(getCategoryId(id))?.scrollIntoView({\n behavior: \"smooth\",\n block: \"start\",\n });\n };\n const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") {\n return;\n }\n const tabs = Array.from(links.current?.querySelectorAll(\"a\") ?? []);\n const current = tabs.indexOf(document.activeElement as HTMLAnchorElement);\n const direction = event.key === \"ArrowRight\" ? 1 : -1;\n const next = tabs[(current + direction + tabs.length) % tabs.length];\n if (next) {\n event.preventDefault();\n next.focus();\n next.click();\n }\n };\n if (menu.isLoading) {\n return <nav aria-label=\"Categories\">Loading categories…</nav>;\n }\n if (menu.error || !menu.data) {\n return <nav aria-label=\"Categories\">Categories unavailable</nav>;\n }\n return (\n <nav ref={links} aria-label=\"Categories\" className=\"z-kit-category-nav\">\n {menu.data.categories.map((category) => (\n <a\n key={category.id}\n href={`#${getCategoryId(category.id)}`}\n aria-current={active === category.id ? \"page\" : undefined}\n onKeyDown={onKeyDown}\n onClick={(event) => {\n event.preventDefault();\n select(category.id);\n }}\n >\n {category.name}\n </a>\n ))}\n </nav>\n );\n}\n"},{"path":"category-nav.css","target":"components/zappie/category-nav.css","sha256":"24598de9279d73a875016ea4c00743ba0e6c2a8db50cf699be19b388341517c7","content":".z-kit-category-nav {\n position: sticky;\n z-index: 1;\n top: 0;\n display: flex;\n gap: var(--z-space-2);\n overflow-x: auto;\n padding: var(--z-space-2) 0;\n background: var(--z-bg);\n font-family: var(--z-font-body);\n scrollbar-width: none;\n}\n\n.z-kit-category-nav a {\n flex: none;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n color: var(--z-text-muted);\n text-decoration: none;\n}\n\n.z-kit-category-nav a[aria-current=\"page\"] {\n border-color: var(--z-accent);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-category-nav a:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-4","--z-bg","--z-border","--z-radius-pill","--z-accent","--z-accent-contrast","--z-text-muted","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/category-nav"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"fulfilment-switch.tsx","target":"components/zappie/fulfilment-switch.tsx","sha256":"08e2a8f7d30310a50bbddf1de6e987aa291268aaaaae70ae985d2c4ede49e77d","content":"\"use client\";\n\nimport { Fulfilment } from \"@zappie/foundations\";\nimport type { CartFulfilment, DeliveryAddress } from \"@zappie/sdk\";\nimport \"./fulfilment-switch.css\";\n\nexport type FulfilmentSwitchProps = {\n branchId: string;\n deliveryAddress?: DeliveryAddress;\n deliveryRequested?: boolean;\n onDeliveryRequest?: () => void;\n onChange?: (value: CartFulfilment) => void;\n};\n\nconst labels = { pickup: \"Pickup\", delivery: \"Delivery\" } as const;\n\nexport function FulfilmentSwitch({\n branchId,\n deliveryAddress,\n deliveryRequested = false,\n onDeliveryRequest,\n onChange,\n}: FulfilmentSwitchProps) {\n return (\n <Fulfilment.Switch className=\"z-kit-fulfilment-switch\">\n {({ options, value, setValue }) => (\n <div aria-label=\"Fulfilment method\" role=\"group\">\n {options\n .filter((option) => option === \"pickup\" || option === \"delivery\")\n .map((option) => {\n const selected =\n value?.type === option ||\n (option === \"delivery\" && deliveryRequested);\n return (\n <button\n key={option}\n type=\"button\"\n aria-pressed={selected}\n data-zappie-fulfilment={option}\n onClick={() => {\n if (\n option === \"delivery\" &&\n !deliveryAddress?.line1.trim() &&\n onDeliveryRequest\n ) {\n onDeliveryRequest();\n return;\n }\n const next: CartFulfilment =\n option === \"delivery\"\n ? {\n type: \"delivery\",\n branchId,\n address: deliveryAddress ?? { line1: \"\" },\n }\n : { type: \"pickup\", branchId };\n setValue(next);\n onChange?.(next);\n }}\n >\n {labels[option]}\n </button>\n );\n })}\n </div>\n )}\n </Fulfilment.Switch>\n );\n}\n"},{"path":"fulfilment-switch.css","target":"components/zappie/fulfilment-switch.css","sha256":"d660056b4618c33b1b0f3a3249818bd5d6ddcb1ff130a800a6631d04a796b063","content":".z-kit-fulfilment-switch > div {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--z-space-1);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-1);\n background: var(--z-surface-raised);\n}\n\n.z-kit-fulfilment-switch button {\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: transparent;\n color: var(--z-text-muted);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-fulfilment-switch button[aria-pressed=\"true\"] {\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-fulfilment-switch button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-4","--z-border","--z-radius-pill","--z-surface-raised","--z-text-muted","--z-accent","--z-accent-contrast","--z-focus-ring"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/fulfilment-switch"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"fulfilment-switch","type":"component","version":"1.0.0","title":"Fulfilment switch","description":"Choose pickup or delivery from the branch options.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"fulfilment-switch.tsx","target":"components/zappie/fulfilment-switch.tsx","sha256":"08e2a8f7d30310a50bbddf1de6e987aa291268aaaaae70ae985d2c4ede49e77d","content":"\"use client\";\n\nimport { Fulfilment } from \"@zappie/foundations\";\nimport type { CartFulfilment, DeliveryAddress } from \"@zappie/sdk\";\nimport \"./fulfilment-switch.css\";\n\nexport type FulfilmentSwitchProps = {\n branchId: string;\n deliveryAddress?: DeliveryAddress;\n deliveryRequested?: boolean;\n onDeliveryRequest?: () => void;\n onChange?: (value: CartFulfilment) => void;\n};\n\nconst labels = { pickup: \"Pickup\", delivery: \"Delivery\" } as const;\n\nexport function FulfilmentSwitch({\n branchId,\n deliveryAddress,\n deliveryRequested = false,\n onDeliveryRequest,\n onChange,\n}: FulfilmentSwitchProps) {\n return (\n <Fulfilment.Switch className=\"z-kit-fulfilment-switch\">\n {({ options, value, setValue }) => (\n <div aria-label=\"Fulfilment method\" role=\"group\">\n {options\n .filter((option) => option === \"pickup\" || option === \"delivery\")\n .map((option) => {\n const selected =\n value?.type === option ||\n (option === \"delivery\" && deliveryRequested);\n return (\n <button\n key={option}\n type=\"button\"\n aria-pressed={selected}\n data-zappie-fulfilment={option}\n onClick={() => {\n if (\n option === \"delivery\" &&\n !deliveryAddress?.line1.trim() &&\n onDeliveryRequest\n ) {\n onDeliveryRequest();\n return;\n }\n const next: CartFulfilment =\n option === \"delivery\"\n ? {\n type: \"delivery\",\n branchId,\n address: deliveryAddress ?? { line1: \"\" },\n }\n : { type: \"pickup\", branchId };\n setValue(next);\n onChange?.(next);\n }}\n >\n {labels[option]}\n </button>\n );\n })}\n </div>\n )}\n </Fulfilment.Switch>\n );\n}\n"},{"path":"fulfilment-switch.css","target":"components/zappie/fulfilment-switch.css","sha256":"d660056b4618c33b1b0f3a3249818bd5d6ddcb1ff130a800a6631d04a796b063","content":".z-kit-fulfilment-switch > div {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--z-space-1);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-1);\n background: var(--z-surface-raised);\n}\n\n.z-kit-fulfilment-switch button {\n border: 0;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: transparent;\n color: var(--z-text-muted);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-fulfilment-switch button[aria-pressed=\"true\"] {\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-fulfilment-switch button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-1","--z-space-2","--z-space-4","--z-border","--z-radius-pill","--z-surface-raised","--z-text-muted","--z-accent","--z-accent-contrast","--z-focus-ring"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/fulfilment-switch"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity"],"files":[{"path":"item-sheet.tsx","target":"components/zappie/item-sheet.tsx","sha256":"1c9dec76dcf1c3f2e51d1b5d526b9ac09a6f79b852545397156d768c30a5c5f4","content":"\"use client\";\n\nimport { ItemOptions } from \"@/components/zappie/item-options\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { ItemForm, Overlay } from \"@zappie/foundations\";\nimport { useItem } from \"@zappie/sdk/react\";\nimport {\n buildStorefrontUrl,\n type StorefrontInstallation,\n type StorefrontPageState,\n} from \"@zappie/sdk/url\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Fragment, useEffect, useMemo, useRef } from \"react\";\nimport \"./item-sheet.css\";\n\nexport type ItemSheetUrl = {\n installation: StorefrontInstallation;\n branch?: string;\n state?: StorefrontPageState;\n navigate: (href: string) => void;\n};\n\nexport type ItemSheetProps = {\n itemId: string;\n branch?: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n url?: ItemSheetUrl;\n};\n\n/** Pass `url` to mirror the controlled sheet at `/item/:id` with URL contract v2. */\nexport function ItemSheet(props: ItemSheetProps) {\n const { itemId, branch, open, onOpenChange, url } = props;\n const item = useItem(itemId, branch);\n const previous = useRef<boolean | undefined>(undefined);\n useEffect(() => {\n const wasOpen = previous.current;\n previous.current = open;\n if (!url || wasOpen === open || (wasOpen === undefined && !open)) {\n return;\n }\n const common = {\n business: url.installation.business,\n ...(url.branch ? { branch: url.branch } : {}),\n state: url.state ?? { query: {} },\n };\n url.navigate(\n buildStorefrontUrl(\n open\n ? { type: \"item\", item: itemId, ...common }\n : { type: \"order-menu\", ...common },\n { installation: url.installation },\n ),\n );\n }, [itemId, open, url]);\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=\"item\"\n id={itemId}\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-item-sheet\">\n <Overlay.Title>{item.data?.name ?? \"Item\"}</Overlay.Title>\n <Overlay.Close aria-label=\"Close item\">×</Overlay.Close>\n {item.isLoading && <p role=\"status\">Loading item…</p>}\n {item.error && <p role=\"alert\">{item.error.message}</p>}\n {item.data && (\n <ItemConfiguration\n item={item.data}\n onAdded={() => onOpenChange(false)}\n />\n )}\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\nfunction ItemConfiguration({\n item,\n onAdded,\n}: {\n item: NonNullable<ReturnType<typeof useItem>[\"data\"]>;\n onAdded: () => void;\n}) {\n return (\n <ItemForm.Root\n item={item}\n onAdded={() => {\n countEvent(\"add\");\n onAdded();\n }}\n className=\"z-kit-item-sheet__form\"\n >\n {(state) => (\n <Fragment>\n {item.description && <p>{item.description}</p>}\n <ItemForm.Options>\n {({ groups }) =>\n groups.map((group) => (\n <ItemOptions key={group.id} group={group} />\n ))\n }\n </ItemForm.Options>\n {state.issues.length > 0 && (\n <div role=\"alert\">\n {state.issues.map((issue, index) => (\n <p key={`${issue.code}:${index}`}>{issue.message}</p>\n ))}\n </div>\n )}\n <div className=\"z-kit-item-sheet__purchase\">\n <Quantity\n value={state.quantity}\n min={item.quantity.min}\n max={item.quantity.max ?? 99}\n step={item.quantity.step}\n onValueChange={state.setQuantity}\n />\n <ItemForm.Add disabled={!item.isAvailable || !state.canAdd}>\n {item.isAvailable ? \"Add\" : (item.stockLabel ?? \"Unavailable\")}{\" \"}\n <Money value={state.total} />\n </ItemForm.Add>\n </div>\n </Fragment>\n )}\n </ItemForm.Root>\n );\n}\n"},{"path":"item-options.tsx","target":"components/zappie/item-options.tsx","sha256":"7b3842cf66b6183994a636c22f86c1fc90c5f718b384560e91ef36c90cdcf32a","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { Choice, OptionGroup } from \"@zappie/foundations\";\nimport type { Choice as ChoiceModel, OptionGroup as Group } from \"@zappie/sdk\";\nimport { Fragment } from \"react\";\n\nexport function ItemOptions({ group }: { group: Group }) {\n return (\n <OptionGroup group={group} className=\"z-kit-item-sheet__group\">\n {({ quantities, setQuantity }) => (\n <Fragment>\n <h3>{group.name}</h3>\n {group.helpText && <p>{group.helpText}</p>}\n {group.kind === \"select\" &&\n group.choices.map((choice) => (\n <ItemChoice\n key={choice.id}\n choice={choice}\n quantity={quantities[choice.id] ?? 0}\n setQuantity={(quantity) => setQuantity(choice.id, quantity)}\n />\n ))}\n {(group.kind !== \"select\" || group.isInformational) && (\n <OptionGroup.Input />\n )}\n </Fragment>\n )}\n </OptionGroup>\n );\n}\n\nfunction ItemChoice({\n choice,\n quantity,\n setQuantity,\n}: {\n choice: ChoiceModel;\n quantity: number;\n setQuantity: (quantity: number) => void;\n}) {\n return (\n <div className=\"z-kit-item-sheet__choice\">\n <Choice choice={choice}>\n <span>{choice.name}</span>\n {(choice.priceHint ?? choice.price).amountMinor !== 0 && (\n <Money value={choice.priceHint ?? choice.price} />\n )}\n </Choice>\n {choice.quantity && quantity > 0 && (\n <Quantity\n value={quantity}\n min={0}\n max={choice.quantity.max ?? 99}\n disabled={!choice.isAvailable}\n label={`${choice.name} quantity`}\n onValueChange={setQuantity}\n />\n )}\n </div>\n );\n}\n"},{"path":"item-sheet.css","target":"components/zappie/item-sheet.css","sha256":"e629d6bf9f28a9c0bf36940175fa5e8c3a1f39ca50f88cb59ae3a8217f3d10ed","content":".z-kit-item-sheet {\n display: grid;\n gap: var(--z-space-4);\n max-height: 90vh;\n padding: var(--z-space-5);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-sheet h2,\n.z-kit-item-sheet h3,\n.z-kit-item-sheet p {\n margin: 0;\n}\n\n.z-kit-item-sheet h2,\n.z-kit-item-sheet h3 {\n font-family: var(--z-font-display);\n}\n\n.z-kit-item-sheet__form,\n.z-kit-item-sheet__group {\n display: grid;\n gap: var(--z-space-3);\n}\n\n.z-kit-item-sheet__choice > button:first-child,\n.z-kit-item-sheet__group input {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n background: var(--z-surface-raised);\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-item-sheet__choice > button[data-selected] {\n border-color: var(--z-accent);\n}\n\n.z-kit-item-sheet__choice {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n\n.z-kit-item-sheet__choice > button:first-child {\n flex: 1;\n}\n\n.z-kit-item-sheet__purchase {\n position: sticky;\n bottom: 0;\n display: flex;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding-top: var(--z-space-3);\n background: var(--z-surface);\n}\n\n.z-kit-item-sheet__purchase > button {\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: inherit;\n}\n\n.z-kit-item-sheet button:focus-visible,\n.z-kit-item-sheet input:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-md","--z-radius-pill","--z-surface","--z-surface-raised","--z-text","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-sheet"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"item-sheet","type":"component","version":"1.0.0","title":"Item sheet","description":"Let shoppers configure an item, with optional v2 URL navigation.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money","quantity"],"files":[{"path":"item-sheet.tsx","target":"components/zappie/item-sheet.tsx","sha256":"1c9dec76dcf1c3f2e51d1b5d526b9ac09a6f79b852545397156d768c30a5c5f4","content":"\"use client\";\n\nimport { ItemOptions } from \"@/components/zappie/item-options\";\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { ItemForm, Overlay } from \"@zappie/foundations\";\nimport { useItem } from \"@zappie/sdk/react\";\nimport {\n buildStorefrontUrl,\n type StorefrontInstallation,\n type StorefrontPageState,\n} from \"@zappie/sdk/url\";\nimport { countEvent } from \"@zappie/sdk/visits\";\nimport { Fragment, useEffect, useMemo, useRef } from \"react\";\nimport \"./item-sheet.css\";\n\nexport type ItemSheetUrl = {\n installation: StorefrontInstallation;\n branch?: string;\n state?: StorefrontPageState;\n navigate: (href: string) => void;\n};\n\nexport type ItemSheetProps = {\n itemId: string;\n branch?: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n url?: ItemSheetUrl;\n};\n\n/** Pass `url` to mirror the controlled sheet at `/item/:id` with URL contract v2. */\nexport function ItemSheet(props: ItemSheetProps) {\n const { itemId, branch, open, onOpenChange, url } = props;\n const item = useItem(itemId, branch);\n const previous = useRef<boolean | undefined>(undefined);\n useEffect(() => {\n const wasOpen = previous.current;\n previous.current = open;\n if (!url || wasOpen === open || (wasOpen === undefined && !open)) {\n return;\n }\n const common = {\n business: url.installation.business,\n ...(url.branch ? { branch: url.branch } : {}),\n state: url.state ?? { query: {} },\n };\n url.navigate(\n buildStorefrontUrl(\n open\n ? { type: \"item\", item: itemId, ...common }\n : { type: \"order-menu\", ...common },\n { installation: url.installation },\n ),\n );\n }, [itemId, open, url]);\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=\"item\"\n id={itemId}\n navigation={navigation}\n presentation=\"sheet\"\n >\n <Overlay.Content className=\"z-kit-item-sheet\">\n <Overlay.Title>{item.data?.name ?? \"Item\"}</Overlay.Title>\n <Overlay.Close aria-label=\"Close item\">×</Overlay.Close>\n {item.isLoading && <p role=\"status\">Loading item…</p>}\n {item.error && <p role=\"alert\">{item.error.message}</p>}\n {item.data && (\n <ItemConfiguration\n item={item.data}\n onAdded={() => onOpenChange(false)}\n />\n )}\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\nfunction ItemConfiguration({\n item,\n onAdded,\n}: {\n item: NonNullable<ReturnType<typeof useItem>[\"data\"]>;\n onAdded: () => void;\n}) {\n return (\n <ItemForm.Root\n item={item}\n onAdded={() => {\n countEvent(\"add\");\n onAdded();\n }}\n className=\"z-kit-item-sheet__form\"\n >\n {(state) => (\n <Fragment>\n {item.description && <p>{item.description}</p>}\n <ItemForm.Options>\n {({ groups }) =>\n groups.map((group) => (\n <ItemOptions key={group.id} group={group} />\n ))\n }\n </ItemForm.Options>\n {state.issues.length > 0 && (\n <div role=\"alert\">\n {state.issues.map((issue, index) => (\n <p key={`${issue.code}:${index}`}>{issue.message}</p>\n ))}\n </div>\n )}\n <div className=\"z-kit-item-sheet__purchase\">\n <Quantity\n value={state.quantity}\n min={item.quantity.min}\n max={item.quantity.max ?? 99}\n step={item.quantity.step}\n onValueChange={state.setQuantity}\n />\n <ItemForm.Add disabled={!item.isAvailable || !state.canAdd}>\n {item.isAvailable ? \"Add\" : (item.stockLabel ?? \"Unavailable\")}{\" \"}\n <Money value={state.total} />\n </ItemForm.Add>\n </div>\n </Fragment>\n )}\n </ItemForm.Root>\n );\n}\n"},{"path":"item-options.tsx","target":"components/zappie/item-options.tsx","sha256":"7b3842cf66b6183994a636c22f86c1fc90c5f718b384560e91ef36c90cdcf32a","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { Quantity } from \"@/components/zappie/quantity\";\nimport { Choice, OptionGroup } from \"@zappie/foundations\";\nimport type { Choice as ChoiceModel, OptionGroup as Group } from \"@zappie/sdk\";\nimport { Fragment } from \"react\";\n\nexport function ItemOptions({ group }: { group: Group }) {\n return (\n <OptionGroup group={group} className=\"z-kit-item-sheet__group\">\n {({ quantities, setQuantity }) => (\n <Fragment>\n <h3>{group.name}</h3>\n {group.helpText && <p>{group.helpText}</p>}\n {group.kind === \"select\" &&\n group.choices.map((choice) => (\n <ItemChoice\n key={choice.id}\n choice={choice}\n quantity={quantities[choice.id] ?? 0}\n setQuantity={(quantity) => setQuantity(choice.id, quantity)}\n />\n ))}\n {(group.kind !== \"select\" || group.isInformational) && (\n <OptionGroup.Input />\n )}\n </Fragment>\n )}\n </OptionGroup>\n );\n}\n\nfunction ItemChoice({\n choice,\n quantity,\n setQuantity,\n}: {\n choice: ChoiceModel;\n quantity: number;\n setQuantity: (quantity: number) => void;\n}) {\n return (\n <div className=\"z-kit-item-sheet__choice\">\n <Choice choice={choice}>\n <span>{choice.name}</span>\n {(choice.priceHint ?? choice.price).amountMinor !== 0 && (\n <Money value={choice.priceHint ?? choice.price} />\n )}\n </Choice>\n {choice.quantity && quantity > 0 && (\n <Quantity\n value={quantity}\n min={0}\n max={choice.quantity.max ?? 99}\n disabled={!choice.isAvailable}\n label={`${choice.name} quantity`}\n onValueChange={setQuantity}\n />\n )}\n </div>\n );\n}\n"},{"path":"item-sheet.css","target":"components/zappie/item-sheet.css","sha256":"e629d6bf9f28a9c0bf36940175fa5e8c3a1f39ca50f88cb59ae3a8217f3d10ed","content":".z-kit-item-sheet {\n display: grid;\n gap: var(--z-space-4);\n max-height: 90vh;\n padding: var(--z-space-5);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-item-sheet h2,\n.z-kit-item-sheet h3,\n.z-kit-item-sheet p {\n margin: 0;\n}\n\n.z-kit-item-sheet h2,\n.z-kit-item-sheet h3 {\n font-family: var(--z-font-display);\n}\n\n.z-kit-item-sheet__form,\n.z-kit-item-sheet__group {\n display: grid;\n gap: var(--z-space-3);\n}\n\n.z-kit-item-sheet__choice > button:first-child,\n.z-kit-item-sheet__group input {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n padding: var(--z-space-3);\n background: var(--z-surface-raised);\n color: var(--z-text);\n font: inherit;\n}\n\n.z-kit-item-sheet__choice > button[data-selected] {\n border-color: var(--z-accent);\n}\n\n.z-kit-item-sheet__choice {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n\n.z-kit-item-sheet__choice > button:first-child {\n flex: 1;\n}\n\n.z-kit-item-sheet__purchase {\n position: sticky;\n bottom: 0;\n display: flex;\n justify-content: space-between;\n gap: var(--z-space-3);\n padding-top: var(--z-space-3);\n background: var(--z-surface);\n}\n\n.z-kit-item-sheet__purchase > button {\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: inherit;\n}\n\n.z-kit-item-sheet button:focus-visible,\n.z-kit-item-sheet input:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-md","--z-radius-pill","--z-surface","--z-surface-raised","--z-text","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body","--z-font-display"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/item-sheet"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["store-header","category-nav","item-card","item-sheet","cart-button","cart-drawer"],"files":[{"path":"menu-page.tsx","target":"components/zappie/menu-page.tsx","sha256":"69998a616c20771910723ec73319316d5cc297271fa2be63a3bd610a0373ebed","content":"\"use client\";\n\nimport { CartButton } from \"@/components/zappie/cart-button\";\nimport { CartDrawer } from \"@/components/zappie/cart-drawer\";\nimport { CategoryNav } from \"@/components/zappie/category-nav\";\nimport { ItemCard } from \"@/components/zappie/item-card\";\nimport { ItemSheet, type ItemSheetUrl } from \"@/components/zappie/item-sheet\";\nimport { StoreHeader } from \"@/components/zappie/store-header\";\nimport { useMenu } from \"@zappie/sdk/react\";\nimport { useState } from \"react\";\nimport \"./menu-page.css\";\n\nexport type MenuPageProps = {\n branch?: string;\n itemUrl?: ItemSheetUrl;\n};\n\nexport function MenuPage({ branch, itemUrl }: MenuPageProps) {\n const menu = useMenu(branch);\n const [itemId, setItemId] = useState<string>();\n const [cartOpen, setCartOpen] = useState(false);\n return (\n <main className=\"z-kit-menu-page\">\n <StoreHeader {...(branch ? { branch } : {})} />\n <CategoryNav {...(branch ? { branch } : {})} />\n {menu.isLoading && <p role=\"status\">Loading menu…</p>}\n {menu.error && <p role=\"alert\">{menu.error.message}</p>}\n {menu.data?.categories.map((category) => (\n <section\n key={category.id}\n id={`category-${category.id}`}\n aria-labelledby={`category-title-${category.id}`}\n >\n <h2 id={`category-title-${category.id}`}>{category.name}</h2>\n {category.description && <p>{category.description}</p>}\n <div className=\"z-kit-menu-page__grid\">\n {category.itemIds.map((id) => (\n <div key={id}>\n <ItemCard itemId={id} {...(branch ? { branch } : {})} />\n <button type=\"button\" onClick={() => setItemId(id)}>\n View details\n </button>\n </div>\n ))}\n </div>\n </section>\n ))}\n {itemId && (\n <ItemSheet\n itemId={itemId}\n {...(branch ? { branch } : {})}\n open\n onOpenChange={(open) => {\n if (!open) {\n setItemId(undefined);\n }\n }}\n {...(itemUrl ? { url: itemUrl } : {})}\n />\n )}\n <CartButton onOpen={() => setCartOpen(true)} />\n <CartDrawer open={cartOpen} onOpenChange={setCartOpen} />\n </main>\n );\n}\n"},{"path":"menu-page.css","target":"components/zappie/menu-page.css","sha256":"e179cf447bfb6a9e0972844679931c07374719d354e3a50c497c53e953e8d4a2","content":".z-kit-menu-page {\n display: grid;\n gap: var(--z-space-6);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-menu-page__grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--z-space-4);\n}\n.z-kit-menu-page section > p {\n color: var(--z-text-muted);\n}\n.z-kit-menu-page section > h2 {\n font-family: var(--z-font-display);\n}\n"}],"tokens":["--z-space-4","--z-space-6","--z-text","--z-text-muted","--z-font-body","--z-font-display"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/menu-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"menu-page","type":"block","version":"1.0.0","title":"Menu page","description":"Combine the catalogue, item view and cart in your framework.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["store-header","category-nav","item-card","item-sheet","cart-button","cart-drawer"],"files":[{"path":"menu-page.tsx","target":"components/zappie/menu-page.tsx","sha256":"69998a616c20771910723ec73319316d5cc297271fa2be63a3bd610a0373ebed","content":"\"use client\";\n\nimport { CartButton } from \"@/components/zappie/cart-button\";\nimport { CartDrawer } from \"@/components/zappie/cart-drawer\";\nimport { CategoryNav } from \"@/components/zappie/category-nav\";\nimport { ItemCard } from \"@/components/zappie/item-card\";\nimport { ItemSheet, type ItemSheetUrl } from \"@/components/zappie/item-sheet\";\nimport { StoreHeader } from \"@/components/zappie/store-header\";\nimport { useMenu } from \"@zappie/sdk/react\";\nimport { useState } from \"react\";\nimport \"./menu-page.css\";\n\nexport type MenuPageProps = {\n branch?: string;\n itemUrl?: ItemSheetUrl;\n};\n\nexport function MenuPage({ branch, itemUrl }: MenuPageProps) {\n const menu = useMenu(branch);\n const [itemId, setItemId] = useState<string>();\n const [cartOpen, setCartOpen] = useState(false);\n return (\n <main className=\"z-kit-menu-page\">\n <StoreHeader {...(branch ? { branch } : {})} />\n <CategoryNav {...(branch ? { branch } : {})} />\n {menu.isLoading && <p role=\"status\">Loading menu…</p>}\n {menu.error && <p role=\"alert\">{menu.error.message}</p>}\n {menu.data?.categories.map((category) => (\n <section\n key={category.id}\n id={`category-${category.id}`}\n aria-labelledby={`category-title-${category.id}`}\n >\n <h2 id={`category-title-${category.id}`}>{category.name}</h2>\n {category.description && <p>{category.description}</p>}\n <div className=\"z-kit-menu-page__grid\">\n {category.itemIds.map((id) => (\n <div key={id}>\n <ItemCard itemId={id} {...(branch ? { branch } : {})} />\n <button type=\"button\" onClick={() => setItemId(id)}>\n View details\n </button>\n </div>\n ))}\n </div>\n </section>\n ))}\n {itemId && (\n <ItemSheet\n itemId={itemId}\n {...(branch ? { branch } : {})}\n open\n onOpenChange={(open) => {\n if (!open) {\n setItemId(undefined);\n }\n }}\n {...(itemUrl ? { url: itemUrl } : {})}\n />\n )}\n <CartButton onOpen={() => setCartOpen(true)} />\n <CartDrawer open={cartOpen} onOpenChange={setCartOpen} />\n </main>\n );\n}\n"},{"path":"menu-page.css","target":"components/zappie/menu-page.css","sha256":"e179cf447bfb6a9e0972844679931c07374719d354e3a50c497c53e953e8d4a2","content":".z-kit-menu-page {\n display: grid;\n gap: var(--z-space-6);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-menu-page__grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--z-space-4);\n}\n.z-kit-menu-page section > p {\n color: var(--z-text-muted);\n}\n.z-kit-menu-page section > h2 {\n font-family: var(--z-font-display);\n}\n"}],"tokens":["--z-space-4","--z-space-6","--z-text","--z-text-muted","--z-font-body","--z-font-display"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/menu-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"money.tsx","target":"components/zappie/money.tsx","sha256":"b07a67c7fe2e27b128fbaf1d1b4ea5f6564e2126775630cc91587f90763d4d58","content":"\"use client\";\n\nimport { Money as MoneyValue } from \"@zappie/foundations\";\nimport type { Money as Amount, MoneyFormatOptions } from \"@zappie/sdk\";\nimport \"./money.css\";\n\nexport function Money({\n value,\n formatOptions,\n className,\n}: {\n value: Amount;\n formatOptions?: MoneyFormatOptions;\n className?: string;\n}) {\n return (\n <MoneyValue\n value={value}\n {...(formatOptions !== undefined ? { formatOptions } : {})}\n className={[\"z-kit-money\", className].filter(Boolean).join(\" \")}\n />\n );\n}\n"},{"path":"money.css","target":"components/zappie/money.css","sha256":"452fbfe110780601ed88bd1aa252e20b4b69376bfc462648ca0ee0d5daaff6be","content":"/* Inline text: it takes the colour of whatever it sits in, such as a filled button. */\n.z-kit-money {\n color: inherit;\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n}\n"}],"tokens":["--z-text","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/money"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"money","type":"component","version":"1.0.0","title":"Money","description":"Format SDK money with your theme styles.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"money.tsx","target":"components/zappie/money.tsx","sha256":"b07a67c7fe2e27b128fbaf1d1b4ea5f6564e2126775630cc91587f90763d4d58","content":"\"use client\";\n\nimport { Money as MoneyValue } from \"@zappie/foundations\";\nimport type { Money as Amount, MoneyFormatOptions } from \"@zappie/sdk\";\nimport \"./money.css\";\n\nexport function Money({\n value,\n formatOptions,\n className,\n}: {\n value: Amount;\n formatOptions?: MoneyFormatOptions;\n className?: string;\n}) {\n return (\n <MoneyValue\n value={value}\n {...(formatOptions !== undefined ? { formatOptions } : {})}\n className={[\"z-kit-money\", className].filter(Boolean).join(\" \")}\n />\n );\n}\n"},{"path":"money.css","target":"components/zappie/money.css","sha256":"452fbfe110780601ed88bd1aa252e20b4b69376bfc462648ca0ee0d5daaff6be","content":"/* Inline text: it takes the colour of whatever it sits in, such as a filled button. */\n.z-kit-money {\n color: inherit;\n font-family: var(--z-font-body);\n font-size: var(--z-text-md);\n}\n"}],"tokens":["--z-text","--z-font-body","--z-text-md"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/money"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"open-status.tsx","target":"components/zappie/open-status.tsx","sha256":"c81df0ef988dfd9912ae5c341f044a7b1fee561e76fc4f45e1385056dc01ff57","content":"\"use client\";\n\nimport { OpenStatus as Status } from \"@zappie/foundations\";\nimport type { Branch } from \"@zappie/sdk\";\nimport \"./open-status.css\";\n\nexport function OpenStatus({\n branch,\n timezone,\n}: {\n branch: Branch;\n timezone?: string;\n}) {\n return (\n <Status\n branch={branch}\n {...(timezone !== undefined ? { timezone } : {})}\n className=\"z-kit-open-status\"\n >\n {({ label }) => label}\n </Status>\n );\n}\n"},{"path":"open-status.css","target":"components/zappie/open-status.css","sha256":"797ba336babbf5fb4ffad66d707c00233b0bb77a33ff9187473653ae229d764f","content":".z-kit-open-status {\n display: inline-flex;\n align-items: center;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-1) var(--z-space-3);\n border: 1px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n font-family: var(--z-font-body);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-open-status[data-state=\"open\"] {\n border-color: var(--z-success);\n color: var(--z-success);\n}\n"}],"tokens":["--z-radius-pill","--z-space-1","--z-space-3","--z-surface-raised","--z-danger","--z-success","--z-font-body","--z-text-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/open-status"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"open-status","type":"component","version":"1.0.0","title":"Open status","description":"Show whether a branch is open and when that changes.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"open-status.tsx","target":"components/zappie/open-status.tsx","sha256":"c81df0ef988dfd9912ae5c341f044a7b1fee561e76fc4f45e1385056dc01ff57","content":"\"use client\";\n\nimport { OpenStatus as Status } from \"@zappie/foundations\";\nimport type { Branch } from \"@zappie/sdk\";\nimport \"./open-status.css\";\n\nexport function OpenStatus({\n branch,\n timezone,\n}: {\n branch: Branch;\n timezone?: string;\n}) {\n return (\n <Status\n branch={branch}\n {...(timezone !== undefined ? { timezone } : {})}\n className=\"z-kit-open-status\"\n >\n {({ label }) => label}\n </Status>\n );\n}\n"},{"path":"open-status.css","target":"components/zappie/open-status.css","sha256":"797ba336babbf5fb4ffad66d707c00233b0bb77a33ff9187473653ae229d764f","content":".z-kit-open-status {\n display: inline-flex;\n align-items: center;\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-1) var(--z-space-3);\n border: 1px solid var(--z-danger);\n background: var(--z-surface-raised);\n color: var(--z-text);\n font-family: var(--z-font-body);\n font-size: var(--z-text-sm);\n}\n\n.z-kit-open-status[data-state=\"open\"] {\n border-color: var(--z-success);\n color: var(--z-success);\n}\n"}],"tokens":["--z-radius-pill","--z-space-1","--z-space-3","--z-surface-raised","--z-danger","--z-success","--z-font-body","--z-text-sm"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/open-status"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["order-status"],"files":[{"path":"order-page.tsx","target":"components/zappie/order-page.tsx","sha256":"fa18441ee701334f546da559740f4691e742d5924b8b00997cbad1d319d18dcd","content":"\"use client\";\n\nimport { OrderStatus } from \"@/components/zappie/order-status\";\nimport type { CustomerClient } from \"@zappie/sdk/customer\";\nimport \"./order-page.css\";\n\nexport type OrderPageProps = {\n customer: CustomerClient;\n orderId: string;\n supportLinks?: { label: string; href: string }[];\n};\n\nexport function OrderPage({\n customer,\n orderId,\n supportLinks = [],\n}: OrderPageProps) {\n return (\n <main className=\"z-kit-order-page\">\n <OrderStatus customer={customer} orderId={orderId} />\n {supportLinks.length > 0 && (\n <nav aria-label=\"Order support\">\n {supportLinks.map((link) => (\n <a key={link.href} href={link.href}>\n {link.label}\n </a>\n ))}\n </nav>\n )}\n </main>\n );\n}\n"},{"path":"order-page.css","target":"components/zappie/order-page.css","sha256":"4134194778aa0af91ab0d76f605107d922764d0bb6a9d1f99972b8715512105b","content":".z-kit-order-page {\n display: grid;\n gap: var(--z-space-5);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-order-page nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-3);\n}\n.z-kit-order-page a {\n color: inherit;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-text","--z-accent","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/order-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"order-page","type":"block","version":"1.0.0","title":"Order page","description":"Show order progress and your support links.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["order-status"],"files":[{"path":"order-page.tsx","target":"components/zappie/order-page.tsx","sha256":"fa18441ee701334f546da559740f4691e742d5924b8b00997cbad1d319d18dcd","content":"\"use client\";\n\nimport { OrderStatus } from \"@/components/zappie/order-status\";\nimport type { CustomerClient } from \"@zappie/sdk/customer\";\nimport \"./order-page.css\";\n\nexport type OrderPageProps = {\n customer: CustomerClient;\n orderId: string;\n supportLinks?: { label: string; href: string }[];\n};\n\nexport function OrderPage({\n customer,\n orderId,\n supportLinks = [],\n}: OrderPageProps) {\n return (\n <main className=\"z-kit-order-page\">\n <OrderStatus customer={customer} orderId={orderId} />\n {supportLinks.length > 0 && (\n <nav aria-label=\"Order support\">\n {supportLinks.map((link) => (\n <a key={link.href} href={link.href}>\n {link.label}\n </a>\n ))}\n </nav>\n )}\n </main>\n );\n}\n"},{"path":"order-page.css","target":"components/zappie/order-page.css","sha256":"4134194778aa0af91ab0d76f605107d922764d0bb6a9d1f99972b8715512105b","content":".z-kit-order-page {\n display: grid;\n gap: var(--z-space-5);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-order-page nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-3);\n}\n.z-kit-order-page a {\n color: inherit;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-text","--z-accent","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/order-page"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money"],"files":[{"path":"order-status.tsx","target":"components/zappie/order-status.tsx","sha256":"b991fd49edd9cb6543bac84c75ae0e314543637b5e78af9e3ba32de718209d63","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { money } from \"@zappie/sdk\";\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport \"./order-status.css\";\nimport { useOrderStatus } from \"./use-order-status\";\n\nexport type OrderStatusProps = { customer: CustomerClient; orderId: string };\n\nexport function OrderStatus({ customer, orderId }: OrderStatusProps) {\n const { order, error } = useOrderStatus(customer, orderId);\n if (error) {\n return (\n <section className=\"z-kit-order-status\" role=\"alert\">\n {error}\n </section>\n );\n }\n if (!order) {\n return (\n <section className=\"z-kit-order-status\" role=\"status\">\n Loading order…\n </section>\n );\n }\n return (\n <section\n className=\"z-kit-order-status\"\n aria-labelledby=\"order-status-title\"\n >\n <h1 id=\"order-status-title\">Order {order.id}</h1>\n <p className=\"z-kit-order-status__current\">{label(order.status)}</p>\n {order.eta_minutes !== undefined && (\n <p>Estimated arrival in {order.eta_minutes} minutes</p>\n )}\n <ol>\n {order.timeline.map((step, index) => (\n <li key={`${step.status}:${step.at}:${index}`}>\n <strong>{label(step.status)}</strong>{\" \"}\n <time dateTime={step.at}>{step.at}</time>\n </li>\n ))}\n </ol>\n <ul>\n {order.lines.map((line, index) => (\n <li key={`${line.item_id}:${index}`}>\n {line.quantity} × {line.name} <Money value={fromWire(line.total)} />\n </li>\n ))}\n </ul>\n <p>\n <strong>Total</strong> <Money value={fromWire(order.total)} />\n </p>\n </section>\n );\n}\n\nfunction label(value: string) {\n return value\n .replaceAll(\"_\", \" \")\n .replace(/^./, (first) => first.toUpperCase());\n}\n\nfunction fromWire(value: CustomerOrder[\"total\"]) {\n return money(value.amount_minor, value.currency);\n}\n"},{"path":"use-order-status.ts","target":"components/zappie/use-order-status.ts","sha256":"4ac07c68e8941f813f0cb19f678a89f7ebbcea47650246959585161258548f74","content":"\"use client\";\n\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport { useEffect, useState } from \"react\";\n\nconst final = new Set([\"completed\", \"cancelled\", \"rejected\"]);\n\nexport function useOrderStatus(customer: CustomerClient, orderId: string) {\n const [order, setOrder] = useState<CustomerOrder>();\n const [error, setError] = useState<string>();\n useEffect(() => {\n const abort = new AbortController();\n let stop = () => {};\n void customer\n .order(orderId)\n .then((value) => {\n if (abort.signal.aborted) {\n return;\n }\n setOrder(value);\n if (final.has(value.status)) {\n return;\n }\n stop = customer.watchOrder(orderId, {\n signal: abort.signal,\n onOrder: (next) => {\n setOrder(next);\n if (final.has(next.status)) {\n const complete = stop;\n stop = () => {};\n complete();\n }\n },\n });\n })\n .catch((reason: unknown) => {\n if (!abort.signal.aborted) {\n setError(\n reason instanceof Error ? reason.message : \"Order unavailable\",\n );\n }\n });\n return () => {\n abort.abort();\n stop();\n };\n }, [customer, orderId]);\n return { order, error };\n}\n"},{"path":"order-status.css","target":"components/zappie/order-status.css","sha256":"340158362a8433f25181bf1c38df261f313e22f85dbb1586e2bd0c376a343cfa","content":".z-kit-order-status {\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-order-status__current {\n color: inherit;\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n.z-kit-order-status ol,\n.z-kit-order-status ul {\n display: grid;\n gap: var(--z-space-2);\n padding-inline-start: var(--z-space-5);\n}\n"}],"tokens":["--z-space-2","--z-space-4","--z-space-5","--z-text","--z-accent","--z-font-body","--z-font-display","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/order-status"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"order-status","type":"component","version":"1.0.0","title":"Order status","description":"Show live order progress, items and totals.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money"],"files":[{"path":"order-status.tsx","target":"components/zappie/order-status.tsx","sha256":"b991fd49edd9cb6543bac84c75ae0e314543637b5e78af9e3ba32de718209d63","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport { money } from \"@zappie/sdk\";\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport \"./order-status.css\";\nimport { useOrderStatus } from \"./use-order-status\";\n\nexport type OrderStatusProps = { customer: CustomerClient; orderId: string };\n\nexport function OrderStatus({ customer, orderId }: OrderStatusProps) {\n const { order, error } = useOrderStatus(customer, orderId);\n if (error) {\n return (\n <section className=\"z-kit-order-status\" role=\"alert\">\n {error}\n </section>\n );\n }\n if (!order) {\n return (\n <section className=\"z-kit-order-status\" role=\"status\">\n Loading order…\n </section>\n );\n }\n return (\n <section\n className=\"z-kit-order-status\"\n aria-labelledby=\"order-status-title\"\n >\n <h1 id=\"order-status-title\">Order {order.id}</h1>\n <p className=\"z-kit-order-status__current\">{label(order.status)}</p>\n {order.eta_minutes !== undefined && (\n <p>Estimated arrival in {order.eta_minutes} minutes</p>\n )}\n <ol>\n {order.timeline.map((step, index) => (\n <li key={`${step.status}:${step.at}:${index}`}>\n <strong>{label(step.status)}</strong>{\" \"}\n <time dateTime={step.at}>{step.at}</time>\n </li>\n ))}\n </ol>\n <ul>\n {order.lines.map((line, index) => (\n <li key={`${line.item_id}:${index}`}>\n {line.quantity} × {line.name} <Money value={fromWire(line.total)} />\n </li>\n ))}\n </ul>\n <p>\n <strong>Total</strong> <Money value={fromWire(order.total)} />\n </p>\n </section>\n );\n}\n\nfunction label(value: string) {\n return value\n .replaceAll(\"_\", \" \")\n .replace(/^./, (first) => first.toUpperCase());\n}\n\nfunction fromWire(value: CustomerOrder[\"total\"]) {\n return money(value.amount_minor, value.currency);\n}\n"},{"path":"use-order-status.ts","target":"components/zappie/use-order-status.ts","sha256":"4ac07c68e8941f813f0cb19f678a89f7ebbcea47650246959585161258548f74","content":"\"use client\";\n\nimport type { CustomerClient, CustomerOrder } from \"@zappie/sdk/customer\";\nimport { useEffect, useState } from \"react\";\n\nconst final = new Set([\"completed\", \"cancelled\", \"rejected\"]);\n\nexport function useOrderStatus(customer: CustomerClient, orderId: string) {\n const [order, setOrder] = useState<CustomerOrder>();\n const [error, setError] = useState<string>();\n useEffect(() => {\n const abort = new AbortController();\n let stop = () => {};\n void customer\n .order(orderId)\n .then((value) => {\n if (abort.signal.aborted) {\n return;\n }\n setOrder(value);\n if (final.has(value.status)) {\n return;\n }\n stop = customer.watchOrder(orderId, {\n signal: abort.signal,\n onOrder: (next) => {\n setOrder(next);\n if (final.has(next.status)) {\n const complete = stop;\n stop = () => {};\n complete();\n }\n },\n });\n })\n .catch((reason: unknown) => {\n if (!abort.signal.aborted) {\n setError(\n reason instanceof Error ? reason.message : \"Order unavailable\",\n );\n }\n });\n return () => {\n abort.abort();\n stop();\n };\n }, [customer, orderId]);\n return { order, error };\n}\n"},{"path":"order-status.css","target":"components/zappie/order-status.css","sha256":"340158362a8433f25181bf1c38df261f313e22f85dbb1586e2bd0c376a343cfa","content":".z-kit-order-status {\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-order-status__current {\n color: inherit;\n font-family: var(--z-font-display);\n font-size: var(--z-text-lg);\n}\n.z-kit-order-status ol,\n.z-kit-order-status ul {\n display: grid;\n gap: var(--z-space-2);\n padding-inline-start: var(--z-space-5);\n}\n"}],"tokens":["--z-space-2","--z-space-4","--z-space-5","--z-text","--z-accent","--z-font-body","--z-font-display","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/order-status"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"zappie-provider.tsx","target":"components/zappie/zappie-provider.tsx","sha256":"f236ea148e636dcb27cad117a33d8685900cc767e49ca4157fc68987fc1d7cba","content":"\"use client\";\n\nimport { createZappie } from \"@zappie/sdk\";\nimport { ZappieProvider as SdkProvider } from \"@zappie/sdk/react\";\nimport type { ThemeFont, TokenName } from \"@zappie/tokens\";\nimport \"@zappie/tokens/tokens.css\";\nimport { useMemo, type CSSProperties, type ReactNode } from \"react\";\nimport \"./provider.css\";\n\ntype ThemeStyle = CSSProperties & Partial<Record<TokenName, string>>;\ntype FontRoles = Partial<Record<ThemeFont[\"role\"], string>>;\n\nexport type ZappieProviderProps = {\n publishableKey: string;\n store: string;\n branch?: string;\n tokens?: Partial<Record<TokenName, string>>;\n fonts?: FontRoles;\n children: ReactNode;\n};\n\nexport function ZappieProvider({\n publishableKey,\n store,\n branch,\n tokens,\n fonts,\n children,\n}: ZappieProviderProps) {\n const client = useMemo(\n () =>\n createZappie({\n key: publishableKey,\n store,\n ...(branch ? { branch } : {}),\n }),\n [branch, publishableKey, store],\n );\n const style: ThemeStyle = {\n ...tokens,\n ...(fonts?.body ? { \"--z-font-body\": fonts.body } : {}),\n ...(fonts?.display ? { \"--z-font-display\": fonts.display } : {}),\n ...(fonts?.mono ? { \"--z-font-mono\": fonts.mono } : {}),\n };\n return (\n <SdkProvider client={client}>\n <div className=\"z-kit-provider\" style={style}>\n {children}\n </div>\n </SdkProvider>\n );\n}\n"},{"path":"provider.css","target":"components/zappie/provider.css","sha256":"afac1fae67d0983cd640d0ad8fb453446a428b3a339bbdf0857a1312606b36d0","content":".z-kit-provider {\n color: var(--z-text);\n background: var(--z-bg);\n font-family: var(--z-font-body);\n}\n"}],"tokens":["--z-bg","--z-text","--z-font-body","--z-font-display","--z-font-mono"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/provider"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"provider","type":"component","version":"1.0.0","title":"Zappie provider","description":"Create a storefront client and apply scoped tokens and fonts.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"zappie-provider.tsx","target":"components/zappie/zappie-provider.tsx","sha256":"f236ea148e636dcb27cad117a33d8685900cc767e49ca4157fc68987fc1d7cba","content":"\"use client\";\n\nimport { createZappie } from \"@zappie/sdk\";\nimport { ZappieProvider as SdkProvider } from \"@zappie/sdk/react\";\nimport type { ThemeFont, TokenName } from \"@zappie/tokens\";\nimport \"@zappie/tokens/tokens.css\";\nimport { useMemo, type CSSProperties, type ReactNode } from \"react\";\nimport \"./provider.css\";\n\ntype ThemeStyle = CSSProperties & Partial<Record<TokenName, string>>;\ntype FontRoles = Partial<Record<ThemeFont[\"role\"], string>>;\n\nexport type ZappieProviderProps = {\n publishableKey: string;\n store: string;\n branch?: string;\n tokens?: Partial<Record<TokenName, string>>;\n fonts?: FontRoles;\n children: ReactNode;\n};\n\nexport function ZappieProvider({\n publishableKey,\n store,\n branch,\n tokens,\n fonts,\n children,\n}: ZappieProviderProps) {\n const client = useMemo(\n () =>\n createZappie({\n key: publishableKey,\n store,\n ...(branch ? { branch } : {}),\n }),\n [branch, publishableKey, store],\n );\n const style: ThemeStyle = {\n ...tokens,\n ...(fonts?.body ? { \"--z-font-body\": fonts.body } : {}),\n ...(fonts?.display ? { \"--z-font-display\": fonts.display } : {}),\n ...(fonts?.mono ? { \"--z-font-mono\": fonts.mono } : {}),\n };\n return (\n <SdkProvider client={client}>\n <div className=\"z-kit-provider\" style={style}>\n {children}\n </div>\n </SdkProvider>\n );\n}\n"},{"path":"provider.css","target":"components/zappie/provider.css","sha256":"afac1fae67d0983cd640d0ad8fb453446a428b3a339bbdf0857a1312606b36d0","content":".z-kit-provider {\n color: var(--z-text);\n background: var(--z-bg);\n font-family: var(--z-font-body);\n}\n"}],"tokens":["--z-bg","--z-text","--z-font-body","--z-font-display","--z-font-mono"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/provider"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits.","requires":{"@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":[],"files":[{"path":"quantity.tsx","target":"components/zappie/quantity.tsx","sha256":"bd1480c30283953a9417904237528614793c90a2be8b0cfd58e4040e01282e22","content":"\"use client\";\n\nimport { Quantity as QuantityParts } from \"@zappie/foundations\";\nimport { Minus, Plus } from \"lucide-react\";\nimport \"./quantity.css\";\n\nexport type QuantityProps = {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n label?: string;\n};\n\nexport function Quantity({ label = \"Quantity\", ...props }: QuantityProps) {\n return (\n <QuantityParts.Root className=\"z-kit-quantity\" {...props}>\n <QuantityParts.Decrement aria-label={`Decrease ${label}`}>\n <Minus aria-hidden=\"true\" />\n </QuantityParts.Decrement>\n <QuantityParts.Value aria-label={label} />\n <QuantityParts.Increment aria-label={`Increase ${label}`}>\n <Plus aria-hidden=\"true\" />\n </QuantityParts.Increment>\n </QuantityParts.Root>\n );\n}\n"},{"path":"quantity.css","target":"components/zappie/quantity.css","sha256":"7b559d289ea0e7b54c0df831c22a1cafd3547f1238840f25b9f9a00eec7f1ff8","content":".z-kit-quantity {\n display: inline-grid;\n grid-template-columns: auto minmax(var(--z-space-7), auto) auto;\n align-items: center;\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-surface);\n}\n\n.z-kit-quantity button,\n.z-kit-quantity input {\n min-height: var(--z-space-7);\n border: 0;\n background: transparent;\n color: var(--z-text);\n font: inherit;\n text-align: center;\n}\n\n.z-kit-quantity button {\n padding: var(--z-space-2) var(--z-space-3);\n cursor: pointer;\n}\n\n.z-kit-quantity button:focus-visible,\n.z-kit-quantity input:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: -2px;\n}\n\n.z-kit-quantity button:disabled {\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-quantity svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-7","--z-border","--z-radius-md","--z-surface","--z-text","--z-text-muted","--z-focus-ring","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/quantity"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"quantity","type":"component","version":"1.0.0","title":"Quantity","description":"Adjust quantity within its limits.","requires":{"@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{"lucide-react":"^0.546.0"},"registryDependencies":[],"files":[{"path":"quantity.tsx","target":"components/zappie/quantity.tsx","sha256":"bd1480c30283953a9417904237528614793c90a2be8b0cfd58e4040e01282e22","content":"\"use client\";\n\nimport { Quantity as QuantityParts } from \"@zappie/foundations\";\nimport { Minus, Plus } from \"lucide-react\";\nimport \"./quantity.css\";\n\nexport type QuantityProps = {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n label?: string;\n};\n\nexport function Quantity({ label = \"Quantity\", ...props }: QuantityProps) {\n return (\n <QuantityParts.Root className=\"z-kit-quantity\" {...props}>\n <QuantityParts.Decrement aria-label={`Decrease ${label}`}>\n <Minus aria-hidden=\"true\" />\n </QuantityParts.Decrement>\n <QuantityParts.Value aria-label={label} />\n <QuantityParts.Increment aria-label={`Increase ${label}`}>\n <Plus aria-hidden=\"true\" />\n </QuantityParts.Increment>\n </QuantityParts.Root>\n );\n}\n"},{"path":"quantity.css","target":"components/zappie/quantity.css","sha256":"7b559d289ea0e7b54c0df831c22a1cafd3547f1238840f25b9f9a00eec7f1ff8","content":".z-kit-quantity {\n display: inline-grid;\n grid-template-columns: auto minmax(var(--z-space-7), auto) auto;\n align-items: center;\n overflow: hidden;\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-surface);\n}\n\n.z-kit-quantity button,\n.z-kit-quantity input {\n min-height: var(--z-space-7);\n border: 0;\n background: transparent;\n color: var(--z-text);\n font: inherit;\n text-align: center;\n}\n\n.z-kit-quantity button {\n padding: var(--z-space-2) var(--z-space-3);\n cursor: pointer;\n}\n\n.z-kit-quantity button:focus-visible,\n.z-kit-quantity input:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: -2px;\n}\n\n.z-kit-quantity button:disabled {\n color: var(--z-text-muted);\n cursor: not-allowed;\n}\n\n.z-kit-quantity svg {\n width: var(--z-text-lg);\n height: var(--z-text-lg);\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-7","--z-border","--z-radius-md","--z-surface","--z-text","--z-text-muted","--z-focus-ring","--z-text-lg"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/quantity"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"schedule-picker.tsx","target":"components/zappie/schedule-picker.tsx","sha256":"5c03feda252601157b1e6abbfd6d95b1fa2147891a446c1498cece6a4b3460db","content":"\"use client\";\n\nimport { Schedule } from \"@zappie/foundations\";\nimport type { Branch, CartFulfilment } from \"@zappie/sdk\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport \"./schedule-picker.css\";\n\nexport type SchedulePickerProps = { branch: Branch };\n\nfunction scheduled(value: CartFulfilment, scheduledFor: string | null) {\n if (value.type === \"dine_in\") {\n return value;\n }\n const { scheduledFor: _current, ...rest } = value;\n return scheduledFor ? { ...rest, scheduledFor } : rest;\n}\n\nexport function SchedulePicker({ branch }: SchedulePickerProps) {\n const cart = useCart();\n const fulfilment = cart.fulfilment;\n const value =\n fulfilment && fulfilment.type !== \"dine_in\"\n ? (fulfilment.scheduledFor ?? null)\n : null;\n const change = (next: string | null) => {\n if (fulfilment) {\n cart.actions.setFulfilment(scheduled(fulfilment, next));\n }\n };\n return (\n <Schedule\n branch={branch}\n value={value}\n onValueChange={change}\n className=\"z-kit-schedule-picker\"\n aria-label=\"Order time\"\n >\n {({ days, canAsap, isAsap, setValue }) => (\n <div>\n <button\n type=\"button\"\n aria-pressed={isAsap}\n disabled={!canAsap}\n onClick={() => setValue(null)}\n >\n ASAP\n </button>\n {days.map((day) => (\n <fieldset key={day.date}>\n <legend>{day.date}</legend>\n {day.slots.map((slot) => (\n <button\n key={slot.value}\n type=\"button\"\n aria-pressed={value === slot.value}\n onClick={() => setValue(slot.value)}\n >\n {slot.label}\n </button>\n ))}\n </fieldset>\n ))}\n </div>\n )}\n </Schedule>\n );\n}\n"},{"path":"schedule-picker.css","target":"components/zappie/schedule-picker.css","sha256":"3392843b2e05cef210cb8d136f7b7e9801e47f33b8d8a69511bdbc4661125bf6","content":".z-kit-schedule-picker > div {\n display: grid;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-schedule-picker fieldset {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-2);\n border: 0;\n padding: 0;\n}\n\n.z-kit-schedule-picker legend {\n width: 100%;\n margin-bottom: var(--z-space-2);\n color: var(--z-text-muted);\n}\n\n.z-kit-schedule-picker button {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-3);\n background: var(--z-surface);\n color: var(--z-text);\n cursor: pointer;\n}\n\n.z-kit-schedule-picker button[aria-pressed=\"true\"] {\n border-color: var(--z-accent);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-schedule-picker button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-border","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/schedule-picker"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"schedule-picker","type":"component","version":"1.0.0","title":"Schedule picker","description":"Choose ASAP or an available time for the cart.","requires":{"@zappie/foundations":"^0.1.0","@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"schedule-picker.tsx","target":"components/zappie/schedule-picker.tsx","sha256":"5c03feda252601157b1e6abbfd6d95b1fa2147891a446c1498cece6a4b3460db","content":"\"use client\";\n\nimport { Schedule } from \"@zappie/foundations\";\nimport type { Branch, CartFulfilment } from \"@zappie/sdk\";\nimport { useCart } from \"@zappie/sdk/react\";\nimport \"./schedule-picker.css\";\n\nexport type SchedulePickerProps = { branch: Branch };\n\nfunction scheduled(value: CartFulfilment, scheduledFor: string | null) {\n if (value.type === \"dine_in\") {\n return value;\n }\n const { scheduledFor: _current, ...rest } = value;\n return scheduledFor ? { ...rest, scheduledFor } : rest;\n}\n\nexport function SchedulePicker({ branch }: SchedulePickerProps) {\n const cart = useCart();\n const fulfilment = cart.fulfilment;\n const value =\n fulfilment && fulfilment.type !== \"dine_in\"\n ? (fulfilment.scheduledFor ?? null)\n : null;\n const change = (next: string | null) => {\n if (fulfilment) {\n cart.actions.setFulfilment(scheduled(fulfilment, next));\n }\n };\n return (\n <Schedule\n branch={branch}\n value={value}\n onValueChange={change}\n className=\"z-kit-schedule-picker\"\n aria-label=\"Order time\"\n >\n {({ days, canAsap, isAsap, setValue }) => (\n <div>\n <button\n type=\"button\"\n aria-pressed={isAsap}\n disabled={!canAsap}\n onClick={() => setValue(null)}\n >\n ASAP\n </button>\n {days.map((day) => (\n <fieldset key={day.date}>\n <legend>{day.date}</legend>\n {day.slots.map((slot) => (\n <button\n key={slot.value}\n type=\"button\"\n aria-pressed={value === slot.value}\n onClick={() => setValue(slot.value)}\n >\n {slot.label}\n </button>\n ))}\n </fieldset>\n ))}\n </div>\n )}\n </Schedule>\n );\n}\n"},{"path":"schedule-picker.css","target":"components/zappie/schedule-picker.css","sha256":"3392843b2e05cef210cb8d136f7b7e9801e47f33b8d8a69511bdbc4661125bf6","content":".z-kit-schedule-picker > div {\n display: grid;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-schedule-picker fieldset {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-2);\n border: 0;\n padding: 0;\n}\n\n.z-kit-schedule-picker legend {\n width: 100%;\n margin-bottom: var(--z-space-2);\n color: var(--z-text-muted);\n}\n\n.z-kit-schedule-picker button {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-3);\n background: var(--z-surface);\n color: var(--z-text);\n cursor: pointer;\n}\n\n.z-kit-schedule-picker button[aria-pressed=\"true\"] {\n border-color: var(--z-accent);\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n\n.z-kit-schedule-picker button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-border","--z-radius-pill","--z-surface","--z-text","--z-text-muted","--z-accent","--z-accent-contrast","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/schedule-picker"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money"],"files":[{"path":"search.tsx","target":"components/zappie/search.tsx","sha256":"3b375a648b5e859795a4112fd5c2dca2e30e855276a6f87871c61c34cf66422d","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport {\n Overlay,\n Search as SearchParts,\n type SearchResult,\n} from \"@zappie/foundations\";\nimport type { Item } from \"@zappie/sdk\";\nimport { useMemo, useState } from \"react\";\nimport \"./search.css\";\nimport { useSearchResults } from \"./use-search-results\";\n\nexport type SearchProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (item: Item) => void;\n branch?: string;\n};\n\nexport function Search({ open, onOpenChange, onSelect, branch }: SearchProps) {\n const [query, setQuery] = useState(\"\");\n const source = useSearchResults(query, branch);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n const choose = (result: SearchResult) => {\n if (result.type === \"item\") {\n onSelect(result.item);\n onOpenChange(false);\n }\n };\n return (\n <Overlay.Root\n param=\"search\"\n id=\"kit\"\n navigation={navigation}\n presentation=\"dialog\"\n >\n <Overlay.Content className=\"z-kit-search\">\n <header>\n <Overlay.Title>Search menu</Overlay.Title>\n <Overlay.Close aria-label=\"Close search\">×</Overlay.Close>\n </header>\n <SearchParts.Root\n query={query}\n onQueryChange={setQuery}\n onSelect={choose}\n source={{\n results: source.results,\n isLoading: source.loading,\n ...(source.error ? { error: source.error } : {}),\n }}\n >\n {(state) => (\n <div>\n <SearchParts.Input aria-label=\"Search menu\" autoFocus />\n <SearchParts.Results>\n {state.results.map((result, index) => (\n <SearchParts.Result\n key={\n result.type === \"item\"\n ? result.item.id\n : result.category.id\n }\n index={index}\n >\n <SearchRow result={result} />\n </SearchParts.Result>\n ))}\n </SearchParts.Results>\n {source.loading && <p role=\"status\">Searching…</p>}\n {source.error && <p role=\"alert\">{source.error}</p>}\n {query.trim() &&\n !source.loading &&\n !source.error &&\n state.results.length === 0 && <p>No results found.</p>}\n </div>\n )}\n </SearchParts.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\nfunction SearchRow({ result }: { result: SearchResult }) {\n if (result.type === \"category\") {\n return <span>{result.category.name}</span>;\n }\n return (\n <span className=\"z-kit-search__result\">\n <span>{result.item.name}</span>\n <Money value={result.item.price} />\n {!result.item.isAvailable && <span>Unavailable</span>}\n </span>\n );\n}\n"},{"path":"use-search-results.ts","target":"components/zappie/use-search-results.ts","sha256":"1cb83872d1fd5c95811f076ac41a3047dba252d02abb23ddd131d285e7bff9d2","content":"\"use client\";\n\nimport type { SearchResult } from \"@zappie/foundations\";\nimport { useZappie } from \"@zappie/sdk/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport function useSearchResults(query: string, branch?: string) {\n const client = useZappie();\n const revision = useRef(0);\n const [results, setResults] = useState<SearchResult[]>([]);\n const [status, setStatus] = useState<{\n loading: boolean;\n error?: string;\n }>({ loading: false });\n useEffect(() => {\n const value = query.trim();\n const current = ++revision.current;\n if (!value) {\n setResults([]);\n setStatus({ loading: false });\n return;\n }\n const abort = new AbortController();\n const timer = setTimeout(() => {\n setStatus({ loading: true });\n void client.menu\n .search(value, {\n ...(branch ? { branch } : {}),\n signal: abort.signal,\n })\n .then((page) => {\n if (revision.current !== current) {\n return;\n }\n setResults([\n ...Object.values(page.menu.items).map((item) => ({\n type: \"item\" as const,\n item,\n score: 1,\n })),\n ...page.menu.categories.map((category) => ({\n type: \"category\" as const,\n category,\n score: 1,\n })),\n ]);\n setStatus({ loading: false });\n })\n .catch((error: unknown) => {\n if (revision.current !== current || abort.signal.aborted) {\n return;\n }\n setStatus({\n loading: false,\n error: error instanceof Error ? error.message : \"Search failed\",\n });\n });\n }, 500);\n return () => {\n clearTimeout(timer);\n abort.abort();\n };\n }, [branch, client, query]);\n return { results, ...status };\n}\n"},{"path":"search.css","target":"components/zappie/search.css","sha256":"f163a6d1cd68ff0ba9b02db3df021300db4727ba2efa39f118aef537a1dce256","content":".z-kit-search {\n display: grid;\n gap: var(--z-space-4);\n padding: var(--z-space-5);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-search header,\n.z-kit-search__result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n.z-kit-search input {\n width: 100%;\n padding: var(--z-space-3);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-bg);\n color: var(--z-text);\n}\n.z-kit-search [role=\"option\"] {\n padding: var(--z-space-3);\n cursor: pointer;\n}\n.z-kit-search [aria-selected=\"true\"] {\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-md","--z-bg","--z-surface","--z-text","--z-accent","--z-accent-contrast","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/search"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"search","type":"component","version":"1.0.0","title":"Search","description":"Search the menu as shoppers type and discard outdated results.","requires":{"@zappie/sdk":"^0.1.0","@zappie/foundations":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":["money"],"files":[{"path":"search.tsx","target":"components/zappie/search.tsx","sha256":"3b375a648b5e859795a4112fd5c2dca2e30e855276a6f87871c61c34cf66422d","content":"\"use client\";\n\nimport { Money } from \"@/components/zappie/money\";\nimport {\n Overlay,\n Search as SearchParts,\n type SearchResult,\n} from \"@zappie/foundations\";\nimport type { Item } from \"@zappie/sdk\";\nimport { useMemo, useState } from \"react\";\nimport \"./search.css\";\nimport { useSearchResults } from \"./use-search-results\";\n\nexport type SearchProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onSelect: (item: Item) => void;\n branch?: string;\n};\n\nexport function Search({ open, onOpenChange, onSelect, branch }: SearchProps) {\n const [query, setQuery] = useState(\"\");\n const source = useSearchResults(query, branch);\n const navigation = useMemo(\n () => ({\n isOpen: open,\n open: () => onOpenChange(true),\n close: () => onOpenChange(false),\n }),\n [onOpenChange, open],\n );\n const choose = (result: SearchResult) => {\n if (result.type === \"item\") {\n onSelect(result.item);\n onOpenChange(false);\n }\n };\n return (\n <Overlay.Root\n param=\"search\"\n id=\"kit\"\n navigation={navigation}\n presentation=\"dialog\"\n >\n <Overlay.Content className=\"z-kit-search\">\n <header>\n <Overlay.Title>Search menu</Overlay.Title>\n <Overlay.Close aria-label=\"Close search\">×</Overlay.Close>\n </header>\n <SearchParts.Root\n query={query}\n onQueryChange={setQuery}\n onSelect={choose}\n source={{\n results: source.results,\n isLoading: source.loading,\n ...(source.error ? { error: source.error } : {}),\n }}\n >\n {(state) => (\n <div>\n <SearchParts.Input aria-label=\"Search menu\" autoFocus />\n <SearchParts.Results>\n {state.results.map((result, index) => (\n <SearchParts.Result\n key={\n result.type === \"item\"\n ? result.item.id\n : result.category.id\n }\n index={index}\n >\n <SearchRow result={result} />\n </SearchParts.Result>\n ))}\n </SearchParts.Results>\n {source.loading && <p role=\"status\">Searching…</p>}\n {source.error && <p role=\"alert\">{source.error}</p>}\n {query.trim() &&\n !source.loading &&\n !source.error &&\n state.results.length === 0 && <p>No results found.</p>}\n </div>\n )}\n </SearchParts.Root>\n </Overlay.Content>\n </Overlay.Root>\n );\n}\n\nfunction SearchRow({ result }: { result: SearchResult }) {\n if (result.type === \"category\") {\n return <span>{result.category.name}</span>;\n }\n return (\n <span className=\"z-kit-search__result\">\n <span>{result.item.name}</span>\n <Money value={result.item.price} />\n {!result.item.isAvailable && <span>Unavailable</span>}\n </span>\n );\n}\n"},{"path":"use-search-results.ts","target":"components/zappie/use-search-results.ts","sha256":"1cb83872d1fd5c95811f076ac41a3047dba252d02abb23ddd131d285e7bff9d2","content":"\"use client\";\n\nimport type { SearchResult } from \"@zappie/foundations\";\nimport { useZappie } from \"@zappie/sdk/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport function useSearchResults(query: string, branch?: string) {\n const client = useZappie();\n const revision = useRef(0);\n const [results, setResults] = useState<SearchResult[]>([]);\n const [status, setStatus] = useState<{\n loading: boolean;\n error?: string;\n }>({ loading: false });\n useEffect(() => {\n const value = query.trim();\n const current = ++revision.current;\n if (!value) {\n setResults([]);\n setStatus({ loading: false });\n return;\n }\n const abort = new AbortController();\n const timer = setTimeout(() => {\n setStatus({ loading: true });\n void client.menu\n .search(value, {\n ...(branch ? { branch } : {}),\n signal: abort.signal,\n })\n .then((page) => {\n if (revision.current !== current) {\n return;\n }\n setResults([\n ...Object.values(page.menu.items).map((item) => ({\n type: \"item\" as const,\n item,\n score: 1,\n })),\n ...page.menu.categories.map((category) => ({\n type: \"category\" as const,\n category,\n score: 1,\n })),\n ]);\n setStatus({ loading: false });\n })\n .catch((error: unknown) => {\n if (revision.current !== current || abort.signal.aborted) {\n return;\n }\n setStatus({\n loading: false,\n error: error instanceof Error ? error.message : \"Search failed\",\n });\n });\n }, 500);\n return () => {\n clearTimeout(timer);\n abort.abort();\n };\n }, [branch, client, query]);\n return { results, ...status };\n}\n"},{"path":"search.css","target":"components/zappie/search.css","sha256":"f163a6d1cd68ff0ba9b02db3df021300db4727ba2efa39f118aef537a1dce256","content":".z-kit-search {\n display: grid;\n gap: var(--z-space-4);\n padding: var(--z-space-5);\n background: var(--z-surface);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-search header,\n.z-kit-search__result {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--z-space-3);\n}\n.z-kit-search input {\n width: 100%;\n padding: var(--z-space-3);\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-md);\n background: var(--z-bg);\n color: var(--z-text);\n}\n.z-kit-search [role=\"option\"] {\n padding: var(--z-space-3);\n cursor: pointer;\n}\n.z-kit-search [aria-selected=\"true\"] {\n background: var(--z-accent);\n color: var(--z-accent-contrast);\n}\n"}],"tokens":["--z-space-3","--z-space-4","--z-space-5","--z-border","--z-radius-md","--z-bg","--z-surface","--z-text","--z-accent","--z-accent-contrast","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/search"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"sign-in-button.tsx","target":"components/zappie/sign-in-button.tsx","sha256":"5646ec6602d4330d6f78fdb3b4c93fa4b5aeb90dd356e872b6264c56a253485f","content":"\"use client\";\n\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport \"./sign-in-button.css\";\n\nexport function SignInButton() {\n const auth = useAuth();\n if (auth.state !== \"authenticated\" || !auth.customer) {\n return (\n <button\n type=\"button\"\n className=\"z-kit-sign-in-button\"\n disabled={auth.state === \"unknown\"}\n data-zappie-signin=\"\"\n onClick={() => auth.signIn()}\n >\n Sign in\n </button>\n );\n }\n const name = auth.customer.given_name ?? \"Your account\";\n return (\n <div className=\"z-kit-sign-in-button__customer\">\n <span>{name}</span>\n <button type=\"button\" onClick={() => void auth.signOut()}>\n Sign out\n </button>\n </div>\n );\n}\n"},{"path":"sign-in-button.css","target":"components/zappie/sign-in-button.css","sha256":"b610a06261fabc6bea6640b93a0d0b64675f9465e6cb2884136756860e41d84d","content":".z-kit-sign-in-button,\n.z-kit-sign-in-button__customer button {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-surface);\n color: var(--z-text);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-sign-in-button__customer {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-sign-in-button:focus-visible,\n.z-kit-sign-in-button__customer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-pill","--z-surface","--z-text","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/sign-in-button"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"sign-in-button","type":"component","version":"1.0.0","title":"Sign-in button","description":"Show customer sign-in and sign-out controls.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"sign-in-button.tsx","target":"components/zappie/sign-in-button.tsx","sha256":"5646ec6602d4330d6f78fdb3b4c93fa4b5aeb90dd356e872b6264c56a253485f","content":"\"use client\";\n\nimport { useAuth } from \"@zappie/sdk/customer/react\";\nimport \"./sign-in-button.css\";\n\nexport function SignInButton() {\n const auth = useAuth();\n if (auth.state !== \"authenticated\" || !auth.customer) {\n return (\n <button\n type=\"button\"\n className=\"z-kit-sign-in-button\"\n disabled={auth.state === \"unknown\"}\n data-zappie-signin=\"\"\n onClick={() => auth.signIn()}\n >\n Sign in\n </button>\n );\n }\n const name = auth.customer.given_name ?? \"Your account\";\n return (\n <div className=\"z-kit-sign-in-button__customer\">\n <span>{name}</span>\n <button type=\"button\" onClick={() => void auth.signOut()}>\n Sign out\n </button>\n </div>\n );\n}\n"},{"path":"sign-in-button.css","target":"components/zappie/sign-in-button.css","sha256":"b610a06261fabc6bea6640b93a0d0b64675f9465e6cb2884136756860e41d84d","content":".z-kit-sign-in-button,\n.z-kit-sign-in-button__customer button {\n border: 1px solid var(--z-border);\n border-radius: var(--z-radius-pill);\n padding: var(--z-space-2) var(--z-space-4);\n background: var(--z-surface);\n color: var(--z-text);\n font: inherit;\n cursor: pointer;\n}\n\n.z-kit-sign-in-button__customer {\n display: inline-flex;\n align-items: center;\n gap: var(--z-space-3);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n\n.z-kit-sign-in-button:focus-visible,\n.z-kit-sign-in-button__customer button:focus-visible {\n outline: 2px solid var(--z-focus-ring);\n outline-offset: 2px;\n}\n"}],"tokens":["--z-space-2","--z-space-3","--z-space-4","--z-border","--z-radius-pill","--z-surface","--z-text","--z-focus-ring","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/sign-in-button"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"store-footer.tsx","target":"components/zappie/store-footer.tsx","sha256":"a3299049ac616b7e33bcb6f1ae0fa19128fe04272b753c30a42fcdf063a25045","content":"\"use client\";\n\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport type { ReactNode } from \"react\";\nimport \"./store-footer.css\";\n\nexport type StoreFooterProps = {\n branch?: string;\n legalLinks?: ReactNode;\n};\n\nexport function StoreFooter({ branch, legalLinks }: StoreFooterProps) {\n const store = useStore();\n const cart = useCart();\n if (!store.data) {\n return <footer className=\"z-kit-store-footer\">{legalLinks}</footer>;\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 <footer className=\"z-kit-store-footer\">\n <strong>{store.data.name}</strong>\n {selected?.address && <address>{selected.address}</address>}\n {selected?.pickupAddress?.coordinates && (\n <a\n href={`https://www.google.com/maps?q=${selected.pickupAddress.coordinates.lat},${selected.pickupAddress.coordinates.lng}`}\n rel=\"noreferrer\"\n >\n Pickup location\n </a>\n )}\n {selected?.phone && (\n <a href={`tel:${selected.phone}`}>{selected.phone}</a>\n )}\n {selected?.socialLinks && (\n <nav aria-label=\"Social links\">\n {selected.socialLinks\n .filter((link) => safe(link.url))\n .map((link) => (\n <a\n key={`${link.type}:${link.url}`}\n href={link.url}\n rel=\"noreferrer\"\n >\n {socialLabel(link.type)}\n </a>\n ))}\n </nav>\n )}\n {legalLinks && (\n <div className=\"z-kit-store-footer__legal\">{legalLinks}</div>\n )}\n </footer>\n );\n}\n\nfunction safe(value: string) {\n try {\n const protocol = new URL(value).protocol;\n return protocol === \"http:\" || protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\nfunction socialLabel(value: string) {\n return value.toLowerCase().replace(/^./, (first) => first.toUpperCase());\n}\n"},{"path":"store-footer.css","target":"components/zappie/store-footer.css","sha256":"8c7da495a94c575be597edb405f558460b8a10f1470d84ebf2247c1f2d0d0a63","content":".z-kit-store-footer {\n display: grid;\n gap: var(--z-space-3);\n padding-block: var(--z-space-5);\n border-top: 1px solid var(--z-border);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-footer nav,\n.z-kit-store-footer__legal {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-3);\n}\n.z-kit-store-footer a {\n color: inherit;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-text","--z-accent","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-footer"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://registry.zappie.ai/v1/schema/item.json","name":"store-footer","type":"component","version":"1.0.0","title":"Store footer","description":"Show branch contacts, social links and your legal links.","requires":{"@zappie/sdk":"^0.1.0","@zappie/tokens":"^0.1.0"},"dependencies":{},"registryDependencies":[],"files":[{"path":"store-footer.tsx","target":"components/zappie/store-footer.tsx","sha256":"a3299049ac616b7e33bcb6f1ae0fa19128fe04272b753c30a42fcdf063a25045","content":"\"use client\";\n\nimport { useCart, useStore } from \"@zappie/sdk/react\";\nimport type { ReactNode } from \"react\";\nimport \"./store-footer.css\";\n\nexport type StoreFooterProps = {\n branch?: string;\n legalLinks?: ReactNode;\n};\n\nexport function StoreFooter({ branch, legalLinks }: StoreFooterProps) {\n const store = useStore();\n const cart = useCart();\n if (!store.data) {\n return <footer className=\"z-kit-store-footer\">{legalLinks}</footer>;\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 <footer className=\"z-kit-store-footer\">\n <strong>{store.data.name}</strong>\n {selected?.address && <address>{selected.address}</address>}\n {selected?.pickupAddress?.coordinates && (\n <a\n href={`https://www.google.com/maps?q=${selected.pickupAddress.coordinates.lat},${selected.pickupAddress.coordinates.lng}`}\n rel=\"noreferrer\"\n >\n Pickup location\n </a>\n )}\n {selected?.phone && (\n <a href={`tel:${selected.phone}`}>{selected.phone}</a>\n )}\n {selected?.socialLinks && (\n <nav aria-label=\"Social links\">\n {selected.socialLinks\n .filter((link) => safe(link.url))\n .map((link) => (\n <a\n key={`${link.type}:${link.url}`}\n href={link.url}\n rel=\"noreferrer\"\n >\n {socialLabel(link.type)}\n </a>\n ))}\n </nav>\n )}\n {legalLinks && (\n <div className=\"z-kit-store-footer__legal\">{legalLinks}</div>\n )}\n </footer>\n );\n}\n\nfunction safe(value: string) {\n try {\n const protocol = new URL(value).protocol;\n return protocol === \"http:\" || protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\nfunction socialLabel(value: string) {\n return value.toLowerCase().replace(/^./, (first) => first.toUpperCase());\n}\n"},{"path":"store-footer.css","target":"components/zappie/store-footer.css","sha256":"8c7da495a94c575be597edb405f558460b8a10f1470d84ebf2247c1f2d0d0a63","content":".z-kit-store-footer {\n display: grid;\n gap: var(--z-space-3);\n padding-block: var(--z-space-5);\n border-top: 1px solid var(--z-border);\n color: var(--z-text);\n font-family: var(--z-font-body);\n}\n.z-kit-store-footer nav,\n.z-kit-store-footer__legal {\n display: flex;\n flex-wrap: wrap;\n gap: var(--z-space-3);\n}\n.z-kit-store-footer a {\n color: inherit;\n}\n"}],"tokens":["--z-space-3","--z-space-5","--z-border","--z-text","--z-accent","--z-font-body"],"frameworks":["react-router","next","vite","astro-react"],"docs":"https://docs.zappie.ai/kit/store-footer"}
@@ -0,0 +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"}
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"version":{"type":"string","minLength":1},"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"type":{"type":"string","enum":["component","block"]},"version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"},"title":{"type":"string","minLength":1},"description":{"type":"string","minLength":1}},"required":["name","type","version","title","description"],"additionalProperties":false}}},"required":["version","items"],"additionalProperties":false}
@@ -0,0 +1 @@
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"$schema":{"type":"string","format":"uri"},"name":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"type":{"type":"string","enum":["component","block"]},"version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"},"title":{"type":"string","minLength":1},"description":{"type":"string","minLength":1},"requires":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"dependencies":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"registryDependencies":{"type":"array","items":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"}},"files":{"minItems":1,"type":"array","items":{"type":"object","properties":{"path":{"type":"string","minLength":1},"target":{"type":"string","minLength":1},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"content":{"type":"string"}},"required":["path","target","sha256","content"],"additionalProperties":false}},"tokens":{"type":"array","items":{"type":"string","pattern":"^--z-[a-z0-9-]+$"}},"frameworks":{"minItems":1,"type":"array","items":{"type":"string","enum":["react-router","next","vite","astro-react"]}},"docs":{"type":"string","format":"uri"}},"required":["$schema","name","type","version","title","description","requires","dependencies","registryDependencies","files","tokens","frameworks","docs"],"additionalProperties":false}
@@ -0,0 +1 @@
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"$schema":{"type":"string","format":"uri"},"framework":{"type":"string","enum":["react-router","next","vite","astro-react"]},"componentsDir":{"type":"string","minLength":1},"alias":{"type":"string","minLength":1},"tsx":{"type":"boolean"},"registry":{"type":"string","minLength":1},"installed":{"type":"object","propertyNames":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"},"additionalProperties":{"type":"object","properties":{"version":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"},"files":{"type":"object","propertyNames":{"type":"string","minLength":1},"additionalProperties":{"type":"string","pattern":"^[a-f0-9]{64}$"}}},"required":["version","files"],"additionalProperties":false}}},"required":["$schema","framework","componentsDir","tsx","registry","installed"],"additionalProperties":false}
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@zappie/kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The registry of copy-in Zappie storefront components, installed with the zappie CLI.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/zappie/storefront.git",
9
+ "directory": "packages/kit"
10
+ },
11
+ "devDependencies": {
12
+ "zod": "^4.6.5"
13
+ },
14
+ "license": "MIT",
15
+ "homepage": "https://github.com/zappie/storefront#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/zappie/storefront/issues"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "provenance": false
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "exports": {
28
+ "./registry/v1/*": "./dist/registry/v1/*"
29
+ },
30
+ "scripts": {
31
+ "build": "node scripts/build-registry.ts",
32
+ "e2e": "playwright test --config playwright.config.ts --project=chromium",
33
+ "e2e:webkit": "playwright test --config playwright.config.ts --project=webkit",
34
+ "playground:build": "vite build playground --config playground/vite.config.ts",
35
+ "test": "vitest run",
36
+ "check-types": "tsc --noEmit -p ."
37
+ }
6
38
  }