create-cartbase 0.1.16 → 0.1.17

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 (34) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +376 -376
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +44 -11
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/products.md +51 -2
  16. package/template/app/docs/regions.md +269 -269
  17. package/template/app/docs/reviews.md +223 -223
  18. package/template/app/docs/search.md +227 -227
  19. package/template/app/docs/store.md +47 -47
  20. package/template/app/docs/subscriptions.md +148 -148
  21. package/template/app/docs/variables.md +315 -315
  22. package/template/app/package.json +1 -1
  23. package/template/app/postcss.config.cjs +11 -11
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
  25. package/template/app/src/app/checkout/page.tsx +48 -48
  26. package/template/app/src/app/globals.css +26 -26
  27. package/template/app/src/app/page.tsx +28 -28
  28. package/template/app/src/app/products/[handle]/page.tsx +87 -87
  29. package/template/app/src/app/providers.tsx +64 -64
  30. package/template/app/src/app/search/page.tsx +23 -23
  31. package/template/app/src/lib/browser-client.ts +35 -35
  32. package/template/app/src/lib/config.ts +41 -41
  33. package/template/app/src/lib/server-client.ts +25 -25
  34. package/template/app/src/lib/cart-actions.ts +0 -47
@@ -603,10 +603,21 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
603
603
  no metadata on update; quantity 0 deletes), `deleteLineItem`. Every
604
604
  mutation returns the FULL decorated cart, which becomes the confirmed
605
605
  snapshot — no page refetch needed.
606
+ - **The instant cart (since 0.18.0)** — every change shows at once and
607
+ reaches the platform through ONE queue (`cart-drawer/mutation-queue`,
608
+ `createCartMutationQueue`): one call at a time, each reading the cart the
609
+ answer before it left, so two answers never overwrite each other and a
610
+ second add never creates a second cart. Fast quantity clicks on one line
611
+ send the number the shopper stopped at; the stepper never locks. A line
612
+ added a moment ago carries `pending: true` and a placeholder id
613
+ (`isPendingLine(line)`); its quantity and removal work at once and are
614
+ sent to the real line when the platform has confirmed it.
606
615
  - **Props contract** — `cart?: Cart|null` (server-fetched snapshot; prop
607
- updates win), `client?: StorefrontClient` (enables `addItem`/
608
- `updateQuantity`/`removeItem`/`refresh`), `cartId?` (retrieve-on-mount
609
- when no snapshot), `onCartChange?(cart)` (fires on every confirmed
616
+ updates win unless a change of the provider's own is on its way; leave it
617
+ out and pass `cartId` so the page never waits for the cart),
618
+ `client?: StorefrontClient` (enables `addLine`/`addItem`/
619
+ `updateQuantity`/`removeItem`/`refresh`), `cartId?` (read in the browser
620
+ on mount, through the queue, when no snapshot), `onCartChange?(cart)` (fires on every confirmed
610
621
  change INCLUDING first-add cart creation — persist `cart.id` here),
611
622
  `onOptimisticError?(failure)` (the ops funnel for every failed
612
623
  optimistic mutation — wire to store logging; replaces the source's
@@ -614,10 +625,13 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
614
625
  the language mounted by `StorefrontLocaleProvider` is the default),
615
626
  `hrefs?: {checkout, browse, productPrefix}`.
616
627
  - **Hook** — `useCartDrawer()` → `{isOpen, open, close, toggle, cart,
617
- addItem(variantId, qty?, display?), updateQuantity(lineId, qty),
618
- removeItem(lineId), refresh, applyOptimistic, dispatchOptimistic,
619
- labels, hrefs}`. `applyOptimistic(action, serverAction)` stays for
620
- store-owned server actions (PDP add via RSC).
628
+ addLine(variantId, qty?, display?)` (answers `{ok: true, cart}` or
629
+ `{ok: false, error}`, the platform's refusal with its `code`),
630
+ `addItem(variantId, qty?, display?)` (the same, answering a boolean),
631
+ `updateQuantity(lineId, qty), removeItem(lineId), refresh, canMutate,
632
+ applyOptimistic, dispatchOptimistic, labels, hrefs}`.
633
+ `applyOptimistic(action, serverAction)` stays for store-owned server
634
+ actions; the SDK methods are the fast path.
621
635
  - **Settings** — store default region + enabled currencies (cart create),
622
636
  B2B price lists via attached customer, gift-card product flags
623
637
  (`is_giftcard` lines are non-discountable), automatic promotions,
@@ -664,7 +678,7 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
664
678
  per-item upsell slot.
665
679
  - Subcomponents: `item/quantity` (`<CartItemQuantity lineId quantity
