create-magic-storefront 0.4.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 (143) hide show
  1. package/package.json +3 -3
  2. package/template/.claude/skills/storefront-design/SKILL.md +14 -10
  3. package/template/.env.example +5 -0
  4. package/template/AGENTS.md +36 -17
  5. package/template/DESIGN.md +117 -0
  6. package/template/PAGES.md +116 -29
  7. package/template/README.md +15 -3
  8. package/template/app/account/addresses/page.tsx +16 -0
  9. package/template/app/account/layout.tsx +14 -0
  10. package/template/app/account/loading.tsx +15 -0
  11. package/template/app/account/orders/[id]/page.tsx +15 -0
  12. package/template/app/account/page.tsx +12 -244
  13. package/template/app/account/profile/page.tsx +16 -0
  14. package/template/app/blog/(index)/loading.tsx +22 -0
  15. package/template/app/blog/{page.tsx → (index)/page.tsx} +42 -25
  16. package/template/app/blog/[handle]/page.tsx +92 -56
  17. package/template/app/blog/author/[handle]/page.tsx +1 -1
  18. package/template/app/cart/layout.tsx +14 -0
  19. package/template/app/cart/page.tsx +49 -101
  20. package/template/app/checkout/layout.tsx +14 -0
  21. package/template/app/checkout/page.tsx +98 -281
  22. package/template/app/collections/[handle]/layout.tsx +20 -0
  23. package/template/app/collections/[handle]/loading.tsx +23 -0
  24. package/template/app/collections/[handle]/page.tsx +131 -15
  25. package/template/app/collections/page.tsx +49 -0
  26. package/template/app/contacts/page.tsx +196 -0
  27. package/template/app/error.tsx +19 -8
  28. package/template/app/fonts/OFL.txt +93 -0
  29. package/template/app/fonts/index.ts +14 -0
  30. package/template/app/fonts/onest-variable.woff2 +0 -0
  31. package/template/app/globals.css +14 -518
  32. package/template/app/layout.tsx +74 -50
  33. package/template/app/not-found.tsx +34 -4
  34. package/template/app/page.tsx +1 -1
  35. package/template/app/pages/[handle]/page.tsx +1 -1
  36. package/template/app/products/[handle]/layout.tsx +20 -0
  37. package/template/app/products/[handle]/loading.tsx +25 -0
  38. package/template/app/products/[handle]/page.tsx +112 -15
  39. package/template/app/providers.tsx +20 -7
  40. package/template/app/search/loading.tsx +12 -0
  41. package/template/app/search/page.tsx +138 -25
  42. package/template/app/sitemap.ts +5 -1
  43. package/template/app/storefront-api/[...path]/route.ts +11 -66
  44. package/template/app/styles/base.css +138 -0
  45. package/template/app/styles/components.css +1108 -0
  46. package/template/app/styles/layout.css +455 -0
  47. package/template/app/styles/pages.css +1839 -0
  48. package/template/app/styles/reset.css +55 -0
  49. package/template/app/styles/sections.css +415 -0
  50. package/template/app/styles/tokens.css +167 -0
  51. package/template/app/styles/utilities.css +37 -0
  52. package/template/app/wishlist/page.tsx +26 -0
  53. package/template/components/account/account-shell.tsx +100 -0
  54. package/template/components/account/addresses.tsx +247 -0
  55. package/template/components/account/order-detail.tsx +281 -0
  56. package/template/components/account/order-list.tsx +94 -0
  57. package/template/components/account/order-status.tsx +19 -0
  58. package/template/components/account/profile.tsx +129 -0
  59. package/template/components/account/sign-in.tsx +164 -0
  60. package/template/components/article-body.tsx +3 -2
  61. package/template/components/article-card.tsx +37 -12
  62. package/template/components/cart/cart-drawer.tsx +149 -0
  63. package/template/components/cart/cart-lines.tsx +89 -0
  64. package/template/components/cart/cart-summary.tsx +191 -0
  65. package/template/components/catalog/catalog-form.tsx +107 -0
  66. package/template/components/catalog/catalog-layout.tsx +82 -0
  67. package/template/components/catalog/facets-shell.tsx +60 -0
  68. package/template/components/catalog/facets.tsx +216 -0
  69. package/template/components/checkout/order-summary.tsx +84 -0
  70. package/template/components/checkout/radio-card.tsx +34 -0
  71. package/template/components/checkout/step-indicator.tsx +56 -0
  72. package/template/components/checkout/steps.tsx +264 -0
  73. package/template/components/checkout/thank-you.tsx +48 -0
  74. package/template/components/checkout/use-checkout.ts +90 -0
  75. package/template/components/footer/site-footer.tsx +132 -0
  76. package/template/components/header/cart-button.tsx +43 -0
  77. package/template/components/header/desktop-nav.tsx +133 -0
  78. package/template/components/header/mobile-menu.tsx +102 -0
  79. package/template/components/header/site-header.tsx +78 -0
  80. package/template/components/page-loading.tsx +20 -0
  81. package/template/components/pager.tsx +3 -1
  82. package/template/components/product/buy-box.tsx +148 -0
  83. package/template/components/product/gallery.tsx +179 -0
  84. package/template/components/product/pre-order-form.tsx +93 -0
  85. package/template/components/product/product-selection.tsx +31 -0
  86. package/template/components/product/recommendations.tsx +26 -0
  87. package/template/components/product/review-list.tsx +159 -0
  88. package/template/components/product/reviews.tsx +80 -0
  89. package/template/components/product/variant-picker.tsx +75 -0
  90. package/template/components/product-card.tsx +103 -0
  91. package/template/components/product-grid.tsx +27 -57
  92. package/template/components/quick-add.tsx +62 -0
  93. package/template/components/search/predictive-search.tsx +282 -0
  94. package/template/components/sections/announcement-bar.tsx +1 -1
  95. package/template/components/sections/banner.tsx +1 -1
  96. package/template/components/sections/collections.tsx +50 -13
  97. package/template/components/sections/deal-of-day.tsx +2 -2
  98. package/template/components/sections/product-shelves.tsx +13 -5
  99. package/template/components/sections/shoppable-stories.tsx +2 -2
  100. package/template/components/sections/store-reviews.tsx +10 -4
  101. package/template/components/share-button.tsx +42 -0
  102. package/template/components/ui/accordion.tsx +35 -0
  103. package/template/components/ui/badge.tsx +20 -0
  104. package/template/components/ui/breadcrumbs.tsx +31 -0
  105. package/template/components/ui/button.tsx +90 -0
  106. package/template/components/ui/drawer.tsx +84 -0
  107. package/template/components/ui/empty-state.tsx +27 -0
  108. package/template/components/ui/field.tsx +33 -0
  109. package/template/components/ui/icon.tsx +112 -0
  110. package/template/components/ui/price.tsx +35 -0
  111. package/template/components/ui/quantity-stepper.tsx +91 -0
  112. package/template/components/ui/rating.tsx +48 -0
  113. package/template/components/ui/skeleton.tsx +46 -0
  114. package/template/components/wishlist/wishlist-view.tsx +134 -0
  115. package/template/lib/article-outline.ts +35 -0
  116. package/template/lib/catalog-params.ts +170 -0
  117. package/template/lib/contacts.ts +34 -0
  118. package/template/lib/errors.ts +29 -0
  119. package/template/lib/i18n.ts +484 -0
  120. package/template/lib/menu.ts +70 -0
  121. package/template/lib/theme-settings.ts +36 -0
  122. package/template/lib/upstream.ts +21 -0
  123. package/template/llms.txt +13 -0
  124. package/template/theme/index.ts +7 -1
  125. package/template/theme/sections/collapsible-content.tsx +75 -0
  126. package/template/theme/sections/collection-list.tsx +32 -4
  127. package/template/theme/sections/featured-collection.tsx +22 -2
  128. package/template/theme/sections/hero.tsx +94 -7
  129. package/template/theme/sections/image-with-text.tsx +19 -3
  130. package/template/theme/sections/multicolumn.tsx +113 -0
  131. package/template/theme/sections/page-content.tsx +10 -4
  132. package/template/theme/sections/product-shelf.tsx +26 -4
  133. package/template/theme/sections/rich-text.tsx +13 -1
  134. package/template/theme/sections/testimonials.tsx +14 -9
  135. package/template/theme/settings.ts +85 -1
  136. package/template/theme/templates/index.json +4 -2
  137. package/template/.screenshots/home-1280.png +0 -0
  138. package/template/.screenshots/home-390.png +0 -0
  139. package/template/.screenshots/home-filled-1280.png +0 -0
  140. package/template/.screenshots/home-filled-390.png +0 -0
  141. package/template/.screenshots/page-1280.png +0 -0
  142. package/template/components/buy-box.tsx +0 -89
  143. package/template/components/cart-link.tsx +0 -15
