@contentrain/astro-kit 0.31.0 → 0.33.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 +4600 -1584
- package/components/_shared/Button.astro +2 -1
- package/components/_shared/KitImage.astro +9 -4
- package/components/_shared/Section.astro +3 -1
- package/components/_shared/SectionHeader.astro +27 -0
- package/components/_shared/measured.ts +23 -0
- package/components/_shared/safe-url.ts +67 -0
- package/components/_shared/social.ts +73 -0
- package/components/announcement/Announcement.astro +2 -1
- package/components/announcement/meta.json +1 -1
- package/components/band/Band.astro +3 -2
- package/components/band/meta.json +1 -0
- package/components/breadcrumb/Breadcrumb.astro +2 -1
- package/components/breadcrumb/meta.json +1 -1
- package/components/card-grid/CardGrid.astro +2 -1
- package/components/card-grid/meta.json +1 -0
- package/components/consent/Consent.astro +2 -1
- package/components/consent/meta.json +1 -1
- package/components/contact-details/ContactDetails.astro +3 -2
- package/components/contact-details/meta.json +1 -0
- package/components/contact-form/ContactForm.astro +51 -14
- package/components/contact-form/fixtures.json +169 -9
- package/components/contact-form/meta.json +284 -15
- package/components/cta/Cta.astro +52 -12
- package/components/cta/fixtures.json +5 -1
- package/components/cta/meta.json +11 -2
- package/components/dialog/Dialog.astro +113 -0
- package/components/dialog/fixtures.json +84 -0
- package/components/dialog/meta.json +252 -0
- package/components/embed/meta.json +1 -1
- package/components/faq/Faq.astro +60 -17
- package/components/faq/fixtures.json +79 -0
- package/components/faq/meta.json +9 -3
- package/components/feature-list/FeatureList.astro +28 -4
- package/components/feature-list/meta.json +16 -3
- package/components/footer/Footer.astro +5 -4
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +49 -14
- package/components/gallery/fixtures.json +275 -0
- package/components/gallery/meta.json +267 -14
- package/components/header/Header.astro +17 -5
- package/components/header/fixtures.json +90 -1
- package/components/header/meta.json +5 -3
- package/components/hero/meta.json +1 -0
- package/components/language-switcher/LanguageSwitcher.astro +68 -0
- package/components/language-switcher/fixtures.json +51 -0
- package/components/language-switcher/meta.json +73 -0
- package/components/logo-cloud/LogoCloud.astro +86 -21
- package/components/logo-cloud/fixtures.json +239 -0
- package/components/logo-cloud/meta.json +145 -5
- package/components/nav/Nav.astro +77 -24
- package/components/nav/NavTree.astro +40 -0
- package/components/nav/fixtures.json +165 -0
- package/components/nav/meta.json +11 -2
- package/components/newsletter/Newsletter.astro +2 -1
- package/components/newsletter/meta.json +1 -0
- package/components/pagination/Pagination.astro +4 -3
- package/components/pagination/meta.json +1 -1
- package/components/post-card/PostCard.astro +8 -5
- package/components/post-card/meta.json +3 -2
- package/components/post-list/PostList.astro +115 -0
- package/components/post-list/fixtures.json +387 -0
- package/components/post-list/meta.json +309 -0
- package/components/pricing/Pricing.astro +128 -24
- package/components/pricing/fixtures.json +288 -4
- package/components/pricing/meta.json +188 -1
- package/components/search-box/SearchBox.astro +51 -0
- package/components/search-box/fixtures.json +31 -0
- package/components/search-box/meta.json +75 -0
- package/components/slider/Slider.astro +19 -115
- package/components/slider/SliderTrack.astro +178 -0
- package/components/slider/fixtures.json +2 -1
- package/components/slider/meta.json +11 -5
- package/components/social-links/SocialLinks.astro +74 -0
- package/components/social-links/fixtures.json +67 -0
- package/components/social-links/meta.json +94 -0
- package/components/split/Split.astro +76 -14
- package/components/split/fixtures.json +8 -1
- package/components/split/meta.json +226 -4
- package/components/stats/Stats.astro +54 -11
- package/components/stats/fixtures.json +260 -0
- package/components/stats/meta.json +151 -4
- package/components/steps/Steps.astro +36 -8
- package/components/steps/fixtures.json +217 -5
- package/components/steps/meta.json +157 -3
- package/components/tabs/Tabs.astro +10 -3
- package/components/tabs/fixtures.json +4 -0
- package/components/tabs/meta.json +5 -2
- package/components/team/Team.astro +54 -9
- package/components/team/fixtures.json +217 -4
- package/components/team/meta.json +169 -3
- package/components/team/team.ts +4 -0
- package/components/testimonial/Testimonial.astro +71 -21
- package/components/testimonial/fixtures.json +73 -0
- package/components/testimonial/meta.json +168 -6
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +124 -3
- package/mapping/divi.json +26 -8
- package/mapping/elementor.json +133 -14
- package/mapping/gutenberg.json +38 -10
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"Button.astro",
|
|
9
9
|
"Section.astro",
|
|
10
10
|
"measured.ts",
|
|
11
|
+
"safe-url.ts",
|
|
11
12
|
"types.ts"
|
|
12
13
|
],
|
|
13
14
|
"variants": {
|
|
@@ -29,6 +30,31 @@
|
|
|
29
30
|
],
|
|
30
31
|
"default": "default",
|
|
31
32
|
"description": "Section background."
|
|
33
|
+
},
|
|
34
|
+
"layout": {
|
|
35
|
+
"options": [
|
|
36
|
+
"cards",
|
|
37
|
+
"table"
|
|
38
|
+
],
|
|
39
|
+
"default": "cards",
|
|
40
|
+
"description": "cards: plans side by side. table: one comparison table, plans as columns and features as rows (for more than four plans)."
|
|
41
|
+
},
|
|
42
|
+
"featureStyle": {
|
|
43
|
+
"options": [
|
|
44
|
+
"check",
|
|
45
|
+
"dot",
|
|
46
|
+
"plain"
|
|
47
|
+
],
|
|
48
|
+
"default": "plain",
|
|
49
|
+
"description": "The mark before each feature in the cards: a check, a dot, or the list's own bullets."
|
|
50
|
+
},
|
|
51
|
+
"billingToggle": {
|
|
52
|
+
"options": [
|
|
53
|
+
"none",
|
|
54
|
+
"radio"
|
|
55
|
+
],
|
|
56
|
+
"default": "none",
|
|
57
|
+
"description": "radio: two labelled radios swap each plan's price for its priceAlt (monthly / yearly), with no script. The radio group is named from the section id, or a hash of the plans without one, so two toggles on a page stay independent."
|
|
32
58
|
}
|
|
33
59
|
},
|
|
34
60
|
"props": {
|
|
@@ -90,6 +116,21 @@
|
|
|
90
116
|
"type": "boolean",
|
|
91
117
|
"label": "Featured",
|
|
92
118
|
"description": "Raises the plan and gives it the badge."
|
|
119
|
+
},
|
|
120
|
+
"badge": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"label": "Badge",
|
|
123
|
+
"description": "A badge text on the plan (\"Best value\"); a featured plan without one reads the section's featuredLabel."
|
|
124
|
+
},
|
|
125
|
+
"priceAlt": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"label": "Price, other period",
|
|
128
|
+
"description": "The price for the other billing period (\"€150\"), shown when the toggle is on its second option."
|
|
129
|
+
},
|
|
130
|
+
"periodAlt": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"label": "Period, other period",
|
|
133
|
+
"description": "What the other price is per (\"/year\")."
|
|
93
134
|
}
|
|
94
135
|
}
|
|
95
136
|
},
|
|
@@ -105,6 +146,152 @@
|
|
|
105
146
|
"type": "string",
|
|
106
147
|
"content": false,
|
|
107
148
|
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
149
|
+
},
|
|
150
|
+
"compact": {
|
|
151
|
+
"type": "boolean",
|
|
152
|
+
"default": false,
|
|
153
|
+
"content": false,
|
|
154
|
+
"description": "A denser card: smaller padding, gap and price."
|
|
155
|
+
},
|
|
156
|
+
"includedLabel": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"default": "Included",
|
|
159
|
+
"content": false,
|
|
160
|
+
"description": "Table cell text for readers where a plan has the feature."
|
|
161
|
+
},
|
|
162
|
+
"notIncludedLabel": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"default": "Not included",
|
|
165
|
+
"content": false,
|
|
166
|
+
"description": "Table cell text for readers where a plan lacks the feature."
|
|
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
|
+
"billingMainLabel": {
|
|
279
|
+
"type": "string",
|
|
280
|
+
"default": "Monthly",
|
|
281
|
+
"content": false,
|
|
282
|
+
"description": "First option of the billing toggle."
|
|
283
|
+
},
|
|
284
|
+
"billingAltLabel": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"default": "Yearly",
|
|
287
|
+
"content": false,
|
|
288
|
+
"description": "Second option of the billing toggle (the plans' priceAlt)."
|
|
289
|
+
},
|
|
290
|
+
"billingLegend": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"default": "Billing period",
|
|
293
|
+
"content": false,
|
|
294
|
+
"description": "Legend of the toggle, read to screen readers."
|
|
108
295
|
}
|
|
109
296
|
},
|
|
110
297
|
"slots": {},
|
|
@@ -121,7 +308,7 @@
|
|
|
121
308
|
"divi/et_pb_pricing_tables"
|
|
122
309
|
]
|
|
123
310
|
},
|
|
124
|
-
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label.",
|
|
311
|
+
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label. The table layout is a real <table> with a caption, column and row headers, and Included / Not included text in every cell; the billing toggle is a fieldset with a legend and labelled radios.",
|
|
125
312
|
"pattern": {
|
|
126
313
|
"summary": "repeated plan units: name + price + feature list + button",
|
|
127
314
|
"signals": [
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A search field that sends the visitor to the site's search page (`/search?q=…`, a plain GET form, no JavaScript
|
|
3
|
+
// here: the page itself shows the results). A labelled `role="search"` landmark; the label is visible only to
|
|
4
|
+
// assistive technology unless `labelStyle` shows it.
|
|
5
|
+
import { tv } from 'tailwind-variants'
|
|
6
|
+
import { safeUrl } from '../_shared/safe-url'
|
|
7
|
+
|
|
8
|
+
const search = tv({
|
|
9
|
+
slots: {
|
|
10
|
+
form: 'flex items-center gap-2',
|
|
11
|
+
label: 'sr-only',
|
|
12
|
+
input: 'min-w-0 flex-1 rounded-md border border-line bg-surface px-3 text-ink placeholder:text-ink-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
13
|
+
button: 'inline-flex shrink-0 cursor-pointer items-center justify-center rounded-md border border-line bg-surface px-3 font-medium text-ink hover:bg-current/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
14
|
+
},
|
|
15
|
+
variants: {
|
|
16
|
+
size: { sm: { input: 'h-9 text-sm', button: 'h-9 text-sm' }, md: { input: 'h-11 text-base', button: 'h-11 text-base' } },
|
|
17
|
+
width: { auto: { form: 'w-full max-w-xs' }, full: { form: 'w-full' } },
|
|
18
|
+
// `text`: the button reads "Search"; `icon`: a magnifier with the same name for assistive technology.
|
|
19
|
+
button: { text: {}, icon: { button: 'px-0 aspect-square' } },
|
|
20
|
+
labelStyle: { hidden: {}, shown: { form: 'flex-wrap', label: 'not-sr-only basis-full text-sm font-medium' } },
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: { size: 'md', width: 'auto', button: 'text', labelStyle: 'hidden' },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** The search page the form sends to (it reads `q`). */
|
|
27
|
+
action?: string | undefined
|
|
28
|
+
/** The landmark's and field's accessible name ("Search"). */
|
|
29
|
+
label?: string | undefined
|
|
30
|
+
placeholder?: string | undefined
|
|
31
|
+
/** The button's name ("Search"). */
|
|
32
|
+
submitLabel?: string | undefined
|
|
33
|
+
size?: 'sm' | 'md' | undefined
|
|
34
|
+
width?: 'auto' | 'full' | undefined
|
|
35
|
+
button?: 'text' | 'icon' | undefined
|
|
36
|
+
labelStyle?: 'hidden' | 'shown' | undefined
|
|
37
|
+
id?: string | undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { action = '/search', label = 'Search', placeholder, submitLabel = label, size, width, button, labelStyle, id = 'site-search' } = Astro.props
|
|
41
|
+
const s = search({ size, width, button, labelStyle })
|
|
42
|
+
---
|
|
43
|
+
<form role="search" method="get" action={safeUrl(action)} class={s.form()} data-cr-part="search-box">
|
|
44
|
+
<label for={`${id}-q`} class={s.label()}>{label}</label>
|
|
45
|
+
<input id={`${id}-q`} type="search" name="q" placeholder={placeholder} autocomplete="off" enterkeyhint="search" class={s.input()} />
|
|
46
|
+
<button type="submit" class={s.button()} aria-label={button === 'icon' ? submitLabel : undefined}>
|
|
47
|
+
{button === 'icon'
|
|
48
|
+
? <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
|
|
49
|
+
: submitLabel}
|
|
50
|
+
</button>
|
|
51
|
+
</form>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "default",
|
|
4
|
+
"props": {
|
|
5
|
+
"label": "Search",
|
|
6
|
+
"placeholder": "Search this site",
|
|
7
|
+
"submitLabel": "Search"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "small-icon",
|
|
12
|
+
"props": {
|
|
13
|
+
"size": "sm",
|
|
14
|
+
"button": "icon",
|
|
15
|
+
"label": "Search",
|
|
16
|
+
"submitLabel": "Search",
|
|
17
|
+
"id": "header-search"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "full-shown-label",
|
|
22
|
+
"props": {
|
|
23
|
+
"width": "full",
|
|
24
|
+
"labelStyle": "shown",
|
|
25
|
+
"label": "Search the journal",
|
|
26
|
+
"placeholder": "Search this site",
|
|
27
|
+
"submitLabel": "Search",
|
|
28
|
+
"id": "journal-search"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "search-box",
|
|
3
|
+
"name": "SearchBox",
|
|
4
|
+
"description": "A search field that sends the visitor to the site's search page (`/search?q=…`): a plain GET form with a labelled role=search landmark, no JavaScript. For a header slot or inline in a page.",
|
|
5
|
+
"category": "chrome",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": ["safe-url.ts"],
|
|
8
|
+
"variants": {
|
|
9
|
+
"size": {
|
|
10
|
+
"options": [
|
|
11
|
+
"sm",
|
|
12
|
+
"md"
|
|
13
|
+
],
|
|
14
|
+
"default": "md",
|
|
15
|
+
"description": "Field and button height (36 and 44 px)."
|
|
16
|
+
},
|
|
17
|
+
"width": {
|
|
18
|
+
"options": [
|
|
19
|
+
"auto",
|
|
20
|
+
"full"
|
|
21
|
+
],
|
|
22
|
+
"default": "auto",
|
|
23
|
+
"description": "A compact field (up to 20 rem) or the whole column."
|
|
24
|
+
},
|
|
25
|
+
"button": {
|
|
26
|
+
"options": [
|
|
27
|
+
"text",
|
|
28
|
+
"icon"
|
|
29
|
+
],
|
|
30
|
+
"default": "text",
|
|
31
|
+
"description": "The button reads its label, or shows a magnifier named by the same label."
|
|
32
|
+
},
|
|
33
|
+
"labelStyle": {
|
|
34
|
+
"options": [
|
|
35
|
+
"hidden",
|
|
36
|
+
"shown"
|
|
37
|
+
],
|
|
38
|
+
"default": "hidden",
|
|
39
|
+
"description": "The field's label for assistive technology only, or printed above the field."
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"props": {
|
|
43
|
+
"action": {
|
|
44
|
+
"type": "url",
|
|
45
|
+
"default": "/search",
|
|
46
|
+
"description": "The search page the form sends to; it reads `q`."
|
|
47
|
+
},
|
|
48
|
+
"label": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "The landmark's and field's accessible name (ui-strings search.label)."
|
|
51
|
+
},
|
|
52
|
+
"placeholder": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Hint text in the field (ui-strings search.placeholder)."
|
|
55
|
+
},
|
|
56
|
+
"submitLabel": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "The button's name (ui-strings search.submit)."
|
|
59
|
+
},
|
|
60
|
+
"id": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"default": "site-search",
|
|
63
|
+
"description": "Prefix of the field's id; set it when two search boxes share a page."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"slots": {},
|
|
67
|
+
"js": "none",
|
|
68
|
+
"dependencies": {},
|
|
69
|
+
"sources": {
|
|
70
|
+
"gutenberg": [
|
|
71
|
+
"core/search"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"a11y": "A <form role=\"search\"> with a real <label> for the type=search field (sr-only unless shown); the button has a text or aria-label name; focus rings on both."
|
|
75
|
+
}
|
|
@@ -2,39 +2,12 @@
|
|
|
2
2
|
// A carousel of images with optional captions. Without JavaScript it is a
|
|
3
3
|
// horizontally scrolling, snapping row — every slide is reachable. With it,
|
|
4
4
|
// Embla turns the row into a paged carousel with previous/next buttons and
|
|
5
|
-
// slide dots.
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
const slider = tv({
|
|
13
|
-
slots: {
|
|
14
|
-
viewport: 'flex snap-x snap-mandatory gap-4 overflow-x-auto scroll-smooth group-data-[ready]:snap-none group-data-[ready]:overflow-hidden [scrollbar-width:none]',
|
|
15
|
-
slide: 'relative min-w-0 shrink-0 snap-start overflow-hidden',
|
|
16
|
-
image: 'w-full object-cover',
|
|
17
|
-
caption: 'flex flex-col items-start gap-2',
|
|
18
|
-
},
|
|
19
|
-
variants: {
|
|
20
|
-
perView: {
|
|
21
|
-
'1': { slide: 'basis-full' },
|
|
22
|
-
'2': { slide: 'basis-[85%] sm:basis-[calc(50%-0.5rem)]' },
|
|
23
|
-
'3': { slide: 'basis-[85%] sm:basis-[calc(50%-0.5rem)] lg:basis-[calc(33.333%-0.667rem)]' },
|
|
24
|
-
},
|
|
25
|
-
aspect: {
|
|
26
|
-
wide: { image: 'aspect-[4/3] md:aspect-[21/9]' },
|
|
27
|
-
standard: { image: 'aspect-[3/2]' },
|
|
28
|
-
square: { image: 'aspect-square' },
|
|
29
|
-
},
|
|
30
|
-
caption: {
|
|
31
|
-
below: { image: 'rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4' },
|
|
32
|
-
// Over the image on wide screens; under it on phones, where the image is too short to hold text.
|
|
33
|
-
overlay: { image: 'rounded-[var(--radius-image,var(--radius-card))] md:rounded-none', slide: 'md:rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4 md:absolute md:inset-x-0 md:bottom-0 md:bg-gradient-to-t md:from-ink/80 md:to-transparent md:p-6 md:pt-16 md:text-surface' },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
defaultVariants: { perView: '1', aspect: 'wide', caption: 'overlay' },
|
|
37
|
-
})
|
|
5
|
+
// slide dots. The markup and the script are in SliderTrack.astro; given a
|
|
6
|
+
// measured style, this wraps it in the section's own frame.
|
|
7
|
+
import Section from '../_shared/Section.astro'
|
|
8
|
+
import type { ImageSource } from '../_shared/image'
|
|
9
|
+
import type { ActionInput, MeasuredStyle } from '../_shared/types'
|
|
10
|
+
import SliderTrack from './SliderTrack.astro'
|
|
38
11
|
|
|
39
12
|
interface Slide {
|
|
40
13
|
/** The image's address (or, until 0.7, an `ImageInput`). */
|
|
@@ -51,6 +24,11 @@ interface Slide {
|
|
|
51
24
|
|
|
52
25
|
interface Props {
|
|
53
26
|
slides: Slide[]
|
|
27
|
+
/** The section's head, drawn above the carousel; none of these given, nothing is drawn. */
|
|
28
|
+
heading?: string | undefined
|
|
29
|
+
intro?: string | undefined
|
|
30
|
+
eyebrow?: string | undefined
|
|
31
|
+
actions?: ActionInput[] | undefined
|
|
54
32
|
/** Accessible name of the carousel ("Featured projects"). */
|
|
55
33
|
label: string
|
|
56
34
|
prevLabel?: string | undefined
|
|
@@ -63,88 +41,14 @@ interface Props {
|
|
|
63
41
|
perView?: '1' | '2' | '3' | undefined
|
|
64
42
|
aspect?: 'wide' | 'standard' | 'square' | undefined
|
|
65
43
|
caption?: 'below' | 'overlay' | undefined
|
|
44
|
+
/** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width (the slides in view), gap. Absent, the site's tokens; present, it wins over `perView`. */
|
|
45
|
+
measured?: MeasuredStyle | undefined
|
|
46
|
+
/** Unique on the page: names the track the buttons control. */
|
|
47
|
+
id?: string | undefined
|
|
66
48
|
}
|
|
67
49
|
|
|
68
|
-
const
|
|
69
|
-
const slides = given.flatMap((slide) => {
|
|
70
|
-
const image = imageOf(slide.image, slide.imageAlt)
|
|
71
|
-
const action = slide.action ?? (slide.ctaLabel && slide.ctaHref ? { label: slide.ctaLabel, href: slide.ctaHref } : undefined)
|
|
72
|
-
return image ? [{ ...slide, image, action }] : []
|
|
73
|
-
}) satisfies Array<{ image: ImageInput, action?: ActionInput | undefined }>
|
|
74
|
-
const slots = slider({ perView, aspect, caption })
|
|
75
|
-
const sizes = perView === '1' ? '(min-width: 1216px) 1152px, 100vw' : perView === '2' ? '(min-width: 640px) 50vw, 85vw' : '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 85vw'
|
|
76
|
-
// Arrows from sm up; on phones the carousel is swiped and the dots show the position.
|
|
77
|
-
const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-center justify-center rounded-full bg-surface/90 text-ink shadow-md ring-1 ring-line hover:bg-surface disabled:opacity-40 sm:group-data-[ready]:flex'
|
|
50
|
+
const props = Astro.props
|
|
78
51
|
---
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
{slides.map((slide, index) => (
|
|
83
|
-
<div class={slots.slide()} role="group" aria-roledescription="slide" aria-label={`${slideLabel} ${index + 1} ${ofLabel} ${slides.length}`}>
|
|
84
|
-
<KitImage image={slide.image} sizes={sizes} priority={index === 0} class={slots.image()} />
|
|
85
|
-
{(slide.title || slide.text || slide.action) && (
|
|
86
|
-
<div class={slots.caption()}>
|
|
87
|
-
{slide.title && <p class="text-xl [font-weight:var(--font-weight-heading,600)] text-balance">{slide.title}</p>}
|
|
88
|
-
{slide.text && <p class="opacity-85">{slide.text}</p>}
|
|
89
|
-
{slide.action && <Button action={slide.action} size="sm" class="mt-2" />}
|
|
90
|
-
</div>
|
|
91
|
-
)}
|
|
92
|
-
</div>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
{slides.length > 1 && (
|
|
97
|
-
<>
|
|
98
|
-
<button type="button" class:list={[arrow, 'start-6 md:start-10']} aria-label={prevLabel} data-prev>
|
|
99
|
-
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 rtl:-scale-x-100" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6" /></svg>
|
|
100
|
-
</button>
|
|
101
|
-
<button type="button" class:list={[arrow, 'end-6 md:end-10']} aria-label={nextLabel} data-next>
|
|
102
|
-
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 rtl:-scale-x-100" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" /></svg>
|
|
103
|
-
</button>
|
|
104
|
-
<div class="mt-4 hidden justify-center group-data-[ready]:flex" data-dots></div>
|
|
105
|
-
</>
|
|
106
|
-
)}
|
|
107
|
-
</section>
|
|
108
|
-
|
|
109
|
-
<script>
|
|
110
|
-
import EmblaCarousel from 'embla-carousel'
|
|
111
|
-
|
|
112
|
-
for (const root of document.querySelectorAll<HTMLElement>('[data-kit-slider]')) {
|
|
113
|
-
const viewport = root.querySelector<HTMLElement>('[data-viewport]')
|
|
114
|
-
if (!viewport || root.dataset.ready !== undefined) continue
|
|
115
|
-
root.dataset.ready = ''
|
|
116
|
-
// Reduced motion: slides change instantly instead of gliding.
|
|
117
|
-
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
118
|
-
// A right-to-left page runs the row from the right: the first slide at the start edge, "next" towards the end.
|
|
119
|
-
const direction = getComputedStyle(root).direction === 'rtl' ? 'rtl' : 'ltr'
|
|
120
|
-
const embla = EmblaCarousel(viewport, { loop: root.dataset.loop !== undefined, align: 'start', containScroll: 'trimSnaps', duration: reduce ? 0 : 25, direction })
|
|
121
|
-
const prev = root.querySelector<HTMLButtonElement>('[data-prev]')
|
|
122
|
-
const next = root.querySelector<HTMLButtonElement>('[data-next]')
|
|
123
|
-
const dots = root.querySelector<HTMLElement>('[data-dots]')
|
|
124
|
-
prev?.addEventListener('click', () => embla.scrollPrev())
|
|
125
|
-
next?.addEventListener('click', () => embla.scrollNext())
|
|
126
|
-
|
|
127
|
-
const label = root.dataset.slideLabel ?? 'Slide'
|
|
128
|
-
const buttons = embla.scrollSnapList().map((_, index) => {
|
|
129
|
-
const dot = document.createElement('button')
|
|
130
|
-
dot.type = 'button'
|
|
131
|
-
// A 24px target around a 10px dot (WCAG 2.5.8): on phones the dots are the only controls.
|
|
132
|
-
dot.className = 'group/dot grid size-6 place-items-center rounded-full'
|
|
133
|
-
const mark = document.createElement('span')
|
|
134
|
-
mark.className = 'size-2.5 rounded-full bg-ink/25 group-aria-[current=true]/dot:bg-ink'
|
|
135
|
-
dot.append(mark)
|
|
136
|
-
dot.setAttribute('aria-label', `${label} ${index + 1}`)
|
|
137
|
-
dot.addEventListener('click', () => embla.scrollTo(index))
|
|
138
|
-
dots?.append(dot)
|
|
139
|
-
return dot
|
|
140
|
-
})
|
|
141
|
-
const sync = () => {
|
|
142
|
-
const selected = embla.selectedScrollSnap()
|
|
143
|
-
buttons.forEach((dot, index) => dot.setAttribute('aria-current', String(index === selected)))
|
|
144
|
-
if (prev) prev.disabled = !embla.canScrollPrev()
|
|
145
|
-
if (next) next.disabled = !embla.canScrollNext()
|
|
146
|
-
}
|
|
147
|
-
embla.on('select', sync).on('reInit', sync)
|
|
148
|
-
sync()
|
|
149
|
-
}
|
|
150
|
-
</script>
|
|
52
|
+
{props.measured
|
|
53
|
+
? <Section measured={props.measured} tone="none" spacing="sm" id={props.id}><SliderTrack {...props} id={props.id ?? 'slider'} framed={false} /></Section>
|
|
54
|
+
: <SliderTrack {...props} />}
|