create-magic-storefront 0.5.0 → 0.6.0

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 (139) hide show
  1. package/package.json +3 -3
  2. package/template/.claude/skills/storefront-design/SKILL.md +14 -10
  3. package/template/AGENTS.md +32 -16
  4. package/template/DESIGN.md +117 -0
  5. package/template/PAGES.md +107 -29
  6. package/template/README.md +15 -3
  7. package/template/app/account/addresses/page.tsx +16 -0
  8. package/template/app/account/layout.tsx +14 -0
  9. package/template/app/account/loading.tsx +15 -0
  10. package/template/app/account/orders/[id]/page.tsx +15 -0
  11. package/template/app/account/page.tsx +12 -244
  12. package/template/app/account/profile/page.tsx +16 -0
  13. package/template/app/blog/(index)/loading.tsx +22 -0
  14. package/template/app/blog/{page.tsx → (index)/page.tsx} +42 -25
  15. package/template/app/blog/[handle]/page.tsx +92 -56
  16. package/template/app/blog/author/[handle]/page.tsx +1 -1
  17. package/template/app/cart/layout.tsx +14 -0
  18. package/template/app/cart/page.tsx +49 -101
  19. package/template/app/checkout/layout.tsx +14 -0
  20. package/template/app/checkout/page.tsx +98 -281
  21. package/template/app/collections/[handle]/layout.tsx +20 -0
  22. package/template/app/collections/[handle]/loading.tsx +23 -0
  23. package/template/app/collections/[handle]/page.tsx +131 -15
  24. package/template/app/collections/page.tsx +49 -0
  25. package/template/app/contacts/page.tsx +196 -0
  26. package/template/app/error.tsx +19 -8
  27. package/template/app/fonts/OFL.txt +93 -0
  28. package/template/app/fonts/index.ts +14 -0
  29. package/template/app/fonts/onest-variable.woff2 +0 -0
  30. package/template/app/globals.css +14 -518
  31. package/template/app/layout.tsx +73 -50
  32. package/template/app/not-found.tsx +34 -4
  33. package/template/app/page.tsx +1 -1
  34. package/template/app/pages/[handle]/page.tsx +1 -1
  35. package/template/app/products/[handle]/layout.tsx +20 -0
  36. package/template/app/products/[handle]/loading.tsx +25 -0
  37. package/template/app/products/[handle]/page.tsx +112 -15
  38. package/template/app/providers.tsx +5 -1
  39. package/template/app/search/loading.tsx +12 -0
  40. package/template/app/search/page.tsx +138 -25
  41. package/template/app/sitemap.ts +5 -1
  42. package/template/app/styles/base.css +138 -0
  43. package/template/app/styles/components.css +1108 -0
  44. package/template/app/styles/layout.css +455 -0
  45. package/template/app/styles/pages.css +1839 -0
  46. package/template/app/styles/reset.css +55 -0
  47. package/template/app/styles/sections.css +415 -0
  48. package/template/app/styles/tokens.css +167 -0
  49. package/template/app/styles/utilities.css +37 -0
  50. package/template/app/wishlist/page.tsx +26 -0
  51. package/template/components/account/account-shell.tsx +100 -0
  52. package/template/components/account/addresses.tsx +247 -0
  53. package/template/components/account/order-detail.tsx +281 -0
  54. package/template/components/account/order-list.tsx +94 -0
  55. package/template/components/account/order-status.tsx +19 -0
  56. package/template/components/account/profile.tsx +129 -0
  57. package/template/components/account/sign-in.tsx +164 -0
  58. package/template/components/article-body.tsx +3 -2
  59. package/template/components/article-card.tsx +37 -12
  60. package/template/components/cart/cart-drawer.tsx +149 -0
  61. package/template/components/cart/cart-lines.tsx +89 -0
  62. package/template/components/cart/cart-summary.tsx +191 -0
  63. package/template/components/catalog/catalog-form.tsx +107 -0
  64. package/template/components/catalog/catalog-layout.tsx +82 -0
  65. package/template/components/catalog/facets-shell.tsx +60 -0
  66. package/template/components/catalog/facets.tsx +216 -0
  67. package/template/components/checkout/order-summary.tsx +84 -0
  68. package/template/components/checkout/radio-card.tsx +34 -0
  69. package/template/components/checkout/step-indicator.tsx +56 -0
  70. package/template/components/checkout/steps.tsx +264 -0
  71. package/template/components/checkout/thank-you.tsx +48 -0
  72. package/template/components/checkout/use-checkout.ts +90 -0
  73. package/template/components/footer/site-footer.tsx +132 -0
  74. package/template/components/header/cart-button.tsx +43 -0
  75. package/template/components/header/desktop-nav.tsx +133 -0
  76. package/template/components/header/mobile-menu.tsx +102 -0
  77. package/template/components/header/site-header.tsx +78 -0
  78. package/template/components/page-loading.tsx +20 -0
  79. package/template/components/pager.tsx +3 -1
  80. package/template/components/product/buy-box.tsx +148 -0
  81. package/template/components/product/gallery.tsx +179 -0
  82. package/template/components/product/pre-order-form.tsx +93 -0
  83. package/template/components/product/product-selection.tsx +31 -0
  84. package/template/components/product/recommendations.tsx +26 -0
  85. package/template/components/product/review-list.tsx +159 -0
  86. package/template/components/product/reviews.tsx +80 -0
  87. package/template/components/product/variant-picker.tsx +75 -0
  88. package/template/components/product-card.tsx +103 -0
  89. package/template/components/product-grid.tsx +27 -57
  90. package/template/components/quick-add.tsx +62 -0
  91. package/template/components/search/predictive-search.tsx +282 -0
  92. package/template/components/sections/announcement-bar.tsx +1 -1
  93. package/template/components/sections/banner.tsx +1 -1
  94. package/template/components/sections/collections.tsx +50 -13
  95. package/template/components/sections/deal-of-day.tsx +2 -2
  96. package/template/components/sections/product-shelves.tsx +13 -5
  97. package/template/components/sections/shoppable-stories.tsx +2 -2
  98. package/template/components/sections/store-reviews.tsx +10 -4
  99. package/template/components/share-button.tsx +42 -0
  100. package/template/components/ui/accordion.tsx +35 -0
  101. package/template/components/ui/badge.tsx +20 -0
  102. package/template/components/ui/breadcrumbs.tsx +31 -0
  103. package/template/components/ui/button.tsx +90 -0
  104. package/template/components/ui/drawer.tsx +84 -0
  105. package/template/components/ui/empty-state.tsx +27 -0
  106. package/template/components/ui/field.tsx +33 -0
  107. package/template/components/ui/icon.tsx +112 -0
  108. package/template/components/ui/price.tsx +35 -0
  109. package/template/components/ui/quantity-stepper.tsx +91 -0
  110. package/template/components/ui/rating.tsx +48 -0
  111. package/template/components/ui/skeleton.tsx +46 -0
  112. package/template/components/wishlist/wishlist-view.tsx +134 -0
  113. package/template/lib/article-outline.ts +35 -0
  114. package/template/lib/catalog-params.ts +170 -0
  115. package/template/lib/contacts.ts +34 -0
  116. package/template/lib/errors.ts +29 -0
  117. package/template/lib/i18n.ts +484 -0
  118. package/template/lib/menu.ts +70 -0
  119. package/template/lib/theme-settings.ts +36 -0
  120. package/template/theme/index.ts +7 -1
  121. package/template/theme/sections/collapsible-content.tsx +75 -0
  122. package/template/theme/sections/collection-list.tsx +32 -4
  123. package/template/theme/sections/featured-collection.tsx +22 -2
  124. package/template/theme/sections/hero.tsx +94 -7
  125. package/template/theme/sections/image-with-text.tsx +19 -3
  126. package/template/theme/sections/multicolumn.tsx +113 -0
  127. package/template/theme/sections/page-content.tsx +10 -4
  128. package/template/theme/sections/product-shelf.tsx +26 -4
  129. package/template/theme/sections/rich-text.tsx +13 -1
  130. package/template/theme/sections/testimonials.tsx +14 -9
  131. package/template/theme/settings.ts +85 -1
  132. package/template/theme/templates/index.json +4 -2
  133. package/template/.screenshots/home-1280.png +0 -0
  134. package/template/.screenshots/home-390.png +0 -0
  135. package/template/.screenshots/home-filled-1280.png +0 -0
  136. package/template/.screenshots/home-filled-390.png +0 -0
  137. package/template/.screenshots/page-1280.png +0 -0
  138. package/template/components/buy-box.tsx +0 -89
  139. package/template/components/cart-link.tsx +0 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-magic-storefront",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Scaffold a Next.js storefront on the MagicStore Storefront API v2",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,8 +18,8 @@
