@contentrain/astro-kit 0.29.4 → 0.31.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 +25 -2
- package/catalog.json +1055 -7
- package/components/_shared/KitImage.astro +6 -4
- package/components/_shared/Section.astro +18 -3
- package/components/_shared/measured.ts +142 -0
- package/components/_shared/types.ts +40 -0
- package/components/band/Band.astro +132 -0
- package/components/band/fixtures.json +8 -0
- package/components/band/meta.json +323 -0
- package/components/card-grid/CardGrid.astro +7 -3
- package/components/card-grid/fixtures.json +2 -1
- package/components/card-grid/meta.json +111 -0
- package/components/contact-details/ContactDetails.astro +7 -2
- package/components/contact-details/meta.json +113 -1
- package/components/contact-form/meta.json +1 -1
- package/components/cta/meta.json +1 -1
- package/components/faq/meta.json +1 -1
- package/components/feature-list/FeatureList.astro +7 -3
- package/components/feature-list/fixtures.json +2 -1
- package/components/feature-list/meta.json +113 -1
- package/components/gallery/meta.json +1 -1
- package/components/hero/Hero.astro +14 -8
- package/components/hero/fixtures.json +2 -1
- package/components/hero/meta.json +111 -0
- package/components/logo-cloud/meta.json +1 -0
- package/components/nav/Nav.astro +7 -1
- package/components/newsletter/Newsletter.astro +5 -1
- package/components/newsletter/meta.json +113 -1
- package/components/post-card/PostCard.astro +9 -2
- package/components/post-card/meta.json +4 -1
- package/components/pricing/meta.json +1 -0
- package/components/split/meta.json +1 -0
- package/components/stats/meta.json +3 -1
- package/components/steps/meta.json +1 -0
- package/components/tabs/Tabs.astro +5 -2
- package/components/tabs/meta.json +2 -1
- package/components/team/meta.json +1 -0
- package/components/testimonial/meta.json +1 -0
- package/package.json +2 -2
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", "Section.astro", "types.ts"],
|
|
7
|
+
"shared": ["Button.astro", "Section.astro", "measured.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." },
|
package/components/faq/meta.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Questions and answers as native <details> disclosures (no script), in lines or cards; optionally one open at a time, and optionally with FAQPage structured data for search engines and AI answers.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["Section.astro"],
|
|
7
|
+
"shared": ["Section.astro", "measured.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"style": { "options": ["lines", "cards", "plain"], "default": "lines", "description": "lines: rows divided by rules. cards: each question in a rounded box. plain: the browser's own disclosure triangle without rules or boxes, questions at the body size (WordPress's details block)." },
|
|
10
10
|
"tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background." },
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// the feature's icon or a shared check, under an optional heading and intro.
|
|
4
4
|
import { tv } from 'tailwind-variants'
|
|
5
5
|
import Section, { type Tone } from '../_shared/Section.astro'
|
|
6
|
+
import { measuredGrid } from '../_shared/measured'
|
|
7
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
6
8
|
|
|
7
9
|
const list = tv({
|
|
8
10
|
slots: {
|
|
@@ -58,23 +60,25 @@ interface Props {
|
|
|
58
60
|
style?: 'icon' | 'check' | 'list' | undefined
|
|
59
61
|
tone?: Tone | undefined
|
|
60
62
|
id?: string | undefined
|
|
63
|
+
/** 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. */
|
|
64
|
+
measured?: MeasuredStyle | undefined
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
const { heading, intro, items, columns, style = 'icon', tone, id } = Astro.props
|
|
67
|
+
const { heading, intro, items, columns, style = 'icon', tone, id, measured } = Astro.props
|
|
64
68
|
const s = list({ columns, style, tone })
|
|
65
69
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
66
70
|
const pathOf = (item: Item) => (style !== 'check' && item.icon && icons[item.icon]) || icons.check
|
|
67
71
|
// A plain list's items are not headings; otherwise one level under the section heading, or the section's when there is none.
|
|
68
72
|
const Title = style === 'list' ? 'span' : heading ? 'h3' : 'h2'
|
|
69
73
|
---
|
|
70
|
-
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
74
|
+
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
71
75
|
{(heading || intro) && (
|
|
72
76
|
<header class="mb-10 flex max-w-2xl flex-col gap-3">
|
|
73
77
|
{heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
|
|
74
78
|
{intro && <p class:list={['text-lg', tone === 'accent' ? undefined : 'opacity-80']} data-kit-text>{intro}</p>}
|
|
75
79
|
</header>
|
|
76
80
|
)}
|
|
77
|
-
<ul class={s.grid()}>
|
|
81
|
+
<ul class={s.grid({ class: measuredGrid(measured) })}>
|
|
78
82
|
{items.map(item => (
|
|
79
83
|
<li class={s.item()}>
|
|
80
84
|
<span class={s.mark()} aria-hidden="true">
|
|
@@ -150,5 +150,6 @@
|
|
|
150
150
|
],
|
|
151
151
|
"style": "list"
|
|
152
152
|
}
|
|
153
|
-
}
|
|
153
|
+
},
|
|
154
|
+
{"name": "measured-gap", "props": {"heading": "Why us", "measured": {"columns": {"390": 1, "768": 2, "1280": 3}, "gap": 16}, "items": [{"title": "Fast", "text": "Static HTML."}, {"title": "Kept", "text": "Every address survives."}, {"title": "Editable", "text": "Content in Studio."}]}}
|
|
154
155
|
]
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": [
|
|
8
|
-
"Section.astro"
|
|
8
|
+
"Section.astro",
|
|
9
|
+
"measured.ts",
|
|
10
|
+
"types.ts"
|
|
9
11
|
],
|
|
10
12
|
"variants": {
|
|
11
13
|
"columns": {
|
|
@@ -89,6 +91,116 @@
|
|
|
89
91
|
"type": "string",
|
|
90
92
|
"content": false,
|
|
91
93
|
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
94
|
+
},
|
|
95
|
+
"measured": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"content": false,
|
|
98
|
+
"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.",
|
|
99
|
+
"fields": {
|
|
100
|
+
"containerPx": {
|
|
101
|
+
"type": "integer",
|
|
102
|
+
"min": 0,
|
|
103
|
+
"max": 2560,
|
|
104
|
+
"description": "The frame's content width."
|
|
105
|
+
},
|
|
106
|
+
"padY": {
|
|
107
|
+
"type": "integer",
|
|
108
|
+
"min": 0,
|
|
109
|
+
"max": 400,
|
|
110
|
+
"description": "Top and bottom padding."
|
|
111
|
+
},
|
|
112
|
+
"padX": {
|
|
113
|
+
"type": "integer",
|
|
114
|
+
"min": 0,
|
|
115
|
+
"max": 200,
|
|
116
|
+
"description": "The frame's side padding (gutter)."
|
|
117
|
+
},
|
|
118
|
+
"bg": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"description": "The section's background.",
|
|
121
|
+
"fields": {
|
|
122
|
+
"color": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
125
|
+
},
|
|
126
|
+
"image": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
129
|
+
},
|
|
130
|
+
"overlay": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
133
|
+
},
|
|
134
|
+
"size": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
137
|
+
},
|
|
138
|
+
"position": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"columns": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
147
|
+
"fields": {
|
|
148
|
+
"390": {
|
|
149
|
+
"type": "integer",
|
|
150
|
+
"min": 1,
|
|
151
|
+
"max": 6,
|
|
152
|
+
"description": "Phone."
|
|
153
|
+
},
|
|
154
|
+
"768": {
|
|
155
|
+
"type": "integer",
|
|
156
|
+
"min": 1,
|
|
157
|
+
"max": 6,
|
|
158
|
+
"description": "Tablet."
|
|
159
|
+
},
|
|
160
|
+
"1280": {
|
|
161
|
+
"type": "integer",
|
|
162
|
+
"min": 1,
|
|
163
|
+
"max": 6,
|
|
164
|
+
"description": "Desktop."
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"gap": {
|
|
169
|
+
"type": "integer",
|
|
170
|
+
"min": 0,
|
|
171
|
+
"max": 200,
|
|
172
|
+
"description": "Gap between items."
|
|
173
|
+
},
|
|
174
|
+
"tone": {
|
|
175
|
+
"type": "select",
|
|
176
|
+
"options": [
|
|
177
|
+
"light",
|
|
178
|
+
"dark"
|
|
179
|
+
],
|
|
180
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
181
|
+
},
|
|
182
|
+
"align": {
|
|
183
|
+
"type": "select",
|
|
184
|
+
"options": [
|
|
185
|
+
"start",
|
|
186
|
+
"center",
|
|
187
|
+
"end"
|
|
188
|
+
],
|
|
189
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
190
|
+
},
|
|
191
|
+
"radius": {
|
|
192
|
+
"type": "integer",
|
|
193
|
+
"min": 0,
|
|
194
|
+
"max": 200,
|
|
195
|
+
"description": "Card and item corner radius."
|
|
196
|
+
},
|
|
197
|
+
"minHeight": {
|
|
198
|
+
"type": "integer",
|
|
199
|
+
"min": 0,
|
|
200
|
+
"max": 2000,
|
|
201
|
+
"description": "The section's minimum height."
|
|
202
|
+
}
|
|
203
|
+
}
|
|
92
204
|
}
|
|
93
205
|
},
|
|
94
206
|
"slots": {},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Images in a square grid or in masonry columns, with optional captions. A click shows the image large in a native popover (no script), opens the image file, or does nothing.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["KitImage.astro", "Section.astro", "image.ts", "types.ts"],
|
|
7
|
+
"shared": ["KitImage.astro", "Section.astro", "image.ts", "measured.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["grid", "masonry"], "default": "grid", "description": "grid: square crops in rows. masonry: columns that keep each image's shape." },
|
|
10
10
|
"columns": { "options": ["2", "3", "4"], "default": "3", "description": "Columns on wide screens; phones show two." },
|
|
@@ -5,7 +5,8 @@ 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 type { ActionInput, ImageInput } from '../_shared/types'
|
|
8
|
+
import type { ActionInput, ImageInput, MeasuredStyle } from '../_shared/types'
|
|
9
|
+
import { measuredFrame } from '../_shared/measured'
|
|
9
10
|
import { coverHeight as heightOf, headingStyle as styleOf } from './hero'
|
|
10
11
|
|
|
11
12
|
const hero = tv({
|
|
@@ -78,23 +79,28 @@ interface Props {
|
|
|
78
79
|
headingScale?: 'default' | 'theme' | undefined
|
|
79
80
|
/** The level whose site size the heading takes, when it is not the level it is written at (`h1` size on an `h2`: a page keeps one `h1`). `own` (absent): the heading's own level under `theme`, as before. */
|
|
80
81
|
headingSize?: 'own' | 'h1' | 'h2' | undefined
|
|
82
|
+
/** 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. */
|
|
83
|
+
measured?: MeasuredStyle | undefined
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
|
|
84
|
-
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id, minHeight, headingScale, headingSize } = Astro.props
|
|
87
|
+
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id, minHeight, headingScale, headingSize, measured } = Astro.props
|
|
85
88
|
const s = hero({ layout, align, leadAlign, actionsAlign, width, tone, headingScale, headingSize })
|
|
86
89
|
const Heading = level === 1 ? 'h1' : 'h2'
|
|
87
90
|
const headingId = id ? `${id}-title` : undefined
|
|
88
91
|
const cover = layout === 'cover' && image
|
|
89
92
|
const coverHeight = cover ? heightOf(minHeight) : undefined
|
|
90
93
|
const headingStyle = styleOf(headingScale, level, headingSize)
|
|
94
|
+
// A cover draws its own image: the measured background image is that image, and the measured overlay is its scrim.
|
|
95
|
+
const sectionMeasured = cover && measured ? { ...measured, bg: measured.bg ? { ...measured.bg, image: undefined, overlay: undefined } : undefined } : measured
|
|
96
|
+
const scrim = cover ? measuredFrame(measured).overlay : undefined
|
|
91
97
|
---
|
|
92
98
|
{cover && width === 'content'
|
|
93
99
|
? (
|
|
94
|
-
<Section tone="none" spacing="sm" width="prose" id={id} aria-labelledby={headingId}>
|
|
100
|
+
<Section measured={sectionMeasured} tone="none" spacing="sm" width="prose" id={id} aria-labelledby={headingId}>
|
|
95
101
|
<div class="relative isolate overflow-hidden bg-ink text-surface">
|
|
96
102
|
<KitImage image={image} priority sizes="(min-width: 768px) 46rem, 100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
97
|
-
<div class=
|
|
103
|
+
<div class:list={['absolute inset-0 -z-10', !scrim && 'bg-ink/60']} style={scrim ? `background: ${scrim}` : undefined} aria-hidden="true" />
|
|
98
104
|
<div class="px-6 py-10 md:px-10">
|
|
99
105
|
<div class={s.grid()} style={coverHeight ? `min-height: ${coverHeight}` : undefined}>
|
|
100
106
|
<div class={s.copy()}>
|
|
@@ -110,10 +116,10 @@ const headingStyle = styleOf(headingScale, level, headingSize)
|
|
|
110
116
|
)
|
|
111
117
|
: cover
|
|
112
118
|
? (
|
|
113
|
-
<Section tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
|
|
119
|
+
<Section measured={sectionMeasured} tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
|
|
114
120
|
<KitImage image={image} priority sizes="100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
115
|
-
<div class=
|
|
116
|
-
<div class=
|
|
121
|
+
<div class:list={['absolute inset-0 -z-10', !scrim && 'bg-ink/60']} style={scrim ? `background: ${scrim}` : undefined} aria-hidden="true" />
|
|
122
|
+
<div class:list={['container-page', measured?.padY === undefined && 'py-16 md:py-28']}>
|
|
117
123
|
<div class={s.grid()} style={coverHeight ? `min-height: ${coverHeight}` : undefined}>
|
|
118
124
|
<div class={s.copy()}>
|
|
119
125
|
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
@@ -126,7 +132,7 @@ const headingStyle = styleOf(headingScale, level, headingSize)
|
|
|
126
132
|
</Section>
|
|
127
133
|
)
|
|
128
134
|
: (
|
|
129
|
-
<Section tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
|
|
135
|
+
<Section measured={sectionMeasured} tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
|
|
130
136
|
{/* Without an image a split has nothing beside the text: the text takes the whole row, not half of it. */}
|
|
131
137
|
<div class={s.grid({ class: image ? undefined : 'md:grid-cols-1' })}>
|
|
132
138
|
<div class={s.copy()}>
|
|
@@ -239,5 +239,6 @@
|
|
|
239
239
|
},
|
|
240
240
|
"headingSize": "h2"
|
|
241
241
|
}
|
|
242
|
-
}
|
|
242
|
+
},
|
|
243
|
+
{"name": "cover-measured-overlay", "props": {"heading": "Fresh eggs, every morning", "lead": "A family farm since 1962.", "layout": "cover", "image": {"src": "/kit-fixtures/photo-4.svg", "alt": "", "width": 1600, "height": 900}, "actions": [{"label": "Visit us", "href": "/visit/"}], "measured": {"padY": 140, "minHeight": 560, "tone": "dark", "bg": {"image": "/kit-fixtures/photo-4.svg", "overlay": "rgba(20, 40, 10, 0.45)"}}}}
|
|
243
244
|
]
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"Button.astro",
|
|
9
9
|
"KitImage.astro",
|
|
10
10
|
"Section.astro",
|
|
11
|
+
"measured.ts",
|
|
11
12
|
"types.ts"
|
|
12
13
|
],
|
|
13
14
|
"variants": {
|
|
@@ -183,6 +184,116 @@
|
|
|
183
184
|
"min": 100,
|
|
184
185
|
"max": 2000,
|
|
185
186
|
"description": "Layout cover only: the cover's own height in pixels, as the source sets it (520); absent, the layout's default."
|
|
187
|
+
},
|
|
188
|
+
"measured": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"content": false,
|
|
191
|
+
"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.",
|
|
192
|
+
"fields": {
|
|
193
|
+
"containerPx": {
|
|
194
|
+
"type": "integer",
|
|
195
|
+
"min": 0,
|
|
196
|
+
"max": 2560,
|
|
197
|
+
"description": "The frame's content width."
|
|
198
|
+
},
|
|
199
|
+
"padY": {
|
|
200
|
+
"type": "integer",
|
|
201
|
+
"min": 0,
|
|
202
|
+
"max": 400,
|
|
203
|
+
"description": "Top and bottom padding."
|
|
204
|
+
},
|
|
205
|
+
"padX": {
|
|
206
|
+
"type": "integer",
|
|
207
|
+
"min": 0,
|
|
208
|
+
"max": 200,
|
|
209
|
+
"description": "The frame's side padding (gutter)."
|
|
210
|
+
},
|
|
211
|
+
"bg": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"description": "The section's background.",
|
|
214
|
+
"fields": {
|
|
215
|
+
"color": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
218
|
+
},
|
|
219
|
+
"image": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
222
|
+
},
|
|
223
|
+
"overlay": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
226
|
+
},
|
|
227
|
+
"size": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
230
|
+
},
|
|
231
|
+
"position": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"columns": {
|
|
238
|
+
"type": "object",
|
|
239
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
240
|
+
"fields": {
|
|
241
|
+
"390": {
|
|
242
|
+
"type": "integer",
|
|
243
|
+
"min": 1,
|
|
244
|
+
"max": 6,
|
|
245
|
+
"description": "Phone."
|
|
246
|
+
},
|
|
247
|
+
"768": {
|
|
248
|
+
"type": "integer",
|
|
249
|
+
"min": 1,
|
|
250
|
+
"max": 6,
|
|
251
|
+
"description": "Tablet."
|
|
252
|
+
},
|
|
253
|
+
"1280": {
|
|
254
|
+
"type": "integer",
|
|
255
|
+
"min": 1,
|
|
256
|
+
"max": 6,
|
|
257
|
+
"description": "Desktop."
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"gap": {
|
|
262
|
+
"type": "integer",
|
|
263
|
+
"min": 0,
|
|
264
|
+
"max": 200,
|
|
265
|
+
"description": "Gap between items."
|
|
266
|
+
},
|
|
267
|
+
"tone": {
|
|
268
|
+
"type": "select",
|
|
269
|
+
"options": [
|
|
270
|
+
"light",
|
|
271
|
+
"dark"
|
|
272
|
+
],
|
|
273
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
274
|
+
},
|
|
275
|
+
"align": {
|
|
276
|
+
"type": "select",
|
|
277
|
+
"options": [
|
|
278
|
+
"start",
|
|
279
|
+
"center",
|
|
280
|
+
"end"
|
|
281
|
+
],
|
|
282
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
283
|
+
},
|
|
284
|
+
"radius": {
|
|
285
|
+
"type": "integer",
|
|
286
|
+
"min": 0,
|
|
287
|
+
"max": 200,
|
|
288
|
+
"description": "Card and item corner radius."
|
|
289
|
+
},
|
|
290
|
+
"minHeight": {
|
|
291
|
+
"type": "integer",
|
|
292
|
+
"min": 0,
|
|
293
|
+
"max": 2000,
|
|
294
|
+
"description": "The section's minimum height."
|
|
295
|
+
}
|
|
296
|
+
}
|
|
186
297
|
}
|
|
187
298
|
},
|
|
188
299
|
"slots": {},
|
package/components/nav/Nav.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
// Site navigation. Wide screens get an inline list whose submenus open on
|
|
3
|
-
// hover and keyboard focus; narrow screens get a toggle that opens the menu as
|
|
3
|
+
// hover and keyboard focus (a chevron button marks the ones that have one); narrow screens get a toggle that opens the menu as
|
|
4
4
|
// a native popover (drawer) or a <details> dropdown. No JavaScript.
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
6
|
import type { NavItem } from '../_shared/types'
|
|
@@ -57,11 +57,17 @@ const drawer = `${id}-drawer`
|
|
|
57
57
|
<li class="group relative">
|
|
58
58
|
<a href={item.href} aria-current={item.href === current ? 'page' : undefined} target={item.newTab ? '_blank' : undefined} rel={item.newTab ? 'noopener' : undefined} class={link()}>{item.label}</a>
|
|
59
59
|
{item.children && item.children.length > 0 && (
|
|
60
|
+
<>
|
|
61
|
+
{/* The chevron is the submenu's handle: focusing it (a tap on a touch screen, Tab on a keyboard) opens the submenu, with no script. */}
|
|
62
|
+
<button type="button" aria-haspopup="true" aria-label={item.label} class="ms-1 inline-flex cursor-pointer items-center align-middle opacity-80 hover:opacity-100">
|
|
63
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
|
64
|
+
</button>
|
|
60
65
|
<ul class={sub()}>
|
|
61
66
|
{item.children.map(child => (
|
|
62
67
|
<li><a href={child.href} aria-current={child.href === current ? 'page' : undefined} class="block rounded-md px-3 py-2 text-sm text-ink no-underline hover:bg-surface-muted">{child.label}</a></li>
|
|
63
68
|
))}
|
|
64
69
|
</ul>
|
|
70
|
+
</>
|
|
65
71
|
)}
|
|
66
72
|
</li>
|
|
67
73
|
))}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// of placing it.
|
|
11
11
|
import { tv } from 'tailwind-variants'
|
|
12
12
|
import Section from '../_shared/Section.astro'
|
|
13
|
+
import type { MeasuredStyle } from '../_shared/types'
|
|
13
14
|
import { newsletterForm } from './newsletter'
|
|
14
15
|
|
|
15
16
|
const newsletter = tv({
|
|
@@ -64,12 +65,15 @@ interface Props {
|
|
|
64
65
|
layout?: 'inline' | 'stacked' | undefined
|
|
65
66
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
66
67
|
id?: string | undefined
|
|
68
|
+
/** 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. */
|
|
69
|
+
measured?: MeasuredStyle | undefined
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const {
|
|
70
73
|
action, provider = 'mailchimp', emailName, askName = false, heading, intro, note,
|
|
71
74
|
emailLabel = 'Email address', nameLabel = 'First name', submitLabel = 'Subscribe', newTab = true,
|
|
72
75
|
layout, tone = 'muted', id = 'newsletter',
|
|
76
|
+
measured,
|
|
73
77
|
} = Astro.props
|
|
74
78
|
const form = newsletterForm(provider, action, { email: emailName }, Astro.site)
|
|
75
79
|
const s = newsletter({ layout, tone })
|
|
@@ -78,7 +82,7 @@ const headingId = heading ? `${id}-title` : undefined
|
|
|
78
82
|
const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' } : {}
|
|
79
83
|
---
|
|
80
84
|
{form && (
|
|
81
|
-
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
85
|
+
<Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
82
86
|
<div class={s.box()}>
|
|
83
87
|
{(heading || intro) && (
|
|
84
88
|
<div class={s.copy()}>
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": [
|
|
8
|
-
"Section.astro"
|
|
8
|
+
"Section.astro",
|
|
9
|
+
"measured.ts",
|
|
10
|
+
"types.ts"
|
|
9
11
|
],
|
|
10
12
|
"variants": {
|
|
11
13
|
"layout": {
|
|
@@ -99,6 +101,116 @@
|
|
|
99
101
|
"type": "string",
|
|
100
102
|
"content": false,
|
|
101
103
|
"description": "Section id, unique per page (two sign-ups on one page need two ids): the heading gets `<id>-title`, the inputs `<id>-email`/`<id>-name`."
|
|
104
|
+
},
|
|
105
|
+
"measured": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"content": false,
|
|
108
|
+
"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.",
|
|
109
|
+
"fields": {
|
|
110
|
+
"containerPx": {
|
|
111
|
+
"type": "integer",
|
|
112
|
+
"min": 0,
|
|
113
|
+
"max": 2560,
|
|
114
|
+
"description": "The frame's content width."
|
|
115
|
+
},
|
|
116
|
+
"padY": {
|
|
117
|
+
"type": "integer",
|
|
118
|
+
"min": 0,
|
|
119
|
+
"max": 400,
|
|
120
|
+
"description": "Top and bottom padding."
|
|
121
|
+
},
|
|
122
|
+
"padX": {
|
|
123
|
+
"type": "integer",
|
|
124
|
+
"min": 0,
|
|
125
|
+
"max": 200,
|
|
126
|
+
"description": "The frame's side padding (gutter)."
|
|
127
|
+
},
|
|
128
|
+
"bg": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"description": "The section's background.",
|
|
131
|
+
"fields": {
|
|
132
|
+
"color": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Fill colour: hex, rgb(a) or hsl(a)."
|
|
135
|
+
},
|
|
136
|
+
"image": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"description": "Background image address (http(s) or root-relative), drawn behind the content."
|
|
139
|
+
},
|
|
140
|
+
"overlay": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "Colour or linear/radial gradient laid over the image or the fill."
|
|
143
|
+
},
|
|
144
|
+
"size": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "cover, contain or auto; anything else draws as cover."
|
|
147
|
+
},
|
|
148
|
+
"position": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "One or two of center/top/bottom/left/right or percentages."
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"columns": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
|
|
157
|
+
"fields": {
|
|
158
|
+
"390": {
|
|
159
|
+
"type": "integer",
|
|
160
|
+
"min": 1,
|
|
161
|
+
"max": 6,
|
|
162
|
+
"description": "Phone."
|
|
163
|
+
},
|
|
164
|
+
"768": {
|
|
165
|
+
"type": "integer",
|
|
166
|
+
"min": 1,
|
|
167
|
+
"max": 6,
|
|
168
|
+
"description": "Tablet."
|
|
169
|
+
},
|
|
170
|
+
"1280": {
|
|
171
|
+
"type": "integer",
|
|
172
|
+
"min": 1,
|
|
173
|
+
"max": 6,
|
|
174
|
+
"description": "Desktop."
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"gap": {
|
|
179
|
+
"type": "integer",
|
|
180
|
+
"min": 0,
|
|
181
|
+
"max": 200,
|
|
182
|
+
"description": "Gap between items."
|
|
183
|
+
},
|
|
184
|
+
"tone": {
|
|
185
|
+
"type": "select",
|
|
186
|
+
"options": [
|
|
187
|
+
"light",
|
|
188
|
+
"dark"
|
|
189
|
+
],
|
|
190
|
+
"description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
|
|
191
|
+
},
|
|
192
|
+
"align": {
|
|
193
|
+
"type": "select",
|
|
194
|
+
"options": [
|
|
195
|
+
"start",
|
|
196
|
+
"center",
|
|
197
|
+
"end"
|
|
198
|
+
],
|
|
199
|
+
"description": "Where the heading and lead sit; read by a component whose own align is not set."
|
|
200
|
+
},
|
|
201
|
+
"radius": {
|
|
202
|
+
"type": "integer",
|
|
203
|
+
"min": 0,
|
|
204
|
+
"max": 200,
|
|
205
|
+
"description": "Card and item corner radius."
|
|
206
|
+
},
|
|
207
|
+
"minHeight": {
|
|
208
|
+
"type": "integer",
|
|
209
|
+
"min": 0,
|
|
210
|
+
"max": 2000,
|
|
211
|
+
"description": "The section's minimum height."
|
|
212
|
+
}
|
|
213
|
+
}
|
|
102
214
|
}
|
|
103
215
|
},
|
|
104
216
|
"slots": {},
|
|
@@ -52,19 +52,26 @@ interface Props {
|
|
|
52
52
|
byline?: 'inline' | 'below' | undefined
|
|
53
53
|
/** The title in the site's link colour, as themes that colour every link do (Hello Elementor). */
|
|
54
54
|
linkTitle?: boolean | undefined
|
|
55
|
+
/** The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give. */
|
|
56
|
+
sizes?: string | undefined
|
|
57
|
+
/** The image block's height as the source sets it (a CSS length or `clamp()`), cropped to fit; wins over `imageAspect`. */
|
|
58
|
+
imageHeight?: string | undefined
|
|
59
|
+
/** The image block's aspect ratio (`16 / 9`) where the source sets an aspect ratio; absent, 16 / 9. */
|
|
60
|
+
imageAspect?: string | undefined
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle } = Astro.props
|
|
63
|
+
const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, sizes, imageHeight, imageAspect } = Astro.props
|
|
58
64
|
const date = rawDate === undefined ? undefined : new Date(rawDate)
|
|
59
65
|
const slots = card({ layout, surface, byline })
|
|
60
66
|
const below = byline === 'below'
|
|
61
67
|
const shown = date?.toLocaleDateString(locale, { dateStyle: 'long', timeZone: 'UTC' })
|
|
68
|
+
const imageStyle = imageHeight ? `height:${imageHeight}` : imageAspect ? `aspect-ratio:${imageAspect}` : undefined
|
|
62
69
|
const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
63
70
|
---
|
|
64
71
|
<article class={slots.root()}>
|
|
65
72
|
{image && layout !== 'minimal' && (
|
|
66
73
|
<div class={slots.media()}>
|
|
67
|
-
<KitImage image={image} sizes={layout === 'horizontal' ? '256px' : '(min-width: 1024px) 360px, 100vw'} class=
|
|
74
|
+
<KitImage image={image} sizes={sizes ?? (layout === 'horizontal' ? '256px' : '(min-width: 1024px) 360px, 100vw')} class:list={['w-full object-cover transition duration-300 group-hover:scale-[1.02]', !imageStyle && 'aspect-[16/9]']} style={imageStyle} />
|
|
68
75
|
</div>
|
|
69
76
|
)}
|
|
70
77
|
<div class={slots.body()}>
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"category": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true } }, "description": "Primary category and its archive." },
|
|
21
21
|
"author": { "type": "string", "description": "Author name (posts.author → authors.name)." },
|
|
22
22
|
"headingLevel": { "type": "integer", "min": 2, "max": 3, "default": 2, "description": "Title heading level in the page outline." },
|
|
23
|
-
"linkTitle": { "type": "boolean", "default": false, "description": "The title in the site's link colour (--color-link), as themes that colour every link do." }
|
|
23
|
+
"linkTitle": { "type": "boolean", "default": false, "description": "The title in the site's link colour (--color-link), as themes that colour every link do." },
|
|
24
|
+
"sizes": { "type": "string", "description": "The image's `sizes` hint; absent, the card's own guess. A list passes the slot its columns give." },
|
|
25
|
+
"imageHeight": { "type": "string", "description": "The image block's height as the source sets it (a CSS length or clamp()), cropped to fit; wins over imageAspect." },
|
|
26
|
+
"imageAspect": { "type": "string", "description": "The image block's aspect ratio (16 / 9) where the source sets an aspect ratio; absent, 16 / 9." }
|
|
24
27
|
},
|
|
25
28
|
"slots": {},
|
|
26
29
|
"js": "none",
|