@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.
- package/README.md +16 -0
- package/catalog.json +123 -15
- package/components/_shared/Lines.astro +12 -0
- package/components/_shared/Section.astro +13 -9
- package/components/_shared/SectionHeader.astro +3 -2
- package/components/_shared/bleed.ts +8 -0
- 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 +84 -24
- package/components/card-grid/fixtures.json +4 -1
- package/components/card-grid/meta.json +21 -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 +44 -15
- package/components/hero/fixtures.json +7 -1
- package/components/hero/meta.json +30 -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 +16 -11
- package/components/split/fixtures.json +4 -1
- package/components/split/meta.json +9 -3
- 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
|
@@ -9,6 +9,8 @@ import Section, { type Tone } from '../_shared/Section.astro'
|
|
|
9
9
|
import { measuredGrid } from '../_shared/measured'
|
|
10
10
|
import type { ActionInput, ImageInput, MeasuredStyle } from '../_shared/types'
|
|
11
11
|
import { safeHref } from '../_shared/safe-url'
|
|
12
|
+
import { instanceId } from '../_shared/instance-id'
|
|
13
|
+
import { muted } from '../_shared/muted'
|
|
12
14
|
|
|
13
15
|
const band = tv({
|
|
14
16
|
slots: {
|
|
@@ -16,16 +18,16 @@ const band = tv({
|
|
|
16
18
|
copy: 'flex max-w-3xl flex-col gap-4',
|
|
17
19
|
eyebrow: 'text-sm font-semibold tracking-wide text-accent-text uppercase',
|
|
18
20
|
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
19
|
-
lead:
|
|
21
|
+
lead: `text-lg text-pretty ${muted}`,
|
|
20
22
|
media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
21
23
|
list: 'grid items-start gap-6 md:gap-8',
|
|
22
24
|
item: 'relative flex flex-col gap-3 rounded-[var(--radius-card)]',
|
|
23
25
|
itemMedia: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
24
26
|
itemTitle: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
|
|
25
27
|
itemValue: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight',
|
|
26
|
-
itemMeta:
|
|
27
|
-
itemText:
|
|
28
|
-
itemPoints:
|
|
28
|
+
itemMeta: `text-sm ${muted}`,
|
|
29
|
+
itemText: muted,
|
|
30
|
+
itemPoints: `flex list-disc flex-col gap-1 ps-5 ${muted}`,
|
|
29
31
|
actions: 'flex flex-wrap gap-3',
|
|
30
32
|
},
|
|
31
33
|
variants: {
|
|
@@ -42,7 +44,7 @@ const band = tv({
|
|
|
42
44
|
center: { copy: 'mx-auto items-center text-center', actions: 'justify-center', item: 'items-center text-center' },
|
|
43
45
|
},
|
|
44
46
|
// Accent backgrounds have the least contrast headroom: text on them is never dimmed.
|
|
45
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: {
|
|
47
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'text-current' } },
|
|
46
48
|
},
|
|
47
49
|
defaultVariants: { layout: 'stack', columns: '3', align: 'start', tone: 'default' },
|
|
48
50
|
})
|
|
@@ -89,10 +91,11 @@ const aligned = align ?? (measured?.align === 'center' ? 'center' : undefined)
|
|
|
89
91
|
const s = band({ layout: media ? layout : 'stack', columns, align: aligned, tone })
|
|
90
92
|
const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
91
93
|
const ItemHeading = `h${heading ? headingLevel + 1 : headingLevel}` as 'h2' | 'h3' | 'h4'
|
|
92
|
-
|
|
94
|
+
// Two bands with the same heading on one page get two ids (`band-<heading>`, then `band-<heading>-2`).
|
|
95
|
+
const headingId = heading ? (id ? `${id}-title` : instanceId(Astro.locals, undefined, `band-${heading.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`)) : undefined
|
|
93
96
|
const hasCopy = Boolean(heading || eyebrow || lead || actions.length)
|
|
94
97
|
---
|
|
95
|
-
<Section tone={tone} id={id} aria-labelledby={headingId} measured={measured}>
|
|
98
|
+
<Section kit="band" tone={tone} id={id} aria-labelledby={headingId} measured={measured}>
|
|
96
99
|
<div class="flex flex-col gap-10">
|
|
97
100
|
{(hasCopy || media) && (
|
|
98
101
|
<div class={s.grid()}>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// nothing hidden. Only a page that has an island loads the module (Astro bundles a component's script once, for the
|
|
10
10
|
// pages that use it).
|
|
11
11
|
import Section from '../_shared/Section.astro'
|
|
12
|
+
import { instanceId } from '../_shared/instance-id'
|
|
12
13
|
import type { MeasuredStyle } from '../_shared/types'
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -29,9 +30,13 @@ interface Props {
|
|
|
29
30
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
30
31
|
/** The source section's measured style (`MeasuredStyle`). */
|
|
31
32
|
measured?: MeasuredStyle | undefined
|
|
33
|
+
/** Section id, unique per page; without one `behavior`, `behavior-2`, … in render order. */
|
|
34
|
+
id?: string | undefined
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const { kind, html, items, title, panel, fact, previousLabel, nextLabel, closeLabel, tone, measured } = Astro.props
|
|
37
|
+
const { kind, html, items, title, panel, fact, previousLabel, nextLabel, closeLabel, tone, measured, id: givenId } = Astro.props
|
|
38
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `behavior`, `behavior-2`, … in render order.
|
|
39
|
+
const id = instanceId(Astro.locals, givenId, 'behavior')
|
|
35
40
|
// Selectors and ids go into attributes only, never into markup or a script; an empty one is left out.
|
|
36
41
|
const attr = (value: string | undefined): string | undefined => (value && value.length <= 300 ? value : undefined)
|
|
37
42
|
const factId = fact && /^[\w:.-]{1,120}$/.test(fact) ? fact : undefined
|
|
@@ -39,7 +44,7 @@ const factId = fact && /^[\w:.-]{1,120}$/.test(fact) ? fact : undefined
|
|
|
39
44
|
const NOSCRIPT = '<style>.cr-behavior dialog:not([open]){display:block;position:static;margin:0;max-width:none}</style>'
|
|
40
45
|
---
|
|
41
46
|
{html && (
|
|
42
|
-
<Section tone={tone} measured={measured}>
|
|
47
|
+
<Section kit="behavior" tone={tone} id={id} measured={measured}>
|
|
43
48
|
<div
|
|
44
49
|
class="cr-behavior"
|
|
45
50
|
data-cr-behavior={kind}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"kind": "disclosure",
|
|
40
40
|
"fact": "interactive:fx3",
|
|
41
41
|
"tone": "inverse",
|
|
42
|
-
"html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-
|
|
42
|
+
"html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx5-a\">Do you ship abroad?</button></h2><div id=\"cr-fx5-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"kind": "disclosure",
|
|
49
49
|
"fact": "interactive:fx3",
|
|
50
50
|
"tone": "accent",
|
|
51
|
-
"html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-
|
|
51
|
+
"html": "<h2><button aria-expanded=\"false\" aria-controls=\"cr-fx6-a\">Do you ship abroad?</button></h2><div id=\"cr-fx6-a\"><p>Yes, to the whole of Europe.</p></div><details><summary>Can I return an order?</summary><p>Within 30 days.</p></details>"
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
{"name": "video-link", "props": {"kind": "anchor", "fact": "interactive:fx5", "html": "<h2>Studio tour</h2><p>A walk through the workshop.</p><a href=\"https://www.youtube.com/watch?v=aqz-KE-bpKQ\" target=\"_blank\" rel=\"noopener noreferrer\" data-cr-video=\"youtube:aqz-KE-bpKQ\"><img src=\"/kit-fixtures/photo-2.svg\" alt=\"\" loading=\"lazy\" width=\"1200\" height=\"800\"><span>Studio tour</span></a>"}},
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"behaviors.ts",
|
|
10
10
|
"embed-frame.ts",
|
|
11
11
|
"embed-target.ts",
|
|
12
|
+
"instance-id.ts",
|
|
12
13
|
"measured.ts",
|
|
14
|
+
"muted.ts",
|
|
13
15
|
"safe-url.ts",
|
|
14
16
|
"types.ts"
|
|
15
17
|
],
|
|
@@ -195,6 +197,11 @@
|
|
|
195
197
|
"description": "The section's minimum height."
|
|
196
198
|
}
|
|
197
199
|
}
|
|
200
|
+
},
|
|
201
|
+
"id": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"content": false,
|
|
204
|
+
"description": "Anchor id of the section. Without one, `behavior`, `behavior-2`, … in render order, so every section on a page has its own."
|
|
198
205
|
}
|
|
199
206
|
},
|
|
200
207
|
"slots": {},
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
import { tv } from 'tailwind-variants'
|
|
5
5
|
import KitImage from '../_shared/KitImage.astro'
|
|
6
6
|
import Section, { type Tone } 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 { safeHref } from '../_shared/safe-url'
|
|
12
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
11
13
|
|
|
12
14
|
const grid = tv({
|
|
13
15
|
slots: {
|
|
@@ -15,7 +17,7 @@ const grid = tv({
|
|
|
15
17
|
card: 'group relative flex flex-col gap-3 rounded-[var(--radius-card)]',
|
|
16
18
|
media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
17
19
|
title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
|
|
18
|
-
text:
|
|
20
|
+
text: muted,
|
|
19
21
|
},
|
|
20
22
|
variants: {
|
|
21
23
|
columns: {
|
|
@@ -26,13 +28,36 @@ const grid = tv({
|
|
|
26
28
|
style: {
|
|
27
29
|
plain: {},
|
|
28
30
|
bordered: { card: 'border border-line p-6', media: '-mx-6 -mt-6 mb-2 rounded-b-none' },
|
|
29
|
-
elevated: { card:
|
|
31
|
+
elevated: { card: `bg-surface p-6 text-ink shadow-md ring-1 ring-line/60 ${mutedReset}`, media: '-mx-6 -mt-6 mb-2 rounded-b-none' },
|
|
30
32
|
},
|
|
31
33
|
align: { start: { card: 'text-start' }, center: { card: 'items-center text-center' } },
|
|
32
34
|
},
|
|
33
35
|
defaultVariants: { columns: '3', style: 'plain', align: 'start' },
|
|
34
36
|
})
|
|
35
37
|
|
|
38
|
+
// `layout: 'rows'`: one item per row, its title, text and link on one axis (an image sits at the row's top, out of the
|
|
39
|
+
// baseline: a head led by an icon too, else the icon's foot would be the baseline and push the text down by its
|
|
40
|
+
// height), a hairline between rows and no card box.
|
|
41
|
+
// With `index`, a numbered list whose numbers (01, 02, …) are drawn in the muted role, tabular; the list itself
|
|
42
|
+
// carries the order for screen readers, so the drawn number is hidden from them.
|
|
43
|
+
const rows = tv({
|
|
44
|
+
slots: {
|
|
45
|
+
list: 'flex flex-col divide-y divide-line border-y border-line',
|
|
46
|
+
row: 'group relative grid gap-x-8 gap-y-2 py-6 md:items-baseline',
|
|
47
|
+
number: `text-sm tabular-nums ${muted}`,
|
|
48
|
+
head: 'flex flex-col gap-2',
|
|
49
|
+
end: 'flex flex-col gap-3 md:items-end',
|
|
50
|
+
media: 'overflow-hidden rounded-[var(--radius-image,var(--radius-card))] md:w-48',
|
|
51
|
+
title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
|
|
52
|
+
text: muted,
|
|
53
|
+
link: 'font-medium text-accent-text underline-offset-4 hover:underline',
|
|
54
|
+
},
|
|
55
|
+
variants: {
|
|
56
|
+
index: { true: { row: 'md:grid-cols-[3rem_minmax(0,1fr)_minmax(0,2fr)_auto]' }, false: { row: 'md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto]' } },
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: { index: false },
|
|
59
|
+
})
|
|
60
|
+
|
|
36
61
|
interface Item {
|
|
37
62
|
title: string
|
|
38
63
|
text?: string | undefined
|
|
@@ -54,43 +79,78 @@ interface Props {
|
|
|
54
79
|
/** Heading level of the section title; card titles are one level below, or at this level when there is no title. */
|
|
55
80
|
headingLevel?: 2 | 3 | undefined
|
|
56
81
|
columns?: '2' | '3' | '4' | undefined
|
|
82
|
+
/** `rows`: one item per row (title, text and link on one axis, a hairline between rows, no card box); `columns`, `style` and the measured columns do not apply. `grid` (absent): cards, as before. */
|
|
83
|
+
layout?: 'grid' | 'rows' | undefined
|
|
84
|
+
/** With `layout: 'rows'`: a numbered list, each row led by its number (01, 02, …) in the muted role. */
|
|
85
|
+
index?: boolean | undefined
|
|
57
86
|
style?: 'plain' | 'bordered' | 'elevated' | undefined
|
|
58
87
|
align?: 'start' | 'center' | undefined
|
|
59
88
|
tone?: Tone | undefined
|
|
60
89
|
/** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
|
|
61
90
|
measured?: MeasuredStyle | undefined
|
|
91
|
+
/** Section id, unique per page; without one `card-grid`, `card-grid-2`, … in render order. Its title is `<id>-title`. */
|
|
92
|
+
id?: string | undefined
|
|
62
93
|
}
|
|
63
94
|
|
|
64
|
-
const { title, intro, items, headingLevel = 2, columns, style, align, tone, measured } = Astro.props
|
|
95
|
+
const { title, intro, items, headingLevel = 2, columns, style, align, tone, measured, id: givenId, layout, index = false } = Astro.props
|
|
96
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `card-grid`, `card-grid-2`, … in render order.
|
|
97
|
+
const id = instanceId(Astro.locals, givenId, 'card-grid')
|
|
65
98
|
const { list, card, media, title: cardTitle, text } = grid({ columns, style, align })
|
|
66
|
-
// Accent backgrounds have the least contrast headroom: text on them is never dimmed.
|
|
67
|
-
const dim = tone === 'accent' ? 'opacity-100' : undefined
|
|
68
99
|
const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
69
100
|
// One level under the section title, or the title's level when there is none.
|
|
70
101
|
const CardHeading = `h${title ? headingLevel + 1 : headingLevel}` as 'h2' | 'h3' | 'h4'
|
|
71
|
-
const headingId = title ?
|
|
102
|
+
const headingId = title ? `${id}-title` : undefined
|
|
103
|
+
const r = rows({ index })
|
|
104
|
+
const List = index ? 'ol' : 'ul'
|
|
105
|
+
const number = (i: number) => String(i + 1).padStart(2, '0')
|
|
72
106
|
---
|
|
73
|
-
<Section measured={measured} tone={tone} aria-labelledby={headingId}>
|
|
107
|
+
<Section kit="card-grid" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
74
108
|
{(title || intro) && (
|
|
75
109
|
<header class:list={['mb-10 flex max-w-2xl flex-col gap-3', align === 'center' && 'mx-auto items-center text-center']}>
|
|
76
110
|
{title && <Heading id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{title}</Heading>}
|
|
77
|
-
{intro && <p class:list={['text-lg',
|
|
111
|
+
{intro && <p class:list={['text-lg', muted]}>{intro}</p>}
|
|
78
112
|
</header>
|
|
79
113
|
)}
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
{layout === 'rows' ? (
|
|
115
|
+
<List class={r.list()} role="list">
|
|
116
|
+
{items.map((item, i) => (
|
|
117
|
+
<li class={r.row()}>
|
|
118
|
+
{index && <span class={r.number()} aria-hidden="true">{number(i)}</span>}
|
|
119
|
+
<div class={r.head({ class: item.icon ? 'md:self-start' : undefined })}>
|
|
120
|
+
{item.icon && <KitImage image={{ src: item.icon, alt: '' }} class="h-8 w-8 object-contain" />}
|
|
121
|
+
{item.eyebrow && <p class="text-sm font-medium text-accent-text">{item.eyebrow}</p>}
|
|
122
|
+
<CardHeading class={r.title()}>
|
|
123
|
+
{item.href && !item.linkLabel
|
|
124
|
+
? <a href={safeHref(item.href)} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent-text">{item.title}</a>
|
|
125
|
+
: item.title}
|
|
126
|
+
</CardHeading>
|
|
127
|
+
</div>
|
|
128
|
+
{item.text ? <p class={r.text()} data-kit-text>{item.text}</p> : <span aria-hidden="true" />}
|
|
129
|
+
{(imageOf(item.image, item.imageAlt) || (item.href && item.linkLabel)) && (
|
|
130
|
+
<div class={r.end({ class: imageOf(item.image, item.imageAlt) ? 'md:self-start' : undefined })}>
|
|
131
|
+
{imageOf(item.image, item.imageAlt) && <div class={r.media()}><KitImage image={imageOf(item.image, item.imageAlt)!} sizes="(min-width: 768px) 12rem, 100vw" class="aspect-[3/2] w-full object-cover" /></div>}
|
|
132
|
+
{item.href && item.linkLabel && <a href={safeHref(item.href)} class={r.link()}>{item.linkLabel}</a>}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</li>
|
|
136
|
+
))}
|
|
137
|
+
</List>
|
|
138
|
+
) : (
|
|
139
|
+
<ul class={list({ class: measuredGrid(measured) })}>
|
|
140
|
+
{items.map(item => (
|
|
141
|
+
<li class={card()}>
|
|
142
|
+
{imageOf(item.image, item.imageAlt) && <div class={media()}><KitImage image={imageOf(item.image, item.imageAlt)!} sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw" class="aspect-[3/2] w-full object-cover transition duration-300 group-hover:scale-[1.02]" /></div>}
|
|
143
|
+
{item.icon && <KitImage image={{ src: item.icon, alt: '' }} class="h-10 w-10 object-contain" />}
|
|
144
|
+
{item.eyebrow && <p class="text-sm font-medium text-accent-text">{item.eyebrow}</p>}
|
|
145
|
+
<CardHeading class={cardTitle()}>
|
|
146
|
+
{item.href && !item.linkLabel
|
|
147
|
+
? <a href={safeHref(item.href)} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent-text">{item.title}</a>
|
|
148
|
+
: item.title}
|
|
149
|
+
</CardHeading>
|
|
150
|
+
{item.text && <p class={text()} data-kit-text>{item.text}</p>}
|
|
151
|
+
{item.href && item.linkLabel && <a href={safeHref(item.href)} class="font-medium text-accent-text underline-offset-4 hover:underline">{item.linkLabel}</a>}
|
|
152
|
+
</li>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
)}
|
|
96
156
|
</Section>
|
|
@@ -5,5 +5,8 @@
|
|
|
5
5
|
{ "name": "accent", "props": { "tone": "accent", "columns": "3", "items": [{ "title": "Fast", "text": "Static HTML, one stylesheet." }, { "title": "Editable", "text": "Content in Contentrain Studio." }, { "title": "Kept", "text": "Every address survives." }] } },
|
|
6
6
|
{"name": "measured-columns", "props": {"title": "Services", "measured": {"containerPx": 1140, "padY": 80, "columns": {"390": 1, "768": 2, "1280": 4}, "gap": 30, "radius": 4, "bg": {"color": "#f4f1ea"}}, "items": [{"title": "Audits", "text": "Where a site loses readers."}, {"title": "Builds", "text": "Static and fast."}, {"title": "Hosting", "text": "Managed for you."}, {"title": "Care", "text": "Kept up to date."}]}},
|
|
7
7
|
{"name": "icon-boxes", "props": {"columns": "3", "align": "center", "items": [{"title": "Design", "text": "Layouts built around your products.", "icon": "/kit-fixtures/icon-1.svg"}, {"title": "Build", "text": "Fast pages that stay fast.", "icon": "/kit-fixtures/icon-1.svg"}, {"title": "Grow", "text": "Monthly reports you can read in five minutes.", "icon": "/kit-fixtures/icon-1.svg"}]}},
|
|
8
|
-
{"name": "link-labels", "props": {"columns": "4", "align": "center", "items": [{"title": "Kasko", "href": "/urunlerimiz/kasko/", "linkLabel": "Teklif Al"}, {"title": "Trafik Sigortası", "href": "/urunlerimiz/trafik-sigortasi/", "linkLabel": "Teklif Al"}, {"title": "Konut Sigortası", "href": "/urunlerimiz/konut-sigortasi/", "linkLabel": "Teklif Al"}, {"title": "Tamamlayıcı Sağlık"}]}}
|
|
8
|
+
{"name": "link-labels", "props": {"columns": "4", "align": "center", "items": [{"title": "Kasko", "href": "/urunlerimiz/kasko/", "linkLabel": "Teklif Al"}, {"title": "Trafik Sigortası", "href": "/urunlerimiz/trafik-sigortasi/", "linkLabel": "Teklif Al"}, {"title": "Konut Sigortası", "href": "/urunlerimiz/konut-sigortasi/", "linkLabel": "Teklif Al"}, {"title": "Tamamlayıcı Sağlık"}]}},
|
|
9
|
+
{"name": "rows-index", "props": {"layout": "rows", "index": true, "title": "How it works", "intro": "Three steps, in this order.", "items": [{"title": "Discovery", "text": "We read every page of the site you have, and write down what it holds.", "href": "/how/discovery/"}, {"title": "An offer", "text": "One price for the whole move, with what it includes and what it leaves out.", "href": "/how/offer/"}, {"title": "The move", "text": "Content, addresses and forms carried over, each one checked against the original.", "href": "/how/move/"}]}},
|
|
10
|
+
{"name": "rows-links-muted", "props": {"layout": "rows", "tone": "muted", "title": "What we do", "items": [{"eyebrow": "Studio", "title": "Design systems", "text": "Tokens, components and documentation your team keeps using.", "href": "/services/design/", "linkLabel": "Read more", "image": {"src": "/kit-fixtures/photo-1.svg", "alt": "", "width": 1200, "height": 800}}, {"eyebrow": "Studio", "title": "Migrations", "text": "WordPress to Astro without losing an address or a ranking.", "href": "/services/migrations/", "linkLabel": "Read more", "image": {"src": "/kit-fixtures/photo-2.svg", "alt": "", "width": 1200, "height": 800}}, {"eyebrow": "Studio", "title": "Performance", "href": "/services/performance/", "linkLabel": "Read more"}]}},
|
|
11
|
+
{"name": "rows-icons", "props": {"layout": "rows", "title": "What the move keeps", "items": [{"icon": "/kit-fixtures/icon-1.svg", "title": "Addresses", "text": "Every old address answers, or sends the visitor to its new one.", "href": "/keeps/addresses/"}, {"icon": "/kit-fixtures/icon-1.svg", "eyebrow": "Forms", "title": "Contact forms", "text": "Each form posts where it did, with the same fields.", "href": "/keeps/forms/", "linkLabel": "How"}, {"title": "Rankings", "text": "Titles, descriptions and structured data carried over as they were."}]}}
|
|
9
12
|
]
|
|
@@ -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
|
],
|
|
@@ -39,6 +41,14 @@
|
|
|
39
41
|
"default": "start",
|
|
40
42
|
"description": "Text alignment inside cards and of the header."
|
|
41
43
|
},
|
|
44
|
+
"layout": {
|
|
45
|
+
"options": [
|
|
46
|
+
"grid",
|
|
47
|
+
"rows"
|
|
48
|
+
],
|
|
49
|
+
"default": "grid",
|
|
50
|
+
"description": "Cards in a grid, or one item per row: title, text and link on one axis, a hairline between rows, no card box (columns and style do not apply)."
|
|
51
|
+
},
|
|
42
52
|
"tone": {
|
|
43
53
|
"options": [
|
|
44
54
|
"default",
|
|
@@ -113,6 +123,12 @@
|
|
|
113
123
|
},
|
|
114
124
|
"description": "The cards. image is an ImageInput ({ src, alt, width?, height? })."
|
|
115
125
|
},
|
|
126
|
+
"index": {
|
|
127
|
+
"type": "boolean",
|
|
128
|
+
"default": false,
|
|
129
|
+
"content": false,
|
|
130
|
+
"description": "With layout rows: a numbered list, each row led by its number (01, 02, …) in the muted role."
|
|
131
|
+
},
|
|
116
132
|
"headingLevel": {
|
|
117
133
|
"type": "integer",
|
|
118
134
|
"min": 2,
|
|
@@ -230,6 +246,11 @@
|
|
|
230
246
|
"description": "The section's minimum height."
|
|
231
247
|
}
|
|
232
248
|
}
|
|
249
|
+
},
|
|
250
|
+
"id": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"content": false,
|
|
253
|
+
"description": "Anchor id of the section; the title gets `<id>-title`. Without one, `card-grid`, `card-grid-2`, … in render order, so every section on a page has its own."
|
|
233
254
|
}
|
|
234
255
|
},
|
|
235
256
|
"slots": {},
|
|
@@ -8,12 +8,14 @@ import Section from '../_shared/Section.astro'
|
|
|
8
8
|
import { measuredGrid } from '../_shared/measured'
|
|
9
9
|
import type { MeasuredStyle } from '../_shared/types'
|
|
10
10
|
import { safeHref } from '../_shared/safe-url'
|
|
11
|
+
import { instanceId } from '../_shared/instance-id'
|
|
12
|
+
import { muted } from '../_shared/muted'
|
|
11
13
|
|
|
12
14
|
const details = tv({
|
|
13
15
|
slots: {
|
|
14
16
|
heading: 'mb-8 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
15
17
|
list: 'grid gap-6',
|
|
16
|
-
term:
|
|
18
|
+
term: `text-sm font-semibold ${muted}`,
|
|
17
19
|
value: 'mt-1 not-italic whitespace-pre-line',
|
|
18
20
|
link: 'underline decoration-line underline-offset-4 hover:decoration-current',
|
|
19
21
|
},
|
|
@@ -50,9 +52,11 @@ interface Props {
|
|
|
50
52
|
const {
|
|
51
53
|
heading, email, phone, address, hours, mapUrl,
|
|
52
54
|
emailLabel = 'Email', phoneLabel = 'Phone', addressLabel = 'Address', hoursLabel = 'Opening hours', mapLabel = 'View on the map',
|
|
53
|
-
layout, id
|
|
55
|
+
layout, id: givenId,
|
|
54
56
|
measured,
|
|
55
57
|
} = Astro.props
|
|
58
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
59
|
+
const id = instanceId(Astro.locals, givenId, 'contact-details')
|
|
56
60
|
const s = details({ layout })
|
|
57
61
|
const headingId = heading ? `${id}-title` : undefined
|
|
58
62
|
// A dialler wants the number without spaces, dashes or brackets; the leading + stays.
|
|
@@ -60,7 +64,7 @@ const tel = phone ? `tel:${phone.replace(/[^\d+]/g, '')}` : undefined
|
|
|
60
64
|
const shown = Boolean(email || phone || address || hours || mapUrl)
|
|
61
65
|
---
|
|
62
66
|
{shown && (
|
|
63
|
-
<Section measured={measured} spacing="md" id={id} aria-labelledby={headingId}>
|
|
67
|
+
<Section kit="contact-details" measured={measured} spacing="md" id={id} aria-labelledby={headingId}>
|
|
64
68
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
65
69
|
<dl class={s.list({ class: measuredGrid(measured) })}>
|
|
66
70
|
{email && <div><dt class={s.term()}>{emailLabel}</dt><dd class={s.value()}><a href={`mailto:${email}`} class={s.link()}>{email}</a></dd></div>}
|
|
@@ -18,6 +18,8 @@ import StudioForm from '../../studio/StudioForm.astro'
|
|
|
18
18
|
import { formHome } from '../../../lib/features'
|
|
19
19
|
import { contactSectionBare } from './contact-form'
|
|
20
20
|
import { safeHref } from '../_shared/safe-url'
|
|
21
|
+
import { instanceId } from '../_shared/instance-id'
|
|
22
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
21
23
|
|
|
22
24
|
export interface ContactDetail {
|
|
23
25
|
/** What the detail is (Phone, Email); a source's icon list often has none. */
|
|
@@ -34,16 +36,16 @@ const contact = tv({
|
|
|
34
36
|
grid: 'grid gap-10',
|
|
35
37
|
copy: 'flex flex-col gap-4',
|
|
36
38
|
heading: 'text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
37
|
-
intro:
|
|
39
|
+
intro: `max-w-xl text-pretty ${muted}`,
|
|
38
40
|
details: 'mt-2 flex flex-col gap-4',
|
|
39
|
-
term:
|
|
41
|
+
term: `text-sm font-semibold ${muted}`,
|
|
40
42
|
value: 'text-current',
|
|
41
43
|
mark: 'mr-2 inline-flex size-5 shrink-0 align-[-0.25em]',
|
|
42
44
|
formColumn: 'flex flex-col gap-4',
|
|
43
45
|
formHeading: 'text-xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-2xl',
|
|
44
46
|
form: 'rounded-card',
|
|
45
47
|
card: 'flex flex-col gap-1 rounded-card border border-line p-4',
|
|
46
|
-
hours:
|
|
48
|
+
hours: `mt-2 whitespace-pre-line text-sm ${muted}`,
|
|
47
49
|
socials: 'mt-2 flex flex-wrap gap-4 text-sm',
|
|
48
50
|
},
|
|
49
51
|
variants: {
|
|
@@ -65,7 +67,7 @@ const contact = tv({
|
|
|
65
67
|
// Beside a form it then titles the form; without one it follows the details.
|
|
66
68
|
headingPlace: { top: {}, after: {} },
|
|
67
69
|
// 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.
|
|
68
|
-
tone: { default: { form: 'border border-line bg-surface p-6 md:p-8' }, muted: { form:
|
|
70
|
+
tone: { default: { form: 'border border-line bg-surface p-6 md:p-8' }, muted: { form: `bg-surface p-6 text-ink md:p-8 ${mutedReset}` }, inverse: { form: `bg-surface p-6 text-ink md:p-8 ${mutedReset}` }, accent: { form: `bg-surface p-6 text-ink md:p-8 ${mutedReset}` } },
|
|
69
71
|
},
|
|
70
72
|
compoundVariants: [
|
|
71
73
|
{ layout: 'stacked', align: 'center', class: { grid: 'mx-auto max-w-2xl' } },
|
|
@@ -100,7 +102,9 @@ interface Props {
|
|
|
100
102
|
id?: string | undefined
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const { model, heading, intro, formHeading, details = [], layout, detailsStyle, panel, hours, socials = [], map, align, tone, headingPlace = 'top', measured, id
|
|
105
|
+
const { model, heading, intro, formHeading, details = [], layout, detailsStyle, panel, hours, socials = [], map, align, tone, headingPlace = 'top', measured, id: givenId } = Astro.props
|
|
106
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
107
|
+
const id = instanceId(Astro.locals, givenId, 'contact')
|
|
104
108
|
// Nothing to render (Studio not bound and no fallback): a split would leave an empty column — the details stand alone.
|
|
105
109
|
const formShown = formHome(model) !== null
|
|
106
110
|
const bare = contactSectionBare({ formShown, heading, intro, details })
|
|
@@ -120,7 +124,7 @@ const LINK = 'underline decoration-line underline-offset-4 hover:decoration-curr
|
|
|
120
124
|
const FormHeading = heading ? 'h3' : 'h2'
|
|
121
125
|
---
|
|
122
126
|
{!bare && (
|
|
123
|
-
<Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
127
|
+
<Section kit="contact-form" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
124
128
|
<div class={s.grid()}>
|
|
125
129
|
{!formOnly && (heading || intro || details.length > 0 || hours || socials.length > 0 || map) && !(headingInForm && !hours && socials.length === 0 && !map && !intro && details.length === 0) && (
|
|
126
130
|
<div class={s.copy()}>
|
|
@@ -157,7 +161,7 @@ const FormHeading = heading ? 'h3' : 'h2'
|
|
|
157
161
|
{map && (
|
|
158
162
|
<div>
|
|
159
163
|
<Embed address={map.address} url={map.url} title={map.title} aspect="map" width="page" />
|
|
160
|
-
{map.address && <p class=
|
|
164
|
+
{map.address && <p class:list={['text-sm', muted]}>{map.address}</p>}
|
|
161
165
|
</div>
|
|
162
166
|
)}
|
|
163
167
|
{!headingTop && headingInCopy && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
package/components/cta/Cta.astro
CHANGED
|
@@ -5,21 +5,23 @@ import { tv } from 'tailwind-variants'
|
|
|
5
5
|
import Button from '../_shared/Button.astro'
|
|
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 type { ActionInput, ImageInput, MeasuredStyle } from '../_shared/types'
|
|
10
|
+
import { muted, mutedAccent, mutedBrand, mutedInverse } from '../_shared/muted'
|
|
9
11
|
|
|
10
12
|
const cta = tv({
|
|
11
13
|
slots: {
|
|
12
14
|
box: 'flex flex-col gap-6',
|
|
13
15
|
copy: 'flex flex-col gap-3',
|
|
14
|
-
eyebrow:
|
|
16
|
+
eyebrow: `text-sm font-semibold tracking-wide uppercase ${muted}`,
|
|
15
17
|
// The text and buttons when a picture stands beside them.
|
|
16
18
|
text: 'flex min-w-0 flex-1 flex-col gap-6',
|
|
17
19
|
// The buttons with the small print under them.
|
|
18
20
|
cta: 'flex flex-col gap-2 md:shrink-0',
|
|
19
|
-
note:
|
|
21
|
+
note: `text-sm ${muted}`,
|
|
20
22
|
media: 'w-full max-w-sm shrink-0 overflow-hidden rounded-[var(--radius-image,var(--radius-card))] md:w-2/5',
|
|
21
23
|
heading: 'text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
22
|
-
body:
|
|
24
|
+
body: `max-w-2xl text-pretty ${muted}`,
|
|
23
25
|
actions: 'flex flex-wrap gap-3',
|
|
24
26
|
},
|
|
25
27
|
variants: {
|
|
@@ -41,7 +43,7 @@ const cta = tv({
|
|
|
41
43
|
},
|
|
42
44
|
// 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.
|
|
43
45
|
// none: no fill of its own — the page background and text colour show through.
|
|
44
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: {
|
|
46
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: {}, brand: {}, none: {} },
|
|
45
47
|
// The buttons' own alignment: `start` keeps them at the start edge under centred text (a WordPress buttons block
|
|
46
48
|
// with no justification sits left in a group whose heading and paragraph are centred). The row is the text's
|
|
47
49
|
// column, as WordPress's content width is, not the whole section. Only centred needs it: inline and stacked
|
|
@@ -52,9 +54,9 @@ const cta = tv({
|
|
|
52
54
|
// A card carries the tone itself, on a plain section; a plain card still needs a surface to read as one.
|
|
53
55
|
{ frame: 'card', tone: 'default', class: { box: 'bg-surface-muted text-ink' } },
|
|
54
56
|
{ frame: 'card', tone: 'muted', class: { box: 'border-transparent bg-surface-muted text-ink' } },
|
|
55
|
-
{ frame: 'card', tone: 'inverse', class: { box:
|
|
56
|
-
{ frame: 'card', tone: 'accent', class: { box:
|
|
57
|
-
{ frame: 'card', tone: 'brand', class: { box:
|
|
57
|
+
{ frame: 'card', tone: 'inverse', class: { box: `border-transparent bg-ink text-surface ${mutedInverse}` } },
|
|
58
|
+
{ frame: 'card', tone: 'accent', class: { box: `border-transparent bg-accent text-accent-ink ${mutedAccent}` } },
|
|
59
|
+
{ frame: 'card', tone: 'brand', class: { box: `border-transparent bg-brand text-brand-ink ${mutedBrand}` } },
|
|
58
60
|
{ layout: 'centered', actionsAlign: 'start', class: { actions: 'w-full max-w-2xl justify-start' } },
|
|
59
61
|
{ layout: 'centered', class: { cta: 'items-center' } },
|
|
60
62
|
],
|
|
@@ -87,19 +89,21 @@ interface Props {
|
|
|
87
89
|
id?: string | undefined
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
const { heading, eyebrow, body, note, image, actions, layout, frame, size, align, mediaSide, tone = 'accent', actionsAlign, bgImage, bgOverlay, measured: given, id } = Astro.props
|
|
92
|
+
const { heading, eyebrow, body, note, image, actions, layout, frame, size, align, mediaSide, tone = 'accent', actionsAlign, bgImage, bgOverlay, measured: given, id: givenId } = Astro.props
|
|
93
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `cta`, `cta-2`, … in render order.
|
|
94
|
+
const id = instanceId(Astro.locals, givenId, 'cta')
|
|
91
95
|
const s = cta({ layout, frame, size, align, mediaSide, tone, actionsAlign })
|
|
92
96
|
// A background picture is the section's measured background: Section draws the image and its overlay, and the text
|
|
93
97
|
// turns light so it reads on either.
|
|
94
98
|
const measured: MeasuredStyle | undefined = bgImage
|
|
95
99
|
? { ...given, tone: 'dark', bg: { ...given?.bg, image: bgImage, overlay: bgOverlay ?? given?.bg?.overlay ?? 'rgba(0, 0, 0, 0.55)' } }
|
|
96
100
|
: given
|
|
97
|
-
const headingId =
|
|
101
|
+
const headingId = `${id}-title`
|
|
98
102
|
// On the accent background the primary button (accent) would vanish: it is drawn as the secondary style there.
|
|
99
103
|
const onAccent = (action: ActionInput): ActionInput => ({ label: action.label, href: action.href, newTab: action.newTab, style: 'secondary' })
|
|
100
104
|
const shown = actions.map(action => (tone === 'accent' && !bgImage && (action.style ?? 'primary') === 'primary' ? onAccent(action) : action))
|
|
101
105
|
---
|
|
102
|
-
<Section measured={measured} tone={frame === 'card' ? 'default' : tone} spacing={size === 'lg' ? 'lg' : 'md'} id={id} aria-labelledby={headingId}>
|
|
106
|
+
<Section kit="cta" measured={measured} tone={frame === 'card' ? 'default' : tone} spacing={size === 'lg' ? 'lg' : 'md'} id={id} aria-labelledby={headingId}>
|
|
103
107
|
<div class:list={[s.box(), image && 'md:flex-row md:items-center md:justify-between md:gap-12']}>
|
|
104
108
|
<div class={image ? s.text() : 'contents'}>
|
|
105
109
|
<div class={s.copy()}>
|
package/components/cta/meta.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "A call to action: a short heading, an optional line of text and one or two buttons — across the whole section, or as a card inside it.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["Button.astro", "KitImage.astro", "Section.astro", "measured.ts", "safe-url.ts", "types.ts"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "Section.astro", "instance-id.ts", "measured.ts", "muted.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["inline", "centered", "stacked"], "default": "inline", "description": "inline: text left, buttons right on wide screens. centered: everything centred. stacked: heading, text and buttons in one column at the start edge on every width." },
|
|
10
10
|
"frame": { "options": ["full", "card"], "default": "full", "description": "full: the tone fills the section. card: a rounded box in a plain section carries the tone." },
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"bgOverlay": { "type": "string", "content": false, "description": "The colour laid over the background image (a hex or rgb colour); default black at 55 %." },
|
|
25
25
|
"actions": { "type": "array", "label": "Buttons", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "label": "Label", "required": true, "description": "The button text." }, "href": { "type": "url", "label": "Link", "required": true, "description": "Where the button goes: a page on this site or a full address." }, "newTab": { "type": "boolean", "label": "Open in a new tab", "description": "Open the link in a new tab (an external site the reader should not leave for)." }, "style": { "type": "select", "label": "Style", "options": ["primary", "secondary", "ghost"], "description": "primary for the main action, secondary or ghost for the ones beside it." } } }, "description": "Buttons, in order; usually one." },
|
|
26
26
|
"measured": { "type": "object", "content": false, "description": "The source section's measured style (MeasuredStyle): frame width, padding, fill, image, columns per width, gap, text tone, radius, minimum height — all in pixels. Absent, the site's tokens; present, it wins over tone and columns. Values that are not a number, a colour or a known keyword are dropped.", "fields": { "containerPx": { "type": "integer", "min": 0, "max": 2560, "description": "The frame's content width." }, "padY": { "type": "integer", "min": 0, "max": 400, "description": "Top and bottom padding." }, "padX": { "type": "integer", "min": 0, "max": 200, "description": "The frame's side padding (gutter)." }, "bg": { "type": "object", "description": "The section's background.", "fields": { "color": { "type": "string", "description": "Fill colour: hex, rgb(a) or hsl(a)." }, "image": { "type": "string", "description": "Background image address (http(s) or root-relative), drawn behind the content." }, "overlay": { "type": "string", "description": "Colour or linear/radial gradient laid over the image or the fill." }, "size": { "type": "string", "description": "cover, contain or auto; anything else draws as cover." }, "position": { "type": "string", "description": "One or two of center/top/bottom/left/right or percentages." } } }, "columns": { "type": "object", "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.", "fields": { "390": { "type": "integer", "min": 1, "max": 6, "description": "Phone." }, "768": { "type": "integer", "min": 1, "max": 6, "description": "Tablet." }, "1280": { "type": "integer", "min": 1, "max": 6, "description": "Desktop." } } }, "gap": { "type": "integer", "min": 0, "max": 200, "description": "Gap between items." }, "tone": { "type": "select", "options": ["light", "dark"], "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides." }, "align": { "type": "select", "options": ["start", "center", "end"], "description": "Where the heading and lead sit; read by a component whose own align is not set." }, "radius": { "type": "integer", "min": 0, "max": 200, "description": "Card and item corner radius." }, "minHeight": { "type": "integer", "min": 0, "max": 2000, "description": "The section's minimum height." } } },
|
|
27
|
-
"id": { "type": "string", "content": false, "description": "Anchor id of the section; the heading gets `<id>-title`." }
|
|
27
|
+
"id": { "type": "string", "content": false, "description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `cta`, `cta-2`, … in render order, so every section on a page has its own." }
|
|
28
28
|
},
|
|
29
29
|
"slots": {},
|
|
30
30
|
"js": "none",
|