@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
@@ -135,5 +135,244 @@
135
135
  "tone": "accent",
136
136
  "style": "grayscale"
137
137
  }
138
+ },
139
+ {
140
+ "name": "row-start-large",
141
+ "props": {
142
+ "heading": "Trusted by",
143
+ "items": [
144
+ {
145
+ "image": "/kit-fixtures/logo.svg",
146
+ "imageAlt": "Company 1",
147
+ "href": "https://example.com"
148
+ },
149
+ {
150
+ "image": "/kit-fixtures/logo.svg",
151
+ "imageAlt": "Company 2"
152
+ },
153
+ {
154
+ "image": "/kit-fixtures/logo.svg",
155
+ "imageAlt": "Company 3",
156
+ "href": "https://example.com"
157
+ },
158
+ {
159
+ "image": "/kit-fixtures/logo.svg",
160
+ "imageAlt": "Company 4"
161
+ },
162
+ {
163
+ "image": "/kit-fixtures/logo.svg",
164
+ "imageAlt": "Company 5",
165
+ "href": "https://example.com"
166
+ },
167
+ {
168
+ "image": "/kit-fixtures/logo.svg",
169
+ "imageAlt": "Company 6"
170
+ }
171
+ ],
172
+ "layout": "row",
173
+ "align": "start",
174
+ "logoHeight": "lg"
175
+ }
176
+ },
177
+ {
178
+ "name": "row-small-mono-inverse",
179
+ "props": {
180
+ "heading": "Trusted by",
181
+ "items": [
182
+ {
183
+ "image": "/kit-fixtures/logo.svg",
184
+ "imageAlt": "Company 1",
185
+ "href": "https://example.com"
186
+ },
187
+ {
188
+ "image": "/kit-fixtures/logo.svg",
189
+ "imageAlt": "Company 2"
190
+ },
191
+ {
192
+ "image": "/kit-fixtures/logo.svg",
193
+ "imageAlt": "Company 3",
194
+ "href": "https://example.com"
195
+ },
196
+ {
197
+ "image": "/kit-fixtures/logo.svg",
198
+ "imageAlt": "Company 4"
199
+ },
200
+ {
201
+ "image": "/kit-fixtures/logo.svg",
202
+ "imageAlt": "Company 5",
203
+ "href": "https://example.com"
204
+ },
205
+ {
206
+ "image": "/kit-fixtures/logo.svg",
207
+ "imageAlt": "Company 6"
208
+ }
209
+ ],
210
+ "layout": "row",
211
+ "logoHeight": "sm",
212
+ "style": "mono-light",
213
+ "tone": "inverse"
214
+ }
215
+ },
216
+ {
217
+ "name": "marquee",
218
+ "props": {
219
+ "heading": "Trusted by",
220
+ "items": [
221
+ {
222
+ "image": "/kit-fixtures/logo.svg",
223
+ "imageAlt": "Company 1",
224
+ "href": "https://example.com"
225
+ },
226
+ {
227
+ "image": "/kit-fixtures/logo.svg",
228
+ "imageAlt": "Company 2"
229
+ },
230
+ {
231
+ "image": "/kit-fixtures/logo.svg",
232
+ "imageAlt": "Company 3",
233
+ "href": "https://example.com"
234
+ },
235
+ {
236
+ "image": "/kit-fixtures/logo.svg",
237
+ "imageAlt": "Company 4"
238
+ },
239
+ {
240
+ "image": "/kit-fixtures/logo.svg",
241
+ "imageAlt": "Company 5",
242
+ "href": "https://example.com"
243
+ },
244
+ {
245
+ "image": "/kit-fixtures/logo.svg",
246
+ "imageAlt": "Company 6"
247
+ }
248
+ ],
249
+ "layout": "marquee",
250
+ "tone": "muted"
251
+ }
252
+ },
253
+ {
254
+ "name": "five-columns",
255
+ "props": {
256
+ "heading": "Trusted by",
257
+ "items": [
258
+ {
259
+ "image": "/kit-fixtures/logo.svg",
260
+ "imageAlt": "Company 1",
261
+ "href": "https://example.com"
262
+ },
263
+ {
264
+ "image": "/kit-fixtures/logo.svg",
265
+ "imageAlt": "Company 2"
266
+ },
267
+ {
268
+ "image": "/kit-fixtures/logo.svg",
269
+ "imageAlt": "Company 3",
270
+ "href": "https://example.com"
271
+ },
272
+ {
273
+ "image": "/kit-fixtures/logo.svg",
274
+ "imageAlt": "Company 4"
275
+ },
276
+ {
277
+ "image": "/kit-fixtures/logo.svg",
278
+ "imageAlt": "Company 5",
279
+ "href": "https://example.com"
280
+ },
281
+ {
282
+ "image": "/kit-fixtures/logo.svg",
283
+ "imageAlt": "Company 6"
284
+ }
285
+ ],
286
+ "columns": "5"
287
+ }
288
+ },
289
+ {
290
+ "name": "six-columns",
291
+ "props": {
292
+ "heading": "Trusted by",
293
+ "items": [
294
+ {
295
+ "image": "/kit-fixtures/logo.svg",
296
+ "imageAlt": "Company 1",
297
+ "href": "https://example.com"
298
+ },
299
+ {
300
+ "image": "/kit-fixtures/logo.svg",
301
+ "imageAlt": "Company 2"
302
+ },
303
+ {
304
+ "image": "/kit-fixtures/logo.svg",
305
+ "imageAlt": "Company 3",
306
+ "href": "https://example.com"
307
+ },
308
+ {
309
+ "image": "/kit-fixtures/logo.svg",
310
+ "imageAlt": "Company 4"
311
+ },
312
+ {
313
+ "image": "/kit-fixtures/logo.svg",
314
+ "imageAlt": "Company 5",
315
+ "href": "https://example.com"
316
+ },
317
+ {
318
+ "image": "/kit-fixtures/logo.svg",
319
+ "imageAlt": "Company 6"
320
+ }
321
+ ],
322
+ "columns": "6"
323
+ }
324
+ },
325
+ {
326
+ "name": "measured-columns",
327
+ "props": {
328
+ "heading": "Trusted by",
329
+ "items": [
330
+ {
331
+ "image": "/kit-fixtures/logo.svg",
332
+ "imageAlt": "Company 1",
333
+ "href": "https://example.com"
334
+ },
335
+ {
336
+ "image": "/kit-fixtures/logo.svg",
337
+ "imageAlt": "Company 2"
338
+ },
339
+ {
340
+ "image": "/kit-fixtures/logo.svg",
341
+ "imageAlt": "Company 3",
342
+ "href": "https://example.com"
343
+ },
344
+ {
345
+ "image": "/kit-fixtures/logo.svg",
346
+ "imageAlt": "Company 4"
347
+ },
348
+ {
349
+ "image": "/kit-fixtures/logo.svg",
350
+ "imageAlt": "Company 5",
351
+ "href": "https://example.com"
352
+ },
353
+ {
354
+ "image": "/kit-fixtures/logo.svg",
355
+ "imageAlt": "Company 6"
356
+ },
357
+ {
358
+ "image": "/kit-fixtures/logo.svg",
359
+ "imageAlt": "Company 7",
360
+ "href": "https://example.com"
361
+ },
362
+ {
363
+ "image": "/kit-fixtures/logo.svg",
364
+ "imageAlt": "Company 8"
365
+ }
366
+ ],
367
+ "measured": {
368
+ "columns": {
369
+ "390": 2,
370
+ "768": 3,
371
+ "1280": 6
372
+ },
373
+ "gap": 32,
374
+ "padY": 48
375
+ }
376
+ }
138
377
  }
