@kwtv/components 0.1.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 +77 -0
- package/package.json +32 -0
- package/src/astro/Accordion.astro +25 -0
- package/src/astro/AccordionItem.astro +77 -0
- package/src/astro/ActiveFilterTags.astro +65 -0
- package/src/astro/Alert.astro +93 -0
- package/src/astro/Badge.astro +138 -0
- package/src/astro/Band.astro +83 -0
- package/src/astro/Breadcrumb.astro +74 -0
- package/src/astro/Button.astro +217 -0
- package/src/astro/Card.astro +115 -0
- package/src/astro/CartLine.astro +112 -0
- package/src/astro/Checkbox.astro +89 -0
- package/src/astro/Chip.astro +130 -0
- package/src/astro/ChipRail.astro +24 -0
- package/src/astro/DotGrid.astro +62 -0
- package/src/astro/EmptyState.astro +53 -0
- package/src/astro/FilterGroup.astro +58 -0
- package/src/astro/Filters.astro +84 -0
- package/src/astro/Footer.astro +237 -0
- package/src/astro/FooterColumn.astro +64 -0
- package/src/astro/Header.astro +116 -0
- package/src/astro/Icon.astro +72 -0
- package/src/astro/IconButton.astro +117 -0
- package/src/astro/Marquee.astro +72 -0
- package/src/astro/NavDropdown.astro +164 -0
- package/src/astro/NavLink.astro +94 -0
- package/src/astro/Newsletter.astro +118 -0
- package/src/astro/Pager.astro +195 -0
- package/src/astro/PickCard.astro +142 -0
- package/src/astro/ProductCard.astro +195 -0
- package/src/astro/Radio.astro +89 -0
- package/src/astro/Skeleton.astro +80 -0
- package/src/astro/Social.astro +55 -0
- package/src/astro/StepIndicator.astro +104 -0
- package/src/astro/Sticker.astro +80 -0
- package/src/astro/Swatch.astro +100 -0
- package/src/astro/SwipeCarousel.astro +14 -0
- package/src/astro/Table.astro +117 -0
- package/src/astro/TableCard.astro +63 -0
- package/src/astro/TextField.astro +231 -0
- package/src/astro/Totals.astro +92 -0
- package/src/react/Drawer.css +67 -0
- package/src/react/Drawer.tsx +115 -0
- package/src/react/Modal.css +116 -0
- package/src/react/Modal.tsx +152 -0
- package/src/react/NewsCarousel.css +86 -0
- package/src/react/NewsCarousel.tsx +97 -0
- package/src/react/QuantityStepper.css +93 -0
- package/src/react/QuantityStepper.tsx +143 -0
- package/src/react/Select.css +183 -0
- package/src/react/Select.tsx +389 -0
- package/src/react/Toast.css +140 -0
- package/src/react/Toast.tsx +157 -0
- package/src/styles/base.css +39 -0
- package/src/styles/controls.css +211 -0
- package/src/styles/motion.css +238 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Button. Four hierarchies, three sizes. One primary action per view.
|
|
6
|
+
* Renders a <button> by default, or an <a> when `href` is set.
|
|
7
|
+
*
|
|
8
|
+
* State classes (.is-hover, .is-focus, .is-active, .is-loading) pin a visual
|
|
9
|
+
* state for specimens and documentation. The real :hover / :focus-visible /
|
|
10
|
+
* :active states render identically.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
variant?: "primary" | "secondary" | "accent" | "ghost";
|
|
14
|
+
size?: "sm" | "md" | "lg";
|
|
15
|
+
block?: boolean;
|
|
16
|
+
href?: string;
|
|
17
|
+
type?: "button" | "submit" | "reset";
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
/** Trailing icon name, rendered after the label. */
|
|
21
|
+
icon?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
variant = "primary",
|
|
27
|
+
size = "md",
|
|
28
|
+
block = false,
|
|
29
|
+
href,
|
|
30
|
+
type = "button",
|
|
31
|
+
disabled = false,
|
|
32
|
+
loading = false,
|
|
33
|
+
icon,
|
|
34
|
+
class: className,
|
|
35
|
+
...rest
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const Tag = href ? "a" : "button";
|
|
39
|
+
|
|
40
|
+
const classes = [
|
|
41
|
+
"ui-btn",
|
|
42
|
+
`ui-btn--${size}`,
|
|
43
|
+
`ui-btn--${variant}`,
|
|
44
|
+
block && "ui-btn--block",
|
|
45
|
+
loading && "is-loading",
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(" ");
|
|
50
|
+
|
|
51
|
+
const tagProps = href
|
|
52
|
+
? { href, ...(disabled ? { "aria-disabled": "true" } : {}) }
|
|
53
|
+
: { type, ...(disabled ? { disabled: true } : {}) };
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<Tag
|
|
57
|
+
class={classes}
|
|
58
|
+
aria-busy={loading ? "true" : undefined}
|
|
59
|
+
{...tagProps}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
<span class="ui-btn__label"><slot /></span>
|
|
63
|
+
{icon && (
|
|
64
|
+
<span class="ui-btn__icon" aria-hidden="true">
|
|
65
|
+
<Icon name={icon as any} size="1em" stroke={1.8} />
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
{loading && <span class="ui-btn__spinner" aria-hidden="true"></span>}
|
|
69
|
+
</Tag>
|
|
70
|
+
|
|
71
|
+
<style>
|
|
72
|
+
.ui-btn {
|
|
73
|
+
--_pad-x: var(--space-5);
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
gap: var(--space-2);
|
|
78
|
+
position: relative;
|
|
79
|
+
min-height: 40px;
|
|
80
|
+
padding: var(--space-2) var(--_pad-x);
|
|
81
|
+
border: 1px solid transparent;
|
|
82
|
+
border-radius: var(--radius-md);
|
|
83
|
+
font-family: var(--font-sans);
|
|
84
|
+
font-size: var(--text-sm);
|
|
85
|
+
font-weight: var(--weight-semibold);
|
|
86
|
+
line-height: 1;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition:
|
|
90
|
+
background-color var(--duration-fast) var(--ease-standard),
|
|
91
|
+
border-color var(--duration-fast) var(--ease-standard),
|
|
92
|
+
transform var(--duration-fast) var(--ease-standard);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Sizes: 32 / 40 / 48 px. */
|
|
96
|
+
.ui-btn--sm {
|
|
97
|
+
min-height: 32px;
|
|
98
|
+
--_pad-x: var(--space-4);
|
|
99
|
+
font-size: var(--text-xs);
|
|
100
|
+
}
|
|
101
|
+
.ui-btn--md {
|
|
102
|
+
min-height: 40px;
|
|
103
|
+
}
|
|
104
|
+
.ui-btn--lg {
|
|
105
|
+
min-height: 48px;
|
|
106
|
+
--_pad-x: var(--space-6);
|
|
107
|
+
font-size: var(--text-base);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.ui-btn--block {
|
|
111
|
+
display: flex;
|
|
112
|
+
width: 100%;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Primary. */
|
|
116
|
+
.ui-btn--primary {
|
|
117
|
+
background: var(--primary);
|
|
118
|
+
color: var(--primary-fg);
|
|
119
|
+
}
|
|
120
|
+
.ui-btn--primary:hover,
|
|
121
|
+
.ui-btn--primary.is-hover {
|
|
122
|
+
background: var(--primary-hover);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Secondary. */
|
|
126
|
+
.ui-btn--secondary {
|
|
127
|
+
background: transparent;
|
|
128
|
+
border-color: var(--border-strong);
|
|
129
|
+
color: var(--text);
|
|
130
|
+
}
|
|
131
|
+
.ui-btn--secondary:hover,
|
|
132
|
+
.ui-btn--secondary.is-hover {
|
|
133
|
+
background: var(--surface-hover);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Accent. One call to action per page. */
|
|
137
|
+
.ui-btn--accent {
|
|
138
|
+
background: var(--accent);
|
|
139
|
+
color: var(--accent-fg);
|
|
140
|
+
}
|
|
141
|
+
.ui-btn--accent:hover,
|
|
142
|
+
.ui-btn--accent.is-hover {
|
|
143
|
+
background: var(--accent-hover);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Ghost. Low-weight actions. */
|
|
147
|
+
.ui-btn--ghost {
|
|
148
|
+
background: transparent;
|
|
149
|
+
color: var(--link);
|
|
150
|
+
padding-inline: var(--space-3);
|
|
151
|
+
}
|
|
152
|
+
.ui-btn--ghost:hover,
|
|
153
|
+
.ui-btn--ghost.is-hover {
|
|
154
|
+
background: var(--surface-hover);
|
|
155
|
+
color: var(--link-hover);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Focus: 2 px ring plus 4 px halo. */
|
|
159
|
+
.ui-btn:focus-visible,
|
|
160
|
+
.ui-btn.is-focus {
|
|
161
|
+
outline: 2px solid var(--focus);
|
|
162
|
+
outline-offset: 2px;
|
|
163
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Pressed: 1 px downward shift. */
|
|
167
|
+
.ui-btn:active,
|
|
168
|
+
.ui-btn.is-active {
|
|
169
|
+
transform: translateY(1px);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Disabled. */
|
|
173
|
+
.ui-btn:disabled,
|
|
174
|
+
.ui-btn[aria-disabled="true"] {
|
|
175
|
+
opacity: 0.5;
|
|
176
|
+
cursor: not-allowed;
|
|
177
|
+
pointer-events: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Loading: label hidden, spinner shown. */
|
|
181
|
+
.ui-btn.is-loading {
|
|
182
|
+
cursor: progress;
|
|
183
|
+
}
|
|
184
|
+
.ui-btn.is-loading .ui-btn__label,
|
|
185
|
+
.ui-btn.is-loading .ui-btn__icon {
|
|
186
|
+
visibility: hidden;
|
|
187
|
+
}
|
|
188
|
+
.ui-btn__spinner {
|
|
189
|
+
position: absolute;
|
|
190
|
+
width: 16px;
|
|
191
|
+
height: 16px;
|
|
192
|
+
border: 2px solid currentColor;
|
|
193
|
+
border-top-color: transparent;
|
|
194
|
+
border-radius: var(--radius-full);
|
|
195
|
+
animation: ui-btn-spin var(--duration-slower) linear infinite;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.ui-btn__icon {
|
|
199
|
+
display: inline-flex;
|
|
200
|
+
font-size: 1em;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@keyframes ui-btn-spin {
|
|
204
|
+
to {
|
|
205
|
+
transform: rotate(360deg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@media (prefers-reduced-motion: reduce) {
|
|
210
|
+
.ui-btn {
|
|
211
|
+
transition: none;
|
|
212
|
+
}
|
|
213
|
+
.ui-btn__spinner {
|
|
214
|
+
animation-duration: 1200ms;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Card. Surface for a block of related content: eyebrow, title, body, footer.
|
|
4
|
+
* Raised for a summary that must sit above the page, accent for a rule the
|
|
5
|
+
* reader has to notice, flat for grouping inside another surface.
|
|
6
|
+
*
|
|
7
|
+
* The title renders as a real heading so the document outline stays navigable.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
variant?: "default" | "raised" | "accent" | "flat";
|
|
11
|
+
eyebrow?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
titleTag?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
variant = "default",
|
|
19
|
+
eyebrow,
|
|
20
|
+
title,
|
|
21
|
+
titleTag = "h3",
|
|
22
|
+
class: className,
|
|
23
|
+
...rest
|
|
24
|
+
} = Astro.props;
|
|
25
|
+
|
|
26
|
+
const Title = titleTag;
|
|
27
|
+
|
|
28
|
+
const classes = ["ui-card", variant !== "default" && `ui-card--${variant}`, className]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(" ");
|
|
31
|
+
|
|
32
|
+
const hasFooter = Astro.slots.has("footer");
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<div class={classes} {...rest}>
|
|
36
|
+
{eyebrow && <span class="ui-card__eyebrow">{eyebrow}</span>}
|
|
37
|
+
{title && <Title class="ui-card__title">{title}</Title>}
|
|
38
|
+
<span class="ui-card__body"><slot /></span>
|
|
39
|
+
{hasFooter && (
|
|
40
|
+
<div class="ui-card__footer">
|
|
41
|
+
<slot name="footer" />
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.ui-card {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: var(--space-2);
|
|
51
|
+
padding: var(--space-5);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: var(--radius-lg);
|
|
54
|
+
background: var(--surface);
|
|
55
|
+
color: var(--text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ui-card__eyebrow {
|
|
59
|
+
font-family: var(--font-mono);
|
|
60
|
+
font-size: var(--text-2xs);
|
|
61
|
+
line-height: var(--leading-2xs);
|
|
62
|
+
font-weight: var(--weight-medium);
|
|
63
|
+
letter-spacing: var(--tracking-widest);
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
color: var(--text-muted);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ui-card__title {
|
|
69
|
+
margin: 0;
|
|
70
|
+
font-size: var(--text-lg);
|
|
71
|
+
line-height: var(--leading-lg);
|
|
72
|
+
font-weight: var(--weight-semibold);
|
|
73
|
+
letter-spacing: var(--tracking-tight);
|
|
74
|
+
color: var(--text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-card__body {
|
|
78
|
+
font-size: var(--text-sm);
|
|
79
|
+
line-height: var(--leading-sm);
|
|
80
|
+
color: var(--text-muted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ui-card__footer {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: var(--space-2);
|
|
87
|
+
margin-top: var(--space-2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Raised: cart summary, order total. */
|
|
91
|
+
.ui-card--raised {
|
|
92
|
+
border-color: transparent;
|
|
93
|
+
box-shadow: var(--shadow-lg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Accent: sage surface, ink-dark text kept at 4.5:1. Buttons inside keep
|
|
97
|
+
their own foreground colour. */
|
|
98
|
+
.ui-card--accent {
|
|
99
|
+
background: var(--surface-accent);
|
|
100
|
+
border-color: var(--border-strong);
|
|
101
|
+
color: var(--surface-accent-fg);
|
|
102
|
+
}
|
|
103
|
+
.ui-card--accent .ui-card__eyebrow,
|
|
104
|
+
.ui-card--accent .ui-card__title,
|
|
105
|
+
.ui-card--accent .ui-card__body {
|
|
106
|
+
color: var(--surface-accent-fg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Flat: no fill, grouping only. */
|
|
110
|
+
.ui-card--flat {
|
|
111
|
+
background: transparent;
|
|
112
|
+
border-color: transparent;
|
|
113
|
+
padding-inline: 0;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Cart line. One row per article: photo, name, the chosen options named in
|
|
4
|
+
* text, a quantity stepper and the line price.
|
|
5
|
+
*
|
|
6
|
+
* The options belong in the note as words ("Maat 140 · sage"), so they are not
|
|
7
|
+
* only visible in the photo. The quantity stepper is a slot: drop in the React
|
|
8
|
+
* QuantityStepper or a static one.
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
img: string;
|
|
12
|
+
alt: string;
|
|
13
|
+
title: string;
|
|
14
|
+
/** Chosen options in plain text, e.g. 'Maat 140 · sage · naam "Lore"'. */
|
|
15
|
+
note?: string;
|
|
16
|
+
price: string;
|
|
17
|
+
removeHref?: string;
|
|
18
|
+
removeLabel?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
img,
|
|
24
|
+
alt,
|
|
25
|
+
title,
|
|
26
|
+
note,
|
|
27
|
+
price,
|
|
28
|
+
removeHref = "#",
|
|
29
|
+
removeLabel = "Verwijderen",
|
|
30
|
+
class: className,
|
|
31
|
+
...rest
|
|
32
|
+
} = Astro.props;
|
|
33
|
+
|
|
34
|
+
const classes = ["ui-line", className].filter(Boolean).join(" ");
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<div class={classes} {...rest}>
|
|
38
|
+
<img class="ui-line__thumb" loading="lazy" decoding="async" src={img} alt={alt} />
|
|
39
|
+
<span class="ui-line__grow">
|
|
40
|
+
<b class="ui-line__title">{title}</b>
|
|
41
|
+
{note && <span class="ui-line__note">{note}</span>}
|
|
42
|
+
<a class="ui-line__remove" href={removeHref}>{removeLabel}</a>
|
|
43
|
+
</span>
|
|
44
|
+
<span class="ui-line__qty"><slot name="qty" /></span>
|
|
45
|
+
<b class="ui-line__price">{price}</b>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.ui-line {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: flex-start;
|
|
52
|
+
gap: var(--space-4);
|
|
53
|
+
padding-block: var(--space-4-5);
|
|
54
|
+
border-top: 1px solid var(--border);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ui-line__thumb {
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
width: 72px;
|
|
60
|
+
height: 72px;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
border-radius: var(--radius-md);
|
|
63
|
+
border: 1px solid var(--border);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-line__grow {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: var(--space-1);
|
|
70
|
+
flex: 1 1 auto;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ui-line__title {
|
|
75
|
+
font-family: var(--font-sans);
|
|
76
|
+
font-size: var(--text-base);
|
|
77
|
+
font-weight: var(--weight-semibold);
|
|
78
|
+
color: var(--text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-line__note {
|
|
82
|
+
font-size: var(--text-sm);
|
|
83
|
+
color: var(--text-muted);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ui-line__remove {
|
|
87
|
+
align-self: flex-start;
|
|
88
|
+
font-size: var(--text-xs);
|
|
89
|
+
color: var(--link);
|
|
90
|
+
}
|
|
91
|
+
.ui-line__remove:hover {
|
|
92
|
+
color: var(--link-hover);
|
|
93
|
+
}
|
|
94
|
+
.ui-line__remove:focus-visible {
|
|
95
|
+
outline: 2px solid var(--focus);
|
|
96
|
+
outline-offset: 2px;
|
|
97
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
98
|
+
border-radius: var(--radius-sm);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ui-line__qty {
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ui-line__price {
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
font-size: var(--text-base);
|
|
108
|
+
font-weight: var(--weight-semibold);
|
|
109
|
+
font-variant-numeric: tabular-nums;
|
|
110
|
+
color: var(--text);
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Checkbox. Native control tinted with accent-color, wrapped in a label so the
|
|
4
|
+
* whole row is the hit area. 17 px box, 32 px row. Disabled rows keep their
|
|
5
|
+
* text legible.
|
|
6
|
+
*
|
|
7
|
+
* When several checkboxes form a question, put them in a <fieldset> with a
|
|
8
|
+
* <legend> in the consuming page so the question is announced first.
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
label?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
id?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
label,
|
|
22
|
+
name,
|
|
23
|
+
value,
|
|
24
|
+
checked = false,
|
|
25
|
+
disabled = false,
|
|
26
|
+
id,
|
|
27
|
+
class: className,
|
|
28
|
+
...rest
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const classes = ["ui-check", disabled && "is-disabled", className]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" ");
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<label class={classes}>
|
|
37
|
+
<input
|
|
38
|
+
type="checkbox"
|
|
39
|
+
name={name}
|
|
40
|
+
value={value}
|
|
41
|
+
id={id}
|
|
42
|
+
checked={checked}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
{...rest}
|
|
45
|
+
/>
|
|
46
|
+
<slot>{label}</slot>
|
|
47
|
+
</label>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.ui-check {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: var(--space-2);
|
|
54
|
+
min-height: 32px;
|
|
55
|
+
font-family: var(--font-sans);
|
|
56
|
+
font-size: var(--text-sm);
|
|
57
|
+
color: var(--text);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ui-check input {
|
|
62
|
+
width: 17px;
|
|
63
|
+
height: 17px;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
accent-color: var(--primary);
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ui-check input:focus-visible {
|
|
70
|
+
outline: 2px solid var(--focus);
|
|
71
|
+
outline-offset: 2px;
|
|
72
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ui-check.is-disabled {
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
color: var(--text-muted);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ui-check.is-disabled input {
|
|
81
|
+
cursor: not-allowed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (max-width: 700px), (pointer: coarse) {
|
|
85
|
+
.ui-check {
|
|
86
|
+
min-height: 40px;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Chip. Size selection and filter shortcuts. A <button> with aria-pressed, not
|
|
4
|
+
* a link. Selection adds a 2 px border so it reads without colour. Sold-out
|
|
5
|
+
* sizes stay visible and struck through (`strikethrough` + `disabled`): the
|
|
6
|
+
* reader learns the size exists but is gone.
|
|
7
|
+
*
|
|
8
|
+
* The `--filter` variant is the 32 px shortcut that opens the filter sheet on
|
|
9
|
+
* mobile.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
variant?: "default" | "filter";
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
strikethrough?: boolean;
|
|
16
|
+
type?: "button" | "submit";
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
variant = "default",
|
|
22
|
+
selected = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
strikethrough = false,
|
|
25
|
+
type = "button",
|
|
26
|
+
class: className,
|
|
27
|
+
...rest
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
const classes = [
|
|
31
|
+
"ui-chip",
|
|
32
|
+
variant === "filter" && "ui-chip--filter",
|
|
33
|
+
selected && "is-selected",
|
|
34
|
+
disabled && "is-disabled",
|
|
35
|
+
strikethrough && "ui-chip--struck",
|
|
36
|
+
className,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(" ");
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<button
|
|
43
|
+
class={classes}
|
|
44
|
+
type={type}
|
|
45
|
+
aria-pressed={String(selected)}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
<slot />
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.ui-chip {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
min-height: 36px;
|
|
58
|
+
padding: 0 var(--space-4);
|
|
59
|
+
background: var(--surface);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-radius: var(--radius-md);
|
|
62
|
+
font-family: var(--font-sans);
|
|
63
|
+
font-size: var(--text-sm);
|
|
64
|
+
font-weight: var(--weight-medium);
|
|
65
|
+
color: var(--text);
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
transition:
|
|
68
|
+
border-color var(--duration-fast) var(--ease-standard),
|
|
69
|
+
background-color var(--duration-fast) var(--ease-standard);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ui-chip:hover,
|
|
73
|
+
.ui-chip.is-hover {
|
|
74
|
+
border-color: var(--border-strong);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-chip:focus-visible,
|
|
78
|
+
.ui-chip.is-focus {
|
|
79
|
+
outline: 2px solid var(--focus);
|
|
80
|
+
outline-offset: 2px;
|
|
81
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Selected: 2 px border plus the accent surface, readable without colour. */
|
|
85
|
+
.ui-chip.is-selected,
|
|
86
|
+
.ui-chip[aria-pressed="true"] {
|
|
87
|
+
border-width: 2px;
|
|
88
|
+
border-color: var(--border-strong);
|
|
89
|
+
background: var(--surface-accent);
|
|
90
|
+
color: var(--surface-accent-fg, var(--text));
|
|
91
|
+
font-weight: var(--weight-semibold);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ui-chip.is-disabled,
|
|
95
|
+
.ui-chip:disabled {
|
|
96
|
+
opacity: 0.5;
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.ui-chip--struck {
|
|
101
|
+
text-decoration: line-through;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Filter chip: 32 px. */
|
|
105
|
+
.ui-chip--filter {
|
|
106
|
+
min-height: 32px;
|
|
107
|
+
border-radius: var(--radius-full);
|
|
108
|
+
padding: 0 var(--space-3);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ui-chip--filter.is-selected,
|
|
112
|
+
.ui-chip--filter[aria-pressed="true"] {
|
|
113
|
+
background: var(--surface-accent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@media (prefers-reduced-motion: reduce) {
|
|
117
|
+
.ui-chip {
|
|
118
|
+
transition: none;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@media (max-width: 700px), (pointer: coarse) {
|
|
123
|
+
.ui-chip {
|
|
124
|
+
min-height: 36px;
|
|
125
|
+
}
|
|
126
|
+
.ui-chip--filter {
|
|
127
|
+
min-height: 40px;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface ChipRailItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
current?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: ChipRailItem[];
|
|
10
|
+
label?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { items, label = "Categorieën", class: className, ...rest } = Astro.props;
|
|
15
|
+
const classes = ["pl-chips", className].filter(Boolean).join(" ");
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<nav class={classes} aria-label={label} {...rest}>
|
|
19
|
+
{items.map((item) => (
|
|
20
|
+
<a class="pl-chip" href={item.href} data-on={item.current ? "1" : undefined} aria-current={item.current ? "page" : undefined}>
|
|
21
|
+
{item.label}
|
|
22
|
+
</a>
|
|
23
|
+
))}
|
|
24
|
+
</nav>
|