@contentrain/astro-kit 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/catalog.json +76 -13
- package/components/_shared/Section.astro +13 -9
- package/components/_shared/SectionHeader.astro +3 -2
- package/components/_shared/instance-id.ts +24 -0
- package/components/_shared/muted.ts +19 -0
- package/components/band/Band.astro +10 -7
- package/components/band/meta.json +2 -0
- package/components/behavior/Behavior.astro +7 -2
- package/components/behavior/fixtures.json +2 -2
- package/components/behavior/meta.json +7 -0
- package/components/card-grid/CardGrid.astro +13 -9
- package/components/card-grid/meta.json +7 -0
- package/components/contact-details/ContactDetails.astro +7 -3
- package/components/contact-details/meta.json +2 -0
- package/components/contact-form/ContactForm.astro +11 -7
- package/components/contact-form/meta.json +2 -0
- package/components/cta/Cta.astro +14 -10
- package/components/cta/meta.json +2 -2
- package/components/dialog/Dialog.astro +6 -3
- package/components/dialog/meta.json +2 -0
- package/components/faq/Faq.astro +8 -4
- package/components/faq/meta.json +1 -1
- package/components/feature-list/FeatureList.astro +10 -6
- package/components/feature-list/meta.json +3 -1
- package/components/footer/Footer.astro +4 -3
- package/components/footer/meta.json +1 -0
- package/components/gallery/Gallery.astro +8 -4
- package/components/gallery/meta.json +2 -0
- package/components/header/Header.astro +8 -4
- package/components/header/meta.json +1 -1
- package/components/hero/Hero.astro +13 -9
- package/components/hero/meta.json +3 -1
- package/components/language-switcher/LanguageSwitcher.astro +3 -2
- package/components/language-switcher/meta.json +1 -1
- package/components/logo-cloud/LogoCloud.astro +9 -5
- package/components/logo-cloud/meta.json +3 -1
- package/components/nav/Nav.astro +10 -6
- package/components/nav/meta.json +2 -0
- package/components/newsletter/Newsletter.astro +9 -5
- package/components/newsletter/meta.json +2 -0
- package/components/post-list/PostList.astro +7 -3
- package/components/post-list/meta.json +2 -0
- package/components/pricing/Pricing.astro +15 -11
- package/components/pricing/meta.json +3 -1
- package/components/search-box/SearchBox.astro +4 -1
- package/components/search-box/meta.json +1 -1
- package/components/slider/Slider.astro +4 -1
- package/components/slider/SliderTrack.astro +11 -5
- package/components/slider/meta.json +1 -1
- package/components/social-links/SocialLinks.astro +2 -1
- package/components/social-links/meta.json +3 -2
- package/components/split/Split.astro +9 -5
- package/components/split/meta.json +3 -1
- package/components/stats/Stats.astro +11 -7
- package/components/stats/meta.json +3 -1
- package/components/steps/Steps.astro +9 -5
- package/components/steps/meta.json +3 -1
- package/components/tabs/Tabs.astro +8 -4
- package/components/tabs/meta.json +1 -1
- package/components/team/Team.astro +10 -6
- package/components/team/meta.json +4 -2
- package/components/testimonial/Testimonial.astro +9 -5
- package/components/testimonial/fixtures.json +1 -1
- package/components/testimonial/meta.json +3 -1
- package/package.json +1 -1
|
@@ -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)
|
package/components/nav/meta.json
CHANGED
|
@@ -13,19 +13,21 @@ import Section from '../_shared/Section.astro'
|
|
|
13
13
|
import type { MeasuredStyle } from '../_shared/types'
|
|
14
14
|
import { newsletterForm } from './newsletter'
|
|
15
15
|
import { safeUrl } from '../_shared/safe-url'
|
|
16
|
+
import { instanceId } from '../_shared/instance-id'
|
|
17
|
+
import { muted } from '../_shared/muted'
|
|
16
18
|
|
|
17
19
|
const newsletter = tv({
|
|
18
20
|
slots: {
|
|
19
21
|
box: 'flex flex-col gap-6',
|
|
20
22
|
copy: '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-xl text-pretty ${muted}`,
|
|
23
25
|
form: 'flex flex-col gap-3',
|
|
24
26
|
field: 'flex min-w-0 flex-col gap-1.5',
|
|
25
27
|
label: 'text-sm font-semibold',
|
|
26
28
|
input: 'min-h-11 w-full rounded-[var(--radius-control,var(--radius-md))] border border-line bg-surface px-3 py-2 text-base text-ink placeholder:text-ink-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
27
29
|
submit: 'inline-flex min-h-11 items-center justify-center rounded-[var(--radius-control,var(--radius-md))] px-5 py-2.5 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 bg-accent text-accent-ink hover:bg-ink hover:text-surface',
|
|
28
|
-
note:
|
|
30
|
+
note: `text-sm ${muted}`,
|
|
29
31
|
},
|
|
30
32
|
variants: {
|
|
31
33
|
layout: {
|
|
@@ -37,7 +39,7 @@ const newsletter = tv({
|
|
|
37
39
|
default: {},
|
|
38
40
|
muted: {},
|
|
39
41
|
inverse: { submit: 'hover:bg-surface hover:text-ink' },
|
|
40
|
-
accent: {
|
|
42
|
+
accent: { submit: 'bg-ink text-surface hover:bg-surface hover:text-ink' },
|
|
41
43
|
},
|
|
42
44
|
},
|
|
43
45
|
defaultVariants: { layout: 'inline', tone: 'muted' },
|
|
@@ -73,9 +75,11 @@ interface Props {
|
|
|
73
75
|
const {
|
|
74
76
|
action, provider = 'mailchimp', emailName, askName = false, heading, intro, note,
|
|
75
77
|
emailLabel = 'Email address', nameLabel = 'First name', submitLabel = 'Subscribe', newTab = true,
|
|
76
|
-
layout, tone = 'muted', id
|
|
78
|
+
layout, tone = 'muted', id: givenId,
|
|
77
79
|
measured,
|
|
78
80
|
} = Astro.props
|
|
81
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
82
|
+
const id = instanceId(Astro.locals, givenId, 'newsletter')
|
|
79
83
|
const form = newsletterForm(provider, action, { email: emailName }, Astro.site)
|
|
80
84
|
const s = newsletter({ layout, tone })
|
|
81
85
|
const headingId = heading ? `${id}-title` : undefined
|
|
@@ -83,7 +87,7 @@ const headingId = heading ? `${id}-title` : undefined
|
|
|
83
87
|
const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' } : {}
|
|
84
88
|
---
|
|
85
89
|
{form && (
|
|
86
|
-
<Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
90
|
+
<Section kit="newsletter" measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
87
91
|
<div class={s.box()}>
|
|
88
92
|
{(heading || intro) && (
|
|
89
93
|
<div class={s.copy()}>
|
|
@@ -10,6 +10,8 @@ import type { MeasuredStyle } from '../_shared/types'
|
|
|
10
10
|
import PostCard from '../post-card/PostCard.astro'
|
|
11
11
|
import type { ImageInput, LinkInput } from '../_shared/types'
|
|
12
12
|
import { safeHref } from '../_shared/safe-url'
|
|
13
|
+
import { instanceId } from '../_shared/instance-id'
|
|
14
|
+
import { muted } from '../_shared/muted'
|
|
13
15
|
|
|
14
16
|
export interface PostItem {
|
|
15
17
|
title: string
|
|
@@ -25,7 +27,7 @@ const list = tv({
|
|
|
25
27
|
slots: {
|
|
26
28
|
header: 'mb-10 flex max-w-2xl flex-col gap-3',
|
|
27
29
|
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
28
|
-
intro:
|
|
30
|
+
intro: `text-lg ${muted}`,
|
|
29
31
|
items: 'grid gap-8',
|
|
30
32
|
more: 'mt-10',
|
|
31
33
|
},
|
|
@@ -74,7 +76,9 @@ interface Props {
|
|
|
74
76
|
id?: string | undefined
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
const { heading, intro, posts, layout = 'grid', columns, showImage = true, showExcerpt = true, showAuthor = true, showCategory = true, excerptLines, locale, timeZone, surface, moreHref, moreLabel, tone, measured, id
|
|
79
|
+
const { heading, intro, posts, layout = 'grid', columns, showImage = true, showExcerpt = true, showAuthor = true, showCategory = true, excerptLines, locale, timeZone, surface, moreHref, moreLabel, tone, measured, id: givenId } = Astro.props
|
|
80
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
81
|
+
const id = instanceId(Astro.locals, givenId, 'posts')
|
|
78
82
|
const s = list({ layout, columns })
|
|
79
83
|
const headingId = heading ? `${id}-title` : undefined
|
|
80
84
|
// Cards sit one heading level under the section's; with no section heading they are the page's h2s.
|
|
@@ -84,7 +88,7 @@ const sizes = layout === 'list' || layout === 'compact' ? '256px' : columns ===
|
|
|
84
88
|
const cardLayout = (i: number) => layout === 'compact' ? 'minimal' : layout === 'list' || (layout === 'featured' && i === 0) ? 'horizontal' : 'vertical'
|
|
85
89
|
---
|
|
86
90
|
{posts.length > 0 && (
|
|
87
|
-
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
91
|
+
<Section kit="post-list" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
88
92
|
{(heading || intro) && (
|
|
89
93
|
<header class={s.header()}>
|
|
90
94
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
import { tv } from 'tailwind-variants'
|
|
9
9
|
import Button from '../_shared/Button.astro'
|
|
10
10
|
import Section, { type Tone } from '../_shared/Section.astro'
|
|
11
|
+
import { instanceId } from '../_shared/instance-id'
|
|
11
12
|
import { measuredGrid } from '../_shared/measured'
|
|
12
13
|
import type { MeasuredStyle } from '../_shared/types'
|
|
13
14
|
import { renderMarkdown } from '../../../lib/markdown'
|
|
15
|
+
import { muted, mutedReset } from '../_shared/muted'
|
|
14
16
|
|
|
15
17
|
const pricing = tv({
|
|
16
18
|
slots: {
|
|
17
19
|
header: 'mx-auto mb-10 flex max-w-2xl flex-col items-center gap-3 text-center',
|
|
18
20
|
heading: 'text-3xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-4xl',
|
|
19
|
-
intro:
|
|
21
|
+
intro: `text-lg ${muted}`,
|
|
20
22
|
list: 'grid items-stretch gap-6 sm:grid-cols-2',
|
|
21
23
|
toggle: 'mx-auto mb-8 flex w-fit rounded-full border border-line p-1',
|
|
22
24
|
toggleOption: 'rounded-full px-4 py-1.5 text-sm font-medium has-[:checked]:bg-accent has-[:checked]:text-accent-ink has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-accent',
|
|
@@ -24,13 +26,13 @@ const pricing = tv({
|
|
|
24
26
|
table: 'w-full min-w-[32rem] border-collapse text-start',
|
|
25
27
|
cell: 'border-b border-line px-4 py-3 text-center align-top',
|
|
26
28
|
rowHead: 'border-b border-line px-4 py-3 text-start font-normal',
|
|
27
|
-
plan:
|
|
29
|
+
plan: `relative flex flex-col gap-5 rounded-card border border-line bg-surface p-6 text-ink md:p-8 ${mutedReset}`,
|
|
28
30
|
badge: 'absolute -top-3 start-6 rounded-full bg-accent px-3 py-1 text-xs font-semibold text-accent-ink',
|
|
29
31
|
name: 'text-lg [font-weight:var(--font-weight-heading,600)]',
|
|
30
32
|
price: 'flex items-baseline gap-1',
|
|
31
33
|
amount: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight',
|
|
32
|
-
period:
|
|
33
|
-
description:
|
|
34
|
+
period: `text-sm ${muted}`,
|
|
35
|
+
description: `text-pretty ${muted}`,
|
|
34
36
|
features: 'prose-site prose max-w-none text-current prose-ul:ps-5 prose-li:my-1',
|
|
35
37
|
action: 'mt-auto pt-2',
|
|
36
38
|
},
|
|
@@ -47,7 +49,7 @@ const pricing = tv({
|
|
|
47
49
|
plain: {},
|
|
48
50
|
},
|
|
49
51
|
compact: { true: { list: 'gap-4', plan: 'gap-3 p-4 md:p-5', amount: 'text-3xl' }, false: {} },
|
|
50
|
-
tone: { default: {}, muted: { plan: 'border-transparent' }, inverse: { plan: 'border-transparent' }, accent: {
|
|
52
|
+
tone: { default: {}, muted: { plan: 'border-transparent' }, inverse: { plan: 'border-transparent' }, accent: { plan: 'border-transparent', badge: 'bg-ink text-surface' } },
|
|
51
53
|
},
|
|
52
54
|
defaultVariants: { columns: '3', featureStyle: 'plain', compact: false, tone: 'default' },
|
|
53
55
|
})
|
|
@@ -95,22 +97,24 @@ interface Props {
|
|
|
95
97
|
id?: string | undefined
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
const { heading, intro, plans, featuredLabel = 'Most popular', columns, layout = 'cards', featureStyle, compact, billingToggle = 'none', billingMainLabel = 'Monthly', billingAltLabel = 'Yearly', billingLegend = 'Billing period', includedLabel = 'Included', notIncludedLabel = 'Not included', tone, measured, id } = Astro.props
|
|
100
|
+
const { heading, intro, plans, featuredLabel = 'Most popular', columns, layout = 'cards', featureStyle, compact, billingToggle = 'none', billingMainLabel = 'Monthly', billingAltLabel = 'Yearly', billingLegend = 'Billing period', includedLabel = 'Included', notIncludedLabel = 'Not included', tone, measured, id: givenId } = Astro.props
|
|
101
|
+
// Every section has an id, so a theme or a design layer can address this one: the page's, or `pricing`, `pricing-2`, … in render order.
|
|
102
|
+
const id = instanceId(Astro.locals, givenId, 'pricing')
|
|
99
103
|
const s = pricing({ columns, featureStyle, compact: compact ?? false, tone })
|
|
100
|
-
// The radio group's name must be unique on the page: the section's id
|
|
101
|
-
const group = id
|
|
104
|
+
// The radio group's name must be unique on the page: the section's id is.
|
|
105
|
+
const group = id
|
|
102
106
|
const toggled = billingToggle === 'radio' && plans.some(plan => plan.priceAlt)
|
|
103
107
|
// The cards' list items, as plain strings, for the table's feature rows (the markdown list's `- ` / `* ` lines).
|
|
104
108
|
const itemsOf = (md?: string) => (md ?? '').split('\n').flatMap((line) => { const m = /^\s*[-*]\s+(.*\S)/.exec(line); return m ? [m[1]!.replace(/[*_`]/g, '')] : [] })
|
|
105
109
|
const rows = layout === 'table' ? [...new Set(plans.flatMap(plan => itemsOf(plan.features)))] : []
|
|
106
110
|
const included = plans.map(plan => new Set(itemsOf(plan.features)))
|
|
107
|
-
const headingId = heading
|
|
111
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
108
112
|
// One level under the section heading, or the section's heading when there is none.
|
|
109
113
|
const PlanHeading = heading ? 'h3' : 'h2'
|
|
110
114
|
const features = await Promise.all(plans.map(plan => renderMarkdown(plan.features)))
|
|
111
115
|
---
|
|
112
116
|
{plans.length > 0 && (
|
|
113
|
-
<Section measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
117
|
+
<Section kit="pricing" measured={measured} tone={tone} id={id} aria-labelledby={headingId}>
|
|
114
118
|
{(heading || intro) && (
|
|
115
119
|
<header class={s.header()}>
|
|
116
120
|
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
@@ -128,7 +132,7 @@ const features = await Promise.all(plans.map(plan => renderMarkdown(plan.feature
|
|
|
128
132
|
<th scope="col" class={s.cell({ class: plan.featured ? 'bg-surface-muted' : undefined })}>
|
|
129
133
|
<span class="block text-lg [font-weight:var(--font-weight-heading,600)]">{plan.name}</span>
|
|
130
134
|
<span class="block text-2xl [font-weight:var(--font-weight-heading,700)]">{plan.price}</span>
|
|
131
|
-
{plan.period && <span class=
|
|
135
|
+
{plan.period && <span class:list={['block text-sm', muted]}>{plan.period}</span>}
|
|
132
136
|
{(plan.badge ?? (plan.featured ? featuredLabel : undefined)) && <span class="mt-1 inline-block rounded-full bg-accent px-3 py-0.5 text-xs font-semibold text-accent-ink">{plan.badge ?? featuredLabel}</span>}
|
|
133
137
|
</th>
|
|
134
138
|
))}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
"shared": [
|
|
8
8
|
"Button.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
|
],
|
|
@@ -145,7 +147,7 @@
|
|
|
145
147
|
"id": {
|
|
146
148
|
"type": "string",
|
|
147
149
|
"content": false,
|
|
148
|
-
"description": "Anchor id of the section; the heading gets `<id>-title`."
|
|
150
|
+
"description": "Anchor id of the section; the heading gets `<id>-title`. Without one, `pricing`, `pricing-2`, … in render order, so every section on a page has its own."
|
|
149
151
|
},
|
|
150
152
|
"compact": {
|
|
151
153
|
"type": "boolean",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// assistive technology unless `labelStyle` shows it.
|
|
5
5
|
import { tv } from 'tailwind-variants'
|
|
6
6
|
import { safeUrl } from '../_shared/safe-url'
|
|
7
|
+
import { instanceId } from '../_shared/instance-id'
|
|
7
8
|
|
|
8
9
|
const search = tv({
|
|
9
10
|
slots: {
|
|
@@ -37,7 +38,9 @@ interface Props {
|
|
|
37
38
|
id?: string | undefined
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const { action = '/search', label = 'Search', placeholder, submitLabel = label, size, width, button, labelStyle, id
|
|
41
|
+
const { action = '/search', label = 'Search', placeholder, submitLabel = label, size, width, button, labelStyle, id: givenId } = Astro.props
|
|
42
|
+
// Two on one page never share an id: their labels and controls would point into each other.
|
|
43
|
+
const id = instanceId(Astro.locals, givenId, 'site-search')
|
|
41
44
|
const s = search({ size, width, button, labelStyle })
|
|
42
45
|
---
|
|
43
46
|
<form role="search" method="get" action={safeUrl(action)} class={s.form()} data-cr-part="search-box">
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "A search field that sends the visitor to the site's search page (`/search?q=…`): a plain GET form with a labelled role=search landmark, no JavaScript. For a header slot or inline in a page.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": ["safe-url.ts"],
|
|
7
|
+
"shared": ["instance-id.ts", "safe-url.ts"],
|
|
8
8
|
"variants": {
|
|
9
9
|
"size": {
|
|
10
10
|
"options": [
|
|
@@ -8,6 +8,7 @@ import Section from '../_shared/Section.astro'
|
|
|
8
8
|
import type { ImageSource } from '../_shared/image'
|
|
9
9
|
import type { ActionInput, MeasuredStyle } from '../_shared/types'
|
|
10
10
|
import SliderTrack from './SliderTrack.astro'
|
|
11
|
+
import { instanceId } from '../_shared/instance-id'
|
|
11
12
|
|
|
12
13
|
interface Slide {
|
|
13
14
|
/** The image's address (or, until 0.7, an `ImageInput`). Optional: a text card (a quote, a testimonial) is a slide too. */
|
|
@@ -54,7 +55,9 @@ interface Props {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const props = Astro.props
|
|
58
|
+
// The section and its track share one id (the track is `<id>-track`), taken once here so the track keeps it.
|
|
59
|
+
const id = props.measured ? instanceId(Astro.locals, props.id, 'slider') : undefined
|
|
57
60
|
---
|
|
58
61
|
{props.measured
|
|
59
|
-
? <Section measured={props.measured} tone="none" spacing="sm" id={
|
|
62
|
+
? <Section kit="slider" measured={props.measured} tone="none" spacing="sm" id={id}><SliderTrack {...props} id={id} framed={false} /></Section>
|
|
60
63
|
: <SliderTrack {...props} />}
|