ferst-core 0.7.6 → 0.7.8
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/bin/ferst-modules.mjs +107 -0
- package/components/BlockRenderer.astro +125 -117
- package/components/Button.astro +1 -1
- package/components/Card.astro +2 -0
- package/components/ProseParts.astro +26 -0
- package/components/SiteHeader.astro +24 -0
- package/components/blocks/Prose.astro +12 -17
- package/components/recipes/Accordion.astro +91 -91
- package/components/recipes/Bento.astro +7 -3
- package/components/recipes/ContactForm.astro +6 -3
- package/components/recipes/Cta.astro +5 -1
- package/components/recipes/Document.astro +7 -11
- package/components/recipes/Faq.astro +5 -1
- package/components/recipes/FeatureCards.astro +115 -112
- package/components/recipes/Form.astro +30 -0
- package/components/recipes/Hero.astro +502 -495
- package/components/recipes/LatestPosts.astro +1 -1
- package/components/recipes/MediaCards.astro +214 -213
- package/components/recipes/MediaText.astro +88 -92
- package/components/recipes/Notice.astro +90 -90
- package/components/recipes/Pricing.astro +7 -4
- package/components/recipes/Stats.astro +155 -152
- package/components/recipes/Steps.astro +124 -121
- package/components/recipes/Table.astro +3 -3
- package/components/recipes/Tabs.astro +138 -134
- package/components/recipes/Tiles.astro +291 -287
- package/content/blocks.ts +943 -895
- package/layouts/Base.astro +495 -484
- package/lib/forms.ts +57 -0
- package/lib/slug.ts +1 -1
- package/package.json +70 -69
|
@@ -1,121 +1,124 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
-
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
-
import Heading from '../blocks/Heading.astro';
|
|
5
|
-
import Icon from '../Icon.astro';
|
|
6
|
-
import { isIconKey } from '../../lib/icons';
|
|
7
|
-
|
|
8
|
-
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
9
|
-
interface Item { title: string; text?: string; icon?: string }
|
|
10
|
-
interface Props {
|
|
11
|
-
title?: string;
|
|
12
|
-
intro?: string;
|
|
13
|
-
layout?: 'horizontal' | 'vertical';
|
|
14
|
-
items?: Item[];
|
|
15
|
-
}
|
|
16
|
-
const { title, intro, layout = 'horizontal', items = [] } = Astro.props;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{item.
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.b-
|
|
91
|
-
|
|
92
|
-
/*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Steps — a Level-2 recipe: a numbered process. `horizontal` lays connected
|
|
3
|
+
* cards in a row; `vertical` a stacked timeline. Composes Heading + Icon. */
|
|
4
|
+
import Heading from '../blocks/Heading.astro';
|
|
5
|
+
import Icon from '../Icon.astro';
|
|
6
|
+
import { isIconKey } from '../../lib/icons';
|
|
7
|
+
|
|
8
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
9
|
+
interface Item { title: string; text?: string; icon?: string }
|
|
10
|
+
interface Props {
|
|
11
|
+
title?: string;
|
|
12
|
+
intro?: string;
|
|
13
|
+
layout?: 'horizontal' | 'vertical';
|
|
14
|
+
items?: Item[];
|
|
15
|
+
}
|
|
16
|
+
const { title, intro, layout = 'horizontal', items = [] } = Astro.props;
|
|
17
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
18
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
19
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
20
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section class="b-steps" data-layout={layout}>
|
|
24
|
+
{(title || intro) && (
|
|
25
|
+
<div class="b-steps__head" data-recipe-head data-content-align={headAlign}>
|
|
26
|
+
{title && <Heading text={title} level={2} />}
|
|
27
|
+
{intro && <p class="b-steps__intro" set:html={inlineMarkdown(intro)} />}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<ol class="b-steps__list">
|
|
31
|
+
{items.map((item, i) => (
|
|
32
|
+
<li class="b-steps__item">
|
|
33
|
+
<span class="b-steps__marker" aria-hidden="true">
|
|
34
|
+
{item.icon && isIconKey(item.icon) ? <Icon name={item.icon} size={20} /> : i + 1}
|
|
35
|
+
</span>
|
|
36
|
+
<div class="b-steps__body">
|
|
37
|
+
<h3 class="b-steps__title">{item.title}</h3>
|
|
38
|
+
{item.text && <p class="b-steps__text">{item.text}</p>}
|
|
39
|
+
</div>
|
|
40
|
+
</li>
|
|
41
|
+
))}
|
|
42
|
+
</ol>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.b-steps {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 2rem;
|
|
50
|
+
max-width: 72rem;
|
|
51
|
+
margin-inline: auto;
|
|
52
|
+
padding-inline: 1rem;
|
|
53
|
+
}
|
|
54
|
+
.b-steps__head {
|
|
55
|
+
max-width: var(--max);
|
|
56
|
+
margin-inline: auto;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 0.75rem;
|
|
60
|
+
}
|
|
61
|
+
.b-steps__intro { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
62
|
+
.b-steps__list {
|
|
63
|
+
list-style: none;
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
display: grid;
|
|
67
|
+
gap: 1.5rem;
|
|
68
|
+
}
|
|
69
|
+
/* Base (and mobile): marker left, body right. */
|
|
70
|
+
.b-steps__item {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 1rem;
|
|
73
|
+
align-items: flex-start;
|
|
74
|
+
}
|
|
75
|
+
.b-steps__marker {
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
width: 2.5rem;
|
|
78
|
+
height: 2.5rem;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
border-radius: var(--radius-pill);
|
|
83
|
+
background: var(--gold);
|
|
84
|
+
color: var(--text-light);
|
|
85
|
+
font-family: var(--font-heading);
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
font-size: 1.05rem;
|
|
88
|
+
}
|
|
89
|
+
.b-steps__title { margin: 0 0 0.35rem; font-size: 1.1rem; color: var(--fg); }
|
|
90
|
+
.b-steps__text { margin: 0; color: var(--muted); line-height: 1.6; }
|
|
91
|
+
|
|
92
|
+
/* Vertical: a centred, stacked timeline. */
|
|
93
|
+
.b-steps[data-layout='vertical'] .b-steps__list { max-width: 40rem; margin-inline: auto; }
|
|
94
|
+
|
|
95
|
+
/* Horizontal (≥48em): connected cards in a row, marker over centred body. */
|
|
96
|
+
@media (min-width: 48em) {
|
|
97
|
+
.b-steps[data-layout='horizontal'] .b-steps__list {
|
|
98
|
+
grid-auto-flow: column;
|
|
99
|
+
grid-auto-columns: 1fr;
|
|
100
|
+
gap: 1rem;
|
|
101
|
+
}
|
|
102
|
+
.b-steps[data-layout='horizontal'] .b-steps__item {
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
align-items: center;
|
|
105
|
+
text-align: center;
|
|
106
|
+
gap: 0.85rem;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
/* Connector from this marker's right edge to the next marker's left edge.
|
|
110
|
+
The +1.5rem / -2rem account for the 2.5rem marker AND the 1rem grid gap,
|
|
111
|
+
so the line sits centred between markers (not falling short). */
|
|
112
|
+
.b-steps[data-layout='horizontal'] .b-steps__item:not(:last-child)::after {
|
|
113
|
+
content: '';
|
|
114
|
+
position: absolute;
|
|
115
|
+
top: calc(1.25rem - 1px);
|
|
116
|
+
left: calc(50% + 1.5rem);
|
|
117
|
+
width: calc(100% - 2rem);
|
|
118
|
+
height: 2px;
|
|
119
|
+
background: var(--border);
|
|
120
|
+
z-index: 0;
|
|
121
|
+
}
|
|
122
|
+
.b-steps[data-layout='horizontal'] .b-steps__marker { position: relative; z-index: 1; }
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -9,7 +9,7 @@ import Heading from '../blocks/Heading.astro';
|
|
|
9
9
|
|
|
10
10
|
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
11
11
|
import { filledColumns } from '../../lib/table';
|
|
12
|
-
import
|
|
12
|
+
import ProseParts from '../ProseParts.astro';
|
|
13
13
|
interface Props {
|
|
14
14
|
title?: string;
|
|
15
15
|
intro?: string;
|
|
@@ -25,7 +25,7 @@ const keep = filledColumns(columns, rows);
|
|
|
25
25
|
{(title || intro) && (
|
|
26
26
|
<div class="b-table__head">
|
|
27
27
|
{title && <Heading text={title} level={2} />}
|
|
28
|
-
{intro &&
|
|
28
|
+
{intro && <ProseParts text={intro} class="b-table__intro" />}
|
|
29
29
|
</div>
|
|
30
30
|
)}
|
|
31
31
|
<div class="b-table__scroll">
|
|
@@ -58,7 +58,7 @@ const keep = filledColumns(columns, rows);
|
|
|
58
58
|
flex-direction: column;
|
|
59
59
|
gap: 0.6rem;
|
|
60
60
|
}
|
|
61
|
-
.b-table__intro {
|
|
61
|
+
:global(.b-table__intro) {
|
|
62
62
|
margin: 0;
|
|
63
63
|
color: var(--muted);
|
|
64
64
|
max-width: var(--max);
|
|
@@ -1,134 +1,138 @@
|
|
|
1
|
-
---
|
|
2
|
-
/** Tabs — a Level-2 recipe: labelled panels, one open at a time. Progressive:
|
|
3
|
-
* the first panel is visible with no JavaScript; a small script adds selection,
|
|
4
|
-
* ARIA state and arrow-key navigation. Composes the Heading primitive. */
|
|
5
|
-
import Heading from '../blocks/Heading.astro';
|
|
6
|
-
|
|
7
|
-
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
8
|
-
interface Item { label: string; heading?: string; text?: string; image?: { src: string; alt?: string } }
|
|
9
|
-
interface Props {
|
|
10
|
-
title?: string;
|
|
11
|
-
items?: Item[];
|
|
12
|
-
}
|
|
13
|
-
const { title, items = [] } = Astro.props;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
---
|
|
2
|
+
/** Tabs — a Level-2 recipe: labelled panels, one open at a time. Progressive:
|
|
3
|
+
* the first panel is visible with no JavaScript; a small script adds selection,
|
|
4
|
+
* ARIA state and arrow-key navigation. Composes the Heading primitive. */
|
|
5
|
+
import Heading from '../blocks/Heading.astro';
|
|
6
|
+
|
|
7
|
+
import { inlineMarkdown } from '../../lib/inline-markdown';
|
|
8
|
+
interface Item { label: string; heading?: string; text?: string; image?: { src: string; alt?: string } }
|
|
9
|
+
interface Props {
|
|
10
|
+
title?: string;
|
|
11
|
+
items?: Item[];
|
|
12
|
+
}
|
|
13
|
+
const { title, items = [] } = Astro.props;
|
|
14
|
+
// Alignment overrides (BlockLayout): the title + intro default to centred; the block's
|
|
15
|
+
// own `align`, or `headingAlign` for the title alone, overrides it from the page data.
|
|
16
|
+
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
17
|
+
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
18
|
+
const uid = 'tabs-' + Math.random().toString(36).slice(2, 9);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<section class="b-tabs">
|
|
22
|
+
{title && <Heading text={title} level={2} align={headAlign} />}
|
|
23
|
+
<div class="b-tabs__inner">
|
|
24
|
+
<div class="b-tabs__list" role="tablist" aria-label={title ?? 'Tabs'}>
|
|
25
|
+
{items.map((item, i) => (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
role="tab"
|
|
29
|
+
id={`${uid}-tab-${i}`}
|
|
30
|
+
aria-controls={`${uid}-panel-${i}`}
|
|
31
|
+
aria-selected={i === 0 ? 'true' : 'false'}
|
|
32
|
+
tabindex={i === 0 ? 0 : -1}
|
|
33
|
+
class="b-tabs__tab"
|
|
34
|
+
>
|
|
35
|
+
{item.label}
|
|
36
|
+
</button>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
{items.map((item, i) => (
|
|
40
|
+
<div
|
|
41
|
+
role="tabpanel"
|
|
42
|
+
id={`${uid}-panel-${i}`}
|
|
43
|
+
aria-labelledby={`${uid}-tab-${i}`}
|
|
44
|
+
class="b-tabs__panel"
|
|
45
|
+
hidden={i !== 0}
|
|
46
|
+
tabindex="0"
|
|
47
|
+
>
|
|
48
|
+
{item.image && (
|
|
49
|
+
<img class="b-tabs__img" src={item.image.src} alt={item.image.alt ?? ''} loading="lazy" decoding="async" />
|
|
50
|
+
)}
|
|
51
|
+
<div class="b-tabs__copy">
|
|
52
|
+
{item.heading && <h3 class="b-tabs__heading">{item.heading}</h3>}
|
|
53
|
+
{item.text && <p class="b-tabs__text" set:html={inlineMarkdown(item.text)} />}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.b-tabs {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 1.5rem;
|
|
65
|
+
max-width: 60rem;
|
|
66
|
+
margin-inline: auto;
|
|
67
|
+
padding-inline: 1rem;
|
|
68
|
+
}
|
|
69
|
+
.b-tabs__list {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
gap: 0.25rem;
|
|
73
|
+
border-bottom: 1px solid var(--border);
|
|
74
|
+
}
|
|
75
|
+
.b-tabs__tab {
|
|
76
|
+
appearance: none;
|
|
77
|
+
background: none;
|
|
78
|
+
border: none;
|
|
79
|
+
border-bottom: 2px solid transparent;
|
|
80
|
+
margin-bottom: -1px;
|
|
81
|
+
padding: 0.7rem 1rem;
|
|
82
|
+
font-family: var(--font-heading);
|
|
83
|
+
font-size: 0.95rem;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
transition: color 0.15s, border-color 0.15s;
|
|
88
|
+
}
|
|
89
|
+
.b-tabs__tab:hover { color: var(--fg); }
|
|
90
|
+
.b-tabs__tab[aria-selected='true'] { color: var(--gold); border-bottom-color: var(--gold); }
|
|
91
|
+
.b-tabs__tab:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 4px; }
|
|
92
|
+
.b-tabs__panel {
|
|
93
|
+
display: grid;
|
|
94
|
+
gap: 1.25rem;
|
|
95
|
+
}
|
|
96
|
+
/* A class `display` beats the UA `[hidden]{display:none}` (equal specificity,
|
|
97
|
+
author origin wins), so make the hidden state explicit — for the first paint
|
|
98
|
+
and every JS toggle. */
|
|
99
|
+
.b-tabs__panel[hidden] {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
@media (min-width: 48em) {
|
|
103
|
+
.b-tabs__panel:has(.b-tabs__img) { grid-template-columns: 1fr 1fr; align-items: center; }
|
|
104
|
+
}
|
|
105
|
+
.b-tabs__panel:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; border-radius: var(--radius-sm); }
|
|
106
|
+
.b-tabs__img { width: 100%; height: auto; border-radius: var(--radius); }
|
|
107
|
+
.b-tabs__heading { margin: 0 0 0.5rem; color: var(--fg); }
|
|
108
|
+
.b-tabs__text { margin: 0; color: var(--muted); line-height: 1.65; }
|
|
109
|
+
</style>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
// Wire every tabs group on the page: selection, ARIA state, arrow-key nav.
|
|
113
|
+
document.querySelectorAll<HTMLElement>('.b-tabs').forEach((root) => {
|
|
114
|
+
const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
|
|
115
|
+
const panels = Array.from(root.querySelectorAll<HTMLElement>('[role="tabpanel"]'));
|
|
116
|
+
if (!tabs.length) return;
|
|
117
|
+
|
|
118
|
+
function select(index: number, focus = true) {
|
|
119
|
+
tabs.forEach((tab, i) => {
|
|
120
|
+
const on = i === index;
|
|
121
|
+
tab.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
122
|
+
tab.tabIndex = on ? 0 : -1;
|
|
123
|
+
panels[i]?.toggleAttribute('hidden', !on);
|
|
124
|
+
});
|
|
125
|
+
if (focus) tabs[index]?.focus();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
tabs.forEach((tab, i) => {
|
|
129
|
+
tab.addEventListener('click', () => select(i, false));
|
|
130
|
+
tab.addEventListener('keydown', (e) => {
|
|
131
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); select((i + 1) % tabs.length); }
|
|
132
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); select((i - 1 + tabs.length) % tabs.length); }
|
|
133
|
+
else if (e.key === 'Home') { e.preventDefault(); select(0); }
|
|
134
|
+
else if (e.key === 'End') { e.preventDefault(); select(tabs.length - 1); }
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
</script>
|