@contentrain/astro-kit 0.34.1 → 0.34.3
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.
|
@@ -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
|
)}
|
|
@@ -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.34.
|
|
3
|
+
"version": "0.34.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"mapping"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@contentrain/types": "1.
|
|
45
|
+
"@contentrain/types": "1.63.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.10.0",
|