@contentrain/astro-kit 0.39.0 → 0.41.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 (76) hide show
  1. package/README.md +18 -2
  2. package/catalog.json +99 -18
  3. package/components/_shared/Section.astro +13 -9
  4. package/components/_shared/SectionHeader.astro +3 -2
  5. package/components/_shared/behaviors.ts +38 -2
  6. package/components/_shared/embed-frame.ts +22 -0
  7. package/components/{embed/embed.ts → _shared/embed-target.ts} +30 -0
  8. package/components/_shared/instance-id.ts +24 -0
  9. package/components/_shared/muted.ts +19 -0
  10. package/components/band/Band.astro +10 -7
  11. package/components/band/meta.json +2 -0
  12. package/components/behavior/Behavior.astro +15 -2
  13. package/components/behavior/fixtures.json +5 -3
  14. package/components/behavior/meta.json +9 -0
  15. package/components/card-grid/CardGrid.astro +13 -9
  16. package/components/card-grid/meta.json +7 -0
  17. package/components/contact-details/ContactDetails.astro +7 -3
  18. package/components/contact-details/meta.json +2 -0
  19. package/components/contact-form/ContactForm.astro +11 -7
  20. package/components/contact-form/meta.json +2 -0
  21. package/components/cta/Cta.astro +14 -10
  22. package/components/cta/meta.json +2 -2
  23. package/components/dialog/Dialog.astro +6 -3
  24. package/components/dialog/meta.json +2 -0
  25. package/components/embed/Embed.astro +7 -10
  26. package/components/embed/meta.json +1 -1
  27. package/components/faq/Faq.astro +8 -4
  28. package/components/faq/meta.json +1 -1
  29. package/components/feature-list/FeatureList.astro +10 -6
  30. package/components/feature-list/meta.json +3 -1
  31. package/components/footer/Footer.astro +4 -3
  32. package/components/footer/meta.json +1 -0
  33. package/components/gallery/Gallery.astro +8 -4
  34. package/components/gallery/meta.json +2 -0
  35. package/components/header/Header.astro +8 -4
  36. package/components/header/meta.json +1 -1
  37. package/components/hero/Hero.astro +13 -9
  38. package/components/hero/meta.json +3 -1
  39. package/components/language-switcher/LanguageSwitcher.astro +3 -2
  40. package/components/language-switcher/meta.json +1 -1
  41. package/components/logo-cloud/LogoCloud.astro +9 -5
  42. package/components/logo-cloud/meta.json +3 -1
  43. package/components/nav/Nav.astro +10 -6
  44. package/components/nav/meta.json +2 -0
  45. package/components/newsletter/Newsletter.astro +9 -5
  46. package/components/newsletter/meta.json +2 -0
  47. package/components/post-list/PostList.astro +7 -3
  48. package/components/post-list/meta.json +2 -0
  49. package/components/pricing/Pricing.astro +15 -11
  50. package/components/pricing/meta.json +3 -1
  51. package/components/search-box/SearchBox.astro +4 -1
  52. package/components/search-box/meta.json +1 -1
  53. package/components/slider/Slider.astro +9 -2
  54. package/components/slider/SliderTrack.astro +71 -15
  55. package/components/slider/fixtures.json +2 -1
  56. package/components/slider/meta.json +5 -4
  57. package/components/social-links/SocialLinks.astro +2 -1
  58. package/components/social-links/meta.json +3 -2
  59. package/components/split/Split.astro +9 -5
  60. package/components/split/meta.json +3 -1
  61. package/components/stats/Stats.astro +11 -7
  62. package/components/stats/meta.json +5 -1
  63. package/components/steps/Steps.astro +9 -5
  64. package/components/steps/meta.json +3 -1
  65. package/components/tabs/Tabs.astro +8 -4
  66. package/components/tabs/meta.json +1 -1
  67. package/components/team/Team.astro +10 -6
  68. package/components/team/meta.json +4 -2
  69. package/components/testimonial/Testimonial.astro +9 -5
  70. package/components/testimonial/fixtures.json +1 -1
  71. package/components/testimonial/meta.json +3 -1
  72. package/mapping/classic.json +14 -7
  73. package/mapping/divi.json +6 -3
  74. package/mapping/elementor.json +14 -7
  75. package/mapping/gutenberg.json +2 -1
  76. package/package.json +1 -1
@@ -11,7 +11,9 @@
11
11
  "Button.astro",
12
12
  "KitImage.astro",
13
13
  "Section.astro",
14
+ "instance-id.ts",
14
15
  "measured.ts",
16
+ "muted.ts",
15
17
  "safe-url.ts",
16
18
  "types.ts"
17
19
  ],
