@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.
Files changed (48) hide show
  1. package/catalog.json +2531 -345
  2. package/components/_shared/measured.ts +10 -0
  3. package/components/contact-form/ContactForm.astro +48 -12
  4. package/components/contact-form/fixtures.json +169 -9
  5. package/components/contact-form/meta.json +283 -15
  6. package/components/cta/Cta.astro +52 -12
  7. package/components/cta/fixtures.json +5 -1
  8. package/components/cta/meta.json +11 -2
  9. package/components/faq/Faq.astro +60 -17
  10. package/components/faq/fixtures.json +75 -0
  11. package/components/faq/meta.json +7 -1
  12. package/components/feature-list/FeatureList.astro +28 -4
  13. package/components/feature-list/meta.json +15 -3
  14. package/components/gallery/Gallery.astro +47 -13
  15. package/components/gallery/fixtures.json +275 -0
  16. package/components/gallery/meta.json +266 -14
  17. package/components/logo-cloud/LogoCloud.astro +85 -21
  18. package/components/logo-cloud/fixtures.json +239 -0
  19. package/components/logo-cloud/meta.json +144 -5
  20. package/components/post-card/PostCard.astro +4 -2
  21. package/components/post-card/meta.json +2 -1
  22. package/components/post-list/PostList.astro +114 -0
  23. package/components/post-list/fixtures.json +387 -0
  24. package/components/post-list/meta.json +308 -0
  25. package/components/pricing/Pricing.astro +128 -24
  26. package/components/pricing/fixtures.json +288 -4
  27. package/components/pricing/meta.json +187 -1
  28. package/components/split/Split.astro +76 -14
  29. package/components/split/fixtures.json +8 -1
  30. package/components/split/meta.json +225 -4
  31. package/components/stats/Stats.astro +42 -10
  32. package/components/stats/fixtures.json +173 -0
  33. package/components/stats/meta.json +150 -4
  34. package/components/steps/Steps.astro +36 -8
  35. package/components/steps/fixtures.json +217 -5
  36. package/components/steps/meta.json +156 -3
  37. package/components/team/Team.astro +52 -8
  38. package/components/team/fixtures.json +217 -4
  39. package/components/team/meta.json +167 -3
  40. package/components/team/team.ts +9 -0
  41. package/components/testimonial/Testimonial.astro +71 -21
  42. package/components/testimonial/fixtures.json +73 -0
  43. package/components/testimonial/meta.json +166 -3
  44. package/mapping/classic.json +7 -1
  45. package/mapping/divi.json +21 -4
  46. package/mapping/elementor.json +33 -8
  47. package/mapping/gutenberg.json +20 -4
  48. package/package.json +1 -1
@@ -4,27 +4,279 @@
4
4
  "description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), opens the image file, or does nothing.",
5
5
  "category": "section",
6
6
  "uses": [],
7
- "shared": ["KitImage.astro", "Section.astro", "image.ts", "measured.ts", "types.ts"],
7
+ "shared": [
8
+ "KitImage.astro",
9
+ "Section.astro",
10
+ "image.ts",
11
+ "measured.ts",
12
+ "types.ts"
13
+ ],
8
14
  "variants": {
9
- "layout": { "options": ["grid", "masonry"], "default": "grid", "description": "grid: square crops in rows. masonry: columns that keep each image's shape." },
10
- "columns": { "options": ["2", "3", "4"], "default": "3", "description": "Columns on wide screens; phones show two." },
11
- "open": { "options": ["popover", "link", "none"], "default": "popover", "description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only." }
15
+ "layout": {
16
+ "options": [
17
+ "grid",
18
+ "masonry",
19
+ "row",
20
+ "mosaic"
21
+ ],
22
+ "default": "grid",
23
+ "description": "grid: equal tiles. masonry: columns keeping each image's height. row: a scroll-snap strip (a focusable region). mosaic: the first tile spans 2×2."
24
+ },
25
+ "columns": {
26
+ "options": [
27
+ "2",
28
+ "3",
29
+ "4",
30
+ "5",
31
+ "6"
32
+ ],
33
+ "default": "3",
34
+ "description": "Images per row on wide screens; two on phones."
35
+ },
36
+ "open": {
37
+ "options": [
38
+ "popover",
39
+ "link",
40
+ "none"
41
+ ],
42
+ "default": "popover",
43
+ "description": "popover: the image opens large in place (Popover API; Escape or a click outside closes it). link: the thumbnail links to the image file (the WordPress 'link to media file'). none: images only."
44
+ },
45
+ "aspect": {
46
+ "options": [
47
+ "square",
48
+ "4:3",
49
+ "16:9",
50
+ "auto"
51
+ ],
52
+ "default": "square",
53
+ "description": "Tile shape (a masonry keeps each image's own)."
54
+ },
55
+ "gap": {
56
+ "options": [
57
+ "sm",
58
+ "md",
59
+ "lg"
60
+ ],
61
+ "default": "md",
62
+ "description": "Space between tiles."
63
+ },
64
+ "captionStyle": {
65
+ "options": [
66
+ "below",
67
+ "overlay"
68
+ ],
69
+ "default": "below",
70
+ "description": "below: caption under the image. overlay: over the image foot on hover and keyboard focus, always shown below md."
71
+ },
72
+ "hoverEffect": {
73
+ "options": [
74
+ "none",
75
+ "zoom"
76
+ ],
77
+ "default": "none",
78
+ "description": "zoom: a slight scale on hover, only where reduced motion is not requested."
79
+ }
12
80
  },
