@contentrain/astro-kit 0.40.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.
- package/README.md +16 -0
- package/catalog.json +76 -13
- package/components/_shared/Section.astro +13 -9
- package/components/_shared/SectionHeader.astro +3 -2
- package/components/_shared/instance-id.ts +24 -0
- package/components/_shared/muted.ts +19 -0
- package/components/band/Band.astro +10 -7
- package/components/band/meta.json +2 -0
- package/components/behavior/Behavior.astro +7 -2
- package/components/behavior/fixtures.json +2 -2
- package/components/behavior/meta.json +7 -0
- package/components/card-grid/CardGrid.astro +13 -9
- package/components/card-grid/meta.json +7 -0
- package/components/contact-details/ContactDetails.astro +7 -3
- package/components/contact-details/meta.json +2 -0
- package/components/contact-form/ContactForm.astro +11 -7
- package/components/contact-form/meta.json +2 -0
- package/components/cta/Cta.astro +14 -10
- package/components/cta/meta.json +2 -2
- package/components/dialog/Dialog.astro +6 -3
- package/components/dialog/meta.json +2 -0
- package/components/faq/Faq.astro +8 -4
- package/components/faq/meta.json +1 -1
- package/components/feature-list/FeatureList.astro +10 -6
- package/components/feature-list/meta.json +3 -1
- package/components/footer/Footer.astro +4 -3
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +8 -4
- package/components/gallery/meta.json +2 -0
- package/components/header/Header.astro +8 -4
- package/components/header/meta.json +1 -1
- package/components/hero/Hero.astro +13 -9
- package/components/hero/meta.json +3 -1
- package/components/language-switcher/LanguageSwitcher.astro +3 -2
- package/components/language-switcher/meta.json +1 -1
- package/components/logo-cloud/LogoCloud.astro +9 -5
- package/components/logo-cloud/meta.json +3 -1
- package/components/nav/Nav.astro +10 -6
- package/components/nav/meta.json +2 -0
- package/components/newsletter/Newsletter.astro +9 -5
- package/components/newsletter/meta.json +2 -0
- package/components/post-list/PostList.astro +7 -3
- package/components/post-list/meta.json +2 -0
- package/components/pricing/Pricing.astro +15 -11
- package/components/pricing/meta.json +3 -1
- package/components/search-box/SearchBox.astro +4 -1
- package/components/search-box/meta.json +1 -1
- package/components/slider/Slider.astro +4 -1
- package/components/slider/SliderTrack.astro +11 -5
- package/components/slider/meta.json +1 -1
- package/components/social-links/SocialLinks.astro +2 -1
- package/components/social-links/meta.json +3 -2
- package/components/split/Split.astro +9 -5
- package/components/split/meta.json +3 -1
- package/components/stats/Stats.astro +11 -7
- package/components/stats/meta.json +3 -1
- package/components/steps/Steps.astro +9 -5
- package/components/steps/meta.json +3 -1
- package/components/tabs/Tabs.astro +8 -4
- package/components/tabs/meta.json +1 -1
- package/components/team/Team.astro +10 -6
- package/components/team/meta.json +4 -2
- package/components/testimonial/Testimonial.astro +9 -5
- package/components/testimonial/fixtures.json +1 -1
- package/components/testimonial/meta.json +3 -1
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ import { safeSrc } from '../_shared/safe-url'
|
|
|
14
14
|
import { onVideoHost, videoTarget, type EmbedTarget } from '../_shared/embed-target'
|
|
15
15
|
import { measuredSlides } from '../_shared/measured'
|
|
16
16
|
import { type ActionInput, type ImageInput, type MeasuredStyle } from '../_shared/types'
|
|
17
|
+
import { instanceId } from '../_shared/instance-id'
|
|
18
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
17
19
|
|
|
18
20
|
const slider = tv({
|
|
19
21
|
slots: {
|
|
@@ -23,7 +25,7 @@ const slider = tv({
|
|
|
23
25
|
image: 'w-full object-cover',
|
|
24
26
|
caption: 'flex flex-col items-start gap-2',
|
|
25
27
|
// A slide with no image: its text as a card the height of the row, never laid over an image that is not there.
|
|
26
|
-
card:
|
|
28
|
+
card: `flex h-full flex-col items-start gap-2 rounded-[var(--radius-card)] bg-surface p-6 text-ink ring-1 ring-line/60 ${mutedReset}`,
|
|
27
29
|
},
|
|
28
30
|
variants: {
|
|
29
31
|
perView: {
|
|
@@ -39,7 +41,7 @@ const slider = tv({
|
|
|
39
41
|
caption: {
|
|
40
42
|
below: { image: 'rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4' },
|
|
41
43
|
// Over the image on wide screens; under it on phones, where the image is too short to hold text.
|
|
42
|
-
overlay: { image: 'rounded-[var(--radius-image,var(--radius-card))] md:rounded-none', slide: 'md:rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4 md:absolute md:inset-x-0 md:bottom-0 md:bg-gradient-to-t md:from-ink/80 md:to-transparent md:p-6 md:pt-16 md:text-surface' },
|
|
44
|
+
overlay: { image: 'rounded-[var(--radius-image,var(--radius-card))] md:rounded-none', slide: 'md:rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4 md:absolute md:inset-x-0 md:bottom-0 md:bg-gradient-to-t md:from-ink/80 md:to-transparent md:p-6 md:pt-16 md:text-surface md:[--kit-muted:var(--color-ink-muted-inverse,var(--color-surface))]' },
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
defaultVariants: { perView: '1', aspect: 'wide', caption: 'overlay' },
|
|
@@ -99,7 +101,9 @@ interface Props {
|
|
|
99
101
|
framed?: boolean | undefined
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
const { slides: given, label, prevLabel = 'Previous slide', nextLabel = 'Next slide', slideLabel = 'Slide', ofLabel = 'of', playLabel = 'Play', loop = false, perView = '1', aspect, caption, heading, intro, eyebrow, actions, measured, id
|
|
104
|
+
const { slides: given, label, prevLabel = 'Previous slide', nextLabel = 'Next slide', slideLabel = 'Slide', ofLabel = 'of', playLabel = 'Play', loop = false, perView = '1', aspect, caption, heading, intro, eyebrow, actions, measured, id: givenId, framed = true } = Astro.props
|
|
105
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
106
|
+
const id = instanceId(Astro.locals, givenId, 'slider')
|
|
103
107
|
// A slide is drawn when it has something to show: an image, a video (a file or another site's), or a text card (a title, a text or a link) where
|
|
104
108
|
// the source carousel moved quotes or testimonials. A slide with none of them is dropped.
|
|
105
109
|
const slides = given.flatMap((slide) => {
|
|
@@ -120,6 +124,8 @@ const below = slider({ perView: measuredRow ? '1' : perView, aspect, caption: 'b
|
|
|
120
124
|
const trackId = `${id}-track`
|
|
121
125
|
const sizes = measuredRow ? '(min-width: 1216px) 1152px, 100vw' : perView === '1' ? '(min-width: 1216px) 1152px, 100vw' : perView === '2' ? '(min-width: 640px) 50vw, 85vw' : '(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 85vw'
|
|
122
126
|
// Arrows from sm up; on phones the carousel is swiped and the dots show the position.
|
|
127
|
+
// Framed, the carousel is its own section: the same hooks as every other (`id`, `data-kit-section`), its padding its own,
|
|
128
|
+
// so a theme's section spacing leaves it as it is.
|
|
123
129
|
const Root = framed ? 'section' : 'div'
|
|
124
130
|
const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-center justify-center rounded-full bg-surface/90 text-ink shadow-md ring-1 ring-line hover:bg-surface disabled:opacity-40 sm:group-data-[ready]:flex'
|
|
125
131
|
// A text card has no picture for the arrows to sit on: over it they would cover its words. A carousel with any text
|
|
@@ -129,7 +135,7 @@ const cards = slides.some(slide => !slide.image && !slide.video && !slide.player
|
|
|
129
135
|
// ratio, so the player that replaces the link moves nothing.
|
|
130
136
|
const under = 'flex size-11 shrink-0 items-center justify-center rounded-full bg-surface text-ink ring-1 ring-line hover:bg-surface disabled:opacity-40'
|
|
131
137
|
---
|
|
132
|
-
<Root class:list={['group relative', framed && 'container-page py-8']} role={framed ? undefined : 'region'} aria-roledescription="carousel" aria-label={label} data-kit-slider data-loop={loop ? '' : undefined} data-slide-label={slideLabel}>
|
|
138
|
+
<Root id={framed ? id : undefined} class:list={['group relative', framed && 'container-page py-8']} data-kit-section={framed ? 'slider' : undefined} data-kit-spacing={framed ? 'none' : undefined} role={framed ? undefined : 'region'} aria-roledescription="carousel" aria-label={label} data-kit-slider data-loop={loop ? '' : undefined} data-slide-label={slideLabel}>
|
|
133
139
|
<SectionHeader heading={heading} intro={intro} eyebrow={eyebrow} actions={actions} />
|
|
134
140
|
<div class={slots.viewport()} data-viewport tabindex="0" role="group" aria-label={label}>
|
|
135
141
|
<div class:list={['flex gap-4 [&>*]:shrink-0 group-data-[ready]:[&>*]:snap-align-none', measuredRow?.track]} id={trackId} data-container>
|
|
@@ -152,7 +158,7 @@ const under = 'flex size-11 shrink-0 items-center justify-center rounded-full bg
|
|
|
152
158
|
{(slide.title || slide.text || slide.action) && (
|
|
153
159
|
<div class={slide.video || slide.player ? below.caption() : slide.image ? slots.caption() : slots.card()}>
|
|
154
160
|
{slide.title && <p class="text-xl [font-weight:var(--font-weight-heading,600)] text-balance">{slide.title}</p>}
|
|
155
|
-
{slide.text && <p class=
|
|
161
|
+
{slide.text && <p class={muted}>{slide.text}</p>}
|
|
156
162
|
{slide.action && <Button action={slide.action} size="sm" class="mt-2" />}
|
|
157
163
|
</div>
|
|
158
164
|
)}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "A carousel of images, videos (a file, or a YouTube/Vimeo link loaded only on click) or text cards, with optional title, text and button per slide. Without JavaScript it is a snapping horizontal scroller; Embla adds paging, arrows and dots.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["Button.astro", "KitImage.astro", "Section.astro", "SectionHeader.astro", "embed-frame.ts", "embed-target.ts", "image.ts", "measured.ts", "safe-url.ts", "types.ts"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "Section.astro", "SectionHeader.astro", "embed-frame.ts", "embed-target.ts", "image.ts", "instance-id.ts", "measured.ts", "muted.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"perView": { "options": ["1", "2", "3"], "default": "1", "description": "Slides visible at once on wide screens." },
|
|
10
10
|
"aspect": { "options": ["wide", "standard", "square"], "default": "wide", "description": "Image aspect ratio: 21:9, 3:2 or 1:1." },
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
6
|
import { addressLabel, NETWORK_MARKS, networkOf } from '../_shared/social'
|
|
7
7
|
import { safeHref } from '../_shared/safe-url'
|
|
8
|
+
import { muted } from '../_shared/muted'
|
|
8
9
|
|
|
9
10
|
const social = tv({
|
|
10
11
|
slots: {
|
|
11
12
|
list: 'flex flex-wrap items-center',
|
|
12
|
-
link:
|
|
13
|
+
link: `inline-flex items-center justify-center gap-2 no-underline ${muted} hover:text-current focus-visible:text-current`,
|
|
13
14
|
mark: 'shrink-0',
|
|
14
15
|
},
|
|
15
16
|
variants: {
|
|
@@ -9,13 +9,15 @@ import Embed from '../embed/Embed.astro'
|
|
|
9
9
|
import FeatureList from '../feature-list/FeatureList.astro'
|
|
10
10
|
import KitImage from '../_shared/KitImage.astro'
|
|
11
11
|
import Section from '../_shared/Section.astro'
|
|
12
|
+
import { instanceId } from '../_shared/instance-id'
|
|
12
13
|
import type { ActionInput, ImageInput, MeasuredStyle } from '../_shared/types'
|
|
14
|
+
import { muted } from '../_shared/muted'
|
|
13
15
|
|
|
14
16
|
const split = tv({
|
|
15
17
|
slots: {
|
|
16
18
|
grid: 'grid items-center gap-10 md:gap-16',
|
|
17
19
|
copy: 'flex flex-col gap-5',
|
|
18
|
-
eyebrow:
|
|
20
|
+
eyebrow: `text-sm font-semibold tracking-wide uppercase ${muted}`,
|
|
19
21
|
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
20
22
|
body: 'prose prose-site max-w-none text-pretty',
|
|
21
23
|
actions: 'flex flex-wrap gap-3 pt-2',
|
|
@@ -42,7 +44,7 @@ const split = tv({
|
|
|
42
44
|
sticky: { true: { media: 'md:sticky md:top-8 md:self-start' }, false: {} },
|
|
43
45
|
align: { start: {}, center: { copy: 'items-center text-center', actions: 'justify-center' } },
|
|
44
46
|
// accent/accent-ink is only ~5:1 — dimming text on it drops body copy under 4.5:1 (WCAG 1.4.3), so accent keeps it at full strength.
|
|
45
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: {
|
|
47
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: {}, brand: {}, none: {} },
|
|
46
48
|
},
|
|
47
49
|
defaultVariants: { imageSide: 'end', ratio: '1:1', imageFit: 'contain', imageShape: 'rounded', imageBleed: 'none', stackOrder: 'text-first', sticky: false, align: 'start', tone: 'default' },
|
|
48
50
|
})
|
|
@@ -98,16 +100,18 @@ interface Props {
|
|
|
98
100
|
id?: string | undefined
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
const { heading, headingLevel = 2, eyebrow, body, actions = [], image, features = [], videoUrl, playLabel, imageSide, ratio = '1:1', imageFit, imageShape, imageBleed, mediaKind, featureStyle = 'check', stackOrder, sticky = false, align, tone, measured, id } = Astro.props
|
|
103
|
+
const { heading, headingLevel = 2, eyebrow, body, actions = [], image, features = [], videoUrl, playLabel, imageSide, ratio = '1:1', imageFit, imageShape, imageBleed, mediaKind, featureStyle = 'check', stackOrder, sticky = false, align, tone, measured, id: givenId } = Astro.props
|
|
104
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `split`, `split-2`, … in render order.
|
|
105
|
+
const id = instanceId(Astro.locals, givenId, 'split')
|
|
102
106
|
const s = split({ imageSide, ratio, imageFit, imageShape, imageBleed, stackOrder, sticky, align, tone })
|
|
103
107
|
const Heading = `h${headingLevel}` as 'h2'
|
|
104
108
|
const columns = COLUMNS[imageSide === 'start' ? 'image' : 'text'][ratio]
|
|
105
109
|
const bleed = imageBleed === 'edge' ? BLEED[imageSide === 'start' ? 'start' : 'end'] : undefined
|
|
106
110
|
const video = mediaKind === 'video' && videoUrl
|
|
107
|
-
const headingId =
|
|
111
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
108
112
|
const html = await renderMarkdown(body)
|
|
109
113
|
---
|
|
110
|
-
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId} class={bleed ? 'overflow-x-clip' : undefined}>
|
|
114
|
+
<Section kit="split" measured={measured} tone={tone} id={id} aria-labelledby={headingId} class={bleed ? 'overflow-x-clip' : undefined}>
|
|
111
115
|
<div class:list={[s.grid(), columns]}>
|
|
112
116
|
<div class={s.copy()}>
|
|
113
117
|
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
@@ -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:
|
|
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:
|
|
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: {
|
|
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
|
|
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',
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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'
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
"
|
|
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:
|
|
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',
|
|
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
|
|
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.
|
|
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",
|