@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
|
@@ -1,6 +1,219 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
{
|
|
3
|
+
"name": "three-card",
|
|
4
|
+
"props": {
|
|
5
|
+
"id": "team",
|
|
6
|
+
"heading": "The team",
|
|
7
|
+
"intro": "Small, senior, and in the same room.",
|
|
8
|
+
"items": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Mira Kovač",
|
|
11
|
+
"role": "Editor",
|
|
12
|
+
"bio": "Keeps every page honest and every sentence short.",
|
|
13
|
+
"image": "/kit-fixtures/avatar-1.svg",
|
|
14
|
+
"href": "/team/mira/"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Jon Ade",
|
|
18
|
+
"role": "Engineer",
|
|
19
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
20
|
+
"image": "/kit-fixtures/avatar-2.svg",
|
|
21
|
+
"href": "/team/jon/"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Lea Park",
|
|
25
|
+
"role": "Designer",
|
|
26
|
+
"bio": "Draws the systems the sites are made of.",
|
|
27
|
+
"image": "/kit-fixtures/avatar-3.svg"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "two-plain",
|
|
34
|
+
"props": {
|
|
35
|
+
"columns": "2",
|
|
36
|
+
"style": "plain",
|
|
37
|
+
"heading": "Founders",
|
|
38
|
+
"items": [
|
|
39
|
+
{
|
|
40
|
+
"name": "Mira Kovač",
|
|
41
|
+
"role": "Co-founder",
|
|
42
|
+
"image": "/kit-fixtures/avatar-1.svg",
|
|
43
|
+
"imageAlt": "Mira Kovač, smiling"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Jon Ade",
|
|
47
|
+
"role": "Co-founder",
|
|
48
|
+
"image": "/kit-fixtures/avatar-2.svg"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "four-card-no-photos",
|
|
55
|
+
"props": {
|
|
56
|
+
"columns": "4",
|
|
57
|
+
"items": [
|
|
58
|
+
{
|
|
59
|
+
"name": "Mira Kovač",
|
|
60
|
+
"role": "Editor"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "Jon Ade",
|
|
64
|
+
"role": "Engineer"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "Lea Park",
|
|
68
|
+
"role": "Designer"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "Sam Ortiz",
|
|
72
|
+
"role": "Producer",
|
|
73
|
+
"bio": "Keeps the schedule."
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "three-plain-role",
|
|
80
|
+
"props": {
|
|
81
|
+
"heading": "Our team",
|
|
82
|
+
"style": "plain",
|
|
83
|
+
"roleStyle": "plain",
|
|
84
|
+
"items": [
|
|
85
|
+
{
|
|
86
|
+
"name": "Mira Kovač",
|
|
87
|
+
"role": "Editor"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "Jon Ade",
|
|
91
|
+
"role": "Photographer"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "Lea Park",
|
|
95
|
+
"role": "Route planner"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "five-circle",
|
|
102
|
+
"props": {
|
|
103
|
+
"columns": "5",
|
|
104
|
+
"imageShape": "circle",
|
|
105
|
+
"align": "center",
|
|
106
|
+
"style": "plain",
|
|
107
|
+
"items": [
|
|
108
|
+
{
|
|
109
|
+
"name": "Mira Kovač",
|
|
110
|
+
"role": "Editor",
|
|
111
|
+
"bio": "Keeps every page honest and every sentence short.",
|
|
112
|
+
"image": "/kit-fixtures/avatar-1.svg",
|
|
113
|
+
"href": "/team/mira/"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "Jon Ade",
|
|
117
|
+
"role": "Engineer",
|
|
118
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
119
|
+
"image": "/kit-fixtures/avatar-2.svg",
|
|
120
|
+
"href": "/team/jon/"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "Lea Park",
|
|
124
|
+
"role": "Designer",
|
|
125
|
+
"bio": "Draws the systems the sites are made of.",
|
|
126
|
+
"image": "/kit-fixtures/avatar-3.svg"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "Ana Ruiz",
|
|
130
|
+
"role": "Editor",
|
|
131
|
+
"bio": "Keeps every page honest and every sentence short.",
|
|
132
|
+
"image": "/kit-fixtures/avatar-1.svg"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "Kai Berg",
|
|
136
|
+
"role": "Engineer",
|
|
137
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
138
|
+
"image": "/kit-fixtures/avatar-2.svg"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "list-rounded",
|
|
145
|
+
"props": {
|
|
146
|
+
"layout": "list",
|
|
147
|
+
"imageShape": "rounded",
|
|
148
|
+
"items": [
|
|
149
|
+
{
|
|
150
|
+
"name": "Mira Kovač",
|
|
151
|
+
"role": "Editor",
|
|
152
|
+
"bio": "Keeps every page honest and every sentence short.",
|
|
153
|
+
"image": "/kit-fixtures/avatar-1.svg",
|
|
154
|
+
"href": "/team/mira/"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "Jon Ade",
|
|
158
|
+
"role": "Engineer",
|
|
159
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
160
|
+
"image": "/kit-fixtures/avatar-2.svg",
|
|
161
|
+
"href": "/team/jon/"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "featured-socials",
|
|
168
|
+
"props": {
|
|
169
|
+
"layout": "featured",
|
|
170
|
+
"items": [
|
|
171
|
+
{
|
|
172
|
+
"name": "Mira Kovač",
|
|
173
|
+
"role": "Editor",
|
|
174
|
+
"bio": "Keeps every page honest and every sentence short.",
|
|
175
|
+
"image": "/kit-fixtures/avatar-1.svg",
|
|
176
|
+
"href": "/team/mira/",
|
|
177
|
+
"links": [
|
|
178
|
+
"https://www.linkedin.com/in/mira",
|
|
179
|
+
"https://github.com/mira",
|
|
180
|
+
"https://mira.example"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "Jon Ade",
|
|
185
|
+
"role": "Engineer",
|
|
186
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
187
|
+
"image": "/kit-fixtures/avatar-2.svg",
|
|
188
|
+
"href": "/team/jon/"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "Lea Park",
|
|
192
|
+
"role": "Designer",
|
|
193
|
+
"bio": "Draws the systems the sites are made of.",
|
|
194
|
+
"image": "/kit-fixtures/avatar-3.svg"
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "bio-clamp",
|
|
201
|
+
"props": {
|
|
202
|
+
"bioClamp": 2,
|
|
203
|
+
"items": [
|
|
204
|
+
{
|
|
205
|
+
"name": "Mira Kovač",
|
|
206
|
+
"role": "Editor",
|
|
207
|
+
"bio": "Spent twelve years editing a daily paper before moving to the web, where she keeps every page honest, every sentence short and every headline checked twice against the story under it.",
|
|
208
|
+
"image": "/kit-fixtures/avatar-1.svg"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "Jon Ade",
|
|
212
|
+
"role": "Engineer",
|
|
213
|
+
"bio": "Builds the sites and keeps them fast.",
|
|
214
|
+
"image": "/kit-fixtures/avatar-2.svg"
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
6
219
|
]
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
"options": [
|
|
17
17
|
"2",
|
|
18
18
|
"3",
|
|
19
|
-
"4"
|
|
19
|
+
"4",
|
|
20
|
+
"5"
|
|
20
21
|
],
|
|
21
22
|
"default": "3",
|
|
22
|
-
"description": "
|
|
23
|
+
"description": "Members per row on wide screens; one on phones, two on tablets."
|
|
23
24
|
},
|
|
24
25
|
"style": {
|
|
25
26
|
"options": [
|
|
@@ -36,6 +37,32 @@
|
|
|
36
37
|
],
|
|
37
38
|
"default": "accent",
|
|
38
39
|
"description": "The member's role. accent: a small accent-coloured line. plain: body-size text in the text colour, as a byline."
|
|
40
|
+
},
|
|
41
|
+
"layout": {
|
|
42
|
+
"options": [
|
|
43
|
+
"grid",
|
|
44
|
+
"list",
|
|
45
|
+
"featured"
|
|
46
|
+
],
|
|
47
|
+
"default": "grid",
|
|
48
|
+
"description": "grid: cards in columns. list: one row each, photo beside text. featured: the first member large over a grid."
|
|
49
|
+
},
|
|
50
|
+
"imageShape": {
|
|
51
|
+
"options": [
|
|
52
|
+
"square",
|
|
53
|
+
"rounded",
|
|
54
|
+
"circle"
|
|
55
|
+
],
|
|
56
|
+
"default": "square",
|
|
57
|
+
"description": "Shape of the portrait."
|
|
58
|
+
},
|
|
59
|
+
"align": {
|
|
60
|
+
"options": [
|
|
61
|
+
"start",
|
|
62
|
+
"center"
|
|
63
|
+
],
|
|
64
|
+
"default": "start",
|
|
65
|
+
"description": "Text and socials from the start edge, or centred."
|
|
39
66
|
}
|
|
40
67
|
},
|
|
41
68
|
"props": {
|
|
@@ -86,6 +113,14 @@
|
|
|
86
113
|
"type": "url",
|
|
87
114
|
"label": "Link",
|
|
88
115
|
"description": "Their profile page; makes the whole entry a link."
|
|
116
|
+
},
|
|
117
|
+
"links": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"label": "Profile links",
|
|
120
|
+
"description": "Addresses of the member's social profiles, in order; each is labelled by the network its host names (LinkedIn, GitHub, …).",
|
|
121
|
+
"items": {
|
|
122
|
+
"type": "url"
|
|
123
|
+
}
|
|
89
124
|
}
|
|
90
125
|
}
|
|
91
126
|
},
|
|
@@ -95,6 +130,135 @@
|
|
|
95
130
|
"type": "string",
|
|
96
131
|
"content": false,
|
|
97
132
|
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
133
|
+
},
|
|
134
|
+
"bioClamp": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"min": 2,
|
|
137
|
+
"max": 4,
|
|
138
|
+
"content": false,
|
|
139
|
+
"description": "Clamp a long bio to this many lines; the full text opens in a native <details> (\"Read more\"), no script."
|
|
140
|
+
},
|
|
141
|
+
"readMoreLabel": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"default": "Read more",
|
|
144
|
+
"content": false,
|
|
145
|
+
"description": "Text of the bio disclosure."
|
|
146
|
+
},
|
|
147
|
+
"socialLabel": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"default": "{name} on {network}",
|
|
150
|
+
"content": false,
|
|
151
|
+
"description": "Accessible name of a profile link; {name} and {network} are replaced."
|
|
152
|
+
},
|
|
153
|
+
"measured": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"content": false,
|
|
156
|
+
"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.",
|
|
157
|
+
"fields": {
|
|
158
|
+
"containerPx": {
|
|
159
|
+
"type": "integer",
|
|
160
|
+
"min": 0,
|
|
161
|
+
"max": 2560,
|
|
162
|
+
"description": "The frame's content width."
|
|
163
|
+
},
|
|
164
|
+
"padY": {
|
|
165
|
+
"type": "integer",
|
|
166
|
+
"min": 0,
|
|
167
|
+
"max": 400,
|
|
168
|
+
"description": "Top and bottom padding."
|
|
169
|
+
},
|
|
170
|
+
"padX": {
|
|
171
|
+
"type": "integer",
|
|
172
|
+
"min": 0,
|
|
173
|
+
"max": 200,
|
|
174
|
+
"description": "The frame's side padding (gutter)."
|
|
175
|
+
},
|
|
176
|
+
"bg": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"description": "The section's background.",
|
|
179
|
+
"fields": {
|
|
180
|
+
"color": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
183
|
+
},
|
|
184
|
+
"image": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
187
|
+
},
|
|
188
|
+
"overlay": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
191
|
+
},
|
|
192
|
+
"size": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
195
|
+
},
|
|
196
|
+
"position": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"columns": {
|
|
203
|
+
"type": "object",
|
|
204
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
205
|
+
"fields": {
|
|
206
|
+
"390": {
|
|
207
|
+
"type": "integer",
|
|
208
|
+
"min": 1,
|
|
209
|
+
"max": 6,
|
|
210
|
+
"description": "Phone."
|
|
211
|
+
},
|
|
212
|
+
"768": {
|
|
213
|
+
"type": "integer",
|
|
214
|
+
"min": 1,
|
|
215
|
+
"max": 6,
|
|
216
|
+
"description": "Tablet."
|
|
217
|
+
},
|
|
218
|
+
"1280": {
|
|
219
|
+
"type": "integer",
|
|
220
|
+
"min": 1,
|
|
221
|
+
"max": 6,
|
|
222
|
+
"description": "Desktop."
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"gap": {
|
|
227
|
+
"type": "integer",
|
|
228
|
+
"min": 0,
|
|
229
|
+
"max": 200,
|
|
230
|
+
"description": "Gap between items."
|
|
231
|
+
},
|
|
232
|
+
"tone": {
|
|
233
|
+
"type": "select",
|
|
234
|
+
"options": [
|
|
235
|
+
"light",
|
|
236
|
+
"dark"
|
|
237
|
+
],
|
|
238
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
239
|
+
},
|
|
240
|
+
"align": {
|
|
241
|
+
"type": "select",
|
|
242
|
+
"options": [
|
|
243
|
+
"start",
|
|
244
|
+
"center",
|
|
245
|
+
"end"
|
|
246
|
+
],
|
|
247
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
248
|
+
},
|
|
249
|
+
"radius": {
|
|
250
|
+
"type": "integer",
|
|
251
|
+
"min": 0,
|
|
252
|
+
"max": 200,
|
|
253
|
+
"description": "Card and item corner radius."
|
|
254
|
+
},
|
|
255
|
+
"minHeight": {
|
|
256
|
+
"type": "integer",
|
|
257
|
+
"min": 0,
|
|
258
|
+
"max": 2000,
|
|
259
|
+
"description": "The section's minimum height."
|
|
260
|
+
}
|
|
261
|
+
}
|
|
98
262
|
}
|
|
99
263
|
},
|
|
100
264
|
"slots": {},
|
|
@@ -111,7 +275,7 @@
|
|
|
111
275
|
"divi/et_pb_team_member"
|
|
112
276
|
]
|
|
113
277
|
},
|
|
114
|
-
"a11y": "Members are a <ul>; each name is an <h3> under the section's <h2> (an <h2> when the section has no heading), and a linked member's link text is the name. A portrait's text defaults to the name. The section is labelled by its heading when it has one.",
|
|
278
|
+
"a11y": "Members are a <ul>; each name is an <h3> under the section's <h2> (an <h2> when the section has no heading), and a linked member's link text is the name. A portrait's text defaults to the name. The section is labelled by its heading when it has one. A member with socials keeps the name link and the social links apart (no nested links); a clamped bio opens in a <details> with the whole text.",
|
|
115
279
|
"pattern": {
|
|
116
280
|
"summary": "repeated person units: portrait + name + role",
|
|
117
281
|
"signals": [
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The network a profile address belongs to, read from its host (`linkedin.com` → LinkedIn); an unknown host is shown as itself.
|
|
2
|
+
const NETWORKS: Record<string, string> = { 'linkedin.com': 'LinkedIn', 'github.com': 'GitHub', 'x.com': 'X', 'twitter.com': 'X', 'instagram.com': 'Instagram', 'facebook.com': 'Facebook', 'youtube.com': 'YouTube', 'dribbble.com': 'Dribbble', 'behance.net': 'Behance', 'medium.com': 'Medium' }
|
|
3
|
+
|
|
4
|
+
export const profile = (href: string) => {
|
|
5
|
+
let host = href
|
|
6
|
+
try { host = new URL(href).hostname.replace(/^www\./, '') } catch { /* a relative address keeps its own text */ }
|
|
7
|
+
// `mailto:` and `tel:` addresses have no host: the address itself is the label, never an empty one.
|
|
8
|
+
return { href, label: NETWORKS[host] ?? (host || href) }
|
|
9
|
+
}
|
|
@@ -6,6 +6,8 @@ import { tv } from 'tailwind-variants'
|
|
|
6
6
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
7
|
import Section from '../_shared/Section.astro'
|
|
8
8
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
9
|
+
import { measuredGrid } from '../_shared/measured'
|
|
10
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
9
11
|
|
|
10
12
|
export interface TestimonialItem {
|
|
11
13
|
quote: string
|
|
@@ -14,6 +16,11 @@ export interface TestimonialItem {
|
|
|
14
16
|
/** The portrait's address (or, until 0.7, an `ImageInput`). */
|
|
15
17
|
avatar?: ImageSource | undefined
|
|
16
18
|
avatarAlt?: string | undefined
|
|
19
|
+
/** Stars out of 5 (0 or absent: none shown). */
|
|
20
|
+
rating?: number | undefined
|
|
21
|
+
/** A company logo beside the name. */
|
|
22
|
+
logo?: ImageSource | undefined
|
|
23
|
+
logoAlt?: string | undefined
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
const testimonial = tv({
|
|
@@ -23,6 +30,10 @@ const testimonial = tv({
|
|
|
23
30
|
card: 'flex h-full flex-col gap-6',
|
|
24
31
|
quote: 'text-pretty',
|
|
25
32
|
person: 'mt-auto flex items-center gap-3 not-italic',
|
|
33
|
+
stars: 'flex gap-0.5 text-accent-text',
|
|
34
|
+
glyph: 'block text-5xl leading-none opacity-40 select-none',
|
|
35
|
+
avatar: 'size-12 shrink-0 rounded-full object-cover',
|
|
36
|
+
logo: 'ms-auto h-6 w-auto max-w-24 object-contain opacity-80',
|
|
26
37
|
name: 'font-semibold',
|
|
27
38
|
role: 'text-sm opacity-75',
|
|
28
39
|
},
|
|
@@ -30,8 +41,17 @@ const testimonial = tv({
|
|
|
30
41
|
layout: {
|
|
31
42
|
single: { heading: 'text-center', list: 'mx-auto max-w-3xl', card: 'items-center text-center', quote: 'text-2xl leading-snug font-medium md:text-3xl', person: 'mt-0' },
|
|
32
43
|
grid: { list: 'md:grid-cols-2 lg:grid-cols-3', card: 'rounded-card border border-line p-6', quote: 'text-base' },
|
|
44
|
+
// Native horizontal scroll with snap points; the next card peeks at the edge. No buttons, no script.
|
|
45
|
+
row: { list: 'flex snap-x snap-mandatory gap-6 overflow-x-auto scroll-ps-4 pb-4 focus-visible:outline-2 focus-visible:outline-offset-4', card: 'w-[85%] shrink-0 snap-start rounded-card border border-line p-6 sm:w-[45%] lg:w-[30%]', quote: 'text-base' },
|
|
46
|
+
// CSS columns for four or more quotes of different lengths.
|
|
47
|
+
masonry: { list: 'block columns-1 gap-6 space-y-0 md:columns-2 lg:columns-3', card: 'mb-6 h-full break-inside-avoid rounded-card border border-line p-6', quote: 'text-base' },
|
|
33
48
|
},
|
|
34
|
-
|
|
49
|
+
avatarSize: { sm: { avatar: 'size-9' }, md: {}, lg: { avatar: 'size-16' } },
|
|
50
|
+
// start: the portrait before the name. top: above the quote.
|
|
51
|
+
avatarPlace: { start: {}, top: {} },
|
|
52
|
+
// marks: the quotation marks around the text, as the source writes it. glyph: one large decorative mark above.
|
|
53
|
+
quoteMark: { marks: {}, glyph: {}, none: {} },
|
|
54
|
+
tone: { default: {}, muted: { card: 'bg-surface' }, inverse: { card: 'border-surface/20' }, accent: { card: 'border-accent-ink/30', role: 'opacity-100', logo: 'opacity-100' } },
|
|
35
55
|
// The quote's size; `auto` is the layout's own (body size in a grid, large for a single quote).
|
|
36
56
|
quoteSize: {
|
|
37
57
|
auto: {},
|
|
@@ -44,47 +64,77 @@ const testimonial = tv({
|
|
|
44
64
|
frame: { card: {}, plain: {} },
|
|
45
65
|
},
|
|
46
66
|
compoundVariants: [
|
|
47
|
-
{ layout: 'grid', tone: 'muted', class: { card: 'border-transparent text-ink' } },
|
|
67
|
+
{ layout: ['grid', 'row', 'masonry'], tone: 'muted', class: { card: 'border-transparent text-ink' } },
|
|
48
68
|
{ frame: 'plain', class: { card: 'rounded-none border-0 bg-transparent p-0' } },
|
|
49
69
|
],
|
|
50
|
-
defaultVariants: { layout: 'grid', tone: 'default', quoteSize: 'auto', frame: 'card' },
|
|
70
|
+
defaultVariants: { layout: 'grid', tone: 'default', quoteSize: 'auto', frame: 'card', avatarSize: 'md', avatarPlace: 'start', quoteMark: 'marks' },
|
|
51
71
|
})
|
|
52
72
|
|
|
53
73
|
interface Props {
|
|
54
74
|
items: TestimonialItem[]
|
|
55
75
|
heading?: string | undefined
|
|
56
|
-
layout?: 'single' | 'grid' | undefined
|
|
76
|
+
layout?: 'single' | 'grid' | 'row' | 'masonry' | undefined
|
|
57
77
|
quoteSize?: 'auto' | 'base' | 'lg' | 'xl' | undefined
|
|
58
78
|
frame?: 'card' | 'plain' | undefined
|
|
79
|
+
avatarSize?: 'sm' | 'md' | 'lg' | undefined
|
|
80
|
+
avatarPlace?: 'start' | 'top' | undefined
|
|
81
|
+
quoteMark?: 'marks' | 'glyph' | 'none' | undefined
|
|
82
|
+
/** The stars' spoken text; `{rating}` stands for the number. */
|
|
83
|
+
ratingLabel?: string | undefined
|
|
84
|
+
/** The scrolling row's name for assistive technology, when there is no heading to name it (layout row). */
|
|
85
|
+
label?: string | undefined
|
|
86
|
+
/** 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. */
|
|
87
|
+
measured?: MeasuredStyle | undefined
|
|
59
88
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
60
89
|
id?: string | undefined
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
const { items, heading, layout, quoteSize, frame, tone, id } = Astro.props
|
|
64
|
-
const s = testimonial({ layout, quoteSize, frame, tone })
|
|
92
|
+
const { items, heading, layout, quoteSize, frame, avatarSize, avatarPlace, quoteMark = 'marks', ratingLabel = '{rating} out of 5 stars', label = 'Testimonials', tone, measured, id } = Astro.props
|
|
93
|
+
const s = testimonial({ layout, quoteSize, frame, avatarSize, avatarPlace, quoteMark, tone })
|
|
65
94
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
66
95
|
// Two quotes share the row: a third column would stand empty beside them and narrow both (IP-4 elementor launch G9).
|
|
67
|
-
const
|
|
96
|
+
const measuredList = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
|
|
97
|
+
const list = s.list({ class: measuredList ?? (items.length === 2 && (layout ?? 'grid') === 'grid' ? 'lg:grid-cols-2' : undefined) })
|
|
68
98
|
// The quotation marks are drawn, not written: the quote's text is the source's words, read once. A quote the source
|
|
69
99
|
// already wrote inside marks keeps its own.
|
|
70
100
|
const quoted = (text: string) => /^\s*["'“‘„«‹「『]/.test(text)
|
|
101
|
+
// The real number is what is said ("4.8 out of 5"); the stars drawn follow it in halves.
|
|
102
|
+
const stars = (item: TestimonialItem) => Math.min(5, Math.max(0, item.rating ?? 0))
|
|
103
|
+
const spoken = (rating: number) => String(Math.round(rating * 10) / 10)
|
|
104
|
+
// A row scrolls: its box is focusable and named, so a keyboard reaches the cards beyond the edge.
|
|
105
|
+
const scrolls = layout === 'row'
|
|
71
106
|
---
|
|
72
107
|
{items.length > 0 && (
|
|
73
|
-
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
108
|
+
<Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
74
109
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
75
|
-
<div class={list}>
|
|
76
|
-
{items.map(item =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
<div class={list} tabindex={scrolls ? 0 : undefined} role={scrolls ? 'region' : undefined} aria-labelledby={scrolls && heading ? headingId : undefined} aria-label={scrolls && !heading ? label : undefined}>
|
|
111
|
+
{items.map((item) => {
|
|
112
|
+
const avatar = imageOf(item.avatar, item.avatarAlt)
|
|
113
|
+
const logo = imageOf(item.logo, item.logoAlt)
|
|
114
|
+
const rating = stars(item)
|
|
115
|
+
return (
|
|
116
|
+
<figure class={s.card()}>
|
|
117
|
+
{avatar && avatarPlace === 'top' && <KitImage image={avatar} sizes="64px" class={s.avatar()} />}
|
|
118
|
+
{rating > 0 && (
|
|
119
|
+
<span class={s.stars()} role="img" aria-label={ratingLabel.replace('{rating}', spoken(rating))}>
|
|
120
|
+
{[1, 2, 3, 4, 5].map(n => (
|
|
121
|
+
<svg viewBox="0 0 24 24" class:list={['size-5', rating < n && (rating >= n - 0.5 ? 'opacity-60' : 'opacity-25')]} fill="currentColor" aria-hidden="true"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z" /></svg>
|
|
122
|
+
))}
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
{quoteMark === 'glyph' && <span class:list={[s.glyph(), "before:content-['“']"]} aria-hidden="true"></span>}
|
|
126
|
+
<blockquote class={s.quote()}><p class:list={[quoteMark === 'marks' && !quoted(item.quote) && "before:content-[open-quote] after:content-[close-quote]"]}>{item.quote}</p></blockquote>
|
|
127
|
+
<figcaption class={s.person()}>
|
|
128
|
+
{avatar && avatarPlace !== 'top' && <KitImage image={avatar} sizes="64px" class={s.avatar()} />}
|
|
129
|
+
<span class="flex flex-col text-start">
|
|
130
|
+
<span class={s.name()}>{item.name}</span>
|
|
131
|
+
{item.role && <span class={s.role()}>{item.role}</span>}
|
|
132
|
+
</span>
|
|
133
|
+
{logo && <KitImage image={logo} sizes="96px" class={s.logo()} />}
|
|
134
|
+
</figcaption>
|
|
135
|
+
</figure>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
88
138
|
</div>
|
|
89
139
|
</Section>
|
|
90
140
|
)}
|