13
81
  "props": {
14
- "items": { "type": "array", "label": "Images", "required": true, "items": { "type": "object", "fields": { "image": { "type": "image", "label": "Image", "required": true, "description": "The image." }, "imageAlt": { "type": "string", "label": "Image description", "description": "What the image shows, for readers who cannot see it." }, "caption": { "type": "string", "label": "Caption", "description": "Caption under the enlarged image." } } }, "description": "The images in order (core/gallery inner images, Elementor gallery items, Divi gallery_ids)." },
15
- "heading": { "type": "string", "label": "Heading", "description": "Section heading above the images." },
16
- "closeLabel": { "type": "string", "content": false, "description": "Text of the viewer's close button (ui-strings gallery.close)." },
17
- "tone": { "type": "select", "options": ["default", "muted", "inverse", "accent"], "content": false, "description": "Section background." },
18
- "id": { "type": "string", "content": false, "description": "Section id, unique per page: viewer ids derive from it (`<id>-image-<n>`)." }
82
+ "items": {
83
+ "type": "array",
84
+ "label": "Images",
85
+ "required": true,
86
+ "items": {
87
+ "type": "object",
88
+ "fields": {
89
+ "image": {
90
+ "type": "image",
91
+ "label": "Image",
92
+ "required": true,
93
+ "description": "The image."
94
+ },
95
+ "imageAlt": {
96
+ "type": "string",
97
+ "label": "Image description",
98
+ "description": "What the image shows, for readers who cannot see it."
99
+ },
100
+ "caption": {
101
+ "type": "string",
102
+ "label": "Caption",
103
+ "description": "Caption under the enlarged image."
104
+ },
105
+ "full": {
106
+ "type": "image",
107
+ "label": "Full-size image",
108
+ "description": "The original file the viewer shows large, where the source links to it; otherwise the image itself."
109
+ }
110
+ }
111
+ },
112
+ "description": "The images in order (core/gallery inner images, Elementor gallery items, Divi gallery_ids)."
113
+ },
114
+ "heading": {
115
+ "type": "string",
116
+ "label": "Heading",
117
+ "description": "Section heading above the images."
118
+ },
119
+ "closeLabel": {
120
+ "type": "string",
121
+ "content": false,
122
+ "description": "Text of the viewer's close button (ui-strings gallery.close)."
123
+ },
124
+ "tone": {
125
+ "type": "select",
126
+ "options": [
127
+ "default",
128
+ "muted",
129
+ "inverse",
130
+ "accent"
131
+ ],
132
+ "content": false,
133
+ "description": "Section background."
134
+ },
135
+ "id": {
136
+ "type": "string",
137
+ "content": false,
138
+ "description": "Section id, unique per page: viewer ids derive from it (`<id>-image-<n>`)."
139
+ },
140
+ "enlargeLabel": {
141
+ "type": "string",
142
+ "default": "Enlarge",
143
+ "content": false,
144
+ "description": "Prefix of each thumbnail button's name for screen readers (\"Enlarge: <description>\")."
145
+ },
146
+ "regionLabel": {
147
+ "type": "string",
148
+ "default": "Images",
149
+ "content": false,
150
+ "description": "Name of the scrolling row's region when the gallery has no heading."
151
+ },
152
+ "measured": {
153
+ "type": "object",
154
+ "content": false,
155
+ "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.",
156
+ "fields": {
157
+ "containerPx": {
158
+ "type": "integer",
159
+ "min": 0,
160
+ "max": 2560,
161
+ "description": "The frame's content width."
162
+ },
163
+ "padY": {
164
+ "type": "integer",
165
+ "min": 0,
166
+ "max": 400,
167
+ "description": "Top and bottom padding."
168
+ },
169
+ "padX": {
170
+ "type": "integer",
171
+ "min": 0,
172
+ "max": 200,
173
+ "description": "The frame's side padding (gutter)."
174
+ },
175
+ "bg": {
176
+ "type": "object",
177
+ "description": "The section's background.",
178
+ "fields": {
179
+ "color": {
180
+ "type": "string",
181
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
182
+ },
183
+ "image": {
184
+ "type": "string",
185
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
186
+ },
187
+ "overlay": {
188
+ "type": "string",
189
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
190
+ },
191
+ "size": {
192
+ "type": "string",
193
+ "description": "cover, contain or auto; anything else draws as cover."
194
+ },
195
+ "position": {
196
+ "type": "string",
197
+ "description": "One or two of center/top/bottom/left/right or percentages."
198
+ }
199
+ }
200
+ },
201
+ "columns": {
202
+ "type": "object",
203
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
204
+ "fields": {
205
+ "390": {
206
+ "type": "integer",
207
+ "min": 1,
208
+ "max": 6,
209
+ "description": "Phone."
210
+ },
211
+ "768": {
212
+ "type": "integer",
213
+ "min": 1,
214
+ "max": 6,
215
+ "description": "Tablet."
216
+ },
217
+ "1280": {
218
+ "type": "integer",
219
+ "min": 1,
220
+ "max": 6,
221
+ "description": "Desktop."
222
+ }
223
+ }
224
+ },
225
+ "gap": {
226
+ "type": "integer",
227
+ "min": 0,
228
+ "max": 200,
229
+ "description": "Gap between items."
230
+ },
231
+ "tone": {
232
+ "type": "select",
233
+ "options": [
234
+ "light",
235
+ "dark"
236
+ ],
237
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
238
+ },
239
+ "align": {
240
+ "type": "select",
241
+ "options": [
242
+ "start",
243
+ "center",
244
+ "end"
245
+ ],
246
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
247
+ },
248
+ "radius": {
249
+ "type": "integer",
250
+ "min": 0,
251
+ "max": 200,
252
+ "description": "Card and item corner radius."
253
+ },
254
+ "minHeight": {
255
+ "type": "integer",
256
+ "min": 0,
257
+ "max": 2000,
258
+ "description": "The section's minimum height."
259
+ }
260
+ }
261
+ }
19
262
  },
