@contentrain/astro-kit 0.31.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.
- package/catalog.json +2531 -345
- package/components/_shared/measured.ts +10 -0
- package/components/contact-form/ContactForm.astro +48 -12
- package/components/contact-form/fixtures.json +169 -9
- package/components/contact-form/meta.json +283 -15
- package/components/cta/Cta.astro +52 -12
- package/components/cta/fixtures.json +5 -1
- package/components/cta/meta.json +11 -2
- package/components/faq/Faq.astro +60 -17
- package/components/faq/fixtures.json +75 -0
- package/components/faq/meta.json +7 -1
- package/components/feature-list/FeatureList.astro +28 -4
- package/components/feature-list/meta.json +15 -3
- package/components/gallery/Gallery.astro +47 -13
- package/components/gallery/fixtures.json +275 -0
- package/components/gallery/meta.json +266 -14
- package/components/logo-cloud/LogoCloud.astro +85 -21
- package/components/logo-cloud/fixtures.json +239 -0
- package/components/logo-cloud/meta.json +144 -5
- package/components/post-card/PostCard.astro +4 -2
- package/components/post-card/meta.json +2 -1
- package/components/post-list/PostList.astro +114 -0
- package/components/post-list/fixtures.json +387 -0
- package/components/post-list/meta.json +308 -0
- package/components/pricing/Pricing.astro +128 -24
- package/components/pricing/fixtures.json +288 -4
- package/components/pricing/meta.json +187 -1
- package/components/split/Split.astro +76 -14
- package/components/split/fixtures.json +8 -1
- package/components/split/meta.json +225 -4
- package/components/stats/Stats.astro +42 -10
- package/components/stats/fixtures.json +173 -0
- package/components/stats/meta.json +150 -4
- package/components/steps/Steps.astro +36 -8
- package/components/steps/fixtures.json +217 -5
- package/components/steps/meta.json +156 -3
- package/components/team/Team.astro +52 -8
- package/components/team/fixtures.json +217 -4
- package/components/team/meta.json +167 -3
- package/components/team/team.ts +9 -0
- package/components/testimonial/Testimonial.astro +71 -21
- package/components/testimonial/fixtures.json +73 -0
- package/components/testimonial/meta.json +166 -3
- package/mapping/classic.json +7 -1
- package/mapping/divi.json +21 -4
- package/mapping/elementor.json +33 -8
- package/mapping/gutenberg.json +20 -4
- package/package.json +1 -1
|
@@ -2,24 +2,47 @@
|
|
|
2
2
|
// A row of figures, each with a short label, under an optional heading and
|
|
3
3
|
// intro. Figures are static text: no counting animation.
|
|
4
4
|
import { tv } from 'tailwind-variants'
|
|
5
|
+
import KitImage from '../_shared/KitImage.astro'
|
|
5
6
|
import Section, { type Tone } from '../_shared/Section.astro'
|
|
7
|
+
import { imageOf, type ImageSource } from '../_shared/image'
|
|
8
|
+
import { measuredGrid } from '../_shared/measured'
|
|
9
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
6
10
|
|
|
7
11
|
const stats = tv({
|
|
8
12
|
slots: {
|
|
9
13
|
list: 'grid grid-cols-2 gap-6 md:gap-8',
|
|
10
14
|
item: 'flex gap-1',
|
|
11
15
|
value: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight tabular-nums md:text-5xl',
|
|
16
|
+
icon: 'mb-3 block size-10 object-contain',
|
|
12
17
|
label: 'text-sm opacity-80',
|
|
13
18
|
},
|
|
14
19
|
variants: {
|
|
20
|
+
// grid: figures in columns. inline: one line that wraps, for two to four short figures.
|
|
21
|
+
layout: {
|
|
22
|
+
grid: {},
|
|
23
|
+
inline: { list: 'flex flex-wrap gap-x-12 gap-y-6 md:gap-x-16' },
|
|
24
|
+
},
|
|
15
25
|
columns: {
|
|
16
26
|
'2': { list: 'md:grid-cols-2' },
|
|
17
27
|
'3': { list: 'md:grid-cols-3' },
|
|
18
28
|
'4': { list: 'md:grid-cols-4' },
|
|
29
|
+
'5': { list: 'md:grid-cols-3 lg:grid-cols-5' },
|
|
30
|
+
'6': { list: 'md:grid-cols-3 lg:grid-cols-6' },
|
|
19
31
|
},
|
|
20
32
|
style: {
|
|
21
33
|
plain: {},
|
|
22
34
|
card: { item: 'rounded-[var(--radius-card)] bg-surface p-6 text-ink ring-1 ring-line/60' },
|
|
35
|
+
// Hairlines between the figures, none before the first, no box: across on a wide screen, above each on a phone.
|
|
36
|
+
// Meant for a single row (figures no more than the columns); a wrapped row starts with a line.
|
|
37
|
+
divided: { item: 'border-t border-line pt-6 first:border-t-0 first:pt-0 md:border-t-0 md:pt-0 md:border-s md:ps-6 md:first:border-s-0 md:first:ps-0' },
|
|
38
|
+
},
|
|
39
|
+
align: { start: {}, center: { item: 'items-center text-center', icon: 'mx-auto' } },
|
|
40
|
+
// The figure's size: md, lg (the default), xl, or a display number.
|
|
41
|
+
valueSize: {
|
|
42
|
+
md: { value: 'text-3xl md:text-4xl' },
|
|
43
|
+
lg: {},
|
|
44
|
+
xl: { value: 'text-5xl md:text-6xl' },
|
|
45
|
+
display: { value: 'text-6xl md:text-7xl' },
|
|
23
46
|
},
|
|
24
47
|
// accent/accent-ink is only ~5:1 — dimming text on it drops it under 4.5:1 (WCAG 1.4.3).
|
|
25
48
|
tone: { default: {}, muted: {}, inverse: {}, accent: { label: 'opacity-100' } },
|
|
@@ -29,7 +52,7 @@ const stats = tv({
|
|
|
29
52
|
// Elementor counter names itself first). The figure is drawn on top either way.
|
|
30
53
|
order: { 'label-first': { item: 'flex-col-reverse' }, 'value-first': { item: 'flex-col' } },
|
|
31
54
|
},
|
|
32
|
-
defaultVariants: { columns: '3', style: 'plain', tone: 'default', labelSize: 'sm', order: 'label-first' },
|
|
55
|
+
defaultVariants: { layout: 'grid', columns: '3', style: 'plain', align: 'start', valueSize: 'lg', tone: 'default', labelSize: 'sm', order: 'label-first' },
|
|
33
56
|
})
|
|
34
57
|
|
|
35
58
|
interface Item {
|
|
@@ -37,26 +60,35 @@ interface Item {
|
|
|
37
60
|
label: string
|
|
38
61
|
prefix?: string | undefined
|
|
39
62
|
suffix?: string | undefined
|
|
63
|
+
/** A small picture above the figure (its address); decorative, so it carries no text. */
|
|
64
|
+
icon?: ImageSource | undefined
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
interface Props {
|
|
43
68
|
heading?: string | undefined
|
|
44
69
|
intro?: string | undefined
|
|
45
70
|
items: Item[]
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
layout?: 'grid' | 'inline' | undefined
|
|
72
|
+
columns?: '2' | '3' | '4' | '5' | '6' | undefined
|
|
73
|
+
style?: 'plain' | 'card' | 'divided' | undefined
|
|
74
|
+
align?: 'start' | 'center' | undefined
|
|
75
|
+
valueSize?: 'md' | 'lg' | 'xl' | 'display' | undefined
|
|
48
76
|
labelSize?: 'sm' | 'base' | 'lg' | undefined
|
|
49
77
|
order?: 'label-first' | 'value-first' | undefined
|
|
50
78
|
tone?: Tone | undefined
|
|
79
|
+
/** 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. */
|
|
80
|
+
measured?: MeasuredStyle | undefined
|
|
51
81
|
id?: string | undefined
|
|
52
82
|
}
|
|
53
83
|
|
|
54
|
-
const { heading, intro, items, columns, style, labelSize, order, tone, id } = Astro.props
|
|
55
|
-
const s = stats({ columns, style, labelSize, order, tone })
|
|
84
|
+
const { heading, intro, items, layout, columns, style, align, valueSize, labelSize, order, tone, measured, id } = Astro.props
|
|
85
|
+
const s = stats({ layout, columns, style, align, valueSize, labelSize, order, tone })
|
|
86
|
+
const grid = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
|
|
56
87
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
88
|
+
const iconOf = (item: Item) => imageOf(item.icon, '')
|
|
57
89
|
// A <dl> names its term first, so figure-first is a plain list, read in the order it is drawn (WCAG 1.3.2).
|
|
58
90
|
---
|
|
59
|
-
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
91
|
+
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
60
92
|
{(heading || intro) && (
|
|
61
93
|
<header class="mb-10 flex max-w-2xl flex-col gap-3">
|
|
62
94
|
{heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
|
|
@@ -64,20 +96,20 @@ const headingId = heading && id ? `${id}-title` : undefined
|
|
|
64
96
|
</header>
|
|
65
97
|
)}
|
|
66
98
|
{order === 'value-first' ? (
|
|
67
|
-
<ul class={s.list()} role="list">
|
|
99
|
+
<ul class={s.list({ class: grid })} role="list">
|
|
68
100
|
{items.map(item => (
|
|
69
101
|
<li class={s.item()}>
|
|
70
|
-
<p class={s.value()}>{item.prefix}{item.value}{item.suffix}</p>
|
|
102
|
+
<p class={s.value()}>{iconOf(item) && <KitImage image={iconOf(item)!} sizes="40px" class={s.icon()} />}{item.prefix}{item.value}{item.suffix}</p>
|
|
71
103
|
<p class={s.label()}>{item.label}</p>
|
|
72
104
|
</li>
|
|
73
105
|
))}
|
|
74
106
|
</ul>
|
|
75
107
|
) : (
|
|
76
|
-
<dl class={s.list()}>
|
|
108
|
+
<dl class={s.list({ class: grid })}>
|
|
77
109
|
{items.map(item => (
|
|
78
110
|
<div class={s.item()}>
|
|
79
111
|
<dt class={s.label()}>{item.label}</dt>
|
|
80
|
-
<dd class={s.value()}>{item.prefix}{item.value}{item.suffix}</dd>
|
|
112
|
+
<dd class={s.value()}>{iconOf(item) && <KitImage image={iconOf(item)!} sizes="40px" class={s.icon()} />}{item.prefix}{item.value}{item.suffix}</dd>
|
|
81
113
|
</div>
|
|
82
114
|
))}
|
|
83
115
|
</dl>
|
|
@@ -156,5 +156,178 @@
|
|
|
156
156
|
}
|
|
157
157
|
]
|
|
158
158
|
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "divided-center-md",
|
|
162
|
+
"props": {
|
|
163
|
+
"items": [
|
|
164
|
+
{
|
|
165
|
+
"value": "12",
|
|
166
|
+
"label": "Years in business",
|
|
167
|
+
"suffix": "+"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"value": "480",
|
|
171
|
+
"label": "Clients served"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"value": "99.9",
|
|
175
|
+
"label": "Uptime",
|
|
176
|
+
"suffix": "%"
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
"style": "divided",
|
|
180
|
+
"align": "center",
|
|
181
|
+
"valueSize": "md"
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "inline-xl",
|
|
186
|
+
"props": {
|
|
187
|
+
"items": [
|
|
188
|
+
{
|
|
189
|
+
"value": "12",
|
|
190
|
+
"label": "Years in business",
|
|
191
|
+
"suffix": "+"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"value": "480",
|
|
195
|
+
"label": "Clients served"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"value": "99.9",
|
|
199
|
+
"label": "Uptime",
|
|
200
|
+
"suffix": "%"
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"layout": "inline",
|
|
204
|
+
"valueSize": "xl",
|
|
205
|
+
"heading": "In numbers"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "display-icons",
|
|
210
|
+
"props": {
|
|
211
|
+
"items": [
|
|
212
|
+
{
|
|
213
|
+
"value": "12",
|
|
214
|
+
"label": "Years in business",
|
|
215
|
+
"suffix": "+",
|
|
216
|
+
"icon": "/kit-fixtures/logo.svg"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"value": "480",
|
|
220
|
+
"label": "Clients served",
|
|
221
|
+
"icon": "/kit-fixtures/logo.svg"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"value": "99.9",
|
|
225
|
+
"label": "Uptime",
|
|
226
|
+
"suffix": "%",
|
|
227
|
+
"icon": "/kit-fixtures/logo.svg"
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
"valueSize": "display"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "five-columns",
|
|
235
|
+
"props": {
|
|
236
|
+
"items": [
|
|
237
|
+
{
|
|
238
|
+
"value": "12",
|
|
239
|
+
"label": "Years in business",
|
|
240
|
+
"suffix": "+"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"value": "480",
|
|
244
|
+
"label": "Clients served"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"value": "99.9",
|
|
248
|
+
"label": "Uptime",
|
|
249
|
+
"suffix": "%"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"value": "7",
|
|
253
|
+
"label": "Offices"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"value": "24/7",
|
|
257
|
+
"label": "Support"
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
"columns": "5"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"name": "six-columns",
|
|
265
|
+
"props": {
|
|
266
|
+
"items": [
|
|
267
|
+
{
|
|
268
|
+
"value": "12",
|
|
269
|
+
"label": "Years in business",
|
|
270
|
+
"suffix": "+"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"value": "480",
|
|
274
|
+
"label": "Clients served"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"value": "99.9",
|
|
278
|
+
"label": "Uptime",
|
|
279
|
+
"suffix": "%"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"value": "7",
|
|
283
|
+
"label": "Offices"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"value": "24/7",
|
|
287
|
+
"label": "Support"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"value": "3",
|
|
291
|
+
"label": "Awards"
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"columns": "6"
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "measured-columns",
|
|
299
|
+
"props": {
|
|
300
|
+
"heading": "By the numbers",
|
|
301
|
+
"intro": "A decade of shipping sites for small teams.",
|
|
302
|
+
"items": [
|
|
303
|
+
{
|
|
304
|
+
"value": "12",
|
|
305
|
+
"label": "Years in business",
|
|
306
|
+
"suffix": "+"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"value": "480",
|
|
310
|
+
"label": "Clients served"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"value": "99.9",
|
|
314
|
+
"label": "Uptime",
|
|
315
|
+
"suffix": "%"
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
"measured": {
|
|
319
|
+
"columns": {
|
|
320
|
+
"390": 1,
|
|
321
|
+
"768": 2,
|
|
322
|
+
"1280": 4
|
|
323
|
+
},
|
|
324
|
+
"gap": 40,
|
|
325
|
+
"padY": 96,
|
|
326
|
+
"bg": {
|
|
327
|
+
"color": "#0f172a"
|
|
328
|
+
},
|
|
329
|
+
"tone": "dark"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
159
332
|
}
|
|
160
333
|
]
|
|
@@ -5,27 +5,58 @@
|
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": [
|
|
8
|
+
"KitImage.astro",
|
|
8
9
|
"Section.astro",
|
|
10
|
+
"image.ts",
|
|
9
11
|
"measured.ts",
|
|
10
12
|
"types.ts"
|
|
11
13
|
],
|
|
12
14
|
"variants": {
|
|
15
|
+
"layout": {
|
|
16
|
+
"options": [
|
|
17
|
+
"grid",
|
|
18
|
+
"inline"
|
|
19
|
+
],
|
|
20
|
+
"default": "grid",
|
|
21
|
+
"description": "grid: figures in columns. inline: one line that wraps, for two to four short figures."
|
|
22
|
+
},
|
|
13
23
|
"columns": {
|
|
14
24
|
"options": [
|
|
15
25
|
"2",
|
|
16
26
|
"3",
|
|
17
|
-
"4"
|
|
27
|
+
"4",
|
|
28
|
+
"5",
|
|
29
|
+
"6"
|
|
18
30
|
],
|
|
19
31
|
"default": "3",
|
|
20
|
-
"description": "Figures per row on wide screens; two on phones."
|
|
32
|
+
"description": "Figures per row on wide screens (grid layout); two on phones."
|
|
21
33
|
},
|
|
22
34
|
"style": {
|
|
23
35
|
"options": [
|
|
24
36
|
"plain",
|
|
25
|
-
"card"
|
|
37
|
+
"card",
|
|
38
|
+
"divided"
|
|
26
39
|
],
|
|
27
40
|
"default": "plain",
|
|
28
|
-
"description": "plain: figures on the section background. card: each figure in its own box."
|
|
41
|
+
"description": "plain: figures on the section background. card: each figure in its own box. divided: hairlines between the figures (across on a wide screen, above each on a phone), no box; for a single row."
|
|
42
|
+
},
|
|
43
|
+
"align": {
|
|
44
|
+
"options": [
|
|
45
|
+
"start",
|
|
46
|
+
"center"
|
|
47
|
+
],
|
|
48
|
+
"default": "start",
|
|
49
|
+
"description": "Alignment of each figure and its label."
|
|
50
|
+
},
|
|
51
|
+
"valueSize": {
|
|
52
|
+
"options": [
|
|
53
|
+
"md",
|
|
54
|
+
"lg",
|
|
55
|
+
"xl",
|
|
56
|
+
"display"
|
|
57
|
+
],
|
|
58
|
+
"default": "lg",
|
|
59
|
+
"description": "The figure's size: md, lg, xl or a display-size number."
|
|
29
60
|
},
|
|
30
61
|
"tone": {
|
|
31
62
|
"options": [
|
|
@@ -94,11 +125,126 @@
|
|
|
94
125
|
"type": "string",
|
|
95
126
|
"label": "Suffix",
|
|
96
127
|
"description": "Printed after the figure (`+`, `%`, `k`)."
|
|
128
|
+
},
|
|
129
|
+
"icon": {
|
|
130
|
+
"type": "image",
|
|
131
|
+
"label": "Icon",
|
|
132
|
+
"description": "A small picture above the figure; decorative."
|
|
97
133
|
}
|
|
98
134
|
}
|
|
99
135
|
},
|
|
100
136
|
"description": "The figures, in order (Elementor counter, Divi number counter, a Gutenberg columns row of big numbers)."
|
|
101
137
|
},
|
|
138
|
+
"measured": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"content": false,
|
|
141
|
+
"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.",
|
|
142
|
+
"fields": {
|
|
143
|
+
"containerPx": {
|
|
144
|
+
"type": "integer",
|
|
145
|
+
"min": 0,
|
|
146
|
+
"max": 2560,
|
|
147
|
+
"description": "The frame's content width."
|
|
148
|
+
},
|
|
149
|
+
"padY": {
|
|
150
|
+
"type": "integer",
|
|
151
|
+
"min": 0,
|
|
152
|
+
"max": 400,
|
|
153
|
+
"description": "Top and bottom padding."
|
|
154
|
+
},
|
|
155
|
+
"padX": {
|
|
156
|
+
"type": "integer",
|
|
157
|
+
"min": 0,
|
|
158
|
+
"max": 200,
|
|
159
|
+
"description": "The frame's side padding (gutter)."
|
|
160
|
+
},
|
|
161
|
+
"bg": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"description": "The section's background.",
|
|
164
|
+
"fields": {
|
|
165
|
+
"color": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
168
|
+
},
|
|
169
|
+
"image": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
172
|
+
},
|
|
173
|
+
"overlay": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
176
|
+
},
|
|
177
|
+
"size": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
180
|
+
},
|
|
181
|
+
"position": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"columns": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
190
|
+
"fields": {
|
|
191
|
+
"390": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"min": 1,
|
|
194
|
+
"max": 6,
|
|
195
|
+
"description": "Phone."
|
|
196
|
+
},
|
|
197
|
+
"768": {
|
|
198
|
+
"type": "integer",
|
|
199
|
+
"min": 1,
|
|
200
|
+
"max": 6,
|
|
201
|
+
"description": "Tablet."
|
|
202
|
+
},
|
|
203
|
+
"1280": {
|
|
204
|
+
"type": "integer",
|
|
205
|
+
"min": 1,
|
|
206
|
+
"max": 6,
|
|
207
|
+
"description": "Desktop."
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"gap": {
|
|
212
|
+
"type": "integer",
|
|
213
|
+
"min": 0,
|
|
214
|
+
"max": 200,
|
|
215
|
+
"description": "Gap between items."
|
|
216
|
+
},
|
|
217
|
+
"tone": {
|
|
218
|
+
"type": "select",
|
|
219
|
+
"options": [
|
|
220
|
+
"light",
|
|
221
|
+
"dark"
|
|
222
|
+
],
|
|
223
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
224
|
+
},
|
|
225
|
+
"align": {
|
|
226
|
+
"type": "select",
|
|
227
|
+
"options": [
|
|
228
|
+
"start",
|
|
229
|
+
"center",
|
|
230
|
+
"end"
|
|
231
|
+
],
|
|
232
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
233
|
+
},
|
|
234
|
+
"radius": {
|
|
235
|
+
"type": "integer",
|
|
236
|
+
"min": 0,
|
|
237
|
+
"max": 200,
|
|
238
|
+
"description": "Card and item corner radius."
|
|
239
|
+
},
|
|
240
|
+
"minHeight": {
|
|
241
|
+
"type": "integer",
|
|
242
|
+
"min": 0,
|
|
243
|
+
"max": 2000,
|
|
244
|
+
"description": "The section's minimum height."
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
102
248
|
"id": {
|
|
103
249
|
"type": "string",
|
|
104
250
|
"content": false,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
// How it works: an ordered sequence of steps, each a title and a line of
|
|
3
3
|
// text, optionally with an image — as a vertical list or a row across the
|
|
4
|
-
// page, numbered or plain.
|
|
4
|
+
// page, numbered or plain. `kind="timeline"` is the dated variant: a date per
|
|
5
|
+
// item on a vertical rail, optionally alternating left and right of it.
|
|
5
6
|
import { tv } from 'tailwind-variants'
|
|
6
7
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
8
|
import Section from '../_shared/Section.astro'
|
|
9
|
+
import { measuredGrid } from '../_shared/measured'
|
|
10
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
8
11
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
9
12
|
|
|
10
13
|
const steps = tv({
|
|
@@ -14,6 +17,7 @@ const steps = tv({
|
|
|
14
17
|
intro: 'text-lg opacity-80',
|
|
15
18
|
list: 'grid gap-8 [counter-reset:kit-step]',
|
|
16
19
|
step: 'flex gap-4 [counter-increment:kit-step]',
|
|
20
|
+
date: 'text-sm font-semibold tabular-nums text-accent-text',
|
|
17
21
|
body: 'flex flex-col gap-2',
|
|
18
22
|
badge: 'shrink-0',
|
|
19
23
|
media: 'mb-2 overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
@@ -24,7 +28,14 @@ const steps = tv({
|
|
|
24
28
|
layout: {
|
|
25
29
|
list: { list: 'max-w-3xl' },
|
|
26
30
|
row: { list: 'sm:grid-cols-2 lg:auto-cols-fr lg:grid-flow-col lg:grid-cols-none', step: 'flex-col' },
|
|
31
|
+
// One rail on the inline-start side (the item's own side is the rail's, mirrored in RTL by the logical `ps-`/`border-s`).
|
|
32
|
+
rail: { list: 'max-w-3xl gap-0', step: 'relative border-s-2 border-line pb-8 ps-6 last:pb-0' },
|
|
33
|
+
// Items alternate either side of a centre rail from `md`; the DOM stays chronological, one rail on the start side on phones.
|
|
34
|
+
alternate: { list: 'gap-0 md:grid-cols-2', step: 'relative border-s-2 border-line pb-8 ps-6 md:[&:nth-child(even)]:col-start-2 md:[&:nth-child(odd)]:col-start-1 md:[&:nth-child(odd)]:border-s-0 md:[&:nth-child(odd)]:border-e-2 md:[&:nth-child(odd)]:pe-6 md:[&:nth-child(odd)]:ps-0 md:[&:nth-child(odd)]:text-end md:[&:nth-child(odd)]:flex-row-reverse md:[&:nth-child(even)]:-ms-px md:[&:nth-child(odd)]:after:start-auto md:[&:nth-child(odd)]:after:-end-[7px]' },
|
|
27
35
|
},
|
|
36
|
+
// What sits on the rail: the number, a plain dot, or the date (the timeline's own marker). The number marker is `style`'s.
|
|
37
|
+
marker: { number: {}, dot: { step: 'before:hidden after:absolute after:top-1.5 after:-start-[7px] after:size-3 after:rounded-full after:bg-accent after:content-[""]' } },
|
|
38
|
+
connector: { none: {}, line: {}, dashed: { step: 'border-dashed' } },
|
|
28
39
|
// The number is drawn from a CSS counter, so the list stays one <ol> a screen reader counts itself. A number the
|
|
29
40
|
// source wrote (`01`) is the page's text and is shown as written, hidden from the screen reader that already counts.
|
|
30
41
|
style: {
|
|
@@ -35,7 +46,7 @@ const steps = tv({
|
|
|
35
46
|
plain: { step: 'border-s-2 border-accent ps-4', badge: 'font-semibold tabular-nums' },
|
|
36
47
|
},
|
|
37
48
|
},
|
|
38
|
-
defaultVariants: { layout: 'list', style: 'numbered' },
|
|
49
|
+
defaultVariants: { layout: 'list', marker: 'number', connector: 'line', style: 'numbered' },
|
|
39
50
|
})
|
|
40
51
|
|
|
41
52
|
interface Step {
|
|
@@ -43,6 +54,10 @@ interface Step {
|
|
|
43
54
|
number?: string | undefined
|
|
44
55
|
title: string
|
|
45
56
|
text: string
|
|
57
|
+
/** A timeline's date as the source writes it (`2019`, `Mar 2021`); `datetime` only when it parses as an ISO date or year. */
|
|
58
|
+
date?: string | undefined
|
|
59
|
+
/** Marks the latest item: a visually hidden "Current" is added for readers. */
|
|
60
|
+
current?: boolean | undefined
|
|
46
61
|
/** The image's address. */
|
|
47
62
|
image?: ImageSource | undefined
|
|
48
63
|
imageAlt?: string | undefined
|
|
@@ -52,30 +67,43 @@ interface Props {
|
|
|
52
67
|
heading?: string | undefined
|
|
53
68
|
intro?: string | undefined
|
|
54
69
|
items: Step[]
|
|
55
|
-
|
|
70
|
+
/** `steps` numbers a sequence; `timeline` carries a date per item (the rail layouts below). */
|
|
71
|
+
kind?: 'steps' | 'timeline' | undefined
|
|
72
|
+
layout?: 'list' | 'row' | 'rail' | 'alternate' | undefined
|
|
73
|
+
marker?: 'number' | 'dot' | undefined
|
|
74
|
+
connector?: 'none' | 'line' | 'dashed' | undefined
|
|
75
|
+
currentLabel?: string | undefined
|
|
56
76
|
style?: 'numbered' | 'plain' | undefined
|
|
77
|
+
/** 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. */
|
|
78
|
+
measured?: MeasuredStyle | undefined
|
|
57
79
|
id?: string | undefined
|
|
58
80
|
}
|
|
59
81
|
|
|
60
|
-
const { heading, intro, items, layout, style, id } = Astro.props
|
|
61
|
-
|
|
82
|
+
const { heading, intro, items, kind = 'steps', layout: givenLayout, marker, connector, currentLabel = 'Current', style, measured, id } = Astro.props
|
|
83
|
+
// A timeline defaults to the rail layout and, without numbers, to the plain dot marker.
|
|
84
|
+
const layout = givenLayout ?? (kind === 'timeline' ? 'rail' : 'list')
|
|
85
|
+
const s = steps({ layout, marker: marker ?? (kind === 'timeline' ? 'dot' : 'number'), connector, style })
|
|
86
|
+
// A bare year or an ISO date is machine-readable; anything else ("Spring 2021") stays text.
|
|
87
|
+
const isoDate = (date: string) => /^\d{4}(-\d{2}(-\d{2})?)?$/.test(date.trim()) ? date.trim() : undefined
|
|
62
88
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
63
89
|
// One level under the section heading, or the section's heading when there is none.
|
|
64
90
|
const StepHeading = heading ? 'h3' : 'h2'
|
|
65
91
|
---
|
|
66
92
|
{items.length > 0 && (
|
|
67
|
-
<Section id={id} aria-labelledby={headingId}>
|
|
93
|
+
<Section measured={measured} id={id} aria-labelledby={headingId}>
|
|
68
94
|
{(heading || intro) && (
|
|
69
95
|
<header class={s.header()}>
|
|
70
96
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
71
97
|
{intro && <p class={s.intro()} data-kit-text>{intro}</p>}
|
|
72
98
|
</header>
|
|
73
99
|
)}
|
|
74
|
-
<ol class={s.list()}>
|
|
100
|
+
<ol class={s.list({ class: layout === 'row' ? measuredGrid(measured) : undefined })}>
|
|
75
101
|
{items.map(step => (
|
|
76
|
-
<li class={s.step({ class: step.number ? 'before:hidden' : undefined })}>
|
|
102
|
+
<li class={s.step({ class: step.number || kind === 'timeline' ? 'before:hidden' : undefined })} aria-current={step.current ? 'step' : undefined}>
|
|
77
103
|
{step.number && <span class={s.badge()} aria-hidden="true">{step.number}</span>}
|
|
78
104
|
<div class={s.body()}>
|
|
105
|
+
{kind === 'timeline' && step.date && (isoDate(step.date) ? <time class={s.date()} datetime={isoDate(step.date)}>{step.date}</time> : <p class={s.date()}>{step.date}</p>)}
|
|
106
|
+
{step.current && <span class="sr-only">{currentLabel}</span>}
|
|
79
107
|
{imageOf(step.image, step.imageAlt) && <div class={s.media()}><KitImage image={imageOf(step.image, step.imageAlt)!} sizes="(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw" class="aspect-[3/2] w-full object-cover" /></div>}
|
|
80
108
|
<StepHeading class={s.title()}>{step.title}</StepHeading>
|
|
81
109
|
<p class={s.text()} data-kit-text>{step.text}</p>
|