@contentrain/astro-kit 0.16.0 → 0.18.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 CHANGED
@@ -2229,6 +2229,22 @@
2229
2229
  ],
2230
2230
  "default": "vertical",
2231
2231
  "description": "Image on top, image beside the text, or text only with a divider."
2232
+ },
2233
+ "surface": {
2234
+ "options": [
2235
+ "plain",
2236
+ "card"
2237
+ ],
2238
+ "default": "plain",
2239
+ "description": "plain: no surface. card: its own background (--color-card), radius and shadow (--shadow-card), the text padded (--spacing-card) and the image flush at the edge, as theme cards are drawn on a tinted page."
2240
+ },
2241
+ "byline": {
2242
+ "options": [
2243
+ "inline",
2244
+ "below"
2245
+ ],
2246
+ "default": "inline",
2247
+ "description": "inline: category · date above the title. below: the category above the title and author / date under it, both in the accent colour, small and semibold."
2232
2248
  }
2233
2249
  },
2234
2250
  "props": {
@@ -3416,6 +3432,14 @@
3416
3432
  ],
3417
3433
  "default": "auto",
3418
3434
  "description": "The quote's text size. auto: the layout's own (body size in a grid, 2xl–3xl for a single quote). base, lg, xl: that size on every width."
3435
+ },
3436
+ "frame": {
3437
+ "options": [
3438
+ "card",
3439
+ "plain"
3440
+ ],
3441
+ "default": "card",
3442
+ "description": "card: each quote in its box (a bordered, padded card in the grid; the surface colour in a muted section). plain: the quote and who said it straight on the section, across the column, with no box, as Elementor's testimonial widget draws them."
3419
3443
  }
3420
3444
  },
3421
3445
  "props": {
@@ -29,8 +29,10 @@ const faq = tv({
29
29
  plain: { list: 'gap-2', summary: 'list-item list-[revert] py-1 text-[length:var(--text-body,1rem)] [font-weight:inherit] [&::-webkit-details-marker]:inline', marker: 'hidden', answer: 'pt-2 ps-5' },
30
30
  },
31
31
  tone: { default: {}, muted: { item: 'bg-surface' }, inverse: { answer: 'prose-invert' }, accent: { answer: 'prose-invert' } },
32
- // heading: the question as a small heading. bold: body size in bold, as accordion and toggle titles often are.
33
- questionStyle: { heading: {}, bold: { summary: 'text-[length:var(--text-body,1rem)] font-bold' } },
32
+ // heading: the question as a small heading. bold: body size in bold, as accordion and toggle titles often are. The
33
+ // weight is an arbitrary property like the summary's own: twMerge replaces one only with another, and a `font-bold`
34
+ // beside it lost in the CSS (IP-4 elementor G9: the question stayed at the heading weight).
35
+ questionStyle: { heading: {}, bold: { summary: 'text-[length:var(--text-body,1rem)] [font-weight:700]' } },
34
36
  },
35
37
  compoundVariants: [
36
38
  { style: 'cards', tone: 'muted', class: { item: 'border-transparent' } },
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  // One post in a list: image, category, date, title, excerpt and author. The
3
- // whole card is clickable through its title link.
3
+ // whole card is clickable through its title link. `surface: card` makes it a
4
+ // card of its own (background, radius, shadow, padded text, image flush);
5
+ // `byline: below` prints the category above the title and `author / date`
6
+ // under it, as card themes do.
4
7
  import { tv } from 'tailwind-variants'
5
8
  import KitImage from '../_shared/KitImage.astro'
6
9
  import type { ImageInput, LinkInput } from '../_shared/types'
@@ -20,8 +23,16 @@ const card = tv({
20
23
  horizontal: { root: 'flex-col sm:flex-row sm:items-start', media: 'w-full sm:w-64', title: 'text-xl', excerpt: 'line-clamp-2' },
21
24
  minimal: { root: 'flex-col border-b border-line pb-5', media: 'hidden', title: 'text-lg', excerpt: 'line-clamp-2' },
22
25
  },
26
+ surface: {
27
+ plain: {},
28
+ card: { root: 'gap-0 overflow-hidden rounded-[var(--radius-card)] bg-card shadow-[var(--shadow-card,none)]', media: 'rounded-none', body: 'gap-3 p-[var(--spacing-card,1.5rem)]' },
29
+ },
30
+ byline: {
31
+ inline: {},
32
+ below: { meta: 'text-xs font-semibold text-accent' },
33
+ },
23
34
  },
24
- defaultVariants: { layout: 'vertical' },
35
+ defaultVariants: { layout: 'vertical', surface: 'plain', byline: 'inline' },
25
36
  })