20
263
  "slots": {},
21
264
  "js": "none",
22
265
  "dependencies": {},
23
266
  "sources": {
24
- "gutenberg": ["core/gallery"],
25
- "elementor": ["elementor/image-gallery", "elementor/gallery"],
26
- "divi": ["divi/et_pb_gallery"],
27
- "classic": ["classic/gallery"]
267
+ "gutenberg": [
268
+ "core/gallery"
269
+ ],
270
+ "elementor": [
271
+ "elementor/image-gallery",
272
+ "elementor/gallery"
273
+ ],
274
+ "divi": [
275
+ "divi/et_pb_gallery"
276
+ ],
277
+ "classic": [
278
+ "classic/gallery"
279
+ ]
28
280
  },
29
- "a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s."
281
+ "a11y": "A list of figures. In popover mode each thumbnail is a <button> named by the image's alt (or caption) that opens the viewer; the viewer has a visible close button, and Escape closes it, returning focus to the thumbnail. Captions are <figcaption>s. A scrolling row is a labelled region with tabindex 0 so the keyboard can scroll it; thumbnail buttons are named \"Enlarge: …\". The viewer has no next/previous (it would need script)."
30
282
  }
@@ -1,32 +1,58 @@
1
1
  ---
2
- // A grid of client or partner logos, each optionally linked, under an
3
- // optional heading.
2
+ // A grid, a row or an endless strip of client or partner logos, each
3
+ // optionally linked, under an optional heading. The strip is CSS only and
4
+ // stands still for visitors who ask for reduced motion.
4
5
  import { tv } from 'tailwind-variants'
