@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
package/src/astro/Pager.astro
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/pager.css";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Pager. Appears once a list passes its page threshold. The current page is a
|
|
6
7
|
* non-link with aria-current="page"; disabled arrows are spans, not links, so
|
|
7
8
|
* keyboard focus never lands on a dead end. On mobile it collapses to
|
|
8
9
|
* previous / current / next.
|
|
10
|
+
*
|
|
11
|
+
* `previousLabel` and `nextLabel` name the arrow links. They default to the
|
|
12
|
+
* Dutch "Vorige pagina" and "Volgende pagina".
|
|
13
|
+
*
|
|
14
|
+
* React and Preact islands write the same markup and import
|
|
15
|
+
* `@kwtv/components/styles/pager.css`.
|
|
9
16
|
*/
|
|
10
17
|
interface Props {
|
|
11
18
|
current: number;
|
|
@@ -23,6 +30,10 @@ interface Props {
|
|
|
23
30
|
/** The query key the preserveScroll behaviour reads and writes. */
|
|
24
31
|
param?: string;
|
|
25
32
|
label?: string;
|
|
33
|
+
/** Accessible name of the previous arrow. Defaults to "Vorige pagina". */
|
|
34
|
+
previousLabel?: string;
|
|
35
|
+
/** Accessible name of the next arrow. Defaults to "Volgende pagina". */
|
|
36
|
+
nextLabel?: string;
|
|
26
37
|
class?: string;
|
|
27
38
|
}
|
|
28
39
|
|
|
@@ -33,6 +44,8 @@ const {
|
|
|
33
44
|
preserveScroll = false,
|
|
34
45
|
param = "page",
|
|
35
46
|
label = "Paginering",
|
|
47
|
+
previousLabel = "Vorige pagina",
|
|
48
|
+
nextLabel = "Volgende pagina",
|
|
36
49
|
class: className,
|
|
37
50
|
...rest
|
|
38
51
|
} = Astro.props;
|
|
@@ -64,7 +77,7 @@ const hasNext = current < total;
|
|
|
64
77
|
{...rest}
|
|
65
78
|
>
|
|
66
79
|
{hasPrev ? (
|
|
67
|
-
<a href={href(current - 1)} aria-label=
|
|
80
|
+
<a href={href(current - 1)} aria-label={previousLabel}>
|
|
68
81
|
<Icon name="chevron-left" size={17} stroke={2.3} />
|
|
69
82
|
</a>
|
|
70
83
|
) : (
|
|
@@ -86,7 +99,7 @@ const hasNext = current < total;
|
|
|
86
99
|
)}
|
|
87
100
|
|
|
88
101
|
{hasNext ? (
|
|
89
|
-
<a href={href(current + 1)} aria-label=
|
|
102
|
+
<a href={href(current + 1)} aria-label={nextLabel}>
|
|
90
103
|
<Icon name="chevron-right" size={17} stroke={2.3} />
|
|
91
104
|
</a>
|
|
92
105
|
) : (
|
|
@@ -166,55 +179,3 @@ const hasNext = current < total;
|
|
|
166
179
|
bindUiPager();
|
|
167
180
|
document.addEventListener("astro:page-load", bindUiPager);
|
|
168
181
|
</script>
|
|
169
|
-
|
|
170
|
-
<style>
|
|
171
|
-
@layer reset, tokens, base, components, motion, utilities;
|
|
172
|
-
|
|
173
|
-
@layer components {
|
|
174
|
-
.ui-pager {
|
|
175
|
-
display: flex;
|
|
176
|
-
align-items: center;
|
|
177
|
-
gap: var(--space-1);
|
|
178
|
-
font-size: var(--text-sm);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.ui-pager a,
|
|
182
|
-
.ui-pager span:not(.ui-pager__gap) {
|
|
183
|
-
display: inline-flex;
|
|
184
|
-
align-items: center;
|
|
185
|
-
justify-content: center;
|
|
186
|
-
min-width: 36px;
|
|
187
|
-
height: 36px;
|
|
188
|
-
padding: 0 var(--space-1);
|
|
189
|
-
border-radius: var(--radius-md);
|
|
190
|
-
color: var(--link);
|
|
191
|
-
text-decoration: none;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.ui-pager a:hover {
|
|
195
|
-
background: var(--surface-hover);
|
|
196
|
-
color: var(--link-hover);
|
|
197
|
-
}
|
|
198
|
-
.ui-pager a:focus-visible {
|
|
199
|
-
outline: 2px solid var(--focus);
|
|
200
|
-
outline-offset: 2px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.ui-pager a.is-current {
|
|
204
|
-
background: var(--primary);
|
|
205
|
-
color: var(--primary-fg);
|
|
206
|
-
font-weight: var(--weight-semibold);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.ui-pager .is-disabled {
|
|
210
|
-
color: var(--border);
|
|
211
|
-
cursor: not-allowed;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.ui-pager__gap {
|
|
215
|
-
min-width: 24px;
|
|
216
|
-
text-align: center;
|
|
217
|
-
color: var(--text-muted);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
</style>
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
+
import "../styles/product-card.css";
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* Product card. The whole card is one link, so the reader hears name, stock and
|
|
4
6
|
* price as a single stop. A corner ribbon marks new, promo, last items or
|
|
5
7
|
* member favourite or sold-out stock, one ribbon at a time. Ribbons are
|
|
6
8
|
* decorative; sold-out text is repeated for assistive technology and the card
|
|
7
9
|
* stays reachable.
|
|
10
|
+
*
|
|
11
|
+
* The ribbon words and the sold-out text default to Dutch. Pass `newLabel`,
|
|
12
|
+
* `promoLabel`, `lastLabel`, `favLabel` and `soldOutLabel` for another
|
|
13
|
+
* language. The screen-reader text is `soldOutLabel` followed by a full stop.
|
|
14
|
+
*
|
|
15
|
+
* React and Preact islands write the same markup and import
|
|
16
|
+
* `@kwtv/components/styles/product-card.css`.
|
|
8
17
|
*/
|
|
9
18
|
interface Props {
|
|
10
19
|
href: string;
|
|
@@ -24,6 +33,16 @@ interface Props {
|
|
|
24
33
|
sizes?: string;
|
|
25
34
|
/** AVIF srcset — when provided, a `<picture>` with an AVIF `<source>` is rendered. */
|
|
26
35
|
avifSrcset?: string;
|
|
36
|
+
/** Ribbon text for `ribbon="new"`. Defaults to "Nieuw". */
|
|
37
|
+
newLabel?: string;
|
|
38
|
+
/** Ribbon text for `ribbon="promo"`. Defaults to "Promo". */
|
|
39
|
+
promoLabel?: string;
|
|
40
|
+
/** Ribbon text for `ribbon="last"`. Defaults to "Laatste". */
|
|
41
|
+
lastLabel?: string;
|
|
42
|
+
/** Ribbon text for `ribbon="fav"`. Defaults to "Favoriet". */
|
|
43
|
+
favLabel?: string;
|
|
44
|
+
/** Sold-out ribbon and screen-reader text. Defaults to "Uitverkocht". */
|
|
45
|
+
soldOutLabel?: string;
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
const {
|
|
@@ -40,14 +59,19 @@ const {
|
|
|
40
59
|
srcset,
|
|
41
60
|
sizes,
|
|
42
61
|
avifSrcset,
|
|
62
|
+
newLabel = "Nieuw",
|
|
63
|
+
promoLabel = "Promo",
|
|
64
|
+
lastLabel = "Laatste",
|
|
65
|
+
favLabel = "Favoriet",
|
|
66
|
+
soldOutLabel = "Uitverkocht",
|
|
43
67
|
...rest
|
|
44
68
|
} = Astro.props;
|
|
45
69
|
|
|
46
70
|
const ribbonLabel: Record<NonNullable<Props["ribbon"]>, string> = {
|
|
47
|
-
new:
|
|
48
|
-
promo:
|
|
49
|
-
last:
|
|
50
|
-
fav:
|
|
71
|
+
new: newLabel,
|
|
72
|
+
promo: promoLabel,
|
|
73
|
+
last: lastLabel,
|
|
74
|
+
fav: favLabel,
|
|
51
75
|
};
|
|
52
76
|
|
|
53
77
|
const classes = ["ui-product", soldOut && "is-disabled", className]
|
|
@@ -61,7 +85,7 @@ const classes = ["ui-product", soldOut && "is-disabled", className]
|
|
|
61
85
|
class={`ui-ribbon ui-ribbon--${soldOut ? "sold-out" : ribbon}`}
|
|
62
86
|
aria-hidden="true"
|
|
63
87
|
>
|
|
64
|
-
<i>{soldOut ?
|
|
88
|
+
<i>{soldOut ? soldOutLabel : ribbonLabel[ribbon!]}</i>
|
|
65
89
|
</span>
|
|
66
90
|
)}
|
|
67
91
|
{avifSrcset ? (
|
|
@@ -91,146 +115,10 @@ const classes = ["ui-product", soldOut && "is-disabled", className]
|
|
|
91
115
|
<span class="ui-product__body">
|
|
92
116
|
<span class="ui-product__title">{title}</span>
|
|
93
117
|
{meta && <span class="ui-product__meta">{meta}</span>}
|
|
94
|
-
{soldOut && <span class="ui-sr-only">
|
|
118
|
+
{soldOut && <span class="ui-sr-only">{soldOutLabel}.</span>}
|
|
95
119
|
<span class="ui-product__price">
|
|
96
120
|
{oldPrice && <s>{oldPrice}</s>}
|
|
97
121
|
{price}
|
|
98
122
|
</span>
|
|
99
123
|
</span>
|
|
100
124
|
</a>
|
|
101
|
-
|
|
102
|
-
<style>
|
|
103
|
-
@layer reset, tokens, base, components, motion, utilities;
|
|
104
|
-
|
|
105
|
-
@layer components {
|
|
106
|
-
.ui-product {
|
|
107
|
-
position: relative;
|
|
108
|
-
display: flex;
|
|
109
|
-
flex-direction: column;
|
|
110
|
-
border: 1px solid var(--border);
|
|
111
|
-
border-radius: var(--radius-lg);
|
|
112
|
-
overflow: hidden;
|
|
113
|
-
background: var(--surface);
|
|
114
|
-
color: var(--text);
|
|
115
|
-
text-decoration: none;
|
|
116
|
-
transition: border-color var(--duration-fast) var(--ease-standard);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.ui-product:hover,
|
|
120
|
-
.ui-product.is-hover {
|
|
121
|
-
border-color: var(--border-strong);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.ui-product:focus-visible {
|
|
125
|
-
outline: 2px solid var(--focus);
|
|
126
|
-
outline-offset: 2px;
|
|
127
|
-
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.ui-product__img {
|
|
131
|
-
display: block;
|
|
132
|
-
width: 100%;
|
|
133
|
-
aspect-ratio: 4 / 3;
|
|
134
|
-
object-fit: cover;
|
|
135
|
-
background: var(--surface-hover);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.ui-product__body {
|
|
139
|
-
display: flex;
|
|
140
|
-
flex-direction: column;
|
|
141
|
-
gap: var(--space-1);
|
|
142
|
-
padding: var(--space-3) var(--space-4) var(--space-4);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.ui-product__title {
|
|
146
|
-
font-size: var(--text-base);
|
|
147
|
-
font-weight: var(--weight-semibold);
|
|
148
|
-
color: var(--text);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.ui-product__meta {
|
|
152
|
-
font-size: var(--text-xs);
|
|
153
|
-
color: var(--text-muted);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.ui-product__price {
|
|
157
|
-
display: flex;
|
|
158
|
-
align-items: baseline;
|
|
159
|
-
gap: var(--space-2);
|
|
160
|
-
margin-top: var(--space-1);
|
|
161
|
-
font-size: var(--text-lg);
|
|
162
|
-
font-weight: var(--weight-semibold);
|
|
163
|
-
font-variant-numeric: tabular-nums;
|
|
164
|
-
color: var(--text);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.ui-product__price s {
|
|
168
|
-
font-size: var(--text-sm);
|
|
169
|
-
font-weight: var(--weight-regular);
|
|
170
|
-
color: var(--text-muted);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* Sold out: card stays readable and clickable. */
|
|
174
|
-
.ui-product.is-disabled .ui-product__img {
|
|
175
|
-
opacity: 0.7;
|
|
176
|
-
}
|
|
177
|
-
.ui-product.is-disabled .ui-product__price {
|
|
178
|
-
color: var(--text-muted);
|
|
179
|
-
font-weight: var(--weight-regular);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/* Ribbon. One per card, decorative. 112 px box. */
|
|
183
|
-
.ui-ribbon {
|
|
184
|
-
position: absolute;
|
|
185
|
-
top: 0;
|
|
186
|
-
left: 0;
|
|
187
|
-
width: 112px;
|
|
188
|
-
height: 112px;
|
|
189
|
-
overflow: hidden;
|
|
190
|
-
pointer-events: none;
|
|
191
|
-
z-index: 1;
|
|
192
|
-
}
|
|
193
|
-
.ui-ribbon i {
|
|
194
|
-
position: absolute;
|
|
195
|
-
top: 22px;
|
|
196
|
-
left: -34px;
|
|
197
|
-
width: 140px;
|
|
198
|
-
transform: rotate(-45deg);
|
|
199
|
-
text-align: center;
|
|
200
|
-
font-size: var(--text-2xs);
|
|
201
|
-
font-weight: var(--weight-bold);
|
|
202
|
-
letter-spacing: var(--tracking-wider);
|
|
203
|
-
text-transform: uppercase;
|
|
204
|
-
font-style: normal;
|
|
205
|
-
padding: var(--space-1) 0;
|
|
206
|
-
color: var(--primary-fg);
|
|
207
|
-
background: var(--primary);
|
|
208
|
-
}
|
|
209
|
-
.ui-ribbon--new i {
|
|
210
|
-
background: var(--primary);
|
|
211
|
-
color: var(--primary-fg);
|
|
212
|
-
}
|
|
213
|
-
.ui-ribbon--promo i {
|
|
214
|
-
background: var(--accent);
|
|
215
|
-
color: var(--accent-fg);
|
|
216
|
-
}
|
|
217
|
-
.ui-ribbon--last i {
|
|
218
|
-
background: var(--warning);
|
|
219
|
-
color: var(--primary-fg);
|
|
220
|
-
}
|
|
221
|
-
.ui-ribbon--fav i {
|
|
222
|
-
background: var(--surface-accent);
|
|
223
|
-
color: var(--surface-accent-fg);
|
|
224
|
-
}
|
|
225
|
-
.ui-ribbon--sold-out i {
|
|
226
|
-
background: var(--danger-bg);
|
|
227
|
-
color: var(--danger);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
@media (prefers-reduced-motion: reduce) {
|
|
231
|
-
.ui-product {
|
|
232
|
-
transition: none;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
</style>
|
package/src/astro/Radio.astro
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import "../styles/filters.css";
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* Radio. Native control tinted with accent-color, wrapped in a label so the
|
|
4
6
|
* whole row is the hit area. One answer per group: give every radio in the
|
|
@@ -6,6 +8,9 @@
|
|
|
6
8
|
*
|
|
7
9
|
* Put the group in a <fieldset> with a <legend> in the consuming page so the
|
|
8
10
|
* question is announced before the options.
|
|
11
|
+
*
|
|
12
|
+
* React and Preact islands write the same markup and import
|
|
13
|
+
* `@kwtv/components/styles/filters.css`.
|
|
9
14
|
*/
|
|
10
15
|
interface Props {
|
|
11
16
|
label?: string;
|
|
@@ -45,49 +50,3 @@ const classes = ["ui-check", disabled && "is-disabled", className]
|
|
|
45
50
|
/>
|
|
46
51
|
<slot>{label}</slot>
|
|
47
52
|
</label>
|
|
48
|
-
|
|
49
|
-
<style>
|
|
50
|
-
@layer reset, tokens, base, components, motion, utilities;
|
|
51
|
-
|
|
52
|
-
@layer components {
|
|
53
|
-
.ui-check {
|
|
54
|
-
display: flex;
|
|
55
|
-
align-items: center;
|
|
56
|
-
gap: var(--space-2);
|
|
57
|
-
min-height: 32px;
|
|
58
|
-
font-family: var(--font-sans);
|
|
59
|
-
font-size: var(--text-sm);
|
|
60
|
-
color: var(--text);
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.ui-check input {
|
|
65
|
-
width: 17px;
|
|
66
|
-
height: 17px;
|
|
67
|
-
flex-shrink: 0;
|
|
68
|
-
accent-color: var(--primary);
|
|
69
|
-
cursor: pointer;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.ui-check input:focus-visible {
|
|
73
|
-
outline: 2px solid var(--focus);
|
|
74
|
-
outline-offset: 2px;
|
|
75
|
-
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.ui-check.is-disabled {
|
|
79
|
-
cursor: not-allowed;
|
|
80
|
-
color: var(--text-muted);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.ui-check.is-disabled input {
|
|
84
|
-
cursor: not-allowed;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@media (max-width: 700px), (pointer: coarse) {
|
|
88
|
-
.ui-check {
|
|
89
|
-
min-height: 40px;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
</style>
|
package/src/astro/Swatch.astro
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
+
import "../styles/filters.css";
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* Colour swatch. 30 px circle, 32 px on touch. A <button> carrying the colour
|
|
4
6
|
* name in `label` (→ aria-label), because the circle alone tells a colour-blind
|
|
5
7
|
* reader nothing. Selection adds a 2 px ring so it reads without colour.
|
|
8
|
+
* When selected, `selectedLabel` is added to the name after a comma.
|
|
9
|
+
*
|
|
10
|
+
* React and Preact islands write the same markup and import
|
|
11
|
+
* `@kwtv/components/styles/filters.css`.
|
|
6
12
|
*/
|
|
7
13
|
interface Props {
|
|
8
14
|
/** CSS colour value for the fill. */
|
|
@@ -11,6 +17,8 @@ interface Props {
|
|
|
11
17
|
label: string;
|
|
12
18
|
selected?: boolean;
|
|
13
19
|
disabled?: boolean;
|
|
20
|
+
/** Added to the accessible name when selected. Defaults to "geselecteerd". */
|
|
21
|
+
selectedLabel?: string;
|
|
14
22
|
type?: "button" | "submit";
|
|
15
23
|
class?: string;
|
|
16
24
|
}
|
|
@@ -20,6 +28,7 @@ const {
|
|
|
20
28
|
label,
|
|
21
29
|
selected = false,
|
|
22
30
|
disabled = false,
|
|
31
|
+
selectedLabel = "geselecteerd",
|
|
23
32
|
type = "button",
|
|
24
33
|
class: className,
|
|
25
34
|
...rest
|
|
@@ -34,7 +43,7 @@ const classes = [
|
|
|
34
43
|
.filter(Boolean)
|
|
35
44
|
.join(" ");
|
|
36
45
|
|
|
37
|
-
const ariaLabel = selected ? `${label},
|
|
46
|
+
const ariaLabel = selected ? `${label}, ${selectedLabel}` : label;
|
|
38
47
|
---
|
|
39
48
|
|
|
40
49
|
<button
|
|
@@ -46,59 +55,3 @@ const ariaLabel = selected ? `${label}, geselecteerd` : label;
|
|
|
46
55
|
disabled={disabled}
|
|
47
56
|
{...rest}
|
|
48
57
|
></button>
|
|
49
|
-
|
|
50
|
-
<style>
|
|
51
|
-
@layer reset, tokens, base, components, motion, utilities;
|
|
52
|
-
|
|
53
|
-
@layer components {
|
|
54
|
-
.ui-swatch {
|
|
55
|
-
width: 30px;
|
|
56
|
-
height: 30px;
|
|
57
|
-
padding: 0;
|
|
58
|
-
background: var(--swatch);
|
|
59
|
-
border: 1px solid var(--border);
|
|
60
|
-
border-radius: var(--radius-full);
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
transition: box-shadow var(--duration-fast) var(--ease-standard);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.ui-swatch:hover,
|
|
66
|
-
.ui-swatch.is-hover {
|
|
67
|
-
box-shadow: 0 0 0 2px var(--border-strong);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.ui-swatch:focus-visible,
|
|
71
|
-
.ui-swatch.is-focus {
|
|
72
|
-
outline: 2px solid var(--focus);
|
|
73
|
-
outline-offset: 2px;
|
|
74
|
-
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* Selected: 2 px ring, readable without colour. */
|
|
78
|
-
.ui-swatch.is-selected,
|
|
79
|
-
.ui-swatch[aria-pressed="true"] {
|
|
80
|
-
box-shadow:
|
|
81
|
-
0 0 0 2px var(--surface),
|
|
82
|
-
0 0 0 4px var(--border-strong);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.ui-swatch.is-disabled,
|
|
86
|
-
.ui-swatch:disabled {
|
|
87
|
-
opacity: 0.5;
|
|
88
|
-
cursor: not-allowed;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@media (prefers-reduced-motion: reduce) {
|
|
92
|
-
.ui-swatch {
|
|
93
|
-
transition: none;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@media (max-width: 700px), (pointer: coarse) {
|
|
98
|
-
.ui-swatch {
|
|
99
|
-
width: 32px;
|
|
100
|
-
height: 32px;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
</style>
|
package/src/react/Drawer.css
CHANGED
|
@@ -74,6 +74,31 @@
|
|
|
74
74
|
flex-shrink: 0;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/* Title row. The title sits left of the close button, on the row inset. */
|
|
78
|
+
.ui-drawer__head {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
gap: var(--space-3);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ui-drawer__head .ui-drawer__close {
|
|
86
|
+
align-self: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ui-drawer__title {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
min-height: 44px;
|
|
93
|
+
margin: 0;
|
|
94
|
+
padding: 0 var(--space-3);
|
|
95
|
+
font-family: var(--font-sans);
|
|
96
|
+
font-size: var(--text-base);
|
|
97
|
+
line-height: var(--leading-base);
|
|
98
|
+
font-weight: var(--weight-semibold);
|
|
99
|
+
color: var(--text);
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
/* Placement. The panel is fixed to one edge, over a scrim that stays dark
|
|
78
103
|
in both themes, like the modal scrim. */
|
|
79
104
|
.ui-drawer-scrim {
|
|
@@ -120,6 +145,15 @@
|
|
|
120
145
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
|
121
146
|
}
|
|
122
147
|
|
|
148
|
+
/* Full width on small screens. Bottom and the panel under the header are */
|
|
149
|
+
/* already full width. */
|
|
150
|
+
@media (max-width: 700px) {
|
|
151
|
+
.ui-drawer--full.ui-drawer--start,
|
|
152
|
+
.ui-drawer--full.ui-drawer--end {
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
123
157
|
@keyframes ui-drawer-fade {
|
|
124
158
|
from {
|
|
125
159
|
opacity: 0;
|
package/src/react/Drawer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, type ReactNode } from "react";
|
|
1
|
+
import { useEffect, useId, useRef, type ReactNode } from "react";
|
|
2
2
|
import "../styles/controls.css";
|
|
3
3
|
import "./Drawer.css";
|
|
4
4
|
|
|
@@ -21,6 +21,12 @@ import "./Drawer.css";
|
|
|
21
21
|
* Pass `closeLabel` to show a close button at the top of the drawer. A drawer
|
|
22
22
|
* with a `placement` covers its trigger, so give it one.
|
|
23
23
|
*
|
|
24
|
+
* Pass `title` to show a heading in the top row, left of the close button.
|
|
25
|
+
* The title then names the drawer through aria-labelledby, in place of
|
|
26
|
+
* `label`. Pass `size="full"` to let a start or end drawer fill the viewport
|
|
27
|
+
* width on screens up to 700 px wide. Wider screens keep the 360 px panel.
|
|
28
|
+
* Without these two props the drawer renders as before.
|
|
29
|
+
*
|
|
24
30
|
* Uses only hooks and DOM calls that preact/compat supports.
|
|
25
31
|
*/
|
|
26
32
|
|
|
@@ -56,6 +62,16 @@ export interface DrawerProps {
|
|
|
56
62
|
placement?: "start" | "end" | "bottom";
|
|
57
63
|
/** Shows a close button with this accessible name, like "Menu sluiten". */
|
|
58
64
|
closeLabel?: string;
|
|
65
|
+
/**
|
|
66
|
+
* A visible h2 in the top row, beside the close button. It becomes the
|
|
67
|
+
* accessible name of the drawer, in place of `label`.
|
|
68
|
+
*/
|
|
69
|
+
title?: string;
|
|
70
|
+
/**
|
|
71
|
+
* "full" fills the viewport width on screens up to 700 px wide. Leave unset
|
|
72
|
+
* for the default width.
|
|
73
|
+
*/
|
|
74
|
+
size?: "default" | "full";
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
const FOCUSABLE =
|
|
@@ -87,7 +103,10 @@ export default function Drawer({
|
|
|
87
103
|
label = "Menu",
|
|
88
104
|
placement,
|
|
89
105
|
closeLabel,
|
|
106
|
+
title,
|
|
107
|
+
size = "default",
|
|
90
108
|
}: DrawerProps) {
|
|
109
|
+
const titleId = useId();
|
|
91
110
|
const rootRef = useRef<HTMLElement>(null);
|
|
92
111
|
const scrimRef = useRef<HTMLDivElement>(null);
|
|
93
112
|
const onCloseRef = useRef(onClose);
|
|
@@ -168,33 +187,45 @@ export default function Drawer({
|
|
|
168
187
|
|
|
169
188
|
if (!open) return null;
|
|
170
189
|
|
|
190
|
+
const closeButton = closeLabel && (
|
|
191
|
+
<button
|
|
192
|
+
className="ui-iconbtn ui-drawer__close"
|
|
193
|
+
type="button"
|
|
194
|
+
aria-label={closeLabel}
|
|
195
|
+
onClick={onClose}
|
|
196
|
+
>
|
|
197
|
+
<svg
|
|
198
|
+
width="20"
|
|
199
|
+
height="20"
|
|
200
|
+
viewBox="0 0 24 24"
|
|
201
|
+
fill="none"
|
|
202
|
+
stroke="currentColor"
|
|
203
|
+
strokeWidth="1.8"
|
|
204
|
+
strokeLinecap="round"
|
|
205
|
+
aria-hidden="true"
|
|
206
|
+
>
|
|
207
|
+
<path d="M7 7l10 10M17 7L7 17" />
|
|
208
|
+
</svg>
|
|
209
|
+
</button>
|
|
210
|
+
);
|
|
211
|
+
|
|
171
212
|
const panel = (
|
|
172
213
|
<nav
|
|
173
|
-
className={`ui-drawer${placement ? ` ui-drawer--${placement}` : ""}`}
|
|
174
|
-
aria-label={label}
|
|
214
|
+
className={`ui-drawer${placement ? ` ui-drawer--${placement}` : ""}${size === "full" ? " ui-drawer--full" : ""}`}
|
|
215
|
+
aria-label={title ? undefined : label}
|
|
216
|
+
aria-labelledby={title ? titleId : undefined}
|
|
175
217
|
ref={rootRef}
|
|
176
218
|
tabIndex={-1}
|
|
177
219
|
>
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
className="ui-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
height="20"
|
|
188
|
-
viewBox="0 0 24 24"
|
|
189
|
-
fill="none"
|
|
190
|
-
stroke="currentColor"
|
|
191
|
-
strokeWidth="1.8"
|
|
192
|
-
strokeLinecap="round"
|
|
193
|
-
aria-hidden="true"
|
|
194
|
-
>
|
|
195
|
-
<path d="M7 7l10 10M17 7L7 17" />
|
|
196
|
-
</svg>
|
|
197
|
-
</button>
|
|
220
|
+
{title ? (
|
|
221
|
+
<div className="ui-drawer__head">
|
|
222
|
+
<h2 className="ui-drawer__title" id={titleId}>
|
|
223
|
+
{title}
|
|
224
|
+
</h2>
|
|
225
|
+
{closeButton}
|
|
226
|
+
</div>
|
|
227
|
+
) : (
|
|
228
|
+
closeButton
|
|
198
229
|
)}
|
|
199
230
|
{groups
|
|
200
231
|
? groups.map((group, gi) => (
|