26
37
 
27
38
  interface Props {
@@ -37,13 +48,17 @@ interface Props {
37
48
  author?: string | undefined
38
49
  headingLevel?: 2 | 3 | undefined
39
50
  layout?: 'vertical' | 'horizontal' | 'minimal' | undefined
51
+ surface?: 'plain' | 'card' | undefined
52
+ byline?: 'inline' | 'below' | undefined
40
53
  /** The title in the site's link colour, as themes that colour every link do (Hello Elementor). */
41
54
  linkTitle?: boolean | undefined
42
55
  }
43
56
 
44
- const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, linkTitle } = Astro.props
57
+ const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle } = Astro.props
45
58
  const date = rawDate === undefined ? undefined : new Date(rawDate)
46
- const slots = card({ layout })
59
+ const slots = card({ layout, surface, byline })
60
+ const below = byline === 'below'
61
+ const shown = date?.toLocaleDateString(locale, { dateStyle: 'long', timeZone: 'UTC' })
47
62
  const Heading = `h${headingLevel}` as 'h2' | 'h3'
48
63
  ---
49
64
  <article class={slots.root()}>
@@ -53,17 +68,29 @@ const Heading = `h${headingLevel}` as 'h2' | 'h3'
53
68
  </div>
54
69
  )}
55
70
  <div class={slots.body()}>
56
- {(category || date) && (
71
+ {below && category && (
72
+ <p class={slots.meta()}>
73
+ <a href={category.href} class="relative z-10 no-underline hover:underline">{category.label}</a>
74
+ </p>
75
+ )}
76
+ {!below && (category || date) && (
57
77
  <p class={slots.meta()}>
58
78
  {category && <a href={category.href} class="relative z-10 font-medium text-accent no-underline hover:underline">{category.label}</a>}
59
79
  {category && date && <span aria-hidden="true">·</span>}
60
- {date && <time datetime={date.toISOString()}>{date.toLocaleDateString(locale, { dateStyle: 'long', timeZone: 'UTC' })}</time>}
80
+ {date && <time datetime={date.toISOString()}>{shown}</time>}
61
81
  </p>
62
82
  )}
63
83
  <Heading class={slots.title({ class: linkTitle ? 'text-[color:var(--color-link,var(--color-accent))]' : undefined })}>
64
84
  <a href={href} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent">{title}</a>
65
85
  </Heading>
86
+ {below && (author || date) && (
87
+ <p class={slots.meta()}>
88
+ {author && <span>{author}</span>}
89
+ {author && date && <span aria-hidden="true">/</span>}
90
+ {date && <time datetime={date.toISOString()}>{shown}</time>}
91
+ </p>
92
+ )}
66
93
  {excerpt && <p class={slots.excerpt()}>{excerpt}</p>}
67
- {author && <p class="text-sm text-ink-muted">{author}</p>}
94
+ {!below && author && <p class="text-sm text-ink-muted">{author}</p>}
68
95
  </div>
69
96
  </article>
@@ -50,5 +50,28 @@
50
50
  "href": "/category/news/"
51
51
  }
52
52
  }
53
+ },
54
+ {
55
+ "name": "card-byline",
56
+ "props": {
57
+ "surface": "card",
58
+ "byline": "below",
59
+ "title": "Laminating dough without a sheeter",
60
+ "href": "/recipes/laminating/",
61
+ "excerpt": "Laminating dough without a sheeter — the short version first.",
62
+ "date": "2026-02-10T09:00:00Z",
63
+ "category": {
64
+ "label": "Recipes",
65
+ "href": "/category/recipes/"
66
+ },
67
+ "author": "admin",
68
+ "image": {
69
+ "src": "/kit-fixtures/photo-1.svg",
70
+ "alt": "",
71
+ "width": 1200,
72
+ "height": 800
73
+ }
74
+ },
75
+ "frame": "narrow"
53
76
  }