5
6
  import KitImage from '../_shared/KitImage.astro'
6
7
  import Section, { type Tone } from '../_shared/Section.astro'
7
8
  import { imageOf, type ImageSource } from '../_shared/image'
9
+ import { measuredGrid } from '../_shared/measured'
10
+ import type { MeasuredStyle } from '../_shared/types'
8
11
 
9
12
  const cloud = tv({
10
13
  slots: {
11
- heading: 'mb-8 text-center text-sm font-semibold tracking-wide uppercase opacity-80',
14
+ heading: 'mb-8 text-sm font-semibold tracking-wide uppercase opacity-80',
12
15
  list: 'grid grid-cols-2 items-center gap-x-8 gap-y-10 sm:grid-cols-3',
13
16
  item: 'flex items-center justify-center',
14
17
  logo: 'h-10 w-auto max-w-full object-contain transition duration-200',
15
18
  },
16
19
  variants: {
20
+ layout: {
21
+ grid: {},
22
+ // One line of logos that wraps on a narrow screen; the logos keep their own widths.
23
+ row: { list: 'flex flex-wrap items-center gap-x-10 gap-y-6', item: 'shrink-0' },
24
+ // Two copies of the list side by side, moved by one keyframe; see the style block below.
25
+ marquee: { list: 'kit-marquee-track flex w-max items-center gap-x-12', item: 'shrink-0' },
26
+ },
17
27
  columns: {
18
28
  '2': { list: 'sm:grid-cols-2' },
19
29
  '3': { list: 'md:grid-cols-3' },
20
30
  '4': { list: 'md:grid-cols-4' },
31
+ '5': { list: 'md:grid-cols-3 lg:grid-cols-5' },
32
+ '6': { list: 'md:grid-cols-3 lg:grid-cols-6' },
33
+ },
34
+ // A fixed height, the width follows: logos come in every aspect ratio.
35
+ logoHeight: {
36
+ sm: { logo: 'h-8' },
37
+ md: {},
38
+ lg: { logo: 'h-14' },
21
39
  },
22
40
  style: {
23
41
  plain: {},
24
42
  // Colour comes back on hover and on keyboard focus of the logo's link.
25
43
  grayscale: { logo: 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0 group-focus-visible:opacity-100 group-focus-visible:grayscale-0' },
44
+ // Dark logos on a dark band: drawn white, full strength.
45
+ 'mono-light': { logo: 'brightness-0 invert' },
26
46
  },
47
+ align: { center: { heading: 'text-center' }, start: { heading: 'text-start' } },
27
48
  tone: { default: {}, muted: {}, inverse: {}, accent: { heading: 'opacity-100' } },
28
49
  },
29
- defaultVariants: { columns: '4', style: 'plain', tone: 'default' },
50
+ compoundVariants: [
51
+ { layout: 'row', align: 'center', class: { list: 'justify-center' } },
52
+ { layout: 'row', align: 'start', class: { list: 'justify-start' } },
53
+ { layout: 'grid', align: 'start', class: { item: 'justify-start' } },
54
+ ],
55
+ defaultVariants: { layout: 'grid', columns: '4', logoHeight: 'md', style: 'plain', align: 'center', tone: 'default' },
30
56
  })
31
57
 
32
58
  interface Item {
@@ -40,30 +66,68 @@ interface Item {
40
66
  interface Props {
41
67
  heading?: string | undefined
42
68
  items: Item[]
43
- columns?: '2' | '3' | '4' | undefined
44
- style?: 'plain' | 'grayscale' | undefined
69
+ layout?: 'grid' | 'row' | 'marquee' | undefined
70
+ columns?: '2' | '3' | '4' | '5' | '6' | undefined
71
+ logoHeight?: 'sm' | 'md' | 'lg' | undefined
72
+ style?: 'plain' | 'grayscale' | 'mono-light' | undefined
73
+ align?: 'center' | 'start' | undefined
45
74
  tone?: Tone | undefined
75
+ /** 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. */
76
+ measured?: MeasuredStyle | undefined
46
77
  id?: string | undefined
47
78
  }
48
79
 
49
- const { heading, items, columns, style, tone, id } = Astro.props
50
- const s = cloud({ columns, style, tone })
80
+ const { heading, items, layout, columns, logoHeight, style, align, tone, measured, id } = Astro.props
81
+ const s = cloud({ layout, columns, logoHeight, style, align, tone })
82
+ // Measured columns and gap shape the grid layout only; a row or a strip keeps its own flow.
83
+ const grid = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
51
84
  const headingId = heading && id ? `${id}-title` : undefined
85
+ const marquee = layout === 'marquee'
86
+ // A linked logo with no name is read as the site it goes to, never as a made-up word.
87
+ const hostOf = (href: string) => {
88
+ try { return new URL(href, 'https://x.invalid').host.replace(/^www\./, '').replace(/^x\.invalid$/, '') } catch { return '' }
89
+ }
90
+ const shown = items.flatMap((item) => {
91
+ const image = imageOf(item.image, item.imageAlt || (item.href ? hostOf(item.href) : ''))
92
+ return image ? [{ image, href: item.href }] : []
93
+ })
94
+ // About four seconds a logo: the strip reads at a pace, whatever its length.
95
+ const duration = `${Math.max(20, shown.length * 4)}s`
52
96
  ---
53
- <Section tone={tone} id={id} aria-labelledby={headingId}>
97
+ <Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
54
98
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
55
- <ul class={s.list()}>
56
- {items.map(item => {
57
- const image = imageOf(item.image, item.imageAlt)
58
- if (!image) return null
59
- const logo = <KitImage image={image} sizes="160px" class={s.logo()} />
60
- return (
99
+ <div class:list={[marquee && 'kit-marquee overflow-hidden']} style={marquee ? `--kit-marquee-duration:${duration}` : undefined}>
100
+ <ul class={s.list({ class: grid })}>
101
+ {shown.map(({ image, href }) => (
61
102
  <li class={s.item()}>
62
- {item.href
63
- ? <a href={item.href} class="group rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4">{logo}</a>
64
- : <span class="group">{logo}</span>}
103
+ {href
104
+ ? <a href={href} class="group rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4"><KitImage image={image} sizes="160px" class={s.logo()} /></a>
105
+ : <span class="group"><KitImage image={image} sizes="160px" class={s.logo()} /></span>}
65
106
  </li>
66
- )
67
- })}
68
- </ul>
107
+ ))}
108
+ </ul>
109
+ {marquee && (
110
+ // The second copy exists only to close the loop: hidden from assistive technology and from the tab order.
111
+ <ul class={`${s.list()} kit-marquee-copy`} aria-hidden="true" inert>
112
+ {shown.map(({ image }) => (
113
+ <li class={s.item()}><span class="group"><KitImage image={image} sizes="160px" class={s.logo()} /></span></li>
114
+ ))}
115
+ </ul>
116
+ )}
117
+ </div>
69
118
  </Section>
119
+ <style>
120
+ /* Reduced motion (the default here): one wrapped row, no copy. */
121
+ .kit-marquee .kit-marquee-track { width: auto; flex-wrap: wrap; justify-content: center; }
122
+ .kit-marquee .kit-marquee-copy { display: none; }
123
+ @media (prefers-reduced-motion: no-preference) {
124
+ .kit-marquee { display: flex; width: 100%; mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent); }
125
+ .kit-marquee .kit-marquee-track,
126
+ .kit-marquee .kit-marquee-copy { display: flex; flex-wrap: nowrap; width: max-content; flex-shrink: 0; justify-content: flex-start; padding-inline-end: 3rem; animation: kit-marquee var(--kit-marquee-duration, 40s) linear infinite; }
127
+ .kit-marquee:hover .kit-marquee-track,
128
+ .kit-marquee:hover .kit-marquee-copy,
129
+ .kit-marquee:focus-within .kit-marquee-track,
130
+ .kit-marquee:focus-within .kit-marquee-copy { animation-play-state: paused; }
131
+ }
132
+ @keyframes kit-marquee { to { transform: translateX(-100%); } }
133
+ </style>