@contentrain/astro-kit 0.40.0 → 0.42.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 (71) hide show
  1. package/README.md +16 -0
  2. package/catalog.json +123 -15
  3. package/components/_shared/Lines.astro +12 -0
  4. package/components/_shared/Section.astro +13 -9
  5. package/components/_shared/SectionHeader.astro +3 -2
  6. package/components/_shared/bleed.ts +8 -0
  7. package/components/_shared/instance-id.ts +24 -0
  8. package/components/_shared/muted.ts +19 -0
  9. package/components/band/Band.astro +10 -7
  10. package/components/band/meta.json +2 -0
  11. package/components/behavior/Behavior.astro +7 -2
  12. package/components/behavior/fixtures.json +2 -2
  13. package/components/behavior/meta.json +7 -0
  14. package/components/card-grid/CardGrid.astro +84 -24
  15. package/components/card-grid/fixtures.json +4 -1
  16. package/components/card-grid/meta.json +21 -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/faq/Faq.astro +8 -4
  26. package/components/faq/meta.json +1 -1
  27. package/components/feature-list/FeatureList.astro +10 -6
  28. package/components/feature-list/meta.json +3 -1
  29. package/components/footer/Footer.astro +4 -3
  30. package/components/footer/meta.json +1 -0
  31. package/components/gallery/Gallery.astro +8 -4
  32. package/components/gallery/meta.json +2 -0
  33. package/components/header/Header.astro +8 -4
  34. package/components/header/meta.json +1 -1
  35. package/components/hero/Hero.astro +44 -15
  36. package/components/hero/fixtures.json +7 -1
  37. package/components/hero/meta.json +30 -1
  38. package/components/language-switcher/LanguageSwitcher.astro +3 -2
  39. package/components/language-switcher/meta.json +1 -1
  40. package/components/logo-cloud/LogoCloud.astro +9 -5
  41. package/components/logo-cloud/meta.json +3 -1
  42. package/components/nav/Nav.astro +10 -6
  43. package/components/nav/meta.json +2 -0
  44. package/components/newsletter/Newsletter.astro +9 -5
  45. package/components/newsletter/meta.json +2 -0
  46. package/components/post-list/PostList.astro +7 -3
  47. package/components/post-list/meta.json +2 -0
  48. package/components/pricing/Pricing.astro +15 -11
  49. package/components/pricing/meta.json +3 -1
  50. package/components/search-box/SearchBox.astro +4 -1
  51. package/components/search-box/meta.json +1 -1
  52. package/components/slider/Slider.astro +4 -1
  53. package/components/slider/SliderTrack.astro +11 -5
  54. package/components/slider/meta.json +1 -1
  55. package/components/social-links/SocialLinks.astro +2 -1
  56. package/components/social-links/meta.json +3 -2
  57. package/components/split/Split.astro +16 -11
  58. package/components/split/fixtures.json +4 -1
  59. package/components/split/meta.json +9 -3
  60. package/components/stats/Stats.astro +11 -7
  61. package/components/stats/meta.json +3 -1
  62. package/components/steps/Steps.astro +9 -5
  63. package/components/steps/meta.json +3 -1
  64. package/components/tabs/Tabs.astro +8 -4
  65. package/components/tabs/meta.json +1 -1
  66. package/components/team/Team.astro +10 -6
  67. package/components/team/meta.json +4 -2
  68. package/components/testimonial/Testimonial.astro +9 -5
  69. package/components/testimonial/fixtures.json +1 -1
  70. package/components/testimonial/meta.json +3 -1
  71. package/package.json +1 -1
@@ -11,7 +11,10 @@
11
11
  "Button.astro",
12
12
  "KitImage.astro",
13
13
  "Section.astro",
14
+ "bleed.ts",
15
+ "instance-id.ts",
14
16
  "measured.ts",
17
+ "muted.ts",
15
18
  "safe-url.ts",
16
19
  "types.ts"
17
20
  ],
@@ -45,10 +48,13 @@
45
48
  "options": [
46
49
  "none",
47
50
  "rounded",
48
- "circle"
51
+ "circle",
52
+ "arch",
53
+ "chamfer",
54
+ "blob"
49
55
  ],
50
56
  "default": "rounded",
51
- "description": "The image's corners: square, the theme's rounding, or a circle."
57
+ "description": "The image's outline: square, the theme's rounding, a circle, or a fixed mask: an arch over the image, a cut corner (chamfer), a soft blob."
52
58
  },
53
59
  "imageBleed": {
54
60
  "options": [
@@ -339,7 +345,7 @@
339
345
  "id": {
340
346
  "type": "string",
341
347
  "content": false,
342
- "description": "Anchor id of the section; the heading gets `<id>-title`."
348
+ "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
349
  },
344
350
  "sticky": {
345
351
  "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' ? (
@@ -12,7 +12,9 @@
12
12
  "embed-frame.ts",
13
13
  "embed-target.ts",
14
14
  "image.ts",
15
+ "instance-id.ts",
15
16
  "measured.ts",
17
+ "muted.ts",
16
18
  "safe-url.ts",
17
19
  "types.ts"
18
20
  ],
@@ -261,7 +263,7 @@
261
263
  "id": {
262
264
  "type": "string",
263
265
  "content": false,
264
- "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."
265
267
  }
266
268
  },
267
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.40.0",
3
+ "version": "0.42.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",