@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.
Files changed (66) hide show
  1. package/catalog.json +1512 -167
  2. package/components/_shared/KitImage.astro +10 -4
  3. package/components/_shared/image.ts +14 -0
  4. package/components/_shared/types.ts +2 -1
  5. package/components/announcement/Announcement.astro +53 -0
  6. package/components/announcement/fixtures.json +6 -0
  7. package/components/announcement/meta.json +53 -0
  8. package/components/card-grid/CardGrid.astro +5 -3
  9. package/components/card-grid/meta.json +25 -11
  10. package/components/consent/Consent.astro +87 -0
  11. package/components/consent/fixtures.json +4 -0
  12. package/components/consent/meta.json +67 -0
  13. package/components/contact-details/ContactDetails.astro +74 -0
  14. package/components/contact-details/fixtures.json +4 -0
  15. package/components/contact-details/meta.json +105 -0
  16. package/components/contact-form/ContactForm.astro +22 -6
  17. package/components/contact-form/fixtures.json +2 -1
  18. package/components/contact-form/meta.json +7 -6
  19. package/components/cta/meta.json +4 -4
  20. package/components/faq/meta.json +6 -6
  21. package/components/feature-list/FeatureList.astro +85 -0
  22. package/components/feature-list/fixtures.json +129 -0
  23. package/components/feature-list/meta.json +118 -0
  24. package/components/footer/Footer.astro +14 -6
  25. package/components/footer/fixtures.json +3 -1
  26. package/components/footer/meta.json +12 -0
  27. package/components/gallery/Gallery.astro +7 -3
  28. package/components/gallery/meta.json +6 -6
  29. package/components/header/Header.astro +19 -8
  30. package/components/header/fixtures.json +2 -1
  31. package/components/hero/meta.json +30 -8
  32. package/components/logo-cloud/LogoCloud.astro +69 -0
  33. package/components/logo-cloud/fixtures.json +139 -0
  34. package/components/logo-cloud/meta.json +107 -0
  35. package/components/newsletter/meta.json +12 -0
  36. package/components/pricing/Pricing.astro +94 -0
  37. package/components/pricing/fixtures.json +6 -0
  38. package/components/pricing/meta.json +133 -0
  39. package/components/prose/Prose.astro +7 -3
  40. package/components/prose/fixtures.json +2 -1
  41. package/components/prose/meta.json +5 -2
  42. package/components/slider/Slider.astro +15 -3
  43. package/components/slider/meta.json +8 -8
  44. package/components/split/Split.astro +64 -0
  45. package/components/split/fixtures.json +6 -0
  46. package/components/split/meta.json +154 -0
  47. package/components/stats/Stats.astro +66 -0
  48. package/components/stats/fixtures.json +98 -0
  49. package/components/stats/meta.json +113 -0
  50. package/components/steps/Steps.astro +77 -0
  51. package/components/steps/fixtures.json +5 -0
  52. package/components/steps/meta.json +105 -0
  53. package/components/team/Team.astro +82 -0
  54. package/components/team/fixtures.json +5 -0
  55. package/components/team/meta.json +116 -0
  56. package/components/testimonial/Testimonial.astro +5 -3
  57. package/components/testimonial/meta.json +21 -20
  58. package/dist/index.d.mts +115 -2
  59. package/dist/index.d.mts.map +1 -1
  60. package/dist/index.mjs +175 -3
  61. package/dist/index.mjs.map +1 -1
  62. package/mapping/classic.json +51 -6
  63. package/mapping/divi.json +359 -9
  64. package/mapping/elementor.json +581 -8
  65. package/mapping/gutenberg.json +420 -4
  66. package/package.json +2 -2