@@ -339,7 +341,7 @@
339
341
  "id": {
340
342
  "type": "string",
341
343
  "content": false,
342
- "description": "Anchor id of the section; the heading gets `<id>-title`."
344
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `split`, `split-2`, … in render order, so every section on a page has its own."
343
345
  },
344
346
  "sticky": {
345
347
  "type": "boolean",
@@ -7,9 +7,11 @@ import { tv } from 'tailwind-variants'
7
7
  import BehaviorScript from '../_shared/BehaviorScript.astro'
8
8
  import KitImage from '../_shared/KitImage.astro'
9
9
  import Section, { type Tone } from '../_shared/Section.astro'
10
+ import { instanceId } from '../_shared/instance-id'
10
11
  import { imageOf, type ImageSource } from '../_shared/image'
11
12
  import { measuredGrid } from '../_shared/measured'
12
13
  import type { MeasuredStyle } from '../_shared/types'
14
+ import { muted, mutedReset } from '../_shared/muted'
13
15
 
14
16
  const stats = tv({
15
17
  slots: {
@@ -20,7 +22,7 @@ const stats = tv({
20
22
  // height comes with valueSize too: a font-size class merged in later would drop one set here.
21
23
  value: 'text-[length:var(--stat-max,2.25rem)] [font-weight:var(--font-weight-heading,700)] tracking-tight tabular-nums whitespace-nowrap [--stat-max-md:3rem] md:text-[length:var(--stat-max-md)]',
22
24
  icon: 'mb-3 block size-10 object-contain',
23
- label: 'text-sm opacity-80',
25
+ label: `text-sm ${muted}`,
24
26
  },
25
27
  variants: {
26
28
  // grid: figures in columns. inline: one line that wraps, for two to four short figures.
@@ -43,7 +45,7 @@ const stats = tv({
43
45
  },
44
46
  style: {
45
47
  plain: {},
46
- card: { item: 'rounded-[var(--radius-card)] bg-surface p-6 text-ink ring-1 ring-line/60' },
48
+ card: { item: `rounded-[var(--radius-card)] bg-surface p-6 text-ink ring-1 ring-line/60 ${mutedReset}` },
47
49
  // Hairlines between the figures, none before the first, no box: across on a wide screen, above each on a phone.
48
50
  // Meant for a single row (figures no more than the columns); a wrapped row starts with a line.
49
51
  divided: { item: 'border-t border-line pt-6 first:border-t-0 first:pt-0 md:border-t-0 md:pt-0 md:border-s md:ps-6 md:first:border-s-0 md:first:ps-0' },
@@ -57,7 +59,7 @@ const stats = tv({
57
59
  display: { value: '[--stat-max:3.75rem] [--stat-max-md:4.5rem] leading-none' },
58
60
  },
59
61
  // accent/accent-ink is only ~5:1 — dimming text on it drops it under 4.5:1 (WCAG 1.4.3).
60
- tone: { default: {}, muted: {}, inverse: {}, accent: { label: 'opacity-100' } },
62
+ tone: { default: {}, muted: {}, inverse: {}, accent: {} },
61
63
  // The label under each figure: small, body size, or large (a counter title set like a subheading).
62
64
  labelSize: { sm: {}, base: { label: 'text-base' }, lg: { label: 'text-lg' } },
63
65
  // Which comes first in the page's text: the source's own order (a Gutenberg figure is a heading over its label, an
@@ -95,23 +97,25 @@ interface Props {
95
97
  id?: string | undefined
96
98
  }
97
99
 
98
- const { heading, intro, items, layout, columns, style, align, valueSize, labelSize, order, motion, tone, measured, id } = Astro.props
100
+ const { heading, intro, items, layout, columns, style, align, valueSize, labelSize, order, motion, tone, measured, id: givenId } = Astro.props
101
+ // Every section has an id, so a theme or a design layer can address this one: the page's, or `stats`, `stats-2`, … in render order.
102
+ const id = instanceId(Astro.locals, givenId, 'stats')
99
103
  const count = motion === 'count'
100
104
  // A count-up island (behaviors.ts `countup`): the root names the kind, each figure's digits are their own span.
101
105
  const island = count ? { 'data-cr-behavior': 'countup', 'data-cr-items': '[data-kit-count]' } : {}
102
106
  const s = stats({ layout, columns, style, align, valueSize, labelSize, order, tone })
103
107
  const grid = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
104
- const headingId = heading && id ? `${id}-title` : undefined
108
+ const headingId = heading ? `${id}-title` : undefined
105
109
  const iconOf = (item: Item) => imageOf(item.icon, '')
106
110
  // The figure's width in characters, for the size it may take in a narrow cell.
107
111
  const widthOf = (item: Item) => `--stat-chars:${(item.prefix ?? '').length + item.value.length + (item.suffix ?? '').length}`
108
112
  // A <dl> names its term first, so figure-first is a plain list, read in the order it is drawn (WCAG 1.3.2).
109
113
  ---
110
- <Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
114
+ <Section kit="stats" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
111
115
  {(heading || intro) && (
112
116
  <header class="mb-10 flex max-w-2xl flex-col gap-3">
113
117
  {heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
114
- {intro && <p class:list={['text-lg', tone === 'accent' ? undefined : 'opacity-80']} data-kit-text>{intro}</p>}
118
+ {intro && <p class:list={['text-lg', muted]} data-kit-text>{intro}</p>}
115
119
  </header>
116
120
  )}
117
121
  {order === 'value-first' ? (
@@ -9,8 +9,12 @@
9
9
  "KitImage.astro",
10
10
  "Section.astro",
11
11
  "behaviors.ts",
12
+ "embed-frame.ts",
13
+ "embed-target.ts",
12
14
  "image.ts",
15
+ "instance-id.ts",
13
16
  "measured.ts",
17
+ "muted.ts",
14
18
  "safe-url.ts",
15
19
  "types.ts"
16
20
  ],
@@ -259,7 +263,7 @@
259
263
  "id": {
260
264
  "type": "string",
261
265
  "content": false,
262
- "description": "Anchor id of the section; the heading gets `<id>-title`."
266
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `stats`, `stats-2`, … in render order, so every section on a page has its own."
263
267
  }
264
268
  },
265
269
  "slots": {},
@@ -6,15 +6,17 @@
6
6
  import { tv } from 'tailwind-variants'
7
7
  import KitImage from '../_shared/KitImage.astro'
8
8
  import Section from '../_shared/Section.astro'
9
+ import { instanceId } from '../_shared/instance-id'
9
10
  import { measuredGrid } from '../_shared/measured'
10
11
  import type { MeasuredStyle } from '../_shared/types'
11
12
  import { imageOf, type ImageSource } from '../_shared/image'
13
+ import { muted } from '../_shared/muted'
12
14
 
13
15
  const steps = tv({
14
16
  slots: {
15
17
  header: 'mb-10 flex max-w-2xl flex-col gap-3',
16
18
  heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
17
- intro: 'text-lg opacity-80',
19
+ intro: `text-lg ${muted}`,
18
20
  list: 'grid gap-8 [counter-reset:kit-step]',
19
21
  step: 'flex gap-4 [counter-increment:kit-step]',
20
22
  date: 'text-sm font-semibold tabular-nums text-accent-text',
@@ -22,7 +24,7 @@ const steps = tv({
22
24
  badge: 'shrink-0',
23
25
  media: 'mb-2 overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
24
26
  title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
25
- text: 'text-pretty opacity-80',
27
+ text: `text-pretty ${muted}`,
26
28
  },
27
29
  variants: {
28
30
  layout: {
@@ -79,18 +81,20 @@ interface Props {
79
81
  id?: string | undefined
80
82
  }
81
83
 
82
- const { heading, intro, items, kind = 'steps', layout: givenLayout, marker, connector, currentLabel = 'Current', style, measured, id } = Astro.props
84
+ const { heading, intro, items, kind = 'steps', layout: givenLayout, marker, connector, currentLabel = 'Current', style, measured, id: givenId } = Astro.props
85
+ // Every section has an id, so a theme or a design layer can address this one: the page's, or `steps`, `steps-2`, … in render order.
86
+ const id = instanceId(Astro.locals, givenId, 'steps')
83
87
  // A timeline defaults to the rail layout and, without numbers, to the plain dot marker.
84
88
  const layout = givenLayout ?? (kind === 'timeline' ? 'rail' : 'list')
85
89
  const s = steps({ layout, marker: marker ?? (kind === 'timeline' ? 'dot' : 'number'), connector, style })
86
90
  // A bare year or an ISO date is machine-readable; anything else ("Spring 2021") stays text.
87
91
  const isoDate = (date: string) => /^\d{4}(-\d{2}(-\d{2})?)?$/.test(date.trim()) ? date.trim() : undefined
88
- const headingId = heading && id ? `${id}-title` : undefined
92
+ const headingId = heading ? `${id}-title` : undefined
89
93
  // One level under the section heading, or the section's heading when there is none.
90
94
  const StepHeading = heading ? 'h3' : 'h2'
91
95
  ---
92
96
  {items.length > 0 && (
93
- <Section measured={measured} id={id} aria-labelledby={headingId}>
97
+ <Section kit="steps" measured={measured} id={id} aria-labelledby={headingId}>
94
98
  {(heading || intro) && (
95
99
  <header class={s.header()}>
96
100
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
@@ -8,7 +8,9 @@
8
8
  "KitImage.astro",
9
9
  "Section.astro",
10
10
  "image.ts",
11
+ "instance-id.ts",
11
12
  "measured.ts",
13
+ "muted.ts",
12
14
  "safe-url.ts",
13
15
  "types.ts"
14
16
  ],
@@ -119,7 +121,7 @@
119
121
  "id": {
120
122
  "type": "string",
121
123
  "content": false,
122
- "description": "Anchor id of the section; the heading gets `<id>-title`."
124
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `steps`, `steps-2`, … in render order, so every section on a page has its own."
123
125
  },
124
126
  "currentLabel": {
125
127
  "type": "string",
@@ -7,6 +7,8 @@ import { tv } from 'tailwind-variants'
7
7
  import Section from '../_shared/Section.astro'
8
8
  import SectionHeader from '../_shared/SectionHeader.astro'
9
9
  import type { ActionInput, MeasuredStyle } from '../_shared/types'
10
+ import { instanceId } from '../_shared/instance-id'
11
+ import { muted } from '../_shared/muted'
10
12
 
11
13
  export interface TabItem {
12
14
  label: string
@@ -19,7 +21,7 @@ const tabs = tv({
19
21
  heading: 'mb-8 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
20
22
  root: 'group/tabs flex flex-col gap-6',
21
23
  list: 'hidden gap-1 group-data-[ready]/tabs:flex',
22
- trigger: 'cursor-pointer rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap opacity-75 transition hover:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent data-[selected]:opacity-100',
24
+ trigger: `cursor-pointer rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap ${muted} transition hover:text-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent data-[selected]:text-current`,
23
25
  panel: 'prose-site prose max-w-none text-current',
24
26
  title: 'mb-3 text-lg [font-weight:var(--font-weight-heading,600)] group-data-[ready]/tabs:sr-only',
25
27
  },
@@ -32,7 +34,7 @@ const tabs = tv({
32
34
  horizontal: { list: 'overflow-x-auto' },
33
35
  vertical: { root: 'md:data-[ready]:flex-row md:data-[ready]:items-start md:data-[ready]:gap-10', list: 'md:w-56 md:shrink-0 md:flex-col', trigger: 'text-start' },
34
36
  },
35
- tone: { default: {}, muted: {}, inverse: { panel: 'prose-invert' }, accent: { panel: 'prose-invert', trigger: 'opacity-100' } },
37
+ tone: { default: {}, muted: {}, inverse: { panel: 'prose-invert' }, accent: { panel: 'prose-invert' } },
36
38
  },
37
39
  compoundVariants: [
38
40
  { style: 'underline', orientation: 'vertical', class: { list: 'md:border-b-0 md:border-s', trigger: 'md:mb-0 md:-ms-px md:border-b-0 md:border-s-2' } },
@@ -57,7 +59,9 @@ interface Props {
57
59
  measured?: MeasuredStyle | undefined
58
60
  }
59
61
 
60
- const { items, heading, intro, eyebrow, actions, label, style, orientation = 'horizontal', tone, id = 'tabs', measured } = Astro.props
62
+ const { items, heading, intro, eyebrow, actions, label, style, orientation = 'horizontal', tone, id: givenId, measured } = Astro.props
63
+ // Two on one page never share an id: their labels and controls would point into each other.
64
+ const id = instanceId(Astro.locals, givenId, 'tabs')
61
65
  const s = tabs({ style, orientation, tone })
62
66
  const headingId = heading ? `${id}-title` : undefined
63
67
  // One level under the section heading, or the section's heading when there is none.
@@ -65,7 +69,7 @@ const PanelHeading = heading ? 'h3' : 'h2'
65
69
  const value = (i: number) => `tab-${i + 1}`
66
70
  ---
67
71
  {items.length > 0 && (
68
- <Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
72
+ <Section kit="tabs" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
69
73
  {intro || eyebrow || actions?.length
70
74
  ? <SectionHeader heading={heading} intro={intro} eyebrow={eyebrow} actions={actions} headingId={headingId} />
71
75
  : heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
@@ -4,7 +4,7 @@
4
4
  "description": "Tabbed panels with the WAI-ARIA tabs pattern from Zag.js (arrow keys, Home/End, roving focus). Without script every panel shows in turn under its own title, so no content is ever hidden from a reader or a crawler.",
5
5
  "category": "content",
6
6
  "uses": [],
7
- "shared": ["Button.astro", "Section.astro", "SectionHeader.astro", "measured.ts", "safe-url.ts", "types.ts"],
7
+ "shared": ["Button.astro", "Section.astro", "SectionHeader.astro", "instance-id.ts", "measured.ts", "muted.ts", "safe-url.ts", "types.ts"],
8
8
  "variants": {
9
9
  "style": { "options": ["underline", "pills"], "default": "underline", "description": "underline: a rule under the list, the selected tab underlined in accent. pills: the selected tab filled with accent." },
10
10
  "orientation": { "options": ["horizontal", "vertical"], "default": "horizontal", "description": "vertical: the tab list beside the panel on wide screens (Elementor vertical tabs)." },
@@ -4,26 +4,28 @@
4
4
  import { tv } from 'tailwind-variants'
5
5
  import KitImage from '../_shared/KitImage.astro'
6
6
  import Section from '../_shared/Section.astro'
7
+ import { instanceId } from '../_shared/instance-id'
7
8
  import { measuredGrid } from '../_shared/measured'
8
9
  import type { MeasuredStyle } from '../_shared/types'
9
10
  import { imageOf, type ImageSource } from '../_shared/image'
10
11
  import { profile } from './team'
11
12
  import { safeHref } from '../_shared/safe-url'
13
+ import { muted } from '../_shared/muted'
12
14
 
13
15
  const team = tv({
14
16
  slots: {
15
17
  header: 'mb-10 flex max-w-2xl flex-col gap-3',
16
18
  heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
17
- intro: 'text-lg opacity-80',
19
+ intro: `text-lg ${muted}`,
18
20
  list: 'grid gap-6 sm:grid-cols-2 md:gap-8',
19
21
  socials: 'relative z-10 mt-1 flex flex-wrap gap-3 text-sm',
20
22
  social: 'rounded text-accent-text underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
21
- more: 'text-pretty opacity-80 [&>summary]:text-sm [&>summary]:font-medium [&>summary]:text-accent-text',
23
+ more: `text-pretty ${muted} [&>summary]:text-sm [&>summary]:font-medium [&>summary]:text-accent-text`,
22
24
  member: 'group relative flex flex-col gap-3',
23
25
  portrait: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
24
26
  name: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)]',
25
27
  role: 'text-sm font-medium text-accent-text',
26
- bio: 'text-pretty opacity-80',
28
+ bio: `text-pretty ${muted}`,
27
29
  },
28
30
  variants: {
29
31
  columns: {
@@ -82,17 +84,19 @@ interface Props {
82
84
  id?: string | undefined
83
85
  }
84
86
 
85
- const { heading, intro, items, columns, layout, imageShape, align, bioClamp, readMoreLabel = 'Read more', socialLabel = '{name} on {network}', style, roleStyle, measured, id } = Astro.props
87
+ const { heading, intro, items, columns, layout, imageShape, align, bioClamp, readMoreLabel = 'Read more', socialLabel = '{name} on {network}', style, roleStyle, measured, id: givenId } = Astro.props
88
+ // Every section has an id, so a theme or a design layer can address this one: the page's, or `team`, `team-2`, … in render order.
89
+ const id = instanceId(Astro.locals, givenId, 'team')
86
90
  const s = team({ columns, layout, imageShape, align, style, roleStyle })
87
91
  const clamp = { 2: 'line-clamp-2', 3: 'line-clamp-3', 4: 'line-clamp-4' } as const
88
92
  // A bio that fits the clamp needs no disclosure: short means about 160 characters per two lines, a rough, build-time guess.
89
93
  const longBio = (bio: string) => bioClamp !== undefined && bio.length > bioClamp * 80
90
- const headingId = heading && id ? `${id}-title` : undefined
94
+ const headingId = heading ? `${id}-title` : undefined
91
95
  // One level under the section heading, or the section's heading when there is none.
92
96
  const NameHeading = heading ? 'h3' : 'h2'
93
97
  ---
94
98
  {items.length > 0 && (
95
- <Section measured={measured} id={id} aria-labelledby={headingId}>
99
+ <Section kit="team" measured={measured} id={id} aria-labelledby={headingId}>
96
100
  {(heading || intro) && (
97
101
  <header class={s.header()}>
98
102
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
@@ -8,9 +8,11 @@
8
8
  "KitImage.astro",
9
9
  "Section.astro",
10
10
  "image.ts",
11
+ "instance-id.ts",
11
12
  "measured.ts",
12
- "social.ts",
13
+ "muted.ts",
13
14
  "safe-url.ts",
15
+ "social.ts",
14
16
  "types.ts"
15
17
  ],
16
18
  "variants": {
@@ -131,7 +133,7 @@
131
133
  "id": {
132
134
  "type": "string",
133
135
  "content": false,
134
- "description": "Anchor id of the section; the heading gets `<id>-title`."
136
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `team`, `team-2`, … in render order, so every section on a page has its own."
135
137
  },
136
138
  "bioClamp": {
137
139
  "type": "integer",
@@ -5,9 +5,11 @@
5
5
  import { tv } from 'tailwind-variants'
6
6
  import KitImage from '../_shared/KitImage.astro'
7
7
  import Section from '../_shared/Section.astro'
8
+ import { instanceId } from '../_shared/instance-id'
8
9
  import { imageOf, type ImageSource } from '../_shared/image'
9
10
  import { measuredGrid } from '../_shared/measured'
10
11
  import type { MeasuredStyle } from '../_shared/types'
12
+ import { muted } from '../_shared/muted'
11
13
 
12
14
  export interface TestimonialItem {
13
15
  quote: string
@@ -35,7 +37,7 @@ const testimonial = tv({
35
37
  avatar: 'size-12 shrink-0 rounded-full object-cover',
36
38
  logo: 'ms-auto h-6 w-auto max-w-24 object-contain opacity-80',
37
39
  name: 'font-semibold',
38
- role: 'text-sm opacity-75',
40
+ role: `text-sm ${muted}`,
39
41
  },
40
42
  variants: {
41
43
  layout: {
@@ -51,7 +53,7 @@ const testimonial = tv({
51
53
  avatarPlace: { start: {}, top: {} },
52
54
  // marks: the quotation marks around the text, as the source writes it. glyph: one large decorative mark above.
53
55
  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' } },
56
+ tone: { default: {}, muted: { card: 'bg-surface' }, inverse: { card: 'border-surface/20' }, accent: { card: 'border-accent-ink/30', logo: 'opacity-100' } },
55
57
  // The quote's size; `auto` is the layout's own (body size in a grid, large for a single quote).
56
58
  quoteSize: {
57
59
  auto: {},
@@ -89,9 +91,11 @@ interface Props {
89
91
  id?: string | undefined
90
92
  }
91
93
 
92
- const { items, heading, layout, quoteSize, frame, avatarSize, avatarPlace, quoteMark = 'marks', ratingLabel = '{rating} out of 5 stars', label = 'Testimonials', tone, measured, id } = Astro.props
94
+ const { items, heading, layout, quoteSize, frame, avatarSize, avatarPlace, quoteMark = 'marks', ratingLabel = '{rating} out of 5 stars', label = 'Testimonials', tone, measured, id: givenId } = Astro.props
95
+ // Every section has an id, so a theme or a design layer can address this one: the page's, or `testimonial`, `testimonial-2`, … in render order.
96
+ const id = instanceId(Astro.locals, givenId, 'testimonial')
93
97
  const s = testimonial({ layout, quoteSize, frame, avatarSize, avatarPlace, quoteMark, tone })
94
- const headingId = heading && id ? `${id}-title` : undefined
98
+ const headingId = heading ? `${id}-title` : undefined
95
99
  // Two quotes share the row: a third column would stand empty beside them and narrow both (IP-4 elementor launch G9).
96
100
  const measuredList = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
97
101
  const list = s.list({ class: measuredList ?? (items.length === 2 && (layout ?? 'grid') === 'grid' ? 'lg:grid-cols-2' : undefined) })
@@ -105,7 +109,7 @@ const spoken = (rating: number) => String(Math.round(rating * 10) / 10)
105
109
  const scrolls = layout === 'row'
106
110
  ---
107
111
  {items.length > 0 && (
108
- <Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
112
+ <Section kit="testimonial" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
109
113
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
110
114
  <div class={list} tabindex={scrolls ? 0 : undefined} role={scrolls ? 'region' : undefined} aria-labelledby={scrolls && heading ? headingId : undefined} aria-label={scrolls && !heading ? label : undefined}>
111
115
  {items.map((item) => {
@@ -100,7 +100,7 @@
100
100
  "name": "measured-columns",
101
101
  "props": {
102
102
  "heading": "What readers say",
103
- "id": "readers",
103
+ "id": "readers-2",
104
104
  "items": [
105
105
  { "quote": "The only travel site I read to the end. Every guide feels written by someone who stayed.", "name": "Ayşe Demir", "role": "Reader since 2019", "avatar": { "src": "/kit-fixtures/avatar-1.svg", "alt": "", "width": 96, "height": 96 } },
106
106
  { "quote": "Their Lisbon guide saved our trip.", "name": "Tom Becker", "avatar": { "src": "/kit-fixtures/avatar-2.svg", "alt": "", "width": 96, "height": 96 } },
@@ -8,7 +8,9 @@
8
8
  "KitImage.astro",
9
9
  "Section.astro",
10
10
  "image.ts",
11
+ "instance-id.ts",
11
12
  "measured.ts",
13
+ "muted.ts",
12
14
  "safe-url.ts",
13
15
  "types.ts"
14
16
  ],
@@ -250,7 +252,7 @@
250
252
  "id": {
251
253
  "type": "string",
252
254
  "content": false,
253
- "description": "Anchor id of the section; the heading gets `<id>-title`."
255
+ "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `testimonial`, `testimonial-2`, … in render order, so every section on a page has its own."
254
256
  },
255
257
  "ratingLabel": {
256
258
  "type": "string",
@@ -102,7 +102,8 @@
102
102
  "prevLabel": "ui:slider.prev",
103
103
  "nextLabel": "ui:slider.next",
104
104
  "slideLabel": "ui:slider.slide",
105
- "ofLabel": "ui:slider.of"
105
+ "ofLabel": "ui:slider.of",
106
+ "playLabel": "ui:embed.play"
106
107
  },
107
108
  "into": "slides",
108
109
  "each": "rs-slide, .tp-revslider-slidesli",
@@ -120,7 +121,8 @@
120
121
  "prevLabel": "ui:slider.prev",
121
122
  "nextLabel": "ui:slider.next",
122
123
  "slideLabel": "ui:slider.slide",
123
- "ofLabel": "ui:slider.of"
124
+ "ofLabel": "ui:slider.of",
125
+ "playLabel": "ui:embed.play"
124
126
  },
125
127
  "into": "slides",
126
128
  "each": ".slides > li",
@@ -138,7 +140,8 @@
138
140
  "prevLabel": "ui:slider.prev",
139
141
  "nextLabel": "ui:slider.next",
140
142
  "slideLabel": "ui:slider.slide",
141
- "ofLabel": "ui:slider.of"
143
+ "ofLabel": "ui:slider.of",
144
+ "playLabel": "ui:embed.play"
142
145
  },
143
146
  "into": "slides",
144
147
  "each": ".n2-ss-slide",
@@ -431,7 +434,8 @@
431
434
  "prevLabel": "ui:slider.prev",
432
435
  "nextLabel": "ui:slider.next",
433
436
  "slideLabel": "ui:slider.slide",
434
- "ofLabel": "ui:slider.of"
437
+ "ofLabel": "ui:slider.of",
438
+ "playLabel": "ui:embed.play"
435
439
  },
436
440
  "into": "slides",
437
441
  "each": ".swiper-slide:not(.swiper-slide-duplicate)",
@@ -450,7 +454,8 @@
450
454
  "prevLabel": "ui:slider.prev",
451
455
  "nextLabel": "ui:slider.next",
452
456
  "slideLabel": "ui:slider.slide",
453
- "ofLabel": "ui:slider.of"
457
+ "ofLabel": "ui:slider.of",
458
+ "playLabel": "ui:embed.play"
454
459
  },
455
460
  "into": "slides",
456
461
  "each": ".slick-slide:not(.slick-cloned)",
@@ -469,7 +474,8 @@
469
474
  "prevLabel": "ui:slider.prev",
470
475
  "nextLabel": "ui:slider.next",
471
476
  "slideLabel": "ui:slider.slide",
472
- "ofLabel": "ui:slider.of"
477
+ "ofLabel": "ui:slider.of",
478
+ "playLabel": "ui:embed.play"
473
479
  },
474
480
  "into": "slides",
475
481
  "each": ".splide__slide:not(.splide__slide--clone)",
@@ -488,7 +494,8 @@
488
494
  "prevLabel": "ui:slider.prev",
489
495
  "nextLabel": "ui:slider.next",
490
496
  "slideLabel": "ui:slider.slide",
491
- "ofLabel": "ui:slider.of"
497
+ "ofLabel": "ui:slider.of",
498
+ "playLabel": "ui:embed.play"
492
499
  },
493
500
  "into": "slides",
494
501
  "each": ".owl-item:not(.cloned)",
package/mapping/divi.json CHANGED
@@ -124,7 +124,8 @@
124
124
  "prevLabel": "ui:slider.prev",
125
125
  "nextLabel": "ui:slider.next",
126
126
  "slideLabel": "ui:slider.slide",
127
- "ofLabel": "ui:slider.of"
127
+ "ofLabel": "ui:slider.of",
128
+ "playLabel": "ui:embed.play"
128
129
  },
129
130
  "into": "slides",
130
131
  "each": ".et_pb_slide",
@@ -145,7 +146,8 @@
145
146
  "prevLabel": "ui:slider.prev",
146
147
  "nextLabel": "ui:slider.next",
147
148
  "slideLabel": "ui:slider.slide",
148
- "ofLabel": "ui:slider.of"
149
+ "ofLabel": "ui:slider.of",
150
+ "playLabel": "ui:embed.play"
149
151
  },
150
152
  "into": "slides",
151
153
  "each": ".et_pb_slide",
@@ -167,7 +169,8 @@
167
169
  "prevLabel": "ui:slider.prev",
168
170
  "nextLabel": "ui:slider.next",
169
171
  "slideLabel": "ui:slider.slide",
170
- "ofLabel": "ui:slider.of"
172
+ "ofLabel": "ui:slider.of",
173
+ "playLabel": "ui:embed.play"
171
174
  },
172
175
  "into": "slides",
173
176
  "item": {
@@ -174,7 +174,8 @@
174
174
  "prevLabel": "ui:slider.prev",
175
175
  "nextLabel": "ui:slider.next",
176
176
  "slideLabel": "ui:slider.slide",
177
- "ofLabel": "ui:slider.of"
177
+ "ofLabel": "ui:slider.of",
178
+ "playLabel": "ui:embed.play"
178
179
  },
179
180
  "into": "slides",
180
181
  "each": ".swiper-slide:not(.swiper-slide-duplicate)",
@@ -192,7 +193,8 @@
192
193
  "prevLabel": "ui:slider.prev",
193
194
  "nextLabel": "ui:slider.next",
194
195
  "slideLabel": "ui:slider.slide",
195
- "ofLabel": "ui:slider.of"
196
+ "ofLabel": "ui:slider.of",
197
+ "playLabel": "ui:embed.play"
196
198
  },
197
199
  "into": "slides",
198
200
  "each": ".swiper-slide:not(.swiper-slide-duplicate)",
@@ -209,7 +211,8 @@
209
211
  "prevLabel": "ui:slider.prev",
210
212
  "nextLabel": "ui:slider.next",
211
213
  "slideLabel": "ui:slider.slide",
212
- "ofLabel": "ui:slider.of"
214
+ "ofLabel": "ui:slider.of",
215
+ "playLabel": "ui:embed.play"
213
216
  },
214
217
  "into": "slides",
215
218
  "each": ".swiper-slide:not(.swiper-slide-duplicate)",
@@ -287,7 +290,8 @@
287
290
  "prevLabel": "ui:slider.prev",
288
291
  "nextLabel": "ui:slider.next",
289
292
  "slideLabel": "ui:slider.slide",
290
- "ofLabel": "ui:slider.of"
293
+ "ofLabel": "ui:slider.of",
294
+ "playLabel": "ui:embed.play"
291
295
  },
292
296
  "into": "slides",
293
297
  "each": ".swiper-slide:not(.swiper-slide-duplicate)",
@@ -306,7 +310,8 @@
306
310
  "prevLabel": "ui:slider.prev",
307
311
  "nextLabel": "ui:slider.next",
308
312
  "slideLabel": "ui:slider.slide",
309
- "ofLabel": "ui:slider.of"
313
+ "ofLabel": "ui:slider.of",
314
+ "playLabel": "ui:embed.play"
310
315
  },
311
316
  "into": "slides",
312
317
  "each": ".slider-wrapper[data-slider-options] > *",
@@ -326,7 +331,8 @@
326
331
  "prevLabel": "ui:slider.prev",
327
332
  "nextLabel": "ui:slider.next",
328
333
  "slideLabel": "ui:slider.slide",
329
- "ofLabel": "ui:slider.of"
334
+ "ofLabel": "ui:slider.of",
335
+ "playLabel": "ui:embed.play"
330
336
  },
331
337
  "into": "slides",
332
338
  "item": {
@@ -1172,7 +1178,8 @@
1172
1178
  "prevLabel": "ui:slider.prev",
1173
1179
  "nextLabel": "ui:slider.next",
1174
1180
  "slideLabel": "ui:slider.slide",
1175
- "ofLabel": "ui:slider.of"
1181
+ "ofLabel": "ui:slider.of",
1182
+ "playLabel": "ui:embed.play"
1176
1183
  },
1177
1184
  "into": "slides",
1178
1185
  "each": "@slides .swiper-slide:not(.swiper-slide-duplicate)",
@@ -219,7 +219,8 @@
219
219
  "prevLabel": "ui:slider.prev",
220
220
  "nextLabel": "ui:slider.next",
221
221
  "slideLabel": "ui:slider.slide",
222
- "ofLabel": "ui:slider.of"
222
+ "ofLabel": "ui:slider.of",
223
+ "playLabel": "ui:embed.play"
223
224
  },
224
225
  "into": "slides",
225
226
  "each": "figure.wp-block-image",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.39.0",
3
+ "version": "0.41.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",