666
680
  maxQuantity? />` — stepper floor 1, calls `updateQuantity` with
667
- `{quantity}`), `item/variant` (`<CartItemVariant variantTitle
681
+ `{quantity}` on every click and never locks), `item/variant` (`<CartItemVariant variantTitle
668
682
  options? />` — Cartbase lines carry the flat `variant_title` string, not
669
683
  an embedded variant object), `item/upsell` (`<CartItemUpsell products
670
684
  onAdd />` — feed from `listRelatedProducts`, `variantId` included so
@@ -799,7 +813,7 @@ components read copy only through the context or explicit `labels` props).
799
813
 
800
814
  ### The product page contract — `products/variant-url`, `products/use-product-actions`
801
815
 
802
- Five rules every buy panel is held to, the package's own and a store's:
816
+ Six rules every buy panel is held to, the package's own and a store's:
803
817
 
804
818
  1. **The address is the state.** The chosen variant is named in the URL as
805
819
  `?variant=<id>`, Shopify's parameter, so a link copied from an email, an
@@ -821,11 +835,17 @@ Five rules every buy panel is held to, the package's own and a store's:
821
835
  the admin shows; never the wire's first variant, which is the database's
822
836
  scan order.
823
837
  5. **The server is asked only for what needs it:** adding to the cart.
838
+ 6. **The add is instant** (since 0.18.0). With no `addToCart` passed, the
839
+ add goes through the mounted `CartDrawerProvider` (`addLine`): the
840
+ drawer opens at the click with the product already in it, the platform
841
+ confirms behind it, and no page render runs. A store that passes its
842
+ own `addToCart` server action keeps the old path (await it, open the
843
+ cart, refresh the page so a server-rendered cart follows).
824
844
 
825
845
  `products/variant-url` is the pure half (`VARIANT_PARAM`,
826
846
  `variantParamValue`, `findVariantByParam`, `defaultVariant`, `variantHref`;
827
847
  unit-tested in tests/unit/storefront-product-page-contract.test.ts).
828
- `useProductActions({product, addToCart, initialVariantId?, onAddToCart?,
848
+ `useProductActions({product, addToCart?, initialVariantId?, onAddToCart?,
829
849
  openCart?, syncAddress?})` is the behaviour as a headless hook: the chosen
830
850
  values, `choose`, the resolved `variant`, `quantity`/`setQuantity`,
831
851
  `inStock` (the server's `in_stock` predicate, never re-derived;
@@ -835,6 +855,19 @@ hook and inherits every rule; `ProductActions` below is the package's own
835
855
  panel on the same hook. Client hook; it reads `useSearchParams`, so mount it
836
856
  under a `<Suspense>` boundary on a static page (a dynamic page needs none).
837
857
 
858
+ ### Pure: `products/sets` (sets from kit links)
859
+
860
+ A set is an inventory kit: its variant draws on the stock items of the
861
+ products inside it. Pass `fields: SET_FIELDS` to `listProducts` or
862
+ `retrieveProduct` and every variant carries `inventory_items`; then
863
+ `setContents(product, variant)` names what a set holds (product, variant,
864
+ title, handle, picture, how many), `setContentsByVariant(products)` keys
865
+ that by variant for a page of products, and `setsContaining(product,
866
+ variant)` names the products holding this variant's own item, the sets it
867
+ is in. `linkOwner(link, productId)` is the rule underneath: an item's owner
868
+ is the other product's variant carrying the item's SKU. Unit-tested
869
+ (tests/unit/storefront-sets.test.ts).
870
+
838
871
  ### Pure: `products/variant-matching` (extra module, Cartbase addition)
839
872
 
840
873
  `optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
@@ -850,7 +883,7 @@ with a legacy flat-row fallback. Unit-tested
850
883
  images `priority`.
851
884
  - **SDK calls** — none (props: `product.images`). Server-safe.
852
885
 
853
- ### `<ProductActions product addToCart initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
886
+ ### `<ProductActions product addToCart? initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
854
887
 
855
888
  - **Purpose** — the package's own buy panel on `useProductActions`: option
856
889
  rows, price, the quantity stepper (`showQuantity`, on by default), the
