@base44/app-plugin-commerce 0.9.4 → 0.10.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.
- package/README.md +6 -3
- package/base44/functions/commerce/storefront-catalog/entry.ts +5 -0
- package/package.json +1 -1
- package/scripts/install.js +3 -0
- package/skills/commerce/SKILL.md +36 -39
- package/skills/commerce/docs/api-storefront.md +1 -1
- package/skills/commerce/installation/install.md +76 -165
- package/skills/commerce/references/admin-localization.md +6 -4
- package/skills/commerce/references/storefront-ui.md +154 -0
- package/src/commerce/storefront/index.js +6 -3
- package/src/commerce/storefront-ui/CartButton.jsx +33 -0
- package/src/commerce/storefront-ui/CartPage.jsx +140 -0
- package/src/commerce/storefront-ui/CheckoutPage.jsx +287 -0
- package/src/commerce/storefront-ui/MiniCart.jsx +128 -0
- package/src/commerce/storefront-ui/OrderReceivedPage.jsx +159 -0
- package/src/commerce/storefront-ui/i18n/index.js +51 -0
- package/src/commerce/storefront-ui/i18n/locales/de.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/en.js +100 -0
- package/src/commerce/storefront-ui/i18n/locales/es.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/fr.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/ja.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/pt.js +89 -0
- package/src/commerce/storefront-ui/index.js +16 -0
- package/src/commerce/storefront-ui/internal.jsx +218 -0
- package/src/commerce/storefront-ui/storefront-ui.css +448 -0
- package/src/commerce/storefront-ui/theme.js +76 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storefront-ui — the shipped Cart / MiniCart / Checkout / OrderReceived styles.
|
|
3
|
+
*
|
|
4
|
+
* Plain CSS on purpose: no Tailwind classes, no @apply, no postcss features, so
|
|
5
|
+
* these surfaces are immune to the app's build configuration. Every color is a
|
|
6
|
+
* PAIR (surface + on-surface); a component never uses one side without the
|
|
7
|
+
* other, which is what makes unreadable text structurally impossible. The
|
|
8
|
+
* `--sfui-*` tokens are the store's override surface — set them once in
|
|
9
|
+
* index.css, always in pairs. Unset, everything resolves through the app
|
|
10
|
+
* template's shadcn variables down to safe literals.
|
|
11
|
+
*
|
|
12
|
+
* Body font is inherited from the page (never set here), so typography follows
|
|
13
|
+
* the site automatically; only the heading font is a token.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.sfui {
|
|
17
|
+
/* paired color tokens — override via --sfui-*, defaults follow the app theme */
|
|
18
|
+
--_surface: var(--sfui-surface, hsl(var(--background, 0 0% 100%)));
|
|
19
|
+
--_on-surface: var(--sfui-on-surface, hsl(var(--foreground, 224 10% 10%)));
|
|
20
|
+
--_subtle: var(--sfui-subtle, hsl(var(--muted, 220 14% 96%)));
|
|
21
|
+
--_on-subtle: var(--sfui-on-subtle, hsl(var(--muted-foreground, 220 9% 40%)));
|
|
22
|
+
--_accent: var(--sfui-accent, hsl(var(--primary, 224 71% 10%)));
|
|
23
|
+
--_on-accent: var(--sfui-on-accent, hsl(var(--primary-foreground, 0 0% 100%)));
|
|
24
|
+
--_danger: var(--sfui-danger, hsl(var(--destructive, 0 72% 42%)));
|
|
25
|
+
--_on-danger: var(--sfui-on-danger, hsl(var(--destructive-foreground, 0 0% 100%)));
|
|
26
|
+
--_border: var(--sfui-border, hsl(var(--border, 220 13% 88%)));
|
|
27
|
+
--_radius: var(--sfui-radius, var(--radius, 0.75rem));
|
|
28
|
+
--_radius-control: var(--sfui-radius-control, calc(var(--_radius) * 0.6));
|
|
29
|
+
--_font-heading: var(--sfui-font-heading, inherit);
|
|
30
|
+
|
|
31
|
+
background: transparent;
|
|
32
|
+
color: var(--_on-surface);
|
|
33
|
+
font: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── primitives ────────────────────────────────────────────────────────────── */
|
|
37
|
+
|
|
38
|
+
.sfui-heading {
|
|
39
|
+
font-family: var(--_font-heading);
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
line-height: 1.15;
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: var(--_on-surface);
|
|
44
|
+
}
|
|
45
|
+
.sfui-h1 { font-size: 1.75rem; letter-spacing: -0.01em; }
|
|
46
|
+
.sfui-h2 { font-size: 1.0625rem; }
|
|
47
|
+
|
|
48
|
+
.sfui-label {
|
|
49
|
+
display: block;
|
|
50
|
+
font-size: 0.6875rem;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
letter-spacing: 0.08em;
|
|
53
|
+
text-transform: uppercase;
|
|
54
|
+
color: var(--_on-subtle);
|
|
55
|
+
margin-block-end: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.sfui-muted { color: var(--_on-subtle); font-size: 0.875rem; }
|
|
59
|
+
|
|
60
|
+
.sfui-panel {
|
|
61
|
+
background: var(--_surface);
|
|
62
|
+
color: var(--_on-surface);
|
|
63
|
+
border: 1px solid var(--_border);
|
|
64
|
+
border-radius: var(--_radius);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sfui-btn {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
gap: 0.5rem;
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
width: 100%;
|
|
74
|
+
min-height: 2.875rem;
|
|
75
|
+
padding: 0.75rem 1.25rem;
|
|
76
|
+
border: 1px solid transparent;
|
|
77
|
+
border-radius: var(--_radius-control);
|
|
78
|
+
font: inherit;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
font-size: 0.9375rem;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
text-align: center;
|
|
84
|
+
background: var(--_accent);
|
|
85
|
+
color: var(--_on-accent);
|
|
86
|
+
transition: opacity 120ms ease;
|
|
87
|
+
}
|
|
88
|
+
.sfui-btn:hover { opacity: 0.9; }
|
|
89
|
+
.sfui-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
90
|
+
.sfui-btn-ghost {
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: var(--_on-surface);
|
|
93
|
+
border-color: var(--_border);
|
|
94
|
+
}
|
|
95
|
+
.sfui-btn-ghost:hover { opacity: 1; background: var(--_subtle); color: var(--_on-subtle); }
|
|
96
|
+
.sfui-btn-inline { width: auto; min-height: 2.5rem; }
|
|
97
|
+
|
|
98
|
+
.sfui a.sfui-link {
|
|
99
|
+
color: var(--_on-surface);
|
|
100
|
+
text-decoration: underline;
|
|
101
|
+
text-underline-offset: 3px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.sfui :is(button, a, input, select, textarea):focus-visible {
|
|
105
|
+
outline: 2px solid var(--_accent);
|
|
106
|
+
outline-offset: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sfui input,
|
|
110
|
+
.sfui select,
|
|
111
|
+
.sfui textarea {
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
width: 100%;
|
|
114
|
+
max-width: 100%;
|
|
115
|
+
min-height: 2.75rem;
|
|
116
|
+
padding: 0.625rem 0.75rem;
|
|
117
|
+
border: 1px solid var(--_border);
|
|
118
|
+
border-radius: var(--_radius-control);
|
|
119
|
+
background: var(--_surface);
|
|
120
|
+
color: var(--_on-surface);
|
|
121
|
+
font: inherit;
|
|
122
|
+
font-size: 0.9375rem;
|
|
123
|
+
}
|
|
124
|
+
.sfui textarea { min-height: 5rem; resize: vertical; }
|
|
125
|
+
.sfui input::placeholder, .sfui textarea::placeholder { color: var(--_on-subtle); }
|
|
126
|
+
|
|
127
|
+
.sfui-error {
|
|
128
|
+
margin: 0.375rem 0 0;
|
|
129
|
+
font-size: 0.8125rem;
|
|
130
|
+
color: var(--_danger);
|
|
131
|
+
}
|
|
132
|
+
.sfui-note {
|
|
133
|
+
margin: 0.375rem 0 0;
|
|
134
|
+
font-size: 0.8125rem;
|
|
135
|
+
color: var(--_on-subtle);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.sfui-spinner {
|
|
139
|
+
inline-size: 1.75rem;
|
|
140
|
+
block-size: 1.75rem;
|
|
141
|
+
border: 2px solid var(--_border);
|
|
142
|
+
border-block-start-color: var(--_accent);
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
animation: sfui-spin 0.8s linear infinite;
|
|
145
|
+
}
|
|
146
|
+
@keyframes sfui-spin { to { transform: rotate(360deg); } }
|
|
147
|
+
|
|
148
|
+
.sfui-state {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 1rem;
|
|
153
|
+
padding: 4rem 1.5rem;
|
|
154
|
+
text-align: center;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── line rows (shared by CartPage and MiniCart) ───────────────────────────── */
|
|
158
|
+
|
|
159
|
+
.sfui-lines { list-style: none; margin: 0; padding: 0; }
|
|
160
|
+
.sfui-line {
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: auto 1fr auto;
|
|
163
|
+
gap: 1rem;
|
|
164
|
+
padding-block: 1.25rem;
|
|
165
|
+
border-block-end: 1px solid var(--_border);
|
|
166
|
+
}
|
|
167
|
+
.sfui-line:last-child { border-block-end: 0; }
|
|
168
|
+
.sfui-line-media {
|
|
169
|
+
inline-size: 4.5rem;
|
|
170
|
+
block-size: 5.5rem;
|
|
171
|
+
border-radius: var(--_radius-control);
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
background: var(--_subtle);
|
|
174
|
+
color: var(--_on-subtle);
|
|
175
|
+
flex-shrink: 0;
|
|
176
|
+
}
|
|
177
|
+
.sfui-line-media img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
|
|
178
|
+
.sfui-line-main { min-inline-size: 0; }
|
|
179
|
+
.sfui-line-name { font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.25rem; }
|
|
180
|
+
.sfui-line-attrs { font-size: 0.8125rem; color: var(--_on-subtle); margin: 0 0 0.625rem; }
|
|
181
|
+
.sfui-line-end { display: flex; flex-direction: column; align-items: end; justify-content: space-between; }
|
|
182
|
+
.sfui-line-price { font-weight: 600; font-size: 0.9375rem; white-space: nowrap; }
|
|
183
|
+
|
|
184
|
+
.sfui-qty {
|
|
185
|
+
display: inline-flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
border: 1px solid var(--_border);
|
|
188
|
+
border-radius: var(--_radius-control);
|
|
189
|
+
background: var(--_surface);
|
|
190
|
+
color: var(--_on-surface);
|
|
191
|
+
}
|
|
192
|
+
.sfui-qty button {
|
|
193
|
+
inline-size: 2rem;
|
|
194
|
+
block-size: 2rem;
|
|
195
|
+
border: 0;
|
|
196
|
+
background: transparent;
|
|
197
|
+
color: var(--_on-surface);
|
|
198
|
+
font: inherit;
|
|
199
|
+
font-size: 1rem;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
border-radius: var(--_radius-control);
|
|
202
|
+
}
|
|
203
|
+
.sfui-qty button:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
204
|
+
.sfui-qty span {
|
|
205
|
+
min-inline-size: 2rem;
|
|
206
|
+
text-align: center;
|
|
207
|
+
font-size: 0.875rem;
|
|
208
|
+
font-variant-numeric: tabular-nums;
|
|
209
|
+
}
|
|
210
|
+
.sfui-line[data-pending="true"] .sfui-qty { opacity: 0.6; }
|
|
211
|
+
|
|
212
|
+
.sfui-line-remove {
|
|
213
|
+
border: 0;
|
|
214
|
+
background: none;
|
|
215
|
+
padding: 0;
|
|
216
|
+
font: inherit;
|
|
217
|
+
font-size: 0.8125rem;
|
|
218
|
+
color: var(--_on-subtle);
|
|
219
|
+
text-decoration: underline;
|
|
220
|
+
text-underline-offset: 3px;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
}
|
|
223
|
+
.sfui-line-remove:hover { color: var(--_danger); }
|
|
224
|
+
|
|
225
|
+
/* ── totals ────────────────────────────────────────────────────────────────── */
|
|
226
|
+
|
|
227
|
+
.sfui-totals { margin: 0; }
|
|
228
|
+
.sfui-totals > div {
|
|
229
|
+
display: flex;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
gap: 1rem;
|
|
232
|
+
font-size: 0.875rem;
|
|
233
|
+
color: var(--_on-subtle);
|
|
234
|
+
padding-block: 0.375rem;
|
|
235
|
+
}
|
|
236
|
+
.sfui-totals > div dt, .sfui-totals > div dd { margin: 0; }
|
|
237
|
+
.sfui-totals > div[data-emphasis="true"] {
|
|
238
|
+
color: var(--_on-surface);
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
font-size: 1rem;
|
|
241
|
+
border-block-start: 1px solid var(--_border);
|
|
242
|
+
margin-block-start: 0.5rem;
|
|
243
|
+
padding-block-start: 0.875rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ── coupon ────────────────────────────────────────────────────────────────── */
|
|
247
|
+
|
|
248
|
+
.sfui-coupon { display: flex; gap: 0.5rem; }
|
|
249
|
+
.sfui-coupon input { flex: 1; min-inline-size: 0; }
|
|
250
|
+
.sfui-coupon-applied {
|
|
251
|
+
display: flex;
|
|
252
|
+
justify-content: space-between;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 0.5rem;
|
|
255
|
+
font-size: 0.875rem;
|
|
256
|
+
padding: 0.5rem 0.75rem;
|
|
257
|
+
border: 1px dashed var(--_border);
|
|
258
|
+
border-radius: var(--_radius-control);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ── cart page ─────────────────────────────────────────────────────────────── */
|
|
262
|
+
|
|
263
|
+
.sfui-cart { padding-block: 2.5rem 4rem; }
|
|
264
|
+
.sfui-cart-inner { display: grid; grid-template-columns: 1fr; gap: 2rem; max-inline-size: 68rem; margin-inline: auto; padding-inline: 1.25rem; }
|
|
265
|
+
.sfui-summary { padding: 1.5rem; align-self: start; display: grid; gap: 1.25rem; }
|
|
266
|
+
@media (min-width: 60rem) {
|
|
267
|
+
.sfui-cart-inner { grid-template-columns: 1fr 24rem; gap: 3rem; }
|
|
268
|
+
.sfui-summary { position: sticky; inset-block-start: 1.5rem; }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ── minicart drawer ───────────────────────────────────────────────────────── */
|
|
272
|
+
|
|
273
|
+
.sfui-drawer-backdrop {
|
|
274
|
+
position: fixed;
|
|
275
|
+
inset: 0;
|
|
276
|
+
background: rgba(10, 10, 12, 0.45);
|
|
277
|
+
z-index: 60;
|
|
278
|
+
animation: sfui-fade 160ms ease;
|
|
279
|
+
}
|
|
280
|
+
.sfui-drawer {
|
|
281
|
+
position: fixed;
|
|
282
|
+
inset-block: 0;
|
|
283
|
+
inline-size: min(26rem, calc(100vw - 2.5rem));
|
|
284
|
+
background: var(--_surface);
|
|
285
|
+
color: var(--_on-surface);
|
|
286
|
+
z-index: 61;
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
box-shadow: 0 0 48px rgba(0, 0, 0, 0.18);
|
|
290
|
+
}
|
|
291
|
+
.sfui-drawer[data-side="right"] { inset-inline-end: 0; animation: sfui-slide-in-end 200ms ease; }
|
|
292
|
+
.sfui-drawer[data-side="left"] { inset-inline-start: 0; animation: sfui-slide-in-start 200ms ease; }
|
|
293
|
+
.sfui-drawer-head {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
gap: 1rem;
|
|
298
|
+
padding: 1.25rem 1.25rem 1rem;
|
|
299
|
+
border-block-end: 1px solid var(--_border);
|
|
300
|
+
}
|
|
301
|
+
.sfui-drawer-close {
|
|
302
|
+
border: 0;
|
|
303
|
+
background: none;
|
|
304
|
+
color: var(--_on-surface);
|
|
305
|
+
font-size: 1.25rem;
|
|
306
|
+
line-height: 1;
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
padding: 0.25rem;
|
|
309
|
+
}
|
|
310
|
+
.sfui-drawer-body { flex: 1; overflow-y: auto; padding-inline: 1.25rem; }
|
|
311
|
+
.sfui-drawer-body .sfui-line-media { inline-size: 3.5rem; block-size: 4.25rem; }
|
|
312
|
+
.sfui-drawer-foot {
|
|
313
|
+
padding: 1rem 1.25rem 1.25rem;
|
|
314
|
+
border-block-start: 1px solid var(--_border);
|
|
315
|
+
display: grid;
|
|
316
|
+
gap: 0.75rem;
|
|
317
|
+
}
|
|
318
|
+
.sfui-drawer-subtotal { display: flex; justify-content: space-between; font-weight: 600; }
|
|
319
|
+
|
|
320
|
+
@keyframes sfui-fade { from { opacity: 0; } }
|
|
321
|
+
@keyframes sfui-slide-in-end { from { transform: translateX(100%); } }
|
|
322
|
+
@keyframes sfui-slide-in-start { from { transform: translateX(-100%); } }
|
|
323
|
+
@media (prefers-reduced-motion: reduce) {
|
|
324
|
+
.sfui-drawer, .sfui-drawer-backdrop { animation: none; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ── cart button (header trigger) ──────────────────────────────────────────── */
|
|
328
|
+
|
|
329
|
+
.sfui-cart-button {
|
|
330
|
+
position: relative;
|
|
331
|
+
display: inline-flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
inline-size: 2.5rem;
|
|
335
|
+
block-size: 2.5rem;
|
|
336
|
+
border: 0;
|
|
337
|
+
background: none;
|
|
338
|
+
color: inherit;
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
border-radius: var(--_radius-control);
|
|
341
|
+
}
|
|
342
|
+
.sfui-cart-button svg { inline-size: 1.375rem; block-size: 1.375rem; }
|
|
343
|
+
.sfui-cart-badge {
|
|
344
|
+
position: absolute;
|
|
345
|
+
inset-block-start: 0.125rem;
|
|
346
|
+
inset-inline-end: 0.125rem;
|
|
347
|
+
min-inline-size: 1.125rem;
|
|
348
|
+
block-size: 1.125rem;
|
|
349
|
+
padding-inline: 0.25rem;
|
|
350
|
+
border-radius: 999px;
|
|
351
|
+
background: var(--_accent);
|
|
352
|
+
color: var(--_on-accent);
|
|
353
|
+
font-size: 0.6875rem;
|
|
354
|
+
font-weight: 700;
|
|
355
|
+
line-height: 1.125rem;
|
|
356
|
+
text-align: center;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ── checkout ──────────────────────────────────────────────────────────────── */
|
|
360
|
+
|
|
361
|
+
.sfui-checkout { padding-block: 2.5rem 4rem; }
|
|
362
|
+
.sfui-checkout-inner { display: grid; grid-template-columns: 1fr; gap: 2rem; max-inline-size: 72rem; margin-inline: auto; padding-inline: 1.25rem; }
|
|
363
|
+
@media (min-width: 64rem) {
|
|
364
|
+
.sfui-checkout-inner[data-layout="two-column"] { grid-template-columns: 1fr 26rem; gap: 3rem; }
|
|
365
|
+
.sfui-checkout-inner[data-layout="two-column"] .sfui-summary { position: sticky; inset-block-start: 1.5rem; }
|
|
366
|
+
}
|
|
367
|
+
.sfui-checkout-form { display: grid; gap: 2rem; min-inline-size: 0; }
|
|
368
|
+
.sfui-section { display: grid; gap: 0.875rem; }
|
|
369
|
+
|
|
370
|
+
/* AddressFields integration: style the fields it exposes via data-part. */
|
|
371
|
+
.sfui-address [data-part="address-fields"] {
|
|
372
|
+
display: grid;
|
|
373
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
374
|
+
gap: 0.875rem 0.75rem;
|
|
375
|
+
}
|
|
376
|
+
.sfui-address [data-part="field"] { grid-column: span 2; min-inline-size: 0; }
|
|
377
|
+
@media (min-width: 40rem) {
|
|
378
|
+
.sfui-address [data-part="field"][data-span="1"] { grid-column: span 1; }
|
|
379
|
+
}
|
|
380
|
+
.sfui-address [data-part="label"] {
|
|
381
|
+
display: block;
|
|
382
|
+
font-size: 0.8125rem;
|
|
383
|
+
font-weight: 500;
|
|
384
|
+
margin-block-end: 0.375rem;
|
|
385
|
+
}
|
|
386
|
+
.sfui-address [data-part="required"] { color: var(--_danger); margin-inline-start: 0.125rem; }
|
|
387
|
+
.sfui-address [data-part="field"][data-invalid] [data-part="control"] { border-color: var(--_danger); }
|
|
388
|
+
.sfui-address [data-part="error"] { margin: 0.375rem 0 0; font-size: 0.8125rem; color: var(--_danger); }
|
|
389
|
+
|
|
390
|
+
.sfui-choices { display: grid; gap: 0.5rem; }
|
|
391
|
+
.sfui-choice {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
gap: 0.75rem;
|
|
395
|
+
padding: 0.875rem 1rem;
|
|
396
|
+
border: 1px solid var(--_border);
|
|
397
|
+
border-radius: var(--_radius-control);
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
}
|
|
400
|
+
.sfui-choice[data-selected="true"] { border-color: var(--_accent); box-shadow: inset 0 0 0 1px var(--_accent); }
|
|
401
|
+
.sfui-choice input { inline-size: 1rem; block-size: 1rem; min-height: 0; margin: 0; accent-color: var(--_accent); flex-shrink: 0; }
|
|
402
|
+
.sfui-choice-main { flex: 1; min-inline-size: 0; }
|
|
403
|
+
.sfui-choice-title { font-size: 0.9375rem; font-weight: 500; }
|
|
404
|
+
.sfui-choice-desc { font-size: 0.8125rem; color: var(--_on-subtle); margin-block-start: 0.125rem; }
|
|
405
|
+
.sfui-choice-cost { font-size: 0.9375rem; font-weight: 600; white-space: nowrap; }
|
|
406
|
+
.sfui-chosen-single {
|
|
407
|
+
display: flex;
|
|
408
|
+
justify-content: space-between;
|
|
409
|
+
gap: 1rem;
|
|
410
|
+
padding: 0.875rem 1rem;
|
|
411
|
+
border: 1px solid var(--_border);
|
|
412
|
+
border-radius: var(--_radius-control);
|
|
413
|
+
background: var(--_subtle);
|
|
414
|
+
color: var(--_on-subtle);
|
|
415
|
+
font-size: 0.9375rem;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.sfui-blockers { margin: 0.75rem 0 0; padding: 0; list-style: none; display: grid; gap: 0.25rem; }
|
|
419
|
+
.sfui-blockers li { font-size: 0.8125rem; color: var(--_on-subtle); }
|
|
420
|
+
.sfui-checkbox { display: flex; align-items: center; gap: 0.625rem; font-size: 0.9375rem; cursor: pointer; }
|
|
421
|
+
.sfui-checkbox input { inline-size: 1rem; block-size: 1rem; min-height: 0; margin: 0; accent-color: var(--_accent); }
|
|
422
|
+
|
|
423
|
+
/* ── order received ────────────────────────────────────────────────────────── */
|
|
424
|
+
|
|
425
|
+
.sfui-order { padding-block: 3rem 4rem; }
|
|
426
|
+
.sfui-order-inner { max-inline-size: 42rem; margin-inline: auto; padding-inline: 1.25rem; display: grid; gap: 1.5rem; }
|
|
427
|
+
.sfui-order-badge {
|
|
428
|
+
inline-size: 3.5rem;
|
|
429
|
+
block-size: 3.5rem;
|
|
430
|
+
margin-inline: auto;
|
|
431
|
+
border: 2px solid var(--_accent);
|
|
432
|
+
border-radius: 50%;
|
|
433
|
+
display: flex;
|
|
434
|
+
align-items: center;
|
|
435
|
+
justify-content: center;
|
|
436
|
+
color: var(--_accent);
|
|
437
|
+
font-size: 1.5rem;
|
|
438
|
+
}
|
|
439
|
+
.sfui-order-head { text-align: center; display: grid; gap: 0.75rem; }
|
|
440
|
+
.sfui-order-card { padding: 1.5rem; display: grid; gap: 1rem; }
|
|
441
|
+
.sfui-order-meta { display: flex; justify-content: space-between; gap: 1rem; padding-block-end: 1rem; border-block-end: 1px solid var(--_border); }
|
|
442
|
+
.sfui-order-meta dt { font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--_on-subtle); margin: 0 0 0.25rem; }
|
|
443
|
+
.sfui-order-meta dd { margin: 0; font-weight: 600; font-size: 0.9375rem; }
|
|
444
|
+
.sfui-instructions { padding: 1.25rem; border: 1px dashed var(--_border); border-radius: var(--_radius-control); display: grid; gap: 0.75rem; }
|
|
445
|
+
.sfui-instructions dl { margin: 0; display: grid; gap: 0.5rem; }
|
|
446
|
+
.sfui-instructions dl > div { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.875rem; }
|
|
447
|
+
.sfui-instructions dt { color: var(--_on-subtle); margin: 0; }
|
|
448
|
+
.sfui-instructions dd { margin: 0; font-weight: 500; overflow-wrap: anywhere; text-align: end; }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import "./storefront-ui.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The design-solidity guard. Every color in storefront-ui exists only as a PAIR
|
|
6
|
+
* (surface + on-surface), so unreadable text requires overriding one side of a
|
|
7
|
+
* pair without the other. This hook closes even that hole: after mount it
|
|
8
|
+
* resolves each pair to real pixels and, when a pair's contrast is unreadable
|
|
9
|
+
* (< 3:1 — WCAG large-text floor), it resets BOTH sides of that pair on the
|
|
10
|
+
* component's own root back to the shipped defaults. Worst case is the default
|
|
11
|
+
* look, never invisible text. In dev it also names the broken pair on the
|
|
12
|
+
* console so the store fixes its override.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PAIRS = [
|
|
16
|
+
["surface", "hsl(0 0% 100%)", "hsl(224 10% 10%)"],
|
|
17
|
+
["subtle", "hsl(220 14% 96%)", "hsl(220 9% 40%)"],
|
|
18
|
+
["accent", "hsl(224 71% 10%)", "hsl(0 0% 100%)"],
|
|
19
|
+
["danger", "hsl(0 72% 42%)", "hsl(0 0% 100%)"],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function parseRgb(computed) {
|
|
23
|
+
const m = /rgba?\(([\d.]+)[ ,]+([\d.]+)[ ,]+([\d.]+)(?:[ ,/]+([\d.]+))?\)/.exec(computed);
|
|
24
|
+
if (!m) return null;
|
|
25
|
+
if (m[4] !== undefined && Number(m[4]) < 0.4) return null; // too transparent to judge
|
|
26
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function luminance([r, g, b]) {
|
|
30
|
+
const f = (c) => {
|
|
31
|
+
const s = c / 255;
|
|
32
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
33
|
+
};
|
|
34
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function contrast(a, b) {
|
|
38
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
39
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useThemeGuard(rootRef) {
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const root = rootRef.current;
|
|
45
|
+
if (!root || typeof window === "undefined" || !window.getComputedStyle) return;
|
|
46
|
+
const probe = document.createElement("div");
|
|
47
|
+
probe.style.position = "absolute";
|
|
48
|
+
probe.style.visibility = "hidden";
|
|
49
|
+
root.appendChild(probe);
|
|
50
|
+
try {
|
|
51
|
+
for (const [name, defBg, defFg] of PAIRS) {
|
|
52
|
+
probe.style.backgroundColor = `var(--_${name})`;
|
|
53
|
+
probe.style.color = `var(--_on-${name})`;
|
|
54
|
+
const cs = window.getComputedStyle(probe);
|
|
55
|
+
const bg = parseRgb(cs.backgroundColor);
|
|
56
|
+
const fg = parseRgb(cs.color);
|
|
57
|
+
if (!bg || !fg) continue;
|
|
58
|
+
if (contrast(bg, fg) < 3) {
|
|
59
|
+
root.style.setProperty(`--_${name}`, defBg);
|
|
60
|
+
root.style.setProperty(`--_on-${name}`, defFg);
|
|
61
|
+
if (typeof console !== "undefined") {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[storefront-ui] --sfui-${name}/--sfui-on-${name} are unreadable together ` +
|
|
64
|
+
`(contrast < 3:1); this surface fell back to the default pair. ` +
|
|
65
|
+
`Override the pair together in index.css.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* the guard must never break a page */
|
|
72
|
+
} finally {
|
|
73
|
+
root.removeChild(probe);
|
|
74
|
+
}
|
|
75
|
+
}, [rootRef]);
|
|
76
|
+
}
|