@base44/app-plugin-commerce 0.4.0 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
5
5
  "keywords": [
6
6
  "base44",
@@ -112,6 +112,7 @@ Open a file when its work starts — not while planning.
112
112
  | [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
113
113
  | [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond 03's recipe, taxes, day-2 edits | 8K |
114
114
  | [`references/online-payments.md`](./references/online-payments.md) | enabling card payments, or wiring the provider — at install or any time later | 8K |
115
+ | [`references/storefront-styling.md`](./references/storefront-styling.md) | writing the CSS for the cart/checkout parts (selectors, geometry vars) | 5K |
115
116
  | [`references/storefront-custom.md`](./references/storefront-custom.md) | a custom cart row / checkout section on the raw hooks | 8K |
116
117
  | [`references/storefront-verification.md`](./references/storefront-verification.md) | driving the storefront from a browser script | 3K |
117
118
  | [`references/reviews.md`](./references/reviews.md) | review policies (login-gated, verified buyers), moderation | 5K |
@@ -10,7 +10,7 @@ carry_forward:
10
10
  - "Branch custom hook-built UI on `status`, never on `isEmpty`/nullable data; every hook goes above its status guards."
11
11
  - "Variant options that aren't buyable render disabled, never hidden; one control per axis, never a list of variations."
12
12
  - "No copy ships: every word the parts render comes from the store's copy file (labels); a ⟨copy: …⟩ placeholder on screen is unfinished work."
13
- - "Identity is encoded once — design classes in index.css (parts styled via [data-part] selectors) plus one or two signature moments per page."
13
+ - "Identity is encoded once — design classes in index.css (parts styled via [data-part] selectors, controls always among them) plus one or two signature moments per page."
14
14
  ---
15
15
 
16
16
  # 02 — Storefront
@@ -62,7 +62,7 @@ The cost driver of a generated storefront is not wiring — it is decoration rep
62
62
 
63
63
  A part renders one section's correct semantic markup — every branch, guard and aria attribute — and nothing else. Your markup arranges the parts (grid, order, sticky aside, accordion steps — a hidden part loses no state; the place-order gate still counts every blocker). Three inputs carry the identity:
64
64
 
65
- - **Styling.** Every element carries `data-part` + state attributes. Style in `index.css` — `[data-part="option"][data-state="selected"]`, `[data-part="row"][data-pending]` and/or pass `className` (part root) and `classes` (keyed by the **same inner `data-part` names**: `classes={{ row: "bag-row", "line-total": "price" }}`). No part ships a class: browser-default means unfinished.
65
+ - **Styling.** The parts ship **layout geometry only** — field grids, labels above full-width controls, square thumbnails, label-left/value-right totals, the drawer as a right-hand panel — and **no** color, border, radius, shadow or font, so an unstyled store reads as unfinished, never broken. The look is yours: every element carries `data-part` + state attributes (`[data-part="option"][data-state="selected"]`, `[data-part="row"][data-pending]`), each shipped rule sits at **specificity 0** (`:where()`), so a plain selector of yours always wins, and `className`/`classes` take the same names (`classes={{ row: "bag-row", "line-total": "price" }}`). **Style the controls at minimum** — `[data-part="control"]`/`[data-part="input"]` need `border`, `background`, `color: inherit`, `font: inherit`, `padding` and a focus ring, or the store ships browser-default white boxes. Space *between* sections is your containers' `gap`; the parts add no outer margins. Selector inventory, the geometry's custom properties (`--commerce-gap`, `--commerce-media-size`, `--commerce-field-columns`, …) and what else repays styling: [`../references/storefront-styling.md`](../references/storefront-styling.md).
66
66
  - **Words.** The copy file below, passed once on `StorefrontProvider labels`; override per Root or part with the same shape. A missing key renders a visible `⟨copy: path⟩` and warns — never silent, never English.
67
67
  - **Navigation — never rendered, never assumed.** A gate renders *your* element when the state calls for it (function children hand you the data); `OrderReceived.PaymentLink` hands `{ url }` to your element; `Checkout.Root` requires `orderReceivedPath`. Nothing assumes a route, or even an anchor.
68
68
 
@@ -111,20 +111,6 @@ export const STORE_COPY = {
111
111
 
112
112
  Blocker lines are what a disabled place-order button says — one per code, naming the thing the customer can fix (`cart_loading`/`shipping_recalculating` are quiet transients, not errors); `shipping.*`/`payment.*` are the pickers' dead-ends, with the server's own message rendered instead where one exists. The key tree is frozen in code as `REQUIRED_LABEL_KEYS`.
113
113
 
114
- ### `data-part` inventory
115
-
116
- | Part root | Inner `data-part`s | State attributes |
117
- |---|---|---|
118
- | `address-fields`; `ship-to-different` | field · label · required · control · error | `data-which`, `data-key`, `data-span`, `data-invalid` |
119
- | `shipping-methods` `payment-methods` | hint · option · option-input · option-label · option-cost / option-description · chosen | `data-state="selected"`, `data-syncing`, `data-severity` |
120
- | `lines` `items`; `notices` | row · media · name · attributes · quantity · line-total · error; notice; `lines` adds stepper · increase · decrease · remove | `data-pending`, `data-empty`, `data-code` |
121
- | `totals`; `payment-instructions` | row · label · value; description · account | `data-key`, `data-emphasis` |
122
- | `coupon-field` | input · apply · error · applied · code · remove | `data-busy` |
123
- | `place-order` · `order-error` · `blockers` | blocker | `data-state="placing"`, `data-code` |
124
- | `trigger` · `drawer` | overlay · panel; `close` | `data-state="open\|closed"` |
125
-
126
- Tailwind-first styling reaches inner parts with arbitrary variants (`[&_[data-part=option]]:flex …`) or `@apply` inside the design classes.
127
-
128
114
  ## What each hook resolves to
129
115
 
130
116
  Everything below is already unwrapped — no `.data`, no envelope; `formatMoney` is `useFormatMoney()`. This table serves the catalog surfaces and row/option overrides; the custom-section shapes (`useCartLine`, `useCartUI`, `useCheckoutContext`, `useOrderReturn`, `cartTotalsLines`/`orderTotalsLines`) live in [`../references/storefront-custom.md`](../references/storefront-custom.md).
@@ -345,7 +331,7 @@ import { OrderReceived } from "@/commerce/storefront";
345
331
 
346
332
  - [ ] Catalog UI in whatever form fits the store; cart (and/or drawer), checkout and `/order-received` built from the parts — all five receipt gates present, `PaymentInstructions` placed on unpaid.
347
333
  - [ ] One copy file passed via `<StorefrontProvider labels={…}>`, **every value rewritten in the store's voice** — no `⟨copy: …⟩` on any page, no reference-example sentence shipped verbatim; the buy button reads for all four `buy.state` values.
348
- - [ ] `index.css` defines the design classes; parts styled via `[data-part]`/`classes` — nothing left browser-default; pages carry class names, not repeated utility runs.
334
+ - [ ] `index.css` defines the design classes; parts styled via `[data-part]`/`classes` — **controls carry border/background/font/padding + a focus ring**, option rows show their selected state, the drawer panel has a surface, nothing left browser-default; pages carry class names, not repeated utility runs.
349
335
  - [ ] **One** `<StorefrontProvider>` above every storefront route (layout-route pattern); no hand-rolled `cart_token`; imports from each section's import line.
350
336
  - [ ] Custom hook-built sections branch on `status` and never re-implement part/hook logic; gateways/currency/countries from `useStoreInfo()`/`useCountries()` only.
351
337
  - [ ] Coupon field placed if the store has coupons; a paging control whenever `hasNext`; ribbons in both the grid and the product page.
@@ -0,0 +1,99 @@
1
+ ---
2
+ read_when: "You are writing the CSS for the cart, drawer, checkout or receipt — the parts' look."
3
+ ---
4
+
5
+ # Styling the parts
6
+
7
+ The parts ship **layout geometry and nothing else** (`src/commerce/storefront/parts/parts.css`,
8
+ loaded automatically by `@/commerce/storefront`): field grids, labels above
9
+ full-width controls, media as a fixed square, rows as media + content +
10
+ controls with the money pushed right, totals as label-left/value-right, the
11
+ drawer as a right-hand panel over a full-viewport overlay. Nothing in it
12
+ carries color, background, border, radius, shadow, font or text decoration — a
13
+ release check enforces that — so an unstyled store reads as *unfinished*, never
14
+ as broken, and the look is entirely yours.
15
+
16
+ **Every rule is wrapped in `:where()`, so its specificity is 0.** A plain
17
+ `[data-part="row"] { display: grid }` in your stylesheet wins — no `!important`,
18
+ no cascade fights, no need to know what the sheet did.
19
+
20
+ ## Two ways in, one vocabulary
21
+
22
+ ```css
23
+ /* index.css — selectors, next to the store's design classes */
24
+ [data-part="control"] { … }
25
+ [data-part="option"][data-state="selected"] { … }
26
+ [data-part="row"][data-pending] { opacity: .55; }
27
+ ```
28
+
29
+ ```jsx
30
+ {/* or classes, keyed by the same data-part names */}
31
+ <Cart.Lines className="bag" classes={{ row: "bag-row", media: "bag-thumb", "line-total": "price" }} />
32
+ ```
33
+
34
+ Tailwind reaches inner parts with arbitrary variants
35
+ (`className="[&_[data-part=option]]:choice-row"`) or `@apply` inside a design
36
+ class. Either way the names are the ones below.
37
+
38
+ ## The minimum that makes a store look designed
39
+
40
+ ⚑ **Style the controls.** Browser-default inputs are a white box in a system
41
+ font — on a dark or branded storefront that alone reads as unfinished:
42
+
43
+ ```css
44
+ [data-part="control"], [data-part="input"] {
45
+ border: …; background: …; color: inherit; font: inherit; padding: …;
46
+ }
47
+ [data-part="control"]:focus-visible { outline: …; } /* keep a visible focus ring */
48
+ [data-part="field"][data-invalid] [data-part="control"] { border-color: …; }
49
+ ```
50
+
51
+ Then, in the store's own values: the labels (`[data-part="label"]` — size,
52
+ tracking, case), the option rows (`[data-part="option"]` — padding, border,
53
+ and the `[data-state="selected"]` treatment, which is the one control customers
54
+ look for), the totals (`[data-part="value"]`, `[data-emphasis]` for the total
55
+ line), the buttons (`[data-part="place-order"]`, `apply`, `remove`,
56
+ `increase`/`decrease`), the notices (`[data-part="error"]`, `blocker`, `hint` —
57
+ `[data-severity="error"]` marks the loud ones), and the drawer surface
58
+ (`[data-part="panel"]` needs a background of its own, `[data-part="overlay"]` a
59
+ scrim).
60
+
61
+ **The parts add no outer margins** — space *between* sections comes from the
62
+ containers you wrap them in (`gap` on your checkout grid, your aside, your
63
+ drawer panel's column).
64
+
65
+ ## Tuning the built-in geometry
66
+
67
+ Set these anywhere — `:root`, a page, one part — instead of rewriting the rules:
68
+
69
+ | Custom property | Default | Controls |
70
+ |---|---|---|
71
+ | `--commerce-gap` | `1rem` | fields, rows, options, panel sections |
72
+ | `--commerce-gap-tight` | `0.4rem` | label→control, name→attributes, stepper |
73
+ | `--commerce-field-columns` | `2` | address-form columns (set `1` in a narrow aside or a media query) |
74
+ | `--commerce-media-size` | `4rem` | cart/summary thumbnail edge |
75
+ | `--commerce-drawer-width` | `28rem` | drawer panel width |
76
+ | `--commerce-drawer-z` | `50` | drawer stacking order (raise above a sticky header) |
77
+
78
+ ## `data-part` inventory
79
+
80
+ | Part root | Inner `data-part`s | State attributes |
81
+ |---|---|---|
82
+ | `address-fields`; `ship-to-different` | field · label · required · control · error | `data-which`, `data-key`, `data-span`, `data-invalid` |
83
+ | `shipping-methods` `payment-methods` | hint · option · option-input · option-label · option-cost / option-description · chosen | `data-state="selected"`, `data-syncing`, `data-severity` |
84
+ | `lines` `items`; `notices` | row · media · content · name · attributes · controls · stepper · increase · decrease · quantity · remove · line-total · error; notice | `data-pending`, `data-empty`, `data-code` |
85
+ | `totals`; `payment-instructions` | row · label · value; description · account | `data-key`, `data-emphasis` |
86
+ | `coupon-field` | input · apply · error · applied · code · remove | `data-busy` |
87
+ | `place-order` · `order-error` · `blockers` | blocker | `data-state="placing"`, `data-code` |
88
+ | `trigger` · `drawer` | overlay · panel; `close` | `data-state="open\|closed"` |
89
+
90
+ `media` is an `<img>` when the line has an image and an empty `<div
91
+ data-part="media" data-empty>` when it doesn't — same box either way, so style
92
+ the placeholder (`[data-empty]`) rather than letting it render as a hole.
93
+
94
+ ## When CSS isn't enough
95
+
96
+ A control that needs different markup takes the part's render override
97
+ (`inputRender`, `optionRender`, `lineRender`, `itemRender`, `fieldRender`) —
98
+ your element, the part's wiring. A whole section that needs different structure
99
+ drops to its hook: [`./storefront-custom.md`](./storefront-custom.md).
@@ -64,15 +64,19 @@
64
64
  * **Parts** — for the four commodity surfaces only (cart, drawer, checkout,
65
65
  * order-received), `Checkout.*` / `Cart.*` / `CartDrawer.*` /
66
66
  * `OrderReceived.*` render each section's correct semantic markup with zero
67
- * CSS, zero copy and zero navigation: you place them in YOUR layout, style
68
- * them via `data-part`/`data-state` selectors or `className`/`classes`, and
69
- * supply every word through `labels` (chain: part prop page Root →
70
- * `<StorefrontProvider labels={…}>`; a missing key renders a visible
71
- * `⟨copy: …⟩` placeholder). Contract tables, the copy example and the
72
- * `data-part` inventory: the commerce skill's install/02-storefront.md use
73
- * the parts from there, not from these files. They compose the hooks above,
74
- * so dropping one section down to its hook is normal; they are also the
75
- * app's own source, editable when a requirement outgrows their props.
67
+ * copy and zero navigation: you place them in YOUR layout, and supply every
68
+ * word through `labels` (chain: part prop → page Root → `<StorefrontProvider
69
+ * labels={…}>`; a missing key renders a visible `⟨copy: …⟩` placeholder).
70
+ * They carry **layout geometry and nothing else** (`parts/parts.css`: field
71
+ * grids, row and thumbnail sizing, totals rows, the drawer panel no color,
72
+ * type or border, every rule at zero specificity), so an unstyled store looks
73
+ * unfinished but never broken; the look arrives as your CSS on
74
+ * `data-part`/`data-state`, or via `className`/`classes`. Contract tables, the
75
+ * copy example and the `data-part` inventory: the commerce skill's
76
+ * install/02-storefront.md — use the parts from there, not from these files.
77
+ * They compose the hooks above, so dropping one section down to its hook is
78
+ * normal; they are also the app's own source, editable when a requirement
79
+ * outgrows their props.
76
80
  */
77
81
  export {
78
82
  StorefrontProvider,
@@ -95,6 +99,11 @@ export {
95
99
  } from "./address";
96
100
 
97
101
  // ── parts: guided sections for the commodity surfaces ──────────────────────
102
+ // The parts' layout geometry (field grids, row/media sizing, totals rows, the
103
+ // drawer panel) — structure only, no look, every rule at zero specificity so
104
+ // the store's CSS wins. See parts/parts.css for the custom properties.
105
+ import "./parts/parts.css";
106
+
98
107
  export { Checkout } from "./parts/checkout";
99
108
  export { Cart } from "./parts/cart";
100
109
  export { CartDrawer } from "./parts/drawer";
@@ -61,45 +61,49 @@ function Lines({ lineRender, className, classes, labels: partLabels }) {
61
61
  ) : (
62
62
  <div data-part="media" data-empty="" className={classes?.media} />
63
63
  )}
64
- <span data-part="name" className={classes?.name}>{item.name}</span>
65
- {attributesLabel(item.attributes) ? (
66
- <span data-part="attributes" className={classes?.attributes}>
67
- {attributesLabel(item.attributes)}
64
+ <span data-part="content" className={classes?.content}>
65
+ <span data-part="name" className={classes?.name}>{item.name}</span>
66
+ {attributesLabel(item.attributes) ? (
67
+ <span data-part="attributes" className={classes?.attributes}>
68
+ {attributesLabel(item.attributes)}
69
+ </span>
70
+ ) : null}
71
+ </span>
72
+ <span data-part="controls" className={classes?.controls}>
73
+ <span data-part="stepper" className={classes?.stepper}>
74
+ <button
75
+ type="button"
76
+ data-part="decrease"
77
+ className={classes?.decrease}
78
+ onClick={l.decrease}
79
+ disabled={!l.canDecrease || l.pending}
80
+ aria-label={nameLabel(L, "aria.decrease", item.name)}
81
+ >
82
+
83
+ </button>
84
+ <span data-part="quantity" className={classes?.quantity}>{l.quantity}</span>
85
+ <button
86
+ type="button"
87
+ data-part="increase"
88
+ className={classes?.increase}
89
+ onClick={l.increase}
90
+ disabled={!l.canIncrease || l.pending}
91
+ aria-label={nameLabel(L, "aria.increase", item.name)}
92
+ >
93
+ +
94
+ </button>
68
95
  </span>
69
- ) : null}
70
- <span data-part="stepper" className={classes?.stepper}>
71
- <button
72
- type="button"
73
- data-part="decrease"
74
- className={classes?.decrease}
75
- onClick={l.decrease}
76
- disabled={!l.canDecrease || l.pending}
77
- aria-label={nameLabel(L, "aria.decrease", item.name)}
78
- >
79
-
80
- </button>
81
- <span data-part="quantity" className={classes?.quantity}>{l.quantity}</span>
82
96
  <button
83
97
  type="button"
84
- data-part="increase"
85
- className={classes?.increase}
86
- onClick={l.increase}
87
- disabled={!l.canIncrease || l.pending}
88
- aria-label={nameLabel(L, "aria.increase", item.name)}
98
+ data-part="remove"
99
+ className={classes?.remove}
100
+ onClick={l.remove}
101
+ disabled={l.pending}
102
+ aria-label={nameLabel(L, "aria.remove", item.name)}
89
103
  >
90
- +
104
+ ×
91
105
  </button>
92
106
  </span>
93
- <button
94
- type="button"
95
- data-part="remove"
96
- className={classes?.remove}
97
- onClick={l.remove}
98
- disabled={l.pending}
99
- aria-label={nameLabel(L, "aria.remove", item.name)}
100
- >
101
- ×
102
- </button>
103
107
  <span data-part="line-total" className={classes?.["line-total"]}>
104
108
  {formatMoney(item.total)}
105
109
  </span>
@@ -1,4 +1,13 @@
1
- import React, { createContext, useCallback, useContext, useId, useMemo, useState } from "react";
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useId,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
2
11
  import { addressFieldSpec, attributesLabel } from "@/commerce/utils";
3
12
  import { CheckoutProvider, useCheckoutContext } from "../useCheckout";
4
13
  import { ShippingMethodPicker, PaymentMethodPicker } from "../pickers";
@@ -19,6 +28,9 @@ import { CouponField, Totals } from "./shared";
19
28
 
20
29
  const CheckoutPartsContext = createContext(null);
21
30
 
31
+ /** Layout effect where there is a DOM; plain effect on the server (no warning). */
32
+ const useCommitEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
33
+
22
34
  /**
23
35
  * Mounts `CheckoutProvider`, resolves the page's phase once (`submitted` >
24
36
  * `loading` > `empty` > `form` — the ordering that kills the empty-bag flash
@@ -53,7 +65,16 @@ function RootPhase({ onPlaced, children }) {
53
65
  : status === "empty"
54
66
  ? "empty"
55
67
  : "form";
56
- const value = useMemo(() => ({ phase, onPlaced }), [phase, onPlaced]);
68
+ // A page that places <Checkout.Blockers> itself must not ALSO get the copy
69
+ // `PlaceOrder` renders by default — the same three reasons printed twice is a
70
+ // real defect an eval caught. Standalone instances register here (before
71
+ // paint), and the button's own copy stands down while one exists.
72
+ const [ownBlockers, setOwnBlockers] = useState(0);
73
+ const registerBlockers = useCallback((delta) => setOwnBlockers((n) => n + delta), []);
74
+ const value = useMemo(
75
+ () => ({ phase, onPlaced, hasOwnBlockers: ownBlockers > 0, registerBlockers }),
76
+ [phase, onPlaced, ownBlockers, registerBlockers],
77
+ );
57
78
  return <CheckoutPartsContext.Provider value={value}>{children}</CheckoutPartsContext.Provider>;
58
79
  }
59
80
 
@@ -378,13 +399,15 @@ function Items({ itemRender, className, classes }) {
378
399
  ) : (
379
400
  <div data-part="media" data-empty="" className={classes?.media} />
380
401
  )}
381
- <span data-part="name" className={classes?.name}>{item.name}</span>
382
- {attributesLabel(item.attributes) ? (
383
- <span data-part="attributes" className={classes?.attributes}>
384
- {attributesLabel(item.attributes)}
385
- </span>
386
- ) : null}
387
- <span data-part="quantity" className={classes?.quantity}>{item.quantity}</span>
402
+ <span data-part="content" className={classes?.content}>
403
+ <span data-part="name" className={classes?.name}>{item.name}</span>
404
+ {attributesLabel(item.attributes) ? (
405
+ <span data-part="attributes" className={classes?.attributes}>
406
+ {attributesLabel(item.attributes)}
407
+ </span>
408
+ ) : null}
409
+ </span>
410
+ <span data-part="quantity" className={classes?.quantity}>×{item.quantity}</span>
388
411
  <span data-part="line-total" className={classes?.["line-total"]}>
389
412
  {formatMoney(item.total)}
390
413
  </span>
@@ -399,12 +422,20 @@ function Items({ itemRender, className, classes }) {
399
422
  /**
400
423
  * The blocker lines — what still stands between the customer and the order,
401
424
  * one line per code in the store's words, gone when the order can be placed.
402
- * `PlaceOrder` renders these itself; use this part only to place them
403
- * elsewhere (then pass `showBlockers={false}` to the button).
425
+ * `PlaceOrder` renders these next to the button by default; placing this part
426
+ * anywhere under `Checkout.Root` moves them there instead — the button's own
427
+ * copy stands down automatically, so the reasons are never printed twice.
404
428
  */
405
- function Blockers({ className, classes, labels: partLabels }) {
429
+ function Blockers({ standalone = true, className, classes, labels: partLabels }) {
406
430
  const checkout = useCheckoutContext();
431
+ const { registerBlockers } = usePartsContext("Blockers");
407
432
  const L = useResolvedLabels(partLabels);
433
+ // Registered before paint, so the button's built-in copy never flashes.
434
+ useCommitEffect(() => {
435
+ if (!standalone) return undefined;
436
+ registerBlockers(1);
437
+ return () => registerBlockers(-1);
438
+ }, [standalone, registerBlockers]);
408
439
  if (checkout.canPlaceOrder) return null;
409
440
  return (
410
441
  <div data-part="blockers" className={className}>
@@ -425,7 +456,7 @@ function Blockers({ className, classes, labels: partLabels }) {
425
456
  */
426
457
  function PlaceOrder({ showBlockers = true, className, classes, labels: partLabels }) {
427
458
  const checkout = useCheckoutContext();
428
- const { onPlaced } = usePartsContext("PlaceOrder");
459
+ const { onPlaced, hasOwnBlockers } = usePartsContext("PlaceOrder");
429
460
  const L = useResolvedLabels(partLabels);
430
461
  const place = useCallback(async () => {
431
462
  const res = await checkout.placeOrder(); // resolves {ok:false} — never throws
@@ -448,7 +479,9 @@ function PlaceOrder({ showBlockers = true, className, classes, labels: partLabel
448
479
  {checkout.orderError.message}
449
480
  </p>
450
481
  )}
451
- {showBlockers && <Blockers className={classes?.blockers} labels={partLabels} />}
482
+ {showBlockers && !hasOwnBlockers && (
483
+ <Blockers standalone={false} className={classes?.blockers} labels={partLabels} />
484
+ )}
452
485
  </>
453
486
  );
454
487
  }
@@ -80,11 +80,13 @@ function Items({ itemRender, className, classes }) {
80
80
  ) : (
81
81
  <div data-part="media" data-empty="" className={classes?.media} />
82
82
  )}
83
- <span data-part="name" className={classes?.name}>{line.name}</span>
84
- {line.attributesLabel ? (
85
- <span data-part="attributes" className={classes?.attributes}>{line.attributesLabel}</span>
86
- ) : null}
87
- <span data-part="quantity" className={classes?.quantity}>{line.quantity}</span>
83
+ <span data-part="content" className={classes?.content}>
84
+ <span data-part="name" className={classes?.name}>{line.name}</span>
85
+ {line.attributesLabel ? (
86
+ <span data-part="attributes" className={classes?.attributes}>{line.attributesLabel}</span>
87
+ ) : null}
88
+ </span>
89
+ <span data-part="quantity" className={classes?.quantity}>×{line.quantity}</span>
88
90
  <span data-part="line-total" className={classes?.["line-total"]}>{line.totalLabel}</span>
89
91
  </>
90
92
  )}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Layout geometry for the commerce parts — **structure only, never a look.**
3
+ *
4
+ * The parts render correct markup; this sheet makes that markup *lay out*
5
+ * correctly: labels above full-width controls in a two-column address grid, a
6
+ * cart row as media + content + controls, totals as label-left/value-right,
7
+ * a drawer that is actually a right-hand panel over an overlay. Without it
8
+ * every store re-derives the same geometry from scratch, and a storefront that
9
+ * gets it wrong reads as broken (staggered input widths, a product thumbnail
10
+ * blown up to the column width, "SetColor: Magenta1$89.00" with no gaps).
11
+ *
12
+ * What this sheet deliberately does NOT contain — the store's identity, and
13
+ * the reason an unstyled storefront still looks unfinished rather than
14
+ * finished-and-generic: no color, no background, no border (beyond zeroing the
15
+ * ones the browser puts on elements the parts chose, like `fieldset`), no
16
+ * radius, no shadow, no font, no text decoration. A release check enforces
17
+ * that list, so this file cannot drift into a theme.
18
+ *
19
+ * **Overriding is free.** Every rule is wrapped in `:where()`, so its
20
+ * specificity is 0 — any selector the store writes wins without `!important`,
21
+ * including a bare `[data-part="row"] { … }`. Tune the built-in geometry
22
+ * through the custom properties below (set them on `:root`, a page, or one
23
+ * part), or replace a rule outright.
24
+ *
25
+ * :root {
26
+ * --commerce-gap: 1rem; gap between fields, rows, options
27
+ * --commerce-gap-tight: 0.4rem; label→control, name→attributes
28
+ * --commerce-field-columns: 2; address-form columns (1 on narrow)
29
+ * --commerce-media-size: 4rem; cart/summary thumbnail edge
30
+ * --commerce-drawer-width: 28rem; drawer panel width
31
+ * --commerce-drawer-z: 50; drawer stacking context
32
+ * }
33
+ *
34
+ * Loaded automatically: `@/commerce/storefront` imports this file. With a
35
+ * bundler that does not take CSS imports from JS, delete that import line and
36
+ * `@import "@/commerce/storefront/parts/parts.css";` from the app's stylesheet
37
+ * instead.
38
+ */
39
+
40
+ :where([data-part]) {
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ /* ── address form ─────────────────────────────────────────────────────────── */
45
+ :where([data-part="address-fields"]) {
46
+ display: grid;
47
+ grid-template-columns: repeat(var(--commerce-field-columns, 2), minmax(0, 1fr));
48
+ gap: var(--commerce-gap, 1rem);
49
+ }
50
+ :where([data-part="field"]) {
51
+ display: grid;
52
+ align-content: start;
53
+ gap: var(--commerce-gap-tight, 0.4rem);
54
+ min-inline-size: 0;
55
+ }
56
+ :where([data-part="field"][data-span="2"]) {
57
+ grid-column: 1 / -1;
58
+ }
59
+ :where([data-part="label"]) {
60
+ display: block;
61
+ }
62
+ :where([data-part="control"]) {
63
+ inline-size: 100%;
64
+ min-inline-size: 0;
65
+ }
66
+ :where([data-part="ship-to-different"]) {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--commerce-gap-tight, 0.4rem);
70
+ }
71
+ :where([data-part="ship-to-different"] [data-part="control"]) {
72
+ inline-size: auto;
73
+ flex: none;
74
+ }
75
+
76
+ /* ── shipping / payment choices ───────────────────────────────────────────── */
77
+ :where([data-part="shipping-methods"], [data-part="payment-methods"]) {
78
+ display: grid;
79
+ gap: var(--commerce-gap-tight, 0.4rem);
80
+ /* the parts chose <fieldset>; neutralize what the browser draws on it */
81
+ margin: 0;
82
+ padding: 0;
83
+ border: 0;
84
+ min-inline-size: 0;
85
+ }
86
+ :where([data-part="option"], [data-part="chosen"]) {
87
+ display: flex;
88
+ flex-wrap: wrap;
89
+ align-items: center;
90
+ gap: var(--commerce-gap-tight, 0.4rem);
91
+ margin: 0;
92
+ }
93
+ :where([data-part="option-input"]) {
94
+ flex: none;
95
+ }
96
+ :where([data-part="option-cost"]) {
97
+ margin-inline-start: auto;
98
+ }
99
+ :where([data-part="option-description"]) {
100
+ flex-basis: 100%;
101
+ }
102
+
103
+ /* ── line rows: cart lines, checkout summary, receipt items ───────────────── */
104
+ :where([data-part="lines"], [data-part="items"], [data-part="notices"]) {
105
+ display: grid;
106
+ gap: var(--commerce-gap, 1rem);
107
+ margin: 0;
108
+ padding: 0;
109
+ list-style: none;
110
+ }
111
+ :where([data-part="row"]) {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ align-items: center;
115
+ gap: var(--commerce-gap-tight, 0.4rem);
116
+ min-inline-size: 0;
117
+ }
118
+ :where([data-part="media"]) {
119
+ flex: none;
120
+ inline-size: var(--commerce-media-size, 4rem);
121
+ max-inline-size: 100%;
122
+ aspect-ratio: 1;
123
+ object-fit: cover;
124
+ }
125
+ :where(img[data-part="media"]) {
126
+ display: block;
127
+ block-size: auto;
128
+ }
129
+ :where([data-part="content"]) {
130
+ display: grid;
131
+ align-content: center;
132
+ gap: calc(var(--commerce-gap-tight, 0.4rem) / 2);
133
+ flex: 1 1 8rem;
134
+ min-inline-size: 0;
135
+ }
136
+ :where([data-part="controls"]) {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: var(--commerce-gap-tight, 0.4rem);
140
+ flex: none;
141
+ }
142
+ :where([data-part="stepper"]) {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: var(--commerce-gap-tight, 0.4rem);
146
+ }
147
+ :where([data-part="line-total"]) {
148
+ margin-inline-start: auto;
149
+ }
150
+ :where([data-part="row"] [data-part="error"]) {
151
+ flex-basis: 100%;
152
+ margin: 0;
153
+ }
154
+
155
+ /* ── totals, payment instructions (label ↔ value pairs) ───────────────────── */
156
+ :where([data-part="totals"], [data-part="account"]) {
157
+ display: grid;
158
+ gap: var(--commerce-gap-tight, 0.4rem);
159
+ margin: 0;
160
+ }
161
+ :where([data-part="totals"] > *, [data-part="account"] > *) {
162
+ display: flex;
163
+ align-items: baseline;
164
+ gap: var(--commerce-gap-tight, 0.4rem);
165
+ min-inline-size: 0;
166
+ }
167
+ :where([data-part="value"]) {
168
+ margin: 0;
169
+ margin-inline-start: auto;
170
+ }
171
+
172
+ /* ── coupon field ─────────────────────────────────────────────────────────── */
173
+ :where([data-part="coupon-field"]) {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ align-items: center;
177
+ gap: var(--commerce-gap-tight, 0.4rem);
178
+ }
179
+ :where([data-part="coupon-field"] [data-part="input"]) {
180
+ flex: 1 1 10rem;
181
+ min-inline-size: 0;
182
+ }
183
+ :where([data-part="coupon-field"] [data-part="error"], [data-part="applied"]) {
184
+ flex-basis: 100%;
185
+ margin: 0;
186
+ }
187
+ :where([data-part="applied"]) {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: var(--commerce-gap-tight, 0.4rem);
191
+ }
192
+
193
+ /* ── place order + its reasons ─────────────────────────────────────────────── */
194
+ :where([data-part="blockers"]) {
195
+ display: grid;
196
+ gap: calc(var(--commerce-gap-tight, 0.4rem) / 2);
197
+ }
198
+ :where([data-part="blocker"], [data-part="order-error"], [data-part="hint"]) {
199
+ margin: 0;
200
+ }
201
+
202
+ /* ── payment instructions ─────────────────────────────────────────────────── */
203
+ :where([data-part="payment-instructions"]) {
204
+ display: grid;
205
+ gap: var(--commerce-gap, 1rem);
206
+ }
207
+ :where([data-part="description"]) {
208
+ margin: 0;
209
+ }
210
+
211
+ /* ── cart drawer: a panel over an overlay, not a block in the header ───────── */
212
+ :where([data-part="drawer"]) {
213
+ position: fixed;
214
+ inset: 0;
215
+ z-index: var(--commerce-drawer-z, 50);
216
+ display: flex;
217
+ justify-content: flex-end;
218
+ }
219
+ :where([data-part="overlay"]) {
220
+ position: absolute;
221
+ inset: 0;
222
+ }
223
+ :where([data-part="panel"]) {
224
+ position: relative;
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: var(--commerce-gap, 1rem);
228
+ inline-size: min(var(--commerce-drawer-width, 28rem), 100%);
229
+ max-inline-size: 100%;
230
+ block-size: 100%;
231
+ overflow-y: auto;
232
+ }