@contentrain/astro-kit 0.30.0 → 0.32.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 (67) hide show
  1. package/README.md +25 -2
  2. package/catalog.json +3305 -83
  3. package/components/_shared/Section.astro +18 -3
  4. package/components/_shared/measured.ts +152 -0
  5. package/components/_shared/types.ts +40 -0
  6. package/components/band/Band.astro +132 -0
  7. package/components/band/fixtures.json +8 -0
  8. package/components/band/meta.json +323 -0
  9. package/components/card-grid/CardGrid.astro +7 -3
  10. package/components/card-grid/fixtures.json +2 -1
  11. package/components/card-grid/meta.json +111 -0
  12. package/components/contact-details/ContactDetails.astro +7 -2
  13. package/components/contact-details/meta.json +113 -1
  14. package/components/contact-form/ContactForm.astro +48 -12
  15. package/components/contact-form/fixtures.json +169 -9
  16. package/components/contact-form/meta.json +283 -15
  17. package/components/cta/Cta.astro +52 -12
  18. package/components/cta/fixtures.json +5 -1
  19. package/components/cta/meta.json +11 -2
  20. package/components/faq/Faq.astro +60 -17
  21. package/components/faq/fixtures.json +75 -0
  22. package/components/faq/meta.json +8 -2
  23. package/components/feature-list/FeatureList.astro +34 -6
  24. package/components/feature-list/fixtures.json +2 -1
  25. package/components/feature-list/meta.json +125 -1
  26. package/components/gallery/Gallery.astro +47 -13
  27. package/components/gallery/fixtures.json +275 -0
  28. package/components/gallery/meta.json +266 -14
  29. package/components/hero/Hero.astro +14 -8
  30. package/components/hero/fixtures.json +2 -1
  31. package/components/hero/meta.json +111 -0
  32. package/components/logo-cloud/LogoCloud.astro +85 -21
  33. package/components/logo-cloud/fixtures.json +239 -0
  34. package/components/logo-cloud/meta.json +145 -5
  35. package/components/newsletter/Newsletter.astro +5 -1
  36. package/components/newsletter/meta.json +113 -1
  37. package/components/post-card/PostCard.astro +4 -2
  38. package/components/post-card/meta.json +2 -1
  39. package/components/post-list/PostList.astro +114 -0
  40. package/components/post-list/fixtures.json +387 -0
  41. package/components/post-list/meta.json +308 -0
  42. package/components/pricing/Pricing.astro +128 -24
  43. package/components/pricing/fixtures.json +288 -4
  44. package/components/pricing/meta.json +188 -1
  45. package/components/split/Split.astro +76 -14
  46. package/components/split/fixtures.json +8 -1
  47. package/components/split/meta.json +226 -4
  48. package/components/stats/Stats.astro +42 -10
  49. package/components/stats/fixtures.json +173 -0
  50. package/components/stats/meta.json +153 -5
  51. package/components/steps/Steps.astro +36 -8
  52. package/components/steps/fixtures.json +217 -5
  53. package/components/steps/meta.json +157 -3
  54. package/components/tabs/Tabs.astro +5 -2
  55. package/components/tabs/meta.json +2 -1
  56. package/components/team/Team.astro +52 -8
  57. package/components/team/fixtures.json +217 -4
  58. package/components/team/meta.json +168 -3
  59. package/components/team/team.ts +9 -0
  60. package/components/testimonial/Testimonial.astro +71 -21
  61. package/components/testimonial/fixtures.json +73 -0
  62. package/components/testimonial/meta.json +167 -3
  63. package/mapping/classic.json +7 -1
  64. package/mapping/divi.json +21 -4
  65. package/mapping/elementor.json +33 -8
  66. package/mapping/gutenberg.json +20 -4
  67. package/package.json +2 -2