@@ -53,16 +53,27 @@ interface Props {
53
53
 
54
54
  const { siteName, tagline, brandSize, linkNav, logo, showName = true, homeHref = '/', items, action, navLabel, menuLabel, closeLabel, layout, tone, position } = Astro.props
55
55
  const { root, inner, brand } = header({ layout, tone, position })
56
+ // A tagline under the name keeps its line on a phone, as themes print it: the navigation wraps below.
57
+ const wrap = tagline ? 'flex-wrap md:flex-nowrap' : undefined
56
58
  ---
57
59
  <header class={root()}>
58
- <div class={inner()}>
59
- <div class="flex flex-col gap-1">
60
- <a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName} style={brandSize ? `font-size: ${brandSize}` : undefined} data-kit-brand>
61
- {logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
62
- {showName && <span>{siteName}</span>}
63
- </a>
64
- {tagline && <p class="opacity-75" data-kit-tagline>{tagline}</p>}
65
- </div>
60
+ <div class={inner({ class: wrap })}>
61
+ {tagline
62
+ ? (
63
+ <div class="flex min-w-0 basis-full flex-col gap-1 md:basis-auto">
64
+ <a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName} style={brandSize ? `font-size: ${brandSize}` : undefined} data-kit-brand>
65
+ {logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
66
+ {showName && <span>{siteName}</span>}
67
+ </a>
68
+ <p class="opacity-75" data-kit-tagline>{tagline}</p>
69
+ </div>
70
+ )
71
+ : (
72
+ <a href={homeHref} class={brand()} aria-label={showName ? undefined : siteName} style={brandSize ? `font-size: ${brandSize}` : undefined} data-kit-brand>
73
+ {logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
74
+ {showName && <span>{siteName}</span>}
75
+ </a>
76
+ )}
66
77
  <div class:list={['flex items-center gap-4', layout === 'centered' ? 'justify-center' : 'ml-auto']}>
67
78
  <Nav items={items} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" color={linkNav ? 'link' : 'current'} align={layout === 'centered' ? 'center' : 'end'} />
68
79
  {action && <Button action={action} size="sm" class="hidden md:inline-flex" />}
@@ -121,5 +121,6 @@
121
121
  }
122
122
  ]
123
123
  }
124
- }
124
+ },
125
+ { "name": "tagline", "props": { "siteName": "Golden Agency", "tagline": "Websites that sell what you make", "items": [{ "label": "Home", "href": "/" }, { "label": "Services", "href": "/services/" }, { "label": "Contact", "href": "/contact/" }], "menuLabel": "Menu", "closeLabel": "Close" } }
125
126
  ]
@@ -42,6 +42,7 @@
42
42
  "props": {
43
43
  "heading": {
44
44
  "type": "string",
45
+ "label": "Heading",
45
46
  "required": true,
46
47
  "description": "The main heading (Cover/Group heading, Elementor heading, Divi header title)."
47
48
  },
@@ -52,39 +53,51 @@
52
53
  "2"
53
54
  ],
54
55
  "default": "1",
56
+ "content": false,
55
57
  "description": "Heading level: 1 when the hero opens the page, 2 when the page already has an h1."
56
58
  },
57
59
  "eyebrow": {
58
60
  "type": "string",
61
+ "label": "Eyebrow",
59
62
  "description": "Short line above the heading."
60
63
  },
61
64
  "lead": {
62
65
  "type": "text",
66
+ "label": "Lead",
63
67
  "description": "Paragraph under the heading."
64
68
  },
65
69
  "actions": {
66
70
  "type": "array",
71
+ "label": "Buttons",
67
72
  "items": {
68
73
  "type": "object",
69
74
  "fields": {
70
75
  "label": {
71
76
  "type": "string",
72
- "required": true
77
+ "label": "Label",
78
+ "required": true,
79
+ "description": "The button text."
73
80
  },
74
81
  "href": {
75
82
  "type": "url",
76
- "required": true
83
+ "label": "Link",
84
+ "required": true,
85
+ "description": "Where the button goes: a page on this site or a full address."
77
86
  },
78
87
  "newTab": {
79
- "type": "boolean"
88
+ "type": "boolean",
89
+ "label": "Open in a new tab",
90
+ "description": "Open the link in a new tab (an external site the reader should not leave for)."
80
91
  },
81
92
  "style": {
82
93
  "type": "select",
94
+ "label": "Style",
83
95
  "options": [
84
96
  "primary",
85
97
  "secondary",
86
98
  "ghost"
87
- ]
99
+ ],
100
+ "description": "primary for the main action, secondary or ghost for the ones beside it."
88
101
  }
89
102
  }
90
103
  },
@@ -92,26 +105,35 @@
92
105
  },