54
77
  ]
@@ -6,7 +6,9 @@
6
6
  "uses": [],
7
7
  "shared": ["KitImage.astro", "types.ts"],
8
8
  "variants": {
9
- "layout": { "options": ["vertical", "horizontal", "minimal"], "default": "vertical", "description": "Image on top, image beside the text, or text only with a divider." }
9
+ "layout": { "options": ["vertical", "horizontal", "minimal"], "default": "vertical", "description": "Image on top, image beside the text, or text only with a divider." },
10
+ "surface": { "options": ["plain", "card"], "default": "plain", "description": "plain: no surface. card: its own background (--color-card), radius and shadow (--shadow-card), the text padded (--spacing-card) and the image flush at the edge, as theme cards are drawn on a tinted page." },
11
+ "byline": { "options": ["inline", "below"], "default": "inline", "description": "inline: category · date above the title. below: the category above the title and author / date under it, both in the accent colour, small and semibold." }
10
12
  },
11
13
  "props": {
12
14
  "title": { "type": "string", "required": true, "description": "Post title (posts.title)." },
@@ -39,11 +39,15 @@ const testimonial = tv({
39
39
  lg: { quote: 'text-lg leading-relaxed md:text-lg' },
40
40
  xl: { quote: 'text-xl leading-snug md:text-xl' },
41
41
  },
42
+ // card: a quote in its box (the grid's border and padding, a muted section's surface). plain: the quote and who said
43
+ // it on the section itself, across the column, as Elementor's testimonial widget draws them.
44
+ frame: { card: {}, plain: {} },
42
45
  },
43
46
  compoundVariants: [
44
47
  { layout: 'grid', tone: 'muted', class: { card: 'border-transparent text-ink' } },
48
+ { frame: 'plain', class: { card: 'rounded-none border-0 bg-transparent p-0' } },
45
49
  ],
46
- defaultVariants: { layout: 'grid', tone: 'default', quoteSize: 'auto' },
50
+ defaultVariants: { layout: 'grid', tone: 'default', quoteSize: 'auto', frame: 'card' },
47
51
  })
48
52
 
