@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,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Decorative drifting dot field used behind short hero copy. */
|
|
3
|
+
interface Props {
|
|
4
|
+
compact?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { compact = false, class: className, ...rest } = Astro.props;
|
|
9
|
+
const classes = ["pl-dotgrid", compact && "pl-dotgrid--compact", className]
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(" ");
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class={classes} {...rest}>
|
|
15
|
+
<span class="pl-dotgrid__dots" aria-hidden="true"></span>
|
|
16
|
+
<span class="pl-dotgrid__content"><slot /></span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.pl-dotgrid {
|
|
21
|
+
position: relative;
|
|
22
|
+
min-height: 132px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
border: 1px solid var(--border);
|
|
25
|
+
border-radius: var(--radius-lg);
|
|
26
|
+
background: var(--surface);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pl-dotgrid__dots {
|
|
30
|
+
position: absolute;
|
|
31
|
+
inset: 0;
|
|
32
|
+
background-image: radial-gradient(var(--sage-light) 1.4px, transparent 1.6px);
|
|
33
|
+
background-size: 28px 28px;
|
|
34
|
+
opacity: 0.7;
|
|
35
|
+
animation: pl-drift 6s linear infinite;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.pl-dotgrid__content {
|
|
39
|
+
position: relative;
|
|
40
|
+
display: block;
|
|
41
|
+
padding: var(--space-5);
|
|
42
|
+
font-size: var(--text-lg);
|
|
43
|
+
font-weight: var(--weight-semibold);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.pl-dotgrid--compact {
|
|
47
|
+
border: 0;
|
|
48
|
+
border-radius: 0;
|
|
49
|
+
}
|
|
50
|
+
.pl-dotgrid--compact .pl-dotgrid__dots {
|
|
51
|
+
background-size: 24px 24px;
|
|
52
|
+
}
|
|
53
|
+
.pl-dotgrid--compact .pl-dotgrid__content {
|
|
54
|
+
font-size: var(--text-base);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
.pl-dotgrid__dots {
|
|
59
|
+
animation: none;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Empty state. Says what is missing and offers the one action that fixes it.
|
|
4
|
+
* Never a shrug: an empty cart links back to the category page, an empty filter
|
|
5
|
+
* result offers to clear filters.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title, class: className, ...rest } = Astro.props;
|
|
13
|
+
const hasAction = Astro.slots.has("action");
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div class={["ui-empty", className].filter(Boolean).join(" ")} {...rest}>
|
|
17
|
+
<b class="ui-empty__title">{title}</b>
|
|
18
|
+
<span class="ui-empty__body"><slot /></span>
|
|
19
|
+
{hasAction && (
|
|
20
|
+
<div class="ui-empty__action">
|
|
21
|
+
<slot name="action" />
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.ui-empty {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
align-items: flex-start;
|
|
31
|
+
gap: var(--space-2);
|
|
32
|
+
padding: var(--space-10) var(--space-6);
|
|
33
|
+
border: 1px dashed var(--border-strong);
|
|
34
|
+
border-radius: var(--radius-lg);
|
|
35
|
+
background: var(--surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-empty__title {
|
|
39
|
+
font-size: var(--text-lg);
|
|
40
|
+
font-weight: var(--weight-semibold);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ui-empty__body {
|
|
45
|
+
font-size: var(--text-sm);
|
|
46
|
+
line-height: var(--leading-sm);
|
|
47
|
+
color: var(--text-muted);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ui-empty__action {
|
|
51
|
+
margin-top: var(--space-2);
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One filter group. A fieldset so the group name is announced before the
|
|
4
|
+
* options, with the name in the legend. Drop Checkbox components into the slot.
|
|
5
|
+
*
|
|
6
|
+
* The rule sits above the heading with more space above it than below, so the
|
|
7
|
+
* rule reads as the top edge of the group rather than a divider hung under the
|
|
8
|
+
* previous one.
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
title: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { title, class: className, ...rest } = Astro.props;
|
|
16
|
+
|
|
17
|
+
const classes = ["ui-filtergroup", className].filter(Boolean).join(" ");
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<fieldset class={classes} {...rest}>
|
|
21
|
+
<legend class="ui-filtergroup__title">{title}</legend>
|
|
22
|
+
<div class="ui-filtergroup__options">
|
|
23
|
+
<slot />
|
|
24
|
+
</div>
|
|
25
|
+
</fieldset>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.ui-filtergroup {
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: var(--space-3) 0 0;
|
|
31
|
+
border: 0;
|
|
32
|
+
/* More space above the rule than below it, so the rule belongs to this
|
|
33
|
+
group's top edge. */
|
|
34
|
+
margin-top: var(--space-4);
|
|
35
|
+
border-top: 1px solid var(--border);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-filtergroup:first-of-type {
|
|
39
|
+
margin-top: 0;
|
|
40
|
+
border-top: 0;
|
|
41
|
+
padding-top: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ui-filtergroup__title {
|
|
45
|
+
padding: 0;
|
|
46
|
+
margin-bottom: var(--space-2);
|
|
47
|
+
font-family: var(--font-sans);
|
|
48
|
+
font-size: var(--text-sm);
|
|
49
|
+
font-weight: var(--weight-semibold);
|
|
50
|
+
color: var(--text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-filtergroup__options {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: var(--space-2);
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Filter sidebar. A 232 px aside on desktop. A bar across the top carries the
|
|
4
|
+
* "Filters" eyebrow and a clear action, then the filter groups follow in the
|
|
5
|
+
* default slot (use FilterGroup with Checkbox inside).
|
|
6
|
+
*
|
|
7
|
+
* On mobile the same groups move into a bottom sheet: render the React Modal
|
|
8
|
+
* with variant="sheet" and place the FilterGroups inside it. The result count
|
|
9
|
+
* should be announced in a polite live region by the consumer when filters
|
|
10
|
+
* change.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Text for the clear-all action. Omit to hide it. */
|
|
14
|
+
clearLabel?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { clearLabel = "Wissen", class: className, ...rest } = Astro.props;
|
|
19
|
+
|
|
20
|
+
const classes = ["ui-filters", className].filter(Boolean).join(" ");
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<aside class={classes} {...rest}>
|
|
24
|
+
<div class="ui-filterbar">
|
|
25
|
+
<span class="ui-eyebrow">Filters</span>
|
|
26
|
+
{clearLabel && <slot name="clear"><button class="ui-filters__clear" type="button">{clearLabel}</button></slot>}
|
|
27
|
+
</div>
|
|
28
|
+
<slot />
|
|
29
|
+
</aside>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
.ui-filters {
|
|
33
|
+
width: 232px;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-filterbar {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
gap: var(--space-3);
|
|
43
|
+
padding-bottom: var(--space-3);
|
|
44
|
+
margin-bottom: var(--space-1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-eyebrow {
|
|
48
|
+
font-family: var(--font-mono);
|
|
49
|
+
font-size: var(--text-2xs);
|
|
50
|
+
font-weight: var(--weight-semibold);
|
|
51
|
+
letter-spacing: var(--tracking-widest);
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
color: var(--text-muted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* A ghost-weight clear action. Kept local so the sidebar needs no button
|
|
57
|
+
import; the consumer may slot in a Button ghost instead. */
|
|
58
|
+
.ui-filters__clear {
|
|
59
|
+
padding: var(--space-1-5) var(--space-2);
|
|
60
|
+
background: transparent;
|
|
61
|
+
border: 0;
|
|
62
|
+
border-radius: var(--radius-sm);
|
|
63
|
+
font-family: var(--font-sans);
|
|
64
|
+
font-size: var(--text-xs);
|
|
65
|
+
font-weight: var(--weight-semibold);
|
|
66
|
+
color: var(--link);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
.ui-filters__clear:hover {
|
|
70
|
+
background: var(--surface-hover);
|
|
71
|
+
color: var(--link-hover);
|
|
72
|
+
}
|
|
73
|
+
.ui-filters__clear:focus-visible {
|
|
74
|
+
outline: 2px solid var(--focus);
|
|
75
|
+
outline-offset: 2px;
|
|
76
|
+
box-shadow: 0 0 0 4px var(--focus-outer);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (max-width: 700px) {
|
|
80
|
+
.ui-filters {
|
|
81
|
+
width: 100%;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One dark footer panel for both sites. A <footer> landmark. Brand, address
|
|
4
|
+
* and socials on the left, link columns, then the newsletter. The bottom bar
|
|
5
|
+
* carries the legal line and the policy links.
|
|
6
|
+
*
|
|
7
|
+
* Slots:
|
|
8
|
+
* - `brand-suffix` sits after the brand name (the shop wordmark).
|
|
9
|
+
* - `address` is the address block.
|
|
10
|
+
* - `social` holds the Social links.
|
|
11
|
+
* - default slot holds the FooterColumn columns and the Newsletter.
|
|
12
|
+
* - `legal` is the legal line, `policies` the policy links, both in the bar.
|
|
13
|
+
*
|
|
14
|
+
* Three columns on desktop (1.5 / 1 / 1.3), stacked on mobile.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
brandName?: string;
|
|
18
|
+
variant?: "site" | "shop";
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
brandName = "Clubnaam",
|
|
24
|
+
variant = "site",
|
|
25
|
+
class: className,
|
|
26
|
+
...rest
|
|
27
|
+
} = Astro.props;
|
|
28
|
+
|
|
29
|
+
const classes = ["ui-footer", `ui-footer--${variant}`, className]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" ");
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<footer class={classes} {...rest}>
|
|
35
|
+
<div class="ui-footer__brandblock">
|
|
36
|
+
<div class="ui-footer__brand">{brandName}<slot name="brand-suffix" /></div>
|
|
37
|
+
<div class="ui-footer__addr"><slot name="address" /></div>
|
|
38
|
+
<div class="ui-footer__social"><slot name="social" /></div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<slot />
|
|
42
|
+
|
|
43
|
+
<div class="ui-footer__bar">
|
|
44
|
+
<span class="ui-footer__legal"><slot name="legal" /></span>
|
|
45
|
+
<span class="ui-footer__policies"><slot name="policies" /></span>
|
|
46
|
+
</div>
|
|
47
|
+
</footer>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.ui-footer {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: 1.5fr 1fr 1.3fr;
|
|
53
|
+
gap: var(--space-10);
|
|
54
|
+
padding: var(--space-10);
|
|
55
|
+
background: var(--sage-xdark);
|
|
56
|
+
color: var(--ivory-light);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ui-footer__brandblock {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
gap: var(--space-3);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ui-footer__brand {
|
|
66
|
+
font-family: var(--font-sans);
|
|
67
|
+
font-size: var(--text-xl);
|
|
68
|
+
font-weight: var(--weight-bold);
|
|
69
|
+
letter-spacing: var(--tracking-tight);
|
|
70
|
+
color: var(--ivory-xlight);
|
|
71
|
+
}
|
|
72
|
+
.ui-footer__brand :global(span) {
|
|
73
|
+
font-weight: var(--weight-regular);
|
|
74
|
+
color: var(--sage-light);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-footer__addr {
|
|
78
|
+
font-size: var(--text-sm);
|
|
79
|
+
line-height: var(--leading-sm);
|
|
80
|
+
color: var(--ivory-light);
|
|
81
|
+
}
|
|
82
|
+
.ui-footer__addr :global(a) {
|
|
83
|
+
color: inherit;
|
|
84
|
+
text-decoration: underline;
|
|
85
|
+
text-underline-offset: 2px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ui-footer__social {
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: var(--space-1);
|
|
91
|
+
margin-top: var(--space-1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* The bottom bar spans all columns. */
|
|
95
|
+
.ui-footer__bar {
|
|
96
|
+
grid-column: 1 / -1;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
gap: var(--space-4);
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
margin-top: var(--space-4);
|
|
103
|
+
padding-top: var(--space-5);
|
|
104
|
+
border-top: 1px solid var(--sage-text);
|
|
105
|
+
font-size: var(--text-xs);
|
|
106
|
+
color: var(--sage-light);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ui-footer__policies {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
gap: var(--space-4);
|
|
113
|
+
}
|
|
114
|
+
.ui-footer__bar :global(a) {
|
|
115
|
+
color: var(--ivory-light);
|
|
116
|
+
text-decoration: underline;
|
|
117
|
+
text-underline-offset: 2px;
|
|
118
|
+
}
|
|
119
|
+
.ui-footer__bar :global(a:hover) {
|
|
120
|
+
color: var(--ivory-xlight);
|
|
121
|
+
}
|
|
122
|
+
.ui-footer__bar :global(a:focus-visible) {
|
|
123
|
+
outline: 2px solid var(--ivory-xlight);
|
|
124
|
+
outline-offset: 2px;
|
|
125
|
+
border-radius: var(--radius-sm);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Pin the stacked state in component explorers and constrained shells. */
|
|
129
|
+
.ui-footer--mobile {
|
|
130
|
+
grid-template-columns: 1fr;
|
|
131
|
+
gap: var(--space-6);
|
|
132
|
+
padding: var(--space-6);
|
|
133
|
+
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
134
|
+
}
|
|
135
|
+
.ui-footer--mobile .ui-footer__bar {
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
align-items: flex-start;
|
|
138
|
+
gap: var(--space-3);
|
|
139
|
+
margin-top: var(--space-2);
|
|
140
|
+
padding-top: var(--space-5);
|
|
141
|
+
}
|
|
142
|
+
.ui-footer--mobile .ui-footer__policies {
|
|
143
|
+
row-gap: var(--space-3);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* The club-site footer interleaves contact details and social links on
|
|
147
|
+
compact screens, so its brand block participates in the outer layout. */
|
|
148
|
+
.ui-footer--mobile.ui-footer--site {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
}
|
|
152
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__brandblock {
|
|
153
|
+
display: contents;
|
|
154
|
+
}
|
|
155
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__brand {
|
|
156
|
+
order: 1;
|
|
157
|
+
font-size: var(--text-lg);
|
|
158
|
+
font-weight: var(--weight-regular);
|
|
159
|
+
color: var(--sage-light);
|
|
160
|
+
}
|
|
161
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__addr {
|
|
162
|
+
order: 2;
|
|
163
|
+
}
|
|
164
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-footer__contact) {
|
|
165
|
+
order: 3;
|
|
166
|
+
gap: var(--space-2);
|
|
167
|
+
margin-top: calc(var(--space-6) * -1 + var(--space-2));
|
|
168
|
+
}
|
|
169
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-footer__contact .ui-footer__title),
|
|
170
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-footer__desktop-only) {
|
|
171
|
+
display: none;
|
|
172
|
+
}
|
|
173
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__social {
|
|
174
|
+
order: 4;
|
|
175
|
+
gap: var(--space-3);
|
|
176
|
+
margin-top: var(--space-1);
|
|
177
|
+
}
|
|
178
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__social :global(.ui-social) {
|
|
179
|
+
width: 48px;
|
|
180
|
+
height: 48px;
|
|
181
|
+
border: 1px solid var(--sage-text);
|
|
182
|
+
border-radius: var(--radius-full);
|
|
183
|
+
}
|
|
184
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-footer__newsletter) {
|
|
185
|
+
order: 5;
|
|
186
|
+
gap: var(--space-4);
|
|
187
|
+
margin-top: var(--space-4);
|
|
188
|
+
}
|
|
189
|
+
.ui-footer--mobile.ui-footer--site .ui-footer__bar {
|
|
190
|
+
order: 6;
|
|
191
|
+
}
|
|
192
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-news) {
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
max-width: none;
|
|
195
|
+
gap: var(--space-3);
|
|
196
|
+
}
|
|
197
|
+
.ui-footer--mobile.ui-footer--site :global(.ui-news__btn) {
|
|
198
|
+
background: var(--ivory-xlight);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.ui-footer--mobile.ui-footer--shop {
|
|
202
|
+
gap: var(--space-6);
|
|
203
|
+
}
|
|
204
|
+
.ui-footer--mobile.ui-footer--shop .ui-footer__brandblock {
|
|
205
|
+
gap: var(--space-5);
|
|
206
|
+
}
|
|
207
|
+
.ui-footer--mobile.ui-footer--shop .ui-footer__brand {
|
|
208
|
+
font-size: var(--text-md);
|
|
209
|
+
}
|
|
210
|
+
.ui-footer--mobile.ui-footer--shop .ui-footer__brand :global(span) {
|
|
211
|
+
font-weight: inherit;
|
|
212
|
+
color: inherit;
|
|
213
|
+
}
|
|
214
|
+
.ui-footer--mobile.ui-footer--shop :global(.ui-footer__shop-links) {
|
|
215
|
+
gap: var(--space-4);
|
|
216
|
+
}
|
|
217
|
+
.ui-footer--mobile.ui-footer--shop :global(.ui-footer__shop-links .ui-footer__title) {
|
|
218
|
+
display: none;
|
|
219
|
+
}
|
|
220
|
+
.ui-footer--mobile.ui-footer--shop .ui-footer__social:empty {
|
|
221
|
+
display: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Stacks on mobile. */
|
|
225
|
+
@media (max-width: 700px) {
|
|
226
|
+
.ui-footer:not(.ui-footer--mobile) {
|
|
227
|
+
grid-template-columns: 1fr;
|
|
228
|
+
gap: var(--space-8);
|
|
229
|
+
padding: var(--space-6) var(--space-4);
|
|
230
|
+
}
|
|
231
|
+
.ui-footer:not(.ui-footer--mobile) .ui-footer__bar {
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
align-items: flex-start;
|
|
234
|
+
gap: var(--space-3);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A footer link column. The title is a mono heading so a reader can skip
|
|
4
|
+
* straight to it. Put the <a> links in the default slot.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title, class: className, ...rest } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const classes = ["ui-footer__col", className].filter(Boolean).join(" ");
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div class={classes} {...rest}>
|
|
17
|
+
<span class="ui-footer__title">{title}</span>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.ui-footer__col {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: var(--space-2-5, var(--space-2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ui-footer__title {
|
|
29
|
+
font-family: var(--font-mono);
|
|
30
|
+
font-size: var(--text-2xs);
|
|
31
|
+
font-weight: var(--weight-semibold);
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
letter-spacing: var(--tracking-widest);
|
|
34
|
+
color: var(--sage-light);
|
|
35
|
+
margin-bottom: var(--space-1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ui-footer__col :global(a),
|
|
39
|
+
.ui-footer__col :global(.ui-footer__note) {
|
|
40
|
+
font-size: var(--text-sm);
|
|
41
|
+
line-height: var(--leading-sm);
|
|
42
|
+
color: var(--ivory-light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ui-footer__col :global(a) {
|
|
46
|
+
text-decoration: underline;
|
|
47
|
+
text-underline-offset: 2px;
|
|
48
|
+
transition: color var(--duration-fast) var(--ease-standard);
|
|
49
|
+
}
|
|
50
|
+
.ui-footer__col :global(a:hover) {
|
|
51
|
+
color: var(--ivory-xlight);
|
|
52
|
+
}
|
|
53
|
+
.ui-footer__col :global(a:focus-visible) {
|
|
54
|
+
outline: 2px solid var(--ivory-xlight);
|
|
55
|
+
outline-offset: 2px;
|
|
56
|
+
border-radius: var(--radius-sm);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (prefers-reduced-motion: reduce) {
|
|
60
|
+
.ui-footer__col :global(a) {
|
|
61
|
+
transition: none;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One header for both sites: brand left, navigation right. Put NavLink,
|
|
4
|
+
* NavDropdown and any action button in the default slot; they sit in the nav.
|
|
5
|
+
*
|
|
6
|
+
* The `logo` slot carries the brand mark (a logo img or placeholder). Below
|
|
7
|
+
* 900 px the nav collapses: put the mobile controls (the menu and cart
|
|
8
|
+
* IconButtons) in the `mobile` slot, and open the Drawer (React) from there.
|
|
9
|
+
*/
|
|
10
|
+
interface Props {
|
|
11
|
+
brandName?: string;
|
|
12
|
+
brandHref?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { brandName = "KWTV", brandHref = "/", class: className, ...rest } =
|
|
17
|
+
Astro.props;
|
|
18
|
+
|
|
19
|
+
const classes = ["ui-header", className].filter(Boolean).join(" ");
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<header class={classes} {...rest}>
|
|
23
|
+
<a class="ui-brand" href={brandHref}>
|
|
24
|
+
<span class="ui-brand__mark"><slot name="logo" /></span>
|
|
25
|
+
<span class="ui-brand__name">{brandName}<slot name="brand-suffix" /></span>
|
|
26
|
+
</a>
|
|
27
|
+
|
|
28
|
+
<nav class="ui-nav">
|
|
29
|
+
<slot />
|
|
30
|
+
</nav>
|
|
31
|
+
|
|
32
|
+
<div class="ui-header__mobile">
|
|
33
|
+
<slot name="mobile" />
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
.ui-header {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
gap: var(--space-6);
|
|
43
|
+
min-height: 73px;
|
|
44
|
+
padding: var(--space-5) var(--space-10);
|
|
45
|
+
background: var(--bg);
|
|
46
|
+
border-bottom: 1px solid var(--border);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ui-brand {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: var(--space-2-5, var(--space-2));
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
color: var(--text);
|
|
55
|
+
}
|
|
56
|
+
.ui-brand:focus-visible {
|
|
57
|
+
outline: 2px solid var(--focus);
|
|
58
|
+
outline-offset: 2px;
|
|
59
|
+
border-radius: var(--radius-sm);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ui-brand__mark {
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ui-brand__name {
|
|
69
|
+
font-family: var(--font-sans);
|
|
70
|
+
font-size: var(--text-lg);
|
|
71
|
+
font-weight: var(--weight-bold);
|
|
72
|
+
letter-spacing: var(--tracking-tight);
|
|
73
|
+
}
|
|
74
|
+
/* The shop wordmark: a lighter "shop" after the name. */
|
|
75
|
+
.ui-brand__name :global(span) {
|
|
76
|
+
font-weight: var(--weight-regular);
|
|
77
|
+
color: var(--text-muted);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ui-nav {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: var(--space-6);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ui-header__mobile {
|
|
87
|
+
display: none;
|
|
88
|
+
gap: var(--space-2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Pin the compact state in component explorers and other constrained shells. */
|
|
92
|
+
.ui-header--mobile {
|
|
93
|
+
padding-inline: var(--space-4);
|
|
94
|
+
}
|
|
95
|
+
.ui-header--mobile .ui-nav {
|
|
96
|
+
display: none;
|
|
97
|
+
}
|
|
98
|
+
.ui-header--mobile .ui-header__mobile {
|
|
99
|
+
display: flex;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Below 900 px the links collapse. The consumer fills the mobile slot with
|
|
103
|
+
the menu and cart buttons and opens the Drawer from there. */
|
|
104
|
+
@media (max-width: 900px) {
|
|
105
|
+
.ui-header,
|
|
106
|
+
.ui-header--mobile {
|
|
107
|
+
padding-inline: var(--space-4);
|
|
108
|
+
}
|
|
109
|
+
.ui-header:not(.ui-header--mobile) .ui-nav {
|
|
110
|
+
display: none;
|
|
111
|
+
}
|
|
112
|
+
.ui-header:not(.ui-header--mobile) .ui-header__mobile {
|
|
113
|
+
display: flex;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|