93
106
  "image": {
94
107
  "type": "object",
108
+ "label": "Image",
95
109
  "fields": {
96
110
  "src": {
97
111
  "type": "image",
98
- "required": true
112
+ "label": "Image",
113
+ "required": true,
114
+ "description": "The image file."
99
115
  },
100
116
  "alt": {
101
117
  "type": "string",
102
- "required": true
118
+ "label": "Image description",
119
+ "description": "What the image shows, for readers who cannot see it; empty when it is decoration."
103
120
  },
104
121
  "width": {
105
- "type": "integer"
122
+ "type": "integer",
123
+ "label": "Width",
124
+ "description": "Width in pixels, as uploaded."
106
125
  },
107
126
  "height": {
108
- "type": "integer"
127
+ "type": "integer",
128
+ "label": "Height",
129
+ "description": "Height in pixels, as uploaded."
109
130
  }
110
131
  },
111
132
  "description": "Hero image; loaded first (the page's largest image)."
112
133
  },
113
134
  "id": {
114
135
  "type": "string",
136
+ "content": false,
115
137
  "description": "Anchor id of the section; the heading gets `<id>-title`."
116
138
  }
117
139
  },
@@ -0,0 +1,69 @@
1
+ ---
2
+ // A grid of client or partner logos, each optionally linked, under an
3
+ // optional heading.
4
+ import { tv } from 'tailwind-variants'
5
+ import KitImage from '../_shared/KitImage.astro'
6
+ import Section, { type Tone } from '../_shared/Section.astro'
7
+ import { imageOf, type ImageSource } from '../_shared/image'
8
+
9
+ const cloud = tv({
10
+ slots: {
11
+ heading: 'mb-8 text-center text-sm font-semibold tracking-wide uppercase opacity-80',
12
+ list: 'grid grid-cols-2 items-center gap-x-8 gap-y-10 sm:grid-cols-3',
13
+ item: 'flex items-center justify-center',
14
+ logo: 'h-10 w-auto max-w-full object-contain transition duration-200',
15
+ },
16
+ variants: {
17
+ columns: {
18
+ '2': { list: 'sm:grid-cols-2' },
19
+ '3': { list: 'md:grid-cols-3' },
20
+ '4': { list: 'md:grid-cols-4' },
21
+ },
22
+ style: {
23
+ plain: {},
24
+ // Colour comes back on hover and on keyboard focus of the logo's link.
25
+ grayscale: { logo: 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0 group-focus-visible:opacity-100 group-focus-visible:grayscale-0' },
26
+ },
27
+ tone: { default: {}, muted: {}, inverse: {}, accent: { heading: 'opacity-100' } },
28
+ },
29
+ defaultVariants: { columns: '4', style: 'plain', tone: 'default' },
30
+ })
31
+
32
+ interface Item {
33
+ /** The logo's address (or, until 0.7, an `ImageInput`). */
34
+ image: ImageSource
35
+ /** The company's name, read in place of the logo. */
36
+ imageAlt: string
37
+ href?: string | undefined
38
+ }
39
+
40
+ interface Props {
41
+ heading?: string | undefined
42
+ items: Item[]
43
+ columns?: '2' | '3' | '4' | undefined
44
+ style?: 'plain' | 'grayscale' | undefined
45
+ tone?: Tone | undefined
46
+ id?: string | undefined
47
+ }
48
+
49
+ const { heading, items, columns, style, tone, id } = Astro.props
50
+ const s = cloud({ columns, style, tone })
51
+ const headingId = heading && id ? `${id}-title` : undefined
52
+ ---
53
+ <Section tone={tone} id={id} aria-labelledby={headingId}>
54
+ {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
55
+ <ul class={s.list()}>
56
+ {items.map(item => {
57
+ const image = imageOf(item.image, item.imageAlt)
58
+ if (!image) return null
59
+ const logo = <KitImage image={image} sizes="160px" class={s.logo()} />
60
+ return (
61
+ <li class={s.item()}>
62
+ {item.href
63
+ ? <a href={item.href} class="group rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4">{logo}</a>
64
+ : <span class="group">{logo}</span>}
65
+ </li>
66
+ )
67
+ })}
68
+ </ul>
69
+ </Section>
@@ -0,0 +1,139 @@
1
+ [
2
+ {
3
+ "name": "four-plain",
4
+ "props": {
5
+ "heading": "Trusted by",
6
+ "items": [
7
+ {
8
+ "image": "/kit-fixtures/logo.svg",
9
+ "imageAlt": "Company 1",
10
+ "href": "https://example.com"
11
+ },
12
+ {
13
+ "image": "/kit-fixtures/logo.svg",
14
+ "imageAlt": "Company 2"
15
+ },
16
+ {
17
+ "image": "/kit-fixtures/logo.svg",
18
+ "imageAlt": "Company 3",
19
+ "href": "https://example.com"
20
+ },
21
+ {
22
+ "image": "/kit-fixtures/logo.svg",
23
+ "imageAlt": "Company 4"
24
+ },
25
+ {
26
+ "image": "/kit-fixtures/logo.svg",
27
+ "imageAlt": "Company 5",
28
+ "href": "https://example.com"
29
+ },
30
+ {
31
+ "image": "/kit-fixtures/logo.svg",
32
+ "imageAlt": "Company 6"
33
+ },
34
+ {
35
+ "image": "/kit-fixtures/logo.svg",
36
+ "imageAlt": "Company 7",
37
+ "href": "https://example.com"
38
+ },
39
+ {
40
+ "image": "/kit-fixtures/logo.svg",
41
+ "imageAlt": "Company 8"
42
+ }
43
+ ]
44
+ }
45
+ },
46
+ {
47
+ "name": "three-grayscale-muted",
48
+ "props": {
49
+ "heading": "Our partners",
50
+ "items": [
51
+ {
52
+ "image": "/kit-fixtures/logo.svg",
53
+ "imageAlt": "Company 1",
54
+ "href": "https://example.com"
55
+ },
56
+ {
57
+ "image": "/kit-fixtures/logo.svg",
58
+ "imageAlt": "Company 2"
59
+ },
60
+ {
61
+ "image": "/kit-fixtures/logo.svg",
62
+ "imageAlt": "Company 3",
63
+ "href": "https://example.com"
64
+ },
65
+ {
66
+ "image": "/kit-fixtures/logo.svg",
67
+ "imageAlt": "Company 4"
68
+ },
69
+ {
70
+ "image": "/kit-fixtures/logo.svg",
71
+ "imageAlt": "Company 5",
72
+ "href": "https://example.com"
73
+ },
74
+ {
75
+ "image": "/kit-fixtures/logo.svg",
76
+ "imageAlt": "Company 6"
77
+ }
78
+ ],
79
+ "columns": "3",
80
+ "style": "grayscale",
81
+ "tone": "muted"
82
+ }
83
+ },
84
+ {
85
+ "name": "two-inverse",
86
+ "props": {
87
+ "items": [
88
+ {
89
+ "image": "/kit-fixtures/logo.svg",
90
+ "imageAlt": "Company 1",
91
+ "href": "https://example.com"
92
+ },
93
+ {
94
+ "image": "/kit-fixtures/logo.svg",
95
+ "imageAlt": "Company 2"
96
+ },
97
+ {
98
+ "image": "/kit-fixtures/logo.svg",
99
+ "imageAlt": "Company 3",
100
+ "href": "https://example.com"
101
+ },
102
+ {
103
+ "image": "/kit-fixtures/logo.svg",
104
+ "imageAlt": "Company 4"
105
+ }
106
+ ],
107
+ "columns": "2",
108
+ "tone": "inverse"
109
+ }
110
+ },
111
+ {
112
+ "name": "accent",
113
+ "props": {
114
+ "heading": "Featured in",
115
+ "items": [
116
+ {
117
+ "image": "/kit-fixtures/logo.svg",
118
+ "imageAlt": "Company 1",
119
+ "href": "https://example.com"
120
+ },
121
+ {
122
+ "image": "/kit-fixtures/logo.svg",
123
+ "imageAlt": "Company 2"
124
+ },
125
+ {
126
+ "image": "/kit-fixtures/logo.svg",
127
+ "imageAlt": "Company 3",
128
+ "href": "https://example.com"
129
+ },
130
+ {
131
+ "image": "/kit-fixtures/logo.svg",
132
+ "imageAlt": "Company 4"
133
+ }
134
+ ],
135
+ "tone": "accent",
136
+ "style": "grayscale"
137
+ }
138
+ }
139
+ ]
@@ -0,0 +1,107 @@
1
+ {
2
+ "id": "logo-cloud",
3
+ "name": "Logo cloud",
4
+ "description": "A grid of client or partner logos, each optionally linked, under an optional heading (\"Trusted by\").",
5
+ "category": "section",
6
+ "uses": [],
7
+ "shared": [
8
+ "KitImage.astro",
9
+ "Section.astro",
10
+ "image.ts",
11
+ "types.ts"
12
+ ],
13
+ "variants": {
14
+ "columns": {
15
+ "options": [
16
+ "2",
17
+ "3",
18
+ "4"
19
+ ],
20
+ "default": "4",
21
+ "description": "Logos per row on wide screens; two on phones, three on tablets."
22
+ },
23
+ "style": {
24
+ "options": [
25
+ "plain",
26
+ "grayscale"
27
+ ],
28
+ "default": "plain",
29
+ "description": "plain: logos in their own colours. grayscale: muted grey, full colour on hover or focus."
30
+ },
31
+ "tone": {
32
+ "options": [
33
+ "default",
34
+ "muted",
35
+ "inverse",
36
+ "accent"
37
+ ],
38
+ "default": "default",
39
+ "description": "Section background."
40
+ }
41
+ },
42
+ "props": {
43
+ "heading": {
44
+ "type": "string",
45
+ "label": "Heading",
46
+ "description": "Line above the logos (Trusted by, Our partners)."
47
+ },
48
+ "items": {
49
+ "type": "array",
50
+ "label": "Logos",
51
+ "required": true,
52
+ "items": {
53
+ "type": "object",
54
+ "fields": {
55
+ "image": {
56
+ "type": "image",
57
+ "label": "Logo",
58
+ "required": true,
59
+ "description": "The logo file."
60
+ },
61
+ "imageAlt": {
62
+ "type": "string",
63
+ "label": "Name",
64
+ "required": true,
65
+ "description": "The company's name, read in place of the logo."
66
+ },
67
+ "href": {
68
+ "type": "url",
69
+ "label": "Link",
70
+ "description": "The company's site, when the logo links there."
71
+ }
72
+ }
73
+ },
74
+ "description": "The logos, in order (a Gutenberg gallery or columns of logos, Elementor image carousel/gallery of logos, Divi images)."
75
+ },
76
+ "id": {
77
+ "type": "string",
78
+ "content": false,
79
+ "description": "Anchor id of the section; the heading gets `<id>-title`."
80
+ }
81
+ },
82
+ "slots": {},
83
+ "js": "none",
84
+ "dependencies": {},
85
+ "sources": {
86
+ "gutenberg": [
87
+ "core/gallery"
88
+ ],
89
+ "elementor": [
90
+ "elementor/image-gallery",
91
+ "elementor/image-carousel"
92
+ ],
93
+ "divi": [
94
+ "divi/et_pb_gallery"
95
+ ]
96
+ },
97
+ "a11y": "A list of logos; each logo's text is the company's name, and a linked logo is one link named by it. The section is labelled by its heading.",
98
+ "pattern": {
99
+ "summary": "row of small logos, often linked",
100
+ "signals": [
101
+ "4+ images",
102
+ "small images",
103
+ "little or no text",
104
+ "optional heading"
105
+ ]
106
+ }
107
+ }
@@ -30,11 +30,13 @@
30
30
  "props": {
31
31
  "action": {
32
32
  "type": "url",
33
+ "label": "Form address",
33
34
  "required": true,
34
35
  "description": "The provider's hosted form endpoint (Mailchimp *.list-manage.com/subscribe/post, Kit app.kit.com/forms/<n>/subscriptions, MailerLite assets.mailerlite.com/jsonp/<n>/forms/<n>/subscribe, Brevo *.sibforms.com/serve/, Buttondown buttondown.com/api/emails/embed-subscribe/). At migration the engine reads it from the source form element (`dom:form@action`) into the section's content, where it stays editable in Studio; it never passes through the fact pack or a model."
35
36
  },
36
37
  "provider": {
37
38
  "type": "select",
39
+ "label": "Email provider",
38
40
  "options": [
39
41
  "mailchimp",
40
42
  "kit",
@@ -48,44 +50,54 @@
48
50
  },
49
51
  "emailName": {
50
52
  "type": "string",
53
+ "label": "Email field name",
51
54
  "description": "Field name for the address when the provider is other or names it differently."
52
55
  },
53
56
  "askName": {
54
57
  "type": "boolean",
58
+ "label": "Ask for a name",
55
59
  "default": false,
56
60
  "description": "Ask for a first name too, when the provider takes one (the source form had a name field)."
57
61
  },
58
62
  "heading": {
59
63
  "type": "string",
64
+ "label": "Heading",
60
65
  "description": "Section heading."
61
66
  },
62
67
  "intro": {
63
68
  "type": "text",
69
+ "label": "Introduction",
64
70
  "description": "A line or two about the newsletter."
65
71
  },
66
72
  "note": {
67
73
  "type": "string",
74
+ "label": "Note",
68
75
  "description": "Small print under the form (privacy, frequency)."
69
76
  },
70
77
  "emailLabel": {
71
78
  "type": "string",
79
+ "content": false,
72
80
  "description": "ui-strings newsletter.email."
73
81
  },
74
82
  "nameLabel": {
75
83
  "type": "string",
84
+ "content": false,
76
85
  "description": "ui-strings newsletter.name."
77
86
  },
78
87
  "submitLabel": {
79
88
  "type": "string",
89
+ "content": false,
80
90
  "description": "ui-strings newsletter.submit."
81
91
  },
82
92
  "newTab": {
83
93
  "type": "boolean",
84
94
  "default": true,
95
+ "content": false,
85
96
  "description": "Open the provider's confirmation page in a new tab (the reader keeps the site)."
86
97
  },
87
98
  "id": {
88
99
  "type": "string",
100
+ "content": false,
89
101
  "description": "Section id, unique per page (two sign-ups on one page need two ids): the heading gets `<id>-title`, the inputs `<id>-email`/`<id>-name`."
90
102
  }
91
103
  },
@@ -0,0 +1,94 @@
1
+ ---
2
+ // Price plans side by side: a name, a price and what it includes, with a
3
+ // button each. The featured plan is raised and carries a badge, so it stands
4
+ // out by more than its colour. Each plan's features are a markdown list,
5
+ // rendered at build time.
6
+ import { tv } from 'tailwind-variants'
7
+ import Button from '../_shared/Button.astro'
8
+ import Section, { type Tone } from '../_shared/Section.astro'
9
+ import { renderMarkdown } from '../../../lib/markdown'
10
+
11
+ const pricing = tv({
12
+ slots: {
13
+ header: 'mx-auto mb-10 flex max-w-2xl flex-col items-center gap-3 text-center',
14
+ heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
15
+ intro: 'text-lg opacity-80',
16
+ list: 'grid items-stretch gap-6 sm:grid-cols-2',
17
+ plan: 'relative flex flex-col gap-5 rounded-card border border-line bg-surface p-6 text-ink md:p-8',
18
+ badge: 'absolute -top-3 left-6 rounded-full bg-accent px-3 py-1 text-xs font-semibold text-accent-ink',
19
+ name: 'text-lg [font-weight:var(--font-weight-heading,600)]',
20
+ price: 'flex items-baseline gap-1',
21
+ amount: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight',
22
+ period: 'text-sm opacity-75',
23
+ description: 'text-pretty opacity-80',
24
+ features: 'prose-site prose max-w-none text-current prose-ul:pl-5 prose-li:my-1',
25
+ action: 'mt-auto pt-2',
26
+ },
27
+ variants: {
28
+ columns: {
29
+ '2': { list: 'mx-auto max-w-4xl' },
30
+ '3': { list: 'lg:grid-cols-3' },
31
+ '4': { list: 'lg:grid-cols-4' },
32
+ },
33
+ tone: { default: {}, muted: { plan: 'border-transparent' }, inverse: { plan: 'border-transparent' }, accent: { intro: 'opacity-100', plan: 'border-transparent', badge: 'bg-ink text-surface' } },
34
+ },
35
+ defaultVariants: { columns: '3', tone: 'default' },
36
+ })
37
+
38
+ interface Plan {
39
+ name: string
40
+ price: string
41
+ period?: string | undefined
42
+ description?: string | undefined
43
+ /** What the plan includes, as a markdown list. */
44
+ features?: string | undefined
45
+ ctaLabel?: string | undefined
46
+ ctaHref?: string | undefined
47
+ featured?: boolean | undefined
48
+ }
49
+
50
+ interface Props {
51
+ heading?: string | undefined
52
+ intro?: string | undefined
53
+ plans: Plan[]
54
+ /** Badge on the featured plan. */
55
+ featuredLabel?: string | undefined
56
+ columns?: '2' | '3' | '4' | undefined
57
+ tone?: Tone | undefined
58
+ id?: string | undefined
59
+ }
60
+
61
+ const { heading, intro, plans, featuredLabel = 'Most popular', columns, tone, id } = Astro.props
62
+ const s = pricing({ columns, tone })
63
+ const headingId = heading && id ? `${id}-title` : undefined
64
+ const features = await Promise.all(plans.map(plan => renderMarkdown(plan.features)))
65
+ ---
66
+ {plans.length > 0 && (
67
+ <Section tone={tone} id={id} aria-labelledby={headingId}>
68
+ {(heading || intro) && (
69
+ <header class={s.header()}>
70
+ {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
71
+ {intro && <p class={s.intro()} data-kit-text>{intro}</p>}
72
+ </header>
73
+ )}
74
+ <ul class={s.list()}>
75
+ {plans.map((plan, i) => (
76
+ <li class={s.plan({ class: plan.featured ? 'ring-2 ring-accent shadow-lg md:-my-2 md:py-10' : undefined })}>
77
+ {plan.featured && <p class={s.badge()}>{featuredLabel}</p>}
78
+ <h3 class={s.name()}>{plan.name}</h3>
79
+ <p class={s.price()}>
80
+ <span class={s.amount()}>{plan.price}</span>
81
+ {plan.period && <span class={s.period()}>{plan.period}</span>}
82
+ </p>
83
+ {plan.description && <p class={s.description()} data-kit-text>{plan.description}</p>}
84
+ {features[i] && <div class={s.features()} set:html={features[i]} />}
85
+ {plan.ctaLabel && plan.ctaHref && (
86
+ <div class={s.action()}>
87
+ <Button action={{ label: plan.ctaLabel, href: plan.ctaHref, style: plan.featured ? 'primary' : 'ghost' }} class="w-full" />
88
+ </div>
89
+ )}
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ </Section>
94
+ )}
@@ -0,0 +1,6 @@
1
+ [
2
+ {"name": "three-featured", "props": {"id": "pricing", "heading": "Pricing", "intro": "Start free, upgrade when the team grows.", "plans": [{"name": "Free", "price": "$0", "period": "/month", "description": "For one person and one site.", "features": "- 1 project\n- Git-backed content\n- Community support", "ctaLabel": "Start free", "ctaHref": "/signup/"}, {"name": "Pro", "price": "$12", "period": "/month", "description": "For freelancers.", "features": "- 5 projects\n- Media library\n- Email support", "ctaLabel": "Choose Pro", "ctaHref": "/signup/?plan=pro", "featured": true}, {"name": "Team", "price": "$29", "period": "/month", "description": "For studios and teams.", "features": "- Unlimited projects\n- Reviews and roles\n- Priority support", "ctaLabel": "Choose Team", "ctaHref": "/signup/?plan=team"}]}},
3
+ {"name": "two-muted", "props": {"columns": "2", "tone": "muted", "featuredLabel": "Best value", "plans": [{"name": "Monthly", "price": "€15", "period": "per month", "features": "- Cancel anytime", "ctaLabel": "Subscribe", "ctaHref": "/buy/monthly/"}, {"name": "Yearly", "price": "€150", "period": "per year", "features": "- Two months free\n- **Priority** support", "ctaLabel": "Subscribe", "ctaHref": "/buy/yearly/", "featured": true}]}},
4
+ {"name": "four-inverse", "props": {"columns": "4", "tone": "inverse", "heading": "Plans", "plans": [{"name": "Solo", "price": "$0", "features": "- 1 site"}, {"name": "Pro", "price": "$12", "period": "/mo", "features": "- 5 sites", "featured": true, "ctaLabel": "Buy", "ctaHref": "/buy/"}, {"name": "Team", "price": "$29", "period": "/mo", "features": "- 20 sites"}, {"name": "Enterprise", "price": "On request", "description": "Custom terms.", "ctaLabel": "Contact us", "ctaHref": "/contact/"}]}},
5
+ {"name": "accent", "props": {"tone": "accent", "heading": "One price", "plans": [{"name": "Studio", "price": "$49", "period": "once", "features": "- Everything", "ctaLabel": "Buy now", "ctaHref": "/buy/", "featured": true}, {"name": "Support", "price": "$9", "period": "/month", "features": "- Updates and help", "ctaLabel": "Add support", "ctaHref": "/buy/support/"}]}}
6
+ ]