@contentrain/astro-kit 0.21.0 → 0.22.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/catalog.json +27 -10
- package/components/card-grid/CardGrid.astro +3 -2
- package/components/card-grid/meta.json +1 -1
- package/components/contact-form/ContactForm.astro +6 -1
- package/components/contact-form/contact-form.ts +8 -0
- package/components/contact-form/fixtures.json +2 -2
- package/components/contact-form/meta.json +2 -2
- package/components/feature-list/FeatureList.astro +2 -1
- package/components/feature-list/meta.json +1 -1
- package/components/hero/Hero.astro +18 -8
- package/components/hero/fixtures.json +24 -0
- package/components/hero/hero.ts +9 -0
- package/components/hero/meta.json +15 -0
- package/components/pricing/Pricing.astro +3 -1
- package/components/pricing/meta.json +1 -1
- package/components/steps/Steps.astro +3 -1
- package/components/steps/meta.json +1 -1
- package/components/tabs/Tabs.astro +3 -1
- package/components/tabs/meta.json +1 -1
- package/components/team/Team.astro +4 -2
- package/components/team/meta.json +1 -1
- package/package.json +2 -2
package/catalog.json
CHANGED
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
"max": 3,
|
|
277
277
|
"default": 2,
|
|
278
278
|
"content": false,
|
|
279
|
-
"description": "Level of the section heading; card titles are one below."
|
|
279
|
+
"description": "Level of the section heading; card titles are one below, or at this level when there is no title."
|
|
280
280
|
}
|
|
281
281
|
},
|
|
282
282
|
"slots": {},
|
|
@@ -507,7 +507,7 @@
|
|
|
507
507
|
{
|
|
508
508
|
"id": "contact-form",
|
|
509
509
|
"name": "Contact form",
|
|
510
|
-
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project. When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
|
|
510
|
+
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project (a section left with only its heading is not drawn: the heading would title a form that is not there). When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
|
|
511
511
|
"category": "section",
|
|
512
512
|
"uses": [],
|
|
513
513
|
"shared": [
|
|
@@ -612,9 +612,10 @@
|
|
|
612
612
|
"classic/ninja-forms"
|
|
613
613
|
]
|
|
614
614
|
},
|
|
615
|
-
"a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading.",
|
|
615
|
+
"a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading. Without a form to show and with no introduction or details, the section is not drawn at all.",
|
|
616
616
|
"files": [
|
|
617
|
-
"ContactForm.astro"
|
|
617
|
+
"ContactForm.astro",
|
|
618
|
+
"contact-form.ts"
|
|
618
619
|
],
|
|
619
620
|
"contentFields": {
|
|
620
621
|
"heading": "heading",
|
|
@@ -1098,7 +1099,7 @@
|
|
|
1098
1099
|
"divi/et_pb_blurb"
|
|
1099
1100
|
]
|
|
1100
1101
|
},
|
|
1101
|
-
"a11y": "A list; icons are decorative (aria-hidden), the title and text carry the meaning. The section is labelled by its heading.",
|
|
1102
|
+
"a11y": "A list; icons are decorative (aria-hidden), the title and text carry the meaning. Each title is an <h3> under the section's <h2>, or an <h2> when the section has no heading (the list style's are plain text). The section is labelled by its heading.",
|
|
1102
1103
|
"pattern": {
|
|
1103
1104
|
"summary": "repeated icon + short title + line of text",
|
|
1104
1105
|
"signals": [
|
|
@@ -1672,6 +1673,14 @@
|
|
|
1672
1673
|
],
|
|
1673
1674
|
"default": "full",
|
|
1674
1675
|
"description": "Layout cover only. full: the image runs the full window width. content: the cover sits in the reading column, as a WordPress cover block that is not alignfull does under the page title."
|
|
1676
|
+
},
|
|
1677
|
+
"headingScale": {
|
|
1678
|
+
"options": [
|
|
1679
|
+
"default",
|
|
1680
|
+
"theme"
|
|
1681
|
+
],
|
|
1682
|
+
"default": "default",
|
|
1683
|
+
"description": "default: the layout's own type scale. theme: the heading takes the site's size for its level (`--text-heading-1` for the page-opening hero, `-2` otherwise), as the source's own h1 does under a block theme."
|
|
1675
1684
|
}
|
|
1676
1685
|
},
|
|
1677
1686
|
"props": {
|
|
@@ -1770,6 +1779,13 @@
|
|
|
1770
1779
|
"type": "string",
|
|
1771
1780
|
"content": false,
|
|
1772
1781
|
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
1782
|
+
},
|
|
1783
|
+
"minHeight": {
|
|
1784
|
+
"type": "integer",
|
|
1785
|
+
"content": false,
|
|
1786
|
+
"min": 100,
|
|
1787
|
+
"max": 2000,
|
|
1788
|
+
"description": "Layout cover only: the cover's own height in pixels, as the source sets it (520); absent, the layout's default."
|
|
1773
1789
|
}
|
|
1774
1790
|
},
|
|
1775
1791
|
"slots": {},
|
|
@@ -1790,7 +1806,8 @@
|
|
|
1790
1806
|
},
|
|
1791
1807
|
"a11y": "A labelled region (aria-labelledby the heading). One h1 by default; `level: 2` inside pages that already have one. The cover image is decorative when its alt is empty; text sits on an ink scrim for contrast.",
|
|
1792
1808
|
"files": [
|
|
1793
|
-
"Hero.astro"
|
|
1809
|
+
"Hero.astro",
|
|
1810
|
+
"hero.ts"
|
|
1794
1811
|
],
|
|
1795
1812
|
"contentFields": {
|
|
1796
1813
|
"heading": "heading",
|
|
@@ -2510,7 +2527,7 @@
|
|
|
2510
2527
|
"divi/et_pb_pricing_tables"
|
|
2511
2528
|
]
|
|
2512
2529
|
},
|
|
2513
|
-
"a11y": "Plans are a <ul>; each name is an <h3
|
|
2530
|
+
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label.",
|
|
2514
2531
|
"pattern": {
|
|
2515
2532
|
"summary": "repeated plan units: name + price + feature list + button",
|
|
2516
2533
|
"signals": [
|
|
@@ -3180,7 +3197,7 @@
|
|
|
3180
3197
|
"divi/et_pb_blurb"
|
|
3181
3198
|
]
|
|
3182
3199
|
},
|
|
3183
|
-
"a11y": "Steps are an <ol>, so their order and count are announced; the visible numbers are CSS counters, not text. Each title is an <h3
|
|
3200
|
+
"a11y": "Steps are an <ol>, so their order and count are announced; the visible numbers are CSS counters, not text. Each title is an <h3> under the section's <h2>, or an <h2> when the section has no heading. The section is labelled by its heading when it has one.",
|
|
3184
3201
|
"pattern": {
|
|
3185
3202
|
"summary": "ordered sequence: numbered title + short text units",
|
|
3186
3203
|
"signals": [
|
|
@@ -3291,7 +3308,7 @@
|
|
|
3291
3308
|
"classic/tabs"
|
|
3292
3309
|
]
|
|
3293
3310
|
},
|
|
3294
|
-
"a11y": "Zag.js tabs: role=tablist/tab/tabpanel with aria-selected, aria-controls and aria-labelledby; arrow keys (per orientation), Home and End move between tabs. Before the script runs every panel is visible with its own h3
|
|
3311
|
+
"a11y": "Zag.js tabs: role=tablist/tab/tabpanel with aria-selected, aria-controls and aria-labelledby; arrow keys (per orientation), Home and End move between tabs. Before the script runs every panel is visible with its own title (an h3 under the section's h2, an h2 when there is no heading); after, the titles become screen-reader-only and the tabs label the panels.",
|
|
3295
3312
|
"files": [
|
|
3296
3313
|
"Tabs.astro"
|
|
3297
3314
|
]
|
|
@@ -3408,7 +3425,7 @@
|
|
|
3408
3425
|
"divi/et_pb_team_member"
|
|
3409
3426
|
]
|
|
3410
3427
|
},
|
|
3411
|
-
"a11y": "Members are a <ul>; each name is an <h3
|
|
3428
|
+
"a11y": "Members are a <ul>; each name is an <h3> under the section's <h2> (an <h2> when the section has no heading), and a linked member's link text is the name. A portrait's text defaults to the name. The section is labelled by its heading when it has one.",
|
|
3412
3429
|
"pattern": {
|
|
3413
3430
|
"summary": "repeated person units: portrait + name + role",
|
|
3414
3431
|
"signals": [
|
|
@@ -44,7 +44,7 @@ interface Props {
|
|
|
44
44
|
title?: string | undefined
|
|
45
45
|
intro?: string | undefined
|
|
46
46
|
items: Item[]
|
|
47
|
-
/** Heading level of the section title; card titles are one level below. */
|
|
47
|
+
/** Heading level of the section title; card titles are one level below, or at this level when there is no title. */
|
|
48
48
|
headingLevel?: 2 | 3 | undefined
|
|
49
49
|
columns?: '2' | '3' | '4' | undefined
|
|
50
50
|
style?: 'plain' | 'bordered' | 'elevated' | undefined
|
|
@@ -57,7 +57,8 @@ const { list, card, media, title: cardTitle, text } = grid({ columns, style, ali
|
|
|
57
57
|
// Accent backgrounds have the least contrast headroom: text on them is never dimmed.
|
|
58
58
|
const dim = tone === 'accent' ? 'opacity-100' : undefined
|
|
59
59
|
const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
60
|
-
|
|
60
|
+
// One level under the section title, or the title's level when there is none.
|
|
61
|
+
const CardHeading = `h${title ? headingLevel + 1 : headingLevel}` as 'h2' | 'h3' | 'h4'
|
|
61
62
|
const headingId = title ? `cards-${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}` : undefined
|
|
62
63
|
---
|
|
63
64
|
<Section tone={tone} aria-labelledby={headingId}>
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"max": 3,
|
|
108
108
|
"default": 2,
|
|
109
109
|
"content": false,
|
|
110
|
-
"description": "Level of the section heading; card titles are one below."
|
|
110
|
+
"description": "Level of the section heading; card titles are one below, or at this level when there is no title."
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
113
|
"slots": {},
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// StudioForm): its fields, spam protection and notifications live in Studio,
|
|
5
5
|
// and submissions arrive there for review — no form plugin, no mail server.
|
|
6
6
|
// Until the site is bound to a Studio project the form renders nothing and the
|
|
7
|
-
// contact details still show
|
|
7
|
+
// contact details still show — and a section left with its heading alone is not
|
|
8
|
+
// drawn (a heading over a form that is not there). When the owner chose another home for forms at
|
|
8
9
|
// migration (site.config.ts `features.forms`), the same place holds a form
|
|
9
10
|
// that posts to their form service, an address to write to, or a link to the
|
|
10
11
|
// form on WordPress.
|
|
@@ -12,6 +13,7 @@ import { tv } from 'tailwind-variants'
|
|
|
12
13
|
import Section from '../_shared/Section.astro'
|
|
13
14
|
import StudioForm from '../../studio/StudioForm.astro'
|
|
14
15
|
import { formHome } from '../../../lib/features'
|
|
16
|
+
import { contactSectionBare } from './contact-form'
|
|
15
17
|
|
|
16
18
|
export interface ContactDetail {
|
|
17
19
|
/** What the detail is (Phone, Email); a source's icon list often has none. */
|
|
@@ -69,6 +71,7 @@ interface Props {
|
|
|
69
71
|
const { model, heading, intro, formHeading, details = [], layout, align, tone, id = 'contact' } = Astro.props
|
|
70
72
|
// Nothing to render (Studio not bound yet): a split would leave an empty column — the details stand alone.
|
|
71
73
|
const formShown = formHome(model) !== null
|
|
74
|
+
const bare = contactSectionBare({ formShown, intro, details })
|
|
72
75
|
const s = contact({ layout: formShown ? layout : 'stacked', align, tone })
|
|
73
76
|
const headingId = heading ? `${id}-title` : undefined
|
|
74
77
|
// A description list needs a term for every detail; an icon list carried from the source has values only.
|
|
@@ -77,6 +80,7 @@ const LINK = 'underline decoration-line underline-offset-4 hover:decoration-curr
|
|
|
77
80
|
// One level under the section heading, or the section's heading when there is none.
|
|
78
81
|
const FormHeading = heading ? 'h3' : 'h2'
|
|
79
82
|
---
|
|
83
|
+
{!bare && (
|
|
80
84
|
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
81
85
|
<div class={s.grid()}>
|
|
82
86
|
{(heading || intro || details.length > 0) && (
|
|
@@ -116,3 +120,4 @@ const FormHeading = heading ? 'h3' : 'h2'
|
|
|
116
120
|
: <StudioForm model={model} class={s.form()} />)}
|
|
117
121
|
</div>
|
|
118
122
|
</Section>
|
|
123
|
+
)}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// When the contact section has nothing to show. Its form is drawn only where a home for it exists (Studio bound, the
|
|
2
|
+
// WordPress page known, a service address that is https); until then the section stands on its heading, introduction
|
|
3
|
+
// and details. A section left with a heading ALONE titles a form that is not there — the page would print "Tell us about
|
|
4
|
+
// your project" over empty space — so it is not drawn at all, and appears with its form once the form has a home.
|
|
5
|
+
|
|
6
|
+
export function contactSectionBare(input: { formShown: boolean, intro?: string | undefined, details?: readonly unknown[] | undefined }): boolean {
|
|
7
|
+
return !input.formShown && !input.intro && !(input.details?.length ?? 0)
|
|
8
|
+
}
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
]
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
{ "name": "stacked-muted", "props": { "model": "contact-submissions", "heading": "Write to us", "layout": "stacked", "tone": "muted", "id": "write" } },
|
|
15
|
+
{ "name": "stacked-muted", "props": { "model": "contact-submissions", "heading": "Write to us", "intro": "We read every message and answer within two working days.", "layout": "stacked", "tone": "muted", "id": "write" } },
|
|
16
16
|
{ "name": "stacked-start", "props": { "model": "contact-submissions", "intro": "Tell us what you make and when you want to launch.", "details": [{ "value": "Studio 4, Harbour Lane" }], "layout": "stacked", "align": "start", "id": "start" } },
|
|
17
17
|
{ "name": "split-inverse", "props": { "model": "contact-submissions", "heading": "Press", "tone": "inverse", "id": "press", "details": [{ "label": "Press office", "value": "press@northwind.example", "href": "mailto:press@northwind.example" }] } },
|
|
18
|
-
{ "name": "stacked-accent", "props": { "model": "newsletter-requests", "heading": "Request a print copy", "layout": "stacked", "tone": "accent", "id": "print" } },
|
|
18
|
+
{ "name": "stacked-accent", "props": { "model": "newsletter-requests", "heading": "Request a print copy", "intro": "Print copies ship in the first week of each quarter.", "layout": "stacked", "tone": "accent", "id": "print" } },
|
|
19
19
|
{ "name": "form-heading", "props": { "model": "contact-submissions", "heading": "Contact", "intro": "Tell us where you are and what you need.", "formHeading": "Tell us about your project", "details": [{ "label": "Address", "value": "12 Harbour Street, Leith" }] } }
|
|
20
20
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "contact-form",
|
|
3
3
|
"name": "Contact form",
|
|
4
|
-
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project. When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
|
|
4
|
+
"description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project (a section left with only its heading is not drawn: the heading would title a form that is not there). When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
|
|
5
5
|
"category": "section",
|
|
6
6
|
"uses": [],
|
|
7
7
|
"shared": ["Section.astro"],
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
"divi": ["divi/et_pb_contact_form"],
|
|
27
27
|
"classic": ["classic/contact-form-7", "classic/wpforms", "classic/gravityforms", "classic/ninja-forms"]
|
|
28
28
|
},
|
|
29
|
-
"a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading."
|
|
29
|
+
"a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading. Without a form to show and with no introduction or details, the section is not drawn at all."
|
|
30
30
|
}
|
|
@@ -64,7 +64,8 @@ const { heading, intro, items, columns, style = 'icon', tone, id } = Astro.props
|
|
|
64
64
|
const s = list({ columns, style, tone })
|
|
65
65
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
66
66
|
const pathOf = (item: Item) => (style !== 'check' && item.icon && icons[item.icon]) || icons.check
|
|
67
|
-
|
|
67
|
+
// A plain list's items are not headings; otherwise one level under the section heading, or the section's when there is none.
|
|
68
|
+
const Title = style === 'list' ? 'span' : heading ? 'h3' : 'h2'
|
|
68
69
|
---
|
|
69
70
|
<Section tone={tone} id={id} aria-labelledby={headingId}>
|
|
70
71
|
{(heading || intro) && (
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"divi/et_pb_blurb"
|
|
107
107
|
]
|
|
108
108
|
},
|
|
109
|
-
"a11y": "A list; icons are decorative (aria-hidden), the title and text carry the meaning. The section is labelled by its heading.",
|
|
109
|
+
"a11y": "A list; icons are decorative (aria-hidden), the title and text carry the meaning. Each title is an <h3> under the section's <h2>, or an <h2> when the section has no heading (the list style's are plain text). The section is labelled by its heading.",
|
|
110
110
|
"pattern": {
|
|
111
111
|
"summary": "repeated icon + short title + line of text",
|
|
112
112
|
"signals": [
|
|
@@ -6,6 +6,7 @@ import Button from '../_shared/Button.astro'
|
|
|
6
6
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
7
|
import Section from '../_shared/Section.astro'
|
|
8
8
|
import type { ActionInput, ImageInput } from '../_shared/types'
|
|
9
|
+
import { coverHeight as heightOf, headingStyle as styleOf } from './hero'
|
|
9
10
|
|
|
10
11
|
const hero = tv({
|
|
11
12
|
slots: {
|
|
@@ -36,6 +37,8 @@ const hero = tv({
|
|
|
36
37
|
// How wide a cover runs: the full window, or the reading column — a WordPress cover block that is not
|
|
37
38
|
// alignfull sits in the content column, the page title above it.
|
|
38
39
|
width: { full: {}, content: {} },
|
|
40
|
+
// The heading's size: the layout's own scale, or the site's size for its level (set inline, see ./hero).
|
|
41
|
+
headingScale: { default: {}, theme: {} },
|
|
39
42
|
},
|
|
40
43
|
compoundVariants: [
|
|
41
44
|
// After the layout's own heights, so a boxed cover keeps WordPress's 430px default.
|
|
@@ -45,7 +48,7 @@ const hero = tv({
|
|
|
45
48
|
{ leadAlign: 'start', class: { lead: 'mx-0 max-w-none self-stretch text-start' } },
|
|
46
49
|
{ actionsAlign: 'start', class: { actions: 'justify-start self-stretch' } },
|
|
47
50
|
],
|
|
48
|
-
defaultVariants: { layout: 'split', tone: 'default', align: 'center', leadAlign: 'inherit', actionsAlign: 'inherit', width: 'full' },
|
|
51
|
+
defaultVariants: { layout: 'split', tone: 'default', align: 'center', leadAlign: 'inherit', actionsAlign: 'inherit', width: 'full', headingScale: 'default' },
|
|
49
52
|
})
|
|
50
53
|
|
|
51
54
|
interface Props {
|
|
@@ -67,13 +70,20 @@ interface Props {
|
|
|
67
70
|
width?: 'full' | 'content' | undefined
|
|
68
71
|
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
69
72
|
id?: string | undefined
|
|
73
|
+
/** A cover's own height in pixels, as the source sets it (`520`); absent, the layout's default. Layout cover only. */
|
|
74
|
+
minHeight?: number | undefined
|
|
75
|
+
/** `theme`: the heading takes the site's size for its level (the source's own `h1` size), not the layout's scale. */
|
|
76
|
+
headingScale?: 'default' | 'theme' | undefined
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
const
|
|
79
|
+
|
|
80
|
+
const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id, minHeight, headingScale } = Astro.props
|
|
81
|
+
const s = hero({ layout, align, leadAlign, actionsAlign, width, tone, headingScale })
|
|
74
82
|
const Heading = level === 1 ? 'h1' : 'h2'
|
|
75
83
|
const headingId = id ? `${id}-title` : undefined
|
|
76
84
|
const cover = layout === 'cover' && image
|
|
85
|
+
const coverHeight = cover ? heightOf(minHeight) : undefined
|
|
86
|
+
const headingStyle = styleOf(headingScale, level)
|
|
77
87
|
---
|
|
78
88
|
{cover && width === 'content'
|
|
79
89
|
? (
|
|
@@ -82,10 +92,10 @@ const cover = layout === 'cover' && image
|
|
|
82
92
|
<KitImage image={image} priority sizes="(min-width: 768px) 46rem, 100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
83
93
|
<div class="absolute inset-0 -z-10 bg-ink/60" aria-hidden="true" />
|
|
84
94
|
<div class="px-6 py-10 md:px-10">
|
|
85
|
-
<div class={s.grid()}>
|
|
95
|
+
<div class={s.grid()} style={coverHeight ? `min-height: ${coverHeight}` : undefined}>
|
|
86
96
|
<div class={s.copy()}>
|
|
87
97
|
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
88
|
-
<Heading id={headingId} class={s.heading()}>{heading}</Heading>
|
|
98
|
+
<Heading id={headingId} class={s.heading()} style={headingStyle}>{heading}</Heading>
|
|
89
99
|
{lead && <p class={s.lead()} data-kit-text>{lead}</p>}
|
|
90
100
|
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
91
101
|
</div>
|
|
@@ -100,10 +110,10 @@ const cover = layout === 'cover' && image
|
|
|
100
110
|
<KitImage image={image} priority sizes="100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
101
111
|
<div class="absolute inset-0 -z-10 bg-ink/60" aria-hidden="true" />
|
|
102
112
|
<div class="container-page py-16 md:py-28">
|
|
103
|
-
<div class={s.grid()}>
|
|
113
|
+
<div class={s.grid()} style={coverHeight ? `min-height: ${coverHeight}` : undefined}>
|
|
104
114
|
<div class={s.copy()}>
|
|
105
115
|
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
106
|
-
<Heading id={headingId} class={s.heading()}>{heading}</Heading>
|
|
116
|
+
<Heading id={headingId} class={s.heading()} style={headingStyle}>{heading}</Heading>
|
|
107
117
|
{lead && <p class={s.lead()} data-kit-text>{lead}</p>}
|
|
108
118
|
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
109
119
|
</div>
|
|
@@ -117,7 +127,7 @@ const cover = layout === 'cover' && image
|
|
|
117
127
|
<div class={s.grid({ class: image ? undefined : 'md:grid-cols-1' })}>
|
|
118
128
|
<div class={s.copy()}>
|
|
119
129
|
{eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
|
|
120
|
-
<Heading id={headingId} class={s.heading()}>{heading}</Heading>
|
|
130
|
+
<Heading id={headingId} class={s.heading()} style={headingStyle}>{heading}</Heading>
|
|
121
131
|
{lead && <p class={s.lead()} data-kit-text>{lead}</p>}
|
|
122
132
|
{actions.length > 0 && <div class={s.actions()}>{actions.map(action => <Button action={action} size="lg" />)}</div>}
|
|
123
133
|
</div>
|
|
@@ -167,5 +167,29 @@
|
|
|
167
167
|
"level": 2,
|
|
168
168
|
"actionsAlign": "start"
|
|
169
169
|
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "cover-content-theme-size",
|
|
173
|
+
"props": {
|
|
174
|
+
"heading": "Independent cinema, a neighbourhood hall",
|
|
175
|
+
"lead": "A hundred and twenty seats, fresh popcorn.",
|
|
176
|
+
"layout": "cover",
|
|
177
|
+
"actions": [
|
|
178
|
+
{
|
|
179
|
+
"label": "This week's programme",
|
|
180
|
+
"href": "/programme/"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"image": {
|
|
184
|
+
"src": "/kit-fixtures/photo-1.svg",
|
|
185
|
+
"alt": "",
|
|
186
|
+
"width": 1600,
|
|
187
|
+
"height": 900
|
|
188
|
+
},
|
|
189
|
+
"width": "content",
|
|
190
|
+
"level": 2,
|
|
191
|
+
"minHeight": 520,
|
|
192
|
+
"headingScale": "theme"
|
|
193
|
+
}
|
|
170
194
|
}
|
|
171
195
|
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// A cover's height reaches the style attribute only as a whole number of pixels: a value from a plan is data, not CSS.
|
|
2
|
+
export const coverHeight = (value: number | undefined): string | undefined => (Number.isInteger(value) && value! >= 100 && value! <= 2000 ? `${value}px` : undefined)
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `theme`: the heading's size is the site's for its level, the one the source's own `h1`/`h2` has (`--text-heading-1`,
|
|
6
|
+
* fluid where the theme's is); the layout's scale is the fallback while the site declares none.
|
|
7
|
+
*/
|
|
8
|
+
export const headingStyle = (scale: 'default' | 'theme' | undefined, level: 1 | 2): string | undefined =>
|
|
9
|
+
scale === 'theme' ? `font-size: var(--text-heading-${level}, var(--text-5xl)); line-height: var(--leading-heading, 1.2)` : undefined
|
|
@@ -61,6 +61,14 @@
|
|
|
61
61
|
],
|
|
62
62
|
"default": "full",
|
|
63
63
|
"description": "Layout cover only. full: the image runs the full window width. content: the cover sits in the reading column, as a WordPress cover block that is not alignfull does under the page title."
|
|
64
|
+
},
|
|
65
|
+
"headingScale": {
|
|
66
|
+
"options": [
|
|
67
|
+
"default",
|
|
68
|
+
"theme"
|
|
69
|
+
],
|
|
70
|
+
"default": "default",
|
|
71
|
+
"description": "default: the layout's own type scale. theme: the heading takes the site's size for its level (`--text-heading-1` for the page-opening hero, `-2` otherwise), as the source's own h1 does under a block theme."
|
|
64
72
|
}
|
|
65
73
|
},
|
|
66
74
|
"props": {
|
|
@@ -159,6 +167,13 @@
|
|
|
159
167
|
"type": "string",
|
|
160
168
|
"content": false,
|
|
161
169
|
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
170
|
+
},
|
|
171
|
+
"minHeight": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"content": false,
|
|
174
|
+
"min": 100,
|
|
175
|
+
"max": 2000,
|
|
176
|
+
"description": "Layout cover only: the cover's own height in pixels, as the source sets it (520); absent, the layout's default."
|
|
162
177
|
}
|
|
163
178
|
},
|
|
164
179
|
"slots": {},
|
|
@@ -61,6 +61,8 @@ interface Props {
|
|
|
61
61
|
const { heading, intro, plans, featuredLabel = 'Most popular', columns, tone, id } = Astro.props
|
|
62
62
|
const s = pricing({ columns, tone })
|
|
63
63
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
64
|
+
// One level under the section heading, or the section's heading when there is none.
|
|
65
|
+
const PlanHeading = heading ? 'h3' : 'h2'
|
|
64
66
|
const features = await Promise.all(plans.map(plan => renderMarkdown(plan.features)))
|
|
65
67
|
---
|
|
66
68
|
{plans.length > 0 && (
|
|
@@ -75,7 +77,7 @@ const features = await Promise.all(plans.map(plan => renderMarkdown(plan.feature
|
|
|
75
77
|
{plans.map((plan, i) => (
|
|
76
78
|
<li class={s.plan({ class: plan.featured ? 'ring-2 ring-accent shadow-lg md:-my-2 md:py-10' : undefined })}>
|
|
77
79
|
{plan.featured && <p class={s.badge()}>{featuredLabel}</p>}
|
|
78
|
-
<
|
|
80
|
+
<PlanHeading class={s.name()}>{plan.name}</PlanHeading>
|
|
79
81
|
<p class={s.price()}>
|
|
80
82
|
<span class={s.amount()}>{plan.price}</span>
|
|
81
83
|
{plan.period && <span class={s.period()}>{plan.period}</span>}
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"divi/et_pb_pricing_tables"
|
|
121
121
|
]
|
|
122
122
|
},
|
|
123
|
-
"a11y": "Plans are a <ul>; each name is an <h3
|
|
123
|
+
"a11y": "Plans are a <ul>; each name is an <h3> under the section's <h2>, or an <h2> when the section has no heading. A featured plan is marked by a text badge and a ring, not colour alone. Buttons are links with their own label.",
|
|
124
124
|
"pattern": {
|
|
125
125
|
"summary": "repeated plan units: name + price + feature list + button",
|
|
126
126
|
"signals": [
|
|
@@ -60,6 +60,8 @@ interface Props {
|
|
|
60
60
|
const { heading, intro, items, layout, style, id } = Astro.props
|
|
61
61
|
const s = steps({ layout, style })
|
|
62
62
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
63
|
+
// One level under the section heading, or the section's heading when there is none.
|
|
64
|
+
const StepHeading = heading ? 'h3' : 'h2'
|
|
63
65
|
---
|
|
64
66
|
{items.length > 0 && (
|
|
65
67
|
<Section id={id} aria-labelledby={headingId}>
|
|
@@ -75,7 +77,7 @@ const headingId = heading && id ? `${id}-title` : undefined
|
|
|
75
77
|
{step.number && <span class={s.badge()} aria-hidden="true">{step.number}</span>}
|
|
76
78
|
<div class={s.body()}>
|
|
77
79
|
{imageOf(step.image, step.imageAlt) && <div class={s.media()}><KitImage image={imageOf(step.image, step.imageAlt)!} sizes="(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw" class="aspect-[3/2] w-full object-cover" /></div>}
|
|
78
|
-
<
|
|
80
|
+
<StepHeading class={s.title()}>{step.title}</StepHeading>
|
|
79
81
|
<p class={s.text()} data-kit-text>{step.text}</p>
|
|
80
82
|
</div>
|
|
81
83
|
</li>
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"divi/et_pb_blurb"
|
|
98
98
|
]
|
|
99
99
|
},
|
|
100
|
-
"a11y": "Steps are an <ol>, so their order and count are announced; the visible numbers are CSS counters, not text. Each title is an <h3
|
|
100
|
+
"a11y": "Steps are an <ol>, so their order and count are announced; the visible numbers are CSS counters, not text. Each title is an <h3> under the section's <h2>, or an <h2> when the section has no heading. The section is labelled by its heading when it has one.",
|
|
101
101
|
"pattern": {
|
|
102
102
|
"summary": "ordered sequence: numbered title + short text units",
|
|
103
103
|
"signals": [
|
|
@@ -52,6 +52,8 @@ interface Props {
|
|
|
52
52
|
const { items, heading, label, style, orientation = 'horizontal', tone, id = 'tabs' } = Astro.props
|
|
53
53
|
const s = tabs({ style, orientation, tone })
|
|
54
54
|
const headingId = heading ? `${id}-title` : undefined
|
|
55
|
+
// One level under the section heading, or the section's heading when there is none.
|
|
56
|
+
const PanelHeading = heading ? 'h3' : 'h2'
|
|
55
57
|
const value = (i: number) => `tab-${i + 1}`
|
|
56
58
|
---
|
|
57
59
|
{items.length > 0 && (
|
|
@@ -63,7 +65,7 @@ const value = (i: number) => `tab-${i + 1}`
|
|
|
63
65
|
</div>
|
|
64
66
|
{items.map((item, i) => (
|
|
65
67
|
<div data-part="content" data-value={value(i)} class="min-w-0 flex-1">
|
|
66
|
-
<
|
|
68
|
+
<PanelHeading class={s.title()}>{item.label}</PanelHeading>
|
|
67
69
|
<div class={s.panel()} set:html={item.content} />
|
|
68
70
|
</div>
|
|
69
71
|
))}
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"divi": ["divi/et_pb_tabs"],
|
|
25
25
|
"classic": ["classic/tabs"]
|
|
26
26
|
},
|
|
27
|
-
"a11y": "Zag.js tabs: role=tablist/tab/tabpanel with aria-selected, aria-controls and aria-labelledby; arrow keys (per orientation), Home and End move between tabs. Before the script runs every panel is visible with its own h3
|
|
27
|
+
"a11y": "Zag.js tabs: role=tablist/tab/tabpanel with aria-selected, aria-controls and aria-labelledby; arrow keys (per orientation), Home and End move between tabs. Before the script runs every panel is visible with its own title (an h3 under the section's h2, an h2 when there is no heading); after, the titles become screen-reader-only and the tabs label the panels."
|
|
28
28
|
}
|
|
@@ -57,6 +57,8 @@ interface Props {
|
|
|
57
57
|
const { heading, intro, items, columns, style, roleStyle, id } = Astro.props
|
|
58
58
|
const s = team({ columns, style, roleStyle })
|
|
59
59
|
const headingId = heading && id ? `${id}-title` : undefined
|
|
60
|
+
// One level under the section heading, or the section's heading when there is none.
|
|
61
|
+
const NameHeading = heading ? 'h3' : 'h2'
|
|
60
62
|
---
|
|
61
63
|
{items.length > 0 && (
|
|
62
64
|
<Section id={id} aria-labelledby={headingId}>
|
|
@@ -71,11 +73,11 @@ const headingId = heading && id ? `${id}-title` : undefined
|
|
|
71
73
|
<li class={s.member()}>
|
|
72
74
|
{/* The portrait's text defaults to the name: a photo of a named person is not decoration. */}
|
|
73
75
|
{imageOf(member.image, member.imageAlt ?? member.name) && <div class={s.portrait()}><KitImage image={imageOf(member.image, member.imageAlt ?? member.name)!} sizes="(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw" class="aspect-square w-full object-cover" /></div>}
|
|
74
|
-
<
|
|
76
|
+
<NameHeading class={s.name()}>
|
|
75
77
|
{member.href
|
|
76
78
|
? <a href={member.href} class="text-current no-underline after:absolute after:inset-0 after:content-[''] hover:text-accent">{member.name}</a>
|
|
77
79
|
: member.name}
|
|
78
|
-
</
|
|
80
|
+
</NameHeading>
|
|
79
81
|
{member.role && <p class={s.role()}>{member.role}</p>}
|
|
80
82
|
{member.bio && <p class={s.bio()} data-kit-text>{member.bio}</p>}
|
|
81
83
|
</li>
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"divi/et_pb_team_member"
|
|
111
111
|
]
|
|
112
112
|
},
|
|
113
|
-
"a11y": "Members are a <ul>; each name is an <h3
|
|
113
|
+
"a11y": "Members are a <ul>; each name is an <h3> under the section's <h2> (an <h2> when the section has no heading), and a linked member's link text is the name. A portrait's text defaults to the name. The section is labelled by its heading when it has one.",
|
|
114
114
|
"pattern": {
|
|
115
115
|
"summary": "repeated person units: portrait + name + role",
|
|
116
116
|
"signals": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/astro-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"mapping"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@contentrain/types": "1.
|
|
45
|
+
"@contentrain/types": "1.41.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.10.0",
|