@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,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The stroke icon set used across the components. One place to change a glyph.
|
|
4
|
+
* Icons are decorative by default (aria-hidden). Pass a `title` only when the
|
|
5
|
+
* icon is the sole carrier of meaning, which in this system it never should be.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
name:
|
|
9
|
+
| "cart"
|
|
10
|
+
| "menu"
|
|
11
|
+
| "close"
|
|
12
|
+
| "check"
|
|
13
|
+
| "check-bold"
|
|
14
|
+
| "chevron-down"
|
|
15
|
+
| "chevron-right"
|
|
16
|
+
| "chevron-left"
|
|
17
|
+
| "info"
|
|
18
|
+
| "alert-circle"
|
|
19
|
+
| "alert-triangle"
|
|
20
|
+
| "arrow-right"
|
|
21
|
+
| "arrow-up-right"
|
|
22
|
+
| "arrow-down"
|
|
23
|
+
| "instagram"
|
|
24
|
+
| "facebook"
|
|
25
|
+
| "youtube";
|
|
26
|
+
size?: number | string;
|
|
27
|
+
stroke?: number;
|
|
28
|
+
class?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { name, size = 20, stroke = 1.8, class: className, title } = Astro.props;
|
|
33
|
+
const labelled = Boolean(title);
|
|
34
|
+
|
|
35
|
+
const paths: Record<Props["name"], string> = {
|
|
36
|
+
cart: '<path d="M6 7h12l-1 12H7L6 7zM9 7V5a3 3 0 0 1 6 0v2"/>',
|
|
37
|
+
menu: '<path d="M4 7h16M4 12h16M4 17h16"/>',
|
|
38
|
+
close: '<path d="M7 7l10 10M17 7L7 17"/>',
|
|
39
|
+
check: '<path d="M5 13l4 4L19 7"/>',
|
|
40
|
+
"check-bold": '<path d="M5 13l4 4L19 7"/>',
|
|
41
|
+
"chevron-down": '<path d="M6 9l6 6 6-6"/>',
|
|
42
|
+
"chevron-right": '<path d="M9 6l6 6-6 6"/>',
|
|
43
|
+
"chevron-left": '<path d="M15 6l-6 6 6 6"/>',
|
|
44
|
+
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.5"/>',
|
|
45
|
+
"alert-circle": '<circle cx="12" cy="12" r="9"/><path d="M12 8v4.5M12 16v.5"/>',
|
|
46
|
+
"alert-triangle": '<path d="M12 4l8.5 15h-17L12 4zM12 10v4M12 16.5v.5"/>',
|
|
47
|
+
"arrow-right": '<path d="M5 12h14M13 6l6 6-6 6"/>',
|
|
48
|
+
"arrow-up-right": '<path d="M7 17 17 7M8 7h9v9"/>',
|
|
49
|
+
"arrow-down": '<path d="M12 5v14M6 13l6 6 6-6"/>',
|
|
50
|
+
instagram:
|
|
51
|
+
'<rect x="4" y="4" width="16" height="16" rx="5"/><circle cx="12" cy="12" r="3.6"/><path d="M17 7.2v.01"/>',
|
|
52
|
+
facebook: '<path d="M14.5 8.5h2M14.5 8.5c0-2 .8-3 2.5-3M14.5 8.5V19M11 12h6"/>',
|
|
53
|
+
youtube:
|
|
54
|
+
'<rect x="3.5" y="6.5" width="17" height="11" rx="3.2"/><path d="M11 10.2l3.4 1.8-3.4 1.8z"/>',
|
|
55
|
+
};
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<svg
|
|
59
|
+
width={size}
|
|
60
|
+
height={size}
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
stroke-width={stroke}
|
|
65
|
+
stroke-linecap="round"
|
|
66
|
+
stroke-linejoin="round"
|
|
67
|
+
class={className}
|
|
68
|
+
role={labelled ? "img" : undefined}
|
|
69
|
+
aria-hidden={labelled ? undefined : "true"}
|
|
70
|
+
aria-label={title}
|
|
71
|
+
set:html={paths[name]}
|
|
72
|
+
/>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Icon button. Square 44 px control for the header: cart and menu. Always
|
|
6
|
+
* carries an aria-label, because an icon-only control has no label to enlarge.
|
|
7
|
+
* The count pill is decorative (aria-hidden); the number must be repeated in
|
|
8
|
+
* the label by the consumer, so it is announced once, not twice.
|
|
9
|
+
*
|
|
10
|
+
* Pass `icon` to use the shared icon set, or a custom SVG via the default slot.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** Decorative count pill. Repeat the number in `label`. */
|
|
16
|
+
count?: number;
|
|
17
|
+
type?: "button" | "submit" | "reset";
|
|
18
|
+
expanded?: boolean;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
label,
|
|
24
|
+
icon,
|
|
25
|
+
count,
|
|
26
|
+
type = "button",
|
|
27
|
+
expanded,
|
|
28
|
+
class: className,
|
|
29
|
+
...rest
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
|
|
32
|
+
const classes = ["ui-iconbtn", className].filter(Boolean).join(" ");
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
class={classes}
|
|
37
|
+
type={type}
|
|
38
|
+
aria-label={label}
|
|
39
|
+
aria-expanded={expanded === undefined ? undefined : String(expanded)}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{icon ? <Icon name={icon as any} size={20} stroke={1.7} /> : <slot />}
|
|
43
|
+
{count !== undefined && (
|
|
44
|
+
<span class="ui-iconbtn__count" aria-hidden="true">{count}</span>
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.ui-iconbtn {
|
|
50
|
+
position: relative;
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
width: 44px;
|
|
55
|
+
height: 44px;
|
|
56
|
+
padding: 0;
|
|
57
|
+
background: transparent;
|
|
58
|
+
border: 1px solid transparent;
|
|
59
|
+
border-radius: var(--radius-md);
|
|
60
|
+
color: var(--text);
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
transition:
|
|
63
|
+
background-color var(--duration-fast) var(--ease-standard),
|
|
64
|
+
border-color var(--duration-fast) var(--ease-standard);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ui-iconbtn:hover,
|
|
68
|
+
.ui-iconbtn.is-hover {
|
|
69
|
+
background: var(--surface-hover);
|
|
70
|
+
border-color: var(--border-strong);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ui-iconbtn:focus-visible,
|
|
74
|
+
.ui-iconbtn.is-focus {
|
|
75
|
+
outline: 2px solid var(--focus);
|
|
76
|
+
outline-offset: 2px;
|
|
77
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ui-iconbtn:active,
|
|
81
|
+
.ui-iconbtn.is-active {
|
|
82
|
+
background: var(--surface-hover);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ui-iconbtn:disabled,
|
|
86
|
+
.ui-iconbtn.is-disabled {
|
|
87
|
+
opacity: 0.5;
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Count pill: 19 px, accent fill, top-right. */
|
|
93
|
+
.ui-iconbtn__count {
|
|
94
|
+
position: absolute;
|
|
95
|
+
top: 3px;
|
|
96
|
+
right: 3px;
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
min-width: 19px;
|
|
101
|
+
height: 19px;
|
|
102
|
+
padding: 0 var(--space-1);
|
|
103
|
+
background: var(--accent);
|
|
104
|
+
color: var(--accent-fg);
|
|
105
|
+
border-radius: var(--radius-full);
|
|
106
|
+
font-family: var(--font-sans);
|
|
107
|
+
font-size: var(--text-2xs);
|
|
108
|
+
font-weight: var(--weight-semibold);
|
|
109
|
+
line-height: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@media (prefers-reduced-motion: reduce) {
|
|
113
|
+
.ui-iconbtn {
|
|
114
|
+
transition: none;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Decorative scrolling band. 22 s loop, content doubled so the loop is seamless,
|
|
4
|
+
* rotated a touch. Decorative, so the whole thing is aria-hidden.
|
|
5
|
+
*
|
|
6
|
+
* Pass the items as the default slot. They are rendered twice automatically is
|
|
7
|
+
* not possible across a slot, so pass a simple `items` array of strings, or use
|
|
8
|
+
* the default slot and duplicate your content yourself. Here we take `items`.
|
|
9
|
+
*
|
|
10
|
+
* Needs the motion layer (`@kwtv/components/styles/motion.css`) for the
|
|
11
|
+
* pl-marquee keyframes, inside a .pl-root.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
items: string[];
|
|
15
|
+
/** Divider glyph between items. */
|
|
16
|
+
divider?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
const { items, divider = "✦", class: className } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class:list={["ui-marquee", className]} aria-hidden="true">
|
|
23
|
+
<div class="ui-marquee__track">
|
|
24
|
+
{
|
|
25
|
+
[0, 1].map(() => (
|
|
26
|
+
<div class="ui-marquee__group">
|
|
27
|
+
{items.map((item) => (
|
|
28
|
+
<>
|
|
29
|
+
<span>{item}</span>
|
|
30
|
+
<span class="ui-marquee__div">{divider}</span>
|
|
31
|
+
</>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
))
|
|
35
|
+
}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.ui-marquee {
|
|
41
|
+
background: var(--sage-xdark);
|
|
42
|
+
color: var(--ivory-light);
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
padding-block: var(--space-3);
|
|
45
|
+
border-block: 1px solid var(--sage-text);
|
|
46
|
+
transform: rotate(-1.2deg) scale(1.02);
|
|
47
|
+
}
|
|
48
|
+
.ui-marquee__track {
|
|
49
|
+
display: flex;
|
|
50
|
+
width: max-content;
|
|
51
|
+
animation: pl-marquee 22s linear infinite;
|
|
52
|
+
font-size: var(--text-sm);
|
|
53
|
+
font-weight: var(--weight-semibold);
|
|
54
|
+
letter-spacing: var(--tracking-wider);
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
}
|
|
57
|
+
.ui-marquee__group {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-6);
|
|
61
|
+
padding-right: var(--space-6);
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
}
|
|
64
|
+
.ui-marquee__div {
|
|
65
|
+
color: var(--sage-light);
|
|
66
|
+
}
|
|
67
|
+
@media (prefers-reduced-motion: reduce) {
|
|
68
|
+
.ui-marquee__track {
|
|
69
|
+
animation: none;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A header navigation group: a button trigger plus a panel of links. The panel
|
|
6
|
+
* opens on hover and on :focus-within, so it is reachable without a mouse. The
|
|
7
|
+
* trigger is a real <button> with aria-expanded; for static rendering the open
|
|
8
|
+
* state is CSS-driven. Pass `open` to pin it open for specimens.
|
|
9
|
+
*
|
|
10
|
+
* Put <a> links in the default slot; they fill the panel.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
label: string;
|
|
14
|
+
open?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { label, open = false, class: className, ...rest } = Astro.props;
|
|
19
|
+
|
|
20
|
+
const classes = ["ui-navgroup", open && "is-open", className]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" ");
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<div class={classes} {...rest}>
|
|
26
|
+
<button class="ui-navlink" type="button" aria-expanded={open ? "true" : "false"}>
|
|
27
|
+
{label}
|
|
28
|
+
<span aria-hidden="true"><Icon name="chevron-down" size={11} stroke={2.4} /></span>
|
|
29
|
+
</button>
|
|
30
|
+
<div class="ui-navpanel">
|
|
31
|
+
<slot />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.ui-navgroup {
|
|
37
|
+
position: relative;
|
|
38
|
+
display: inline-block;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* The trigger carries .ui-navlink, but that class is styled in NavLink.astro
|
|
42
|
+
and Astro scopes styles per component: NavLink's rules only match NavLink's
|
|
43
|
+
own markup. Without this block the button falls back to the browser default
|
|
44
|
+
chrome, a grey fill and a border. Restated here so the trigger matches the
|
|
45
|
+
sibling links it sits next to. */
|
|
46
|
+
.ui-navlink {
|
|
47
|
+
position: relative;
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--space-1-5);
|
|
51
|
+
padding: 7px 10px;
|
|
52
|
+
margin: -7px -10px;
|
|
53
|
+
border-radius: var(--radius-sm);
|
|
54
|
+
font-family: var(--font-sans);
|
|
55
|
+
font-size: var(--text-sm);
|
|
56
|
+
font-weight: var(--weight-medium);
|
|
57
|
+
line-height: 1;
|
|
58
|
+
color: var(--text);
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
background: none;
|
|
61
|
+
border: 0;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
transition: color var(--duration-fast) var(--ease-standard);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-navlink::after {
|
|
67
|
+
content: "";
|
|
68
|
+
position: absolute;
|
|
69
|
+
left: 10px;
|
|
70
|
+
right: 10px;
|
|
71
|
+
bottom: 2px;
|
|
72
|
+
height: 2px;
|
|
73
|
+
background: currentColor;
|
|
74
|
+
opacity: 0;
|
|
75
|
+
transition: opacity var(--duration-fast) var(--ease-standard);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ui-navlink:hover,
|
|
79
|
+
.ui-navlink.is-hover {
|
|
80
|
+
color: var(--link-hover);
|
|
81
|
+
}
|
|
82
|
+
.ui-navlink:hover::after,
|
|
83
|
+
.ui-navlink.is-hover::after {
|
|
84
|
+
opacity: 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* While the panel is open the trigger reads as active, the same way the
|
|
88
|
+
current page does. */
|
|
89
|
+
.ui-navgroup:hover > .ui-navlink::after,
|
|
90
|
+
.ui-navgroup:focus-within > .ui-navlink::after,
|
|
91
|
+
.ui-navgroup.is-open > .ui-navlink::after {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ui-navlink:focus-visible,
|
|
96
|
+
.ui-navlink.is-focus {
|
|
97
|
+
outline: 2px solid var(--focus);
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* The chevron turns over when the panel is open. */
|
|
103
|
+
.ui-navlink > span {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
line-height: 0;
|
|
106
|
+
transition: transform var(--duration-fast) var(--ease-standard);
|
|
107
|
+
}
|
|
108
|
+
.ui-navgroup:hover > .ui-navlink > span,
|
|
109
|
+
.ui-navgroup:focus-within > .ui-navlink > span,
|
|
110
|
+
.ui-navgroup.is-open > .ui-navlink > span {
|
|
111
|
+
transform: rotate(180deg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.ui-navpanel {
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: 100%;
|
|
117
|
+
left: -10px;
|
|
118
|
+
z-index: var(--z-dropdown);
|
|
119
|
+
min-width: 216px;
|
|
120
|
+
margin-top: var(--space-2);
|
|
121
|
+
padding: var(--space-2);
|
|
122
|
+
display: none;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: var(--space-px);
|
|
125
|
+
background: var(--surface);
|
|
126
|
+
border: 1px solid var(--border);
|
|
127
|
+
border-radius: var(--radius-lg);
|
|
128
|
+
box-shadow: var(--shadow-lg);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Opens on hover and when any child holds focus. */
|
|
132
|
+
.ui-navgroup:hover .ui-navpanel,
|
|
133
|
+
.ui-navgroup:focus-within .ui-navpanel,
|
|
134
|
+
.ui-navgroup.is-open .ui-navpanel {
|
|
135
|
+
display: flex;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ui-navpanel :global(a) {
|
|
139
|
+
display: block;
|
|
140
|
+
padding: var(--space-2) var(--space-3);
|
|
141
|
+
border-radius: var(--radius-sm);
|
|
142
|
+
font-size: var(--text-sm);
|
|
143
|
+
color: var(--text);
|
|
144
|
+
text-decoration: none;
|
|
145
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
146
|
+
}
|
|
147
|
+
.ui-navpanel :global(a:hover) {
|
|
148
|
+
background: var(--surface-hover);
|
|
149
|
+
color: var(--link-hover);
|
|
150
|
+
}
|
|
151
|
+
.ui-navpanel :global(a:focus-visible) {
|
|
152
|
+
outline: 2px solid var(--focus);
|
|
153
|
+
outline-offset: -2px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
.ui-navpanel :global(a),
|
|
158
|
+
.ui-navlink,
|
|
159
|
+
.ui-navlink::after,
|
|
160
|
+
.ui-navlink > span {
|
|
161
|
+
transition: none;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A navigation link for the header. Pads out to a 34 px row so the focus ring
|
|
4
|
+
* never crowds the text, with an equal negative margin so the header rhythm is
|
|
5
|
+
* untouched. Hover draws a 2 px line; the current page adds the line plus
|
|
6
|
+
* semibold and carries aria-current="page".
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
href: string;
|
|
10
|
+
current?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { href, current = false, class: className, ...rest } = Astro.props;
|
|
15
|
+
|
|
16
|
+
const classes = ["ui-navlink", current && "is-current", className]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(" ");
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<a
|
|
22
|
+
class={classes}
|
|
23
|
+
href={href}
|
|
24
|
+
aria-current={current ? "page" : undefined}
|
|
25
|
+
{...rest}
|
|
26
|
+
>
|
|
27
|
+
<slot />
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.ui-navlink {
|
|
32
|
+
position: relative;
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: var(--space-1-5);
|
|
36
|
+
/* 7 px block / 10 px inline padding with an equal negative margin, so the
|
|
37
|
+
focus ring has room but the header rhythm does not shift. */
|
|
38
|
+
padding: 7px 10px;
|
|
39
|
+
margin: -7px -10px;
|
|
40
|
+
border-radius: var(--radius-sm);
|
|
41
|
+
font-family: var(--font-sans);
|
|
42
|
+
font-size: var(--text-sm);
|
|
43
|
+
font-weight: var(--weight-medium);
|
|
44
|
+
line-height: 1;
|
|
45
|
+
color: var(--text);
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
background: none;
|
|
48
|
+
border: 0;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
transition: color var(--duration-fast) var(--ease-standard);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-navlink::after {
|
|
54
|
+
content: "";
|
|
55
|
+
position: absolute;
|
|
56
|
+
left: 10px;
|
|
57
|
+
right: 10px;
|
|
58
|
+
bottom: 2px;
|
|
59
|
+
height: 2px;
|
|
60
|
+
background: currentColor;
|
|
61
|
+
opacity: 0;
|
|
62
|
+
transition: opacity var(--duration-fast) var(--ease-standard);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ui-navlink:hover,
|
|
66
|
+
.ui-navlink.is-hover {
|
|
67
|
+
color: var(--link-hover);
|
|
68
|
+
}
|
|
69
|
+
.ui-navlink:hover::after,
|
|
70
|
+
.ui-navlink.is-hover::after {
|
|
71
|
+
opacity: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ui-navlink.is-current {
|
|
75
|
+
font-weight: var(--weight-semibold);
|
|
76
|
+
}
|
|
77
|
+
.ui-navlink.is-current::after {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-navlink:focus-visible,
|
|
82
|
+
.ui-navlink.is-focus {
|
|
83
|
+
outline: 2px solid var(--focus);
|
|
84
|
+
outline-offset: 2px;
|
|
85
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media (prefers-reduced-motion: reduce) {
|
|
89
|
+
.ui-navlink,
|
|
90
|
+
.ui-navlink::after {
|
|
91
|
+
transition: none;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The newsletter sign-up. A real <label> tied to the field by `for`, hidden
|
|
4
|
+
* with .ui-sr-only (not display:none) so it still names the field; the
|
|
5
|
+
* placeholder is an example, never the label. Type and inputmode are email.
|
|
6
|
+
* The field and button are 48 px and stack on mobile with the --stacked
|
|
7
|
+
* modifier. Ivory on dark sage clears 4.5:1.
|
|
8
|
+
*
|
|
9
|
+
* The confirmation message after submit is the consumer's concern; this is
|
|
10
|
+
* only the form.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
action?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
buttonText?: string;
|
|
20
|
+
stacked?: boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
action,
|
|
26
|
+
method = "post",
|
|
27
|
+
id = "newsletter-email",
|
|
28
|
+
label = "E-mailadres voor de nieuwsbrief",
|
|
29
|
+
name = "email",
|
|
30
|
+
placeholder = "jij@voorbeeld.be",
|
|
31
|
+
buttonText = "Inschrijven",
|
|
32
|
+
stacked = false,
|
|
33
|
+
class: className,
|
|
34
|
+
...rest
|
|
35
|
+
} = Astro.props;
|
|
36
|
+
|
|
37
|
+
const classes = ["ui-news", stacked && "ui-news--stacked", className]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(" ");
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<form class={classes} action={action} method={method} {...rest}>
|
|
43
|
+
<label class="ui-sr-only" for={id}>{label}</label>
|
|
44
|
+
<input
|
|
45
|
+
class="ui-news__input"
|
|
46
|
+
id={id}
|
|
47
|
+
name={name}
|
|
48
|
+
type="email"
|
|
49
|
+
inputmode="email"
|
|
50
|
+
placeholder={placeholder}
|
|
51
|
+
autocomplete="email"
|
|
52
|
+
/>
|
|
53
|
+
<button class="ui-news__btn" type="submit">{buttonText}</button>
|
|
54
|
+
</form>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.ui-news {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: var(--space-2);
|
|
60
|
+
max-width: 420px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ui-news--stacked {
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ui-news__input {
|
|
68
|
+
flex: 1;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
min-height: 48px;
|
|
71
|
+
padding: 0 var(--space-4);
|
|
72
|
+
background: var(--ivory-xlight);
|
|
73
|
+
border: 1px solid transparent;
|
|
74
|
+
border-radius: var(--radius-md);
|
|
75
|
+
font-family: var(--font-sans);
|
|
76
|
+
font-size: var(--text-sm);
|
|
77
|
+
color: var(--text);
|
|
78
|
+
}
|
|
79
|
+
.ui-news__input::placeholder {
|
|
80
|
+
color: var(--text-muted);
|
|
81
|
+
}
|
|
82
|
+
.ui-news__input:focus-visible,
|
|
83
|
+
.ui-news__input.is-focus {
|
|
84
|
+
outline: 2px solid var(--ivory-xlight);
|
|
85
|
+
outline-offset: 2px;
|
|
86
|
+
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.25);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ui-news__btn {
|
|
90
|
+
flex: none;
|
|
91
|
+
min-height: 48px;
|
|
92
|
+
padding: 0 var(--space-5);
|
|
93
|
+
background: var(--sage-light);
|
|
94
|
+
color: var(--sage-xdark);
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: var(--radius-md);
|
|
97
|
+
font-family: var(--font-sans);
|
|
98
|
+
font-size: var(--text-sm);
|
|
99
|
+
font-weight: var(--weight-semibold);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
102
|
+
}
|
|
103
|
+
.ui-news__btn:hover,
|
|
104
|
+
.ui-news__btn.is-hover {
|
|
105
|
+
background: var(--sage-xlight);
|
|
106
|
+
}
|
|
107
|
+
.ui-news__btn:focus-visible {
|
|
108
|
+
outline: 2px solid var(--ivory-xlight);
|
|
109
|
+
outline-offset: 2px;
|
|
110
|
+
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.25);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (prefers-reduced-motion: reduce) {
|
|
114
|
+
.ui-news__btn {
|
|
115
|
+
transition: none;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
</style>
|