@contentrain/astro-kit 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 +89 -0
- package/catalog.json +1780 -0
- package/components/_shared/Button.astro +37 -0
- package/components/_shared/KitImage.astro +21 -0
- package/components/_shared/Section.astro +43 -0
- package/components/_shared/types.ts +26 -0
- package/components/breadcrumb/Breadcrumb.astro +56 -0
- package/components/breadcrumb/fixtures.json +5 -0
- package/components/breadcrumb/meta.json +27 -0
- package/components/card-grid/CardGrid.astro +82 -0
- package/components/card-grid/fixtures.json +6 -0
- package/components/card-grid/meta.json +123 -0
- package/components/contact-form/ContactForm.astro +79 -0
- package/components/contact-form/fixtures.json +18 -0
- package/components/contact-form/meta.json +28 -0
- package/components/cta/Cta.astro +64 -0
- package/components/cta/fixtures.json +6 -0
- package/components/cta/meta.json +28 -0
- package/components/faq/Faq.astro +79 -0
- package/components/faq/fixtures.json +36 -0
- package/components/faq/meta.json +30 -0
- package/components/footer/Footer.astro +87 -0
- package/components/footer/fixtures.json +5 -0
- package/components/footer/meta.json +31 -0
- package/components/gallery/Gallery.astro +88 -0
- package/components/gallery/fixtures.json +191 -0
- package/components/gallery/meta.json +30 -0
- package/components/header/Header.astro +62 -0
- package/components/header/fixtures.json +125 -0
- package/components/header/meta.json +34 -0
- package/components/hero/Hero.astro +84 -0
- package/components/hero/fixtures.json +95 -0
- package/components/hero/meta.json +127 -0
- package/components/nav/Nav.astro +102 -0
- package/components/nav/fixtures.json +80 -0
- package/components/nav/meta.json +30 -0
- package/components/pagination/Pagination.astro +69 -0
- package/components/pagination/fixtures.json +6 -0
- package/components/pagination/meta.json +31 -0
- package/components/post-card/PostCard.astro +67 -0
- package/components/post-card/fixtures.json +54 -0
- package/components/post-card/meta.json +32 -0
- package/components/prose/Prose.astro +30 -0
- package/components/prose/fixtures.json +4 -0
- package/components/prose/meta.json +69 -0
- package/components/slider/Slider.astro +136 -0
- package/components/slider/fixtures.json +5 -0
- package/components/slider/meta.json +32 -0
- package/components/tabs/Tabs.astro +101 -0
- package/components/tabs/fixtures.json +37 -0
- package/components/tabs/meta.json +28 -0
- package/components/testimonial/Testimonial.astro +70 -0
- package/components/testimonial/fixtures.json +26 -0
- package/components/testimonial/meta.json +97 -0
- package/dist/index.d.mts +150 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +199 -0
- package/dist/index.mjs.map +1 -0
- package/mapping/classic.json +261 -0
- package/mapping/divi.json +295 -0
- package/mapping/elementor.json +322 -0
- package/mapping/gutenberg.json +395 -0
- package/package.json +63 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Site header: brand (logo and/or name), navigation and an optional call to
|
|
3
|
+
// action.
|
|
4
|
+
import { tv } from 'tailwind-variants'
|
|
5
|
+
import Button from '../_shared/Button.astro'
|
|
6
|
+
import KitImage from '../_shared/KitImage.astro'
|
|
7
|
+
import type { ActionInput, ImageInput, NavItem } from '../_shared/types'
|
|
8
|
+
import Nav from '../nav/Nav.astro'
|
|
9
|
+
|
|
10
|
+
const header = tv({
|
|
11
|
+
slots: {
|
|
12
|
+
root: 'z-40 w-full',
|
|
13
|
+
inner: 'container-page flex items-center gap-6 py-4',
|
|
14
|
+
brand: 'flex shrink-0 items-center gap-3 text-lg font-semibold tracking-tight text-current no-underline',
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
layout: {
|
|
18
|
+
split: { inner: 'justify-between' },
|
|
19
|
+
centered: { inner: 'flex-col gap-3 md:gap-4' },
|
|
20
|
+
'brand-left': { inner: 'justify-start [&>nav]:ml-10' },
|
|
21
|
+
},
|
|
22
|
+
tone: {
|
|
23
|
+
default: { root: 'border-b border-line bg-surface text-ink' },
|
|
24
|
+
inverse: { root: 'bg-ink text-surface' },
|
|
25
|
+
transparent: { root: 'absolute inset-x-0 top-0 bg-transparent text-surface' },
|
|
26
|
+
},
|
|
27
|
+
position: { static: {}, sticky: { root: 'sticky top-0' } },
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { layout: 'split', tone: 'default', position: 'static' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
siteName: string
|
|
34
|
+
logo?: ImageInput | undefined
|
|
35
|
+
/** Show the site name next to the logo. */
|
|
36
|
+
showName?: boolean | undefined
|
|
37
|
+
homeHref?: string | undefined
|
|
38
|
+
items: NavItem[]
|
|
39
|
+
action?: ActionInput | undefined
|
|
40
|
+
navLabel?: string | undefined
|
|
41
|
+
menuLabel?: string | undefined
|
|
42
|
+
closeLabel?: string | undefined
|
|
43
|
+
layout?: 'split' | 'centered' | 'brand-left' | undefined
|
|
44
|
+
tone?: 'default' | 'inverse' | 'transparent' | undefined
|
|
45
|
+
position?: 'static' | 'sticky' | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { siteName, logo, showName = true, homeHref = '/', items, action, navLabel, menuLabel, closeLabel, layout, tone, position } = Astro.props
|
|
49
|
+
const { root, inner, brand } = header({ layout, tone, position })
|
|
50
|
+
---
|
|
51
|
+
<header class={root()}>
|
|
52
|
+
<div class={inner()}>
|
|
53
|
+
<a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName}>
|
|
54
|
+
{logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
|
|
55
|
+
{showName && <span>{siteName}</span>}
|
|
56
|
+
</a>
|
|
57
|
+
<div class:list={['flex items-center gap-4', layout === 'centered' ? 'justify-center' : 'ml-auto']}>
|
|
58
|
+
<Nav items={items} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} align={layout === 'centered' ? 'center' : 'end'} />
|
|
59
|
+
{action && <Button action={action} size="sm" class="hidden md:inline-flex" />}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "split",
|
|
4
|
+
"props": {
|
|
5
|
+
"siteName": "Northwind Journal",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"label": "Journal",
|
|
9
|
+
"href": "/"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "About",
|
|
13
|
+
"href": "/about/"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"label": "Contact",
|
|
17
|
+
"href": "/contact/"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"action": {
|
|
21
|
+
"label": "Subscribe",
|
|
22
|
+
"href": "/subscribe/"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "centered-inverse",
|
|
28
|
+
"props": {
|
|
29
|
+
"siteName": "Northwind Journal",
|
|
30
|
+
"layout": "centered",
|
|
31
|
+
"tone": "inverse",
|
|
32
|
+
"items": [
|
|
33
|
+
{
|
|
34
|
+
"label": "Journal",
|
|
35
|
+
"href": "/"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"label": "Guides",
|
|
39
|
+
"href": "/category/guides/"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"label": "About",
|
|
43
|
+
"href": "/about/"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "brand-left-logo",
|
|
50
|
+
"props": {
|
|
51
|
+
"siteName": "Northwind",
|
|
52
|
+
"layout": "brand-left",
|
|
53
|
+
"logo": {
|
|
54
|
+
"src": "/kit-fixtures/logo.svg",
|
|
55
|
+
"alt": "",
|
|
56
|
+
"width": 32,
|
|
57
|
+
"height": 32
|
|
58
|
+
},
|
|
59
|
+
"items": [
|
|
60
|
+
{
|
|
61
|
+
"label": "Work",
|
|
62
|
+
"href": "/work/"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"label": "Studio",
|
|
66
|
+
"href": "/studio/"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "transparent-sticky",
|
|
73
|
+
"frame": "dark",
|
|
74
|
+
"props": {
|
|
75
|
+
"siteName": "Northwind Journal",
|
|
76
|
+
"tone": "transparent",
|
|
77
|
+
"position": "sticky",
|
|
78
|
+
"items": [
|
|
79
|
+
{
|
|
80
|
+
"label": "Journal",
|
|
81
|
+
"href": "/"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"label": "About",
|
|
85
|
+
"href": "/about/"
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"action": {
|
|
89
|
+
"label": "Get in touch",
|
|
90
|
+
"href": "/contact/",
|
|
91
|
+
"style": "ghost"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "split-submenu-last",
|
|
97
|
+
"props": {
|
|
98
|
+
"siteName": "Northwind",
|
|
99
|
+
"items": [
|
|
100
|
+
{
|
|
101
|
+
"label": "Journal",
|
|
102
|
+
"href": "/"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"label": "About",
|
|
106
|
+
"href": "/about/"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"label": "Services",
|
|
110
|
+
"href": "/services/",
|
|
111
|
+
"children": [
|
|
112
|
+
{
|
|
113
|
+
"label": "Design systems",
|
|
114
|
+
"href": "/services/design/"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"label": "Migrations",
|
|
118
|
+
"href": "/services/migrations/"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "header",
|
|
3
|
+
"name": "Header",
|
|
4
|
+
"description": "Site header: brand (logo and/or name), navigation and an optional call-to-action button.",
|
|
5
|
+
"category": "chrome",
|
|
6
|
+
"uses": ["nav"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "types.ts"],
|
|
8
|
+
"variants": {
|
|
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
|
+
"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." }
|
|
12
|
+
},
|
|
13
|
+
"props": {
|
|
14
|
+
"siteName": { "type": "string", "required": true, "description": "Site title (site.title)." },
|
|
15
|
+
"logo": { "type": "object", "fields": { "src": { "type": "image", "required": true }, "alt": { "type": "string", "required": true }, "width": { "type": "integer" }, "height": { "type": "integer" } }, "description": "Logo image (site.logo)." },
|
|
16
|
+
"showName": { "type": "boolean", "default": true, "description": "Show the site name next to the logo." },
|
|
17
|
+
"homeHref": { "type": "url", "default": "/", "description": "Where the brand links." },
|
|
18
|
+
"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)." },
|
|
19
|
+
"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." },
|
|
20
|
+
"navLabel": { "type": "string", "description": "Accessible name of the navigation (ui-strings nav.label)." },
|
|
21
|
+
"menuLabel": { "type": "string", "description": "Narrow-screen toggle text (ui-strings nav.menu)." },
|
|
22
|
+
"closeLabel": { "type": "string", "description": "Drawer close text (ui-strings nav.close)." }
|
|
23
|
+
},
|
|
24
|
+
"slots": {},
|
|
25
|
+
"js": "none",
|
|
26
|
+
"dependencies": {},
|
|
27
|
+
"sources": {
|
|
28
|
+
"gutenberg": ["core/template-part:header", "core/site-title", "core/site-logo"],
|
|
29
|
+
"elementor": ["elementor/theme-site-logo", "elementor/theme-site-title"],
|
|
30
|
+
"divi": ["divi/et_pb_menu"],
|
|
31
|
+
"classic": ["classic/header"]
|
|
32
|
+
},
|
|
33
|
+
"a11y": "One <header> landmark; the brand link is named by the site name when only the logo shows. Navigation as in nav."
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The page's opening section: heading, lead, actions and an image — beside the
|
|
3
|
+
// text, below it, or behind it as a cover with the text over a scrim.
|
|
4
|
+
import { tv } from 'tailwind-variants'
|
|
5
|
+
import Button from '../_shared/Button.astro'
|
|
6
|
+
import KitImage from '../_shared/KitImage.astro'
|
|
7
|
+
import Section from '../_shared/Section.astro'
|
|
8
|
+
import type { ActionInput, ImageInput } from '../_shared/types'
|
|
9
|
+
|
|
10
|
+
const hero = tv({
|
|
11
|
+
slots: {
|
|
12
|
+
grid: 'grid items-center gap-10',
|
|
13
|
+
copy: 'flex flex-col gap-5',
|
|
14
|
+
eyebrow: 'text-sm font-semibold tracking-wide uppercase opacity-80',
|
|
15
|
+
heading: 'text-4xl font-bold tracking-tight text-balance md:text-5xl lg:text-6xl',
|
|
16
|
+
lead: 'max-w-2xl text-lg text-pretty opacity-85 md:text-xl',
|
|
17
|
+
actions: 'flex flex-wrap gap-3 pt-2',
|
|
18
|
+
media: 'overflow-hidden rounded-card',
|
|
19
|
+
},
|
|
20
|
+
variants: {
|
|
21
|
+
layout: {
|
|
22
|
+
split: { grid: 'md:grid-cols-2' },
|
|
23
|
+
centered: { copy: 'items-center text-center', lead: 'mx-auto', actions: 'justify-center', media: 'mx-auto w-full max-w-4xl' },
|
|
24
|
+
cover: { grid: 'relative z-10 min-h-[28rem] md:min-h-[36rem]', copy: 'max-w-3xl' },
|
|
25
|
+
},
|
|
26
|
+
// accent/accent-ink is only ~5:1 — dimming text on it drops body copy under 4.5:1 (WCAG 1.4.3), so accent keeps it at full strength.
|
|
27
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'opacity-100', lead: 'opacity-100' } },
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { layout: 'split', tone: 'default' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
heading: string
|
|
34
|
+
/** Heading level: 1 when the hero opens the page (the default), 2 inside a page that already has one. */
|
|
35
|
+
level?: 1 | 2 | undefined
|
|
36
|
+
eyebrow?: string | undefined
|
|
37
|
+
lead?: string | undefined
|
|
38
|
+
actions?: ActionInput[] | undefined
|
|
39
|
+
image?: ImageInput | undefined
|
|
40
|
+
layout?: 'split' | 'centered' | 'cover' | undefined
|
|
41
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
42
|
+
id?: string | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', tone, id } = Astro.props
|
|
46
|
+
const s = hero({ layout, tone })
|
|
47
|
+
const Heading = level === 1 ? 'h1' : 'h2'
|
|
48
|
+
const headingId = id ? `${id}-title` : undefined
|
|
49
|
+
const cover = layout === 'cover' && image
|
|
50
|
+
---
|
|
51
|
+
{cover
|
|
52
|
+
? (
|
|
53
|
+
<Section tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
|
|
54
|
+
<KitImage image={image} priority sizes="100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
55
|
+
<div class="absolute inset-0 -z-10 bg-ink/60" aria-hidden="true" />
|
|
56
|
+
<div class="container-page py-16 md:py-28">
|
|
57
|
+
<div class={s.grid()}>
|
|
58
|
+
<div class={s.copy()}>
|
|
59
|
+
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
60
|
+
<Heading id={headingId} class={s.heading()}>{heading}</Heading>
|
|
61
|
+
{lead && <p class={s.lead()}>{lead}</p>}
|
|
62
|
+
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</Section>
|
|
67
|
+
)
|
|
68
|
+
: (
|
|
69
|
+
<Section tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
|
|
70
|
+
<div class={s.grid()}>
|
|
71
|
+
<div class={s.copy()}>
|
|
72
|
+
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
73
|
+
<Heading id={headingId} class={s.heading()}>{heading}</Heading>
|
|
74
|
+
{lead && <p class={s.lead()}>{lead}</p>}
|
|
75
|
+
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
76
|
+
</div>
|
|
77
|
+
{image && (
|
|
78
|
+
<div class={s.media()}>
|
|
79
|
+
<KitImage image={image} priority sizes={layout === 'split' ? '(min-width: 768px) 50vw, 100vw' : '(min-width: 1024px) 56rem, 100vw'} class="h-auto w-full object-cover" />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</Section>
|
|
84
|
+
)}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "split",
|
|
4
|
+
"props": {
|
|
5
|
+
"eyebrow": "Northwind Journal",
|
|
6
|
+
"heading": "Field notes on slow travel and good coffee",
|
|
7
|
+
"lead": "Essays, guides and photographs from twelve years on the road — now in one place, and easier to read.",
|
|
8
|
+
"actions": [
|
|
9
|
+
{
|
|
10
|
+
"label": "Start reading",
|
|
11
|
+
"href": "/journal/"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"label": "About the journal",
|
|
15
|
+
"href": "/about/",
|
|
16
|
+
"style": "ghost"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"image": {
|
|
20
|
+
"src": "/kit-fixtures/photo-1.svg",
|
|
21
|
+
"alt": "A café terrace in the morning",
|
|
22
|
+
"width": 1200,
|
|
23
|
+
"height": 800
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "centered-muted",
|
|
29
|
+
"props": {
|
|
30
|
+
"heading": "Plan a calmer trip",
|
|
31
|
+
"lead": "Our guides pick fewer places and give each one more time.",
|
|
32
|
+
"layout": "centered",
|
|
33
|
+
"tone": "muted",
|
|
34
|
+
"level": 2,
|
|
35
|
+
"actions": [
|
|
36
|
+
{
|
|
37
|
+
"label": "Browse guides",
|
|
38
|
+
"href": "/category/guides/"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"image": {
|
|
42
|
+
"src": "/kit-fixtures/photo-2.svg",
|
|
43
|
+
"alt": "",
|
|
44
|
+
"width": 1600,
|
|
45
|
+
"height": 700
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "cover",
|
|
51
|
+
"props": {
|
|
52
|
+
"heading": "The long way round",
|
|
53
|
+
"lead": "A year of trains across Europe, one station at a time.",
|
|
54
|
+
"layout": "cover",
|
|
55
|
+
"actions": [
|
|
56
|
+
{
|
|
57
|
+
"label": "Read the series",
|
|
58
|
+
"href": "/series/long-way/"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"image": {
|
|
62
|
+
"src": "/kit-fixtures/photo-3.svg",
|
|
63
|
+
"alt": "",
|
|
64
|
+
"width": 1600,
|
|
65
|
+
"height": 900
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "inverse-text-only",
|
|
71
|
+
"props": {
|
|
72
|
+
"heading": "Contact",
|
|
73
|
+
"lead": "Questions, corrections and invitations are all welcome.",
|
|
74
|
+
"tone": "inverse",
|
|
75
|
+
"level": 2
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "accent",
|
|
80
|
+
"props": {
|
|
81
|
+
"heading": "Spring issue is out",
|
|
82
|
+
"tone": "accent",
|
|
83
|
+
"level": 2,
|
|
84
|
+
"actions": [
|
|
85
|
+
{
|
|
86
|
+
"label": "Read it",
|
|
87
|
+
"href": "/issues/spring/",
|
|
88
|
+
"style": "secondary"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"lead": "Twelve new guides, and the long read on night trains.",
|
|
92
|
+
"eyebrow": "New"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hero",
|
|
3
|
+
"name": "Hero",
|
|
4
|
+
"description": "The page's opening section: optional eyebrow, heading, lead, action buttons and an image — beside the text, below it, or behind it as a full-bleed cover with a scrim.",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": [
|
|
8
|
+
"Button.astro",
|
|
9
|
+
"KitImage.astro",
|
|
10
|
+
"Section.astro",
|
|
11
|
+
"types.ts"
|
|
12
|
+
],
|
|
13
|
+
"variants": {
|
|
14
|
+
"layout": {
|
|
15
|
+
"options": [
|
|
16
|
+
"split",
|
|
17
|
+
"centered",
|
|
18
|
+
"cover"
|
|
19
|
+
],
|
|
20
|
+
"default": "split",
|
|
21
|
+
"description": "split: text and image side by side (stacked on phones). centered: centred text with the image below. cover: the image fills the section behind the text, darkened by a scrim; without an image it falls back to split."
|
|
22
|
+
},
|
|
23
|
+
"tone": {
|
|
24
|
+
"options": [
|
|
25
|
+
"default",
|
|
26
|
+
"muted",
|
|
27
|
+
"inverse",
|
|
28
|
+
"accent"
|
|
29
|
+
],
|
|
30
|
+
"default": "default",
|
|
31
|
+
"description": "Section background (ignored by cover, which is always light text on the image)."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"props": {
|
|
35
|
+
"heading": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"required": true,
|
|
38
|
+
"description": "The main heading (Cover/Group heading, Elementor heading, Divi header title)."
|
|
39
|
+
},
|
|
40
|
+
"level": {
|
|
41
|
+
"type": "select",
|
|
42
|
+
"options": [
|
|
43
|
+
"1",
|
|
44
|
+
"2"
|
|
45
|
+
],
|
|
46
|
+
"default": "1",
|
|
47
|
+
"description": "Heading level: 1 when the hero opens the page, 2 when the page already has an h1."
|
|
48
|
+
},
|
|
49
|
+
"eyebrow": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Short line above the heading."
|
|
52
|
+
},
|
|
53
|
+
"lead": {
|
|
54
|
+
"type": "text",
|
|
55
|
+
"description": "Paragraph under the heading."
|
|
56
|
+
},
|
|
57
|
+
"actions": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"fields": {
|
|
62
|
+
"label": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"required": true
|
|
65
|
+
},
|
|
66
|
+
"href": {
|
|
67
|
+
"type": "url",
|
|
68
|
+
"required": true
|
|
69
|
+
},
|
|
70
|
+
"newTab": {
|
|
71
|
+
"type": "boolean"
|
|
72
|
+
},
|
|
73
|
+
"style": {
|
|
74
|
+
"type": "select",
|
|
75
|
+
"options": [
|
|
76
|
+
"primary",
|
|
77
|
+
"secondary",
|
|
78
|
+
"ghost"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"description": "Buttons, in order."
|
|
84
|
+
},
|
|
85
|
+
"image": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"fields": {
|
|
88
|
+
"src": {
|
|
89
|
+
"type": "image",
|
|
90
|
+
"required": true
|
|
91
|
+
},
|
|
92
|
+
"alt": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"required": true
|
|
95
|
+
},
|
|
96
|
+
"width": {
|
|
97
|
+
"type": "integer"
|
|
98
|
+
},
|
|
99
|
+
"height": {
|
|
100
|
+
"type": "integer"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"description": "Hero image; loaded first (the page's largest image)."
|
|
104
|
+
},
|
|
105
|
+
"id": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"slots": {},
|
|
111
|
+
"js": "none",
|
|
112
|
+
"dependencies": {},
|
|
113
|
+
"sources": {
|
|
114
|
+
"gutenberg": [
|
|
115
|
+
"core/cover",
|
|
116
|
+
"core/media-text"
|
|
117
|
+
],
|
|
118
|
+
"divi": [
|
|
119
|
+
"divi/et_pb_fullwidth_header",
|
|
120
|
+
"divi/et_pb_fullwidth_image"
|
|
121
|
+
],
|
|
122
|
+
"classic": [
|
|
123
|
+
"classic/hero"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"a11y": "A labelled region (aria-labelledby the heading). One h1 by default; `level: 2` inside pages that already have one. The cover image is decorative when its alt is empty; text sits on an ink scrim for contrast."
|
|
127
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Site navigation. Wide screens get an inline list whose submenus open on
|
|
3
|
+
// hover and keyboard focus; narrow screens get a toggle that opens the menu as
|
|
4
|
+
// a native popover (drawer) or a <details> dropdown. No JavaScript.
|
|
5
|
+
import { tv } from 'tailwind-variants'
|
|
6
|
+
import type { NavItem } from '../_shared/types'
|
|
7
|
+
|
|
8
|
+
const nav = tv({
|
|
9
|
+
slots: {
|
|
10
|
+
list: 'hidden items-center md:flex',
|
|
11
|
+
link: 'text-current no-underline opacity-80 hover:opacity-100 aria-[current=page]:font-semibold aria-[current=page]:opacity-100',
|
|
12
|
+
// 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',
|
|
14
|
+
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
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
// Submenus open towards the page: leftwards under an end-aligned menu.
|
|
18
|
+
align: { start: { list: 'justify-start', sub: 'left-0' }, end: { list: 'justify-end', sub: 'right-0' }, center: { list: 'justify-center', sub: 'left-0' } },
|
|
19
|
+
density: { compact: { list: 'gap-5 text-sm' }, comfortable: { list: 'gap-8 text-base' } },
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: { align: 'end', density: 'compact' },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
items: NavItem[]
|
|
26
|
+
/** Accessible name of the navigation landmark ("Main"). */
|
|
27
|
+
label?: string | undefined
|
|
28
|
+
/** Text on the narrow-screen toggle ("Menu"). */
|
|
29
|
+
menuLabel?: string | undefined
|
|
30
|
+
/** Text on the drawer's close button ("Close"). */
|
|
31
|
+
closeLabel?: string | undefined
|
|
32
|
+
mobile?: 'popover' | 'details' | undefined
|
|
33
|
+
align?: 'start' | 'end' | 'center' | undefined
|
|
34
|
+
density?: 'compact' | 'comfortable' | undefined
|
|
35
|
+
/** Unique per page when a page has two navigations. */
|
|
36
|
+
id?: string | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', align, density, id = 'site-nav' } = Astro.props
|
|
40
|
+
const { list, link, sub, toggle } = nav({ align, density })
|
|
41
|
+
const current = Astro.url.pathname
|
|
42
|
+
const drawer = `${id}-drawer`
|
|
43
|
+
---
|
|
44
|
+
{items.length > 0 && (
|
|
45
|
+
<nav aria-label={label} class="relative">
|
|
46
|
+
<ul class={list()}>
|
|
47
|
+
{items.map(item => (
|
|
48
|
+
<li class="group relative">
|
|
49
|
+
<a href={item.href} aria-current={item.href === current ? 'page' : undefined} target={item.newTab ? '_blank' : undefined} rel={item.newTab ? 'noopener' : undefined} class={link()}>{item.label}</a>
|
|
50
|
+
{item.children && item.children.length > 0 && (
|
|
51
|
+
<ul class={sub()}>
|
|
52
|
+
{item.children.map(child => (
|
|
53
|
+
<li><a href={child.href} aria-current={child.href === current ? 'page' : undefined} class="block rounded-md px-3 py-2 text-sm text-ink no-underline hover:bg-surface-muted">{child.label}</a></li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
)}
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
|
|
61
|
+
{mobile === 'popover'
|
|
62
|
+
? (
|
|
63
|
+
<>
|
|
64
|
+
<button type="button" popovertarget={drawer} class={toggle()}>
|
|
65
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
66
|
+
{menuLabel}
|
|
67
|
+
</button>
|
|
68
|
+
<div id={drawer} popover class="m-0 ml-auto h-dvh max-h-none w-80 max-w-[85vw] border-l border-line bg-surface p-6 text-ink shadow-xl backdrop:bg-ink/40">
|
|
69
|
+
<button type="button" popovertarget={drawer} popovertargetaction="hide" class="mb-6 ml-auto flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-surface-muted">
|
|
70
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
71
|
+
{closeLabel}
|
|
72
|
+
</button>
|
|
73
|
+
<ul class="flex flex-col gap-1">
|
|
74
|
+
{items.map(item => (
|
|
75
|
+
<li>
|
|
76
|
+
<a href={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>
|
|
77
|
+
{item.children && item.children.length > 0 && (
|
|
78
|
+
<ul class="ml-3 border-l border-line pl-3">
|
|
79
|
+
{item.children.map(child => <li><a href={child.href} class="block rounded-md px-3 py-1.5 text-sm text-ink-muted no-underline hover:text-ink">{child.label}</a></li>)}
|
|
80
|
+
</ul>
|
|
81
|
+
)}
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
: (
|
|
89
|
+
<details class="relative md:hidden">
|
|
90
|
+
<summary class={toggle()}>
|
|
91
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
92
|
+
{menuLabel}
|
|
93
|
+
</summary>
|
|
94
|
+
<ul class="absolute right-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">
|
|
95
|
+
{items.flatMap(item => [item, ...(item.children ?? [])]).map(item => (
|
|
96
|
+
<li><a href={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></li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
</details>
|
|
100
|
+
)}
|
|
101
|
+
</nav>
|
|
102
|
+
)}
|