@contentrain/astro-kit 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +20 -3
- package/catalog.json +243 -2
- package/components/_shared/Button.astro +1 -1
- package/components/breadcrumb/Breadcrumb.astro +1 -1
- package/components/card-grid/CardGrid.astro +3 -3
- package/components/contact-form/ContactForm.astro +1 -1
- package/components/cta/Cta.astro +1 -1
- package/components/embed/Embed.astro +104 -0
- package/components/embed/embed.ts +106 -0
- package/components/embed/fixtures.json +34 -0
- package/components/embed/meta.json +32 -0
- package/components/faq/Faq.astro +2 -2
- package/components/footer/Footer.astro +4 -4
- package/components/footer/meta.json +107 -14
- package/components/gallery/Gallery.astro +3 -3
- package/components/header/Header.astro +1 -1
- package/components/hero/Hero.astro +12 -5
- package/components/hero/fixtures.json +21 -0
- package/components/hero/meta.json +8 -0
- package/components/nav/Nav.astro +4 -2
- package/components/nav/meta.json +86 -13
- package/components/newsletter/Newsletter.astro +110 -0
- package/components/newsletter/fixtures.json +6 -0
- package/components/newsletter/meta.json +105 -0
- package/components/newsletter/newsletter.ts +73 -0
- package/components/pagination/Pagination.astro +1 -1
- package/components/post-card/PostCard.astro +2 -2
- package/components/slider/Slider.astro +3 -3
- package/components/tabs/Tabs.astro +2 -2
- package/components/testimonial/Testimonial.astro +1 -1
- package/dist/index.d.mts +11 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +19 -7
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +91 -1
- package/mapping/divi.json +22 -1
- package/mapping/elementor.json +46 -1
- package/mapping/gutenberg.json +65 -3
- package/package.json +11 -11
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "embed",
|
|
3
|
+
"name": "Embed",
|
|
4
|
+
"description": "A video, map or player from another site (YouTube, Vimeo, Google Maps, OpenStreetMap, Spotify, SoundCloud, Calendly, Typeform, Loom, Wistia), loaded only on click: until then a poster link to the provider's page, with no iframe, no third-party request and no cookie (YouTube through youtube-nocookie.com). Unknown providers are never framed; they stay a plain link.",
|
|
5
|
+
"category": "content",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": ["KitImage.astro", "types.ts"],
|
|
8
|
+
"variants": {
|
|
9
|
+
"aspect": { "options": ["auto", "video", "portrait", "square", "map"], "default": "auto", "description": "auto: 16:9 for videos, 9:16 for YouTube Shorts, 4:3 (16:9 wide) for maps, a fixed strip for audio players. The others force a ratio." },
|
|
10
|
+
"width": { "options": ["prose", "page"], "default": "prose", "description": "prose: as wide as the reading column. page: the full page frame." }
|
|
11
|
+
},
|
|
12
|
+
"props": {
|
|
13
|
+
"url": { "type": "url", "description": "A video page (youtube.com/watch, youtu.be, /shorts/, vimeo.com/<id>) or a known provider's embed address (google.com/maps/embed, open.spotify.com/embed, w.soundcloud.com/player, calendly.com, *.typeform.com/to, loom.com/embed, fast.wistia.net/embed)." },
|
|
14
|
+
"address": { "type": "string", "description": "A place or address shown on a Google map when there is no url (Elementor google_maps address, Divi map address)." },
|
|
15
|
+
"title": { "type": "string", "description": "What it is: the player's accessible name and the poster's text." },
|
|
16
|
+
"caption": { "type": "string", "description": "A line under the embed." },
|
|
17
|
+
"poster": { "type": "object", "fields": { "src": { "type": "image", "required": true }, "alt": { "type": "string", "required": true }, "width": { "type": "integer" }, "height": { "type": "integer" } }, "description": "Local poster image (an ImageInput; its alt is not used, the link names the embed). Without one the poster is a plain dark panel. Never a provider thumbnail URL: that would be a third-party request before the click." },
|
|
18
|
+
"playLabel": { "type": "string", "description": "Poster action for a video or audio player (ui-strings embed.play)." },
|
|
19
|
+
"showLabel": { "type": "string", "description": "Poster action for a map or widget (ui-strings embed.show)." },
|
|
20
|
+
"id": { "type": "string", "description": "Element id." }
|
|
21
|
+
},
|
|
22
|
+
"slots": {},
|
|
23
|
+
"js": "vanilla",
|
|
24
|
+
"dependencies": {},
|
|
25
|
+
"sources": {
|
|
26
|
+
"gutenberg": ["core/embed"],
|
|
27
|
+
"elementor": ["elementor/video", "elementor/google_maps"],
|
|
28
|
+
"divi": ["divi/et_pb_video", "divi/et_pb_map"],
|
|
29
|
+
"classic": ["classic/youtube", "classic/vimeo", "classic/google-maps"]
|
|
30
|
+
},
|
|
31
|
+
"a11y": "The poster is one link named '<action>: <title>' that opens the provider's page without script. With script, Enter or a click puts the iframe in its place with the title as its accessible name and moves focus into it. Modified clicks (new tab) keep the link behaviour. The play icon is decorative."
|
|
32
|
+
}
|
package/components/faq/Faq.astro
CHANGED
|
@@ -14,10 +14,10 @@ export interface FaqItem {
|
|
|
14
14
|
|
|
15
15
|
const faq = tv({
|
|
16
16
|
slots: {
|
|
17
|
-
heading: 'mb-8 text-2xl font-
|
|
17
|
+
heading: 'mb-8 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
18
18
|
list: 'flex flex-col',
|
|
19
19
|
item: 'group',
|
|
20
|
-
summary: 'flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg font-
|
|
20
|
+
summary: 'flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-lg [font-weight:var(--font-weight-heading,600)] [&::-webkit-details-marker]:hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
21
21
|
marker: 'size-5 shrink-0 transition-transform duration-200 group-open:rotate-45',
|
|
22
22
|
answer: 'prose-site prose max-w-none pb-5 text-current',
|
|
23
23
|
},
|
|
@@ -29,7 +29,7 @@ interface Props {
|
|
|
29
29
|
siteName: string
|
|
30
30
|
tagline?: string | undefined
|
|
31
31
|
/** Link groups for the `columns` layout. */
|
|
32
|
-
columns?: Array<{ title
|
|
32
|
+
columns?: Array<{ title?: string | undefined, label?: string | undefined, links: LinkInput[] }> | undefined
|
|
33
33
|
/** A single row of links (the footer menu). */
|
|
34
34
|
links?: LinkInput[] | undefined
|
|
35
35
|
social?: LinkInput[] | undefined
|
|
@@ -54,8 +54,8 @@ const line = copyright ?? `© ${new Date().getFullYear()} ${siteName}`
|
|
|
54
54
|
</div>
|
|
55
55
|
{layout === 'columns'
|
|
56
56
|
? columns.map(column => (
|
|
57
|
-
<nav aria-label={column.title}>
|
|
58
|
-
<p class="mb-3 font-semibold">{column.title}</p>
|
|
57
|
+
<nav aria-label={column.label ?? column.title} data-cr-part="nav-footer">
|
|
58
|
+
{column.title && <p class="mb-3 font-semibold">{column.title}</p>}
|
|
59
59
|
<ul class="flex flex-col gap-2">
|
|
60
60
|
{column.links.map(item => <li><a href={item.href} class={link()}>{item.label}</a></li>)}
|
|
61
61
|
</ul>
|
|
@@ -69,7 +69,7 @@ const line = copyright ?? `© ${new Date().getFullYear()} ${siteName}`
|
|
|
69
69
|
</ul>
|
|
70
70
|
)}
|
|
71
71
|
{links.length > 0 && (
|
|
72
|
-
<nav aria-label={navLabel}>
|
|
72
|
+
<nav aria-label={navLabel} data-cr-part="nav-footer">
|
|
73
73
|
<ul class="flex flex-wrap gap-x-6 gap-y-2">
|
|
74
74
|
{links.map(item => <li><a href={item.href} class={link()}>{item.label}</a></li>)}
|
|
75
75
|
</ul>
|
|
@@ -4,28 +4,121 @@
|
|
|
4
4
|
"description": "Site footer: brand and tagline, link columns or one row of links, social links and the copyright line.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": [
|
|
7
|
+
"shared": [
|
|
8
|
+
"types.ts"
|
|
9
|
+
],
|
|
8
10
|
"variants": {
|
|
9
|
-
"layout": {
|
|
10
|
-
|
|
11
|
+
"layout": {
|
|
12
|
+
"options": [
|
|
13
|
+
"simple",
|
|
14
|
+
"columns"
|
|
15
|
+
],
|
|
16
|
+
"default": "simple",
|
|
17
|
+
"description": "simple: brand left, links and copyright right. columns: brand column plus one column per link group."
|
|
18
|
+
},
|
|
19
|
+
"tone": {
|
|
20
|
+
"options": [
|
|
21
|
+
"default",
|
|
22
|
+
"muted",
|
|
23
|
+
"inverse"
|
|
24
|
+
],
|
|
25
|
+
"default": "default",
|
|
26
|
+
"description": "Surface with a top border, muted surface, or dark (ink) background."
|
|
27
|
+
}
|
|
11
28
|
},
|
|
12
29
|
"props": {
|
|
13
|
-
"siteName": {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
30
|
+
"siteName": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"required": true,
|
|
33
|
+
"description": "Site title (site.title or site.organization)."
|
|
34
|
+
},
|
|
35
|
+
"tagline": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "One line under the name (site.tagline)."
|
|
38
|
+
},
|
|
39
|
+
"columns": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"fields": {
|
|
44
|
+
"title": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Heading shown above the links; leave out when the source shows none."
|
|
47
|
+
},
|
|
48
|
+
"label": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Accessible name of the column navigation (the menu name); defaults to the title."
|
|
51
|
+
},
|
|
52
|
+
"links": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"required": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"description": "Link groups for the columns layout; each footer menu or widget area becomes one."
|
|
59
|
+
},
|
|
60
|
+
"links": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"fields": {
|
|
65
|
+
"label": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"required": true
|
|
68
|
+
},
|
|
69
|
+
"href": {
|
|
70
|
+
"type": "url",
|
|
71
|
+
"required": true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"description": "One row of links (the footer menu) for the simple layout."
|
|
76
|
+
},
|
|
77
|
+
"social": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"fields": {
|
|
82
|
+
"label": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"required": true
|
|
85
|
+
},
|
|
86
|
+
"href": {
|
|
87
|
+
"type": "url",
|
|
88
|
+
"required": true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"description": "Social profile links (site.same_as)."
|
|
93
|
+
},
|
|
94
|
+
"copyright": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Copyright line; defaults to © <year> <siteName>."
|
|
97
|
+
},
|
|
98
|
+
"navLabel": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Accessible name of the footer navigation (ui-strings nav.footer)."
|
|
101
|
+
}
|
|
20
102
|
},
|
|
21
103
|
"slots": {},
|
|
22
104
|
"js": "none",
|
|
23
105
|
"dependencies": {},
|
|
24
106
|
"sources": {
|
|
25
|
-
"gutenberg": [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
107
|
+
"gutenberg": [
|
|
108
|
+
"core/template-part:footer",
|
|
109
|
+
"core/social-links"
|
|
110
|
+
],
|
|
111
|
+
"elementor": [
|
|
112
|
+
"elementor/social-icons",
|
|
113
|
+
"elementor/icon-list"
|
|
114
|
+
],
|
|
115
|
+
"divi": [
|
|
116
|
+
"divi/et_pb_social_media_follow"
|
|
117
|
+
],
|
|
118
|
+
"classic": [
|
|
119
|
+
"classic/footer",
|
|
120
|
+
"classic/widget-area"
|
|
121
|
+
]
|
|
29
122
|
},
|
|
30
123
|
"a11y": "One <footer> landmark; each link group is a <nav> named by its title. Social links open in a new tab with rel=me."
|
|
31
124
|
}
|
|
@@ -14,10 +14,10 @@ export interface GalleryItem {
|
|
|
14
14
|
|
|
15
15
|
const gallery = tv({
|
|
16
16
|
slots: {
|
|
17
|
-
heading: 'mb-8 text-2xl font-
|
|
17
|
+
heading: 'mb-8 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight md:text-3xl',
|
|
18
18
|
list: 'gap-3 md:gap-4',
|
|
19
19
|
item: 'm-0',
|
|
20
|
-
frame: 'block w-full cursor-zoom-in overflow-hidden rounded-card focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
20
|
+
frame: 'block w-full cursor-zoom-in overflow-hidden rounded-[var(--radius-image,var(--radius-card))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
21
21
|
thumb: 'h-full w-full object-cover transition-transform duration-300 hover:scale-[1.02]',
|
|
22
22
|
caption: 'mt-2 text-sm opacity-75',
|
|
23
23
|
viewer: 'm-auto max-h-[92vh] max-w-[92vw] bg-transparent p-0 backdrop:bg-ink/85',
|
|
@@ -74,7 +74,7 @@ const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 2
|
|
|
74
74
|
{open === 'popover' && (
|
|
75
75
|
<div id={viewer} popover class={s.viewer()}>
|
|
76
76
|
<figure class="m-0 flex flex-col items-center gap-3">
|
|
77
|
-
<KitImage image={item.image} sizes="92vw" class="max-h-[80vh] w-auto rounded-card object-contain" />
|
|
77
|
+
<KitImage image={item.image} sizes="92vw" class="max-h-[80vh] w-auto rounded-[var(--radius-image,var(--radius-card))] object-contain" />
|
|
78
78
|
{item.caption && <figcaption class="text-sm text-surface">{item.caption}</figcaption>}
|
|
79
79
|
<button type="button" popovertarget={viewer} popovertargetaction="hide" class="rounded-md border border-surface/40 px-4 py-2 text-sm text-surface hover:bg-surface/10">{closeLabel}</button>
|
|
80
80
|
</figure>
|
|
@@ -55,7 +55,7 @@ const { root, inner, brand } = header({ layout, tone, position })
|
|
|
55
55
|
{showName && <span>{siteName}</span>}
|
|
56
56
|
</a>
|
|
57
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'} />
|
|
58
|
+
<Nav items={items} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" align={layout === 'centered' ? 'center' : 'end'} />
|
|
59
59
|
{action && <Button action={action} size="sm" class="hidden md:inline-flex" />}
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
|
@@ -12,10 +12,10 @@ const hero = tv({
|
|
|
12
12
|
grid: 'grid items-center gap-10',
|
|
13
13
|
copy: 'flex flex-col gap-5',
|
|
14
14
|
eyebrow: 'text-sm font-semibold tracking-wide uppercase opacity-80',
|
|
15
|
-
heading: 'text-4xl font-
|
|
15
|
+
heading: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-5xl lg:text-6xl',
|
|
16
16
|
lead: 'max-w-2xl text-lg text-pretty opacity-85 md:text-xl',
|
|
17
17
|
actions: 'flex flex-wrap gap-3 pt-2',
|
|
18
|
-
media: 'overflow-hidden rounded-card',
|
|
18
|
+
media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
19
19
|
},
|
|
20
20
|
variants: {
|
|
21
21
|
layout: {
|
|
@@ -25,8 +25,13 @@ const hero = tv({
|
|
|
25
25
|
},
|
|
26
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
27
|
tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'opacity-100', lead: 'opacity-100' } },
|
|
28
|
+
// Where a cover sets its text over the image. WordPress covers centre it unless told otherwise.
|
|
29
|
+
align: { start: {}, center: {} },
|
|
28
30
|
},
|
|
29
|
-
|
|
31
|
+
compoundVariants: [
|
|
32
|
+
{ layout: 'cover', align: 'center', class: { grid: 'justify-items-center', copy: 'mx-auto items-center text-center', lead: 'mx-auto', actions: 'justify-center' } },
|
|
33
|
+
],
|
|
34
|
+
defaultVariants: { layout: 'split', tone: 'default', align: 'center' },
|
|
30
35
|
})
|
|
31
36
|
|
|
32
37
|
interface Props {
|
|
@@ -38,12 +43,14 @@ interface Props {
|
|
|
38
43
|
actions?: ActionInput[] | undefined
|
|
39
44
|
image?: ImageInput | undefined
|
|
40
45
|
layout?: 'split' | 'centered' | 'cover' | undefined
|
|
46
|
+
/** Text position over a cover image; other layouts ignore it. */
|
|
47
|
+
align?: 'start' | 'center' | undefined
|
|
41
48
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
42
49
|
id?: string | undefined
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', tone, id } = Astro.props
|
|
46
|
-
const s = hero({ layout, tone })
|
|
52
|
+
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, tone, id } = Astro.props
|
|
53
|
+
const s = hero({ layout, align, tone })
|
|
47
54
|
const Heading = level === 1 ? 'h1' : 'h2'
|
|
48
55
|
const headingId = id ? `${id}-title` : undefined
|
|
49
56
|
const cover = layout === 'cover' && image
|
|
@@ -66,6 +66,27 @@
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
"name": "cover-start",
|
|
71
|
+
"props": {
|
|
72
|
+
"heading": "The long way round",
|
|
73
|
+
"lead": "A year of trains across Europe, one station at a time.",
|
|
74
|
+
"layout": "cover",
|
|
75
|
+
"actions": [
|
|
76
|
+
{
|
|
77
|
+
"label": "Read the series",
|
|
78
|
+
"href": "/series/long-way/"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"image": {
|
|
82
|
+
"src": "/kit-fixtures/photo-3.svg",
|
|
83
|
+
"alt": "",
|
|
84
|
+
"width": 1600,
|
|
85
|
+
"height": 900
|
|
86
|
+
},
|
|
87
|
+
"align": "start"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
69
90
|
{
|
|
70
91
|
"name": "inverse-text-only",
|
|
71
92
|
"props": {
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
],
|
|
30
30
|
"default": "default",
|
|
31
31
|
"description": "Section background (ignored by cover, which is always light text on the image)."
|
|
32
|
+
},
|
|
33
|
+
"align": {
|
|
34
|
+
"options": [
|
|
35
|
+
"start",
|
|
36
|
+
"center"
|
|
37
|
+
],
|
|
38
|
+
"default": "center",
|
|
39
|
+
"description": "Text position over a cover image (layout cover only): centred, as WordPress covers default to, or at the start edge."
|
|
32
40
|
}
|
|
33
41
|
},
|
|
34
42
|
"props": {
|
package/components/nav/Nav.astro
CHANGED
|
@@ -34,15 +34,17 @@ interface Props {
|
|
|
34
34
|
density?: 'compact' | 'comfortable' | undefined
|
|
35
35
|
/** Unique per page when a page has two navigations. */
|
|
36
36
|
id?: string | undefined
|
|
37
|
+
/** What the navigation is, for checks that find a page's parts (`data-cr-part`, e.g. "nav-header"). */
|
|
38
|
+
part?: string | undefined
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', align, density, id = 'site-nav' } = Astro.props
|
|
41
|
+
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', align, density, id = 'site-nav', part } = Astro.props
|
|
40
42
|
const { list, link, sub, toggle } = nav({ align, density })
|
|
41
43
|
const current = Astro.url.pathname
|
|
42
44
|
const drawer = `${id}-drawer`
|
|
43
45
|
---
|
|
44
46
|
{items.length > 0 && (
|
|
45
|
-
<nav aria-label={label} class="relative">
|
|
47
|
+
<nav aria-label={label} data-cr-part={part} class="relative">
|
|
46
48
|
<ul class={list()}>
|
|
47
49
|
{items.map(item => (
|
|
48
50
|
<li class="group relative">
|
package/components/nav/meta.json
CHANGED
|
@@ -4,27 +4,100 @@
|
|
|
4
4
|
"description": "Site navigation: an inline list with hover/focus submenus on wide screens and a toggle on narrow ones that opens a native popover drawer or a <details> dropdown. No JavaScript.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": [
|
|
7
|
+
"shared": [
|
|
8
|
+
"types.ts"
|
|
9
|
+
],
|
|
8
10
|
"variants": {
|
|
9
|
-
"mobile": {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
"mobile": {
|
|
12
|
+
"options": [
|
|
13
|
+
"popover",
|
|
14
|
+
"details"
|
|
15
|
+
],
|
|
16
|
+
"default": "popover",
|
|
17
|
+
"description": "Narrow-screen menu: a side drawer (Popover API) or a dropdown under the toggle."
|
|
18
|
+
},
|
|
19
|
+
"align": {
|
|
20
|
+
"options": [
|
|
21
|
+
"start",
|
|
22
|
+
"end",
|
|
23
|
+
"center"
|
|
24
|
+
],
|
|
25
|
+
"default": "end",
|
|
26
|
+
"description": "Horizontal alignment of the inline list."
|
|
27
|
+
},
|
|
28
|
+
"density": {
|
|
29
|
+
"options": [
|
|
30
|
+
"compact",
|
|
31
|
+
"comfortable"
|
|
32
|
+
],
|
|
33
|
+
"default": "compact",
|
|
34
|
+
"description": "Gap and text size of the inline list."
|
|
35
|
+
}
|
|
12
36
|
},
|
|
13
37
|
"props": {
|
|
14
|
-
"items": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"required": true,
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"fields": {
|
|
44
|
+
"label": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"required": true
|
|
47
|
+
},
|
|
48
|
+
"href": {
|
|
49
|
+
"type": "url",
|
|
50
|
+
"required": true
|
|
51
|
+
},
|
|
52
|
+
"newTab": {
|
|
53
|
+
"type": "boolean"
|
|
54
|
+
},
|
|
55
|
+
"children": {
|
|
56
|
+
"type": "array"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"description": "Menu entries; one level of children is shown."
|
|
61
|
+
},
|
|
62
|
+
"label": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Accessible name of the navigation landmark (ui-strings nav.label)."
|
|
65
|
+
},
|
|
66
|
+
"menuLabel": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Text of the narrow-screen toggle (ui-strings nav.menu)."
|
|
69
|
+
},
|
|
70
|
+
"closeLabel": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Text of the drawer's close button (ui-strings nav.close)."
|
|
73
|
+
},
|
|
74
|
+
"id": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Unique id when a page renders two navigations."
|
|
77
|
+
},
|
|
78
|
+
"part": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "What the navigation is, for checks that find a page's parts (data-cr-part), e.g. \"nav-header\"."
|
|
81
|
+
}
|
|
19
82
|
},
|
|
20
83
|
"slots": {},
|
|
21
84
|
"js": "none",
|
|
22
85
|
"dependencies": {},
|
|
23
86
|
"sources": {
|
|
24
|
-
"gutenberg": [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
"gutenberg": [
|
|
88
|
+
"core/navigation",
|
|
89
|
+
"core/page-list"
|
|
90
|
+
],
|
|
91
|
+
"elementor": [
|
|
92
|
+
"elementor/nav-menu"
|
|
93
|
+
],
|
|
94
|
+
"divi": [
|
|
95
|
+
"divi/et_pb_menu",
|
|
96
|
+
"divi/et_pb_fullwidth_menu"
|
|
97
|
+
],
|
|
98
|
+
"classic": [
|
|
99
|
+
"classic/nav-menu"
|
|
100
|
+
]
|
|
28
101
|
},
|
|
29
102
|
"a11y": "A <nav> landmark with a label; the current page carries aria-current. Submenus open on :focus-within as well as hover. The drawer is a native popover: Escape and light dismiss close it and focus returns to the toggle."
|
|
30
103
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A newsletter sign-up that posts straight to the list provider's hosted form
|
|
3
|
+
// endpoint (Mailchimp, Kit, MailerLite, Brevo, Buttondown): a plain HTML form,
|
|
4
|
+
// no script, no server of ours. The provider shows its own confirmation page,
|
|
5
|
+
// in a new tab by default so the reader keeps the site. The action must be
|
|
6
|
+
// the named provider's hosted endpoint (exact host and path) — a WordPress-side
|
|
7
|
+
// handler (MC4WP, Divi signup, Jetpack) is dead on a static site — or, for
|
|
8
|
+
// `other`, an https address off the site's own origin. Otherwise the section
|
|
9
|
+
// renders nothing; the migration plan lists such a sign-up for review instead
|
|
10
|
+
// of placing it.
|
|
11
|
+
import { tv } from 'tailwind-variants'
|
|
12
|
+
import Section from '../_shared/Section.astro'
|
|
13
|
+
import { newsletterForm } from './newsletter'
|
|
14
|
+
|
|
15
|
+
const newsletter = tv({
|
|
16
|
+
slots: {
|
|
17
|
+
box: 'flex flex-col gap-6',
|
|
18
|
+
copy: 'flex flex-col gap-3',
|
|
19
|
+
heading: 'text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
20
|
+
intro: 'max-w-xl text-pretty opacity-85',
|
|
21
|
+
form: 'flex flex-col gap-3',
|
|
22
|
+
field: 'flex min-w-0 flex-col gap-1.5',
|
|
23
|
+
label: 'text-sm font-semibold',
|
|
24
|
+
input: 'min-h-11 w-full rounded-[var(--radius-control,var(--radius-md))] border border-line bg-surface px-3 py-2 text-base text-ink placeholder:text-ink-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
25
|
+
submit: 'inline-flex min-h-11 items-center justify-center rounded-[var(--radius-control,var(--radius-md))] px-5 py-2.5 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 bg-accent text-accent-ink hover:bg-ink hover:text-surface',
|
|
26
|
+
note: 'text-sm opacity-75',
|
|
27
|
+
},
|
|
28
|
+
variants: {
|
|
29
|
+
layout: {
|
|
30
|
+
inline: { box: 'md:flex-row md:items-end md:justify-between md:gap-12', copy: 'md:max-w-md', form: 'md:flex-row md:items-end md:shrink-0' },
|
|
31
|
+
stacked: { box: 'mx-auto max-w-xl items-stretch text-center', intro: 'mx-auto', form: 'text-start' },
|
|
32
|
+
},
|
|
33
|
+
// 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.
|
|
34
|
+
tone: {
|
|
35
|
+
default: {},
|
|
36
|
+
muted: {},
|
|
37
|
+
inverse: { submit: 'hover:bg-surface hover:text-ink' },
|
|
38
|
+
accent: { intro: 'opacity-100', note: 'opacity-100', submit: 'bg-ink text-surface hover:bg-surface hover:text-ink' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { layout: 'inline', tone: 'muted' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
/** The provider's form endpoint (Mailchimp `https://….list-manage.com/subscribe/post?u=…&id=…`, Kit `https://app.kit.com/forms/<id>/subscriptions`, …). */
|
|
46
|
+
action: string
|
|
47
|
+
provider?: 'mailchimp' | 'kit' | 'mailerlite' | 'brevo' | 'buttondown' | 'other' | undefined
|
|
48
|
+
/** Field name for the address when the provider is `other` or names it differently. */
|
|
49
|
+
emailName?: string | undefined
|
|
50
|
+
/** Ask for a first name too (when the provider takes one). */
|
|
51
|
+
askName?: boolean | undefined
|
|
52
|
+
heading?: string | undefined
|
|
53
|
+
intro?: string | undefined
|
|
54
|
+
/** Small print under the form (privacy, frequency). */
|
|
55
|
+
note?: string | undefined
|
|
56
|
+
/** ui-strings newsletter.email */
|
|
57
|
+
emailLabel?: string | undefined
|
|
58
|
+
/** ui-strings newsletter.name */
|
|
59
|
+
nameLabel?: string | undefined
|
|
60
|
+
/** ui-strings newsletter.submit */
|
|
61
|
+
submitLabel?: string | undefined
|
|
62
|
+
/** Open the provider's confirmation page in a new tab. */
|
|
63
|
+
newTab?: boolean | undefined
|
|
64
|
+
layout?: 'inline' | 'stacked' | undefined
|
|
65
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
66
|
+
id?: string | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
action, provider = 'mailchimp', emailName, askName = false, heading, intro, note,
|
|
71
|
+
emailLabel = 'Email address', nameLabel = 'First name', submitLabel = 'Subscribe', newTab = true,
|
|
72
|
+
layout, tone = 'muted', id = 'newsletter',
|
|
73
|
+
} = Astro.props
|
|
74
|
+
const form = newsletterForm(provider, action, { email: emailName }, Astro.site)
|
|
75
|
+
const s = newsletter({ layout, tone })
|
|
76
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
77
|
+
// `rel` on a form is HTML (the provider's page gets no opener and no referrer) but not in Astro's form attribute types.
|
|
78
|
+
const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' } : {}
|
|
79
|
+
---
|
|
80
|
+
{form && (
|
|
81
|
+
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
82
|
+
<div class={s.box()}>
|
|
83
|
+
{(heading || intro) && (
|
|
84
|
+
<div class={s.copy()}>
|
|
85
|
+
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
86
|
+
{intro && <p class={s.intro()}>{intro}</p>}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<div class="flex flex-col gap-2">
|
|
90
|
+
<form method="post" action={form.action} target={newTab ? '_blank' : undefined} {...formRel} class={s.form()} aria-labelledby={headingId} aria-label={headingId ? undefined : submitLabel}>
|
|
91
|
+
{askName && form.name && (
|
|
92
|
+
<div class={s.field()}>
|
|
93
|
+
<label for={`${id}-name`} class={s.label()}>{nameLabel}</label>
|
|
94
|
+
<input id={`${id}-name`} name={form.name} type="text" autocomplete="given-name" class={s.input()} />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
<div class={s.field({ class: 'md:min-w-72' })}>
|
|
98
|
+
<label for={`${id}-email`} class={s.label()}>{emailLabel}</label>
|
|
99
|
+
<input id={`${id}-email`} name={form.email} type="email" required autocomplete="email" inputmode="email" class={s.input()} />
|
|
100
|
+
</div>
|
|
101
|
+
{form.hidden.map(field => (
|
|
102
|
+
<div aria-hidden="true" class="absolute -left-[5000px]"><input type="text" name={field.name} value={field.value} tabindex="-1" autocomplete="off" /></div>
|
|
103
|
+
))}
|
|
104
|
+
<button type="submit" class={s.submit()}>{submitLabel}</button>
|
|
105
|
+
</form>
|
|
106
|
+
{note && <p class={s.note()}>{note}</p>}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</Section>
|
|
110
|
+
)}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"name": "mailchimp-inline-muted", "props": {"action": "https://example.us21.list-manage.com/subscribe/post?u=0a1b2c3d&id=9f8e7d", "heading": "Letters from the field", "intro": "One long read a month, nothing else.", "note": "Unsubscribe any time. We never share your address.", "id": "newsletter-mailchimp"}},
|
|
3
|
+
{"name": "kit-stacked-name", "props": {"action": "https://app.kit.com/forms/1234567/subscriptions", "provider": "kit", "askName": true, "layout": "stacked", "tone": "default", "heading": "Join the journal", "submitLabel": "Sign me up", "id": "newsletter-kit"}},
|
|
4
|
+
{"name": "brevo-inverse", "props": {"action": "https://a1b2c3d4.sibforms.com/serve/MUIFAB", "provider": "brevo", "tone": "inverse", "heading": "News from the studio", "id": "newsletter-brevo"}},
|
|
5
|
+
{"name": "other-accent", "props": {"action": "https://buttondown.com/api/emails/embed-subscribe/journal", "provider": "buttondown", "tone": "accent", "id": "newsletter-accent"}}
|
|
6
|
+
]
|