@kwtv/components 0.1.1 → 0.1.3
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/package.json +2 -2
- package/src/astro/Alert.astro +1 -1
- package/src/astro/Band.astro +20 -6
- package/src/astro/DotGrid.astro +12 -3
- package/src/astro/EmptyState.astro +3 -4
- package/src/astro/Footer.astro +9 -9
- package/src/astro/Marquee.astro +3 -3
- package/src/astro/NavDropdown.astro +51 -4
- package/src/astro/Newsletter.astro +8 -8
- package/src/astro/Sticker.astro +2 -2
- package/src/astro/TextField.astro +1 -1
- package/src/react/NewsCarousel.css +39 -3
- package/src/react/NewsCarousel.tsx +57 -15
- package/src/react/Select.css +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kwtv/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "KWTV UI components for Astro, with React islands. Built on the KWTV design tokens.",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@kwtv/tokens": "^0.1.
|
|
37
|
+
"@kwtv/tokens": "^0.1.3"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/astro/Alert.astro
CHANGED
|
@@ -47,7 +47,7 @@ const classes = ["ui-alert", `ui-alert--${tone}`, className]
|
|
|
47
47
|
gap: var(--space-2-5, 0.625rem);
|
|
48
48
|
padding: var(--space-3) var(--space-4);
|
|
49
49
|
border-left: 3px solid currentColor;
|
|
50
|
-
border-radius: var(--radius-
|
|
50
|
+
border-radius: var(--radius-md);
|
|
51
51
|
font-family: var(--font-sans);
|
|
52
52
|
font-size: var(--text-sm);
|
|
53
53
|
line-height: var(--leading-sm);
|
package/src/astro/Band.astro
CHANGED
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* That is the whole reason this is a component and not a background utility:
|
|
8
8
|
* putting body text on sage without the flip fails AA.
|
|
9
9
|
*
|
|
10
|
-
* Cards placed on the band
|
|
11
|
-
* as
|
|
10
|
+
* Cards placed on the band move to a lighter tint of the band's own surface,
|
|
11
|
+
* so they read as part of the band rather than a return to plain paper.
|
|
12
|
+
*
|
|
13
|
+
* Corner radius matches every other surface in the system (Card, PickCard,
|
|
14
|
+
* NavDropdown's panel): --radius-lg. A band that sits inset in a page, next
|
|
15
|
+
* to a rounded card, needs the same rounding or the two read as unrelated.
|
|
12
16
|
*/
|
|
13
17
|
interface Props {
|
|
14
18
|
/** Renders as <section> by default. Use "div" when the band is not a
|
|
@@ -29,7 +33,8 @@ const classes = ["ui-band", className].filter(Boolean).join(" ");
|
|
|
29
33
|
<style>
|
|
30
34
|
.ui-band {
|
|
31
35
|
background: var(--surface-accent);
|
|
32
|
-
border
|
|
36
|
+
border: 1px solid var(--border-strong);
|
|
37
|
+
border-radius: var(--radius-lg);
|
|
33
38
|
color: var(--surface-accent-fg);
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -56,11 +61,20 @@ const classes = ["ui-band", className].filter(Boolean).join(" ");
|
|
|
56
61
|
color: var(--surface-accent-muted);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/* A card on the band
|
|
64
|
+
/* A card on the band moves to a lighter tint of the band's own surface,
|
|
65
|
+
rather than to plain paper, so it still reads as part of the band. It
|
|
66
|
+
carries its own foreground role: --surface-accent-fg is tuned for the
|
|
67
|
+
band's own, darker/mid tint and is not guaranteed to clear AA on this
|
|
68
|
+
lighter one. */
|
|
60
69
|
.ui-band :global(.ui-card),
|
|
61
70
|
.ui-band :global(.ui-card--raised) {
|
|
62
|
-
background: var(--surface);
|
|
63
|
-
border-color: var(--border);
|
|
71
|
+
background: var(--surface-accent-subtle);
|
|
72
|
+
border-color: var(--border-strong);
|
|
73
|
+
}
|
|
74
|
+
.ui-band :global(.ui-card__eyebrow),
|
|
75
|
+
.ui-band :global(.ui-card__title),
|
|
76
|
+
.ui-band :global(.ui-card__body) {
|
|
77
|
+
color: var(--surface-accent-subtle-fg);
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
/* Buttons keep their own label colours: they carry their own fill, so the
|
package/src/astro/DotGrid.astro
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
/** Decorative drifting dot field used behind short hero copy.
|
|
2
|
+
/** Decorative drifting dot field used behind short hero copy.
|
|
3
|
+
* Override --dotgrid-color and --dotgrid-opacity for a different backing surface;
|
|
4
|
+
* verify the brightest moving dot against the copy, not only the flat surface.
|
|
5
|
+
*/
|
|
3
6
|
interface Props {
|
|
4
7
|
compact?: boolean;
|
|
5
8
|
class?: string;
|
|
@@ -29,9 +32,9 @@ const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
|
|
|
29
32
|
.pl-dotgrid__dots {
|
|
30
33
|
position: absolute;
|
|
31
34
|
inset: 0;
|
|
32
|
-
background-image: radial-gradient(var(--sage-light) 1.4px, transparent 1.6px);
|
|
35
|
+
background-image: radial-gradient(var(--dotgrid-color, var(--sage-light)) 1.4px, transparent 1.6px);
|
|
33
36
|
background-size: 28px 28px;
|
|
34
|
-
opacity:
|
|
37
|
+
opacity: var(--dotgrid-opacity, var(--dotgrid-theme-opacity, .7));
|
|
35
38
|
animation: pl-drift 6s linear infinite;
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -54,6 +57,12 @@ const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
|
|
|
54
57
|
font-size: var(--text-base);
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/* Explicit themes win over OS preference, as in the token cascade. */
|
|
61
|
+
:global([data-theme="dark"]) .pl-dotgrid { --dotgrid-theme-opacity: .2; }
|
|
62
|
+
@media (prefers-color-scheme: dark) {
|
|
63
|
+
:global(:root:not([data-theme])) .pl-dotgrid { --dotgrid-theme-opacity: .2; }
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
@media (prefers-reduced-motion: reduce) {
|
|
58
67
|
.pl-dotgrid__dots {
|
|
59
68
|
animation: none;
|
|
@@ -29,7 +29,7 @@ const hasAction = Astro.slots.has("action");
|
|
|
29
29
|
flex-direction: column;
|
|
30
30
|
align-items: flex-start;
|
|
31
31
|
gap: var(--space-2);
|
|
32
|
-
padding: var(--space-
|
|
32
|
+
padding: var(--space-8) var(--space-6);
|
|
33
33
|
border: 1px dashed var(--border-strong);
|
|
34
34
|
border-radius: var(--radius-lg);
|
|
35
35
|
background: var(--surface);
|
|
@@ -47,7 +47,6 @@ const hasAction = Astro.slots.has("action");
|
|
|
47
47
|
color: var(--text-muted);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
50
|
+
/* The flex `gap` above already spaces the action from the body; an added
|
|
51
|
+
margin here doubled that gap and threw off the rhythm between the two. */
|
|
53
52
|
</style>
|
package/src/astro/Footer.astro
CHANGED
|
@@ -52,8 +52,8 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
52
52
|
grid-template-columns: 1.5fr 1fr 1.3fr;
|
|
53
53
|
gap: var(--space-10);
|
|
54
54
|
padding: var(--space-10);
|
|
55
|
-
background: var(--
|
|
56
|
-
color: var(--
|
|
55
|
+
background: var(--surface-inverse);
|
|
56
|
+
color: var(--surface-inverse-fg);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
.ui-footer__brandblock {
|
|
@@ -71,13 +71,13 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
71
71
|
}
|
|
72
72
|
.ui-footer__brand :global(span) {
|
|
73
73
|
font-weight: var(--weight-regular);
|
|
74
|
-
color: var(--
|
|
74
|
+
color: var(--surface-inverse-muted);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
.ui-footer__addr {
|
|
78
78
|
font-size: var(--text-sm);
|
|
79
79
|
line-height: var(--leading-sm);
|
|
80
|
-
color: var(--
|
|
80
|
+
color: var(--surface-inverse-fg);
|
|
81
81
|
}
|
|
82
82
|
.ui-footer__addr :global(a) {
|
|
83
83
|
color: inherit;
|
|
@@ -101,9 +101,9 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
101
101
|
flex-wrap: wrap;
|
|
102
102
|
margin-top: var(--space-4);
|
|
103
103
|
padding-top: var(--space-5);
|
|
104
|
-
border-top: 1px solid var(--
|
|
104
|
+
border-top: 1px solid var(--surface-inverse-border);
|
|
105
105
|
font-size: var(--text-xs);
|
|
106
|
-
color: var(--
|
|
106
|
+
color: var(--surface-inverse-muted);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
.ui-footer__policies {
|
|
@@ -112,7 +112,7 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
112
112
|
gap: var(--space-4);
|
|
113
113
|
}
|
|
114
114
|
.ui-footer__bar :global(a) {
|
|
115
|
-
color: var(--
|
|
115
|
+
color: var(--surface-inverse-fg);
|
|
116
116
|
text-decoration: underline;
|
|
117
117
|
text-underline-offset: 2px;
|
|
118
118
|
}
|
|
@@ -156,7 +156,7 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
156
156
|
order: 1;
|
|
157
157
|
font-size: var(--text-lg);
|
|
158
158
|
font-weight: var(--weight-regular);
|
|
159
|
-
color: var(--
|
|
159
|
+
color: var(--surface-inverse-muted);
|
|
160
160
|
}
|
|
161
161
|
.ui-footer--mobile.ui-footer--site .ui-footer__addr {
|
|
162
162
|
order: 2;
|
|
@@ -178,7 +178,7 @@ const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
|
178
178
|
.ui-footer--mobile.ui-footer--site .ui-footer__social :global(.ui-social) {
|
|
179
179
|
width: 48px;
|
|
180
180
|
height: 48px;
|
|
181
|
-
border: 1px solid var(--
|
|
181
|
+
border: 1px solid var(--surface-inverse-border);
|
|
182
182
|
border-radius: var(--radius-full);
|
|
183
183
|
}
|
|
184
184
|
.ui-footer--mobile.ui-footer--site :global(.ui-footer__newsletter) {
|
package/src/astro/Marquee.astro
CHANGED
|
@@ -38,11 +38,11 @@ const { items, divider = "✦", class: className } = Astro.props;
|
|
|
38
38
|
|
|
39
39
|
<style>
|
|
40
40
|
.ui-marquee {
|
|
41
|
-
background: var(--
|
|
42
|
-
color: var(--
|
|
41
|
+
background: var(--surface-banner);
|
|
42
|
+
color: var(--surface-inverse-fg);
|
|
43
43
|
overflow: hidden;
|
|
44
44
|
padding-block: var(--space-3);
|
|
45
|
-
border-block: 1px solid var(--
|
|
45
|
+
border-block: 1px solid var(--surface-inverse-border);
|
|
46
46
|
transform: rotate(-1.2deg) scale(1.02);
|
|
47
47
|
}
|
|
48
48
|
.ui-marquee__track {
|
|
@@ -3,9 +3,13 @@ import Icon from "./Icon.astro";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A header navigation group: a button trigger plus a panel of links. The panel
|
|
6
|
-
* opens on hover and on :focus-within
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* opens on hover and on :focus-within for mouse and keyboard users, but a tap
|
|
7
|
+
* on a touchscreen fires neither, and Safari does not focus a <button> on
|
|
8
|
+
* click either: without more, those users could never open it. So the trigger
|
|
9
|
+
* also carries a click handler that toggles `.is-open` directly, with an
|
|
10
|
+
* outside-click and Escape to close. That script is what makes the dropdown
|
|
11
|
+
* reachable by tap; the CSS-only hover/focus-within path keeps working
|
|
12
|
+
* underneath it for everyone else. Pass `open` to pin it open for specimens.
|
|
9
13
|
*
|
|
10
14
|
* Put <a> links in the default slot; they fill the panel.
|
|
11
15
|
*/
|
|
@@ -22,7 +26,7 @@ const classes = ["ui-navgroup", open && "is-open", className]
|
|
|
22
26
|
.join(" ");
|
|
23
27
|
---
|
|
24
28
|
|
|
25
|
-
<div class={classes} {...rest}>
|
|
29
|
+
<div class={classes} data-navdropdown {...rest}>
|
|
26
30
|
<button class="ui-navlink" type="button" aria-expanded={open ? "true" : "false"}>
|
|
27
31
|
{label}
|
|
28
32
|
<span aria-hidden="true"><Icon name="chevron-down" size={11} stroke={2.4} /></span>
|
|
@@ -32,6 +36,49 @@ const classes = ["ui-navgroup", open && "is-open", className]
|
|
|
32
36
|
</div>
|
|
33
37
|
</div>
|
|
34
38
|
|
|
39
|
+
<script>
|
|
40
|
+
// Click-to-toggle, on top of the CSS hover/focus-within path: the only way
|
|
41
|
+
// a touchscreen tap, or a Safari click (which does not focus a <button>),
|
|
42
|
+
// can open the panel at all.
|
|
43
|
+
document.querySelectorAll<HTMLElement>("[data-navdropdown]").forEach((group) => {
|
|
44
|
+
const trigger = group.querySelector<HTMLButtonElement>(".ui-navlink");
|
|
45
|
+
if (!trigger) return;
|
|
46
|
+
|
|
47
|
+
const close = () => {
|
|
48
|
+
group.classList.remove("is-open");
|
|
49
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
trigger.addEventListener("click", (event) => {
|
|
53
|
+
const willOpen = !group.classList.contains("is-open");
|
|
54
|
+
document.querySelectorAll<HTMLElement>("[data-navdropdown].is-open").forEach((other) => {
|
|
55
|
+
if (other === group) return;
|
|
56
|
+
other.classList.remove("is-open");
|
|
57
|
+
other.querySelector<HTMLButtonElement>(".ui-navlink")?.setAttribute("aria-expanded", "false");
|
|
58
|
+
});
|
|
59
|
+
group.classList.toggle("is-open", willOpen);
|
|
60
|
+
trigger.setAttribute("aria-expanded", String(willOpen));
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
group.addEventListener("keydown", (event) => {
|
|
65
|
+
if (event.key === "Escape") {
|
|
66
|
+
close();
|
|
67
|
+
trigger.focus();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
document.addEventListener("click", (event) => {
|
|
73
|
+
document.querySelectorAll<HTMLElement>("[data-navdropdown].is-open").forEach((group) => {
|
|
74
|
+
if (!group.contains(event.target as Node)) {
|
|
75
|
+
group.classList.remove("is-open");
|
|
76
|
+
group.querySelector<HTMLButtonElement>(".ui-navlink")?.setAttribute("aria-expanded", "false");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
35
82
|
<style>
|
|
36
83
|
.ui-navgroup {
|
|
37
84
|
position: relative;
|
|
@@ -69,21 +69,21 @@ const classes = ["ui-news", stacked && "ui-news--stacked", className]
|
|
|
69
69
|
min-width: 0;
|
|
70
70
|
min-height: 48px;
|
|
71
71
|
padding: 0 var(--space-4);
|
|
72
|
-
background: var(--
|
|
73
|
-
border: 1px solid
|
|
72
|
+
background: var(--surface-light);
|
|
73
|
+
border: 1px solid var(--border-strong-light);
|
|
74
74
|
border-radius: var(--radius-md);
|
|
75
75
|
font-family: var(--font-sans);
|
|
76
76
|
font-size: var(--text-sm);
|
|
77
|
-
color: var(--text);
|
|
77
|
+
color: var(--text-light);
|
|
78
78
|
}
|
|
79
79
|
.ui-news__input::placeholder {
|
|
80
|
-
color: var(--text-muted);
|
|
80
|
+
color: var(--text-muted-light);
|
|
81
81
|
}
|
|
82
82
|
.ui-news__input:focus-visible,
|
|
83
83
|
.ui-news__input.is-focus {
|
|
84
|
-
outline: 2px solid var(--
|
|
84
|
+
outline: 2px solid var(--focus);
|
|
85
85
|
outline-offset: 2px;
|
|
86
|
-
box-shadow: 0 0 0 4px
|
|
86
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
.ui-news__btn {
|
|
@@ -105,9 +105,9 @@ const classes = ["ui-news", stacked && "ui-news--stacked", className]
|
|
|
105
105
|
background: var(--sage-xlight);
|
|
106
106
|
}
|
|
107
107
|
.ui-news__btn:focus-visible {
|
|
108
|
-
outline: 2px solid var(--
|
|
108
|
+
outline: 2px solid var(--focus);
|
|
109
109
|
outline-offset: 2px;
|
|
110
|
-
box-shadow: 0 0 0 4px
|
|
110
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
@media (prefers-reduced-motion: reduce) {
|
package/src/astro/Sticker.astro
CHANGED
|
@@ -14,7 +14,7 @@ const classes = ["pl-sticker", `pl-sticker--${size}`, className]
|
|
|
14
14
|
|
|
15
15
|
<span class={classes} aria-hidden="true" {...rest}>
|
|
16
16
|
<svg class="pl-sticker__disc" viewBox="0 0 112 112">
|
|
17
|
-
<circle cx="56" cy="56" r="52" fill="var(--
|
|
17
|
+
<circle cx="56" cy="56" r="52" fill="var(--surface-inverse)"></circle>
|
|
18
18
|
<circle class="pl-sticker__ring" cx="56" cy="56" r="47" fill="none" stroke="var(--sage-light)" stroke-width="1.5" stroke-dasharray="5 7"></circle>
|
|
19
19
|
</svg>
|
|
20
20
|
<span class="pl-sticker__content">
|
|
@@ -58,7 +58,7 @@ const classes = ["pl-sticker", `pl-sticker--${size}`, className]
|
|
|
58
58
|
align-items: center;
|
|
59
59
|
justify-content: center;
|
|
60
60
|
gap: var(--space-1);
|
|
61
|
-
color: var(--
|
|
61
|
+
color: var(--surface-inverse-fg);
|
|
62
62
|
text-align: center;
|
|
63
63
|
font-size: var(--text-xs);
|
|
64
64
|
font-weight: var(--weight-semibold);
|
|
@@ -144,7 +144,7 @@ const charCount = count && maxlength ? `${String(value).length} / ${maxlength}`
|
|
|
144
144
|
min-height: 40px;
|
|
145
145
|
padding: var(--space-2) var(--space-3);
|
|
146
146
|
background: var(--surface);
|
|
147
|
-
border: 1px solid var(--border);
|
|
147
|
+
border: 1px solid var(--border-strong);
|
|
148
148
|
border-radius: var(--radius-md);
|
|
149
149
|
font-family: var(--font-sans);
|
|
150
150
|
font-size: var(--text-sm);
|
|
@@ -32,6 +32,43 @@
|
|
|
32
32
|
outline-offset: 2px;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* Prev/next arrows. Sit in the controls row beside the dots, never over the
|
|
36
|
+
item itself, so they never compete with the item's own link target. */
|
|
37
|
+
.pl-news-controls {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: flex-start;
|
|
41
|
+
gap: var(--space-3);
|
|
42
|
+
margin-top: var(--space-2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.pl-news-nav {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
width: 28px;
|
|
50
|
+
height: 28px;
|
|
51
|
+
padding: 0;
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: var(--radius-full);
|
|
54
|
+
background: var(--surface);
|
|
55
|
+
color: var(--text);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition:
|
|
58
|
+
background-color var(--duration-fast) var(--ease-standard),
|
|
59
|
+
border-color var(--duration-fast) var(--ease-standard);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.pl-news-nav:hover {
|
|
63
|
+
background: var(--surface-hover);
|
|
64
|
+
border-color: var(--border-strong);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.pl-news-nav:focus-visible {
|
|
68
|
+
outline: 2px solid var(--focus);
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
.ui-eyebrow {
|
|
36
73
|
font-family: var(--font-mono);
|
|
37
74
|
font-size: var(--text-2xs);
|
|
@@ -52,7 +89,6 @@
|
|
|
52
89
|
display: flex;
|
|
53
90
|
align-items: center;
|
|
54
91
|
gap: var(--space-1-5);
|
|
55
|
-
margin-top: var(--space-2);
|
|
56
92
|
}
|
|
57
93
|
|
|
58
94
|
.pl-news-dot {
|
|
@@ -61,7 +97,7 @@
|
|
|
61
97
|
padding: 0;
|
|
62
98
|
border: 0;
|
|
63
99
|
border-radius: var(--radius-full);
|
|
64
|
-
background: var(--border);
|
|
100
|
+
background: var(--border-strong);
|
|
65
101
|
cursor: pointer;
|
|
66
102
|
transition:
|
|
67
103
|
width var(--duration-base) var(--ease-enter),
|
|
@@ -71,7 +107,7 @@
|
|
|
71
107
|
|
|
72
108
|
.pl-news-dot[data-on] {
|
|
73
109
|
width: 22px;
|
|
74
|
-
background: var(--
|
|
110
|
+
background: var(--primary);
|
|
75
111
|
}
|
|
76
112
|
|
|
77
113
|
.pl-news-dot:focus-visible {
|
|
@@ -3,9 +3,11 @@ import "./NewsCarousel.css";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* News carousel. Cross-fades between items on a timer. Dots are real buttons
|
|
6
|
-
* with a label per item
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* with a label per item; prev/next arrows sit either side of the dots and
|
|
7
|
+
* step one item at a time, rather than sitting over the item and competing
|
|
8
|
+
* with its own link target. The off-screen item is pointer-events:none and
|
|
9
|
+
* cannot be tabbed into. The loop pauses on hover, on focus, and when the tab
|
|
10
|
+
* is hidden, and stops entirely under prefers-reduced-motion.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
export interface NewsItem {
|
|
@@ -20,8 +22,24 @@ export interface NewsCarouselProps {
|
|
|
20
22
|
interval?: number;
|
|
21
23
|
/** Accessible label builder for each dot. */
|
|
22
24
|
dotLabel?: (index: number, item: NewsItem) => string;
|
|
25
|
+
/** Accessible label for the previous-item control. */
|
|
26
|
+
prevLabel?: string;
|
|
27
|
+
/** Accessible label for the next-item control. */
|
|
28
|
+
nextLabel?: string;
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
const ChevronIcon = ({ direction }: { direction: "left" | "right" }) => (
|
|
32
|
+
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
33
|
+
<path
|
|
34
|
+
d={direction === "left" ? "M15 6l-6 6 6 6" : "M9 6l6 6-6 6"}
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
strokeWidth={2.2}
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
strokeLinejoin="round"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
|
|
25
43
|
const prefersReducedMotion = () =>
|
|
26
44
|
typeof window !== "undefined" &&
|
|
27
45
|
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -30,6 +48,8 @@ export default function NewsCarousel({
|
|
|
30
48
|
items,
|
|
31
49
|
interval = 5000,
|
|
32
50
|
dotLabel = (i) => `Nieuwsbericht ${i + 1}`,
|
|
51
|
+
prevLabel = "Vorig nieuwsbericht",
|
|
52
|
+
nextLabel = "Volgend nieuwsbericht",
|
|
33
53
|
}: NewsCarouselProps) {
|
|
34
54
|
const [active, setActive] = useState(0);
|
|
35
55
|
const paused = useRef(false);
|
|
@@ -54,6 +74,10 @@ export default function NewsCarousel({
|
|
|
54
74
|
paused.current = false;
|
|
55
75
|
};
|
|
56
76
|
|
|
77
|
+
const step = (delta: number) => {
|
|
78
|
+
setActive((a) => (a + delta + items.length) % items.length);
|
|
79
|
+
};
|
|
80
|
+
|
|
57
81
|
return (
|
|
58
82
|
<div>
|
|
59
83
|
<div
|
|
@@ -78,18 +102,36 @@ export default function NewsCarousel({
|
|
|
78
102
|
))}
|
|
79
103
|
</div>
|
|
80
104
|
{items.length > 1 && (
|
|
81
|
-
<div className="pl-news-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
<div className="pl-news-controls">
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="pl-news-nav pl-news-nav--prev"
|
|
109
|
+
aria-label={prevLabel}
|
|
110
|
+
onClick={() => step(-1)}
|
|
111
|
+
>
|
|
112
|
+
<ChevronIcon direction="left" />
|
|
113
|
+
</button>
|
|
114
|
+
<div className="pl-news-dots">
|
|
115
|
+
{items.map((item, i) => (
|
|
116
|
+
<button
|
|
117
|
+
key={i}
|
|
118
|
+
type="button"
|
|
119
|
+
className="pl-news-dot"
|
|
120
|
+
data-on={i === active ? "1" : undefined}
|
|
121
|
+
aria-label={dotLabel(i, item)}
|
|
122
|
+
aria-current={i === active ? "true" : undefined}
|
|
123
|
+
onClick={() => setActive(i)}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
className="pl-news-nav pl-news-nav--next"
|
|
130
|
+
aria-label={nextLabel}
|
|
131
|
+
onClick={() => step(1)}
|
|
132
|
+
>
|
|
133
|
+
<ChevronIcon direction="right" />
|
|
134
|
+
</button>
|
|
93
135
|
</div>
|
|
94
136
|
)}
|
|
95
137
|
</div>
|
package/src/react/Select.css
CHANGED