@@ -1,520 +1,16 @@
1
- :root {
2
- --bg: #fff;
3
- --fg: #16161a;
4
- --muted: #6b6b76;
5
- --line: #e7e7ec;
6
- --danger: #c62828;
7
- /* Controls and image placeholders: white and light grey here, Telegram's colors in a Mini App. */
8
- --surface: #fff;
9
- --placeholder: #f3f3f6;
10
- /* Brand tokens: app/layout.tsx overrides them with the merchant's colors (lib/brand.ts). */
11
- --accent: #1f5eff;
12
- --on-accent: #fff;
13
- --nav-bg: var(--bg);
14
- --nav-fg: var(--fg);
15
- --label-bg: var(--accent);
16
- --label-fg: var(--on-accent);
17
- font-family:
18
- system-ui,
19
- -apple-system,
20
- 'Segoe UI',
21
- Roboto,
22
- sans-serif;
23
- color: var(--fg);
24
- background: var(--bg);
25
- }
26
1
  /*
27
- * Inside a Telegram Mini App (components/telegram-shell.tsx marks <html>): Telegram's theme, which
28
- * its script writes as --tg-theme-* on <html>, with the defaults above as fallbacks. The merchant's
29
- * accent from <html style> (lib/brand.ts) still wins over Telegram's button color.
2
+ * The storefront's styles, one file per cascade layer (DESIGN.md). The order below is the
3
+ * precedence: a later layer wins over an earlier one whatever the selector. Components are the
4
+ * reusable parts; the layout, sections and pages that place them may restyle them; a utility
5
+ * beats everything.
30
6
  */
