@contentrain/astro-kit 0.4.0 → 0.6.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 +2 -0
- package/catalog.json +1519 -165
- package/components/_shared/KitImage.astro +10 -4
- package/components/_shared/image.ts +14 -0
- package/components/_shared/types.ts +2 -1
- package/components/announcement/Announcement.astro +53 -0
- package/components/announcement/fixtures.json +6 -0
- package/components/announcement/meta.json +53 -0
- package/components/card-grid/CardGrid.astro +5 -3
- package/components/card-grid/meta.json +25 -11
- package/components/consent/Consent.astro +87 -0
- package/components/consent/fixtures.json +4 -0
- package/components/consent/meta.json +67 -0
- package/components/contact-details/ContactDetails.astro +74 -0
- package/components/contact-details/fixtures.json +4 -0
- package/components/contact-details/meta.json +105 -0
- package/components/contact-form/ContactForm.astro +22 -6
- package/components/contact-form/fixtures.json +2 -1
- package/components/contact-form/meta.json +7 -6
- package/components/cta/meta.json +4 -4
- package/components/faq/Faq.astro +1 -1
- package/components/faq/meta.json +7 -7
- package/components/feature-list/FeatureList.astro +85 -0
- package/components/feature-list/fixtures.json +129 -0
- package/components/feature-list/meta.json +118 -0
- package/components/footer/Footer.astro +15 -7
- package/components/footer/fixtures.json +3 -1
- package/components/footer/meta.json +12 -0
- package/components/gallery/Gallery.astro +7 -3
- package/components/gallery/meta.json +6 -6
- package/components/header/Header.astro +6 -4
- package/components/header/fixtures.json +2 -1
- package/components/hero/meta.json +30 -8
- package/components/logo-cloud/LogoCloud.astro +69 -0
- package/components/logo-cloud/fixtures.json +139 -0
- package/components/logo-cloud/meta.json +107 -0
- package/components/nav/Nav.astro +11 -4
- package/components/nav/fixtures.json +1 -0
- package/components/nav/meta.json +8 -0
- package/components/newsletter/meta.json +12 -0
- package/components/pricing/Pricing.astro +94 -0
- package/components/pricing/fixtures.json +6 -0
- package/components/pricing/meta.json +133 -0
- package/components/prose/Prose.astro +7 -3
- package/components/prose/fixtures.json +2 -1
- package/components/prose/meta.json +5 -2
- package/components/slider/Slider.astro +15 -3
- package/components/slider/meta.json +8 -8
- package/components/split/Split.astro +64 -0
- package/components/split/fixtures.json +6 -0
- package/components/split/meta.json +154 -0
- package/components/stats/Stats.astro +66 -0
- package/components/stats/fixtures.json +98 -0
- package/components/stats/meta.json +113 -0
- package/components/steps/Steps.astro +77 -0
- package/components/steps/fixtures.json +5 -0
- package/components/steps/meta.json +105 -0
- package/components/team/Team.astro +82 -0
- package/components/team/fixtures.json +5 -0
- package/components/team/meta.json +116 -0
- package/components/testimonial/Testimonial.astro +5 -3
- package/components/testimonial/meta.json +21 -20
- package/dist/index.d.mts +115 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +175 -3
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +51 -6
- package/mapping/divi.json +359 -9
- package/mapping/elementor.json +581 -8
- package/mapping/gutenberg.json +420 -4
- package/package.json +2 -2
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
// StudioForm): its fields, spam protection and notifications live in Studio,
|
|
5
5
|
// and submissions arrive there for review — no form plugin, no mail server.
|
|
6
6
|
// Until the site is bound to a Studio project the form renders nothing and the
|
|
7
|
-
// contact details still show.
|
|
7
|
+
// contact details still show. When the owner chose another home for forms at
|
|
8
|
+
// migration (site.config.ts `features.forms`), the same place holds a form
|
|
9
|
+
// that posts to their form service, an address to write to, or a link to the
|
|
10
|
+
// form on WordPress.
|
|
8
11
|
import { tv } from 'tailwind-variants'
|
|
9
12
|
import Section from '../_shared/Section.astro'
|
|
10
13
|
import StudioForm from '../../studio/StudioForm.astro'
|
|
11
|
-
import {
|
|
14
|
+
import { formHome } from '../../../lib/features'
|
|
12
15
|
|
|
13
16
|
export interface ContactDetail {
|
|
14
17
|
label: string
|
|
@@ -26,6 +29,8 @@ const contact = tv({
|
|
|
26
29
|
details: 'mt-2 flex flex-col gap-4',
|
|
27
30
|
term: 'text-sm font-semibold opacity-75',
|
|
28
31
|
value: 'text-current',
|
|
32
|
+
formColumn: 'flex flex-col gap-4',
|
|
33
|
+
formHeading: 'text-xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-2xl',
|
|
29
34
|
form: 'rounded-card',
|
|
30
35
|
},
|
|
31
36
|
variants: {
|
|
@@ -44,17 +49,21 @@ interface Props {
|
|
|
44
49
|
model: string
|
|
45
50
|
heading?: string | undefined
|
|
46
51
|
intro?: string | undefined
|
|
52
|
+
/** The form's own heading, above the form ("Tell us about your project"). */
|
|
53
|
+
formHeading?: string | undefined
|
|
47
54
|
details?: ContactDetail[] | undefined
|
|
48
55
|
layout?: 'split' | 'stacked' | undefined
|
|
49
56
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
50
57
|
id?: string | undefined
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
const { model, heading, intro, details = [], layout, tone, id = 'contact' } = Astro.props
|
|
54
|
-
//
|
|
55
|
-
const formShown =
|
|
60
|
+
const { model, heading, intro, formHeading, details = [], layout, tone, id = 'contact' } = Astro.props
|
|
61
|
+
// Nothing to render (Studio not bound yet): a split would leave an empty column — the details stand alone.
|
|
62
|
+
const formShown = formHome(model) !== null
|
|
56
63
|
const s = contact({ layout: formShown ? layout : 'stacked', tone })
|
|
57
64
|
const headingId = heading ? `${id}-title` : undefined
|
|
65
|
+
// One level under the section heading, or the section's heading when there is none.
|
|
66
|
+
const FormHeading = heading ? 'h3' : 'h2'
|
|
58
67
|
---
|
|
59
68
|
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
60
69
|
<div class={s.grid()}>
|
|
@@ -74,6 +83,13 @@ const headingId = heading ? `${id}-title` : undefined
|
|
|
74
83
|
)}
|
|
75
84
|
</div>
|
|
76
85
|
)}
|
|
77
|
-
{formShown &&
|
|
86
|
+
{formShown && (formHeading
|
|
87
|
+
? (
|
|
88
|
+
<div class={s.formColumn()}>
|
|
89
|
+
<FormHeading class={s.formHeading()}>{formHeading}</FormHeading>
|
|
90
|
+
<StudioForm model={model} class={s.form()} />
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
: <StudioForm model={model} class={s.form()} />)}
|
|
78
94
|
</div>
|
|
79
95
|
</Section>
|
|
@@ -14,5 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
{ "name": "stacked-muted", "props": { "model": "contact-submissions", "heading": "Write to us", "layout": "stacked", "tone": "muted", "id": "write" } },
|
|
16
16
|
{ "name": "split-inverse", "props": { "model": "contact-submissions", "heading": "Press", "tone": "inverse", "id": "press", "details": [{ "label": "Press office", "value": "press@northwind.example", "href": "mailto:press@northwind.example" }] } },
|
|
17
|
-
{ "name": "stacked-accent", "props": { "model": "newsletter-requests", "heading": "Request a print copy", "layout": "stacked", "tone": "accent", "id": "print" } }
|
|
17
|
+
{ "name": "stacked-accent", "props": { "model": "newsletter-requests", "heading": "Request a print copy", "layout": "stacked", "tone": "accent", "id": "print" } },
|
|
18
|
+
{ "name": "form-heading", "props": { "model": "contact-submissions", "heading": "Contact", "intro": "Tell us where you are and what you need.", "formHeading": "Tell us about your project", "details": [{ "label": "Address", "value": "12 Harbour Street, Leith" }] } }
|
|
18
19
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "contact-form",
|
|
3
3
|
"name": "Contact form",
|
|
4
|
-
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project.",
|
|
4
|
+
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project. When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": ["Section.astro"],
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background; the form always sits on a surface card." }
|
|
11
11
|
},
|
|
12
12
|
"props": {
|
|
13
|
-
"model": { "type": "string", "required": true, "description": "The Studio model submissions are saved to — the form's fields are that model's form settings (from the plugin form's fields at migration)." },
|
|
14
|
-
"heading": { "type": "string", "description": "Section heading." },
|
|
15
|
-
"intro": { "type": "text", "description": "A line or two above the details." },
|
|
16
|
-
"
|
|
17
|
-
"
|
|
13
|
+
"model": { "type": "string", "required": true, "content": false, "description": "The Studio model submissions are saved to — the form's fields are that model's form settings (from the plugin form's fields at migration)." },
|
|
14
|
+
"heading": { "type": "string", "label": "Heading", "description": "Section heading." },
|
|
15
|
+
"intro": { "type": "text", "label": "Introduction", "description": "A line or two above the details." },
|
|
16
|
+
"formHeading": { "type": "string", "label": "Form heading", "description": "The form's own heading, shown above the form." },
|
|
17
|
+
"details": { "type": "array", "label": "Contact details", "items": { "type": "object", "fields": { "label": { "type": "string", "label": "Label", "required": true, "description": "What the detail is (Phone, Email, Address)." }, "value": { "type": "string", "label": "Value", "required": true, "description": "The detail as shown." }, "href": { "type": "url", "label": "Link", "description": "A tel:, mailto: or map link for it." } } }, "description": "Ways to reach you, in order (Email → mailto:, Phone → tel:, Address → map link)." },
|
|
18
|
+
"id": { "type": "string", "content": false, "description": "Section id; the heading gets `<id>-title`." }
|
|
18
19
|
},
|
|
19
20
|
"slots": {},
|
|
20
21
|
"js": "studio",
|
package/components/cta/meta.json
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"tone": { "options": ["default", "muted", "inverse", "accent"], "default": "accent", "description": "Background. On accent, primary buttons are drawn in the secondary style so they stay visible." }
|
|
12
12
|
},
|
|
13
13
|
"props": {
|
|
14
|
-
"heading": { "type": "string", "required": true, "description": "The call's heading (Divi CTA title, Elementor call-to-action title)." },
|
|
15
|
-
"body": { "type": "text", "description": "One or two sentences under the heading." },
|
|
16
|
-
"actions": { "type": "array", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "newTab": { "type": "boolean" }, "style": { "type": "select", "options": ["primary", "secondary", "ghost"] } } }, "description": "Buttons, in order; usually one." },
|
|
17
|
-
"id": { "type": "string", "description": "Anchor id of the section; the heading gets `<id>-title`." }
|
|
14
|
+
"heading": { "type": "string", "label": "Heading", "required": true, "description": "The call's heading (Divi CTA title, Elementor call-to-action title)." },
|
|
15
|
+
"body": { "type": "text", "label": "Text", "description": "One or two sentences under the heading." },
|
|
16
|
+
"actions": { "type": "array", "label": "Buttons", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "label": "Label", "required": true, "description": "The button text." }, "href": { "type": "url", "label": "Link", "required": true, "description": "Where the button goes: a page on this site or a full address." }, "newTab": { "type": "boolean", "label": "Open in a new tab", "description": "Open the link in a new tab (an external site the reader should not leave for)." }, "style": { "type": "select", "label": "Style", "options": ["primary", "secondary", "ghost"], "description": "primary for the main action, secondary or ghost for the ones beside it." } } }, "description": "Buttons, in order; usually one." },
|
|
17
|
+
"id": { "type": "string", "content": false, "description": "Anchor id of the section; the heading gets `<id>-title`." }
|
|
18
18
|
},
|
|
19
19
|
"slots": {},
|
|
20
20
|
"js": "none",
|
package/components/faq/Faq.astro
CHANGED
|
@@ -26,7 +26,7 @@ const faq = tv({
|
|
|
26
26
|
lines: { list: 'divide-y divide-line border-y border-line' },
|
|
27
27
|
cards: { list: 'gap-3', item: 'rounded-card border border-line px-5', summary: 'py-5' },
|
|
28
28
|
// WordPress's details block: the browser's own disclosure triangle, no rules or box.
|
|
29
|
-
plain: { list: 'gap-2', summary: 'list-item list-[revert] py-1 text-
|
|
29
|
+
plain: { list: 'gap-2', summary: 'list-item list-[revert] py-1 text-[length:var(--text-body,1rem)] [font-weight:inherit] [&::-webkit-details-marker]:inline', marker: 'hidden', answer: 'pt-2 pl-5' },
|
|
30
30
|
},
|
|
31
31
|
tone: { default: {}, muted: { item: 'bg-surface' }, inverse: { answer: 'prose-invert' }, accent: { answer: 'prose-invert' } },
|
|
32
32
|
},
|
package/components/faq/meta.json
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": ["Section.astro"],
|
|
8
8
|
"variants": {
|
|
9
|
-
"style": { "options": ["lines", "cards", "plain"], "default": "lines", "description": "lines: rows divided by rules. cards: each question in a rounded box. plain: the browser's own disclosure triangle without rules or boxes (WordPress's details block)." },
|
|
9
|
+
"style": { "options": ["lines", "cards", "plain"], "default": "lines", "description": "lines: rows divided by rules. cards: each question in a rounded box. plain: the browser's own disclosure triangle without rules or boxes, questions at the body size (WordPress's details block)." },
|
|
10
10
|
"tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background." }
|
|
11
11
|
},
|
|
12
12
|
"props": {
|
|
13
|
-
"items": { "type": "array", "required": true, "items": { "type": "object", "fields": { "question": { "type": "string", "required": true }, "answer": { "type": "richtext", "required": true } } }, "description": "The questions in order (core/details summary + content, Elementor accordion/toggle tabs, Divi accordion/toggle items)." },
|
|
14
|
-
"heading": { "type": "string", "description": "Section heading." },
|
|
15
|
-
"exclusive": { "type": "boolean", "default": false, "description": "One answer open at a time (Elementor accordion, Divi accordion). Off for toggles." },
|
|
16
|
-
"openFirst": { "type": "boolean", "default": false, "description": "The first answer starts open (Divi open_toggle, Elementor active item 1)." },
|
|
17
|
-
"schema": { "type": "boolean", "default": false, "description": "Emit FAQPage JSON-LD built from these items. Use on at most one FAQ per page, and only when the page is really a FAQ." },
|
|
18
|
-
"id": { "type": "string", "description": "Section id; also the exclusive group's name." }
|
|
13
|
+
"items": { "type": "array", "label": "Questions", "required": true, "items": { "type": "object", "fields": { "question": { "type": "string", "label": "Question", "required": true, "description": "The question, as the visitor would ask it." }, "answer": { "type": "richtext", "label": "Answer", "required": true, "description": "The answer; links and lists allowed." } } }, "description": "The questions in order (core/details summary + content, Elementor accordion/toggle tabs, Divi accordion/toggle items)." },
|
|
14
|
+
"heading": { "type": "string", "label": "Heading", "description": "Section heading." },
|
|
15
|
+
"exclusive": { "type": "boolean", "default": false, "content": false, "description": "One answer open at a time (Elementor accordion, Divi accordion). Off for toggles." },
|
|
16
|
+
"openFirst": { "type": "boolean", "default": false, "content": false, "description": "The first answer starts open (Divi open_toggle, Elementor active item 1)." },
|
|
17
|
+
"schema": { "type": "boolean", "default": false, "content": false, "description": "Emit FAQPage JSON-LD built from these items. Use on at most one FAQ per page, and only when the page is really a FAQ." },
|
|
18
|
+
"id": { "type": "string", "content": false, "description": "Section id; also the exclusive group's name." }
|
|
19
19
|
},
|
|
20
20
|
"slots": {},
|
|
21
21
|
"js": "none",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A list of short features — a title and a line of text each — marked with
|
|
3
|
+
// the feature's icon or a shared check, under an optional heading and intro.
|
|
4
|
+
import { tv } from 'tailwind-variants'
|
|
5
|
+
import Section, { type Tone } from '../_shared/Section.astro'
|
|
6
|
+
|
|
7
|
+
const list = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
grid: 'grid gap-x-8 gap-y-10 sm:grid-cols-2',
|
|
10
|
+
item: 'flex gap-4',
|
|
11
|
+
mark: 'flex size-6 shrink-0 items-center justify-center text-accent',
|
|
12
|
+
title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
|
|
13
|
+
text: 'mt-1 opacity-80',
|
|
14
|
+
},
|
|
15
|
+
variants: {
|
|
16
|
+
columns: {
|
|
17
|
+
'2': {},
|
|
18
|
+
'3': { grid: 'md:grid-cols-3' },
|
|
19
|
+
'4': { grid: 'lg:grid-cols-4' },
|
|
20
|
+
},
|
|
21
|
+
style: {
|
|
22
|
+
icon: { mark: 'size-10 rounded-[var(--radius-card)] bg-accent/10' },
|
|
23
|
+
check: {},
|
|
24
|
+
},
|
|
25
|
+
// On accent the mark takes the ink colour, and text is never dimmed.
|
|
26
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: { mark: 'text-current', text: 'opacity-100' } },
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: { columns: '3', style: 'icon', tone: 'default' },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 24×24 stroke paths (Lucide-style); unknown names fall back to check.
|
|
32
|
+
const icons: Record<string, string> = {
|
|
33
|
+
check: 'M20 6 9 17l-5-5',
|
|
34
|
+
star: 'm12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z',
|
|
35
|
+
bolt: 'M13 2 3 14h9l-1 8 10-12h-9z',
|
|
36
|
+
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
|
|
37
|
+
heart: 'M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z',
|
|
38
|
+
globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20',
|
|
39
|
+
clock: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 6v6l4 2',
|
|
40
|
+
chat: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Item {
|
|
44
|
+
title: string
|
|
45
|
+
text?: string | undefined
|
|
46
|
+
/** A kit icon name (check, star, bolt, shield, heart, globe, clock, chat). */
|
|
47
|
+
icon?: string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
heading?: string | undefined
|
|
52
|
+
intro?: string | undefined
|
|
53
|
+
items: Item[]
|
|
54
|
+
columns?: '2' | '3' | '4' | undefined
|
|
55
|
+
style?: 'icon' | 'check' | undefined
|
|
56
|
+
tone?: Tone | undefined
|
|
57
|
+
id?: string | undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { heading, intro, items, columns, style = 'icon', tone, id } = Astro.props
|
|
61
|
+
const s = list({ columns, style, tone })
|
|
62
|
+
const headingId = heading && id ? `${id}-title` : undefined
|
|
63
|
+
const pathOf = (item: Item) => (style === 'icon' && item.icon && icons[item.icon]) || icons.check
|
|
64
|
+
---
|
|
65
|
+
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
66
|
+
{(heading || intro) && (
|
|
67
|
+
<header class="mb-10 flex max-w-2xl flex-col gap-3">
|
|
68
|
+
{heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
|
|
69
|
+
{intro && <p class:list={['text-lg', tone === 'accent' ? undefined : 'opacity-80']} data-kit-text>{intro}</p>}
|
|
70
|
+
</header>
|
|
71
|
+
)}
|
|
72
|
+
<ul class={s.grid()}>
|
|
73
|
+
{items.map(item => (
|
|
74
|
+
<li class={s.item()}>
|
|
75
|
+
<span class={s.mark()} aria-hidden="true">
|
|
76
|
+
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={pathOf(item)} /></svg>
|
|
77
|
+
</span>
|
|
78
|
+
<div>
|
|
79
|
+
<h3 class={s.title()}>{item.title}</h3>
|
|
80
|
+
{item.text && <p class={s.text()} data-kit-text>{item.text}</p>}
|
|
81
|
+
</div>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
</Section>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "three-icon",
|
|
4
|
+
"props": {
|
|
5
|
+
"heading": "Why teams switch",
|
|
6
|
+
"intro": "Everything a small site needs, nothing it doesn't.",
|
|
7
|
+
"items": [
|
|
8
|
+
{
|
|
9
|
+
"title": "Fast by default",
|
|
10
|
+
"text": "Static pages served from the edge.",
|
|
11
|
+
"icon": "bolt"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"title": "Secure",
|
|
15
|
+
"text": "No database to break into.",
|
|
16
|
+
"icon": "shield"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"title": "Global",
|
|
20
|
+
"text": "Every language your readers speak.",
|
|
21
|
+
"icon": "globe"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "Loved by editors",
|
|
25
|
+
"text": "Review changes before they go live.",
|
|
26
|
+
"icon": "heart"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "Always on time",
|
|
30
|
+
"icon": "clock"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"title": "Real support",
|
|
34
|
+
"text": "Talk to the people who built it.",
|
|
35
|
+
"icon": "chat"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "two-check-muted",
|
|
42
|
+
"props": {
|
|
43
|
+
"heading": "Included",
|
|
44
|
+
"items": [
|
|
45
|
+
{
|
|
46
|
+
"title": "Fast by default",
|
|
47
|
+
"text": "Static pages served from the edge.",
|
|
48
|
+
"icon": "bolt"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "Secure",
|
|
52
|
+
"text": "No database to break into.",
|
|
53
|
+
"icon": "shield"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"title": "Global",
|
|
57
|
+
"text": "Every language your readers speak.",
|
|
58
|
+
"icon": "globe"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"title": "Loved by editors",
|
|
62
|
+
"text": "Review changes before they go live.",
|
|
63
|
+
"icon": "heart"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"columns": "2",
|
|
67
|
+
"style": "check",
|
|
68
|
+
"tone": "muted"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "four-inverse",
|
|
73
|
+
"props": {
|
|
74
|
+
"items": [
|
|
75
|
+
{
|
|
76
|
+
"title": "Fast by default",
|
|
77
|
+
"text": "Static pages served from the edge.",
|
|
78
|
+
"icon": "bolt"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"title": "Secure",
|
|
82
|
+
"text": "No database to break into.",
|
|
83
|
+
"icon": "shield"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"title": "Global",
|
|
87
|
+
"text": "Every language your readers speak.",
|
|
88
|
+
"icon": "globe"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"title": "Loved by editors",
|
|
92
|
+
"text": "Review changes before they go live.",
|
|
93
|
+
"icon": "heart"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"title": "Unknown icon falls back",
|
|
97
|
+
"icon": "nope"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"columns": "4",
|
|
101
|
+
"tone": "inverse"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "accent-check",
|
|
106
|
+
"props": {
|
|
107
|
+
"heading": "What you get",
|
|
108
|
+
"items": [
|
|
109
|
+
{
|
|
110
|
+
"title": "Fast by default",
|
|
111
|
+
"text": "Static pages served from the edge.",
|
|
112
|
+
"icon": "bolt"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"title": "Secure",
|
|
116
|
+
"text": "No database to break into.",
|
|
117
|
+
"icon": "shield"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"title": "Global",
|
|
121
|
+
"text": "Every language your readers speak.",
|
|
122
|
+
"icon": "globe"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"style": "check",
|
|
126
|
+
"tone": "accent"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-list",
|
|
3
|
+
"name": "Feature list",
|
|
4
|
+
"description": "A list of short features, each a title and a line of text marked with an icon or a check, under an optional heading and intro. Lighter than a card grid: no images, no links.",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": [
|
|
8
|
+
"Section.astro"
|
|
9
|
+
],
|
|
10
|
+
"variants": {
|
|
11
|
+
"columns": {
|
|
12
|
+
"options": [
|
|
13
|
+
"2",
|
|
14
|
+
"3",
|
|
15
|
+
"4"
|
|
16
|
+
],
|
|
17
|
+
"default": "3",
|
|
18
|
+
"description": "Features per row on wide screens; one on phones, two on tablets."
|
|
19
|
+
},
|
|
20
|
+
"style": {
|
|
21
|
+
"options": [
|
|
22
|
+
"icon",
|
|
23
|
+
"check"
|
|
24
|
+
],
|
|
25
|
+
"default": "icon",
|
|
26
|
+
"description": "icon: each feature's own icon in a tinted badge. check: the same check mark before every feature."
|
|
27
|
+
},
|
|
28
|
+
"tone": {
|
|
29
|
+
"options": [
|
|
30
|
+
"default",
|
|
31
|
+
"muted",
|
|
32
|
+
"inverse",
|
|
33
|
+
"accent"
|
|
34
|
+
],
|
|
35
|
+
"default": "default",
|
|
36
|
+
"description": "Section background."
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"props": {
|
|
40
|
+
"heading": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"label": "Heading",
|
|
43
|
+
"description": "Section heading."
|
|
44
|
+
},
|
|
45
|
+
"intro": {
|
|
46
|
+
"type": "text",
|
|
47
|
+
"label": "Introduction",
|
|
48
|
+
"description": "A sentence under the heading."
|
|
49
|
+
},
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"label": "Features",
|
|
53
|
+
"required": true,
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"fields": {
|
|
57
|
+
"title": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"label": "Title",
|
|
60
|
+
"required": true,
|
|
61
|
+
"description": "The feature, in a few words."
|
|
62
|
+
},
|
|
63
|
+
"text": {
|
|
64
|
+
"type": "text",
|
|
65
|
+
"label": "Text",
|
|
66
|
+
"description": "A sentence about it."
|
|
67
|
+
},
|
|
68
|
+
"icon": {
|
|
69
|
+
"type": "select",
|
|
70
|
+
"label": "Icon",
|
|
71
|
+
"options": [
|
|
72
|
+
"check",
|
|
73
|
+
"star",
|
|
74
|
+
"bolt",
|
|
75
|
+
"shield",
|
|
76
|
+
"heart",
|
|
77
|
+
"globe",
|
|
78
|
+
"clock",
|
|
79
|
+
"chat"
|
|
80
|
+
],
|
|
81
|
+
"description": "The kit icon shown with the icon style; check when absent."
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"description": "The features, in order (Elementor icon list or icon boxes without images, Gutenberg list of features, Divi blurbs with icons)."
|
|
86
|
+
},
|
|
87
|
+
"id": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"content": false,
|
|
90
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"slots": {},
|
|
94
|
+
"js": "none",
|
|
95
|
+
"dependencies": {},
|
|
96
|
+
"sources": {
|
|
97
|
+
"gutenberg": [
|
|
98
|
+
"core/list"
|
|
99
|
+
],
|
|
100
|
+
"elementor": [
|
|
101
|
+
"elementor/icon-list",
|
|
102
|
+
"elementor/icon-box"
|
|
103
|
+
],
|
|
104
|
+
"divi": [
|
|
105
|
+
"divi/et_pb_blurb"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"a11y": "A list; icons are decorative (aria-hidden), the title and text carry the meaning. The section is labelled by its heading.",
|
|
109
|
+
"pattern": {
|
|
110
|
+
"summary": "repeated icon + short title + line of text",
|
|
111
|
+
"signals": [
|
|
112
|
+
"3+ repeated items",
|
|
113
|
+
"icons",
|
|
114
|
+
"no images",
|
|
115
|
+
"short text"
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -21,8 +21,12 @@ const footer = tv({
|
|
|
21
21
|
muted: { root: 'bg-surface-muted text-ink' },
|
|
22
22
|
inverse: { root: 'bg-ink text-surface' },
|
|
23
23
|
},
|
|
24
|
+
align: { start: {}, center: { inner: 'items-center justify-items-center text-center md:items-center [&_ul]:justify-center' } },
|
|
24
25
|
},
|
|
25
|
-
|
|
26
|
+
compoundVariants: [
|
|
27
|
+
{ layout: 'simple', align: 'center', class: { inner: 'md:flex-col md:justify-center' } },
|
|
28
|
+
],
|
|
29
|
+
defaultVariants: { layout: 'simple', tone: 'default', align: 'start' },
|
|
26
30
|
})
|
|
27
31
|
|
|
28
32
|
interface Props {
|
|
@@ -35,23 +39,27 @@ interface Props {
|
|
|
35
39
|
social?: LinkInput[] | undefined
|
|
36
40
|
/** The copyright line, e.g. "© 2026 Northwind Studio"; `{year}` is the current year. */
|
|
37
41
|
copyright?: string | undefined
|
|
42
|
+
/** Label of the cookie settings link after the copyright line ("Cookie settings"); it reopens the consent bar, so a visitor can withdraw consent as easily as they gave it. Only with a consent bar on the page. */
|
|
43
|
+
consentLabel?: string | undefined
|
|
38
44
|
/** Accessible name of the footer navigation ("Footer"). */
|
|
39
45
|
navLabel?: string | undefined
|
|
40
46
|
layout?: 'simple' | 'columns' | undefined
|
|
41
47
|
tone?: 'default' | 'muted' | 'inverse' | undefined
|
|
48
|
+
align?: 'start' | 'center' | undefined
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
const { siteName, tagline, columns = [], links = [], social = [], copyright, navLabel = 'Footer', layout = 'simple', tone } = Astro.props
|
|
45
|
-
const { root, inner, muted, link } = footer({ layout, tone })
|
|
51
|
+
const { siteName, tagline, columns = [], links = [], social = [], copyright, consentLabel, navLabel = 'Footer', layout = 'simple', tone, align } = Astro.props
|
|
52
|
+
const { root, inner, muted, link } = footer({ layout, tone, align })
|
|
46
53
|
const year = String(new Date().getFullYear())
|
|
47
54
|
const line = copyright?.replaceAll('{year}', year) ?? `© ${year} ${siteName}`
|
|
48
55
|
---
|
|
56
|
+
<!-- The cookie settings control is a button, not a link: the Consent component's listener reopens its bar on any [data-kit-consent-open]. -->
|
|
49
57
|
<footer class={root()}>
|
|
50
|
-
<div class={inner()} style={layout === 'columns' ? `--footer-columns: ${Math.max(columns.length, 1)}` : undefined}>
|
|
58
|
+
<div class={inner()} data-kit-footer style={layout === 'columns' ? `--footer-columns: ${Math.max(columns.length, 1)}` : undefined}>
|
|
51
59
|
<div class="flex flex-col gap-3">
|
|
52
60
|
<p class="text-base font-semibold" data-kit-brand>{siteName}</p>
|
|
53
61
|
{tagline && <p class={muted()} data-kit-tagline>{tagline}</p>}
|
|
54
|
-
{layout === 'columns' && <p class:list={[muted(), 'mt-auto pt-6']}>{line}</p>}
|
|
62
|
+
{layout === 'columns' && <p class:list={[muted(), 'mt-auto pt-6']}>{line}{consentLabel && <> · <button type="button" data-kit-consent-open class={link()}>{consentLabel}</button></>}</p>}
|
|
55
63
|
</div>
|
|
56
64
|
{layout === 'columns'
|
|
57
65
|
? columns.map(column => (
|
|
@@ -63,7 +71,7 @@ const line = copyright?.replaceAll('{year}', year) ?? `© ${year} ${siteName}`
|
|
|
63
71
|
</nav>
|
|
64
72
|
))
|
|
65
73
|
: (
|
|
66
|
-
<div class=
|
|
74
|
+
<div class:list={['flex flex-col gap-4', align === 'center' ? 'items-center' : 'md:items-end']}>
|
|
67
75
|
{social.length > 0 && (
|
|
68
76
|
<ul class="flex flex-wrap gap-4">
|
|
69
77
|
{social.map(item => <li><a href={item.href} rel="me noopener" target="_blank" class={link()}>{item.label}</a></li>)}
|
|
@@ -76,7 +84,7 @@ const line = copyright?.replaceAll('{year}', year) ?? `© ${year} ${siteName}`
|
|
|
76
84
|
</ul>
|
|
77
85
|
</nav>
|
|
78
86
|
)}
|
|
79
|
-
<p class={muted()}>{line}</p>
|
|
87
|
+
<p class={muted()}>{line}{consentLabel && <> · <button type="button" data-kit-consent-open class={link()}>{consentLabel}</button></>}</p>
|
|
80
88
|
</div>
|
|
81
89
|
)}
|
|
82
90
|
{layout === 'columns' && social.length > 0 && (
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
[
|
|
2
2
|
{ "name": "simple", "props": { "siteName": "Northwind Studio", "copyright": "© 2026 Northwind Studio", "links": [{ "label": "About", "href": "/about/" }, { "label": "Privacy", "href": "/privacy/" }, { "label": "RSS feed", "href": "/rss.xml" }] } },
|
|
3
3
|
{ "name": "columns-inverse", "props": { "siteName": "Northwind Studio", "tagline": "Calm, fast websites for teams who write.", "layout": "columns", "tone": "inverse", "copyright": "© 2026 Northwind Studio", "columns": [{ "title": "Studio", "links": [{ "label": "About", "href": "/about/" }, { "label": "Team", "href": "/about/team/" }] }, { "title": "Journal", "links": [{ "label": "Guides", "href": "/category/guides/" }, { "label": "News", "href": "/category/news/" }] }, { "title": "Contact", "links": [{ "label": "Email", "href": "mailto:hello@example.com" }] }], "social": [{ "label": "GitHub", "href": "https://github.com/contentrain" }, { "label": "Mastodon", "href": "https://mastodon.social/@contentrain" }] } },
|
|
4
|
-
{ "name": "muted", "props": { "siteName": "Northwind", "tone": "muted", "copyright": "© 2026 Northwind", "links": [{ "label": "Imprint", "href": "/imprint/" }] } }
|
|
4
|
+
{ "name": "muted", "props": { "siteName": "Northwind", "tone": "muted", "copyright": "© 2026 Northwind", "links": [{ "label": "Imprint", "href": "/imprint/" }] } },
|
|
5
|
+
{ "name": "centered", "props": { "siteName": "Golden Agency", "tagline": "Websites for makers", "align": "center", "copyright": "© 2026 Golden Agency", "links": [{ "label": "Services", "href": "/services/" }, { "label": "Contact", "href": "/contact/" }], "social": [{ "label": "Instagram", "href": "https://instagram.com/" }] } },
|
|
6
|
+
{ "name": "consent-settings", "props": { "siteName": "Golden Agency", "copyright": "© 2026 Golden Agency", "consentLabel": "Cookie settings", "links": [{ "label": "Privacy", "href": "/privacy/" }] } }
|
|
5
7
|
]
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
],
|
|
25
25
|
"default": "default",
|
|
26
26
|
"description": "Surface with a top border, muted surface, or dark (ink) background."
|
|
27
|
+
},
|
|
28
|
+
"align": {
|
|
29
|
+
"options": [
|
|
30
|
+
"start",
|
|
31
|
+
"center"
|
|
32
|
+
],
|
|
33
|
+
"default": "start",
|
|
34
|
+
"description": "start: brand and links to the sides (left-aligned on phones). center: everything centred in one column, as themes with a centred footer print it."
|
|
27
35
|
}
|
|
28
36
|
},
|
|
29
37
|
"props": {
|
|
@@ -95,6 +103,10 @@
|
|
|
95
103
|
"type": "string",
|
|
96
104
|
"description": "Copyright line; {year} is the current year. Defaults to © <year> <siteName>."
|
|
97
105
|
},
|
|
106
|
+
"consentLabel": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "Label of the cookie settings button after the copyright line; it reopens the consent bar (withdrawal, GDPR 7(3)). Set only when the site has a consent bar."
|
|
109
|
+
},
|
|
98
110
|
"navLabel": {
|
|
99
111
|
"type": "string",
|
|
100
112
|
"description": "Accessible name of the footer navigation (ui-strings nav.footer)."
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
6
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
7
|
import Section from '../_shared/Section.astro'
|
|
8
|
-
import type
|
|
8
|
+
import { imageOf, type ImageSource } from '../_shared/image'
|
|
9
|
+
import { type ImageInput } from '../_shared/types'
|
|
9
10
|
|
|
10
11
|
export interface GalleryItem {
|
|
11
|
-
image
|
|
12
|
+
/** The image's address (or, until 0.7, an `ImageInput`). */
|
|
13
|
+
image: ImageSource
|
|
14
|
+
imageAlt?: string | undefined
|
|
12
15
|
caption?: string | undefined
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -50,7 +53,8 @@ interface Props {
|
|
|
50
53
|
id?: string | undefined
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
const { items, heading, layout, columns, open = 'popover', closeLabel = 'Close', tone, id = 'gallery' } = Astro.props
|
|
56
|
+
const { items: given, heading, layout, columns, open = 'popover', closeLabel = 'Close', tone, id = 'gallery' } = Astro.props
|
|
57
|
+
const items = given.flatMap(item => { const image = imageOf(item.image, item.imageAlt); return image ? [{ ...item, image }] : [] }) satisfies Array<{ image: ImageInput, caption?: string | undefined }>
|
|
54
58
|
const s = gallery({ layout, columns, open })
|
|
55
59
|
const headingId = heading ? `${id}-title` : undefined
|
|
56
60
|
const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 25vw, 50vw' : '(min-width: 768px) 33vw, 50vw'
|