@usequeek/theme-kit 0.1.21 → 0.1.23

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 (86) hide show
  1. package/README.md +293 -117
  2. package/api/collections.ts +5 -5
  3. package/api/shop.ts +1 -1
  4. package/client.ts +2 -0
  5. package/components/checkout/checkout-contact.tsx +3 -3
  6. package/components/image.tsx +5 -5
  7. package/components/language-menu.css +349 -0
  8. package/components/language-menu.tsx +371 -0
  9. package/components/language-switcher.css +131 -0
  10. package/components/language-switcher.tsx +175 -0
  11. package/components/markdown.tsx +1 -1
  12. package/components/powered-by-queek.tsx +4 -4
  13. package/components/product-media-frame.tsx +1 -1
  14. package/components/product-metafields.tsx +2 -3
  15. package/components/variant-picker.tsx +6 -9
  16. package/demo.ts +10 -14
  17. package/design-token-preview.tsx +0 -2
  18. package/edit-mode-active.tsx +3 -4
  19. package/hooks/use-announcement.ts +1 -1
  20. package/hooks/use-auth-flow.tsx +1 -1
  21. package/hooks/use-locales.ts +107 -0
  22. package/hooks/use-posts.ts +1 -1
  23. package/hooks/use-product-detail.ts +1 -2
  24. package/hooks/use-product-sections.ts +4 -6
  25. package/hooks/use-products.ts +5 -8
  26. package/index.ts +13 -16
  27. package/locales/en.default.json +4 -0
  28. package/next-reference.d.ts +2 -4
  29. package/package.json +3 -2
  30. package/page-renderer.tsx +5 -6
  31. package/provider.tsx +22 -2
  32. package/scripts/verify-package.ts +6 -8
  33. package/sdk/client.ts +13 -12
  34. package/sdk/error-handler.ts +1 -1
  35. package/sdk/platform.ts +6 -8
  36. package/sdk/queek-client.ts +11 -12
  37. package/sdk/server-store-client.ts +13 -17
  38. package/shared-blocks/metaobjects.tsx +1 -1
  39. package/stores/cart-store.ts +2 -2
  40. package/stores/checkout-store.ts +1 -1
  41. package/stores/edit-preview-store.ts +5 -6
  42. package/stores/user-store.ts +4 -5
  43. package/strings/pseudo-locale.ts +3 -4
  44. package/strings/theme-strings.ts +9 -7
  45. package/tests/auth-email-errors.test.tsx +2 -2
  46. package/tests/currency-contract.test.ts +2 -3
  47. package/tests/display-format.test.ts +17 -25
  48. package/tests/fixtures/external-theme.tsx +23 -2
  49. package/tests/language-menu.test.tsx +598 -0
  50. package/tests/language-switcher.test.tsx +350 -0
  51. package/tests/list-pagination.test.tsx +8 -8
  52. package/tests/locale-switch.test.ts +197 -0
  53. package/tests/locale-transport.test.tsx +3 -3
  54. package/tests/markdown-render.test.ts +5 -6
  55. package/tests/menu-placement.test.ts +63 -0
  56. package/tests/orders-walk.test.ts +5 -5
  57. package/tests/page-renderer-signature.test.tsx +1 -1
  58. package/tests/powered-by-queek.test.tsx +3 -3
  59. package/tests/powered-by-url.test.ts +2 -2
  60. package/tests/public-surface.test.ts +1 -4
  61. package/tests/shopper-chrome-strings.test.tsx +5 -6
  62. package/tests/storefront-client-scope.test.ts +5 -5
  63. package/tests/theme-hook-placement.test.tsx +2 -2
  64. package/tests/theme-strings-client.test.tsx +5 -5
  65. package/tests/theme-strings-pseudo.test.tsx +1 -1
  66. package/tests/theme-strings-typed.test.tsx +1 -1
  67. package/tests/theme-strings.test.tsx +8 -11
  68. package/tests/use-shop-prefetch.test.tsx +1 -1
  69. package/theme-mount.tsx +6 -11
  70. package/types/block.ts +7 -7
  71. package/types/media.ts +8 -9
  72. package/types/product.ts +8 -12
  73. package/types/theme.ts +26 -25
  74. package/types/vendor.ts +10 -12
  75. package/utils/announcement.ts +3 -3
  76. package/utils/benefit-icon.tsx +9 -11
  77. package/utils/brand.ts +7 -10
  78. package/utils/format.ts +5 -7
  79. package/utils/image-srcset.ts +2 -2
  80. package/utils/locale-switch.ts +208 -0
  81. package/utils/locale.ts +10 -11
  82. package/utils/menu-link.ts +1 -1
  83. package/utils/menu-placement.ts +91 -0
  84. package/utils/product-media.ts +1 -1
  85. package/utils/product-normalizer.ts +2 -2
  86. package/utils/vendor-host.ts +7 -11
