@contentrain/astro-kit 0.5.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/catalog.json +1512 -167
- 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/meta.json +6 -6
- 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 +14 -6
- 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 +19 -8
- 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/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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pricing",
|
|
3
|
+
"name": "Pricing",
|
|
4
|
+
"description": "Price plans side by side: name, price, period, a short description, a feature list and a button each. The featured plan is raised and badged.",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": [
|
|
8
|
+
"Button.astro",
|
|
9
|
+
"Section.astro",
|
|
10
|
+
"types.ts"
|
|
11
|
+
],
|
|
12
|
+
"variants": {
|
|
13
|
+
"columns": {
|
|
14
|
+
"options": [
|
|
15
|
+
"2",
|
|
16
|
+
"3",
|
|
17
|
+
"4"
|
|
18
|
+
],
|
|
19
|
+
"default": "3",
|
|
20
|
+
"description": "Plans per row on wide screens; one on phones, two on tablets."
|
|
21
|
+
},
|
|
22
|
+
"tone": {
|
|
23
|
+
"options": [
|
|
24
|
+
"default",
|
|
25
|
+
"muted",
|
|
26
|
+
"inverse",
|
|
27
|
+
"accent"
|
|
28
|
+
],
|
|
29
|
+
"default": "default",
|
|
30
|
+
"description": "Section background."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"props": {
|
|
34
|
+
"heading": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"label": "Heading",
|
|
37
|
+
"description": "Section heading above the plans."
|
|
38
|
+
},
|
|
39
|
+
"intro": {
|
|
40
|
+
"type": "text",
|
|
41
|
+
"label": "Intro",
|
|
42
|
+
"description": "A line or two under the heading."
|
|
43
|
+
},
|
|
44
|
+
"plans": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"label": "Plans",
|
|
47
|
+
"required": true,
|
|
48
|
+
"items": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"fields": {
|
|
51
|
+
"name": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"label": "Name",
|
|
54
|
+
"required": true,
|
|
55
|
+
"description": "The plan's name."
|
|
56
|
+
},
|
|
57
|
+
"price": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"label": "Price",
|
|
60
|
+
"required": true,
|
|
61
|
+
"description": "The price as shown, with its currency (\"$12\", \"Free\", \"On request\")."
|
|
62
|
+
},
|
|
63
|
+
"period": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"label": "Period",
|
|
66
|
+
"description": "What the price is per (\"/month\")."
|
|
67
|
+
},
|
|
68
|
+
"description": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"label": "Description",
|
|
71
|
+
"description": "One line on who the plan is for."
|
|
72
|
+
},
|
|
73
|
+
"features": {
|
|
74
|
+
"type": "markdown",
|
|
75
|
+
"label": "Features",
|
|
76
|
+
"description": "What the plan includes, as a markdown list."
|
|
77
|
+
},
|
|
78
|
+
"ctaLabel": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"label": "Button label",
|
|
81
|
+
"description": "The plan's button text."
|
|
82
|
+
},
|
|
83
|
+
"ctaHref": {
|
|
84
|
+
"type": "url",
|
|
85
|
+
"label": "Button link",
|
|
86
|
+
"description": "Where the button goes."
|
|
87
|
+
},
|
|
88
|
+
"featured": {
|
|
89
|
+
"type": "boolean",
|
|
90
|
+
"label": "Featured",
|
|
91
|
+
"description": "Raises the plan and gives it the badge."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"description": "The plans, in display order (Divi pricing tables, Elementor price table)."
|
|
96
|
+
},
|
|
97
|
+
"featuredLabel": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"default": "Most popular",
|
|
100
|
+
"content": false,
|
|
101
|
+
"description": "Badge text on featured plans."
|
|
102
|
+
},
|
|
103
|
+
"id": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"content": false,
|
|
106
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"slots": {},
|
|
110
|
+
"js": "none",
|
|
111
|
+
"dependencies": {},
|
|
112
|
+
"sources": {
|
|
113
|
+
"gutenberg": [
|
|
114
|
+
"core/columns"
|
|
115
|
+
],
|
|
116
|
+
"elementor": [
|
|
117
|
+
"elementor/price-table"
|
|
118
|
+
],
|
|
119
|
+
"divi": [
|
|
120
|
+
"divi/et_pb_pricing_tables"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"a11y": "Plans are a <ul>; each name is an <h3>. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label.",
|
|
124
|
+
"pattern": {
|
|
125
|
+
"summary": "repeated plan units: name + price + feature list + button",
|
|
126
|
+
"signals": [
|
|
127
|
+
"2-4 columns",
|
|
128
|
+
"price text",
|
|
129
|
+
"list",
|
|
130
|
+
"1 button per column"
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// as HTML. Only post and page bodies carry `data-pagefind-body`, so search
|
|
8
8
|
// indexes content, not chrome.
|
|
9
9
|
import { tv } from 'tailwind-variants'
|
|
10
|
+
import { renderMarkdown } from '../../../lib/markdown'
|
|
10
11
|
|
|
11
12
|
const prose = tv({
|
|
12
13
|
base: 'prose prose-site max-w-none',
|
|
@@ -18,13 +19,16 @@ const prose = tv({
|
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
20
21
|
/** Rendered HTML of a richtext field. */
|
|
21
|
-
html
|
|
22
|
+
html?: string | undefined
|
|
23
|
+
/** A markdown field (a page section's text), rendered at build time. Used when `html` is absent. */
|
|
24
|
+
markdown?: string | undefined
|
|
22
25
|
/** Index this body for site search. */
|
|
23
26
|
searchable?: boolean | undefined
|
|
24
27
|
class?: string | undefined
|
|
25
28
|
size?: 'base' | 'lg' | undefined
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
const { html, searchable = true, class: className, size } = Astro.props
|
|
31
|
+
const { html, markdown, searchable = true, class: className, size } = Astro.props
|
|
32
|
+
const body = html ?? await renderMarkdown(markdown)
|
|
29
33
|
---
|
|
30
|
-
<div class={prose({ size, class: className })} data-pagefind-body={searchable ? '' : undefined} set:html={
|
|
34
|
+
{body && <div class={prose({ size, class: className })} data-pagefind-body={searchable ? '' : undefined} set:html={body} />}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
[
|
|
2
2
|
{ "name": "lg", "frame": "narrow", "props": { "html": "<h2 class=\"wp-block-heading\">Keeping what works</h2><p>A careful migration keeps <strong>addresses</strong>, <a href=\"/about/\">formatting</a> and rankings.</p><ul class=\"wp-block-list\"><li>Permalinks stay put.</li><li>Archives survive.</li></ul><blockquote class=\"wp-block-quote\"><p>The fastest request is the one the server never has to think about.</p><cite>Every performance engineer</cite></blockquote><div class=\"wp-block-buttons is-layout-flex\"><div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"/contact/\">Get in touch</a></div></div>" } },
|
|
3
|
-
{ "name": "base", "frame": "narrow", "props": { "size": "base", "searchable": false, "html": "<p class=\"has-drop-cap\">Moving a WordPress site to a static stack is mostly about keeping what already works.</p><figure class=\"wp-block-table is-style-stripes\"><table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Requests</td><td>84</td><td>9</td></tr><tr><td>LCP</td><td>3.1 s</td><td>0.8 s</td></tr></tbody></table></figure>" } }
|
|
3
|
+
{ "name": "base", "frame": "narrow", "props": { "size": "base", "searchable": false, "html": "<p class=\"has-drop-cap\">Moving a WordPress site to a static stack is mostly about keeping what already works.</p><figure class=\"wp-block-table is-style-stripes\"><table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Requests</td><td>84</td><td>9</td></tr><tr><td>LCP</td><td>3.1 s</td><td>0.8 s</td></tr></tbody></table></figure>" } },
|
|
4
|
+
{ "name": "markdown", "frame": "narrow", "props": { "markdown": "## From a page section\n\nA section's text is **markdown** in the page model, with [a link](/about/) and a list:\n\n- Written once\n- Rendered at build time", "size": "base" } }
|
|
4
5
|
]
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
"props": {
|
|
19
19
|
"html": {
|
|
20
20
|
"type": "richtext",
|
|
21
|
-
"
|
|
22
|
-
|
|
21
|
+
"description": "Rendered HTML of a richtext field (posts.body, pages.body). One of html or markdown is given."
|
|
22
|
+
},
|
|
23
|
+
"markdown": {
|
|
24
|
+
"type": "markdown",
|
|
25
|
+
"description": "A markdown field (a page section's text, prose.markdown in a page model), rendered at build time with links through the public-link filter. Used when html is absent."
|
|
23
26
|
},
|
|
24
27
|
"searchable": {
|
|
25
28
|
"type": "boolean",
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { tv } from 'tailwind-variants'
|
|
7
7
|
import Button from '../_shared/Button.astro'
|
|
8
8
|
import KitImage from '../_shared/KitImage.astro'
|
|
9
|
-
import
|
|
9
|
+
import { imageOf, type ImageSource } from '../_shared/image'
|
|
10
|
+
import { type ActionInput, type ImageInput } from '../_shared/types'
|
|
10
11
|
|
|
11
12
|
const slider = tv({
|
|
12
13
|
slots: {
|
|
@@ -36,9 +37,15 @@ const slider = tv({
|
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
interface Slide {
|
|
39
|
-
image
|
|
40
|
+
/** The image's address (or, until 0.7, an `ImageInput`). */
|
|
41
|
+
image: ImageSource
|
|
42
|
+
imageAlt?: string | undefined
|
|
40
43
|
title?: string | undefined
|
|
41
44
|
text?: string | undefined
|
|
45
|
+
/** The slide's link, flat so a page stays within Contentrain's nesting depth. */
|
|
46
|
+
ctaLabel?: string | undefined
|
|
47
|
+
ctaHref?: string | undefined
|
|
48
|
+
/** Until 0.7: the same link as one object. */
|
|
42
49
|
action?: ActionInput | undefined
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -58,7 +65,12 @@ interface Props {
|
|
|
58
65
|
caption?: 'below' | 'overlay' | undefined
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
const { slides, label, prevLabel = 'Previous slide', nextLabel = 'Next slide', slideLabel = 'Slide', ofLabel = 'of', loop = false, perView = '1', aspect, caption } = Astro.props
|
|
68
|
+
const { slides: given, label, prevLabel = 'Previous slide', nextLabel = 'Next slide', slideLabel = 'Slide', ofLabel = 'of', loop = false, perView = '1', aspect, caption } = Astro.props
|
|
69
|
+
const slides = given.flatMap((slide) => {
|
|
70
|
+
const image = imageOf(slide.image, slide.imageAlt)
|
|
71
|
+
const action = slide.action ?? (slide.ctaLabel && slide.ctaHref ? { label: slide.ctaLabel, href: slide.ctaHref } : undefined)
|
|
72
|
+
return image ? [{ ...slide, image, action }] : []
|
|
73
|
+
}) satisfies Array<{ image: ImageInput, action?: ActionInput | undefined }>
|
|
62
74
|
const slots = slider({ perView, aspect, caption })
|
|
63
75
|
const sizes = perView === '1' ? '(min-width: 1216px) 1152px, 100vw' : perView === '2' ? '(min-width: 640px) 50vw, 85vw' : '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 85vw'
|
|
64
76
|
// Arrows from sm up; on phones the carousel is swiped and the dots show the position.
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
"description": "A carousel of images with optional title, text and button per slide. Without JavaScript it is a snapping horizontal scroller; Embla adds paging, arrows and dots.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["Button.astro", "KitImage.astro", "types.ts"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "image.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"perView": { "options": ["1", "2", "3"], "default": "1", "description": "Slides visible at once on wide screens." },
|
|
10
10
|
"aspect": { "options": ["wide", "standard", "square"], "default": "wide", "description": "Image aspect ratio: 21:9, 3:2 or 1:1." },
|
|
11
11
|
"caption": { "options": ["below", "overlay"], "default": "overlay", "description": "Caption under the image, or over its lower edge on a gradient." }
|
|
12
12
|
},
|
|
13
13
|
"props": {
|
|
14
|
-
"slides": { "type": "array", "required": true, "items": { "type": "object", "fields": { "image": { "type": "
|
|
15
|
-
"label": { "type": "string", "required": true, "description": "Accessible name of the carousel." },
|
|
16
|
-
"prevLabel": { "type": "string", "description": "ui-strings slider.prev." },
|
|
17
|
-
"nextLabel": { "type": "string", "description": "ui-strings slider.next." },
|
|
18
|
-
"slideLabel": { "type": "string", "description": "ui-strings slider.slide." },
|
|
19
|
-
"ofLabel": { "type": "string", "description": "ui-strings slider.of." },
|
|
20
|
-
"loop": { "type": "boolean", "default": false, "description": "Wrap from the last slide to the first." }
|
|
14
|
+
"slides": { "type": "array", "label": "Slides", "required": true, "items": { "type": "object", "fields": { "image": { "type": "image", "label": "Image", "required": true, "description": "The slide image." }, "imageAlt": { "type": "string", "label": "Image description", "description": "What the image shows, for readers who cannot see it." }, "title": { "type": "string", "label": "Title", "description": "Heading over the slide." }, "text": { "type": "text", "label": "Text", "description": "A line or two under the heading." }, "ctaLabel": { "type": "string", "label": "Button label", "description": "Button text; the slide has no button without it." }, "ctaHref": { "type": "url", "label": "Button link", "description": "Where the button goes." } } }, "description": "Slides: the image's address and text, and an optional link (ctaLabel + ctaHref)." },
|
|
15
|
+
"label": { "type": "string", "required": true, "content": false, "description": "Accessible name of the carousel." },
|
|
16
|
+
"prevLabel": { "type": "string", "content": false, "description": "ui-strings slider.prev." },
|
|
17
|
+
"nextLabel": { "type": "string", "content": false, "description": "ui-strings slider.next." },
|
|
18
|
+
"slideLabel": { "type": "string", "content": false, "description": "ui-strings slider.slide." },
|
|
19
|
+
"ofLabel": { "type": "string", "content": false, "description": "ui-strings slider.of." },
|
|
20
|
+
"loop": { "type": "boolean", "default": false, "content": false, "description": "Wrap from the last slide to the first." }
|
|
21
21
|
},
|
|
22
22
|
"slots": {},
|
|
23
23
|
"js": "embla",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
// An image beside a block of text: eyebrow, heading, a markdown body and
|
|
3
|
+
// buttons. The image takes either side on wide screens and stacks above the
|
|
4
|
+
// text on phones.
|
|
5
|
+
import { tv } from 'tailwind-variants'
|
|
6
|
+
import { renderMarkdown } from '../../../lib/markdown'
|
|
7
|
+
import Button from '../_shared/Button.astro'
|
|
8
|
+
import KitImage from '../_shared/KitImage.astro'
|
|
9
|
+
import Section from '../_shared/Section.astro'
|
|
10
|
+
import type { ActionInput, ImageInput } from '../_shared/types'
|
|
11
|
+
|
|
12
|
+
const split = tv({
|
|
13
|
+
slots: {
|
|
14
|
+
grid: 'grid items-center gap-10 md:grid-cols-2 md:gap-16',
|
|
15
|
+
copy: 'flex flex-col gap-5',
|
|
16
|
+
eyebrow: 'text-sm font-semibold tracking-wide uppercase opacity-80',
|
|
17
|
+
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
18
|
+
body: 'prose prose-site max-w-none text-pretty',
|
|
19
|
+
actions: 'flex flex-wrap gap-3 pt-2',
|
|
20
|
+
media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
21
|
+
},
|
|
22
|
+
variants: {
|
|
23
|
+
// Source order is text first; `start` moves the image before it on wide screens only.
|
|
24
|
+
imageSide: { start: { media: 'md:order-first' }, end: {} },
|
|
25
|
+
align: { start: {}, center: { copy: 'items-center text-center', actions: 'justify-center' } },
|
|
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' } },
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { imageSide: 'end', align: 'start', tone: 'default' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
heading: string
|
|
34
|
+
eyebrow?: string | undefined
|
|
35
|
+
/** Markdown, rendered at build time. */
|
|
36
|
+
body?: string | undefined
|
|
37
|
+
actions?: ActionInput[] | undefined
|
|
38
|
+
image?: ImageInput | undefined
|
|
39
|
+
imageSide?: 'start' | 'end' | undefined
|
|
40
|
+
align?: 'start' | 'center' | undefined
|
|
41
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
42
|
+
id?: string | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { heading, eyebrow, body, actions = [], image, imageSide, align, tone, id } = Astro.props
|
|
46
|
+
const s = split({ imageSide, align, tone })
|
|
47
|
+
const headingId = id ? `${id}-title` : undefined
|
|
48
|
+
const html = await renderMarkdown(body)
|
|
49
|
+
---
|
|
50
|
+
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
51
|
+
<div class={s.grid()}>
|
|
52
|
+
<div class={s.copy()}>
|
|
53
|
+
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
54
|
+
<h2 id={headingId} class={s.heading()}>{heading}</h2>
|
|
55
|
+
{html && <div class={s.body()} data-kit-text set:html={html} />}
|
|
56
|
+
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
{image && (
|
|
59
|
+
<div class={s.media()}>
|
|
60
|
+
<KitImage image={image} sizes="(min-width: 768px) 50vw, 100vw" class="h-auto w-full object-cover" />
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</Section>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "end-default", "props": { "eyebrow": "Our studio", "heading": "Small team, careful work", "body": "We have built websites for independent publishers since 2012.\n\nEvery project gets **one** designer and one engineer from start to launch.", "actions": [{ "label": "Meet the team", "href": "/team/" }, { "label": "Our process", "href": "/process/", "style": "ghost" }], "image": { "src": "/kit-fixtures/photo-2.svg", "alt": "The studio's workshop", "width": 1200, "height": 800 } } },
|
|
3
|
+
{ "name": "start-muted", "props": { "imageSide": "start", "tone": "muted", "heading": "Content you can edit", "body": "Pages live as structured content in your own repository, editable in [Contentrain Studio](/studio/).", "image": { "src": "/kit-fixtures/photo-3.svg", "alt": "", "width": 1200, "height": 800 } } },
|
|
4
|
+
{ "name": "center-inverse", "props": { "align": "center", "tone": "inverse", "heading": "Fast by default", "body": "Static HTML and one stylesheet.", "actions": [{ "label": "See the numbers", "href": "/performance/" }], "image": { "src": "/kit-fixtures/photo-4.svg", "alt": "", "width": 1200, "height": 800 } } },
|
|
5
|
+
{ "name": "accent", "props": { "tone": "accent", "eyebrow": "New", "heading": "Now with search", "body": "Every page is indexed at build time.", "image": { "src": "/kit-fixtures/photo-5.svg", "alt": "", "width": 1200, "height": 800 } } }
|
|
6
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "split",
|
|
3
|
+
"name": "Split",
|
|
4
|
+
"description": "An image beside a block of text — eyebrow, heading, a markdown body and buttons — on either side, stacked on phones. The media-and-text section of a landing page (Gutenberg media-text, a two-column Elementor container with an image, Divi's image + text row).",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": [
|
|
8
|
+
"Button.astro",
|
|
9
|
+
"KitImage.astro",
|
|
10
|
+
"Section.astro",
|
|
11
|
+
"types.ts"
|
|
12
|
+
],
|
|
13
|
+
"variants": {
|
|
14
|
+
"imageSide": {
|
|
15
|
+
"options": [
|
|
16
|
+
"start",
|
|
17
|
+
"end"
|
|
18
|
+
],
|
|
19
|
+
"default": "end",
|
|
20
|
+
"description": "Which side the image takes on wide screens: start (left in left-to-right text) or end. Phones always show it after the text."
|
|
21
|
+
},
|
|
22
|
+
"align": {
|
|
23
|
+
"options": [
|
|
24
|
+
"start",
|
|
25
|
+
"center"
|
|
26
|
+
],
|
|
27
|
+
"default": "start",
|
|
28
|
+
"description": "Alignment of the text block."
|
|
29
|
+
},
|
|
30
|
+
"tone": {
|
|
31
|
+
"options": [
|
|
32
|
+
"default",
|
|
33
|
+
"muted",
|
|
34
|
+
"inverse",
|
|
35
|
+
"accent"
|
|
36
|
+
],
|
|
37
|
+
"default": "default",
|
|
38
|
+
"description": "Section background."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"props": {
|
|
42
|
+
"heading": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"label": "Heading",
|
|
45
|
+
"required": true,
|
|
46
|
+
"description": "The section heading."
|
|
47
|
+
},
|
|
48
|
+
"eyebrow": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"label": "Eyebrow",
|
|
51
|
+
"description": "Short line above the heading."
|
|
52
|
+
},
|
|
53
|
+
"body": {
|
|
54
|
+
"type": "markdown",
|
|
55
|
+
"label": "Text",
|
|
56
|
+
"description": "The text beside the image, as markdown: paragraphs, emphasis, links, lists."
|
|
57
|
+
},
|
|
58
|
+
"actions": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"label": "Buttons",
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"fields": {
|
|
64
|
+
"label": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"label": "Label",
|
|
67
|
+
"required": true,
|
|
68
|
+
"description": "The button text."
|
|
69
|
+
},
|
|
70
|
+
"href": {
|
|
71
|
+
"type": "url",
|
|
72
|
+
"label": "Link",
|
|
73
|
+
"required": true,
|
|
74
|
+
"description": "Where the button goes: a page on this site or a full address."
|
|
75
|
+
},
|
|
76
|
+
"newTab": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"label": "Open in a new tab",
|
|
79
|
+
"description": "Open the link in a new tab (an external site the reader should not leave for)."
|
|
80
|
+
},
|
|
81
|
+
"style": {
|
|
82
|
+
"type": "select",
|
|
83
|
+
"label": "Style",
|
|
84
|
+
"options": [
|
|
85
|
+
"primary",
|
|
86
|
+
"secondary",
|
|
87
|
+
"ghost"
|
|
88
|
+
],
|
|
89
|
+
"description": "primary for the main action, secondary or ghost for the ones beside it."
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"description": "Buttons under the text, in order (Elementor button, Gutenberg buttons, Divi button)."
|
|
94
|
+
},
|
|
95
|
+
"image": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"label": "Image",
|
|
98
|
+
"fields": {
|
|
99
|
+
"src": {
|
|
100
|
+
"type": "image",
|
|
101
|
+
"label": "Image",
|
|
102
|
+
"required": true,
|
|
103
|
+
"description": "The image file."
|
|
104
|
+
},
|
|
105
|
+
"alt": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"label": "Image description",
|
|
108
|
+
"description": "What the image shows, for readers who cannot see it; empty when it is decoration."
|
|
109
|
+
},
|
|
110
|
+
"width": {
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"label": "Width",
|
|
113
|
+
"description": "Width in pixels, as uploaded."
|
|
114
|
+
},
|
|
115
|
+
"height": {
|
|
116
|
+
"type": "integer",
|
|
117
|
+
"label": "Height",
|
|
118
|
+
"description": "Height in pixels, as uploaded."
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"description": "The image beside the text. Width and height, when known, let the site reserve its space."
|
|
122
|
+
},
|
|
123
|
+
"id": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"content": false,
|
|
126
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"slots": {},
|
|
130
|
+
"js": "none",
|
|
131
|
+
"dependencies": {},
|
|
132
|
+
"sources": {
|
|
133
|
+
"gutenberg": [
|
|
134
|
+
"core/media-text"
|
|
135
|
+
],
|
|
136
|
+
"elementor": [
|
|
137
|
+
"elementor/image-box"
|
|
138
|
+
],
|
|
139
|
+
"divi": [
|
|
140
|
+
"divi/et_pb_image"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"a11y": "The section is labelled by its heading. The image is a separate figure in reading order after the text; an empty description marks a decorative one.",
|
|
144
|
+
"pattern": {
|
|
145
|
+
"summary": "image beside heading + text",
|
|
146
|
+
"signals": [
|
|
147
|
+
"2 columns",
|
|
148
|
+
"1 image",
|
|
149
|
+
"heading",
|
|
150
|
+
"text",
|
|
151
|
+
"0–2 buttons"
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A row of figures, each with a short label, under an optional heading and
|
|
3
|
+
// intro. Figures are static text: no counting animation.
|
|
4
|
+
import { tv } from 'tailwind-variants'
|
|
5
|
+
import Section, { type Tone } from '../_shared/Section.astro'
|
|
6
|
+
|
|
7
|
+
const stats = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
list: 'grid grid-cols-2 gap-6 md:gap-8',
|
|
10
|
+
item: 'flex flex-col-reverse gap-1',
|
|
11
|
+
value: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight tabular-nums md:text-5xl',
|
|
12
|
+
label: 'text-sm opacity-80',
|
|
13
|
+
},
|
|
14
|
+
variants: {
|
|
15
|
+
columns: {
|
|
16
|
+
'2': { list: 'md:grid-cols-2' },
|
|
17
|
+
'3': { list: 'md:grid-cols-3' },
|
|
18
|
+
'4': { list: 'md:grid-cols-4' },
|
|
19
|
+
},
|
|
20
|
+
style: {
|
|
21
|
+
plain: {},
|
|
22
|
+
card: { item: 'rounded-[var(--radius-card)] bg-surface p-6 text-ink ring-1 ring-line/60' },
|
|
23
|
+
},
|
|
24
|
+
// accent/accent-ink is only ~5:1 — dimming text on it drops it under 4.5:1 (WCAG 1.4.3).
|
|
25
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: { label: 'opacity-100' } },
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: { columns: '3', style: 'plain', tone: 'default' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
interface Item {
|
|
31
|
+
value: string
|
|
32
|
+
label: string
|
|
33
|
+
prefix?: string | undefined
|
|
34
|
+
suffix?: string | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
heading?: string | undefined
|
|
39
|
+
intro?: string | undefined
|
|
40
|
+
items: Item[]
|
|
41
|
+
columns?: '2' | '3' | '4' | undefined
|
|
42
|
+
style?: 'plain' | 'card' | undefined
|
|
43
|
+
tone?: Tone | undefined
|
|
44
|
+
id?: string | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { heading, intro, items, columns, style, tone, id } = Astro.props
|
|
48
|
+
const s = stats({ columns, style, tone })
|
|
49
|
+
const headingId = heading && id ? `${id}-title` : undefined
|
|
50
|
+
---
|
|
51
|
+
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
52
|
+
{(heading || intro) && (
|
|
53
|
+
<header class="mb-10 flex max-w-2xl flex-col gap-3">
|
|
54
|
+
{heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
|
|
55
|
+
{intro && <p class:list={['text-lg', tone === 'accent' ? undefined : 'opacity-80']} data-kit-text>{intro}</p>}
|
|
56
|
+
</header>
|
|
57
|
+
)}
|
|
58
|
+
<dl class={s.list()}>
|
|
59
|
+
{items.map(item => (
|
|
60
|
+
<div class={s.item()}>
|
|
61
|
+
<dt class={s.label()}>{item.label}</dt>
|
|
62
|
+
<dd class={s.value()}>{item.prefix}{item.value}{item.suffix}</dd>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</dl>
|
|
66
|
+
</Section>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "three-plain",
|
|
4
|
+
"props": {
|
|
5
|
+
"heading": "By the numbers",
|
|
6
|
+
"intro": "A decade of shipping sites for small teams.",
|
|
7
|
+
"items": [
|
|
8
|
+
{
|
|
9
|
+
"value": "12",
|
|
10
|
+
"label": "Years in business",
|
|
11
|
+
"suffix": "+"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"value": "480",
|
|
15
|
+
"label": "Clients served"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"value": "99.9",
|
|
19
|
+
"label": "Uptime",
|
|
20
|
+
"suffix": "%"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "four-card-muted",
|
|
27
|
+
"props": {
|
|
28
|
+
"items": [
|
|
29
|
+
{
|
|
30
|
+
"value": "12",
|
|
31
|
+
"label": "Years in business",
|
|
32
|
+
"suffix": "+"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"value": "480",
|
|
36
|
+
"label": "Clients served"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"value": "99.9",
|
|
40
|
+
"label": "Uptime",
|
|
41
|
+
"suffix": "%"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"value": "2",
|
|
45
|
+
"label": "Revenue handled",
|
|
46
|
+
"prefix": "$",
|
|
47
|
+
"suffix": "M"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"columns": "4",
|
|
51
|
+
"style": "card",
|
|
52
|
+
"tone": "muted"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "two-inverse",
|
|
57
|
+
"props": {
|
|
58
|
+
"heading": "Results",
|
|
59
|
+
"items": [
|
|
60
|
+
{
|
|
61
|
+
"value": "12",
|
|
62
|
+
"label": "Years in business",
|
|
63
|
+
"suffix": "+"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"value": "480",
|
|
67
|
+
"label": "Clients served"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"columns": "2",
|
|
71
|
+
"tone": "inverse"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "card-accent",
|
|
76
|
+
"props": {
|
|
77
|
+
"heading": "Our reach",
|
|
78
|
+
"items": [
|
|
79
|
+
{
|
|
80
|
+
"value": "12",
|
|
81
|
+
"label": "Years in business",
|
|
82
|
+
"suffix": "+"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"value": "480",
|
|
86
|
+
"label": "Clients served"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"value": "99.9",
|
|
90
|
+
"label": "Uptime",
|
|
91
|
+
"suffix": "%"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"style": "card",
|
|
95
|
+
"tone": "accent"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|