@@ -0,0 +1,308 @@
1
+ {
2
+ "id": "post-list",
3
+ "name": "Post list",
4
+ "description": "A list of posts as cards: a grid, stacked rows, a featured first post over a grid, or a compact title-and-date list. The posts come from the site's own collection; the switches decide which parts of a card show.",
5
+ "category": "section",
6
+ "uses": [
7
+ "post-card"
8
+ ],
9
+ "shared": [
10
+ "Section.astro",
11
+ "measured.ts",
12
+ "types.ts"
13
+ ],
14
+ "variants": {
15
+ "layout": {
16
+ "options": [
17
+ "grid",
18
+ "list",
19
+ "featured",
20
+ "compact"
21
+ ],
22
+ "default": "grid",
23
+ "description": "grid: cards in columns. list: stacked horizontal cards. featured: the first post large over a grid. compact: title and date only."
24
+ },
25
+ "columns": {
26
+ "options": [
27
+ "2",
28
+ "3",
29
+ "4"
30
+ ],
31
+ "default": "3",
32
+ "description": "Cards per row on wide screens (grid and featured)."
33
+ }
34
+ },
35
+ "props": {
36
+ "heading": {
37
+ "type": "string",
38
+ "label": "Heading",
39
+ "description": "Section heading above the posts."
40
+ },
41
+ "intro": {
42
+ "type": "text",
43
+ "label": "Intro",
44
+ "description": "A line or two under the heading."
45
+ },
46
+ "posts": {
47
+ "type": "array",
48
+ "label": "Posts",
49
+ "required": true,
50
+ "content": false,
51
+ "description": "The posts to show, newest first: from the posts collection (the query SDK), never copied from a page.",
52
+ "items": {
53
+ "type": "object",
54
+ "fields": {
55
+ "title": {
56
+ "type": "string",
57
+ "required": true
58
+ },
59
+ "href": {
60
+ "type": "url",
61
+ "required": true
62
+ },
63
+ "excerpt": {
64
+ "type": "text"
65
+ },
66
+ "date": {
67
+ "type": "datetime"
68
+ },
69
+ "image": {
70
+ "type": "object",
71
+ "fields": {
72
+ "src": {
73
+ "type": "image",
74
+ "required": true
75
+ },
76
+ "alt": {
77
+ "type": "string",
78
+ "required": true
79
+ },
80
+ "width": {
81
+ "type": "integer"
82
+ },
83
+ "height": {
84
+ "type": "integer"
85
+ }
86
+ }
87
+ },
88
+ "category": {
89
+ "type": "object",
90
+ "fields": {
91
+ "label": {
92
+ "type": "string",
93
+ "required": true
94
+ },
95
+ "href": {
96
+ "type": "url",
97
+ "required": true
98
+ }
99
+ }
100
+ },
101
+ "author": {
102
+ "type": "string"
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "showImage": {
108
+ "type": "boolean",
109
+ "default": true,
110
+ "content": false,
111
+ "description": "Show each post's cover."
112
+ },
113
+ "showExcerpt": {
114
+ "type": "boolean",
115
+ "default": true,
116
+ "content": false,
117
+ "description": "Show each post's excerpt."
118
+ },
119
+ "showAuthor": {
120
+ "type": "boolean",
121
+ "default": true,
122
+ "content": false,
123
+ "description": "Show each post's author."
124
+ },
125
+ "showCategory": {
126
+ "type": "boolean",
127
+ "default": true,
128
+ "content": false,
129
+ "description": "Show each post's category."
130
+ },
131
+ "excerptLines": {
132
+ "type": "integer",
133
+ "min": 2,
134
+ "max": 4,
135
+ "content": false,
136
+ "description": "Clamp each excerpt to this many lines."
137
+ },
138
+ "locale": {
139
+ "type": "string",
140
+ "content": false,
141
+ "description": "BCP 47 locale the dates are written in (site.language)."
142
+ },
143
+ "surface": {
144
+ "type": "string",
145
+ "content": false,
146
+ "description": "plain or card: whether each post is a card of its own (post-card surface)."
147
+ },
148
+ "moreHref": {
149
+ "type": "url",
150
+ "content": false,
151
+ "description": "Where the full archive lives; with moreLabel a link follows the list."
152
+ },
153
+ "moreLabel": {
154
+ "type": "string",
155
+ "content": false,
156
+ "description": "Text of the archive link."
157
+ },
158
+ "tone": {
159
+ "type": "string",
160
+ "content": false,
161
+ "description": "Section background: default, muted, inverse or accent."
162
+ },
163
+ "id": {
164
+ "type": "string",
165
+ "content": false,
166
+ "description": "Anchor id of the section; the heading gets `<id>-title`."
167
+ },
168
+ "measured": {
169
+ "type": "object",
170
+ "content": false,
171
+ "description": "The source section's measured style (MeasuredStyle): frame width, padding, fill, image, columns per width, gap, text tone, radius, minimum height — all in pixels. Absent, the site's tokens; present, it wins over tone and columns. Values that are not a number, a colour or a known keyword are dropped.",
172
+ "fields": {
173
+ "containerPx": {
174
+ "type": "integer",
175
+ "min": 0,
176
+ "max": 2560,
177
+ "description": "The frame's content width."
178
+ },
179
+ "padY": {
180
+ "type": "integer",
181
+ "min": 0,
182
+ "max": 400,
183
+ "description": "Top and bottom padding."
184
+ },
185
+ "padX": {
186
+ "type": "integer",
187
+ "min": 0,
188
+ "max": 200,
189
+ "description": "The frame's side padding (gutter)."
190
+ },
191
+ "bg": {
192
+ "type": "object",
193
+ "description": "The section's background.",
194
+ "fields": {
195
+ "color": {
196
+ "type": "string",
197
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
198
+ },
199
+ "image": {
200
+ "type": "string",
201
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
202
+ },
203
+ "overlay": {
204
+ "type": "string",
205
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
206
+ },
207
+ "size": {
208
+ "type": "string",
209
+ "description": "cover, contain or auto; anything else draws as cover."
210
+ },
211
+ "position": {
212
+ "type": "string",
213
+ "description": "One or two of center/top/bottom/left/right or percentages."
214
+ }
215
+ }
216
+ },
217
+ "columns": {
218
+ "type": "object",
219
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
220
+ "fields": {
221
+ "390": {
222
+ "type": "integer",
223
+ "min": 1,
224
+ "max": 6,
225
+ "description": "Phone."
226
+ },
227
+ "768": {
228
+ "type": "integer",
229
+ "min": 1,
230
+ "max": 6,
231
+ "description": "Tablet."
232
+ },
233
+ "1280": {
234
+ "type": "integer",
235
+ "min": 1,
236
+ "max": 6,
237
+ "description": "Desktop."
238
+ }
239
+ }
240
+ },
241
+ "gap": {
242
+ "type": "integer",
243
+ "min": 0,
244
+ "max": 200,
245
+ "description": "Gap between items."
246
+ },
247
+ "tone": {
248
+ "type": "select",
249
+ "options": [
250
+ "light",
251
+ "dark"
252
+ ],
253
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
254
+ },
255
+ "align": {
256
+ "type": "select",
257
+ "options": [
258
+ "start",
259
+ "center",
260
+ "end"
261
+ ],
262
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
263
+ },
264
+ "radius": {
265
+ "type": "integer",
266
+ "min": 0,
267
+ "max": 200,
268
+ "description": "Card and item corner radius."
269
+ },
270
+ "minHeight": {
271
+ "type": "integer",
272
+ "min": 0,
273
+ "max": 2000,
274
+ "description": "The section's minimum height."
275
+ }
276
+ }
277
+ }
278
+ },
279
+ "slots": {},
280
+ "js": "none",
281
+ "dependencies": {},
282
+ "sources": {
283
+ "gutenberg": [
284
+ "core/query",
285
+ "core/latest-posts"
286
+ ],
287
+ "elementor": [
288
+ "elementor/posts",
289
+ "elementor/archive-posts"
290
+ ],
291
+ "divi": [
292
+ "divi/et_pb_blog"
293
+ ],
294
+ "classic": [
295
+ "classic/archive-item"
296
+ ]
297
+ },
298
+ "a11y": "A <ul> of cards, each an <article> with one link (the title, stretched over the card) and a <time datetime>. Cards sit one heading level under the section heading, or at h2 when there is none. Images are decorative next to their title; the featured card keeps reading order.",
299
+ "pattern": {
300
+ "summary": "a repeated post card: image + title + excerpt + date, as a grid, rows or a featured first post",
301
+ "signals": [
302
+ "2-4 columns",
303
+ "date text",
304
+ "one link per card",
305
+ "repeated card structure"
306
+ ]
307
+ }
308
+ }
@@ -2,10 +2,14 @@
2
2
  // Price plans side by side: a name, a price and what it includes, with a
3
3
  // button each. The featured plan is raised and carries a badge, so it stands
4
4
  // out by more than its colour. Each plan's features are a markdown list,
5
- // rendered at build time.
5
+ // rendered at build time. Variants: `layout="table"` is a comparison table
6
+ // (plan columns, feature rows); `billingToggle="radio"` swaps monthly and yearly
7
+ // prices with two radio inputs and CSS `:has()` — no script.
6
8
  import { tv } from 'tailwind-variants'
7
9
  import Button from '../_shared/Button.astro'
8
10
  import Section, { type Tone } from '../_shared/Section.astro'
11
+ import { measuredGrid } from '../_shared/measured'
12
+ import type { MeasuredStyle } from '../_shared/types'
9
13
  import { renderMarkdown } from '../../../lib/markdown'
10
14
 
11
15
  const pricing = tv({
@@ -14,6 +18,12 @@ const pricing = tv({
14
18
  heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
15
19
  intro: 'text-lg opacity-80',
16
20
  list: 'grid items-stretch gap-6 sm:grid-cols-2',
21
+ toggle: 'mx-auto mb-8 flex w-fit rounded-full border border-line p-1',
22
+ toggleOption: 'rounded-full px-4 py-1.5 text-sm font-medium has-[:checked]:bg-accent has-[:checked]:text-accent-ink has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-accent',
23
+ tableWrap: 'overflow-x-auto',
24
+ table: 'w-full min-w-[32rem] border-collapse text-start',
25
+ cell: 'border-b border-line px-4 py-3 text-center align-top',
26
+ rowHead: 'border-b border-line px-4 py-3 text-start font-normal',
17
27
  plan: 'relative flex flex-col gap-5 rounded-card border border-line bg-surface p-6 text-ink md:p-8',
18
28
  badge: 'absolute -top-3 start-6 rounded-full bg-accent px-3 py-1 text-xs font-semibold text-accent-ink',
19
29
  name: 'text-lg [font-weight:var(--font-weight-heading,600)]',
@@ -30,9 +40,16 @@ const pricing = tv({
30
40
  '3': { list: 'lg:grid-cols-3' },
31
41
  '4': { list: 'lg:grid-cols-4' },
32
42
  },
43
+ // Which mark leads each feature in the cards: a check, a dot or none (the markdown list's own bullets).
44
+ featureStyle: {
45
+ check: { features: '[&_ul]:list-none [&_ul]:ps-0 [&_li]:flex [&_li]:gap-2 [&_li]:before:shrink-0 [&_li]:before:font-semibold [&_li]:before:text-accent-text [&_li]:before:content-["✓"]' },
46
+ dot: { features: '[&_ul]:list-none [&_ul]:ps-0 [&_li]:flex [&_li]:gap-2 [&_li]:before:mt-2 [&_li]:before:size-1.5 [&_li]:before:shrink-0 [&_li]:before:rounded-full [&_li]:before:bg-accent [&_li]:before:content-[""]' },
47
+ plain: {},
48
+ },
49
+ compact: { true: { list: 'gap-4', plan: 'gap-3 p-4 md:p-5', amount: 'text-3xl' }, false: {} },
33
50
  tone: { default: {}, muted: { plan: 'border-transparent' }, inverse: { plan: 'border-transparent' }, accent: { intro: 'opacity-100', plan: 'border-transparent', badge: 'bg-ink text-surface' } },
34
51
  },
35
- defaultVariants: { columns: '3', tone: 'default' },
52
+ defaultVariants: { columns: '3', featureStyle: 'plain', compact: false, tone: 'default' },
36
53
  })
37
54
 
38
55
  interface Plan {
@@ -45,6 +62,11 @@ interface Plan {
45
62
  ctaLabel?: string | undefined
46
63
  ctaHref?: string | undefined
47
64
  featured?: boolean | undefined
65
+ /** A free-text badge ("Best value"); a featured plan without one reads `featuredLabel`. */
66
+ badge?: string | undefined
67
+ /** The other billing period's price and period (`billingToggle="radio"`). */
68
+ priceAlt?: string | undefined
69
+ periodAlt?: string | undefined
48
70
  }
49
71
 
50
72
  interface Props {
@@ -54,43 +76,125 @@ interface Props {
54
76
  /** Badge on the featured plan. */
55
77
  featuredLabel?: string | undefined
56
78
  columns?: '2' | '3' | '4' | undefined
79
+ /** `cards` side by side, or one comparison `table` (plans as columns, features as rows). */
80
+ layout?: 'cards' | 'table' | undefined
81
+ featureStyle?: 'check' | 'dot' | 'plain' | undefined
82
+ compact?: boolean | undefined
83
+ /** `radio`: two labelled radios swap each plan's price for its `priceAlt` — no script. */
84
+ billingToggle?: 'none' | 'radio' | undefined
85
+ /** Texts of the toggle: its two options and the legend read to screen readers. */
86
+ billingMainLabel?: string | undefined
87
+ billingAltLabel?: string | undefined
88
+ billingLegend?: string | undefined
89
+ /** Table cell texts for screen readers. */
90
+ includedLabel?: string | undefined
91
+ notIncludedLabel?: string | undefined
57
92
  tone?: Tone | undefined
93
+ /** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
94
+ measured?: MeasuredStyle | undefined
58
95
  id?: string | undefined
59
96
  }
60
97
 
61
- const { heading, intro, plans, featuredLabel = 'Most popular', columns, tone, id } = Astro.props
62
- const s = pricing({ columns, tone })
98
+ const { heading, intro, plans, featuredLabel = 'Most popular', columns, layout = 'cards', featureStyle, compact, billingToggle = 'none', billingMainLabel = 'Monthly', billingAltLabel = 'Yearly', billingLegend = 'Billing period', includedLabel = 'Included', notIncludedLabel = 'Not included', tone, measured, id } = Astro.props
99
+ const s = pricing({ columns, featureStyle, compact: compact ?? false, tone })
100
+ // The radio group's name must be unique on the page: the section's id, or a hash of its plans when it has none.
101
+ const group = id ?? `pricing-${[...JSON.stringify(plans)].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7).toString(36)}`
102
+ const toggled = billingToggle === 'radio' && plans.some(plan => plan.priceAlt)
103
+ // The cards' list items, as plain strings, for the table's feature rows (the markdown list's `- ` / `* ` lines).
104
+ const itemsOf = (md?: string) => (md ?? '').split('\n').flatMap((line) => { const m = /^\s*[-*]\s+(.*\S)/.exec(line); return m ? [m[1]!.replace(/[*_`]/g, '')] : [] })
105
+ const rows = layout === 'table' ? [...new Set(plans.flatMap(plan => itemsOf(plan.features)))] : []
106
+ const included = plans.map(plan => new Set(itemsOf(plan.features)))
63
107
  const headingId = heading && id ? `${id}-title` : undefined
64
108
  // One level under the section heading, or the section's heading when there is none.
65
109
  const PlanHeading = heading ? 'h3' : 'h2'
66
110
  const features = await Promise.all(plans.map(plan => renderMarkdown(plan.features)))
67
111
  ---
68
112
  {plans.length > 0 && (
69
- <Section tone={tone} id={id} aria-labelledby={headingId}>
113
+ <Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
70
114
  {(heading || intro) && (
71
115
  <header class={s.header()}>
72
116
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
73
117
  {intro && <p class={s.intro()} data-kit-text>{intro}</p>}
74
118
  </header>
75
119
  )}
76
- <ul class={s.list()}>
77
- {plans.map((plan, i) => (
78
- <li class={s.plan({ class: plan.featured ? 'ring-2 ring-accent shadow-lg md:-my-2 md:py-10' : undefined })}>
79
- {plan.featured && <p class={s.badge()}>{featuredLabel}</p>}
80
- <PlanHeading class={s.name()}>{plan.name}</PlanHeading>
81
- <p class={s.price()}>
82
- <span class={s.amount()}>{plan.price}</span>
83
- {plan.period && <span class={s.period()}>{plan.period}</span>}
84
- </p>
85
- {plan.description && <p class={s.description()} data-kit-text>{plan.description}</p>}
86
- {features[i] && <div class={s.features()} set:html={features[i]} />}
87
- {plan.ctaLabel && plan.ctaHref && (
88
- <div class={s.action()}>
89
- <Button action={{ label: plan.ctaLabel, href: plan.ctaHref, style: plan.featured ? 'primary' : 'ghost' }} class="w-full" />
90
- </div>
91
- )}
92
- </li>
93
- ))}
94
- </ul>
120
+ {layout === 'table' ? (
121
+ <div class={s.tableWrap()} tabindex="0" role="region" aria-labelledby={headingId} aria-label={headingId ? undefined : plans.map(plan => plan.name).join(', ')}>
122
+ <table class={s.table()}>
123
+ <caption class="sr-only">{heading ?? plans.map(plan => plan.name).join(', ')}</caption>
124
+ <thead>
125
+ <tr>
126
+ <td class={s.cell()} />
127
+ {plans.map(plan => (
128
+ <th scope="col" class={s.cell({ class: plan.featured ? 'bg-surface-muted' : undefined })}>
129
+ <span class="block text-lg [font-weight:var(--font-weight-heading,600)]">{plan.name}</span>
130
+ <span class="block text-2xl [font-weight:var(--font-weight-heading,700)]">{plan.price}</span>
131
+ {plan.period && <span class="block text-sm opacity-75">{plan.period}</span>}
132
+ {(plan.badge ?? (plan.featured ? featuredLabel : undefined)) && <span class="mt-1 inline-block rounded-full bg-accent px-3 py-0.5 text-xs font-semibold text-accent-ink">{plan.badge ?? featuredLabel}</span>}
133
+ </th>
134
+ ))}
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ {rows.map(row => (
139
+ <tr>
140
+ <th scope="row" class={s.rowHead()}>{row}</th>
141
+ {plans.map((plan, i) => (
142
+ <td class={s.cell({ class: plan.featured ? 'bg-surface-muted' : undefined })}>
143
+ {included[i]!.has(row)
144
+ ? <><span aria-hidden="true">✓</span><span class="sr-only">{includedLabel}</span></>
145
+ : <><span aria-hidden="true" class="opacity-50">–</span><span class="sr-only">{notIncludedLabel}</span></>}
146
+ </td>
147
+ ))}
148
+ </tr>
149
+ ))}
150
+ {plans.some(plan => plan.ctaLabel && plan.ctaHref) && (
151
+ <tr>
152
+ <td class={s.cell()} />
153
+ {plans.map(plan => (
154
+ <td class={s.cell({ class: plan.featured ? 'bg-surface-muted' : undefined })}>
155
+ {plan.ctaLabel && plan.ctaHref && <Button action={{ label: plan.ctaLabel, href: plan.ctaHref, style: plan.featured ? 'primary' : 'ghost' }} />}
156
+ </td>
157
+ ))}
158
+ </tr>
159
+ )}
160
+ </tbody>
161
+ </table>
162
+ </div>
163
+ ) : (
164
+ <div class={toggled ? '[&:has(input[value=alt]:checked)_[data-price=main]]:hidden [&:not(:has(input[value=alt]:checked))_[data-price=alt]]:hidden' : undefined}>
165
+ {toggled && (
166
+ <fieldset class={s.toggle()}>
167
+ <legend class="sr-only">{billingLegend}</legend>
168
+ <label class={s.toggleOption()}><input type="radio" name={`${group}-billing`} value="main" checked class="sr-only" />{billingMainLabel}</label>
169
+ <label class={s.toggleOption()}><input type="radio" name={`${group}-billing`} value="alt" class="sr-only" />{billingAltLabel}</label>
170
+ </fieldset>
171
+ )}
172
+ <ul class={s.list({ class: measuredGrid(measured) })}>
173
+ {plans.map((plan, i) => (
174
+ <li class={s.plan({ class: plan.featured ? 'ring-2 ring-accent shadow-lg md:-my-2 md:py-10' : undefined })}>
175
+ {(plan.badge ?? (plan.featured ? featuredLabel : undefined)) && <p class={s.badge()}>{plan.badge ?? featuredLabel}</p>}
176
+ <PlanHeading class={s.name()}>{plan.name}</PlanHeading>
177
+ <p class={s.price()} data-price="main">
178
+ <span class={s.amount()}>{plan.price}</span>
179
+ {plan.period && <span class={s.period()}>{plan.period}</span>}
180
+ </p>
181
+ {toggled && plan.priceAlt && (
182
+ <p class={s.price()} data-price="alt">
183
+ <span class={s.amount()}>{plan.priceAlt}</span>
184
+ {plan.periodAlt && <span class={s.period()}>{plan.periodAlt}</span>}
185
+ </p>
186
+ )}
187
+ {plan.description && <p class={s.description()} data-kit-text>{plan.description}</p>}
188
+ {features[i] && <div class={s.features()} set:html={features[i]} />}
189
+ {plan.ctaLabel && plan.ctaHref && (
190
+ <div class={s.action()}>
191
+ <Button action={{ label: plan.ctaLabel, href: plan.ctaHref, style: plan.featured ? 'primary' : 'ghost' }} class="w-full" />
192
+ </div>
193
+ )}
194
+ </li>
195
+ ))}
196
+ </ul>
197
+ </div>
198
+ )}
95
199
  </Section>
96
200
  )}