@@ -35,11 +35,11 @@ type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
35
35
  fallback?: string;
36
36
  placeholder?: PlaceholderProp;
37
37
  /**
38
- * The backend's srcset-ready tier map (product.media.image_variants,
38
+ * The API's srcset-ready tier map (product.media.image_variants,
39
39
  * vendor.logo_variants, collection.image_variants, mediaIndex[url], …).
40
- * Purely additive: when absent — as it is at every call site that hasn't been
41
- * migrated, and whenever the backend legitimately sends null (off-host image,
42
- * SVG, legacy upload) — this component behaves exactly as it did before.
40
+ * Purely additive: when absent — and whenever the API legitimately sends null
41
+ * (off-host image, SVG, legacy upload) — this component renders the plain
42
+ * image URL.
43
43
  */
44
44
  variants?: ImageVariants | null;
45
45
  /**
@@ -133,7 +133,7 @@ export function Image({ fallback, placeholder, variants, intent = 'raw', onError
133
133
  // image is invisible until hover. Eager, a server-rendered grid of 24 cards
134
134
  // made React preload the first ~10 (also into the 103 Early Hints header) and
135
135
  // fetch every image at once, racing the page's render-blocking CSS: /shop's
136
- // first paint went 1.8 s → 2.9 s on Slow 4G (24/9/26). Lazy is next/image's
136
+ // first paint went from 1.8 s to 2.9 s on Slow 4G. Lazy is next/image's
137
137
  // default as well. A theme whose card IS the page's main image passes
138
138
  // loading="eager" or fetchPriority="high".
139
139
  const lazyByDefault = (intent === 'card' || intent === 'card2x') && props.fetchPriority !== 'high';
@@ -0,0 +1,349 @@
1
+ /* ============================================================
2
+ LanguageSwitcher variant="menu" — a trigger and a list of language links:
3
+ a popover on wide screens, a bottom sheet below 640px. Prefix: qn-lang.
4
+
5
+ STYLING SURFACE. Every part has a class AND a data-part attribute:
6
+ root .qn-lang--menu [data-qn-language-switcher][data-variant="menu"]
7
+ trigger .qn-lang__trigger [data-part="trigger"]
8
+ label/code .qn-lang__trigger-label [data-part="trigger-label"]
9
+ .qn-lang__trigger-code [data-part="trigger-code"]
10
+ chevron .qn-lang__chevron [data-part="chevron"]
11
+ scrim .qn-lang__scrim [data-part="scrim"]
12
+ popover/sheet .qn-lang__popover [data-part="popover"]
13
+ grabber .qn-lang__handle [data-part="handle"]
14
+ group label .qn-lang__title [data-part="title"]
15
+ list .qn-lang__options [data-part="list"]
16
+ item .qn-lang__item [data-part="item"]
17
+ link .qn-lang__option [data-part="option"] (aria-current / data-current on the current one)
18
+ name/code .qn-lang__name / __code [data-part="name"] / [data-part="code"]
19
+ marker .qn-lang__mark [data-part="mark"]
20
+ State on the root: data-state="open|closed", data-side="bottom|top",
21
+ data-align="start|end", data-mode="popover|sheet" (while open),
22
+ data-count="<n>", data-many="true" (more than 10), data-show-code="true".
23
+
24
+ VARIABLES (--qn-lang-*): each is read as var(--qn-lang-x, var(--qn-token, fallback)),
25
+ and none is declared on the root, so set them on the root, an ancestor or :root.
26
+ surface ink muted border radius item-radius item-hover current-bg accent
27
+ focus shadow row-height sheet-row-height gap pad offset popover-width
28
+ many-width many-columns list-max sheet-radius scrim z duration ease
29
+ Logical properties only: it mirrors itself in right-to-left storefronts.
30
+ ============================================================ */
31
+
32
+ .qn-lang--menu {
33
+ position: relative;
34
+ display: inline-block;
35
+ max-inline-size: 100%;
36
+ }
37
+
38
+ /* ── trigger ───────────────────────────────────────────── */
39
+
40
+ .qn-lang__trigger {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ gap: 8px;
44
+ min-inline-size: 44px;
45
+ min-block-size: 44px;
46
+ padding-inline: 12px;
47
+ border: 1px solid var(--qn-lang-border, var(--qn-border, color-mix(in srgb, currentColor 22%, transparent)));
48
+ border-radius: var(--qn-lang-radius, var(--qn-radius-sm, 6px));
49
+ background: transparent;
50
+ color: var(--qn-lang-ink, var(--qn-text, inherit));
51
+ font: inherit;
52
+ line-height: 1.25;
53
+ white-space: nowrap;
54
+ cursor: pointer;
55
+ }
56
+
57
+ .qn-lang__trigger:hover {
58
+ border-color: var(--qn-lang-ink, var(--qn-text, currentColor));
59
+ }
60
+
61
+ .qn-lang__trigger:focus-visible,
62
+ .qn-lang__option:focus-visible {
63
+ outline: 2px solid var(--qn-lang-focus, var(--qn-focus, currentColor));
64
+ outline-offset: 2px;
65
+ }
66
+
67
+ .qn-lang__trigger-code,
68
+ .qn-lang__code {
69
+ display: none;
70
+ color: var(--qn-lang-muted, var(--qn-text-muted, color-mix(in srgb, currentColor 60%, transparent)));
71
+ font-size: 0.8em;
72
+ letter-spacing: 0.04em;
73
+ }
74
+
75
+ .qn-lang--menu[data-show-code='true'] .qn-lang__trigger-code,
76
+ .qn-lang--menu[data-show-code='true'] .qn-lang__code {
77
+ display: inline;
78
+ }
79
+
80
+ .qn-lang__trigger .qn-lang__chevron {
81
+ position: static;
82
+ inline-size: 10px;
83
+ block-size: 10px;
84
+ flex: none;
85
+ fill: none;
86
+ stroke: currentColor;
87
+ stroke-width: 1.8;
88
+ }
89
+
90
+ /* ── popover ───────────────────────────────────────────── */
91
+
92
+ .qn-lang__popover {
93
+ position: absolute;
94
+ z-index: var(--qn-lang-z, 50);
95
+ display: flex;
96
+ flex-direction: column;
97
+ box-sizing: border-box;
98
+ inline-size: var(--qn-lang-popover-width, 240px);
99
+ max-inline-size: calc(100vw - 32px);
100
+ max-block-size: var(--qn-lang-avail, none);
101
+ padding: var(--qn-lang-pad, 4px);
102
+ overflow: hidden;
103
+ border: 1px solid var(--qn-lang-border, var(--qn-border, color-mix(in srgb, currentColor 16%, transparent)));
104
+ border-radius: var(--qn-lang-radius, var(--qn-radius-md, 8px));
105
+ background: var(--qn-lang-surface, var(--qn-surface, Canvas));
106
+ color: var(--qn-lang-ink, var(--qn-text, CanvasText));
107
+ box-shadow: var(--qn-lang-shadow, 0 4px 16px rgb(0 0 0 / 0.08));
108
+ font: inherit;
109
+ text-align: start;
110
+ translate: var(--qn-lang-shift, 0px) 0;
111
+ }
112
+
113
+ .qn-lang__popover[hidden],
114
+ .qn-lang__scrim[hidden] {
115
+ display: none;
116
+ }
117
+
118
+ .qn-lang--menu[data-side='bottom'] > .qn-lang__popover {
119
+ inset-block-start: calc(100% + var(--qn-lang-offset, 8px));
120
+ }
121
+
122
+ .qn-lang--menu[data-side='top'] > .qn-lang__popover {
123
+ inset-block-end: calc(100% + var(--qn-lang-offset, 8px));
124
+ }
125
+
126
+ .qn-lang--menu[data-align='end'] > .qn-lang__popover {
127
+ inset-inline-end: 0;
128
+ }
129
+
130
+ .qn-lang--menu[data-align='start'] > .qn-lang__popover {
131
+ inset-inline-start: 0;
132
+ }
133
+
134
+ .qn-lang--menu[data-many='true'] > .qn-lang__popover {
135
+ inline-size: var(--qn-lang-many-width, var(--qn-lang-popover-width, 240px));
136
+ }
137
+
138
+ .qn-lang__handle,
139
+ .qn-lang__scrim {
140
+ display: none;
141
+ }
142
+
143
+ .qn-lang__title {
144
+ padding: 8px 12px 6px;
145
+ color: var(--qn-lang-muted, var(--qn-text-muted, color-mix(in srgb, currentColor 60%, transparent)));
146
+ font-size: 0.75em;
147
+ font-weight: 600;
148
+ letter-spacing: 0.06em;
149
+ text-transform: uppercase;
150
+ }
151
+
152
+ /* ── list ──────────────────────────────────────────────── */
153
+
154
+ .qn-lang__options {
155
+ display: grid;
156
+ grid-template-columns: repeat(var(--qn-lang-many-columns, 1), minmax(0, 1fr));
157
+ gap: var(--qn-lang-gap, 0);
158
+ flex: 1 1 auto;
159
+ min-block-size: 0;
160
+ max-block-size: var(--qn-lang-list-max, calc(var(--qn-lang-row-height, 44px) * 8.5));
161
+ margin: 0;
162
+ padding: 0;
163
+ overflow-y: auto;
164
+ overscroll-behavior: contain;
165
+ list-style: none;
166
+ }
167
+
168
+ /* The extra columns only apply to long lists. */
169
+ .qn-lang--menu:not([data-many='true']) .qn-lang__options {
170
+ grid-template-columns: minmax(0, 1fr);
171
+ }
172
+
173
+ .qn-lang__item {
174
+ display: block;
175
+ margin: 0;
176
+ padding: 0;
177
+ }
178
+
179
+ .qn-lang__option {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 12px;
183
+ box-sizing: border-box;
184
+ min-block-size: var(--qn-lang-row-height, 44px);
185
+ padding-inline: 12px;
186
+ border-radius: var(--qn-lang-item-radius, var(--qn-radius-sm, 6px));
187
+ color: inherit;
188
+ font: inherit;
189
+ text-decoration: none;
190
+ cursor: pointer;
191
+ }
192
+
193
+ .qn-lang__option:hover {
194
+ background: var(--qn-lang-item-hover, color-mix(in srgb, currentColor 6%, transparent));
195
+ }
196
+
197
+ .qn-lang__option[aria-current='true'] {
198
+ background: var(--qn-lang-current-bg, transparent);
199
+ font-weight: 600;
200
+ }
201
+
202
+ .qn-lang__option:focus-visible {
203
+ outline-offset: -2px;
204
+ }
205
+
206
+ .qn-lang__name {
207
+ flex: 1 1 auto;
208
+ min-inline-size: 0;
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ white-space: nowrap;
212
+ }
213
+
214
+ .qn-lang__code {
215
+ flex: none;
216
+ }
217
+
218
+ .qn-lang__mark {
219
+ display: inline-flex;
220
+ inline-size: 16px;
221
+ block-size: 16px;
222
+ flex: none;
223
+ color: var(--qn-lang-accent, var(--qn-accent, currentColor));
224
+ visibility: hidden;
225
+ }
226
+
227
+ .qn-lang__option[aria-current='true'] .qn-lang__mark {
228
+ visibility: visible;
229
+ }
230
+
231
+ .qn-lang__mark svg {
232
+ inline-size: 100%;
233
+ block-size: 100%;
234
+ fill: none;
235
+ stroke: currentColor;
236
+ stroke-width: 1.8;
237
+ stroke-linecap: round;
238
+ stroke-linejoin: round;
239
+ }
240
+
241
+ /* ── motion ────────────────────────────────────────────── */
242
+
243
+ @keyframes qn-lang-pop-down {
244
+ from { opacity: 0; transform: translateY(-4px); }
245
+ to { opacity: 1; transform: none; }
246
+ }
247
+
248
+ @keyframes qn-lang-pop-up {
249
+ from { opacity: 0; transform: translateY(4px); }
250
+ to { opacity: 1; transform: none; }
251
+ }
252
+
253
+ @keyframes qn-lang-sheet-in {
254
+ from { transform: translateY(100%); }
255
+ to { transform: none; }
256
+ }
257
+
258
+ @keyframes qn-lang-fade-in {
259
+ from { opacity: 0; }
260
+ to { opacity: 1; }
261
+ }
262
+
263
+ .qn-lang--menu[data-side='bottom'] > .qn-lang__popover:not([hidden]) {
264
+ animation: qn-lang-pop-down var(--qn-lang-duration, 160ms) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
265
+ }
266
+
267
+ .qn-lang--menu[data-side='top'] > .qn-lang__popover:not([hidden]) {
268
+ animation: qn-lang-pop-up var(--qn-lang-duration, 160ms) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
269
+ }
270
+
271
+ @media (prefers-reduced-motion: no-preference) {
272
+ .qn-lang__chevron {
273
+ transition: rotate var(--qn-lang-duration, 160ms) var(--qn-lang-ease, ease);
274
+ }
275
+ }
276
+
277
+ .qn-lang--menu[data-state='open'] .qn-lang__trigger .qn-lang__chevron {
278
+ rotate: 180deg;
279
+ }
280
+
281
+ /* ── bottom sheet (narrow screens) ─────────────────────── */
282
+
283
+ @media (max-width: 639.98px) {
284
+ .qn-lang__scrim:not([hidden]) {
285
+ position: fixed;
286
+ inset: 0;
287
+ z-index: calc(var(--qn-lang-z, 50) - 1);
288
+ display: block;
289
+ background: var(--qn-lang-scrim, rgb(0 0 0 / 0.4));
290
+ animation: qn-lang-fade-in var(--qn-lang-duration, 160ms) linear;
291
+ }
292
+
293
+ /* [data-side][data-align] outranks the popover placement rules above. */
294
+ .qn-lang--menu[data-side][data-align] > .qn-lang__popover {
295
+ position: fixed;
296
+ inset-block: auto 0;
297
+ inset-inline: 0;
298
+ inline-size: auto;
299
+ max-inline-size: none;
300
+ max-block-size: 85vh;
301
+ max-block-size: 85dvh;
302
+ padding: var(--qn-lang-pad, 4px) var(--qn-lang-pad, 4px) calc(var(--qn-lang-pad, 4px) + env(safe-area-inset-bottom, 0px));
303
+ border-inline-width: 0;
304
+ border-block-end-width: 0;
305
+ border-start-start-radius: var(--qn-lang-sheet-radius, 16px);
306
+ border-start-end-radius: var(--qn-lang-sheet-radius, 16px);
307
+ border-end-start-radius: 0;
308
+ border-end-end-radius: 0;
309
+ translate: none;
310
+ }
311
+
312
+ .qn-lang--menu[data-side][data-align] > .qn-lang__popover:not([hidden]) {
313
+ animation: qn-lang-sheet-in calc(var(--qn-lang-duration, 160ms) * 1.5) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
314
+ }
315
+
316
+ .qn-lang__handle {
317
+ display: block;
318
+ inline-size: 36px;
319
+ block-size: 4px;
320
+ margin: 8px auto 4px;
321
+ border-radius: 2px;
322
+ background: color-mix(in srgb, currentColor 25%, transparent);
323
+ }
324
+
325
+ .qn-lang--menu .qn-lang__options,
326
+ .qn-lang--menu:not([data-many='true']) .qn-lang__options {
327
+ grid-template-columns: minmax(0, 1fr);
328
+ max-block-size: none;
329
+ }
330
+
331
+ .qn-lang__option {
332
+ min-block-size: var(--qn-lang-sheet-row-height, 52px);
333
+ }
334
+ }
335
+
336
+ /* Page scroll is locked behind an open sheet. */
337
+ body.qn-lang-lock {
338
+ overflow: hidden;
339
+ }
340
+
341
+ @media (prefers-reduced-motion: reduce) {
342
+ .qn-lang--menu[data-side][data-align] > .qn-lang__popover:not([hidden]) {
343
+ animation: qn-lang-fade-in 80ms linear;
344
+ }
345
+
346
+ .qn-lang__scrim:not([hidden]) {
347
+ animation-duration: 80ms;
348
+ }
349
+ }