139
378
  ]
@@ -1,32 +1,62 @@
1
1
  {
2
2
  "id": "logo-cloud",
3
3
  "name": "Logo cloud",
4
- "description": "A grid of client or partner logos, each optionally linked, under an optional heading (\"Trusted by\").",
4
+ "description": "A grid, a row or an endless strip of client or partner logos, each optionally linked, under an optional heading (\"Trusted by\").",
5
5
  "category": "section",
6
6
  "uses": [],
7
7
  "shared": [
8
8
  "KitImage.astro",
9
9
  "Section.astro",
10
10
  "image.ts",
11
+ "measured.ts",
11
12
  "types.ts"
12
13
  ],
13
14
  "variants": {
15
+ "layout": {
16
+ "options": [
17
+ "grid",
18
+ "row",
19
+ "marquee"
20
+ ],
21
+ "default": "grid",
22
+ "description": "grid: logos in columns. row: one line that wraps. marquee: an endless CSS-only strip that stands still for reduced motion."
23
+ },
14
24
  "columns": {
15
25
  "options": [
16
26
  "2",
17
27
  "3",
18
- "4"
28
+ "4",
29
+ "5",
30
+ "6"
19
31
  ],
20
32
  "default": "4",
21
- "description": "Logos per row on wide screens; two on phones, three on tablets."
33
+ "description": "Logos per row on wide screens (grid layout); two on phones, three on tablets."
34
+ },
35
+ "logoHeight": {
36
+ "options": [
37
+ "sm",
38
+ "md",
39
+ "lg"
40
+ ],
41
+ "default": "md",
42
+ "description": "Height of every logo (the width follows): small, medium or large."
22
43
  },
23
44
  "style": {
24
45
  "options": [
25
46
  "plain",
26
- "grayscale"
47
+ "grayscale",
48
+ "mono-light"
27
49
  ],
28
50
  "default": "plain",
29
- "description": "plain: logos in their own colours. grayscale: muted grey, full colour on hover or focus."
51
+ "description": "plain: logos in their own colours. grayscale: muted grey, full colour on hover or focus. mono-light: drawn white, for a dark band."
52
+ },
53
+ "align": {
54
+ "options": [
55
+ "center",
56
+ "start"
57
+ ],
58
+ "default": "center",
59
+ "description": "Alignment of the heading and of the logos in a row."
30
60
  },
31
61
  "tone": {
32
62
  "options": [
@@ -73,6 +103,116 @@
73
103
  },
74
104
  "description": "The logos, in order (a Gutenberg gallery or columns of logos, Elementor image carousel/gallery of logos, Divi images)."
75
105
  },
106
+ "measured": {
107
+ "type": "object",
108
+ "content": false,
109
+ "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.",
110
+ "fields": {
111
+ "containerPx": {
112
+ "type": "integer",
113
+ "min": 0,
114
+ "max": 2560,
115
+ "description": "The frame's content width."
116
+ },
117
+ "padY": {
118
+ "type": "integer",
119
+ "min": 0,
120
+ "max": 400,
121
+ "description": "Top and bottom padding."
122
+ },
123
+ "padX": {
124
+ "type": "integer",
125
+ "min": 0,
126
+ "max": 200,
127
+ "description": "The frame's side padding (gutter)."
128
+ },
129
+ "bg": {
130
+ "type": "object",
131
+ "description": "The section's background.",
132
+ "fields": {
133
+ "color": {
134
+ "type": "string",
135
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
136
+ },
137
+ "image": {
138
+ "type": "string",
139
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
140
+ },
141
+ "overlay": {
142
+ "type": "string",
143
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
144
+ },
145
+ "size": {
146
+ "type": "string",
147
+ "description": "cover, contain or auto; anything else draws as cover."
148
+ },
149
+ "position": {
150
+ "type": "string",
151
+ "description": "One or two of center/top/bottom/left/right or percentages."
152
+ }
153
+ }
154
+ },
155
+ "columns": {
156
+ "type": "object",
157
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
158
+ "fields": {
159
+ "390": {
160
+ "type": "integer",
161
+ "min": 1,
162
+ "max": 6,
163
+ "description": "Phone."
164
+ },
165
+ "768": {
166
+ "type": "integer",
167
+ "min": 1,
168
+ "max": 6,
169
+ "description": "Tablet."
170
+ },
171
+ "1280": {
172
+ "type": "integer",
173
+ "min": 1,
174
+ "max": 6,
175
+ "description": "Desktop."
176
+ }
177
+ }
178
+ },
179
+ "gap": {
180
+ "type": "integer",
181
+ "min": 0,
182
+ "max": 200,
183
+ "description": "Gap between items."
184
+ },
185
+ "tone": {
186
+ "type": "select",
187
+ "options": [
188
+ "light",
189
+ "dark"
190
+ ],
191
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
192
+ },
193
+ "align": {
194
+ "type": "select",
195
+ "options": [
196
+ "start",
197
+ "center",
198
+ "end"
199
+ ],
200
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
201
+ },
202
+ "radius": {
203
+ "type": "integer",
204
+ "min": 0,
205
+ "max": 200,
206
+ "description": "Card and item corner radius."
207
+ },
208
+ "minHeight": {
209
+ "type": "integer",
210
+ "min": 0,
211
+ "max": 2000,
212
+ "description": "The section's minimum height."
213
+ }
214
+ }
215
+ },
76
216
  "id": {
77
217
  "type": "string",
78
218
  "content": false,
@@ -10,6 +10,7 @@
10
10
  // of placing it.
11
11
  import { tv } from 'tailwind-variants'
12
12
  import Section from '../_shared/Section.astro'
13
+ import type { MeasuredStyle } from '../_shared/types'
13
14
  import { newsletterForm } from './newsletter'
14
15
 
15
16
  const newsletter = tv({
@@ -64,12 +65,15 @@ interface Props {
64
65
  layout?: 'inline' | 'stacked' | undefined
65
66
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
66
67
  id?: string | undefined
68
+ /** 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. */
69
+ measured?: MeasuredStyle | undefined
67
70
  }
68
71
 
69
72
  const {
70
73
  action, provider = 'mailchimp', emailName, askName = false, heading, intro, note,
71
74
  emailLabel = 'Email address', nameLabel = 'First name', submitLabel = 'Subscribe', newTab = true,
72
75
  layout, tone = 'muted', id = 'newsletter',
76
+ measured,
73
77
  } = Astro.props
74
78
  const form = newsletterForm(provider, action, { email: emailName }, Astro.site)
75
79
  const s = newsletter({ layout, tone })
@@ -78,7 +82,7 @@ const headingId = heading ? `${id}-title` : undefined
78
82
  const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' } : {}
79
83
  ---
80
84
  {form && (
81
- <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
85
+ <Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
82
86
  <div class={s.box()}>
83
87
  {(heading || intro) && (
84
88
  <div class={s.copy()}>
@@ -5,7 +5,9 @@
5
5
  "category": "section",
6
6
  "uses": [],
7
7
  "shared": [
8
- "Section.astro"
8
+ "Section.astro",
9
+ "measured.ts",
10
+ "types.ts"
9
11
  ],
10
12
  "variants": {
11
13
  "layout": {
@@ -99,6 +101,116 @@
99
101
  "type": "string",
100
102
  "content": false,
101
103
  "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`."
104
+ },
105
+ "measured": {
106
+ "type": "object",
107
+ "content": false,
108
+ "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.",
109
+ "fields": {
110
+ "containerPx": {
111
+ "type": "integer",
112
+ "min": 0,
113
+ "max": 2560,
114
+ "description": "The frame's content width."
115
+ },
116
+ "padY": {
117
+ "type": "integer",
118
+ "min": 0,
119
+ "max": 400,
120
+ "description": "Top and bottom padding."
121
+ },
122
+ "padX": {
123
+ "type": "integer",
124
+ "min": 0,
125
+ "max": 200,
126
+ "description": "The frame's side padding (gutter)."
127
+ },
128
+ "bg": {
129
+ "type": "object",
130
+ "description": "The section's background.",
131
+ "fields": {
132
+ "color": {
133
+ "type": "string",
134
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
135
+ },
136
+ "image": {
137
+ "type": "string",
138
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
139
+ },
140
+ "overlay": {
141
+ "type": "string",
142
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
143
+ },
144
+ "size": {
145
+ "type": "string",
146
+ "description": "cover, contain or auto; anything else draws as cover."
147
+ },
148
+ "position": {
149
+ "type": "string",
150
+ "description": "One or two of center/top/bottom/left/right or percentages."
151
+ }
152
+ }
153
+ },
154
+ "columns": {
155
+ "type": "object",
156
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
157
+ "fields": {
158
+ "390": {
159
+ "type": "integer",
160
+ "min": 1,
161
+ "max": 6,
162
+ "description": "Phone."
163
+ },
164
+ "768": {
165
+ "type": "integer",
166
+ "min": 1,
167
+ "max": 6,
168
+ "description": "Tablet."
169
+ },
170
+ "1280": {
171
+ "type": "integer",
172
+ "min": 1,
173
+ "max": 6,
174
+ "description": "Desktop."
175
+ }
176
+ }
177
+ },
178
+ "gap": {
179
+ "type": "integer",
180
+ "min": 0,
181
+ "max": 200,
182
+ "description": "Gap between items."
183
+ },
184
+ "tone": {
185
+ "type": "select",
186
+ "options": [
187
+ "light",
188
+ "dark"
189
+ ],
190
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
191
+ },
192
+ "align": {
193
+ "type": "select",
194
+ "options": [
195
+ "start",
196
+ "center",
197
+ "end"
198
+ ],
199
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
200
+ },
201
+ "radius": {
202
+ "type": "integer",
203
+ "min": 0,
204
+ "max": 200,
205
+ "description": "Card and item corner radius."
206
+ },
207
+ "minHeight": {
208
+ "type": "integer",
209
+ "min": 0,
210
+ "max": 2000,
211
+ "description": "The section's minimum height."
212
+ }
213
+ }
102
214
  }
103
215
  },
104
216
  "slots": {},
@@ -50,6 +50,8 @@ interface Props {
50
50
  layout?: 'vertical' | 'horizontal' | 'minimal' | undefined
51
51
  surface?: 'plain' | 'card' | undefined
52
52
  byline?: 'inline' | 'below' | undefined
53
+ /** Clamp the excerpt to this many lines; absent, the layout's own (3 vertical, 2 otherwise). */
54
+ excerptLines?: 2 | 3 | 4 | undefined
53
55
  /** The title in the site's link colour, as themes that colour every link do (Hello Elementor). */
54
56
  linkTitle?: boolean | undefined
55
57
  /** The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give. */
@@ -60,7 +62,7 @@ interface Props {
60
62
  imageAspect?: string | undefined
61
63
  }
62
64
 
63
- const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, sizes, imageHeight, imageAspect } = Astro.props
65
+ const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, excerptLines, sizes, imageHeight, imageAspect } = Astro.props
64
66
  const date = rawDate === undefined ? undefined : new Date(rawDate)
65
67
  const slots = card({ layout, surface, byline })
66
68
  const below = byline === 'below'
@@ -97,7 +99,7 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3'
97
99
  {date && <time datetime={date.toISOString()}>{shown}</time>}
98
100
  </p>
99
101
  )}
100
- {excerpt && <p class={slots.excerpt()}>{excerpt}</p>}
102
+ {excerpt && <p class={slots.excerpt({ class: excerptLines ? { 2: 'line-clamp-2', 3: 'line-clamp-3', 4: 'line-clamp-4' }[excerptLines] : undefined })}>{excerpt}</p>}
101
103
  {!below && author && <p class="text-sm text-ink-muted">{author}</p>}
102
104
  </div>
103
105
  </article>
@@ -23,7 +23,8 @@
23
23
  "linkTitle": { "type": "boolean", "default": false, "description": "The title in the site's link colour (--color-link), as themes that colour every link do." },
24
24
  "sizes": { "type": "string", "description": "The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give." },
25
25
  "imageHeight": { "type": "string", "description": "The image block's height as the source sets it (a CSS length or clamp()), cropped to fit; wins over imageAspect." },
26
- "imageAspect": { "type": "string", "description": "The image block's aspect ratio (16 / 9) where the source sets an aspect ratio; absent, 16 / 9." }
26
+ "imageAspect": { "type": "string", "description": "The image block's aspect ratio (16 / 9) where the source sets an aspect ratio; absent, 16 / 9." },
27
+ "excerptLines": { "type": "integer", "min": 2, "max": 4, "content": false, "description": "Clamp the excerpt to this many lines; absent, the layout's own (3 vertical, 2 otherwise)." }
27
28
  },
28
29
  "slots": {},
29
30
  "js": "none",