31
- html[data-telegram] {
32
- --bg: var(--tg-theme-bg-color, #fff);
33
- --fg: var(--tg-theme-text-color, #16161a);
34
- --muted: var(--tg-theme-hint-color, #6b6b76);
35
- --danger: var(--tg-theme-destructive-text-color, #c62828);
36
- --surface: var(--tg-theme-secondary-bg-color, #fff);
37
- --placeholder: var(--tg-theme-secondary-bg-color, #f3f3f6);
38
- --accent: var(--tg-theme-button-color, #1f5eff);
39
- --on-accent: var(--tg-theme-button-text-color, #fff);
40
- }
41
- html[data-telegram][data-color-scheme='dark'] {
42
- --line: rgba(255, 255, 255, 0.14);
43
- color-scheme: dark;
44
- }
45
- html[data-telegram] body {
46
- /* Telegram's stable viewport (the sheet can be dragged), the dynamic viewport outside it. */
47
- min-height: var(--tg-viewport-stable-height, 100dvh);
48
- padding-left: max(env(safe-area-inset-left), var(--tg-safe-area-inset-left, 0px));
49
- padding-right: max(env(safe-area-inset-right), var(--tg-safe-area-inset-right, 0px));
50
- padding-bottom: max(env(safe-area-inset-bottom), var(--tg-safe-area-inset-bottom, 0px));
51
- }
52
- * {
53
- box-sizing: border-box;
54
- }
55
- body {
56
- margin: 0;
57
- }
58
- a {
59
- color: inherit;
60
- }
61
- main {
62
- max-width: 1100px;
63
- margin: 0 auto;
64
- padding: 24px 16px 64px;
65
- }
66
- header.site {
67
- border-bottom: 1px solid var(--line);
68
- background: var(--nav-bg);
69
- color: var(--nav-fg);
70
- }
71
- header.site nav {
72
- max-width: 1100px;
73
- margin: 0 auto;
74
- padding: 12px 16px;
75
- display: flex;
76
- gap: 16px;
77
- align-items: center;
78
- flex-wrap: wrap;
79
- }
80
- header.site .brand {
81
- font-weight: 700;
82
- text-decoration: none;
83
- margin-right: auto;
84
- }
85
- header.site .brand img {
86
- display: block;
87
- width: auto;
88
- height: 32px;
89
- }
90
- .grid {
91
- display: grid;
92
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
93
- gap: 20px;
94
- }
95
- .card {
96
- text-decoration: none;
97
- display: flex;
98
- flex-direction: column;
99
- gap: 6px;
100
- }
101
- .card img,
102
- .card .noimg {
103
- width: 100%;
104
- /* The intrinsic width/height attributes (no layout shift) must not override the ratio. */
105
- height: auto;
106
- aspect-ratio: 1;
107
- object-fit: cover;
108
- background: var(--placeholder);
109
- border-radius: 8px;
110
- }
111
- .muted {
112
- color: var(--muted);
113
- }
114
- .error {
115
- color: var(--danger);
116
- }
117
- .row {
118
- display: flex;
119
- gap: 12px;
120
- align-items: center;
121
- flex-wrap: wrap;
122
- }
123
- .stack {
124
- display: flex;
125
- flex-direction: column;
126
- gap: 12px;
127
- }
128
- .product {
129
- display: grid;
130
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
131
- gap: 32px;
132
- }
133
- @media (max-width: 720px) {
134
- .product {
135
- grid-template-columns: 1fr;
136
- }
137
- }
138
- .product img {
139
- width: 100%;
140
- height: auto;
141
- border-radius: 8px;
142
- }
143
- button,
144
- .button {
145
- font: inherit;
146
- padding: 10px 16px;
147
- border-radius: 8px;
148
- border: 1px solid var(--line);
149
- background: var(--surface);
150
- color: var(--fg);
151
- min-height: 44px;
152
- cursor: pointer;
153
- text-decoration: none;
154
- }
155
- button.primary,
156
- .button.primary {
157
- background: var(--accent);
158
- border-color: var(--accent);
159
- color: var(--on-accent);
160
- }
161
- button[aria-pressed='true'] {
162
- border-color: var(--fg);
163
- }
164
- button:disabled {
165
- opacity: 0.45;
166
- cursor: not-allowed;
167
- }
168
- input,
169
- select {
170
- font: inherit;
171
- min-height: 44px;
172
- padding: 9px 12px;
173
- background: var(--surface);
174
- color: var(--fg);
175
- border: 1px solid var(--line);
176
- border-radius: 8px;
177
- }
178
- table {
179
- width: 100%;
180
- border-collapse: collapse;
181
- }
182
- td,
183
- th {
184
- padding: 10px 6px;
185
- border-bottom: 1px solid var(--line);
186
- text-align: left;
187
- vertical-align: middle;
188
- }
189
- .cart-lines {
190
- list-style: none;
191
- margin: 0;
192
- padding: 0;
193
- }
194
- /* Phone: image and title on one row, the controls under the title. Wider: all on one row. */
195
- .cart-line {
196
- display: grid;
197
- grid-template-columns: 64px minmax(0, 1fr);
198
- gap: 8px 12px;
199
- padding: 12px 0;
200
- border-bottom: 1px solid var(--line);
201
- }
202
- .cart-line-controls {
203
- grid-column: 2;
204
- display: flex;
205
- gap: 12px;
206
- align-items: center;
207
- flex-wrap: wrap;
208
- }
209
- @media (min-width: 720px) {
210
- .cart-line {
211
- grid-template-columns: 64px minmax(0, 1fr) auto;
212
- align-items: center;
213
- }
214
- .cart-line-controls {
215
- grid-column: auto;
216
- }
217
- }
218
- .pager {
219
- display: flex;
220
- gap: 8px;
221
- margin-top: 24px;
222
- }
223
- fieldset {
224
- border: 1px solid var(--line);
225
- border-radius: 8px;
226
- padding: 16px;
227
- }
228
- .home {
229
- display: flex;
230
- flex-direction: column;
231
- gap: 40px;
232
- }
233
- .announcement {
234
- margin: 0;
235
- padding: 10px 16px;
236
- border-radius: 8px;
237
- background: var(--fg);
238
- color: var(--bg);
239
- text-align: center;
240
- overflow: hidden;
241
- white-space: nowrap;
242
- }
243
- .announcement-ltr span,
244
- .announcement-rtl span {
245
- display: inline-block;
246
- animation: marquee 18s linear infinite;
247
- }
248
- .announcement-rtl span {
249
- animation-direction: reverse;
250
- }
251
- @keyframes marquee {
252
- from {
253
- transform: translateX(-100%);
254
- }
255
- to {
256
- transform: translateX(100%);
257
- }
258
- }
259
- @media (prefers-reduced-motion: reduce) {
260
- .announcement span {
261
- animation: none;
262
- }
263
- }
264
- .banner {
265
- display: grid;
266
- grid-auto-flow: column;
267
- grid-auto-columns: 100%;
268
- overflow-x: auto;
269
- scroll-snap-type: x mandatory;
270
- gap: 16px;
271
- }
272
- .banner > * {
273
- scroll-snap-align: start;
274
- }
275
- .banner img {
276
- width: 100%;
277
- height: auto;
278
- border-radius: 8px;
279
- }
280
- .deal {
281
- display: grid;
282
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
283
- gap: 24px;
284
- padding: 16px;
285
- border: 2px solid var(--accent);
286
- border-radius: 12px;
287
- }
288
- @media (max-width: 720px) {
289
- .deal {
290
- grid-template-columns: 1fr;
291
- }
292
- }
293
- .deal img {
294
- width: 100%;
295
- height: auto;
296
- border-radius: 8px;
297
- }
298
- .badge {
299
- align-self: flex-start;
300
- padding: 2px 10px;
301
- border-radius: 999px;
302
- background: var(--label-bg);
303
- color: var(--label-fg);
304
- }
305
- .stories {
306
- display: flex;
307
- gap: 16px;
308
- overflow-x: auto;
309
- }
310
- .story {
311
- display: flex;
312
- flex-direction: column;
313
- align-items: center;
314
- gap: 6px;
315
- text-decoration: none;
316
- flex: 0 0 88px;
317
- }
318
- .story img,
319
- .story .noimg {
320
- width: 80px;
321
- height: 80px;
322
- object-fit: cover;
323
- background: var(--placeholder);
324
- border-radius: 12px;
325
- }
326
- .stories-circle .story img,
327
- .stories-circle .story .noimg {
328
- border-radius: 50%;
329
- }
330
- blockquote {
331
- margin: 0;
332
- padding: 16px;
333
- border: 1px solid var(--line);
334
- border-radius: 8px;
335
- }
336
- footer.site {
337
- border-top: 1px solid var(--line);
338
- }
339
- footer.site nav {
340
- max-width: 1100px;
341
- margin: 0 auto;
342
- padding: 16px;
343
- display: flex;
344
- gap: 16px;
345
- flex-wrap: wrap;
346
- }
347
- .articles {
348
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
349
- }
350
- .article-card img,
351
- .article-card .noimg {
352
- aspect-ratio: 16 / 9;
353
- }
354
- .article-card .badge {
355
- align-self: flex-start;
356
- }
357
- .chips a {
358
- display: inline-flex;
359
- align-items: center;
360
- gap: 6px;
361
- min-height: 44px;
362
- padding: 0 14px;
363
- border: 1px solid var(--line);
364
- border-radius: 999px;
365
- text-decoration: none;
366
- }
367
- .chips a[aria-current='page'] {
368
- background: var(--accent);
369
- border-color: var(--accent);
370
- color: var(--on-accent);
371
- }
372
- .article {
373
- max-width: 760px;
374
- margin: 0 auto;
375
- }
376
- .article-cover {
377
- width: 100%;
378
- height: auto;
379
- border-radius: 8px;
380
- }
381
- .prose {
382
- line-height: 1.65;
383
- }
384
- .prose img {
385
- max-width: 100%;
386
- height: auto;
387
- }
388
- .prose table {
389
- display: block;
390
- overflow-x: auto;
391
- }
392
- .article-product {
393
- max-width: 260px;
394
- margin: 16px 0;
395
- }
396
- .callout {
397
- margin: 16px 0;
398
- padding: 12px 16px;
399
- border-left: 4px solid var(--accent);
400
- border-radius: 8px;
401
- background: var(--placeholder);
402
- }
403
- .callout[data-callout='warning'] {
404
- border-left-color: var(--danger);
405
- }
406
- .author img {
407
- border-radius: 50%;
408
- object-fit: cover;
409
- }
410
- .author h1 {
411
- margin: 0;
412
- }
413
- /* Theme sections (theme/sections/): the merchant edits their content in the admin. */
414
- /* A section with nothing to show yet renders nothing, but ThemeSections still wraps it. */
415
- [data-section-id]:empty {
416
- display: none;
417
- }
418
- .site-announcement {
419
- margin: 0;
420
- padding: 8px 16px;
421
- background: var(--accent);
422
- color: var(--on-accent);
423
- text-align: center;
424
- }
425
- .hero {
426
- display: grid;
427
- border-radius: 12px;
428
- overflow: hidden;
429
- background: var(--placeholder);
430
- }
431
- .hero > img,
432
- .hero-text {
433
- grid-area: 1 / 1;
434
- }
435
- .hero > img {
436
- width: 100%;
437
- height: auto;
438
- aspect-ratio: 21 / 9;
439
- object-fit: cover;
440
- }
441
- .hero-text {
442
- align-self: center;
443
- padding: 40px 24px;
444
- }
445
- .hero > img + .hero-text {
446
- background: linear-gradient(to top, rgb(0 0 0 / 0.7), rgb(0 0 0 / 0.35) 55%, transparent);
447
- color: #fff;
448
- text-shadow: 0 1px 2px rgb(0 0 0 / 0.4);
449
- align-self: stretch;
450
- justify-content: flex-end;
451
- }
452
- .hero-center .hero-text {
453
- align-items: center;
454
- text-align: center;
455
- }
456
- .hero-text h1,
457
- .hero-text p {
458
- margin: 0;
459
- max-width: 40ch;
460
- }
461
- .hero-text .button,
462
- .image-with-text .button {
463
- align-self: flex-start;
464
- text-shadow: none;
465
- }
466
- .hero-center .hero-text .button {
467
- align-self: center;
468
- }
469
- @media (max-width: 720px) {
470
- .hero > img {
471
- aspect-ratio: 4 / 5;
472
- }
473
- }
474
- .rich-text-narrow {
475
- max-width: 760px;
476
- }
477
- .image-with-text {
478
- display: grid;
479
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
480
- gap: 32px;
481
- align-items: center;
482
- }
483
- .image-with-text.image-right > img {
484
- order: 2;
485
- }
486
- .image-with-text > img {
487
- width: 100%;
488
- height: auto;
489
- aspect-ratio: 4 / 3;
490
- object-fit: cover;
491
- border-radius: 12px;
492
- }
493
- @media (max-width: 720px) {
494
- .image-with-text {
495
- grid-template-columns: 1fr;
496
- }
497
- .image-with-text.image-right > img {
498
- order: 0;
499
- }
500
- }
501
- .testimonials {
502
- display: grid;
503
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
504
- gap: 20px;
505
- }
506
- .testimonials figure {
507
- margin: 0;
508
- padding: 16px;
509
- border: 1px solid var(--line);
510
- border-radius: 12px;
511
- }
512
- .testimonials blockquote {
513
- margin: 0;
514
- padding: 0;
515
- border: 0;
516
- }
517
- .testimonials .rating {
518
- color: var(--accent);
519
- letter-spacing: 2px;
520
- }
7
+ @layer reset, tokens, base, components, layout, sections, pages, utilities;
8
+
9
+ @import './styles/reset.css';
10
+ @import './styles/tokens.css';
11
+ @import './styles/base.css';
12
+ @import './styles/components.css';
13
+ @import './styles/layout.css';
14
+ @import './styles/sections.css';
15
+ @import './styles/pages.css';
16
+ @import './styles/utilities.css';
@@ -1,18 +1,19 @@
1
1
  import type { Metadata } from 'next';
2
- import Link from 'next/link';
3
2
  import Script from 'next/script';
4
3
  import type { ReactNode } from 'react';
5
- import { Image } from '@magicstoreai/hydrogen';
6
4
  // From /core, not the "use client" entry: a plain string a server component can read.
7
5
  import { TELEGRAM_WEB_APP_SCRIPT } from '@magicstoreai/hydrogen/core';
8
- import { loadThemeSettings } from '@magicstoreai/hydrogen/server';
9
- import { CartLink } from '@/components/cart-link';
6
+ import { SiteFooter } from '@/components/footer/site-footer';
7
+ import { SiteHeader } from '@/components/header/site-header';
10
8
  import { TelegramShell } from '@/components/telegram-shell';
11
9
  import { api, locale } from '@/lib/api';
12
- import { themePreview } from '@/lib/preview';
13
10
  import { brandStyle } from '@/lib/brand';
11
+ import { hasContacts } from '@/lib/contacts';
12
+ import { credentialsMode } from '@/lib/upstream';
14
13
  import { messages } from '@/lib/i18n';
15
- import theme from '@/theme';
14
+ import { footerNav, mainNav, readMenu, type NavItem } from '@/lib/menu';
15
+ import { globalSettings, themeAttributes } from '@/lib/theme-settings';
16
+ import { onest } from './fonts';
16
17
  import { Providers } from './providers';
17
18
  import './globals.css';
18
19
 
@@ -30,65 +31,88 @@ export async function generateMetadata(): Promise<Metadata> {
30
31
  }
31
32
 
32
33
  export default async function RootLayout({ children }: { children: ReactNode }) {
33
- const [{ data: shop }, { data: collections }, { data: pages }, themeSettings] = await Promise.all(
34
- [
35
- api.shop(),
36
- api.collectionsIndex({ query: { perPage: 6 } }),
37
- api.pagesIndex(),
38
- themePreview().then((preview) => loadThemeSettings(api, theme, { locale, preview })),
39
- ],
40
- );
41
- const announcement = themeSettings.announcement as string;
34
+ const [
35
+ { data: shop },
36
+ mainMenu,
37
+ footerMenu,
38
+ { data: collections },
39
+ { data: pages },
40
+ contacts,
41
+ themeSettings,
42
+ ] = await Promise.all([
43
+ api.shop(),
44
+ readMenu(() => api.menusShow({ path: { handle: 'main' } })),
45
+ readMenu(() => api.menusShow({ path: { handle: 'footer' } })),
46
+ // The header's fallback when the shop has no main menu; the footer's top collections.
47
+ api.collectionsIndex({ query: { 'filter[root]': 'true', perPage: 12 } }),
48
+ api.pagesIndex(),
49
+ // Every page renders the header and footer: a failed contacts read leaves the footer without
50
+ // its contacts column (and /contacts shows the error itself) rather than failing every page.
51
+ api.contacts().then(
52
+ ({ data }) => data,
53
+ () => null,
54
+ ),
55
+ globalSettings(),
56
+ ]);
57
+ const { announcement } = themeSettings;
42
58
  const t = messages(locale);
59
+ // Without a footer menu, the shop's pages are the footer's links (as before menus existed).
60
+ const footer = footerNav(footerMenu);
61
+ const footerLinks =
62
+ footer.columns.length + footer.links.length + footer.policies.length > 0
63
+ ? footer
64
+ : { ...footer, links: pages.map((page) => link(page.title, `/pages/${page.handle}`)) };
43
65
 
44
66
  return (
45
67
  // The merchant's colors, read per request: a change in the admin shows after SHOP_UPDATED.
68
+ // The theme's global settings as data-* (lib/theme-settings.ts), read by app/styles.
46
69
  // Telegram's script (beforeInteractive) writes --tg-viewport-* into <html style> before React
47
70
  // hydrates; suppressHydrationWarning covers this element's attributes only, not its children.
48
- <html lang={locale} style={brandStyle(shop.branding)} suppressHydrationWarning>
71
+ <html
72
+ lang={locale}
73
+ className={onest.variable}
74
+ style={brandStyle(shop.branding)}
75
+ {...themeAttributes(themeSettings)}
76
+ suppressHydrationWarning
77
+ >
49
78
  <body>
50
79
  {/* Telegram's Mini App script, only for a shop with a bot: nobody else can open it in Telegram. */}
51
80
  {shop.features.telegramBot && (
52
81
  <Script src={TELEGRAM_WEB_APP_SCRIPT} strategy="beforeInteractive" />
53
82
  )}
54
- <Providers shop={shop}>
83
+ <Providers shop={shop} credentials={credentialsMode()} cartType={themeSettings.cartType}>
84
+ <a href="#main" className="skip-link">
85
+ {t.skipToContent}
86
+ </a>
55
87
  <TelegramShell />
56
88
  {announcement && <p className="site-announcement">{announcement}</p>}
57
- <header className="site">
58
- <nav>
59
- <Link href="/" className="brand">
60
- {shop.branding.logo ? (
61
- <Image data={shop.branding.logo} alt={shop.name} height={32} loading="eager" />
62
- ) : (
63
- shop.name
64
- )}
65
- </Link>
66
- {collections.map((collection) => (
67
- <Link key={collection.id} href={`/collections/${collection.handle}`}>
68
- {collection.title}
69
- </Link>
70
- ))}
71
- {/* Only when an article is readable in this language (`features.blog`). */}
72
- {shop.features.blog && <Link href="/blog">{t.blog}</Link>}
73
- <Link href="/search">{t.search}</Link>
74
- <Link href="/account">{t.account}</Link>
75
- <CartLink />
76
- </nav>
77
- </header>
78
- <main>{children}</main>
79
- {pages.length > 0 && (
80
- <footer className="site">
81
- <nav>
82
- {pages.map((page) => (
83
- <Link key={page.handle} href={`/pages/${page.handle}`}>
84
- {page.title}
85
- </Link>
86
- ))}
87
- </nav>
88
- </footer>
89
- )}
89
+ <SiteHeader
90
+ shop={shop}
91
+ nav={mainNav(mainMenu, collections)}
92
+ showContacts={contacts !== null && hasContacts(contacts)}
93
+ t={t}
94
+ />
95
+ <main id="main" className="site-main" tabIndex={-1}>
96
+ {children}
97
+ </main>
98
+ <SiteFooter
99
+ shop={shop}
100
+ nav={footerLinks}
101
+ collections={[
102
+ ...collections
103
+ .slice(0, 6)
104
+ .map((collection) => link(collection.title, `/collections/${collection.handle}`)),
105
+ ...(collections.length > 0 ? [link(t.seeAll, '/collections')] : []),
106
+ ]}
107
+ contacts={contacts}
108
+ t={t}
109
+ />
90
110
  </Providers>
91
111
  </body>
92
112
  </html>
93
113
  );
94
114
  }
115
+
116
+ function link(title: string, href: string): NavItem {
117
+ return { title, href, children: [] };
118
+ }