@@ -1,91 +1,91 @@
1
- # Consent
2
-
3
- The store's CMP configuration for the built-in Consent Mode v2 banner
4
- (consent-management card). Defaults are **always applied server-side**, so
5
- the payload is complete and renderable even for an unconfigured store
6
- (compliant built-in modal, BG + EN copy shipped).
7
-
8
- ## Storefront wiring (the trap that matters)
9
-
10
- - Mount `<ConsentInit required={consent.enabled}>` as the **first child of
11
- `<body>`** — it sets the synchronous Consent Mode v2 DEFAULT and must
12
- **never wait on this fetch in the browser** (async default = first-hit
13
- consent race). Resolve this config server-side (RSC) and inline the
14
- setting into the document; the prop is required, a layout without it
15
- does not compile.
16
- - **The switch decides the default; a stored choice wins.** `enabled: true`
17
- is a store that collects consent: every visitor starts DENIED until they
18
- choose on the banner. `enabled: false` is a store with no consent gate:
19
- every visitor starts GRANTED, and every configured pixel fires. Either
20
- way a choice already in the `_1c_consent` cookie is what counts, so a
21
- visitor who declined keeps that decision if the banner is switched off.
22
- - Render the built-in banner only when `enabled && mode === "builtin"`.
23
- - `mode: "external"` = the merchant's CMP owns the UI and must write the
24
- same `_1c_consent` cookie (or call `setConsent()`) — all Cartbase-side tag
25
- gating works off that one seam.
26
- - Choices persist 12 months in the cookie. Rybbit (platform analytics)
27
- stays outside consent by design.
28
- - Pair with [integrations.md](integrations.md): `tracking.consent_required`
29
- mirrors `enabled` here, and `<StorefrontTags>` hands it to every pixel
30
- that gates its own SDK (Meta, TikTok, ChatGPT). Google reads the
31
- `<ConsentInit>` default.
32
-
33
- ## GET /api/store/consent — the CMP config
34
-
35
- - **Purpose**: everything the banner needs to render, per locale.
36
- - **Auth**: anon (`x-client-id`).
37
- - **Request**: no params.
38
- - **Response** — the EXACT allowlist (nothing else will ever appear here):
39
-
40
- ```jsonc
41
- {
42
- "consent": {
43
- "enabled": true,
44
- "mode": "builtin", // "builtin" | "external"
45
- "layout": "modal", // "modal" (blocking) | "banner-bottom" (non-blocking)
46
- "privacy_href": "/cookies",
47
- "reject_on_first_layer": false,
48
- "copy": {
49
- "bg": {
50
- "title": "Преди да продължиш",
51
- "body": "…",
52
- "privacy_link_label": "…",
53
- "accept_label": "…", "settings_label": "…", "reject_label": "…",
54
- "settings_title": "…",
55
- "accept_all_label": "…", "save_label": "…", "reject_all_label": "…",
56
- "necessary_label": "…", "necessary_description": "…",
57
- "analytics_label": "…", "analytics_description": "…",
58
- "ads_label": "…", "ads_description": "…"
59
- },
60
- "en": { /* same 16 keys — every field always present per locale */ }
61
- }
62
- }
63
- }
64
- ```
65
-
66
- - **Errors**: `400 missing_client_id` only — the payload itself always
67
- succeeds (a corrupt/missing stored config degrades to defaults, never to
68
- a broken banner).
69
- - **SDK**: `consent.getConsent(client)`
70
- - **Components**: `<ConsentInit required={consent.enabled}>` +
71
- `<ConsentBanner copy={copy[locale]} layout privacyHref
72
- rejectOnFirstLayer>` (consent family; reference impl
73
- `src/components/storefront/consent/`).
74
- - **Settings**: admin → Settings → Consent (enabled/mode/layout/
75
- privacy_href/copy per locale).
76
-
77
- ```bash
78
- # Complete, renderable config — even on an unconfigured store the defaults
79
- # make every documented key present.
80
- BODY=$(curl -sf "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
81
- echo "$BODY" | grep -q '"consent"'
82
- echo "$BODY" | grep -q '"mode"'
83
- echo "$BODY" | grep -q '"layout"'
84
- echo "$BODY" | grep -q '"privacy_href"'
85
- echo "$BODY" | grep -q '"reject_on_first_layer"'
86
- echo "$BODY" | grep -q '"analytics_label"'
87
- echo "$BODY" | grep -q '"ads_description"'
88
- # Missing tenant header → the standard 400.
89
- STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/consent")
90
- test "$STATUS" = 400
91
- ```
1
+ # Consent
2
+
3
+ The store's CMP configuration for the built-in Consent Mode v2 banner
4
+ (consent-management card). Defaults are **always applied server-side**, so
5
+ the payload is complete and renderable even for an unconfigured store
6
+ (compliant built-in modal, BG + EN copy shipped).
7
+
8
+ ## Storefront wiring (the trap that matters)
9
+
10
+ - Mount `<ConsentInit required={consent.enabled}>` as the **first child of
11
+ `<body>`** — it sets the synchronous Consent Mode v2 DEFAULT and must
12
+ **never wait on this fetch in the browser** (async default = first-hit
13
+ consent race). Resolve this config server-side (RSC) and inline the
14
+ setting into the document; the prop is required, a layout without it
15
+ does not compile.
16
+ - **The switch decides the default; a stored choice wins.** `enabled: true`
17
+ is a store that collects consent: every visitor starts DENIED until they
18
+ choose on the banner. `enabled: false` is a store with no consent gate:
19
+ every visitor starts GRANTED, and every configured pixel fires. Either
20
+ way a choice already in the `_1c_consent` cookie is what counts, so a
21
+ visitor who declined keeps that decision if the banner is switched off.
22
+ - Render the built-in banner only when `enabled && mode === "builtin"`.
23
+ - `mode: "external"` = the merchant's CMP owns the UI and must write the
24
+ same `_1c_consent` cookie (or call `setConsent()`) — all Cartbase-side tag
25
+ gating works off that one seam.
26
+ - Choices persist 12 months in the cookie. Rybbit (platform analytics)
27
+ stays outside consent by design.
28
+ - Pair with [integrations.md](integrations.md): `tracking.consent_required`
29
+ mirrors `enabled` here, and `<StorefrontTags>` hands it to every pixel
30
+ that gates its own SDK (Meta, TikTok, ChatGPT). Google reads the
31
+ `<ConsentInit>` default.
32
+
33
+ ## GET /api/store/consent — the CMP config
34
+
35
+ - **Purpose**: everything the banner needs to render, per locale.
36
+ - **Auth**: anon (`x-client-id`).
37
+ - **Request**: no params.
38
+ - **Response** — the EXACT allowlist (nothing else will ever appear here):
39
+
40
+ ```jsonc
41
+ {
42
+ "consent": {
43
+ "enabled": true,
44
+ "mode": "builtin", // "builtin" | "external"
45
+ "layout": "modal", // "modal" (blocking) | "banner-bottom" (non-blocking)
46
+ "privacy_href": "/cookies",
47
+ "reject_on_first_layer": false,
48
+ "copy": {
49
+ "bg": {
50
+ "title": "Преди да продължиш",
51
+ "body": "…",
52
+ "privacy_link_label": "…",
53
+ "accept_label": "…", "settings_label": "…", "reject_label": "…",
54
+ "settings_title": "…",
55
+ "accept_all_label": "…", "save_label": "…", "reject_all_label": "…",
56
+ "necessary_label": "…", "necessary_description": "…",
57
+ "analytics_label": "…", "analytics_description": "…",
58
+ "ads_label": "…", "ads_description": "…"
59
+ },
60
+ "en": { /* same 16 keys — every field always present per locale */ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ - **Errors**: `400 missing_client_id` only — the payload itself always
67
+ succeeds (a corrupt/missing stored config degrades to defaults, never to
68
+ a broken banner).
69
+ - **SDK**: `consent.getConsent(client)`
70
+ - **Components**: `<ConsentInit required={consent.enabled}>` +
71
+ `<ConsentBanner copy={copy[locale]} layout privacyHref
72
+ rejectOnFirstLayer>` (consent family; reference impl
73
+ `src/components/storefront/consent/`).
74
+ - **Settings**: admin → Settings → Consent (enabled/mode/layout/
75
+ privacy_href/copy per locale).
76
+
77
+ ```bash
78
+ # Complete, renderable config — even on an unconfigured store the defaults
79
+ # make every documented key present.
80
+ BODY=$(curl -sf "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
81
+ echo "$BODY" | grep -q '"consent"'
82
+ echo "$BODY" | grep -q '"mode"'
83
+ echo "$BODY" | grep -q '"layout"'
84
+ echo "$BODY" | grep -q '"privacy_href"'
85
+ echo "$BODY" | grep -q '"reject_on_first_layer"'
86
+ echo "$BODY" | grep -q '"analytics_label"'
87
+ echo "$BODY" | grep -q '"ads_description"'
88
+ # Missing tenant header → the standard 400.
89
+ STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/consent")
90
+ test "$STATUS" = 400
91
+ ```