@contentrain/astro-kit 0.32.0 → 0.33.1
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/catalog.json +850 -19
- package/components/_shared/Button.astro +2 -1
- package/components/_shared/KitImage.astro +9 -4
- package/components/_shared/Section.astro +3 -1
- package/components/_shared/SectionHeader.astro +27 -0
- package/components/_shared/measured.ts +13 -0
- package/components/_shared/safe-url.ts +67 -0
- package/components/_shared/social.ts +73 -0
- package/components/announcement/Announcement.astro +2 -1
- package/components/announcement/meta.json +1 -1
- package/components/band/Band.astro +3 -2
- package/components/band/meta.json +1 -0
- package/components/breadcrumb/Breadcrumb.astro +2 -1
- package/components/breadcrumb/meta.json +1 -1
- package/components/card-grid/CardGrid.astro +2 -1
- package/components/card-grid/meta.json +1 -0
- package/components/consent/Consent.astro +2 -1
- package/components/consent/meta.json +1 -1
- package/components/contact-details/ContactDetails.astro +3 -2
- package/components/contact-details/meta.json +1 -0
- package/components/contact-form/ContactForm.astro +4 -3
- package/components/contact-form/meta.json +1 -0
- package/components/cta/meta.json +1 -1
- package/components/dialog/Dialog.astro +113 -0
- package/components/dialog/fixtures.json +84 -0
- package/components/dialog/meta.json +252 -0
- package/components/embed/meta.json +1 -1
- package/components/faq/fixtures.json +4 -0
- package/components/faq/meta.json +2 -2
- package/components/feature-list/meta.json +1 -0
- package/components/footer/Footer.astro +5 -4
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +2 -1
- package/components/gallery/meta.json +1 -0
- package/components/header/Header.astro +17 -5
- package/components/header/fixtures.json +90 -1
- package/components/header/meta.json +5 -3
- package/components/hero/meta.json +1 -0
- package/components/language-switcher/LanguageSwitcher.astro +68 -0
- package/components/language-switcher/fixtures.json +51 -0
- package/components/language-switcher/meta.json +73 -0
- package/components/logo-cloud/LogoCloud.astro +2 -1
- package/components/logo-cloud/meta.json +1 -0
- package/components/nav/Nav.astro +77 -24
- package/components/nav/NavTree.astro +40 -0
- package/components/nav/fixtures.json +165 -0
- package/components/nav/meta.json +11 -2
- package/components/newsletter/Newsletter.astro +2 -1
- package/components/newsletter/meta.json +1 -0
- package/components/pagination/Pagination.astro +4 -3
- package/components/pagination/meta.json +1 -1
- package/components/post-card/PostCard.astro +4 -3
- package/components/post-card/meta.json +1 -1
- package/components/post-list/PostList.astro +3 -2
- package/components/post-list/meta.json +1 -0
- package/components/pricing/meta.json +1 -0
- package/components/search-box/SearchBox.astro +51 -0
- package/components/search-box/fixtures.json +31 -0
- package/components/search-box/meta.json +75 -0
- package/components/slider/Slider.astro +19 -115
- package/components/slider/SliderTrack.astro +178 -0
- package/components/slider/fixtures.json +2 -1
- package/components/slider/meta.json +11 -5
- package/components/social-links/SocialLinks.astro +74 -0
- package/components/social-links/fixtures.json +67 -0
- package/components/social-links/meta.json +94 -0
- package/components/split/meta.json +1 -0
- package/components/stats/Stats.astro +19 -8
- package/components/stats/fixtures.json +87 -0
- package/components/stats/meta.json +1 -0
- package/components/steps/meta.json +1 -0
- package/components/tabs/Tabs.astro +10 -3
- package/components/tabs/fixtures.json +4 -0
- package/components/tabs/meta.json +5 -2
- package/components/team/Team.astro +3 -2
- package/components/team/meta.json +2 -0
- package/components/team/team.ts +3 -8
- package/components/testimonial/meta.json +2 -3
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +117 -2
- package/mapping/divi.json +6 -5
- package/mapping/elementor.json +120 -7
- package/mapping/gutenberg.json +20 -8
- package/package.json +2 -2
|
@@ -122,5 +122,94 @@
|
|
|
122
122
|
]
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
|
-
{ "name": "tagline", "props": { "siteName": "Golden Agency", "tagline": "Websites that sell what you make", "items": [{ "label": "Home", "href": "/" }, { "label": "Services", "href": "/services/" }, { "label": "Contact", "href": "/contact/" }], "menuLabel": "Menu", "closeLabel": "Close" } }
|
|
125
|
+
{ "name": "tagline", "props": { "siteName": "Golden Agency", "tagline": "Websites that sell what you make", "items": [{ "label": "Home", "href": "/" }, { "label": "Services", "href": "/services/" }, { "label": "Contact", "href": "/contact/" }], "menuLabel": "Menu", "closeLabel": "Close" } },
|
|
126
|
+
{
|
|
127
|
+
"name": "mega-menu",
|
|
128
|
+
"props": {
|
|
129
|
+
"siteName": "Northwind",
|
|
130
|
+
"menu": "mega",
|
|
131
|
+
"items": [
|
|
132
|
+
{
|
|
133
|
+
"label": "Hakkımızda",
|
|
134
|
+
"href": "/hakkimizda/"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"label": "Ürünlerimiz",
|
|
138
|
+
"href": "#",
|
|
139
|
+
"children": [
|
|
140
|
+
{
|
|
141
|
+
"label": "Aracım İçin",
|
|
142
|
+
"href": "#",
|
|
143
|
+
"children": [
|
|
144
|
+
{
|
|
145
|
+
"label": "Trafik Sigortası",
|
|
146
|
+
"href": "/urunlerimiz/trafik-sigortasi/"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"label": "Kasko",
|
|
150
|
+
"href": "/urunlerimiz/kasko/"
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"label": "Evim İçin",
|
|
156
|
+
"href": "#",
|
|
157
|
+
"children": [
|
|
158
|
+
{
|
|
159
|
+
"label": "Dask",
|
|
160
|
+
"href": "/urunlerimiz/dask/"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"label": "Konut Sigortası",
|
|
164
|
+
"href": "/urunlerimiz/konut-sigortasi/"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"label": "Sağlığım İçin",
|
|
170
|
+
"href": "#",
|
|
171
|
+
"children": [
|
|
172
|
+
{
|
|
173
|
+
"label": "Sağlık Sigortası",
|
|
174
|
+
"href": "/urunlerimiz/saglik-sigortasi/"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"label": "Tamamlayıcı Sağlık",
|
|
178
|
+
"href": "/urunlerimiz/tamamlayici-saglik/"
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"label": "Diğer",
|
|
184
|
+
"href": "#",
|
|
185
|
+
"children": [
|
|
186
|
+
{
|
|
187
|
+
"label": "İş Yeri Sigortası",
|
|
188
|
+
"href": "/urunlerimiz/is-yeri-sigortasi/"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"label": "Seyahat Sigortası",
|
|
192
|
+
"href": "/urunlerimiz/seyahat-sigortasi/"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"label": "Anlaşmalı Sigorta Şirketleri",
|
|
200
|
+
"href": "/anlasmali-sigorta-sirketleri/"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"label": "İletişim",
|
|
204
|
+
"href": "/iletisim/"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"action": {
|
|
208
|
+
"label": "Get a quote",
|
|
209
|
+
"href": "/contact/"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{ "name": "search", "props": { "siteName": "Northwind Journal", "items": [{ "label": "Journal", "href": "/" }, { "label": "About", "href": "/about/" }], "search": { "label": "Search", "placeholder": "Search this site", "submitLabel": "Search" } } },
|
|
214
|
+
{"name": "mega-menu-search", "props": {"siteName": "Northwind", "menu": "mega", "items": [{"label": "Hakkımızda", "href": "/hakkimizda/"}, {"label": "Ürünlerimiz", "href": "#", "children": [{"label": "Aracım İçin", "href": "#", "children": [{"label": "Trafik Sigortası", "href": "/urunlerimiz/trafik-sigortasi/"}, {"label": "Kasko", "href": "/urunlerimiz/kasko/"}]}, {"label": "Evim İçin", "href": "#", "children": [{"label": "Dask", "href": "/urunlerimiz/dask/"}, {"label": "Konut Sigortası", "href": "/urunlerimiz/konut-sigortasi/"}]}, {"label": "Sağlığım İçin", "href": "#", "children": [{"label": "Sağlık Sigortası", "href": "/urunlerimiz/saglik-sigortasi/"}, {"label": "Tamamlayıcı Sağlık", "href": "/urunlerimiz/tamamlayici-saglik/"}]}, {"label": "Diğer", "href": "#", "children": [{"label": "İş Yeri Sigortası", "href": "/urunlerimiz/is-yeri-sigortasi/"}, {"label": "Seyahat Sigortası", "href": "/urunlerimiz/seyahat-sigortasi/"}]}]}, {"label": "Anlaşmalı Sigorta Şirketleri", "href": "/anlasmali-sigorta-sirketleri/"}, {"label": "İletişim", "href": "/iletisim/"}], "action": {"label": "Get a quote", "href": "/contact/"}, "search": {"label": "Search", "placeholder": "Ara", "submitLabel": "Ara"}}}
|
|
126
215
|
]
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
"name": "Header",
|
|
4
4
|
"description": "Site header: brand (logo and/or name), navigation and an optional call-to-action button.",
|
|
5
5
|
"category": "chrome",
|
|
6
|
-
"uses": ["nav"],
|
|
7
|
-
"shared": ["Button.astro", "KitImage.astro", "types.ts"],
|
|
6
|
+
"uses": ["nav", "search-box"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["split", "centered", "brand-left"], "default": "split", "description": "split: brand left, navigation right. centered: brand above centred navigation. brand-left: navigation right after the brand." },
|
|
10
10
|
"tone": { "options": ["default", "inverse", "transparent"], "default": "default", "description": "Surface with a bottom border, dark (ink) background, or transparent over the hero that follows." },
|
|
11
|
-
"position": { "options": ["static", "sticky"], "default": "static", "description": "Sticky keeps the header at the top while scrolling." }
|
|
11
|
+
"position": { "options": ["static", "sticky"], "default": "static", "description": "Sticky keeps the header at the top while scrolling." },
|
|
12
|
+
"menu": { "options": ["flyout", "mega"], "default": "flyout", "description": "How submenus open on wide screens: a list under the item (flyout) or a wide panel of columns (mega). Submenus nest to any depth." }
|
|
12
13
|
},
|
|
13
14
|
"props": {
|
|
14
15
|
"siteName": { "type": "string", "required": true, "description": "Site title (site.title)." },
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"homeHref": { "type": "url", "default": "/", "description": "Where the brand links." },
|
|
27
28
|
"items": { "type": "array", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "newTab": { "type": "boolean" }, "children": { "type": "array" } } }, "description": "Primary menu (menus → menu-items)." },
|
|
28
29
|
"action": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "style": { "type": "select", "options": ["primary", "secondary", "ghost"] } }, "description": "Header button, shown on wide screens." },
|
|
30
|
+
"search": { "type": "object", "fields": { "label": { "type": "string" }, "placeholder": { "type": "string" }, "submitLabel": { "type": "string" }, "action": { "type": "url" } }, "description": "A search field in the bar: a GET form to the site's search page (default /search); below the bar on a phone." },
|
|
29
31
|
"navLabel": { "type": "string", "description": "Accessible name of the navigation (ui-strings nav.label)." },
|
|
30
32
|
"menuLabel": { "type": "string", "description": "Narrow-screen toggle text (ui-strings nav.menu)." },
|
|
31
33
|
"closeLabel": { "type": "string", "description": "Drawer close text (ui-strings nav.close)." }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Links to the same page in the site's other languages. Each link carries `hreflang` and its own `lang`, so the
|
|
3
|
+
// language's name is read in that language; the current one is marked `aria-current`. A plain list, or a native
|
|
4
|
+
// `<details>` menu that shows the current language: no JavaScript. The addresses come from the caller (the page's
|
|
5
|
+
// equivalent in each language); a link without one is not drawn.
|
|
6
|
+
import { tv } from 'tailwind-variants'
|
|
7
|
+
import { safeHref } from '../_shared/safe-url'
|
|
8
|
+
|
|
9
|
+
const switcher = tv({
|
|
10
|
+
slots: {
|
|
11
|
+
list: 'flex flex-wrap items-center gap-x-4 gap-y-1',
|
|
12
|
+
link: 'text-current no-underline opacity-80 hover:opacity-100 hover:underline aria-[current=true]:font-semibold aria-[current=true]:opacity-100',
|
|
13
|
+
summary: 'inline-flex cursor-pointer list-none items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-current/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent [&::-webkit-details-marker]:hidden',
|
|
14
|
+
panel: 'absolute z-30 mt-1 flex min-w-40 flex-col gap-1 rounded-lg border border-line bg-surface p-2 text-ink shadow-lg',
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
layout: { list: {}, menu: {} },
|
|
18
|
+
align: { start: { panel: 'start-0' }, end: { panel: 'end-0' } },
|
|
19
|
+
// `name`: "Deutsch"; `code`: "DE".
|
|
20
|
+
labelStyle: { name: {}, code: { link: 'uppercase' } },
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: { layout: 'list', align: 'start', labelStyle: 'name' },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** The languages (none: nothing is drawn), `lang` a BCP 47 code ("de", "pt-BR"); `label` the language's own name ("Deutsch"). */
|
|
27
|
+
languages?: Array<{ lang: string, label: string, href: string, current?: boolean | undefined }> | undefined
|
|
28
|
+
/** Accessible name ("Language"). */
|
|
29
|
+
label?: string | undefined
|
|
30
|
+
layout?: 'list' | 'menu' | undefined
|
|
31
|
+
align?: 'start' | 'end' | undefined
|
|
32
|
+
labelStyle?: 'name' | 'code' | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { languages = [], label = 'Language', layout, align, labelStyle } = Astro.props
|
|
36
|
+
const s = switcher({ layout, align, labelStyle })
|
|
37
|
+
const shown = languages.filter(language => language.href)
|
|
38
|
+
const current = shown.find(language => language.current)
|
|
39
|
+
const text = (language: { lang: string, label: string }) => (labelStyle === 'code' ? language.lang.split('-')[0]! : language.label)
|
|
40
|
+
---
|
|
41
|
+
{shown.length > 1 && (layout === 'menu'
|
|
42
|
+
? (
|
|
43
|
+
<details class="relative" data-cr-part="language-switcher">
|
|
44
|
+
<summary class={s.summary()}>
|
|
45
|
+
<span class="sr-only">{label}: </span>
|
|
46
|
+
<span lang={current?.lang}>{current ? text(current) : label}</span>
|
|
47
|
+
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6" /></svg>
|
|
48
|
+
</summary>
|
|
49
|
+
<ul class={s.panel()}>
|
|
50
|
+
{shown.map(language => (
|
|
51
|
+
<li>
|
|
52
|
+
<a href={safeHref(language.href)} lang={language.lang} hreflang={language.lang} aria-current={language.current ? 'true' : undefined} class={s.link()}>{text(language)}</a>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</details>
|
|
57
|
+
)
|
|
58
|
+
: (
|
|
59
|
+
<nav aria-label={label} data-cr-part="language-switcher">
|
|
60
|
+
<ul class={s.list()}>
|
|
61
|
+
{shown.map(language => (
|
|
62
|
+
<li>
|
|
63
|
+
<a href={safeHref(language.href)} lang={language.lang} hreflang={language.lang} aria-current={language.current ? 'true' : undefined} class={s.link()}>{text(language)}</a>
|
|
64
|
+
</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
</nav>
|
|
68
|
+
))}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "list",
|
|
4
|
+
"props": {
|
|
5
|
+
"languages": [
|
|
6
|
+
{
|
|
7
|
+
"lang": "en",
|
|
8
|
+
"label": "English",
|
|
9
|
+
"href": "/about/",
|
|
10
|
+
"current": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"lang": "de",
|
|
14
|
+
"label": "Deutsch",
|
|
15
|
+
"href": "/de/ueber-uns/"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"lang": "tr",
|
|
19
|
+
"label": "Türkçe",
|
|
20
|
+
"href": "/tr/hakkimizda/"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "menu-end-code",
|
|
27
|
+
"props": {
|
|
28
|
+
"layout": "menu",
|
|
29
|
+
"align": "end",
|
|
30
|
+
"labelStyle": "code",
|
|
31
|
+
"languages": [
|
|
32
|
+
{
|
|
33
|
+
"lang": "en",
|
|
34
|
+
"label": "English",
|
|
35
|
+
"href": "/",
|
|
36
|
+
"current": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"lang": "pt-BR",
|
|
40
|
+
"label": "Português",
|
|
41
|
+
"href": "/pt/"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"lang": "ar",
|
|
45
|
+
"label": "العربية",
|
|
46
|
+
"href": "/ar/"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "language-switcher",
|
|
3
|
+
"name": "LanguageSwitcher",
|
|
4
|
+
"description": "Links to the same page in the site's other languages, each with hreflang and its own lang; the current one is marked aria-current. A plain list or a native <details> menu; no JavaScript. The addresses are the page's equivalents in each language, supplied by the caller.",
|
|
5
|
+
"category": "chrome",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": ["safe-url.ts"],
|
|
8
|
+
"variants": {
|
|
9
|
+
"layout": {
|
|
10
|
+
"options": [
|
|
11
|
+
"list",
|
|
12
|
+
"menu"
|
|
13
|
+
],
|
|
14
|
+
"default": "list",
|
|
15
|
+
"description": "A row of links, or a menu that shows the current language and opens on click."
|
|
16
|
+
},
|
|
17
|
+
"align": {
|
|
18
|
+
"options": [
|
|
19
|
+
"start",
|
|
20
|
+
"end"
|
|
21
|
+
],
|
|
22
|
+
"default": "start",
|
|
23
|
+
"description": "Which edge the menu panel lines up with."
|
|
24
|
+
},
|
|
25
|
+
"labelStyle": {
|
|
26
|
+
"options": [
|
|
27
|
+
"name",
|
|
28
|
+
"code"
|
|
29
|
+
],
|
|
30
|
+
"default": "name",
|
|
31
|
+
"description": "The language's own name (\"Deutsch\") or its code (\"DE\")."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"props": {
|
|
35
|
+
"languages": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"fields": {
|
|
40
|
+
"lang": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"required": true
|
|
43
|
+
},
|
|
44
|
+
"label": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"required": true
|
|
47
|
+
},
|
|
48
|
+
"href": {
|
|
49
|
+
"type": "url",
|
|
50
|
+
"required": true
|
|
51
|
+
},
|
|
52
|
+
"current": {
|
|
53
|
+
"type": "boolean"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"description": "The languages: BCP 47 code, the language's own name, the page's address in it, and whether it is the page being read."
|
|
58
|
+
},
|
|
59
|
+
"label": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Accessible name (ui-strings language.label)."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"slots": {},
|
|
65
|
+
"js": "none",
|
|
66
|
+
"dependencies": {},
|
|
67
|
+
"sources": {
|
|
68
|
+
"gutenberg": [
|
|
69
|
+
"polylang/language-switcher"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"a11y": "List layout: a labelled <nav>; menu layout: a native <details> whose summary carries the label for assistive technology and the current language visibly. Every link has lang and hreflang, so screen readers pronounce each name in its language; the current one is aria-current=true. Fewer than two languages draw nothing."
|
|
73
|
+
}
|
|
@@ -8,6 +8,7 @@ import Section, { type Tone } from '../_shared/Section.astro'
|
|
|
8
8
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
9
9
|
import { measuredGrid } from '../_shared/measured'
|
|
10
10
|
import type { MeasuredStyle } from '../_shared/types'
|
|
11
|
+
import { safeHref } from '../_shared/safe-url'
|
|
11
12
|
|
|
12
13
|
const cloud = tv({
|
|
13
14
|
slots: {
|
|
@@ -101,7 +102,7 @@ const duration = `${Math.max(20, shown.length * 4)}s`
|
|
|
101
102
|
{shown.map(({ image, href }) => (
|
|
102
103
|
<li class={s.item()}>
|
|
103
104
|
{href
|
|
104
|
-
? <a href={href} class="group rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4"><KitImage image={image} sizes="160px" class={s.logo()} /></a>
|
|
105
|
+
? <a href={safeHref(href)} class="group rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4"><KitImage image={image} sizes="160px" class={s.logo()} /></a>
|
|
105
106
|
: <span class="group"><KitImage image={image} sizes="160px" class={s.logo()} /></span>}
|
|
106
107
|
</li>
|
|
107
108
|
))}
|
package/components/nav/Nav.astro
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
// Site navigation. Wide screens get an inline list whose submenus open on
|
|
3
3
|
// hover and keyboard focus (a chevron button marks the ones that have one); narrow screens get a toggle that opens the menu as
|
|
4
|
-
// a native popover (drawer) or a <details> dropdown.
|
|
4
|
+
// a native popover (drawer) or a <details> dropdown. Submenus nest to any depth; `menu="mega"` draws them as a wide panel of
|
|
5
|
+
// columns. Everything works without JavaScript; a small script (only when a menu has submenus) is the disclosure pattern's
|
|
6
|
+
// last step: it keeps aria-expanded true to what is open and lets Escape close a panel and return to its button.
|
|
5
7
|
import { tv } from 'tailwind-variants'
|
|
8
|
+
import NavTree from './NavTree.astro'
|
|
6
9
|
import type { NavItem } from '../_shared/types'
|
|
10
|
+
import { safeHref } from '../_shared/safe-url'
|
|
7
11
|
|
|
8
12
|
const nav = tv({
|
|
9
13
|
slots: {
|
|
10
14
|
list: 'hidden items-center md:flex',
|
|
11
15
|
link: 'no-underline aria-[current=page]:font-semibold',
|
|
12
16
|
// hidden, not invisible: a closed submenu takes no space, so one under the last item cannot widen the page.
|
|
13
|
-
sub: 'absolute top-full z-30 hidden min-w-48 rounded-lg border border-line bg-surface p-2 text-ink shadow-lg group-hover:block group-focus-within:block',
|
|
17
|
+
sub: 'absolute top-full z-30 hidden min-w-48 rounded-lg border border-line bg-surface p-2 text-ink shadow-lg group-hover:block group-focus-within:block group-data-[closed]:hidden!',
|
|
14
18
|
toggle: 'inline-flex cursor-pointer list-none items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-current/10 md:hidden [&::-webkit-details-marker]:hidden',
|
|
15
19
|
},
|
|
16
20
|
variants: {
|
|
17
21
|
// Submenus open towards the page: leftwards under an end-aligned menu.
|
|
18
22
|
align: { start: { list: 'justify-start', sub: 'start-0' }, end: { list: 'justify-end', sub: 'end-0' }, center: { list: 'justify-center', sub: 'start-0' } },
|
|
23
|
+
// flyout: a list under the item. mega: a wide panel, one column per child with the grandchildren listed under it.
|
|
24
|
+
menu: { flyout: {}, mega: { sub: 'w-max max-w-[min(64rem,90vw)] p-5' } },
|
|
19
25
|
density: { compact: { list: 'gap-5 text-sm' }, comfortable: { list: 'gap-8 text-base' } },
|
|
20
26
|
// current: the surrounding text, dimmed until hovered. link: the site's link colour at full strength, as themes that
|
|
21
27
|
// colour every link draw their menu; the toggle keeps the surrounding text.
|
|
@@ -24,7 +30,7 @@ const nav = tv({
|
|
|
24
30
|
link: { link: 'text-[color:var(--color-link,var(--color-accent))]' },
|
|
25
31
|
},
|
|
26
32
|
},
|
|
27
|
-
defaultVariants: { align: 'end', density: 'compact', color: 'current' },
|
|
33
|
+
defaultVariants: { menu: 'flyout', align: 'end', density: 'compact', color: 'current' },
|
|
28
34
|
})
|
|
29
35
|
|
|
30
36
|
interface Props {
|
|
@@ -36,6 +42,7 @@ interface Props {
|
|
|
36
42
|
/** Text on the drawer's close button ("Close"). */
|
|
37
43
|
closeLabel?: string | undefined
|
|
38
44
|
mobile?: 'popover' | 'details' | undefined
|
|
45
|
+
menu?: 'flyout' | 'mega' | undefined
|
|
39
46
|
align?: 'start' | 'end' | 'center' | undefined
|
|
40
47
|
density?: 'compact' | 'comfortable' | undefined
|
|
41
48
|
color?: 'current' | 'link' | undefined
|
|
@@ -45,28 +52,40 @@ interface Props {
|
|
|
45
52
|
part?: string | undefined
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', align, density, color, id = 'site-nav', part } = Astro.props
|
|
49
|
-
const { list, link, sub, toggle } = nav({ align, density, color })
|
|
55
|
+
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', menu, align, density, color, id = 'site-nav', part } = Astro.props
|
|
56
|
+
const { list, link, sub, toggle } = nav({ menu, align, density, color })
|
|
50
57
|
const current = Astro.url.pathname
|
|
51
58
|
const drawer = `${id}-drawer`
|
|
59
|
+
const hasSub = items.some(item => (item.children?.length ?? 0) > 0)
|
|
60
|
+
// An empty href or "#" is a group label (a WordPress custom link), not a link.
|
|
61
|
+
const real = (href: string | undefined) => !!href && href !== '#'
|
|
52
62
|
---
|
|
53
63
|
{items.length > 0 && (
|
|
54
64
|
<nav aria-label={label} data-cr-part={part} class="relative">
|
|
55
65
|
<ul class={list()}>
|
|
56
|
-
{items.map(item => (
|
|
66
|
+
{items.map((item, index) => (
|
|
57
67
|
<li class="group relative">
|
|
58
|
-
|
|
68
|
+
{real(item.href)
|
|
69
|
+
? <a href={safeHref(item.href)} aria-current={item.href === current ? 'page' : undefined} target={item.newTab ? '_blank' : undefined} rel={item.newTab ? 'noopener' : undefined} class={link()}>{item.label}</a>
|
|
70
|
+
: !(item.children && item.children.length > 0) && <span class={link()}>{item.label}</span>}
|
|
59
71
|
{item.children && item.children.length > 0 && (
|
|
60
72
|
<>
|
|
61
|
-
{/* The
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
{/* The button is the submenu's handle: focusing it (a tap on a touch screen, Tab on a keyboard) opens the submenu, with no script. With no real link of its own the item is the button, label and all. */}
|
|
74
|
+
{real(item.href)
|
|
75
|
+
? (
|
|
76
|
+
<button type="button" data-kit-sub aria-controls={`${id}-sub-${index}`} aria-label={item.label} class="ms-1 inline-flex cursor-pointer items-center align-middle opacity-80 hover:opacity-100">
|
|
77
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
|
78
|
+
</button>
|
|
79
|
+
)
|
|
80
|
+
: (
|
|
81
|
+
<button type="button" data-kit-sub aria-controls={`${id}-sub-${index}`} class={`${link()} inline-flex cursor-pointer items-center gap-1`}>
|
|
82
|
+
{item.label}
|
|
83
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
|
84
|
+
</button>
|
|
85
|
+
)}
|
|
86
|
+
<div id={`${id}-sub-${index}`} class={sub()}>
|
|
87
|
+
<NavTree items={item.children} current={current} kind={menu === 'mega' ? 'mega' : 'flyout'} />
|
|
88
|
+
</div>
|
|
70
89
|
</>
|
|
71
90
|
)}
|
|
72
91
|
</li>
|
|
@@ -88,12 +107,10 @@ const drawer = `${id}-drawer`
|
|
|
88
107
|
<ul class="flex flex-col gap-1">
|
|
89
108
|
{items.map(item => (
|
|
90
109
|
<li>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
</ul>
|
|
96
|
-
)}
|
|
110
|
+
{real(item.href)
|
|
111
|
+
? <a href={safeHref(item.href)} aria-current={item.href === current ? 'page' : undefined} class="block rounded-md px-3 py-2 text-ink no-underline hover:bg-surface-muted aria-[current=page]:font-semibold">{item.label}</a>
|
|
112
|
+
: <span class="block px-3 py-2 font-semibold text-ink">{item.label}</span>}
|
|
113
|
+
{item.children && item.children.length > 0 && <NavTree items={item.children} current={current} kind="drawer" />}
|
|
97
114
|
</li>
|
|
98
115
|
))}
|
|
99
116
|
</ul>
|
|
@@ -107,11 +124,47 @@ const drawer = `${id}-drawer`
|
|
|
107
124
|
{menuLabel}
|
|
108
125
|
</summary>
|
|
109
126
|
<ul class="absolute end-0 z-30 mt-2 flex w-64 flex-col gap-1 rounded-lg border border-line bg-surface p-3 text-ink shadow-lg">
|
|
110
|
-
{items.
|
|
111
|
-
<li
|
|
127
|
+
{items.map(item => (
|
|
128
|
+
<li>
|
|
129
|
+
{real(item.href)
|
|
130
|
+
? <a href={safeHref(item.href)} aria-current={item.href === current ? 'page' : undefined} class="block rounded-md px-3 py-2 text-sm text-ink no-underline hover:bg-surface-muted">{item.label}</a>
|
|
131
|
+
: <span class="block px-3 py-2 text-sm font-semibold text-ink">{item.label}</span>}
|
|
132
|
+
{item.children && item.children.length > 0 && <NavTree items={item.children} current={current} kind="drawer" />}
|
|
133
|
+
</li>
|
|
112
134
|
))}
|
|
113
135
|
</ul>
|
|
114
136
|
</details>
|
|
115
137
|
)}
|
|
116
138
|
</nav>
|
|
117
139
|
)}
|
|
140
|
+
{hasSub && (
|
|
141
|
+
<script>
|
|
142
|
+
// Disclosure navigation, last step: aria-expanded follows what is open (hover, focus or a click), Escape closes the
|
|
143
|
+
// open panel and returns to its button. Without this script the panels still open on hover and focus.
|
|
144
|
+
for (const nav of document.querySelectorAll<HTMLElement>('nav')) {
|
|
145
|
+
for (const button of nav.querySelectorAll<HTMLButtonElement>('button[data-kit-sub]')) {
|
|
146
|
+
const item = button.closest<HTMLElement>('li')
|
|
147
|
+
const panel = item?.querySelector<HTMLElement>(`#${CSS.escape(button.getAttribute('aria-controls') ?? '')}`)
|
|
148
|
+
if (!item || !panel) continue
|
|
149
|
+
const sync = () => button.setAttribute('aria-expanded', String(!item.hasAttribute('data-closed') && getComputedStyle(panel).display !== 'none'))
|
|
150
|
+
sync()
|
|
151
|
+
item.addEventListener('mouseenter', () => { item.removeAttribute('data-closed'); queueMicrotask(sync) })
|
|
152
|
+
item.addEventListener('mouseleave', () => queueMicrotask(sync))
|
|
153
|
+
item.addEventListener('focusin', () => { item.removeAttribute('data-closed'); queueMicrotask(sync) })
|
|
154
|
+
item.addEventListener('focusout', () => queueMicrotask(sync))
|
|
155
|
+
item.addEventListener('keydown', (event) => {
|
|
156
|
+
if (event.key !== 'Escape' || item.hasAttribute('data-closed') || getComputedStyle(panel).display === 'none') return
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
item.setAttribute('data-closed', '')
|
|
159
|
+
if (panel.contains(document.activeElement)) button.focus()
|
|
160
|
+
sync()
|
|
161
|
+
})
|
|
162
|
+
button.addEventListener('click', () => {
|
|
163
|
+
if (item.hasAttribute('data-closed')) item.removeAttribute('data-closed')
|
|
164
|
+
else if (getComputedStyle(panel).display !== 'none') item.setAttribute('data-closed', '')
|
|
165
|
+
sync()
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
)}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The submenu levels of Nav, rendered recursively so a menu of any depth keeps every level. An entry with no real link
|
|
3
|
+
// (an empty href or "#": a WordPress "custom link" used as a group heading) is a label, not `<a href="#">`.
|
|
4
|
+
import NavTree from './NavTree.astro'
|
|
5
|
+
import type { NavItem } from '../_shared/types'
|
|
6
|
+
import { safeHref } from '../_shared/safe-url'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: NavItem[]
|
|
10
|
+
/** The current page's path, for aria-current. */
|
|
11
|
+
current?: string | undefined
|
|
12
|
+
/** flyout: a list under the item. drawer: the narrow-screen menu. mega: the columns of a wide panel. */
|
|
13
|
+
kind: 'flyout' | 'drawer' | 'mega'
|
|
14
|
+
/** 2 for the first level under a top item, 3 for the next. */
|
|
15
|
+
level?: number | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { items, current, kind, level = 2 } = Astro.props
|
|
19
|
+
const real = (href: string | undefined) => !!href && href !== '#'
|
|
20
|
+
// Column headings in a mega panel; deeper rows (and every drawer row below the top) are quieter.
|
|
21
|
+
const heading = kind === 'mega' && level === 2
|
|
22
|
+
const row = heading
|
|
23
|
+
? 'block px-3 py-2 text-sm font-semibold text-ink'
|
|
24
|
+
: level === 2 && kind === 'flyout'
|
|
25
|
+
? 'block px-3 py-2 text-sm text-ink'
|
|
26
|
+
: 'block px-3 py-1.5 text-sm text-ink-muted'
|
|
27
|
+
const hover = 'rounded-md no-underline hover:bg-surface-muted hover:text-ink'
|
|
28
|
+
// Nested lists are indented, except the first list of a flyout or mega panel, which fills the panel.
|
|
29
|
+
const nested = kind === 'drawer' || level > 2 ? 'ms-3 border-s border-line ps-3' : ''
|
|
30
|
+
---
|
|
31
|
+
<ul class={heading ? 'grid grid-flow-col auto-cols-fr gap-x-8 gap-y-4' : nested}>
|
|
32
|
+
{items.map(item => (
|
|
33
|
+
<li>
|
|
34
|
+
{real(item.href)
|
|
35
|
+
? <a href={safeHref(item.href)} aria-current={item.href === current ? 'page' : undefined} target={item.newTab ? '_blank' : undefined} rel={item.newTab ? 'noopener' : undefined} class={`${row} ${hover}`}>{item.label}</a>
|
|
36
|
+
: <span class={`${row} font-semibold`}>{item.label}</span>}
|
|
37
|
+
{item.children && item.children.length > 0 && <NavTree items={item.children} current={current} kind={kind === 'mega' ? 'flyout' : kind} level={level + 1} />}
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|