@contentrain/astro-kit 0.34.2 → 0.35.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 +9 -0
- package/components/_shared/KitImage.astro +8 -2
- package/components/post-card/PostCard.astro +4 -2
- package/components/post-card/meta.json +1 -0
- package/components/post-list/PostList.astro +4 -1
- package/components/post-list/meta.json +5 -0
- package/components/slider/SliderTrack.astro +21 -5
- package/package.json +2 -2
package/catalog.json
CHANGED
|
@@ -4566,6 +4566,10 @@
|
|
|
4566
4566
|
"type": "string",
|
|
4567
4567
|
"description": "Locale the date is written in (site.language)."
|
|
4568
4568
|
},
|
|
4569
|
+
"timeZone": {
|
|
4570
|
+
"type": "string",
|
|
4571
|
+
"description": "Time zone the date is read in (the site's: an IANA name or a ±hh:mm offset); absent, UTC."
|
|
4572
|
+
},
|
|
4569
4573
|
"image": {
|
|
4570
4574
|
"type": "object",
|
|
4571
4575
|
"fields": {
|
|
@@ -4804,6 +4808,11 @@
|
|
|
4804
4808
|
"content": false,
|
|
4805
4809
|
"description": "BCP 47 locale the dates are written in (site.language)."
|
|
4806
4810
|
},
|
|
4811
|
+
"timeZone": {
|
|
4812
|
+
"type": "string",
|
|
4813
|
+
"content": false,
|
|
4814
|
+
"description": "Time zone the dates are read in (the site's: an IANA name or a ±hh:mm offset); absent, UTC."
|
|
4815
|
+
},
|
|
4807
4816
|
"surface": {
|
|
4808
4817
|
"type": "string",
|
|
4809
4818
|
"content": false,
|
|
@@ -42,8 +42,14 @@ const image: ImageInput = { ...(measured ? { ...given, ...measured } : given), s
|
|
|
42
42
|
const loading = priority ? 'eager' : 'lazy'
|
|
43
43
|
const fetchpriority = priority ? 'high' : 'auto'
|
|
44
44
|
const path = image.src.startsWith('/') && !image.src.startsWith('//') ? image.src.split(/[?#]/)[0]! : undefined
|
|
45
|
+
// Under Astro's `base` (a site served from `/blog/`) public/ is served in that directory: a root address is the file's
|
|
46
|
+
// path in public/, printed in the directory. An address already in it is not put there twice.
|
|
47
|
+
const base = (import.meta.env.BASE_URL ?? '/').replace(/\/+$/, '')
|
|
48
|
+
const inBase = (at: string) => base !== '' && (at === base || at.startsWith(`${base}/`))
|
|
49
|
+
const filePath = path !== undefined && inBase(path) ? path.slice(base.length) : path
|
|
50
|
+
const shown = path !== undefined && base !== '' && !inBase(path) ? `${base}${image.src}` : url
|
|
45
51
|
// A malformed escape (`%E0%A4%A`) is no file in public/: the image stays a plain <img> rather than failing the build.
|
|
46
|
-
const decoded = (() => { try { return
|
|
52
|
+
const decoded = (() => { try { return filePath === undefined ? undefined : decodeURI(filePath) } catch { return undefined } })()
|
|
47
53
|
const load = decoded === undefined ? undefined : LOCAL[`/public${decoded}`]
|
|
48
54
|
const local = load ? await load() : undefined
|
|
49
55
|
// The stored size (WordPress's chosen size of the file), else the file's own.
|
|
@@ -57,5 +63,5 @@ const widthsUpTo = (max: number) => [...STEPS.filter(step => step < max), max]
|
|
|
57
63
|
? <Image src={local} alt={image.alt ?? ''} {...size} widths={widthsUpTo(image.width ?? local.width)} sizes={sizes} class={className} style={style} loading={loading} fetchpriority={fetchpriority} />
|
|
58
64
|
: image.width && image.height && path === undefined
|
|
59
65
|
? <Image src={url} alt={image.alt ?? ''} width={image.width} height={image.height} widths={widthsUpTo(image.width)} sizes={sizes} class={className} style={style} loading={loading} fetchpriority={fetchpriority} />
|
|
60
|
-
: <img src={
|
|
66
|
+
: <img src={safeSrc(shown)} alt={image.alt ?? ''} width={image.width} height={image.height} class={className} style={style} loading={loading} fetchpriority={fetchpriority} decoding="async" />
|
|
61
67
|
)}
|
|
@@ -44,6 +44,8 @@ interface Props {
|
|
|
44
44
|
date?: Date | string | undefined
|
|
45
45
|
/** BCP 47 locale the date is written in (site.language). */
|
|
46
46
|
locale?: string | undefined
|
|
47
|
+
/** The time zone the date is read in (the site's, an IANA name or a `±hh:mm` offset); absent, UTC. */
|
|
48
|
+
timeZone?: string | undefined
|
|
47
49
|
image?: ImageInput | undefined
|
|
48
50
|
category?: LinkInput | undefined
|
|
49
51
|
author?: string | undefined
|
|
@@ -63,11 +65,11 @@ interface Props {
|
|
|
63
65
|
imageAspect?: string | undefined
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
const { title, href, excerpt, date: rawDate, locale = 'en', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, excerptLines, sizes, imageHeight, imageAspect } = Astro.props
|
|
68
|
+
const { title, href, excerpt, date: rawDate, locale = 'en', timeZone = 'UTC', image, category, author, headingLevel = 2, layout, surface, byline, linkTitle, excerptLines, sizes, imageHeight, imageAspect } = Astro.props
|
|
67
69
|
const date = rawDate === undefined ? undefined : new Date(rawDate)
|
|
68
70
|
const slots = card({ layout, surface, byline })
|
|
69
71
|
const below = byline === 'below'
|
|
70
|
-
const shown = date?.toLocaleDateString(locale, { dateStyle: 'long', timeZone
|
|
72
|
+
const shown = date?.toLocaleDateString(locale, { dateStyle: 'long', timeZone })
|
|
71
73
|
const imageStyle = imageHeight ? `height:${imageHeight}` : imageAspect ? `aspect-ratio:${imageAspect}` : undefined
|
|
72
74
|
const Heading = `h${headingLevel}` as 'h2' | 'h3'
|
|
73
75
|
---
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"excerpt": { "type": "text", "description": "Summary (posts.excerpt)." },
|
|
17
17
|
"date": { "type": "datetime", "description": "Publication date (posts.published_at)." },
|
|
18
18
|
"locale": { "type": "string", "description": "Locale the date is written in (site.language)." },
|
|
19
|
+
"timeZone": { "type": "string", "description": "Time zone the date is read in (the site's: an IANA name or a ±hh:mm offset); absent, UTC." },
|
|
19
20
|
"image": { "type": "object", "fields": { "src": { "type": "image", "required": true }, "alt": { "type": "string", "required": true }, "width": { "type": "integer" }, "height": { "type": "integer" } }, "description": "Cover (posts.cover → media)." },
|
|
20
21
|
"category": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true } }, "description": "Primary category and its archive." },
|
|
21
22
|
"author": { "type": "string", "description": "Author name (posts.author → authors.name)." },
|
|
@@ -61,6 +61,8 @@ interface Props {
|
|
|
61
61
|
excerptLines?: 2 | 3 | 4 | undefined
|
|
62
62
|
/** BCP 47 locale the dates are written in. */
|
|
63
63
|
locale?: string | undefined
|
|
64
|
+
/** The time zone the dates are read in (the site's: an IANA name or a `±hh:mm` offset); absent, UTC. */
|
|
65
|
+
timeZone?: string | undefined
|
|
64
66
|
/** Card surface: none, or each post its own card. */
|
|
65
67
|
surface?: 'plain' | 'card' | undefined
|
|
66
68
|
/** Where the full archive lives; with `moreLabel` a link follows the list. */
|
|
@@ -72,7 +74,7 @@ interface Props {
|
|
|
72
74
|
id?: string | undefined
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
const { heading, intro, posts, layout = 'grid', columns, showImage = true, showExcerpt = true, showAuthor = true, showCategory = true, excerptLines, locale, surface, moreHref, moreLabel, tone, measured, id = 'posts' } = Astro.props
|
|
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 = 'posts' } = Astro.props
|
|
76
78
|
const s = list({ layout, columns })
|
|
77
79
|
const headingId = heading ? `${id}-title` : undefined
|
|
78
80
|
// Cards sit one heading level under the section's; with no section heading they are the page's h2s.
|
|
@@ -98,6 +100,7 @@ const cardLayout = (i: number) => layout === 'compact' ? 'minimal' : layout ===
|
|
|
98
100
|
excerpt={showExcerpt && layout !== 'compact' ? post.excerpt : undefined}
|
|
99
101
|
date={post.date}
|
|
100
102
|
locale={locale}
|
|
103
|
+
timeZone={timeZone}
|
|
101
104
|
image={showImage && layout !== 'compact' ? post.image : undefined}
|
|
102
105
|
category={showCategory && layout !== 'compact' ? post.category : undefined}
|
|
103
106
|
author={showAuthor && layout !== 'compact' ? post.author : undefined}
|
|
@@ -141,6 +141,11 @@
|
|
|
141
141
|
"content": false,
|
|
142
142
|
"description": "BCP 47 locale the dates are written in (site.language)."
|
|
143
143
|
},
|
|
144
|
+
"timeZone": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"content": false,
|
|
147
|
+
"description": "Time zone the dates are read in (the site's: an IANA name or a ±hh:mm offset); absent, UTC."
|
|
148
|
+
},
|
|
144
149
|
"surface": {
|
|
145
150
|
"type": "string",
|
|
146
151
|
"content": false,
|
|
@@ -14,7 +14,8 @@ import { type ActionInput, type ImageInput, type MeasuredStyle } from '../_share
|
|
|
14
14
|
|
|
15
15
|
const slider = tv({
|
|
16
16
|
slots: {
|
|
17
|
-
viewport
|
|
17
|
+
// A block viewport around the flex row, as Embla's own markup has it: the row is the container Embla moves.
|
|
18
|
+
viewport: 'snap-x snap-mandatory overflow-x-auto scroll-smooth group-data-[ready]:snap-none group-data-[ready]:overflow-hidden [scrollbar-width:none] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
18
19
|
slide: 'relative min-w-0 shrink-0 snap-start overflow-hidden',
|
|
19
20
|
image: 'w-full object-cover',
|
|
20
21
|
caption: 'flex flex-col items-start gap-2',
|
|
@@ -140,6 +141,7 @@ const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-cente
|
|
|
140
141
|
const prev = root.querySelector<HTMLButtonElement>('[data-prev]')
|
|
141
142
|
const next = root.querySelector<HTMLButtonElement>('[data-next]')
|
|
142
143
|
const dots = root.querySelector<HTMLElement>('[data-dots]')
|
|
144
|
+
const track = root.querySelector<HTMLElement>('[data-container]')
|
|
143
145
|
prev?.addEventListener('click', () => embla.scrollPrev())
|
|
144
146
|
next?.addEventListener('click', () => embla.scrollNext())
|
|
145
147
|
// The row takes focus (the buttons are hidden on phones): the arrow keys page it, and a slide change is announced.
|
|
@@ -150,10 +152,13 @@ const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-cente
|
|
|
150
152
|
else embla.scrollPrev()
|
|
151
153
|
})
|
|
152
154
|
// No autoplay: the carousel only moves when asked, so a polite announcement of the slide is right.
|
|
153
|
-
|
|
155
|
+
track?.setAttribute('aria-live', 'polite')
|
|
154
156
|
|
|
155
157
|
const label = root.dataset.slideLabel ?? 'Slide'
|
|
156
|
-
|
|
158
|
+
// One dot per snap, drawn again whenever Embla measures again: the snaps of a carousel set up before its page was
|
|
159
|
+
// laid out (a background tab, a frame sized later) are not the ones it ends up with.
|
|
160
|
+
let buttons: HTMLButtonElement[] = []
|
|
161
|
+
const drawDots = () => { buttons = embla.scrollSnapList().map((_, index) => {
|
|
157
162
|
const dot = document.createElement('button')
|
|
158
163
|
dot.type = 'button'
|
|
159
164
|
// A 24px target around a 10px dot (WCAG 2.5.8): on phones the dots are the only controls.
|
|
@@ -163,16 +168,27 @@ const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-cente
|
|
|
163
168
|
dot.append(mark)
|
|
164
169
|
dot.setAttribute('aria-label', `${label} ${index + 1}`)
|
|
165
170
|
dot.addEventListener('click', () => embla.scrollTo(index))
|
|
166
|
-
dots?.append(dot)
|
|
167
171
|
return dot
|
|
168
172
|
})
|
|
173
|
+
dots?.replaceChildren(...buttons) }
|
|
174
|
+
drawDots()
|
|
169
175
|
const sync = () => {
|
|
170
176
|
const selected = embla.selectedScrollSnap()
|
|
171
177
|
buttons.forEach((dot, index) => dot.setAttribute('aria-current', String(index === selected)))
|
|
172
178
|
if (prev) prev.disabled = !embla.canScrollPrev()
|
|
173
179
|
if (next) next.disabled = !embla.canScrollNext()
|
|
174
180
|
}
|
|
175
|
-
embla.on('select', sync).on('reInit', sync)
|
|
181
|
+
embla.on('select', sync).on('reInit', () => { drawDots(); sync() })
|
|
176
182
|
sync()
|
|
183
|
+
// Set up before the layout was final, Embla measures once more when it is: the page loaded, its fonts in, the tab in
|
|
184
|
+
// view. A background tab lays nothing out (ctrl/cmd-click), so its carousel had one snap and a dead "next" until then;
|
|
185
|
+
// Embla's own resize watch does not fire there, since nothing changed size while the tab was hidden. Only a carousel
|
|
186
|
+
// whose row overflows while Embla can page neither way is measured again: one that already pages is left where it is.
|
|
187
|
+
const measure = () => {
|
|
188
|
+
if (track && track.scrollWidth > viewport.clientWidth + 1 && !embla.canScrollNext() && !embla.canScrollPrev()) embla.reInit()
|
|
189
|
+
}
|
|
190
|
+
if (document.readyState !== 'complete') addEventListener('load', measure, { once: true })
|
|
191
|
+
void document.fonts?.ready.then(measure)
|
|
192
|
+
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') measure() })
|
|
177
193
|
}
|
|
178
194
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrain/astro-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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.63.
|
|
45
|
+
"@contentrain/types": "1.63.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.10.0",
|