18
18
  "esbuild": "^0.27.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@magicstoreai/hydrogen": "0.5.0",
22
- "@magicstoreai/storefront-client": "0.5.0",
21
+ "@magicstoreai/hydrogen": "0.6.0",
22
+ "@magicstoreai/storefront-client": "0.6.0",
23
23
  "@types/node": "^26.6.2",
24
24
  "react": "^19.3.0",
25
25
  "tsup": "^8.5.1",
@@ -61,10 +61,11 @@ The merchant changes colors in the admin, and `SHOP_UPDATED` revalidates the sho
61
61
 
62
62
  - **Never hard-code the merchant's colors** into CSS. The starter does this already:
63
63
  `app/layout.tsx` puts `brandStyle(shop.branding)` (`lib/brand.ts`) on `<html style>`, and
64
- `app/globals.css` declares every token with the design's own fallback, used when a color is `null`.
65
- Keep that wiring; restyle through the tokens.
64
+ `app/styles/tokens.css` declares every token with the design's own fallback, used when a color is
65
+ `null`. Keep that wiring; restyle through the tokens.
66
66
  - Tokens: `--accent` / `--on-accent` (`colors.main` / `buttonText`), `--nav-bg` / `--nav-fg`
67
- (header), `--label-bg` / `--label-fg` (badges: "New", "Sale", "Pre-order").
67
+ (header), `--label-bg` / `--label-fg` (badges: "New", "Sale", "Pre-order"); the accent as text on
68
+ the page is `--accent-text`, never `--accent` (a pale accent fails on white).
68
69
  - One accent, used the same way on every page (the taste skill's Color Consistency Lock).
69
70
  - A text color the merchant didn't set is picked by contrast (`readableTextOn`), never assumed
70
71
  white. Only hex colors are accepted: anything else is dropped, never written into the style.
@@ -72,8 +73,10 @@ The merchant changes colors in the admin, and `SHOP_UPDATED` revalidates the sho
72
73
  rather than overriding it.
73
74
  - Merchant colors are unknown at design time, so check contrast with a light, a dark and a
74
75
  saturated accent, not only the one shop you are looking at.
75
- - Everything else (neutrals, radius, spacing, type scale, shadows) belongs to the design and lives
76
- in `globals.css` as tokens.
76
+ - Everything else (neutrals, radius, spacing, type scale, shadows) belongs to the design. The
77
+ token table, theme settings, CSS layers and the UI primitives in `components/ui/` are in
78
+ `DESIGN.md`: read it and build from them instead of restating values or writing one-off
79
+ components.
77
80
 
78
81
  ## 3. Page patterns
79
82
 
@@ -94,7 +97,7 @@ The whole card is one link; a quick-add button, if any, is a separate control.
94
97
  below the fold. Grid: 2 columns on mobile, 3 to 5 on desktop depending on density. Sorting and
95
98
  filters sit above the grid; pagination via `<Pagination>`.
96
99
 
97
- **Product page** (`app/products/[handle]`, `components/buy-box.tsx`). Gallery left, buy box right
100
+ **Product page** (`app/products/[handle]`, `components/product/`). Gallery left, buy box right
98
101
  on desktop; on mobile gallery, then title, price, options, add to cart. Price and add to cart are
99
102
  visible without scrolling on a 390×844 screen. Options: sold-out values stay visible but disabled
100
103
  (`isOptionValueAvailable`), never hidden. Stock text comes from `quantityAvailable`; do not add
@@ -128,10 +131,11 @@ tap targets at least 44px, no hover-only controls, no information that only appe
128
131
 
129
132
  ## 5. Stack
130
133
 
131
- Keep the starter's approach: plain CSS with custom properties in `app/globals.css`, no UI
132
- framework. Add Tailwind v4 or Motion only when the owner of this storefront asks for it. If you add
133
- Motion, use it only in `'use client'` leaf components on the home page and content pages. Fonts via
134
- `next/font`. Before importing any package, check `package.json` (taste skill Section 3.F).
134
+ Keep the starter's approach: plain CSS with custom properties, one file per layer in
135
+ `app/styles/` (`DESIGN.md`), no UI framework. Add Tailwind v4 or Motion only when the owner of this
136
+ storefront asks for it. If you add Motion, use it only in `'use client'` leaf components on the home
137
+ page and content pages. Fonts via `next/font`. Before importing any package, check `package.json`
138
+ (taste skill Section 3.F).
135
139
 
136
140
  ## Overrides
137
141
 
@@ -51,6 +51,13 @@ from a landing page: brand colors come from `shop.branding` at runtime, product
51
51
  come only from the API, nothing is invented (prices, badges, reviews, urgency), and the cart and
52
52
  checkout stay conventional.
53
53
 
54
+ `DESIGN.md` is the design system: tokens (`app/styles/tokens.css`), type, the global theme
55
+ settings (`theme/settings.ts` → `data-*` on `<html>`: corner radius, image ratio, card style,
56
+ sticky header, vendor, rating, cart type, announcement), the CSS layers in `app/styles/`, and the
57
+ contrast and layout-shift rules. Build pages from the primitives in `components/ui/` (`Button`,
58
+ `Drawer`, `Accordion`, `QuantityStepper`, `Badge`, `Skeleton`, `Breadcrumbs`, `EmptyState`,
59
+ `Field`, `Price`, `Rating`, `Icon`) — never one-off markup with its own styles.
60
+
54
61
  Before calling visual work done, follow `.claude/skills/storefront-verify/SKILL.md`: build, run,
55
62
  screenshot every page type at 390 and 1280px, fix what you see. Interactive UI follows
56
63
  `.claude/skills/web-interface-guidelines/SKILL.md`; React and Next.js code follows
@@ -58,22 +65,31 @@ screenshot every page type at 390 and 1280px, fix what you see. Interactive UI f
58
65
 
59
66
  ## Where things go
60
67
 
61
- | Path | What |
62
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
63
- | `app/` | Pages (server components) and route handlers |
64
- | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
65
- | `app/blog/` | Blog: index, category, author, tag and article pages (`PAGES.md`, Blog) |
66
- | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
67
- | `app/page.tsx` | Home: the theme's `index` template (`loadTemplate` + `ThemeSections`, `PAGES.md`) |
68
- | `theme/` | The theme: `sections/` (schema + component), `templates/*.json`, `settings.ts`, registered in `index.ts` |
69
- | `components/sections/` | One renderer per `GET /home` section type (the `platform-home` theme section); an unknown type renders nothing |
70
- | `components/` | Shared UI; client components start with `'use client'` |
71
- | `components/telegram-shell.tsx` | Telegram Mini App: theme marks, back arrow, the one automatic Telegram sign-in (`PAGES.md`) |
72
- | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
73
- | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
74
- | `app/storefront-api/[...path]` | Same-origin proxy (`createStorefrontProxy`); holds tokens and cart / checkout ids in httpOnly cookies |
75
- | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
76
- | `app/api/magicstore/preview` | Theme editor preview: signed token → draft mode; pages pass `themePreview()` (`lib/preview.ts`) as `preview` |
68
+ | Path | What |
69
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
70
+ | `app/` | Pages (server components) and route handlers |
71
+ | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
72
+ | `app/not-found.tsx` | 404 with search; a route with a `loading.tsx` checks its resource in `layout.tsx` (a real 404 status) |
73
+ | `app/collections/` | All collections; a collection with facets and sort from the URL (`lib/catalog-params.ts`, `components/catalog/`) |
74
+ | `app/products/[handle]` | Product: `components/product/` (gallery, variant picker, buy box, reviews, recommendations) |
75
+ | `app/cart`, `app/checkout` | Cart page (`components/cart/`), checkout steps (`components/checkout/`); titles in their `layout.tsx` |
76
+ | `app/account/` | Overview, order, addresses, profile (`components/account/`) |
77
+ | `app/wishlist`, `app/contacts` | Wishlist (`features.wishlist`), contacts from `api.contacts()` (`lib/contacts.ts`) |
78
+ | `app/blog/` | Blog: index, category, author, tag and article pages (`PAGES.md`, Blog) |
79
+ | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
80
+ | `app/page.tsx` | Home: the theme's `index` template (`loadTemplate` + `ThemeSections`, `PAGES.md`) |
81
+ | `theme/` | The theme: `sections/` (schema + component), `templates/*.json`, `settings.ts`, registered in `index.ts` |
82
+ | `components/sections/` | One renderer per `GET /home` section type (the `platform-home` theme section); an unknown type renders nothing |
83
+ | `components/` | Shared UI; client components start with `'use client'` |
84
+ | `components/ui/` | Primitives every page is built from (`DESIGN.md`, Components) |
85
+ | `components/header/`, `footer/` | Desktop nav and phone drawer from `lib/menu.ts`; footer columns and contacts |
86
+ | `app/styles/` | Layered CSS (`tokens`, `base`, `components`, `layout`, `sections`, `pages`, `utilities`), imported by `globals.css` |
87
+ | `components/telegram-shell.tsx` | Telegram Mini App: theme marks, back arrow, the one automatic Telegram sign-in (`PAGES.md`) |
88
+ | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
89
+ | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
90
+ | `app/storefront-api/[...path]` | Same-origin proxy (`createStorefrontProxy`); holds tokens and cart / checkout ids in httpOnly cookies |
91
+ | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
92
+ | `app/api/magicstore/preview` | Theme editor preview: signed token → draft mode; pages pass `themePreview()` (`lib/preview.ts`) as `preview` |
77
93
 
78
94
  ## Creating a new section
79
95
 
@@ -0,0 +1,117 @@
1
+ # Design
2
+
3
+ The contract for anyone (person or agent) who restyles this storefront. It says what the design is
4
+ made of and where each part lives; the reasoning and the rules behind it are in the skills, not here:
5
+
6
+ - `.claude/skills/storefront-design/SKILL.md`: what a store needs, what is off limits, pre-flight.
7
+ It wins over everything below.
8
+ - `.claude/skills/design-taste-frontend/SKILL.md`: taste (design read, dials, anti-slop rules).
9
+ - `.claude/skills/web-interface-guidelines/SKILL.md`: keyboard, focus, forms, touch targets.
10
+ - `.claude/skills/storefront-verify/SKILL.md`: look at every page type before calling it done.
11
+
12
+ ## Design read
13
+
14
+ > Reading this as: a general-purpose commerce theme for mainstream online shoppers in Uzbekistan
15
+ > and Russia (mobile-first, often inside Telegram), with a calm Dawn-style language, accent from
16
+ > `branding.colors.main`, leaning toward one neutral grotesk (Onest) with Cyrillic and Uzbek Latin,
17
+ > portrait 3:4 product media, and low density.
18
+
19
+ What the shops showed (2026-10-03, `humo.magicstore.dev`, `market.magicbot.test`):
20
+
21
+ - Catalogs of electronics accessories, cosmetics, clothing, toys: anything. Prices from 27 000 to
22
+ 2 650 000 UZS, often with a `compareAtPrice`.
23
+ - Product photos are marketplace cards: 3:4 portrait (600×800, 800×1066), busy, saturated, with
24
+ text printed on them. The interface around them stays quiet: neutral surfaces, one accent, no
25
+ decoration competing with the photo.
26
+ - Branding: a saturated accent (`#16A34A`, `#2563eb`), a dark navbar (`#052E16`, `#0F172A`) with
27
+ white text, a logo. Image sizes come without `width`/`height`, so every media box has a fixed
28
+ ratio.
29
+
30
+ Dials (taste skill §1):
31
+
32
+ | Pages | `DESIGN_VARIANCE` | `MOTION_INTENSITY` | `VISUAL_DENSITY` |
33
+ | ----------------------------------------- | ----------------- | ------------------ | ---------------- |
34
+ | Catalog, product, cart, checkout, account | 4 | 3 | 4 |
35
+ | Home and content pages (blog, pages) | 5 | 4 | 3 |
36
+
37
+ ## Tokens
38
+
39
+ All in `app/styles/tokens.css`. Components never use a raw color, size or radius; they use these.
40
+
41
+ | Group | Tokens | Notes |
42
+ | -------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
43
+ | Brand | `--accent --on-accent --accent-text --nav-bg --nav-fg --label-bg --label-fg` | From `shop.branding` at runtime (`lib/brand.ts`); fallbacks in `tokens.css` |
44
+ | Neutrals | `--bg --fg --muted --line --surface --placeholder --subtle --danger --success --warning` | Cool neutral grey; Telegram's theme replaces them in a Mini App |
45
+ | Spacing | `--space-1` … `--space-10` (4, 8, 12, 16, 24, 32, 48, 64, 96, 128 px) | Gaps and padding only from this scale |
46
+ | Type | `--text-xs` … `--text-4xl`, `--leading-tight/snug/normal`, `--font-body`, `--font-heading` | Sizes from `xl` up are fluid (`clamp`) |
47
+ | Shape | `--radius-sm` (controls) `--radius-md` (cards, media) `--radius-lg` (panels) `--radius-pill` | One scale, set by the `cornerRadius` theme setting |
48
+ | Depth | `--shadow-1` (raised) `--shadow-2` (drawers, menus) | Tinted with the foreground, never pure black |
49
+ | Layout | `--container` (1320px) `--gutter` (16px phone, up to 48px) `--header-h` | |
50
+ | Layers | `--z-header --z-drawer --z-toast` | Nothing else sets `z-index` above 1 |
51
+ | Motion | `--duration-fast --duration --ease` | `0ms` under `prefers-reduced-motion` |
52
+ | Media | `--media-ratio` | Product image ratio, from the `productImageRatio` theme setting |
53
+
54
+ Breakpoints, mobile first: **750px** (tablet) and **990px** (desktop).
55
+
56
+ Shape rule (taste skill §4.4): buttons, inputs and chips use `--radius-sm` (chips and badges may use
57
+ `--radius-pill`); cards and images `--radius-md`; drawers and menus `--radius-lg`. Nothing else.
58
+
59
+ ## Type
60
+
61
+ Onest (variable, 100 to 900, OFL, `app/fonts/`), self-hosted through `next/font/local` so builds
62
+ need no network. It covers Cyrillic (with `Ғ Қ Ҳ Ў`) and Uzbek Latin (`oʻ gʻ`, U+02BB). One family:
63
+ headings take weight 600, body 400, prices 500. No second display face, no serif.
64
+
65
+ ## Theme settings
66
+
67
+ Merchant-editable in the admin (`theme/settings.ts`), mapped by `app/layout.tsx` to `data-*` on
68
+ `<html>` and read by `tokens.css`:
69
+
70
+ | Setting | `data-*` | Values |
71
+ | ------------------------------------------------------ | ---------------------- | -------------------------------------------- |
72
+ | `cornerRadius` | `data-radius` | `none`, `small` (default), `medium`, `large` |
73
+ | `productImageRatio` | `data-image-ratio` | `square`, `portrait` (default), `natural` |
74
+ | `cardStyle` | `data-card-style` | `minimal` (default), `card` |
75
+ | `stickyHeader` | `data-sticky-header` | on by default |
76
+ | `showVendor`, `showRating`, `cartType`, `announcement` | — (read by components) | |
77
+
78
+ Colors are not theme settings: they stay in `shop.branding`.
79
+
80
+ ## CSS architecture
81
+
82
+ `app/globals.css` declares the layer order and imports one file per layer from `app/styles/`:
83
+
84
+ ```
85
+ @layer reset, tokens, base, components, layout, sections, pages, utilities;
86
+ ```
87
+
88
+ Class names are BEM-lite without a prefix: a block (`.product-card`), its elements
89
+ (`.product-card__media`), modifiers as a second class (`.button--primary`) or a `data-*` attribute
90
+ for state (`[data-state='open']`, `[aria-expanded='true']`). Telegram overrides stay on
91
+ `html[data-telegram]`.
92
+
93
+ ## Components
94
+
95
+ UI primitives in `components/ui/` (reuse them, never restyle a page with one-off markup): `Icon`,
96
+ `Button`, `IconButton`, `Drawer`, `Accordion`, `QuantityStepper`, `Badge`, `Skeleton`,
97
+ `Breadcrumbs`, `EmptyState`, `Field`, `Price`, `Rating`.
98
+
99
+ ## Accessibility and stability
100
+
101
+ - Contrast AA (4.5:1) for what the design owns: a text color the merchant left empty is black or
102
+ white by contrast (`lib/brand.ts`); a pair the merchant set is theirs (point a low one out to the
103
+ owner); the accent used as text on the page is `--accent-text` (half way to `--fg`, AA for pale
104
+ and dark accents alike). In Telegram's dark scheme the status colors switch to lighter values.
105
+ Text on a status or muted fill uses `var(--bg)`, not a fixed white.
106
+ - Tap targets at least 44px (24px with spacing for text links in lists); a visible focus ring on
107
+ everything focusable; drawers trap focus and return it; nothing only on hover.
108
+ - No layout shift: images keep their box (`aspect-ratio`), the LCP image is eager with
109
+ `fetchpriority="high"`, `loading.tsx` skeletons take the final sizes, and `.site-main` is at least
110
+ a screen tall so the footer never moves on screen.
111
+
112
+ ## Merchant data vs design
113
+
114
+ The merchant owns: colors, logo, favicon, every product, collection, page and article, banner
115
+ images, section content and order, the announcement. The design owns: neutrals, type, spacing,
116
+ shape, layout, interface words (`lib/i18n.ts`). A missing value hides its block; it is never
117
+ replaced with invented copy, images, ratings or urgency.
package/template/PAGES.md CHANGED
@@ -42,6 +42,12 @@ storefront token sends the browser straight to the API; then the tokens and the
42
42
  ## Layout — `app/layout.tsx`
43
43
 
44
44
  - Reads `api.shop()` and the navigation (`api.collectionsIndex` or `api.menusShow({ path: { handle: 'main' } })`).
45
+ `lib/menu.ts` turns them into links: `mainNav` (the `main` menu, else the root collections) for
46
+ `components/header/desktop-nav.tsx` and the phone drawer `components/header/mobile-menu.tsx`;
47
+ `footerNav` (the `footer` menu) for `components/footer/site-footer.tsx`, whose contact column
48
+ comes from `api.contacts()` through `lib/contacts.ts`.
49
+ - `<main>` is at least a screen tall, so the footer starts below the fold: a streamed page or a
50
+ client list replacing its placeholder moves nothing on screen.
45
51
  - `generateMetadata`: `shop.seo.title || shop.name`, `shop.seo.description ?? shop.description`,
46
52
  the favicon from `shop.branding.favicon`.
47
53
  - The merchant's colors: `brandStyle(shop.branding)` (`lib/brand.ts`) on `<html style>`, as the
@@ -121,40 +127,73 @@ a contract change. The steps, the setting types and a worked example (`size-guid
121
127
  `AGENTS.md` and `.claude/skills/theme-section/SKILL.md`; `theme/sections/testimonials.tsx` is a
122
128
  section with blocks to copy from.
123
129
 
130
+ ## Collections — `app/collections/page.tsx`
131
+
132
+ - Every root collection: `api.collectionsIndex({ query: { 'filter[root]': 'true', perPage: 100 } })`
133
+ as tiles (`CollectionTiles` in `components/sections/collections.tsx`; text cards when no
134
+ collection has an image), with its product count. The footer and the phone menu link it.
135
+ - States: no collections yet (an empty state with search).
136
+
124
137
  ## Collection — `app/collections/[handle]/page.tsx`
125
138
 
126
- - `api.collectionsShow({ path: { handle } })` and `api.collectionsProducts({ path: { handle }, query: { page, perPage } })`,
127
- both through `orNotFound`.
128
- - Filters and sorts: `api.collectionsFilters` lists the facets; pass them as `filter[…]` and `sort`
129
- query parameters. An unknown filter or sort is a `VALIDATION_FAILED`, not ignored.
130
- - SEO: `pageMeta({ seo: collection.seo, title, description })`, and `breadcrumbJsonLd`.
131
- - States: an empty collection ("Nothing here yet"), a `page` past the last one (empty list), 404.
132
-
133
- ## Product — `app/products/[handle]/page.tsx`, `components/buy-box.tsx`
134
-
135
- - `api.productsShow({ path: { handle } })` through `orNotFound`. Recommendations:
136
- `productsRecommendations` (`intent=RELATED` or `COMPLEMENTARY`); reviews: `productsReviews`
137
- (`meta.rating` has the average and breakdown). Both are catalog reads, cached like the product.
138
- - The buy box is a client component: `useVariantSelection(product)` (or `ProductProvider` +
139
- `useProduct`), `useCart().addLine({ productId, variantId, quantity })`, `useWishlist()`, and
140
- `useAnalytics().productView(...)`.
141
- - Price: the selected variant's `price` / `compareAtPrice`, else the product's, with `<Money>`.
139
+ - `app/collections/[handle]/layout.tsx` checks `api.collectionsShow` through `orNotFound` before
140
+ anything streams, so a missing handle is a real 404 (the route's `loading.tsx` would have sent
141
+ 200). The page reads the same cached request.
142
+ - `api.collectionsShow`, `api.collectionsFilters` and the shop, then `api.collectionsProducts` with
143
+ the URL's filters, and the child collections (`hasChildren`) as chips; the FAQ below.
144
+ - Filters and sort live in the URL, so every view is a link and works without JavaScript:
145
+ `lib/catalog-params.ts` reads them (`parseCatalog`) against the facets the API offered and
146
+ drops the rest — an unknown filter or sort is a `VALIDATION_FAILED`, not ignored — then builds
147
+ the API query (`apiQuery`) and every link (`catalogQuery`, `without`, `cleared`, `onPage`).
148
+ `components/catalog/catalog-layout.tsx` renders the facets (`facets.tsx`: lists, price range,
149
+ in stock; a sidebar on desktop, a drawer on phones through `facets-shell.tsx`), the count, the
150
+ sort, the active filters as removable chips, the grid and the pager. Search uses the same layout.
151
+ - SEO: `pageMeta({ seo: collection.seo, title, description })`, `breadcrumbJsonLd`; filtered and
152
+ sorted views are `noindex`.
153
+ - States: an empty collection ("Nothing here yet"), a filter with no match (clear the filters), a
154
+ `page` past the last one (empty list), 404.
155
+
156
+ ## Product — `app/products/[handle]/page.tsx`, `components/product/`
157
+
158
+ - `app/products/[handle]/layout.tsx` checks the product exists before the page streams (a real 404,
159
+ as for a collection).
160
+ - `api.productsShow({ path: { handle } })` through `orNotFound`, then in parallel: the shop,
161
+ `productsReviews` (first page; `meta.rating` has the average and the per-rating `breakdown`) and
162
+ `productsRecommendations` (`intent=COMPLEMENTARY` "pairs well with", `RELATED` "you may also
163
+ like"). All are catalog reads, cached like the product.
164
+ - `ProductSelection` (`product-selection.tsx`) holds `useVariantSelection(product)` for the parts
165
+ that depend on the variant: `Gallery` shows its `image`, `VariantPicker` its options (pills, a
166
+ `<select>` past eight values), `BuyBox` its price, stock and add to cart.
167
+ - The buy box: `<Price>` of the selected variant (else the product), the stock the API reports
168
+ (`quantityAvailable`; nothing when `null`), `QuantityStepper` bounded by it,
169
+ `useCart().addLine({ productId, variantId, quantity })` then the cart drawer, `useWishlist()`
170
+ (only with `features.wishlist`), `shop.checkout.deliveryText`, and
171
+ `useAnalytics().productView(...)`. A product with `preOrder` takes a pre-order instead
172
+ (`pre-order-form.tsx`, `preOrdersStore`: name and phone, none for a signed-in customer).
173
+ - Below: description and `faq` as accordions, share (Web Share API, else copy the link); reviews
174
+ (`reviews.tsx` server-renders the summary and first page, `review-list.tsx` loads more and
175
+ filters by `filter[rating]`); the two recommendation shelves.
142
176
  - SEO: `pageMeta({ seo, title, description, images })`, and
143
- `jsonLdScript(productJsonLd(product, { url }))` in a `<script type="application/ld+json">`.
177
+ `jsonLdScript(productJsonLd(product, { url }))` plus `breadcrumbJsonLd` in
178
+ `<script type="application/ld+json">`.
144
179
  - States: 404; out of stock (`availableForSale` false on the product or the selected variant —
145
- disable "add to cart", say "Sold out"); an option value that leads nowhere
146
- (`isAvailable(name, value)` false — disable it); a failed add (`CART_LINES_UNAVAILABLE` and others:
147
- show `detail`); no images (`<Image fallback>`).
180
+ "Sold out", no stepper); an option value no variant for sale has (`isAvailable(name, value)`
181
+ false — struck through, still selectable: it moves to the closest variant); a failed add
182
+ (`CART_LINES_UNAVAILABLE` and others: show `detail`); no images (a placeholder); no reviews (the
183
+ section is hidden); no recommendations (the shelf is hidden).
148
184
 
149
185
  ## Search — `app/search/page.tsx`
150
186
 
151
187
  - `api.search({ query: { q, page, perPage } })` — not cached (no tag). Suggestions while typing:
152
- `searchSuggestions({ query: { q } })` from the browser; popular queries: `searchTrending`.
153
- - Facets: `searchFilters({ query: { q } })`.
154
- - States: no query yet (show the form only), no results, pagination.
188
+ `searchSuggestions({ query: { q } })` from the browser (`components/search/`); popular queries:
189
+ `searchTrending`.
190
+ - Facets: `searchFilters({ query: { q } })`, rendered by the collection's `CatalogLayout` with `q`
191
+ kept in every link.
192
+ - States: no query yet (the form and popular searches), no results (an empty state and popular
193
+ searches), no match for the filters (clear them), pagination.
155
194
  - Search pages are usually `noindex`; report `useAnalytics().search(q, total)`.
156
195
 
157
- ## Cart — `app/cart/page.tsx`
196
+ ## Cart — `app/cart/page.tsx`, `components/cart/`
158
197
 
159
198
  - Client only: `useCart()` — `cart`, `status` (`loading` / `updating` / `idle`), `error`, and the
160
199
  changes (`updateLine`, `removeLine`, `setDiscountCodes`, `setNote`, `setGift`, `setPoints`).
@@ -162,9 +201,13 @@ section with blocks to copy from.
162
201
  Discount codes answer `applicable` and a `reasonCode`.
163
202
  - States: loading (the stored cart is being fetched), empty or no cart, a line no longer available,
164
203
  a rejected code, the last failed change (`error`, the cart is already back to the server's).
204
+ - The page lists the lines; `components/cart/cart-summary.tsx` holds the discount codes (a
205
+ rejected one says why, from `reasonCode`), the note (saved on blur through `setNote`), the
206
+ discounts and the total from `cart.cost`, and the checkout button. The cart drawer is the same
207
+ cart, opened after every add.
165
208
  - Never put the cart id in a URL.
166
209
 
167
- ## Checkout — `app/checkout/page.tsx`
210
+ ## Checkout — `app/checkout/page.tsx`, `components/checkout/`
168
211
 
169
212
  - Client only. `checkoutsStore({ body: { cartId } })` (safe to repeat), then fill what
170
213
  `checkout.missing` lists, in order: `CONTACT` → `DELIVERY_ADDRESS` (or a pickup point from
@@ -176,10 +219,25 @@ section with blocks to copy from.
176
219
  - States: empty cart, preparing, each step's validation errors (`VALIDATION_FAILED` → per field),
177
220
  `DELIVERY_QUOTE_EXPIRED` (list the options again), `CHECKOUT_NOT_READY`, `CHECKOUT_CLOSED`,
178
221
  `STORE_UNAVAILABLE`, a payment that is still pending.
222
+ - `components/checkout/use-checkout.ts` runs the steps and maps `VALIDATION_FAILED` to
223
+ `fieldErrors`; `steps.tsx` renders contact, delivery and payment (`radio-card.tsx` for the
224
+ choices) behind `step-indicator.tsx`; `order-summary.tsx` shows the lines and `checkout.cost`
225
+ (collapsed on phones); `thank-you.tsx` the order number after completion.
179
226
  - Never put the checkout id in a URL or a log.
180
227
 
181
- ## Account — `app/account/page.tsx`
182
-
228
+ ## Account — `app/account/`, `components/account/`
229
+
230
+ - Pages: overview with the orders (`app/account/page.tsx`), an order
231
+ (`app/account/orders/[id]/page.tsx`), addresses (`app/account/addresses/page.tsx`) and the
232
+ profile (`app/account/profile/page.tsx`), each inside `components/account/account-shell.tsx`
233
+ (sign-in gate, header, navigation). Client pages take their titles from
234
+ `app/account/layout.tsx`.
235
+ - An order (`order-detail.tsx`): its lines, amounts and status; "Order again" calls
236
+ `useCart().reorder(orderId)` (the server copies what is still for sale into a new cart, which
237
+ becomes the buyer's) and opens the cart; "Cancel" asks to confirm, then
238
+ `customerOrdersCancellation`.
239
+ - Addresses: `customerAddresses*` (add, edit, delete, make default). Profile: name, gender,
240
+ language through `customerUpdate`, and sign out.
183
241
  - Client only. Signed out:
184
242
  - Phone: `useCustomer().requestOtp(phone)` → `verifyOtp(phone, code)` (`OTP_INVALID`: re-type;
185
243
  `OTP_EXPIRED`: ask again). On a shop whose sign-in code is off
@@ -246,7 +304,7 @@ same API.
246
304
 
247
305
  All blog reads are `magicstore:pages`. Rules: the backend's `docs/api/v2/blog.md`.
248
306
 
249
- - **Index** `app/blog/page.tsx`: heading and intro from `shop.blog` (else `t.blog`), categories
307
+ - **Index** `app/blog/(index)/page.tsx` (a route group: its `loading.tsx` covers the index only): heading and intro from `shop.blog` (else `t.blog`), categories
250
308
  (`api.blogCategoriesIndex()`, `CategoryNav`), editor's picks (`filter[featured]=true`, first page
251
309
  only), a search box (`q`, at most 200 characters) and newest / popular (`sort=-recentViews`),
252
310
  then `api.blogArticlesIndex({ query: { page, perPage: 12, … } })` with `Pager`. Only a `sort` the
@@ -278,6 +336,21 @@ All blog reads are `magicstore:pages`. Rules: the backend's `docs/api/v2/blog.md
278
336
  `X-Anonymous-Id`; the proxy forwards it). `ArticleView` reports `ARTICLE_VIEW`.
279
337
  - Dates render in `shop.timezone` (`ArticleDate`).
280
338
 
339
+ ## Wishlist — `app/wishlist/page.tsx`
340
+
341
+ - Only with `shop.features.wishlist` (else 404); `noindex`. `useWishlist()` holds the ids (the
342
+ browser for a guest, the account when signed in); `components/wishlist/wishlist-view.tsx` reads
343
+ them with `productsIndex({ query: { 'filter[ids]': … } })` in chunks of 100 and keeps the
344
+ wishlist's order. A guest sees that signing in keeps the list.
345
+ - States: empty, signed out (the hint), a product no longer for sale (dropped by the API).
346
+
347
+ ## Contacts — `app/contacts/page.tsx`
348
+
349
+ - `api.contacts()`: phones and emails (`tel:` and `mailto:` links via `lib/contacts.ts`), the head
350
+ office with its hours (weekday names from `Intl`, closed days), branches with a map link,
351
+ requisites, socials. Nothing the shop did not fill in is shown; no contacts at all → an empty
352
+ state.
353
+
281
354
  ## Content pages, sitemap — `app/pages/[handle]/page.tsx`, `app/sitemap.ts`
282
355
 
283
356
  - `api.pagesShow({ path: { handle } })` (`magicstore:pages`) through `orNotFound`,
@@ -291,7 +364,12 @@ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain` (`lib/site.ts`
291
364
  ## Every page
292
365
 
293
366
  - Import only the SDK's entry points; `npx create-magic-storefront check` passes.
294
- - Not found → `notFound()`; any other failure shows `describe(error, t.tryAgainLater)`
367
+ - Not found → `notFound()` before anything streams: a route with a `loading.tsx` checks the
368
+ resource in its `layout.tsx` (see Collection), else the answer is a 200 marked `noindex`. The blog
369
+ index keeps its skeleton in the route group `app/blog/(index)/` so articles are not covered by it.
370
+ - Loading: `loading.tsx` skeletons (`components/page-loading.tsx`, `components/ui/skeleton.tsx`)
371
+ take the final layout's sizes.
372
+ - Any other failure shows `describe(error, t.tryAgainLater)`
295
373
  (`lib/errors.ts`) or lets `app/error.tsx` catch it (it shows `detail`, and "temporarily closed"
296
374
  on `STORE_UNAVAILABLE`).
297
375
  - The interface speaks the storefront's language: every word the storefront itself shows comes from
@@ -2,9 +2,13 @@
2
2
 
3
3
  A Next.js (App Router) storefront on the MagicStore Storefront API v2, built only on the public SDK
4
4
  (`@magicstoreai/storefront-client`, `@magicstoreai/hydrogen`). The home page from the merchant's
5
- sections (or your own composition: `GET /home` is optional), catalog, collections, search, product with variants, cart, checkout (pickup or delivery,
6
- payment), OTP sign-in and orders, SEO and JSON-LD, a webhook that revalidates cached pages — and it
7
- runs as a Telegram Mini App (sign-in from the bot, back button, Telegram's theme, phone sharing).
5
+ sections (or your own composition: `GET /home` is optional), all collections, a collection and
6
+ search with facets and sort, product with gallery, variants, reviews and recommendations, wishlist,
7
+ cart drawer and cart page, a stepped checkout (pickup or delivery, payment), an account (OTP
8
+ sign-in, orders with reorder and cancel, addresses, profile), blog, contacts and content pages,
9
+ SEO and JSON-LD, a webhook that revalidates cached pages — and it runs as a Telegram Mini App
10
+ (sign-in from the bot, back button, Telegram's theme, phone sharing). The design is a calm,
11
+ Dawn-like theme in plain CSS with tokens; the merchant's colors and theme settings restyle it.
8
12
 
9
13
  ```bash
10
14
  cp .env.example .env.local # set MAGICSTORE_SHOP_DOMAIN
@@ -15,6 +19,7 @@ npm run dev
15
19
  | Where | What |
16
20
  | ------------------------------ | ----------------------------------------------------------------------------- |
17
21
  | `PAGES.md` | How each page type is built: data calls, cache tags, SEO, required states. |
22
+ | `DESIGN.md` | The design system: tokens, type, theme settings, CSS layers, UI primitives. |
18
23
  | `lib/api.ts` | The server-side client. Public reads are cached under `magicstore:*` tags. |
19
24
  | `components/sections/` | One renderer per `GET /home` section type; an unknown type renders nothing. |
20
25
  | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist and analytics in the browser. |
@@ -41,3 +46,10 @@ The same secret signs the theme editor's preview links: the admin opens
41
46
  and serves the draft theme to that browser only (`lib/preview.ts`); `?exit=1` ends it. Previews also need
42
47
  `MAGICSTORE_STOREFRONT_TOKEN`, the storefront token that webhook belongs to: the API previews only a
43
48
  read that names its token.
49
+
50
+ **Look before you ship.** After a visual change, build, run and screenshot every page type at 390
51
+ and 1280px (`.claude/skills/storefront-verify/SKILL.md`); the screenshots go to `.screenshots/`,
52
+ which git ignores.
53
+
54
+ **Font.** Onest, self-hosted from `app/fonts/` (SIL Open Font License 1.1, `app/fonts/OFL.txt`), so
55
+ builds need no network.
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { useMagicStore } from '@magicstoreai/hydrogen';
4
+ import { AccountShell } from '@/components/account/account-shell';
5
+ import { Addresses } from '@/components/account/addresses';
6
+ import { messages } from '@/lib/i18n';
7
+
8
+ export default function AddressesPage() {
9
+ const t = messages(useMagicStore().locale);
10
+ return (
11
+ <AccountShell>
12
+ <h2 className="account__section-title">{t.addresses}</h2>
13
+ <Addresses />
14
+ </AccountShell>
15
+ );
16
+ }
@@ -0,0 +1,14 @@
1
+ import type { Metadata } from 'next';
2
+ import type { ReactNode } from 'react';
3
+ import { locale } from '@/lib/api';
4
+ import { messages } from '@/lib/i18n';
5
+
6
+ // Account pages are client components; their title and robots come from here. Personal: not indexed.
7
+ export const metadata: Metadata = {
8
+ title: messages(locale).account,
9
+ robots: { index: false, follow: false },
10
+ };
11
+
12
+ export default function AccountLayout({ children }: { children: ReactNode }) {
13
+ return children;
14
+ }
@@ -0,0 +1,15 @@
1
+ import { PageLoading } from '@/components/page-loading';
2
+ import { Skeleton, TitleSkeleton } from '@/components/ui/skeleton';
3
+
4
+ export default function AccountLoading() {
5
+ return (
6
+ <PageLoading>
7
+ <TitleSkeleton width="30%" />
8
+ <div className="stack">
9
+ <Skeleton height="5.5rem" />
10
+ <Skeleton height="5.5rem" />
11
+ <Skeleton height="5.5rem" />
12
+ </div>
13
+ </PageLoading>
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ import { useParams } from 'next/navigation';
4
+ import { AccountShell } from '@/components/account/account-shell';
5
+ import { OrderDetail } from '@/components/account/order-detail';
6
+
7
+ /** One order. Order ids are the customer's own (another's answers `NOT_FOUND`), so the path is safe. */
8
+ export default function OrderPage() {
9
+ const { id } = useParams<{ id: string }>();
10
+ return (
11
+ <AccountShell>
12
+ <OrderDetail id={id} />
13
+ </AccountShell>
14
+ );
15
+ }