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
@@ -0,0 +1,55 @@
1
+ @layer reset {
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ }
7
+ html {
8
+ -webkit-text-size-adjust: 100%;
9
+ text-size-adjust: 100%;
10
+ }
11
+ body,
12
+ h1,
13
+ h2,
14
+ h3,
15
+ h4,
16
+ p,
17
+ figure,
18
+ blockquote,
19
+ dl,
20
+ dd {
21
+ margin: 0;
22
+ }
23
+ img,
24
+ svg,
25
+ video {
26
+ display: block;
27
+ max-width: 100%;
28
+ }
29
+ input,
30
+ button,
31
+ select,
32
+ textarea {
33
+ font: inherit;
34
+ color: inherit;
35
+ }
36
+ button {
37
+ cursor: pointer;
38
+ }
39
+ fieldset {
40
+ margin: 0;
41
+ padding: 0;
42
+ border: 0;
43
+ min-width: 0;
44
+ }
45
+ legend {
46
+ padding: 0;
47
+ }
48
+ dialog {
49
+ padding: 0;
50
+ border: 0;
51
+ max-width: none;
52
+ max-height: none;
53
+ color: inherit;
54
+ }
55
+ }
@@ -0,0 +1,415 @@
1
+ /* Theme sections (theme/sections/) and GET /home renderers (components/sections/). */
2
+ @layer sections {
3
+ .section-header {
4
+ display: flex;
5
+ flex-wrap: wrap;
6
+ align-items: baseline;
7
+ justify-content: space-between;
8
+ gap: var(--space-2) var(--space-4);
9
+ margin-bottom: var(--space-5);
10
+ }
11
+ .section-header__link {
12
+ font-size: var(--text-sm);
13
+ font-weight: var(--weight-medium);
14
+ }
15
+
16
+ /* Hero (Dawn's image banner): the text over the image, or below it (`stacked`, and on phones
17
+ with `stack-mobile`); without an image, the text alone on a quiet fill. */
18
+ .hero {
19
+ --hero-h: 26rem;
20
+ display: grid;
21
+ overflow: hidden;
22
+ border-radius: var(--radius-lg);
23
+ background: var(--subtle);
24
+ }
25
+ .hero--small {
26
+ --hero-h: 20rem;
27
+ }
28
+ .hero--large {
29
+ --hero-h: 32rem;
30
+ }
31
+ @media (min-width: 750px) {
32
+ .hero {
33
+ --hero-h: 34rem;
34
+ }
35
+ .hero--small {
36
+ --hero-h: 26rem;
37
+ }
38
+ .hero--large {
39
+ --hero-h: 44rem;
40
+ }
41
+ }
42
+ .hero__media {
43
+ position: relative;
44
+ min-height: var(--hero-h);
45
+ }
46
+ .hero__media img {
47
+ position: absolute;
48
+ inset: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ object-fit: cover;
52
+ }
53
+ /* As the image: its own proportions, nothing cropped. */
54
+ .hero--adapt .hero__media {
55
+ min-height: 0;
56
+ }
57
+ .hero--adapt .hero__media img {
58
+ position: static;
59
+ height: auto;
60
+ }
61
+ .hero__content {
62
+ display: flex;
63
+ flex-direction: column;
64
+ justify-content: flex-end;
65
+ gap: var(--space-4);
66
+ padding: var(--space-8) var(--gutter);
67
+ }
68
+ @media (min-width: 990px) {
69
+ .hero__content {
70
+ padding: var(--space-9) var(--space-8);
71
+ }
72
+ }
73
+ .hero--top .hero__content {
74
+ justify-content: flex-start;
75
+ }
76
+ .hero--middle .hero__content {
77
+ justify-content: center;
78
+ }
79
+ .hero__heading {
80
+ max-width: 22ch;
81
+ font-size: var(--text-4xl);
82
+ }
83
+ .hero__text {
84
+ max-width: 46ch;
85
+ font-size: var(--text-lg);
86
+ }
87
+ .hero__content .button {
88
+ align-self: flex-start;
89
+ }
90
+ .hero--center .hero__content {
91
+ align-items: center;
92
+ text-align: center;
93
+ }
94
+ .hero--center .hero__content .button {
95
+ align-self: center;
96
+ }
97
+
98
+ /* Text over the image: on a shade the merchant sets (`overlayOpacity`). */
99
+ .hero--overlay .hero__media,
100
+ .hero--overlay .hero__content {
101
+ grid-area: 1 / 1;
102
+ }
103
+ .hero--overlay .hero__media::after {
104
+ content: '';
105
+ position: absolute;
106
+ inset: 0;
107
+ background: rgb(0 0 0 / var(--hero-overlay, 0.4));
108
+ }
109
+ .hero--overlay .hero__content {
110
+ position: relative;
111
+ color: #fff;
112
+ }
113
+ .hero--overlay .hero__heading,
114
+ .hero--overlay .hero__text {
115
+ text-shadow: 0 1px 12px rgb(0 0 0 / 0.25);
116
+ }
117
+
118
+ /* Text below the image: the banner keeps its own lettering readable. */
119
+ .hero--stacked {
120
+ background: none;
121
+ }
122
+ .hero--stacked .hero__media {
123
+ overflow: hidden;
124
+ border-radius: var(--radius-lg);
125
+ }
126
+ .hero--stacked .hero__content {
127
+ padding: var(--space-5) 0 0;
128
+ }
129
+ @media (max-width: 749px) {
130
+ .hero--stack-mobile.hero--overlay {
131
+ background: none;
132
+ }
133
+ .hero--stack-mobile.hero--overlay .hero__media {
134
+ grid-area: auto;
135
+ overflow: hidden;
136
+ border-radius: var(--radius-lg);
137
+ }
138
+ .hero--stack-mobile.hero--overlay .hero__media::after {
139
+ content: none;
140
+ }
141
+ .hero--stack-mobile.hero--overlay .hero__content {
142
+ grid-area: auto;
143
+ padding: var(--space-5) 0 0;
144
+ color: inherit;
145
+ }
146
+ .hero--stack-mobile .hero__heading,
147
+ .hero--stack-mobile .hero__text {
148
+ text-shadow: none;
149
+ }
150
+ /* Stacked on a phone, the image keeps its proportions: a banner is seldom made for 4:5. */
151
+ .hero--stack-mobile .hero__media {
152
+ min-height: 0;
153
+ }
154
+ .hero--stack-mobile .hero__media img {
155
+ position: static;
156
+ height: auto;
157
+ }
158
+ }
159
+
160
+ /* Rich text */
161
+ .rich-text {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: var(--space-4);
165
+ }
166
+ .rich-text--narrow {
167
+ max-width: 48rem;
168
+ margin-inline: auto;
169
+ }
170
+ .rich-text--center {
171
+ align-items: center;
172
+ text-align: center;
173
+ }
174
+
175
+ /* Image with text */
176
+ .image-with-text {
177
+ display: grid;
178
+ gap: var(--space-5);
179
+ align-items: center;
180
+ }
181
+ @media (min-width: 750px) {
182
+ .image-with-text {
183
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
184
+ gap: var(--space-8);
185
+ }
186
+ .image-with-text--image-right .image-with-text__media {
187
+ order: 2;
188
+ }
189
+ }
190
+ .image-with-text__media {
191
+ overflow: hidden;
192
+ border-radius: var(--radius-md);
193
+ background: var(--placeholder);
194
+ }
195
+ .image-with-text__media img {
196
+ width: 100%;
197
+ height: auto;
198
+ }
199
+ .image-with-text--portrait .image-with-text__media {
200
+ aspect-ratio: 4 / 5;
201
+ }
202
+ .image-with-text--square .image-with-text__media {
203
+ aspect-ratio: 1;
204
+ }
205
+ .image-with-text--landscape .image-with-text__media {
206
+ aspect-ratio: 4 / 3;
207
+ }
208
+ .image-with-text:not(.image-with-text--adapt) .image-with-text__media img {
209
+ height: 100%;
210
+ object-fit: cover;
211
+ }
212
+ .image-with-text__text {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: var(--space-4);
216
+ }
217
+ .image-with-text__text .button {
218
+ align-self: flex-start;
219
+ }
220
+
221
+ /* Testimonials (merchant-entered quotes) and store reviews (GET /reviews/site). */
222
+ .testimonials__grid,
223
+ .review-grid {
224
+ display: grid;
225
+ grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
226
+ gap: var(--space-4);
227
+ }
228
+ .testimonial,
229
+ .review-card {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: var(--space-3);
233
+ margin: 0;
234
+ padding: var(--space-5);
235
+ border-radius: var(--radius-md);
236
+ background: var(--subtle);
237
+ }
238
+ .testimonial__text {
239
+ margin: 0;
240
+ font-size: var(--text-lg);
241
+ line-height: var(--leading-snug);
242
+ }
243
+
244
+ /* Banner slider (GET /home BANNER) */
245
+ .banner-slider {
246
+ display: grid;
247
+ grid-auto-flow: column;
248
+ grid-auto-columns: 100%;
249
+ gap: var(--space-4);
250
+ overflow-x: auto;
251
+ scroll-snap-type: x mandatory;
252
+ scrollbar-width: none;
253
+ }
254
+ .banner-slider > * {
255
+ scroll-snap-align: start;
256
+ }
257
+ .banner-slider img {
258
+ width: 100%;
259
+ height: auto;
260
+ border-radius: var(--radius-md);
261
+ }
262
+
263
+ /* Deal of the day */
264
+ .deal-of-day {
265
+ display: grid;
266
+ gap: var(--space-5);
267
+ padding: var(--space-5);
268
+ border: 1px solid var(--accent);
269
+ border-radius: var(--radius-lg);
270
+ }
271
+ @media (min-width: 750px) {
272
+ .deal-of-day {
273
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
274
+ align-items: center;
275
+ }
276
+ }
277
+ .deal-of-day img,
278
+ .deal-of-day .media-fallback {
279
+ width: 100%;
280
+ height: auto;
281
+ aspect-ratio: var(--media-ratio);
282
+ object-fit: cover;
283
+ border-radius: var(--radius-md);
284
+ }
285
+
286
+ /* Stories */
287
+ .stories {
288
+ display: flex;
289
+ gap: var(--space-4);
290
+ overflow-x: auto;
291
+ scrollbar-width: none;
292
+ }
293
+ .story {
294
+ display: flex;
295
+ flex: 0 0 5.5rem;
296
+ flex-direction: column;
297
+ align-items: center;
298
+ gap: var(--space-2);
299
+ font-size: var(--text-xs);
300
+ text-align: center;
301
+ text-decoration: none;
302
+ }
303
+ .story img,
304
+ .story .media-fallback {
305
+ width: 5rem;
306
+ height: 5rem;
307
+ object-fit: cover;
308
+ border-radius: var(--radius-md);
309
+ background: var(--placeholder);
310
+ }
311
+ .stories--circle .story img,
312
+ .stories--circle .story .media-fallback {
313
+ border-radius: 50%;
314
+ }
315
+
316
+ /* Announcement bar (GET /home) */
317
+ .announcement-bar {
318
+ margin: 0;
319
+ padding: var(--space-3) var(--space-4);
320
+ overflow: hidden;
321
+ border-radius: var(--radius-md);
322
+ background: var(--fg);
323
+ color: var(--bg);
324
+ text-align: center;
325
+ white-space: nowrap;
326
+ }
327
+ .announcement-bar--ltr span,
328
+ .announcement-bar--rtl span {
329
+ display: inline-block;
330
+ animation: marquee 18s linear infinite;
331
+ }
332
+ .announcement-bar--rtl span {
333
+ animation-direction: reverse;
334
+ }
335
+ @keyframes marquee {
336
+ from {
337
+ transform: translateX(-100%);
338
+ }
339
+ to {
340
+ transform: translateX(100%);
341
+ }
342
+ }
343
+ @media (prefers-reduced-motion: reduce) {
344
+ .announcement-bar span {
345
+ animation: none;
346
+ }
347
+ }
348
+
349
+ /* Multicolumn: the merchant's columns; a sideways row on phones. */
350
+ .multicolumn__list {
351
+ display: grid;
352
+ grid-auto-flow: column;
353
+ grid-auto-columns: 78%;
354
+ gap: var(--space-4);
355
+ margin: 0 calc(var(--gutter) * -1);
356
+ padding: 0 var(--gutter) var(--space-2);
357
+ overflow-x: auto;
358
+ list-style: none;
359
+ scroll-snap-type: x mandatory;
360
+ scroll-padding-inline: var(--gutter);
361
+ scrollbar-width: none;
362
+ }
363
+ .multicolumn__list::-webkit-scrollbar {
364
+ display: none;
365
+ }
366
+ @media (min-width: 750px) {
367
+ .multicolumn__list {
368
+ grid-auto-flow: row;
369
+ grid-template-columns: repeat(var(--columns, 3), minmax(0, 1fr));
370
+ gap: var(--space-6);
371
+ margin: 0;
372
+ padding: 0;
373
+ overflow: visible;
374
+ }
375
+ }
376
+ .multicolumn__item {
377
+ display: flex;
378
+ flex-direction: column;
379
+ gap: var(--space-3);
380
+ scroll-snap-align: start;
381
+ }
382
+ .multicolumn--center .multicolumn__item {
383
+ align-items: center;
384
+ text-align: center;
385
+ }
386
+ .multicolumn--center .section-header {
387
+ justify-content: center;
388
+ }
389
+ .multicolumn__media {
390
+ overflow: hidden;
391
+ aspect-ratio: 3 / 2;
392
+ border-radius: var(--radius-md);
393
+ background: var(--placeholder);
394
+ }
395
+ .multicolumn--center .multicolumn__media {
396
+ align-self: stretch;
397
+ }
398
+ .multicolumn__media img {
399
+ width: 100%;
400
+ height: 100%;
401
+ object-fit: cover;
402
+ }
403
+ .multicolumn__heading {
404
+ font-size: var(--text-lg);
405
+ }
406
+ .multicolumn__link {
407
+ font-weight: var(--weight-medium);
408
+ }
409
+
410
+ /* Collapsible content (a FAQ) */
411
+ .collapsible-content {
412
+ max-width: 48rem;
413
+ margin-inline: auto;
414
+ }
415
+ }
@@ -0,0 +1,167 @@
1
+ /*
2
+ * Design tokens (DESIGN.md). Components use only these, never a raw color, size or radius.
3
+ * Brand tokens are overridden per request by <html style> with the merchant's colors (lib/brand.ts);
4
+ * the values here are the design's fallbacks for a color the merchant left empty.
5
+ */
6
+ @layer tokens {
7
+ :root {
8
+ /* Neutrals: one cool grey family. */
9
+ --bg: #fff;
10
+ --fg: #14161a;
11
+ --muted: #5b616e;
12
+ --line: #e3e5e9;
13
+ --line-strong: #c9cdd4;
14
+ --danger: #c62828;
15
+ --success: #1a7f37;
16
+ --warning: #b45309;
17
+ /* Controls and image placeholders: white and light grey here, Telegram's colors in a Mini App. */
18
+ --surface: #fff;
19
+ --placeholder: #f2f3f5;
20
+ --subtle: #f6f7f8;
21
+ --overlay: rgb(20 22 26 / 0.5);
22
+
23
+ /* Brand: app/layout.tsx overrides them with the merchant's colors (lib/brand.ts). */
24
+ --accent: #1f5eff;
25
+ --on-accent: #fff;
26
+ /* The accent as text on the page: half way to the text color keeps AA (≥ 4.5) for any accent,
27
+ pale or dark, in a light and a dark scheme. */
28
+ --accent-text: color-mix(in oklab, var(--accent) 50%, var(--fg));
29
+ --nav-bg: var(--bg);
30
+ --nav-fg: var(--fg);
31
+ --label-bg: var(--accent);
32
+ --label-fg: var(--on-accent);
33
+
34
+ /* Spacing: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128. */
35
+ --space-1: 0.25rem;
36
+ --space-2: 0.5rem;
37
+ --space-3: 0.75rem;
38
+ --space-4: 1rem;
39
+ --space-5: 1.5rem;
40
+ --space-6: 2rem;
41
+ --space-7: 3rem;
42
+ --space-8: 4rem;
43
+ --space-9: 6rem;
44
+ --space-10: 8rem;
45
+
46
+ /* Type: one family (app/layout.tsx sets --font-onest); sizes from xl up are fluid. */
47
+ --font-body:
48
+ var(--font-onest, system-ui), system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
49
+ --font-heading: var(--font-body);
50
+ --text-xs: 0.75rem;
51
+ --text-sm: 0.875rem;
52
+ --text-base: 1rem;
53
+ --text-lg: 1.125rem;
54
+ --text-xl: clamp(1.25rem, 1.15rem + 0.4vw, 1.375rem);
55
+ --text-2xl: clamp(1.375rem, 1.2rem + 0.8vw, 1.75rem);
56
+ --text-3xl: clamp(1.75rem, 1.4rem + 1.4vw, 2.5rem);
57
+ --text-4xl: clamp(2.125rem, 1.6rem + 2.4vw, 3.5rem);
58
+ --leading-tight: 1.15;
59
+ --leading-snug: 1.3;
60
+ --leading-normal: 1.55;
61
+ --weight-regular: 400;
62
+ --weight-medium: 500;
63
+ --weight-heading: 600;
64
+
65
+ /* Shape: controls, cards and media, panels. data-radius (theme setting) picks the scale. */
66
+ --radius-sm: 4px;
67
+ --radius-md: 6px;
68
+ --radius-lg: 10px;
69
+ --radius-pill: 999px;
70
+
71
+ /* Depth: tinted with the foreground, never pure black. */
72
+ --shadow-1: 0 1px 2px rgb(20 22 26 / 0.06), 0 1px 3px rgb(20 22 26 / 0.08);
73
+ --shadow-2: 0 16px 40px -12px rgb(20 22 26 / 0.28), 0 2px 6px rgb(20 22 26 / 0.08);
74
+
75
+ /* Layout. Breakpoints (mobile first): 750px and 990px. */
76
+ --container: 1320px;
77
+ --gutter: 1rem;
78
+ --header-h: 3.75rem;
79
+ --section-gap: var(--space-7);
80
+
81
+ /* Layers: nothing else sets a z-index above 1. */
82
+ --z-header: 20;
83
+ --z-drawer: 40;
84
+ --z-toast: 50;
85
+
86
+ /* Motion. */
87
+ --duration-fast: 150ms;
88
+ --duration: 250ms;
89
+ --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
90
+
91
+ /* Product media ratio: data-image-ratio (theme setting). */
92
+ --media-ratio: 3 / 4;
93
+
94
+ font-family: var(--font-body);
95
+ color: var(--fg);
96
+ background: var(--bg);
97
+ }
98
+
99
+ @media (min-width: 750px) {
100
+ :root {
101
+ --gutter: 2rem;
102
+ --section-gap: var(--space-8);
103
+ }
104
+ }
105
+ @media (min-width: 990px) {
106
+ :root {
107
+ --gutter: 3rem;
108
+ --header-h: 4.5rem;
109
+ }
110
+ }
111
+ @media (prefers-reduced-motion: reduce) {
112
+ :root {
113
+ --duration-fast: 0ms;
114
+ --duration: 0ms;
115
+ }
116
+ }
117
+
118
+ /* Theme settings (theme/settings.ts), mapped to data-* on <html> by app/layout.tsx. */
119
+ :root[data-radius='none'] {
120
+ --radius-sm: 0;
121
+ --radius-md: 0;
122
+ --radius-lg: 0;
123
+ }
124
+ :root[data-radius='medium'] {
125
+ --radius-sm: 8px;
126
+ --radius-md: 12px;
127
+ --radius-lg: 16px;
128
+ }
129
+ :root[data-radius='large'] {
130
+ --radius-sm: 12px;
131
+ --radius-md: 20px;
132
+ --radius-lg: 28px;
133
+ }
134
+ :root[data-image-ratio='square'] {
135
+ --media-ratio: 1;
136
+ }
137
+ :root[data-image-ratio='natural'] {
138
+ --media-ratio: auto;
139
+ }
140
+
141
+ /*
142
+ * Inside a Telegram Mini App (components/telegram-shell.tsx marks <html>): Telegram's theme, which
143
+ * its script writes as --tg-theme-* on <html>, with the defaults above as fallbacks. The merchant's
144
+ * accent from <html style> (lib/brand.ts) still wins over Telegram's button color.
145
+ */
146
+ html[data-telegram] {
147
+ --bg: var(--tg-theme-bg-color, #fff);
148
+ --fg: var(--tg-theme-text-color, #14161a);
149
+ --muted: var(--tg-theme-hint-color, #5b616e);
150
+ --danger: var(--tg-theme-destructive-text-color, #c62828);
151
+ --surface: var(--tg-theme-secondary-bg-color, #fff);
152
+ --placeholder: var(--tg-theme-secondary-bg-color, #f2f3f5);
153
+ --subtle: var(--tg-theme-secondary-bg-color, #f6f7f8);
154
+ --accent: var(--tg-theme-button-color, #1f5eff);
155
+ --on-accent: var(--tg-theme-button-text-color, #fff);
156
+ }
157
+ html[data-telegram][data-color-scheme='dark'] {
158
+ --line: rgb(255 255 255 / 0.14);
159
+ --line-strong: rgb(255 255 255 / 0.28);
160
+ --overlay: rgb(0 0 0 / 0.6);
161
+ /* Status colors light enough for AA on Telegram's dark backgrounds. */
162
+ --danger: var(--tg-theme-destructive-text-color, #ff6b6b);
163
+ --success: #3fb950;
164
+ --warning: #f0a33a;
165
+ color-scheme: dark;
166
+ }
167
+ }
@@ -0,0 +1,37 @@
1
+ /* Small single-purpose helpers; they win over every other layer. */
2
+ @layer utilities {
3
+ .stack {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--space-3);
7
+ }
8
+ .stack--loose {
9
+ gap: var(--space-5);
10
+ }
11
+ .row {
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ align-items: center;
15
+ gap: var(--space-3);
16
+ }
17
+ .muted {
18
+ color: var(--muted);
19
+ }
20
+ .error {
21
+ color: var(--danger);
22
+ }
23
+ .text-sm {
24
+ font-size: var(--text-sm);
25
+ }
26
+ .visually-hidden {
27
+ position: absolute !important;
28
+ width: 1px;
29
+ height: 1px;
30
+ margin: -1px;
31
+ padding: 0;
32
+ overflow: hidden;
33
+ clip: rect(0 0 0 0);
34
+ white-space: nowrap;
35
+ border: 0;
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ import type { Metadata } from 'next';
2
+ import { notFound } from 'next/navigation';
3
+ import { WishlistView } from '@/components/wishlist/wishlist-view';
4
+ import { api, locale } from '@/lib/api';
5
+ import { messages } from '@/lib/i18n';
6
+
7
+ // Personal: nothing here for a search engine.
8
+ export const metadata: Metadata = {
9
+ title: messages(locale).wishlist,
10
+ robots: { index: false, follow: false },
11
+ };
12
+
13
+ /** The wishlist, only for a shop that has one (`features.wishlist`); the list lives in the browser. */
14
+ export default async function WishlistPage() {
15
+ const { data: shop } = await api.shop();
16
+ if (!shop.features.wishlist) {
17
+ notFound();
18
+ }
19
+ const t = messages(locale);
20
+ return (
21
+ <>
22
+ <h1 className="page-title">{t.wishlist}</h1>
23
+ <WishlistView />
24
+ </>
25
+ );
26
+ }