49
53
  interface Props {
@@ -51,13 +55,16 @@ interface Props {
51
55
  heading?: string | undefined
52
56
  layout?: 'single' | 'grid' | undefined
53
57
  quoteSize?: 'auto' | 'base' | 'lg' | 'xl' | undefined
58
+ frame?: 'card' | 'plain' | undefined
54
59
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
55
60
  id?: string | undefined
56
61
  }
57
62
 
58
- const { items, heading, layout, quoteSize, tone, id } = Astro.props
59
- const s = testimonial({ layout, quoteSize, tone })
63
+ const { items, heading, layout, quoteSize, frame, tone, id } = Astro.props
64
+ const s = testimonial({ layout, quoteSize, frame, tone })
60
65
  const headingId = heading && id ? `${id}-title` : undefined
66
+ // Two quotes share the row: a third column would stand empty beside them and narrow both (IP-4 elementor launch G9).
67
+ const list = s.list(items.length === 2 && (layout ?? 'grid') === 'grid' ? { class: 'lg:grid-cols-2' } : {})
61
68
  // The quotation marks are drawn, not written: the quote's text is the source's words, read once. A quote the source
62
69
  // already wrote inside marks keeps its own.
63
70
  const quoted = (text: string) => /^\s*["'“‘„«‹「『]/.test(text)
@@ -65,7 +72,7 @@ const quoted = (text: string) => /^\s*["'“‘„«‹「『]/.test(text)
65
72
  {items.length > 0 && (
66
73
  <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
67
74
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
68
- <div class={s.list()}>
75
+ <div class={list}>
69
76
  {items.map(item => (
70
77
  <figure class={s.card()}>
71
78
  <blockquote class={s.quote()}><p class:list={[!quoted(item.quote) && "before:content-[open-quote] after:content-[close-quote]"]}>{item.quote}</p></blockquote>
@@ -35,6 +35,14 @@
35
35
  "name": "single-quote-lg",
36
36
  "props": { "layout": "single", "quoteSize": "lg", "items": [{ "quote": "We planned less and saw more.", "name": "Jonas Weber", "role": "Reader" }] }
37
37
  },
38
+ {
39
+ "name": "grid-plain",
40
+ "props": { "frame": "plain", "quoteSize": "xl", "items": [{ "quote": "We sold out our first kiln batch on launch day.", "name": "Ruth Adeyemi", "role": "Potter" }, { "quote": "Six weeks, one price, and they answered every email.", "name": "Daniel Okafor", "role": "Leatherworker" }] }
41
+ },
42
+ {
43
+ "name": "single-plain-muted",
44
+ "props": { "layout": "single", "frame": "plain", "tone": "muted", "items": [{ "quote": "Calm pages and a fast checkout.", "name": "Ruth Adeyemi" }] }
45
+ },
38
46
  {
39
47
  "name": "grid-source-quoted",
40
48
  "props": { "items": [{ "quote": "“We set it up before lunch.”", "name": "Grace Lin", "role": "Head of Support, Fernhill" }, { "quote": "People just read the page.", "name": "Omar Haddad", "role": "Support lead" }] }
@@ -38,6 +38,14 @@
38
38
  ],
39
39
  "default": "auto",
40
40
  "description": "The quote's text size. auto: the layout's own (body size in a grid, 2xl–3xl for a single quote). base, lg, xl: that size on every width."
41
+ },
42
+ "frame": {
43
+ "options": [
44
+ "card",
45
+ "plain"
46
+ ],
47
+ "default": "card",
48
+ "description": "card: each quote in its box (a bordered, padded card in the grid; the surface colour in a muted section). plain: the quote and who said it straight on the section, across the column, with no box, as Elementor's testimonial widget draws them."
41
49
  }
42
50
  },
43
51
  "props": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "astro-kit-mapping@1",
3
3
  "builder": "elementor",
4
- "version": "9",
4
+ "version": "11",
5
5
  "fallback": "prose",
6
6
  "defaults": {
7
7
  "elementor/video": {
@@ -276,6 +276,9 @@
276
276
  {
277
277
  "match": "elementor/testimonial",
278
278
  "component": "testimonial",
279
+ "variant": {
280
+ "frame": "plain"
281
+ },
279
282
  "group": "siblings",
280
283
  "into": "items",
281
284
  "item": {
@@ -605,6 +608,9 @@
605
608
  {
606
609
  "id": "team.columns",
607
610
  "component": "team",
611
+ "variant": {
612
+ "roleStyle": "plain"
613
+ },
608
614
  "when": {
609
615
  "columns": [
610
616
  2,
@@ -761,6 +767,9 @@
761
767
  {
762
768
  "id": "stats.counter",
763
769
  "component": "stats",
770
+ "variant": {
771
+ "labelSize": "lg"
772
+ },
764
773
  "slots": {
765
774
  "title": {
766
775
  "match": "elementor/heading",
@@ -848,6 +857,10 @@
848
857
  {
849
858
  "id": "testimonial.quotes",
850
859
  "component": "testimonial",
860
+ "variant": {
861
+ "quoteSize": "xl",
862
+ "frame": "plain"
863
+ },
851
864
  "slots": {
852
865
  "title": {
853
866
  "match": "elementor/heading",
@@ -874,6 +887,9 @@
874
887
  {
875
888
  "id": "faq.accordion",
876
889
  "component": "faq",
890
+ "variant": {
891
+ "questionStyle": "bold"
892
+ },
877
893
  "slots": {
878
894
  "title": {
879
895
  "match": "elementor/heading",
@@ -898,6 +914,9 @@
898
914
  {
899
915
  "id": "faq.toggle",
900
916
  "component": "faq",
917
+ "variant": {
918
+ "questionStyle": "bold"
919
+ },
901
920
  "slots": {
902
921
  "title": {
903
922
  "match": "elementor/heading",
@@ -1103,7 +1122,7 @@
1103
1122
  "id": "feature-list.icon-list",
1104
1123
  "component": "feature-list",
1105
1124
  "variant": {
1106
- "style": "icon"
1125
+ "style": "list"
1107
1126
  },
1108
1127
  "slots": {
1109
1128
  "title": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "license": "MIT",
5
5
  "description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "mapping"
43
43
  ],
44
44
  "dependencies": {
45
- "@contentrain/types": "1.35.0"
45
+ "@contentrain/types": "1.36.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.10.0",