@kwtv/components 0.9.0 → 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 +92 -17
- package/package.json +2 -2
- package/src/astro/ActiveFilterTags.astro +4 -34
- package/src/astro/Badge.astro +4 -84
- package/src/astro/Checkbox.astro +5 -64
- package/src/astro/Chip.astro +14 -85
- package/src/astro/CopyChip.astro +22 -3
- package/src/astro/Filters.astro +11 -2
- package/src/astro/Pager.astro +15 -54
- package/src/astro/ProductCard.astro +30 -142
- package/src/astro/Radio.astro +5 -46
- package/src/astro/Swatch.astro +10 -57
- package/src/react/Drawer.css +34 -0
- package/src/react/Drawer.tsx +54 -23
- package/src/styles/badge.css +95 -0
- package/src/styles/filters.css +255 -0
- package/src/styles/pager.css +63 -0
- package/src/styles/product-card.css +148 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Badge styles as plain, importable CSS. This is the single copy of the
|
|
3
|
+
* .ui-badge rules. Badge.astro imports it itself, so a pure-Astro app needs
|
|
4
|
+
* nothing extra.
|
|
5
|
+
*
|
|
6
|
+
* React and Preact islands cannot render Astro components. An island that
|
|
7
|
+
* shows a badge or a removable filter tag writes the same markup as the
|
|
8
|
+
* Astro component and imports this sheet once:
|
|
9
|
+
*
|
|
10
|
+
* import "@kwtv/components/styles/badge.css";
|
|
11
|
+
*
|
|
12
|
+
* Import `@kwtv/tokens/tokens.css` before this file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@layer reset, tokens, base, components, motion, utilities;
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.ui-badge {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--space-1-5);
|
|
22
|
+
min-height: 24px;
|
|
23
|
+
padding: 0 var(--space-2);
|
|
24
|
+
border: 1px solid transparent;
|
|
25
|
+
border-radius: var(--radius-full);
|
|
26
|
+
font-family: var(--font-sans);
|
|
27
|
+
font-size: var(--text-xs);
|
|
28
|
+
font-weight: var(--weight-medium);
|
|
29
|
+
line-height: 1;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ui-badge__icon {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-badge--neutral {
|
|
39
|
+
background: var(--surface-accent);
|
|
40
|
+
color: var(--surface-accent-fg, var(--text));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ui-badge--success {
|
|
44
|
+
background: var(--success-bg);
|
|
45
|
+
color: var(--success);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ui-badge--warning {
|
|
49
|
+
background: var(--warning-bg);
|
|
50
|
+
color: var(--warning);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-badge--danger {
|
|
54
|
+
background: var(--danger-bg);
|
|
55
|
+
color: var(--danger);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Outline keeps the strong border and full-strength text, so it reads */
|
|
59
|
+
/* without relying on a fill. */
|
|
60
|
+
.ui-badge--outline {
|
|
61
|
+
background: transparent;
|
|
62
|
+
border: 1px solid var(--border-strong);
|
|
63
|
+
color: var(--text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-badge--removable {
|
|
67
|
+
padding-right: var(--space-1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Small × target grows with padding plus an equal negative margin, so the */
|
|
71
|
+
/* hit area grows without moving the layout. */
|
|
72
|
+
.ui-badge__x {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
padding: var(--space-2);
|
|
77
|
+
margin: calc(-1 * var(--space-2));
|
|
78
|
+
margin-left: 0;
|
|
79
|
+
background: transparent;
|
|
80
|
+
border: 0;
|
|
81
|
+
border-radius: var(--radius-sm);
|
|
82
|
+
color: inherit;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ui-badge__x:hover {
|
|
87
|
+
color: var(--danger);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ui-badge__x:focus-visible {
|
|
91
|
+
outline: 2px solid var(--focus);
|
|
92
|
+
outline-offset: 2px;
|
|
93
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Filter and selection styles as plain, importable CSS. This is the single
|
|
3
|
+
* copy of the .ui-chip, .ui-swatch, .ui-check and .ui-filtertags rules.
|
|
4
|
+
* Chip.astro, Swatch.astro, Checkbox.astro, Radio.astro and
|
|
5
|
+
* ActiveFilterTags.astro import it themselves, so a pure-Astro app needs
|
|
6
|
+
* nothing extra. Bundlers emit it once, however many components and pages
|
|
7
|
+
* pull it in.
|
|
8
|
+
*
|
|
9
|
+
* React and Preact islands cannot render Astro components. An island that
|
|
10
|
+
* shows chips, swatches, checkboxes, radios or the active filter row writes
|
|
11
|
+
* the same markup as the Astro component and imports this sheet once:
|
|
12
|
+
*
|
|
13
|
+
* import "@kwtv/components/styles/filters.css";
|
|
14
|
+
*
|
|
15
|
+
* The active filter tags are outline badges, so that island imports
|
|
16
|
+
* `styles/badge.css` too. Import `@kwtv/tokens/tokens.css` before this file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@layer reset, tokens, base, components, motion, utilities;
|
|
20
|
+
|
|
21
|
+
@layer components {
|
|
22
|
+
/* ---- Chip ----------------------------------------------------------------- */
|
|
23
|
+
.ui-chip {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
min-height: 36px;
|
|
28
|
+
padding: 0 var(--space-4);
|
|
29
|
+
background: var(--surface);
|
|
30
|
+
border: 1px solid var(--border);
|
|
31
|
+
border-radius: var(--radius-md);
|
|
32
|
+
font-family: var(--font-sans);
|
|
33
|
+
font-size: var(--text-sm);
|
|
34
|
+
font-weight: var(--weight-medium);
|
|
35
|
+
color: var(--text);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
transition:
|
|
38
|
+
border-color var(--duration-fast) var(--ease-standard),
|
|
39
|
+
background-color var(--duration-fast) var(--ease-standard);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.ui-chip:hover,
|
|
43
|
+
.ui-chip.is-hover {
|
|
44
|
+
border-color: var(--border-strong);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-chip:focus-visible,
|
|
48
|
+
.ui-chip.is-focus {
|
|
49
|
+
outline: 2px solid var(--focus);
|
|
50
|
+
outline-offset: 2px;
|
|
51
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Selected: 2 px border plus the accent surface, readable without colour. */
|
|
55
|
+
.ui-chip.is-selected,
|
|
56
|
+
.ui-chip[aria-pressed="true"] {
|
|
57
|
+
border-width: 2px;
|
|
58
|
+
border-color: var(--border-strong);
|
|
59
|
+
background: var(--surface-accent);
|
|
60
|
+
color: var(--surface-accent-fg, var(--text));
|
|
61
|
+
font-weight: var(--weight-semibold);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ui-chip.is-disabled,
|
|
65
|
+
.ui-chip:disabled {
|
|
66
|
+
opacity: 0.5;
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ui-chip--struck {
|
|
71
|
+
text-decoration: line-through;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Filter chip: 32 px. */
|
|
75
|
+
.ui-chip--filter {
|
|
76
|
+
min-height: 32px;
|
|
77
|
+
border-radius: var(--radius-full);
|
|
78
|
+
padding: 0 var(--space-3);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-chip--filter.is-selected,
|
|
82
|
+
.ui-chip--filter[aria-pressed="true"] {
|
|
83
|
+
background: var(--surface-accent);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (prefers-reduced-motion: reduce) {
|
|
87
|
+
.ui-chip {
|
|
88
|
+
transition: none;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (max-width: 700px), (pointer: coarse) {
|
|
93
|
+
.ui-chip {
|
|
94
|
+
min-height: 36px;
|
|
95
|
+
}
|
|
96
|
+
.ui-chip--filter {
|
|
97
|
+
min-height: 40px;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ---- Swatch --------------------------------------------------------------- */
|
|
102
|
+
.ui-swatch {
|
|
103
|
+
width: 30px;
|
|
104
|
+
height: 30px;
|
|
105
|
+
padding: 0;
|
|
106
|
+
background: var(--swatch);
|
|
107
|
+
border: 1px solid var(--border);
|
|
108
|
+
border-radius: var(--radius-full);
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
transition: box-shadow var(--duration-fast) var(--ease-standard);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.ui-swatch:hover,
|
|
114
|
+
.ui-swatch.is-hover {
|
|
115
|
+
box-shadow: 0 0 0 2px var(--border-strong);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ui-swatch:focus-visible,
|
|
119
|
+
.ui-swatch.is-focus {
|
|
120
|
+
outline: 2px solid var(--focus);
|
|
121
|
+
outline-offset: 2px;
|
|
122
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Selected: 2 px ring, readable without colour. */
|
|
126
|
+
.ui-swatch.is-selected,
|
|
127
|
+
.ui-swatch[aria-pressed="true"] {
|
|
128
|
+
box-shadow:
|
|
129
|
+
0 0 0 2px var(--surface),
|
|
130
|
+
0 0 0 4px var(--border-strong);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ui-swatch.is-disabled,
|
|
134
|
+
.ui-swatch:disabled {
|
|
135
|
+
opacity: 0.5;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (prefers-reduced-motion: reduce) {
|
|
140
|
+
.ui-swatch {
|
|
141
|
+
transition: none;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@media (max-width: 700px), (pointer: coarse) {
|
|
146
|
+
.ui-swatch {
|
|
147
|
+
width: 32px;
|
|
148
|
+
height: 32px;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ---- Checkbox and Radio --------------------------------------------------- */
|
|
153
|
+
/* Checkbox and Radio share the row. The input type picks the box. */
|
|
154
|
+
.ui-check {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: flex-start;
|
|
157
|
+
gap: var(--space-2);
|
|
158
|
+
min-height: 32px;
|
|
159
|
+
font-family: var(--font-sans);
|
|
160
|
+
font-size: var(--text-sm);
|
|
161
|
+
color: var(--text);
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ui-check:has(> input[type="radio"]) {
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ui-check input[type="checkbox"] {
|
|
170
|
+
appearance: none;
|
|
171
|
+
-webkit-appearance: none;
|
|
172
|
+
width: 17px;
|
|
173
|
+
height: 17px;
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
margin-top: calc((var(--leading-sm, 1.375rem) - 17px) / 2);
|
|
176
|
+
border: 1.5px solid var(--border-strong);
|
|
177
|
+
border-radius: var(--radius-xs, 3px);
|
|
178
|
+
background: var(--surface);
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
transition:
|
|
181
|
+
background-color var(--duration-fast, 100ms) var(--ease-standard, ease),
|
|
182
|
+
border-color var(--duration-fast, 100ms) var(--ease-standard, ease);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ui-check input[type="checkbox"]:checked {
|
|
186
|
+
background: var(--primary);
|
|
187
|
+
border-color: var(--primary);
|
|
188
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 10' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='1 5 4.5 8.5 11 1'/%3E%3C/svg%3E");
|
|
189
|
+
background-repeat: no-repeat;
|
|
190
|
+
background-position: center;
|
|
191
|
+
background-size: 10px 8px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.ui-check input[type="radio"] {
|
|
195
|
+
width: 17px;
|
|
196
|
+
height: 17px;
|
|
197
|
+
flex-shrink: 0;
|
|
198
|
+
accent-color: var(--primary);
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.ui-check input:focus-visible {
|
|
203
|
+
outline: 2px solid var(--focus);
|
|
204
|
+
outline-offset: 2px;
|
|
205
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ui-check.is-disabled {
|
|
209
|
+
cursor: not-allowed;
|
|
210
|
+
color: var(--text-muted);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.ui-check.is-disabled input {
|
|
214
|
+
cursor: not-allowed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.ui-check.is-disabled input[type="checkbox"] {
|
|
218
|
+
opacity: 0.45;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@media (max-width: 700px), (pointer: coarse) {
|
|
222
|
+
.ui-check {
|
|
223
|
+
min-height: 40px;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ---- Active filter tags --------------------------------------------------- */
|
|
228
|
+
.ui-filtertags {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-wrap: wrap;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: var(--space-2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ui-filtertags__clear {
|
|
236
|
+
padding: var(--space-1-5) var(--space-2);
|
|
237
|
+
background: transparent;
|
|
238
|
+
border: 0;
|
|
239
|
+
border-radius: var(--radius-sm);
|
|
240
|
+
font-family: var(--font-sans);
|
|
241
|
+
font-size: var(--text-xs);
|
|
242
|
+
font-weight: var(--weight-semibold);
|
|
243
|
+
color: var(--link);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
}
|
|
246
|
+
.ui-filtertags__clear:hover {
|
|
247
|
+
background: var(--surface-hover);
|
|
248
|
+
color: var(--link-hover);
|
|
249
|
+
}
|
|
250
|
+
.ui-filtertags__clear:focus-visible {
|
|
251
|
+
outline: 2px solid var(--focus);
|
|
252
|
+
outline-offset: 2px;
|
|
253
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pager styles as plain, importable CSS. This is the single copy of the
|
|
3
|
+
* .ui-pager rules. Pager.astro imports it itself, so a pure-Astro app needs
|
|
4
|
+
* nothing extra.
|
|
5
|
+
*
|
|
6
|
+
* React and Preact islands cannot render Astro components. An island that
|
|
7
|
+
* renders a pager writes the same markup as the Astro component and imports
|
|
8
|
+
* this sheet once:
|
|
9
|
+
*
|
|
10
|
+
* import "@kwtv/components/styles/pager.css";
|
|
11
|
+
*
|
|
12
|
+
* Import `@kwtv/tokens/tokens.css` before this file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@layer reset, tokens, base, components, motion, utilities;
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.ui-pager {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--space-1);
|
|
22
|
+
font-size: var(--text-sm);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ui-pager a,
|
|
26
|
+
.ui-pager span:not(.ui-pager__gap) {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
min-width: 36px;
|
|
31
|
+
height: 36px;
|
|
32
|
+
padding: 0 var(--space-1);
|
|
33
|
+
border-radius: var(--radius-md);
|
|
34
|
+
color: var(--link);
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-pager a:hover {
|
|
39
|
+
background: var(--surface-hover);
|
|
40
|
+
color: var(--link-hover);
|
|
41
|
+
}
|
|
42
|
+
.ui-pager a:focus-visible {
|
|
43
|
+
outline: 2px solid var(--focus);
|
|
44
|
+
outline-offset: 2px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-pager a.is-current {
|
|
48
|
+
background: var(--primary);
|
|
49
|
+
color: var(--primary-fg);
|
|
50
|
+
font-weight: var(--weight-semibold);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-pager .is-disabled {
|
|
54
|
+
color: var(--border);
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ui-pager__gap {
|
|
59
|
+
min-width: 24px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
color: var(--text-muted);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Product card styles as plain, importable CSS. This is the single copy of
|
|
3
|
+
* the .ui-product and .ui-ribbon rules. ProductCard.astro imports it itself,
|
|
4
|
+
* so a pure-Astro app needs nothing extra.
|
|
5
|
+
*
|
|
6
|
+
* React and Preact islands cannot render Astro components. An island that
|
|
7
|
+
* renders product cards writes the same markup as the Astro component and
|
|
8
|
+
* imports this sheet once:
|
|
9
|
+
*
|
|
10
|
+
* import "@kwtv/components/styles/product-card.css";
|
|
11
|
+
*
|
|
12
|
+
* The sold-out text uses .ui-sr-only from `styles/base.css`. Import
|
|
13
|
+
* `@kwtv/tokens/tokens.css` before this file.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@layer reset, tokens, base, components, motion, utilities;
|
|
17
|
+
|
|
18
|
+
@layer components {
|
|
19
|
+
.ui-product {
|
|
20
|
+
position: relative;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
border: 1px solid var(--border);
|
|
24
|
+
border-radius: var(--radius-lg);
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background: var(--surface);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
transition: border-color var(--duration-fast) var(--ease-standard);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ui-product:hover,
|
|
33
|
+
.ui-product.is-hover {
|
|
34
|
+
border-color: var(--border-strong);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ui-product:focus-visible {
|
|
38
|
+
outline: 2px solid var(--focus);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ui-product__img {
|
|
44
|
+
display: block;
|
|
45
|
+
width: 100%;
|
|
46
|
+
aspect-ratio: 4 / 3;
|
|
47
|
+
object-fit: cover;
|
|
48
|
+
background: var(--surface-hover);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ui-product__body {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--space-1);
|
|
55
|
+
padding: var(--space-3) var(--space-4) var(--space-4);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ui-product__title {
|
|
59
|
+
font-size: var(--text-base);
|
|
60
|
+
font-weight: var(--weight-semibold);
|
|
61
|
+
color: var(--text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ui-product__meta {
|
|
65
|
+
font-size: var(--text-xs);
|
|
66
|
+
color: var(--text-muted);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ui-product__price {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: baseline;
|
|
72
|
+
gap: var(--space-2);
|
|
73
|
+
margin-top: var(--space-1);
|
|
74
|
+
font-size: var(--text-lg);
|
|
75
|
+
font-weight: var(--weight-semibold);
|
|
76
|
+
font-variant-numeric: tabular-nums;
|
|
77
|
+
color: var(--text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ui-product__price s {
|
|
81
|
+
font-size: var(--text-sm);
|
|
82
|
+
font-weight: var(--weight-regular);
|
|
83
|
+
color: var(--text-muted);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Sold out: card stays readable and clickable. */
|
|
87
|
+
.ui-product.is-disabled .ui-product__img {
|
|
88
|
+
opacity: 0.7;
|
|
89
|
+
}
|
|
90
|
+
.ui-product.is-disabled .ui-product__price {
|
|
91
|
+
color: var(--text-muted);
|
|
92
|
+
font-weight: var(--weight-regular);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Ribbon. One per card, decorative. 112 px box. */
|
|
96
|
+
.ui-ribbon {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 0;
|
|
99
|
+
left: 0;
|
|
100
|
+
width: 112px;
|
|
101
|
+
height: 112px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
z-index: 1;
|
|
105
|
+
}
|
|
106
|
+
.ui-ribbon i {
|
|
107
|
+
position: absolute;
|
|
108
|
+
top: 22px;
|
|
109
|
+
left: -34px;
|
|
110
|
+
width: 140px;
|
|
111
|
+
transform: rotate(-45deg);
|
|
112
|
+
text-align: center;
|
|
113
|
+
font-size: var(--text-2xs);
|
|
114
|
+
font-weight: var(--weight-bold);
|
|
115
|
+
letter-spacing: var(--tracking-wider);
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
font-style: normal;
|
|
118
|
+
padding: var(--space-1) 0;
|
|
119
|
+
color: var(--primary-fg);
|
|
120
|
+
background: var(--primary);
|
|
121
|
+
}
|
|
122
|
+
.ui-ribbon--new i {
|
|
123
|
+
background: var(--primary);
|
|
124
|
+
color: var(--primary-fg);
|
|
125
|
+
}
|
|
126
|
+
.ui-ribbon--promo i {
|
|
127
|
+
background: var(--accent);
|
|
128
|
+
color: var(--accent-fg);
|
|
129
|
+
}
|
|
130
|
+
.ui-ribbon--last i {
|
|
131
|
+
background: var(--warning);
|
|
132
|
+
color: var(--primary-fg);
|
|
133
|
+
}
|
|
134
|
+
.ui-ribbon--fav i {
|
|
135
|
+
background: var(--surface-accent);
|
|
136
|
+
color: var(--surface-accent-fg);
|
|
137
|
+
}
|
|
138
|
+
.ui-ribbon--sold-out i {
|
|
139
|
+
background: var(--danger-bg);
|
|
140
|
+
color: var(--danger);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@media (prefers-reduced-motion: reduce) {
|
|
144
|
+
.ui-product {
|
|
145
|
+
transition: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|