@contentrain/astro-kit 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/catalog.json +99 -18
- package/components/_shared/Section.astro +13 -9
- package/components/_shared/SectionHeader.astro +3 -2
- package/components/_shared/behaviors.ts +38 -2
- package/components/_shared/embed-frame.ts +22 -0
- package/components/{embed/embed.ts → _shared/embed-target.ts} +30 -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 +15 -2
- package/components/behavior/fixtures.json +5 -3
- package/components/behavior/meta.json +9 -0
- package/components/card-grid/CardGrid.astro +13 -9
- package/components/card-grid/meta.json +7 -0
- package/components/contact-details/ContactDetails.astro +7 -3
- package/components/contact-details/meta.json +2 -0
- package/components/contact-form/ContactForm.astro +11 -7
- package/components/contact-form/meta.json +2 -0
- package/components/cta/Cta.astro +14 -10
- package/components/cta/meta.json +2 -2
- package/components/dialog/Dialog.astro +6 -3
- package/components/dialog/meta.json +2 -0
- package/components/embed/Embed.astro +7 -10
- package/components/embed/meta.json +1 -1
- package/components/faq/Faq.astro +8 -4
- package/components/faq/meta.json +1 -1
- package/components/feature-list/FeatureList.astro +10 -6
- package/components/feature-list/meta.json +3 -1
- package/components/footer/Footer.astro +4 -3
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +8 -4
- package/components/gallery/meta.json +2 -0
- package/components/header/Header.astro +8 -4
- package/components/header/meta.json +1 -1
- package/components/hero/Hero.astro +13 -9
- package/components/hero/meta.json +3 -1
- package/components/language-switcher/LanguageSwitcher.astro +3 -2
- package/components/language-switcher/meta.json +1 -1
- package/components/logo-cloud/LogoCloud.astro +9 -5
- package/components/logo-cloud/meta.json +3 -1
- package/components/nav/Nav.astro +10 -6
- package/components/nav/meta.json +2 -0
- package/components/newsletter/Newsletter.astro +9 -5
- package/components/newsletter/meta.json +2 -0
- package/components/post-list/PostList.astro +7 -3
- package/components/post-list/meta.json +2 -0
- package/components/pricing/Pricing.astro +15 -11
- package/components/pricing/meta.json +3 -1
- package/components/search-box/SearchBox.astro +4 -1
- package/components/search-box/meta.json +1 -1
- package/components/slider/Slider.astro +9 -2
- package/components/slider/SliderTrack.astro +71 -15
- package/components/slider/fixtures.json +2 -1
- package/components/slider/meta.json +5 -4
- package/components/social-links/SocialLinks.astro +2 -1
- package/components/social-links/meta.json +3 -2
- package/components/split/Split.astro +9 -5
- package/components/split/meta.json +3 -1
- package/components/stats/Stats.astro +11 -7
- package/components/stats/meta.json +5 -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/mapping/classic.json +14 -7
- package/mapping/divi.json +6 -3
- package/mapping/elementor.json +14 -7
- package/mapping/gutenberg.json +2 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { tv } from 'tailwind-variants'
|
|
9
9
|
import KitImage from '../_shared/KitImage.astro'
|
|
10
10
|
import type { ImageInput } from '../_shared/types'
|
|
11
|
-
import { embedTarget, mapTarget } from '
|
|
11
|
+
import { embedTarget, mapTarget } from '../_shared/embed-target'
|
|
12
12
|
|
|
13
13
|
const embed = tv({
|
|
14
14
|
slots: {
|
|
@@ -84,20 +84,17 @@ const linkUrl = url && /^https?:\/\//i.test(url) ? url : undefined
|
|
|
84
84
|
)}
|
|
85
85
|
|
|
86
86
|
<script>
|
|
87
|
+
import { playerFrame, plainClick } from '../_shared/embed-frame'
|
|
88
|
+
|
|
87
89
|
// Click (or Enter) on the poster: the player replaces it, focused, so keyboard and screen-reader users land in it.
|
|
88
90
|
// A modified click (new tab, new window) keeps the link's own behaviour.
|
|
89
91
|
document.addEventListener('click', (event) => {
|
|
90
|
-
if (event
|
|
92
|
+
if (!plainClick(event)) return
|
|
91
93
|
const link = (event.target as Element | null)?.closest<HTMLAnchorElement>('[data-kit-embed] a[data-embed-src]')
|
|
92
|
-
|
|
94
|
+
// An address that is no known player stays what it is: a link.
|
|
95
|
+
const frame = link && playerFrame(link.dataset.embedSrc, link.dataset.embedTitle ?? '')
|
|
96
|
+
if (!link || !frame) return
|
|
93
97
|
event.preventDefault()
|
|
94
|
-
const frame = document.createElement('iframe')
|
|
95
|
-
frame.src = link.dataset.embedSrc
|
|
96
|
-
frame.title = link.dataset.embedTitle ?? ''
|
|
97
|
-
frame.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write'
|
|
98
|
-
frame.allowFullscreen = true
|
|
99
|
-
frame.referrerPolicy = 'strict-origin-when-cross-origin'
|
|
100
|
-
frame.className = 'absolute inset-0 h-full w-full border-0'
|
|
101
98
|
link.replaceWith(frame)
|
|
102
99
|
frame.focus()
|
|
103
100
|
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "A video, map or player from another site (YouTube, Vimeo, Google Maps, OpenStreetMap, Spotify, SoundCloud, Calendly, Typeform, Google Forms, Loom, Wistia), loaded only on click: until then a poster link to the provider's page, with no iframe, no third-party request and no cookie (YouTube through youtube-nocookie.com). Unknown providers are never framed; they stay a plain link.",
|
|
5
5
|
"category": "content",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["KitImage.astro", "safe-url.ts", "types.ts"],
|
|
7
|
+
"shared": ["KitImage.astro", "embed-frame.ts", "embed-target.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"aspect": { "options": ["auto", "video", "portrait", "square", "map"], "default": "auto", "description": "auto: 16:9 for videos, 9:16 for YouTube Shorts, 4:3 (16:9 wide) for maps, a fixed strip for audio players. The others force a ratio." },
|
|
10
10
|
"width": { "options": ["prose", "page"], "default": "prose", "description": "prose: as wide as the reading column. page: the full page frame." }
|
package/components/faq/Faq.astro
CHANGED
|
@@ -7,6 +7,8 @@ import { tv } from 'tailwind-variants'
|
|
|
7
7
|
import Section from '../_shared/Section.astro'
|
|
8
8
|
import { measuredColumns } from '../_shared/measured'
|
|
9
9
|
import type { MeasuredStyle } from '../_shared/types'
|
|
10
|
+
import { instanceId } from '../_shared/instance-id'
|
|
11
|
+
import { muted } from '../_shared/muted'
|
|
10
12
|
|
|
11
13
|
export interface FaqItem {
|
|
12
14
|
question: string
|
|
@@ -19,7 +21,7 @@ const faq = tv({
|
|
|
19
21
|
root: '',
|
|
20
22
|
head: 'mb-8 flex flex-col gap-3',
|
|
21
23
|
heading: 'text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
22
|
-
intro:
|
|
24
|
+
intro: `max-w-2xl text-lg ${muted}`,
|
|
23
25
|
list: 'flex flex-col',
|
|
24
26
|
item: 'group',
|
|
25
27
|
question: '',
|
|
@@ -37,7 +39,7 @@ const faq = tv({
|
|
|
37
39
|
columns: { '1': {}, '2': { list: 'lg:block lg:columns-2 lg:gap-x-12', item: 'break-inside-avoid' } },
|
|
38
40
|
// The marker in each question's row: a plus that turns into a cross, or a chevron that turns over.
|
|
39
41
|
icon: { plus: {}, chevron: { marker: 'group-open:rotate-180' } },
|
|
40
|
-
numbered: { true: { list: '[counter-reset:faq]', item: '[counter-increment:faq]', question: 'before:me-4 before:tabular-nums before:
|
|
42
|
+
numbered: { true: { list: '[counter-reset:faq]', item: '[counter-increment:faq]', question: 'before:me-4 before:tabular-nums before:text-[color:var(--kit-muted,var(--color-ink-muted))] before:content-[counter(faq,decimal-leading-zero)]' }, false: {} },
|
|
41
43
|
style: {
|
|
42
44
|
lines: { list: 'divide-y divide-line border-y border-line' },
|
|
43
45
|
cards: { list: 'gap-3', item: 'rounded-card border border-line px-5', summary: 'py-5' },
|
|
@@ -84,7 +86,9 @@ interface Props {
|
|
|
84
86
|
id?: string | undefined
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const { items, heading, intro, layout, columns, icon, numbered = false, style, questionStyle, exclusive = false, openFirst = false, schema = false, tone, measured, id
|
|
89
|
+
const { items, heading, intro, layout, columns, icon, numbered = false, style, questionStyle, exclusive = false, openFirst = false, schema = false, tone, measured, id: givenId } = Astro.props
|
|
90
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
91
|
+
const id = instanceId(Astro.locals, givenId, 'faq')
|
|
88
92
|
const s = faq({ layout, columns, icon, numbered, style, questionStyle: style === 'plain' ? 'heading' : questionStyle, tone })
|
|
89
93
|
// Two columns and the split layout need the page's width; one column reads in the narrow one.
|
|
90
94
|
const measuredCols = measuredColumns(measured)
|
|
@@ -103,7 +107,7 @@ const structured = schema && items.length > 0
|
|
|
103
107
|
: null
|
|
104
108
|
---
|
|
105
109
|
{items.length > 0 && (
|
|
106
|
-
<Section measured={measured} tone={tone} spacing="md" width={wide ? 'page' : 'prose'} id={id} aria-labelledby={headingId}>
|
|
110
|
+
<Section kit="faq" measured={measured} tone={tone} spacing="md" width={wide ? 'page' : 'prose'} id={id} aria-labelledby={headingId}>
|
|
107
111
|
<div class={s.root()}>
|
|
108
112
|
{(heading || intro) && (
|
|
109
113
|
<header class={s.head()}>
|
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", "measured.ts", "safe-url.ts", "types.ts"],
|
|
7
|
+
"shared": ["Section.astro", "instance-id.ts", "measured.ts", "muted.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["single", "split"], "default": "single", "description": "single: the heading above one column. split: the heading and introduction stay in a left column beside the list; stacks on a phone." },
|
|
10
10
|
"columns": { "options": ["1", "2"], "default": "1", "description": "Columns of questions on wide screens." },
|
|
@@ -4,8 +4,10 @@
|
|
|
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'
|
|
10
|
+
import { muted } from '../_shared/muted'
|
|
9
11
|
|
|
10
12
|
const list = tv({
|
|
11
13
|
slots: {
|
|
@@ -13,7 +15,7 @@ const list = tv({
|
|
|
13
15
|
item: 'flex gap-4',
|
|
14
16
|
mark: 'flex size-6 shrink-0 items-center justify-center text-accent-text',
|
|
15
17
|
title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
|
|
16
|
-
text:
|
|
18
|
+
text: `mt-1 ${muted}`,
|
|
17
19
|
},
|
|
18
20
|
variants: {
|
|
19
21
|
columns: {
|
|
@@ -28,7 +30,7 @@ const list = tv({
|
|
|
28
30
|
list: { grid: 'flex flex-col gap-3', item: 'items-center gap-3', mark: 'size-5', title: 'text-[length:var(--text-body,1rem)] leading-normal [font-weight:inherit] text-pretty', text: 'mt-0' },
|
|
29
31
|
},
|
|
30
32
|
// On accent the mark takes the ink colour, and text is never dimmed.
|
|
31
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: { mark: 'text-current'
|
|
33
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: { mark: 'text-current' } },
|
|
32
34
|
// A list inside another section's column: one column, tighter, no section around it.
|
|
33
35
|
bare: { true: { grid: 'sm:grid-cols-1 gap-y-5' }, false: {} },
|
|
34
36
|
},
|
|
@@ -73,9 +75,11 @@ interface Props {
|
|
|
73
75
|
measured?: MeasuredStyle | undefined
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
const { heading, intro, items, columns, style = 'icon', tone, bare = false, titleLevel = 3, id, measured } = Astro.props
|
|
78
|
+
const { heading, intro, items, columns, style = 'icon', tone, bare = false, titleLevel = 3, id: givenId, measured } = Astro.props
|
|
79
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `feature-list`, `feature-list-2`, … in render order.
|
|
80
|
+
const id = instanceId(Astro.locals, givenId, 'feature-list')
|
|
77
81
|
const s = list({ columns, style, tone, bare })
|
|
78
|
-
const headingId = heading
|
|
82
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
79
83
|
const pathOf = (item: Item) => (style !== 'check' && item.icon && icons[item.icon]) || icons.check
|
|
80
84
|
// The feature's own image icon, where the style shows each feature's own mark.
|
|
81
85
|
const imageOf = (item: Item) => (style !== 'check' && item.iconImage) || undefined
|
|
@@ -101,11 +105,11 @@ const Title = style === 'list' ? 'span' : bare ? `h${titleLevel}` as 'h3' : head
|
|
|
101
105
|
</ul>
|
|
102
106
|
)
|
|
103
107
|
: (
|
|
104
|
-
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
108
|
+
<Section kit="feature-list" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
105
109
|
{(heading || intro) && (
|
|
106
110
|
<header class="mb-10 flex max-w-2xl flex-col gap-3">
|
|
107
111
|
{heading && <h2 id={headingId} class="text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl">{heading}</h2>}
|
|
108
|
-
{intro && <p class:list={['text-lg',
|
|
112
|
+
{intro && <p class:list={['text-lg', muted]} data-kit-text>{intro}</p>}
|
|
109
113
|
</header>
|
|
110
114
|
)}
|
|
111
115
|
<ul class={s.grid({ class: measuredGrid(measured) })}>
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
"shared": [
|
|
8
8
|
"KitImage.astro",
|
|
9
9
|
"Section.astro",
|
|
10
|
+
"instance-id.ts",
|
|
10
11
|
"measured.ts",
|
|
12
|
+
"muted.ts",
|
|
11
13
|
"safe-url.ts",
|
|
12
14
|
"types.ts"
|
|
13
15
|
],
|
|
@@ -219,7 +221,7 @@
|
|
|
219
221
|
"id": {
|
|
220
222
|
"type": "string",
|
|
221
223
|
"content": false,
|
|
222
|
-
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
224
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `feature-list`, `feature-list-2`, … in render order, so every section on a page has its own."
|
|
223
225
|
}
|
|
224
226
|
},
|
|
225
227
|
"slots": {},
|
|
@@ -5,13 +5,14 @@ import { tv } from 'tailwind-variants'
|
|
|
5
5
|
import type { LinkInput } from '../_shared/types'
|
|
6
6
|
import { contactKeys, footerLinkSizeClasses, footerVars, type ContactInput } from './footer'
|
|
7
7
|
import { safeHref } from '../_shared/safe-url'
|
|
8
|
+
import { muted as quiet, mutedInverse } from '../_shared/muted'
|
|
8
9
|
|
|
9
10
|
const footer = tv({
|
|
10
11
|
slots: {
|
|
11
12
|
root: 'mt-auto',
|
|
12
13
|
inner: 'container-page py-10 text-sm',
|
|
13
|
-
muted:
|
|
14
|
-
link:
|
|
14
|
+
muted: quiet,
|
|
15
|
+
link: `no-underline ${quiet} hover:text-current hover:underline`,
|
|
15
16
|
},
|
|
16
17
|
variants: {
|
|
17
18
|
layout: {
|
|
@@ -21,7 +22,7 @@ const footer = tv({
|
|
|
21
22
|
tone: {
|
|
22
23
|
default: { root: 'bg-surface text-ink' },
|
|
23
24
|
muted: { root: 'bg-surface-muted text-ink' },
|
|
24
|
-
inverse: { root:
|
|
25
|
+
inverse: { root: `bg-ink text-surface ${mutedInverse}` },
|
|
25
26
|
},
|
|
26
27
|
// The border on the footer's top side: the default tone's own unless `border` says otherwise.
|
|
27
28
|
border: { on: { root: 'border-t border-line' }, off: {} },
|
|
@@ -12,6 +12,8 @@ import type { MeasuredStyle } from '../_shared/types'
|
|
|
12
12
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
13
13
|
import { type ImageInput } from '../_shared/types'
|
|
14
14
|
import { safeHref } from '../_shared/safe-url'
|
|
15
|
+
import { instanceId } from '../_shared/instance-id'
|
|
16
|
+
import { muted } from '../_shared/muted'
|
|
15
17
|
|
|
16
18
|
export interface GalleryItem {
|
|
17
19
|
/** The image's address (or, until 0.7, an `ImageInput`). */
|
|
@@ -29,7 +31,7 @@ const gallery = tv({
|
|
|
29
31
|
item: 'm-0',
|
|
30
32
|
frame: 'group relative block w-full cursor-zoom-in overflow-hidden rounded-[var(--radius-image,var(--radius-card))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
31
33
|
thumb: 'h-full w-full object-cover transition-transform duration-300 hover:scale-[1.02]',
|
|
32
|
-
caption:
|
|
34
|
+
caption: `mt-2 text-sm ${muted}`,
|
|
33
35
|
viewer: 'm-auto max-h-[92vh] max-w-[92vw] bg-transparent p-0 backdrop:bg-ink/85',
|
|
34
36
|
},
|
|
35
37
|
variants: {
|
|
@@ -85,14 +87,16 @@ interface Props {
|
|
|
85
87
|
id?: string | undefined
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const { items: given, heading, layout, columns, aspect, gap, captionStyle, hoverEffect, enlargeLabel = 'Enlarge', regionLabel = 'Images', open = 'popover', closeLabel = 'Close', tone, measured, id
|
|
90
|
+
const { items: given, heading, layout, columns, aspect, gap, captionStyle, hoverEffect, enlargeLabel = 'Enlarge', regionLabel = 'Images', open = 'popover', closeLabel = 'Close', tone, measured, id: givenId } = Astro.props
|
|
91
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
92
|
+
const id = instanceId(Astro.locals, givenId, 'gallery')
|
|
89
93
|
const items = given.flatMap(item => { const image = imageOf(item.image, item.imageAlt); return image ? [{ ...item, image, full: imageOf(item.full, item.imageAlt) }] : [] }) satisfies Array<{ image: ImageInput, caption?: string | undefined }>
|
|
90
94
|
const s = gallery({ layout, columns, open, aspect, gap, captionStyle, hoverEffect })
|
|
91
95
|
const headingId = heading ? `${id}-title` : undefined
|
|
92
96
|
const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 25vw, 50vw' : '(min-width: 768px) 33vw, 50vw'
|
|
93
97
|
---
|
|
94
98
|
{items.length > 0 && (
|
|
95
|
-
<Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
99
|
+
<Section kit="gallery" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
96
100
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
97
101
|
{/* A scrolling strip is a labelled, focusable region so the keyboard can scroll it. */}
|
|
98
102
|
<div tabindex={layout === 'row' ? 0 : undefined} role={layout === 'row' ? 'region' : undefined} aria-label={layout === 'row' ? heading ?? regionLabel : undefined}>
|
|
@@ -107,7 +111,7 @@ const sizes = columns === '2' ? '50vw' : columns === '4' ? '(min-width: 768px) 2
|
|
|
107
111
|
: open === 'link'
|
|
108
112
|
? <a href={safeHref(item.image.src)} class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></a>
|
|
109
113
|
: <div class={s.frame()}><KitImage image={item.image} sizes={sizes} class={s.thumb()} /></div>}
|
|
110
|
-
{item.caption && <figcaption class={s.caption(
|
|
114
|
+
{item.caption && <figcaption class={s.caption()}>{item.caption}</figcaption>}
|
|
111
115
|
</figure>
|
|
112
116
|
{open === 'popover' && (
|
|
113
117
|
<div id={viewer} popover class={s.viewer()}>
|
|
@@ -7,8 +7,10 @@ import KitImage from '../_shared/KitImage.astro'
|
|
|
7
7
|
import type { ActionInput, ImageInput, NavItem } from '../_shared/types'
|
|
8
8
|
import Nav from '../nav/Nav.astro'
|
|
9
9
|
import SearchBox from '../search-box/SearchBox.astro'
|
|
10
|
+
import { instanceId } from '../_shared/instance-id'
|
|
10
11
|
import { safeHref } from '../_shared/safe-url'
|
|
11
12
|
import { brandColorValue } from './header'
|
|
13
|
+
import { muted, mutedInverse } from '../_shared/muted'
|
|
12
14
|
|
|
13
15
|
const header = tv({
|
|
14
16
|
slots: {
|
|
@@ -24,8 +26,8 @@ const header = tv({
|
|
|
24
26
|
},
|
|
25
27
|
tone: {
|
|
26
28
|
default: { root: 'bg-surface text-ink' },
|
|
27
|
-
inverse: { root:
|
|
28
|
-
transparent: { root:
|
|
29
|
+
inverse: { root: `bg-ink text-surface ${mutedInverse}` },
|
|
30
|
+
transparent: { root: `absolute inset-x-0 top-0 bg-transparent text-surface ${mutedInverse}` },
|
|
29
31
|
},
|
|
30
32
|
position: { static: {}, sticky: { root: 'sticky top-0' } },
|
|
31
33
|
bordered: { true: {}, false: {} },
|
|
@@ -76,6 +78,8 @@ interface Props {
|
|
|
76
78
|
|
|
77
79
|
const { siteName, tagline, brandSize, brandWeight, brandTracking, bordered, offsetTop, padTop, padBottom, linkNav, brandColor, logo, showName = true, homeHref = '/', items, action, search, navLabel, menuLabel, closeLabel, layout, tone, position, menu } = Astro.props
|
|
78
80
|
const { root, inner, brand } = header({ layout, tone, position, bordered })
|
|
81
|
+
// The header's search field: `header-search` on a page, a second header's its own (its label points at its own input).
|
|
82
|
+
const searchId = search ? instanceId(Astro.locals, undefined, 'header-search') : undefined
|
|
79
83
|
const rootStyle = offsetTop ? `margin-top: ${offsetTop}` : undefined
|
|
80
84
|
const innerStyle = [padTop && `padding-top: ${padTop}`, padBottom && `padding-bottom: ${padBottom}`].filter(Boolean).join('; ') || undefined
|
|
81
85
|
const brandColour = brandColorValue(brandColor)
|
|
@@ -93,7 +97,7 @@ const wrap = search ? 'flex-wrap lg:flex-nowrap' : tagline ? 'flex-wrap md:flex-
|
|
|
93
97
|
{logo && <KitImage image={logo} priority class="h-8 w-auto" sizes="160px" />}
|
|
94
98
|
{showName && <span>{siteName}</span>}
|
|
95
99
|
</a>
|
|
96
|
-
<p class=
|
|
100
|
+
<p class={muted} data-kit-tagline>{tagline}</p>
|
|
97
101
|
</div>
|
|
98
102
|
)
|
|
99
103
|
: (
|
|
@@ -104,7 +108,7 @@ const wrap = search ? 'flex-wrap lg:flex-nowrap' : tagline ? 'flex-wrap md:flex-
|
|
|
104
108
|
)}
|
|
105
109
|
{search && (
|
|
106
110
|
<div class="order-last basis-full lg:order-none lg:ms-auto lg:basis-auto">
|
|
107
|
-
<SearchBox action={search.action} label={search.label} placeholder={search.placeholder} submitLabel={search.submitLabel} size="sm" id=
|
|
111
|
+
<SearchBox action={search.action} label={search.label} placeholder={search.placeholder} submitLabel={search.submitLabel} size="sm" id={searchId} />
|
|
108
112
|
</div>
|
|
109
113
|
)}
|
|
110
114
|
<div class:list={['flex items-center gap-4', layout === 'centered' ? 'justify-center' : 'ms-auto']}>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Site header: brand (logo and/or name), navigation and an optional call-to-action button.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": ["nav", "search-box"],
|
|
7
|
-
"shared": ["Button.astro", "KitImage.astro", "safe-url.ts", "types.ts"],
|
|
7
|
+
"shared": ["Button.astro", "KitImage.astro", "instance-id.ts", "muted.ts", "safe-url.ts", "types.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": { "options": ["split", "centered", "brand-left"], "default": "split", "description": "split: brand left, navigation right. centered: brand above centred navigation. brand-left: navigation right after the brand." },
|
|
10
10
|
"tone": { "options": ["default", "inverse", "transparent"], "default": "default", "description": "Surface with a bottom border, dark (ink) background, or transparent over the hero that follows." },
|
|
@@ -5,17 +5,19 @@ 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'
|
|
9
10
|
import { measuredFrame } from '../_shared/measured'
|
|
10
11
|
import { coverHeight as heightOf, headingStyle as styleOf } from './hero'
|
|
12
|
+
import { muted, mutedInverse } from '../_shared/muted'
|
|
11
13
|
|
|
12
14
|
const hero = tv({
|
|
13
15
|
slots: {
|
|
14
16
|
grid: 'grid items-center gap-10',
|
|
15
17
|
copy: 'flex flex-col gap-5',
|
|
16
|
-
eyebrow:
|
|
18
|
+
eyebrow: `text-sm font-semibold tracking-wide uppercase ${muted}`,
|
|
17
19
|
heading: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-5xl lg:text-6xl',
|
|
18
|
-
lead:
|
|
20
|
+
lead: `max-w-2xl text-lg text-pretty ${muted} md:text-xl`,
|
|
19
21
|
// A hero's short promise list ("Fast, easy and painless"), each with its icon when it has one.
|
|
20
22
|
points: 'flex flex-wrap gap-x-6 gap-y-2 text-base font-medium md:text-lg',
|
|
21
23
|
point: 'flex items-center gap-2',
|
|
@@ -29,7 +31,7 @@ const hero = tv({
|
|
|
29
31
|
cover: { grid: 'relative z-10 min-h-[28rem] md:min-h-[36rem]', copy: 'max-w-3xl' },
|
|
30
32
|
},
|
|
31
33
|
// 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.
|
|
32
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: {
|
|
34
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: {} },
|
|
33
35
|
// Where a cover sets its text over the image. WordPress covers centre it unless told otherwise.
|
|
34
36
|
align: { start: {}, center: {} },
|
|
35
37
|
// The lead's own alignment: `start` keeps it at the start edge, across the column, under a centred heading
|
|
@@ -89,10 +91,12 @@ interface Props {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
const { heading, level = 1, eyebrow, lead, items = [], actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id, minHeight, headingScale, headingSize, measured } = Astro.props
|
|
94
|
+
const { heading, level = 1, eyebrow, lead, items = [], actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id: givenId, minHeight, headingScale, headingSize, measured } = Astro.props
|
|
95
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `hero`, `hero-2`, … in render order.
|
|
96
|
+
const id = instanceId(Astro.locals, givenId, 'hero')
|
|
93
97
|
const s = hero({ layout, align, leadAlign, actionsAlign, width, tone, headingScale, headingSize })
|
|
94
98
|
const Heading = level === 1 ? 'h1' : 'h2'
|
|
95
|
-
const headingId =
|
|
99
|
+
const headingId = `${id}-title`
|
|
96
100
|
const cover = layout === 'cover' && image
|
|
97
101
|
const coverHeight = cover ? heightOf(minHeight) : undefined
|
|
98
102
|
const headingStyle = styleOf(headingScale, level, headingSize)
|
|
@@ -102,8 +106,8 @@ const scrim = cover ? measuredFrame(measured).overlay : undefined
|
|
|
102
106
|
---
|
|
103
107
|
{cover && width === 'content'
|
|
104
108
|
? (
|
|
105
|
-
<Section measured={sectionMeasured} tone="none" spacing="sm" width="prose" id={id} aria-labelledby={headingId}>
|
|
106
|
-
<div class=
|
|
109
|
+
<Section kit="hero" measured={sectionMeasured} tone="none" spacing="sm" width="prose" id={id} aria-labelledby={headingId}>
|
|
110
|
+
<div class={`relative isolate overflow-hidden bg-ink text-surface ${mutedInverse}`}>
|
|
107
111
|
<KitImage image={image} priority sizes="(min-width: 768px) 46rem, 100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
108
112
|
<div class:list={['absolute inset-0 -z-10', !scrim && 'bg-ink/60']} style={scrim ? `background: ${scrim}` : undefined} aria-hidden="true" />
|
|
109
113
|
<div class="px-6 py-10 md:px-10">
|
|
@@ -122,7 +126,7 @@ const scrim = cover ? measuredFrame(measured).overlay : undefined
|
|
|
122
126
|
)
|
|
123
127
|
: cover
|
|
124
128
|
? (
|
|
125
|
-
<Section measured={sectionMeasured} tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
|
|
129
|
+
<Section kit="hero" measured={sectionMeasured} tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
|
|
126
130
|
<KitImage image={image} priority sizes="100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
|
|
127
131
|
<div class:list={['absolute inset-0 -z-10', !scrim && 'bg-ink/60']} style={scrim ? `background: ${scrim}` : undefined} aria-hidden="true" />
|
|
128
132
|
<div class:list={['container-page', measured?.padY === undefined && 'py-16 md:py-28']}>
|
|
@@ -139,7 +143,7 @@ const scrim = cover ? measuredFrame(measured).overlay : undefined
|
|
|
139
143
|
</Section>
|
|
140
144
|
)
|
|
141
145
|
: (
|
|
142
|
-
<Section measured={sectionMeasured} tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
|
|
146
|
+
<Section kit="hero" measured={sectionMeasured} tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
|
|
143
147
|
{/* Without an image a split has nothing beside the text: the text takes the whole row, not half of it. */}
|
|
144
148
|
<div class={s.grid({ class: image ? undefined : 'md:grid-cols-1' })}>
|
|
145
149
|
<div class={s.copy()}>
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"Button.astro",
|
|
9
9
|
"KitImage.astro",
|
|
10
10
|
"Section.astro",
|
|
11
|
+
"instance-id.ts",
|
|
11
12
|
"measured.ts",
|
|
13
|
+
"muted.ts",
|
|
12
14
|
"safe-url.ts",
|
|
13
15
|
"types.ts"
|
|
14
16
|
],
|
|
@@ -198,7 +200,7 @@
|
|
|
198
200
|
"id": {
|
|
199
201
|
"type": "string",
|
|
200
202
|
"content": false,
|
|
201
|
-
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
203
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `hero`, `hero-2`, … in render order, so every section on a page has its own."
|
|
202
204
|
},
|
|
203
205
|
"minHeight": {
|
|
204
206
|
"type": "integer",
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
// equivalent in each language); a link without one is not drawn.
|
|
6
6
|
import { tv } from 'tailwind-variants'
|
|
7
7
|
import { safeHref } from '../_shared/safe-url'
|
|
8
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
8
9
|
|
|
9
10
|
const switcher = tv({
|
|
10
11
|
slots: {
|
|
11
12
|
list: 'flex flex-wrap items-center gap-x-4 gap-y-1',
|
|
12
|
-
link:
|
|
13
|
+
link: `no-underline ${muted} hover:text-current hover:underline aria-[current=true]:font-semibold aria-[current=true]:text-current`,
|
|
13
14
|
summary: 'inline-flex cursor-pointer list-none items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-current/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent [&::-webkit-details-marker]:hidden',
|
|
14
|
-
panel:
|
|
15
|
+
panel: `absolute z-30 mt-1 flex min-w-40 flex-col gap-1 rounded-lg border border-line bg-surface p-2 text-ink shadow-lg ${mutedReset}`,
|
|
15
16
|
},
|
|
16
17
|
variants: {
|
|
17
18
|
layout: { list: {}, menu: {} },
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Links to the same page in the site's other languages, each with hreflang and its own lang; the current one is marked aria-current. A plain list or a native <details> menu; no JavaScript. The addresses are the page's equivalents in each language, supplied by the caller.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["safe-url.ts"],
|
|
7
|
+
"shared": ["muted.ts", "safe-url.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"layout": {
|
|
10
10
|
"options": [
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
6
|
import KitImage from '../_shared/KitImage.astro'
|
|
7
7
|
import Section, { type Tone } from '../_shared/Section.astro'
|
|
8
|
+
import { instanceId } from '../_shared/instance-id'
|
|
8
9
|
import { imageOf, type ImageSource } from '../_shared/image'
|
|
9
10
|
import { measuredGrid } from '../_shared/measured'
|
|
10
11
|
import type { MeasuredStyle } from '../_shared/types'
|
|
11
12
|
import { safeHref } from '../_shared/safe-url'
|
|
13
|
+
import { muted } from '../_shared/muted'
|
|
12
14
|
|
|
13
15
|
const cloud = tv({
|
|
14
16
|
slots: {
|
|
15
|
-
heading:
|
|
17
|
+
heading: `mb-8 text-sm font-semibold tracking-wide uppercase ${muted}`,
|
|
16
18
|
list: 'grid grid-cols-2 items-center gap-x-8 gap-y-10 sm:grid-cols-3',
|
|
17
19
|
item: 'flex items-center justify-center',
|
|
18
20
|
logo: 'h-10 w-auto max-w-full object-contain transition duration-200',
|
|
@@ -46,7 +48,7 @@ const cloud = tv({
|
|
|
46
48
|
'mono-light': { logo: 'brightness-0 invert' },
|
|
47
49
|
},
|
|
48
50
|
align: { center: { heading: 'text-center' }, start: { heading: 'text-start' } },
|
|
49
|
-
tone: { default: {}, muted: {}, inverse: {}, accent: {
|
|
51
|
+
tone: { default: {}, muted: {}, inverse: {}, accent: {} },
|
|
50
52
|
},
|
|
51
53
|
compoundVariants: [
|
|
52
54
|
{ layout: 'row', align: 'center', class: { list: 'justify-center' } },
|
|
@@ -78,11 +80,13 @@ interface Props {
|
|
|
78
80
|
id?: string | undefined
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const { heading, items, layout, columns, logoHeight, style, align, tone, measured, id } = Astro.props
|
|
83
|
+
const { heading, items, layout, columns, logoHeight, style, align, tone, measured, id: givenId } = Astro.props
|
|
84
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `logo-cloud`, `logo-cloud-2`, … in render order.
|
|
85
|
+
const id = instanceId(Astro.locals, givenId, 'logo-cloud')
|
|
82
86
|
const s = cloud({ layout, columns, logoHeight, style, align, tone })
|
|
83
87
|
// Measured columns and gap shape the grid layout only; a row or a strip keeps its own flow.
|
|
84
88
|
const grid = (layout ?? 'grid') === 'grid' ? measuredGrid(measured) : undefined
|
|
85
|
-
const headingId = heading
|
|
89
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
86
90
|
const marquee = layout === 'marquee'
|
|
87
91
|
// A linked logo with no name is read as the site it goes to, never as a made-up word.
|
|
88
92
|
const hostOf = (href: string) => {
|
|
@@ -95,7 +99,7 @@ const shown = items.flatMap((item) => {
|
|
|
95
99
|
// About four seconds a logo: the strip reads at a pace, whatever its length.
|
|
96
100
|
const duration = `${Math.max(20, shown.length * 4)}s`
|
|
97
101
|
---
|
|
98
|
-
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
102
|
+
<Section kit="logo-cloud" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
99
103
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
100
104
|
<div class:list={[marquee && 'kit-marquee overflow-hidden']} style={marquee ? `--kit-marquee-duration:${duration}` : undefined}>
|
|
101
105
|
<ul class={s.list({ class: grid })}>
|
|
@@ -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
|
],
|
|
@@ -217,7 +219,7 @@
|
|
|
217
219
|
"id": {
|
|
218
220
|
"type": "string",
|
|
219
221
|
"content": false,
|
|
220
|
-
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
222
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `logo-cloud`, `logo-cloud-2`, … in render order, so every section on a page has its own."
|
|
221
223
|
}
|
|
222
224
|
},
|
|
223
225
|
"slots": {},
|
package/components/nav/Nav.astro
CHANGED
|
@@ -8,13 +8,15 @@ import { tv } from 'tailwind-variants'
|
|
|
8
8
|
import NavTree from './NavTree.astro'
|
|
9
9
|
import type { NavItem } from '../_shared/types'
|
|
10
10
|
import { safeHref } from '../_shared/safe-url'
|
|
11
|
+
import { instanceId } from '../_shared/instance-id'
|
|
12
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
11
13
|
|
|
12
14
|
const nav = tv({
|
|
13
15
|
slots: {
|
|
14
16
|
list: 'hidden items-center md:flex',
|
|
15
17
|
link: 'no-underline aria-[current=page]:font-semibold',
|
|
16
18
|
// hidden, not invisible: a closed submenu takes no space, so one under the last item cannot widen the page.
|
|
17
|
-
sub:
|
|
19
|
+
sub: `absolute top-full z-30 hidden min-w-48 rounded-lg border border-line bg-surface p-2 text-ink shadow-lg group-hover:block group-focus-within:block group-data-[closed]:hidden! ${mutedReset}`,
|
|
18
20
|
toggle: 'inline-flex cursor-pointer list-none items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-current/10 md:hidden [&::-webkit-details-marker]:hidden',
|
|
19
21
|
},
|
|
20
22
|
variants: {
|
|
@@ -23,10 +25,10 @@ const nav = tv({
|
|
|
23
25
|
// flyout: a list under the item. mega: a wide panel, one column per child with the grandchildren listed under it.
|
|
24
26
|
menu: { flyout: {}, mega: { sub: 'w-max max-w-[min(64rem,90vw)] p-5' } },
|
|
25
27
|
density: { compact: { list: 'gap-5 text-sm' }, comfortable: { list: 'gap-8 text-base' } },
|
|
26
|
-
// current: the surrounding text
|
|
28
|
+
// current: the surrounding text's muted role until hovered. link: the site's link colour at full strength, as themes that
|
|
27
29
|
// colour every link draw their menu; the toggle keeps the surrounding text.
|
|
28
30
|
color: {
|
|
29
|
-
current: { link:
|
|
31
|
+
current: { link: `${muted} hover:text-current aria-[current=page]:text-current` },
|
|
30
32
|
link: { link: 'text-[color:var(--color-link,var(--color-accent))]' },
|
|
31
33
|
},
|
|
32
34
|
},
|
|
@@ -52,7 +54,9 @@ interface Props {
|
|
|
52
54
|
part?: string | undefined
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', menu, align, density, color, id
|
|
57
|
+
const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', menu, align, density, color, id: givenId, part } = 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, 'site-nav')
|
|
56
60
|
const { list, link, sub, toggle } = nav({ menu, align, density, color })
|
|
57
61
|
const current = Astro.url.pathname
|
|
58
62
|
const drawer = `${id}-drawer`
|
|
@@ -99,7 +103,7 @@ const real = (href: string | undefined) => !!href && href !== '#'
|
|
|
99
103
|
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
100
104
|
{menuLabel}
|
|
101
105
|
</button>
|
|
102
|
-
<div id={drawer} popover class=
|
|
106
|
+
<div id={drawer} popover class={`m-0 ms-auto h-dvh max-h-none w-80 max-w-[85vw] border-s border-line bg-surface p-6 text-ink shadow-xl backdrop:bg-ink/40 ${mutedReset}`}>
|
|
103
107
|
<button type="button" popovertarget={drawer} popovertargetaction="hide" class="mb-6 ms-auto flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-surface-muted">
|
|
104
108
|
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
105
109
|
{closeLabel}
|
|
@@ -123,7 +127,7 @@ const real = (href: string | undefined) => !!href && href !== '#'
|
|
|
123
127
|
<svg aria-hidden="true" viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
124
128
|
{menuLabel}
|
|
125
129
|
</summary>
|
|
126
|
-
<ul class=
|
|
130
|
+
<ul class={`absolute end-0 z-30 mt-2 flex w-64 flex-col gap-1 rounded-lg border border-line bg-surface p-3 text-ink shadow-lg ${mutedReset}`}>
|
|
127
131
|
{items.map(item => (
|
|
128
132
|
<li>
|
|
129
133
|
{real(item.href)
|