@mnstry/atelier 0.2.0-alpha.16 → 0.2.0-alpha.18

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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Native <details> disclosures with interruptible open and close motion
3
+ * (browser). Keyboard and no-script behaviour stay native. data-state follows
4
+ * the reader's intent at once, so icons turn with the click while the panel
5
+ * is still moving. Reduced motion opens and closes instantly.
6
+ */
7
+ import { animationAllowed } from './motion.mjs'
8
+
9
+ const PROPERTIES = ['height', 'paddingTop', 'paddingBottom', 'marginTop', 'marginBottom', 'opacity']
10
+
11
+ export function enhanceDisclosures(root = document, { openMs = 360, closeMs = 280 } = {}) {
12
+ root.querySelectorAll('details').forEach(details => {
13
+ const summary = details.querySelector(':scope > summary')
14
+ const content = summary?.nextElementSibling
15
+ if (!summary || !content || details.dataset.disclosureReady !== undefined) return
16
+ details.dataset.disclosureReady = ''
17
+ details.dataset.state = details.open ? 'open' : 'closed'
18
+ if (!content.animate) return
19
+ let animation = null
20
+ let opening = details.open
21
+ const values = () => {
22
+ const style = getComputedStyle(content)
23
+ return Object.fromEntries(PROPERTIES.map(property => [property, style[property]]))
24
+ }
25
+ const clear = () => { content.style.overflow = ''; content.style.boxSizing = '' }
26
+ summary.addEventListener('click', event => {
27
+ event.preventDefault()
28
+ if (!animation) opening = details.open
29
+ opening = !opening
30
+ details.dataset.state = opening ? 'open' : 'closed'
31
+ if (!opening && content.contains(document.activeElement)) summary.focus()
32
+ const current = animation ? values() : null
33
+ animation?.cancel()
34
+ animation = null
35
+ if (!animationAllowed()) { details.open = opening; clear(); return }
36
+ details.open = true
37
+ content.style.overflow = 'hidden'
38
+ content.style.boxSizing = 'border-box'
39
+ const expanded = values()
40
+ const collapsed = Object.fromEntries(PROPERTIES.map(property => [property, property === 'opacity' ? '0' : '0px']))
41
+ animation = content.animate([current || (opening ? collapsed : expanded), opening ? expanded : collapsed], {
42
+ duration: opening ? openMs : closeMs,
43
+ easing: opening ? 'cubic-bezier(.22, 1, .36, 1)' : 'cubic-bezier(.4, 0, 1, 1)',
44
+ fill: 'both',
45
+ })
46
+ animation.onfinish = () => { details.open = opening; animation.cancel(); animation = null; clear() }
47
+ })
48
+ details.addEventListener('toggle', () => { if (!animation) details.dataset.state = details.open ? 'open' : 'closed' })
49
+ })
50
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Atelier UI primitives (Node-safe): pure state, styles and markup
3
+ * helpers. Browser enhancers live in ./browser.mjs (@mnstry/atelier/ui/primitives/browser).
4
+ */
5
+ export * from './state.mjs'
6
+ export { carouselStyles } from './styles.mjs'
7
+ export { renderCarouselControls } from './render.mjs'
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One owner for motion (browser): the reduced-motion preference and hidden tabs
3
+ * stop every effect started through these helpers.
4
+ */
5
+ import { motionAllowed } from './state.mjs'
6
+
7
+ const listeners = new Set()
8
+ const running = new Set()
9
+ let reduced = null
10
+ let started = false
11
+
12
+ function preference() {
13
+ if (!reduced && typeof matchMedia === 'function') reduced = matchMedia('(prefers-reduced-motion: reduce)')
14
+ return reduced
15
+ }
16
+
17
+ /** True when animation may run now. */
18
+ export function animationAllowed() {
19
+ return motionAllowed(Boolean(preference()?.matches), false, typeof document !== 'undefined' && document.hidden)
20
+ }
21
+
22
+ /** Call `listener` whenever the motion preference or tab visibility changes. */
23
+ export function onMotionChange(listener) {
24
+ listeners.add(listener)
25
+ startMotionWatch()
26
+ return () => listeners.delete(listener)
27
+ }
28
+
29
+ /** A Web Animation owned by the motion layer: skipped when motion is off, finished when it turns off. */
30
+ export function animate(element, frames, options) {
31
+ if (!animationAllowed() || !element.animate) return null
32
+ const animation = element.animate(frames, options)
33
+ running.add(animation)
34
+ animation.finished.catch(() => {}).finally(() => running.delete(animation))
35
+ return animation
36
+ }
37
+
38
+ function sync() {
39
+ document.documentElement.classList.toggle('motion-paused', Boolean(preference()?.matches))
40
+ if (!animationAllowed()) running.forEach(animation => animation.finish())
41
+ listeners.forEach(listener => listener())
42
+ }
43
+
44
+ export function startMotionWatch() {
45
+ if (started || typeof document === 'undefined') return
46
+ started = true
47
+ preference()?.addEventListener('change', sync)
48
+ document.addEventListener('visibilitychange', sync)
49
+ sync()
50
+ }
@@ -0,0 +1,22 @@
1
+ /** HTML for the carousel controls and live status (tenant-neutral, escaped). */
2
+ const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[character]))
3
+ const chevron = direction => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${direction === 'previous' ? 'M15 5l-7 7 7 7' : 'M9 5l7 7-7 7'}"/></svg>`
4
+ const toggleIcons = '<svg class="system-carousel__pause" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg><svg class="system-carousel__play" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5l12 7-12 7z"/></svg>'
5
+
6
+ /**
7
+ * Controls for a carousel: optional scroll bar, optional autoplay toggle,
8
+ * previous and next buttons, and a polite status for reader-initiated moves.
9
+ */
10
+ export function renderCarouselControls({ label = 'Carousel', autoplay = 'off', scrollbar = true, arrows = true } = {}) {
11
+ const name = escape(label)
12
+ const parts = []
13
+ if (scrollbar) parts.push('<div class="system-carousel__scrollbar" data-scrollbar aria-hidden="true"><span></span><span hidden></span></div>')
14
+ const buttons = []
15
+ if (autoplay === 'discrete' || autoplay === 'smooth') buttons.push(`<button type="button" class="system-carousel__button system-carousel__toggle" data-autoplay-toggle aria-label="Pause automatic scrolling" hidden>${toggleIcons}</button>`)
16
+ if (arrows) {
17
+ buttons.push(`<button type="button" class="system-carousel__button" data-prev aria-label="Previous: ${name}">${chevron('previous')}</button>`)
18
+ buttons.push(`<button type="button" class="system-carousel__button" data-next aria-label="Next: ${name}">${chevron('next')}</button>`)
19
+ }
20
+ if (buttons.length) parts.push(`<div class="system-carousel__buttons">${buttons.join('')}</div>`)
21
+ return `<div class="system-carousel__controls" data-controls hidden>${parts.join('')}</div><p class="system-carousel__status" data-status aria-live="polite"></p>`
22
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Layout-stable page scroll lock (browser), the contract documented in
3
+ * portable-presentation.md: measure the scrollbar a lock hides, pay it back as
4
+ * body end padding, expose it as --scroll-lock-gap for fixed and sticky
5
+ * elements, and restore both when the last lock is released. Locks are
6
+ * counted, so one closing overlay cannot release another's.
7
+ */
8
+ import { scrollLockGap } from './state.mjs'
9
+
10
+ const holders = new Set()
11
+ let saved = null
12
+
13
+ /** Lock page scrolling for `owner` (any value). Returns a release function. */
14
+ export function lockScroll(owner = Symbol('scroll-lock')) {
15
+ holders.add(owner)
16
+ if (!saved) {
17
+ const html = document.documentElement
18
+ const body = document.body
19
+ // Measure the body's laid-out width, not the root's clientWidth: with a stable
20
+ // scrollbar gutter some engines report the root inconsistently during the change.
21
+ const before = body.getBoundingClientRect().width
22
+ saved = { padding: body.style.paddingInlineEnd, overflow: html.style.overflow, overscroll: html.style.overscrollBehavior }
23
+ html.style.overflow = 'hidden'
24
+ html.style.overscrollBehavior = 'none'
25
+ const gap = scrollLockGap(before, body.getBoundingClientRect().width)
26
+ const existing = parseFloat(getComputedStyle(body).paddingInlineEnd) || 0
27
+ if (gap) body.style.paddingInlineEnd = `${existing + gap}px`
28
+ html.style.setProperty('--scroll-lock-gap', `${gap}px`)
29
+ html.dataset.scrollLocked = ''
30
+ }
31
+ return () => unlockScroll(owner)
32
+ }
33
+
34
+ /** Release `owner`'s lock; the page unlocks when no holder remains. */
35
+ export function unlockScroll(owner) {
36
+ holders.delete(owner)
37
+ if (holders.size || !saved) return
38
+ const html = document.documentElement
39
+ document.body.style.paddingInlineEnd = saved.padding
40
+ html.style.overflow = saved.overflow
41
+ html.style.overscrollBehavior = saved.overscroll
42
+ html.style.removeProperty('--scroll-lock-gap')
43
+ delete html.dataset.scrollLocked
44
+ saved = null
45
+ }
46
+
47
+ export const scrollLocked = () => holders.size > 0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Pure state for Atelier UI primitives. No DOM access, so every rule
3
+ * here is testable in Node and shared by the browser enhancers.
4
+ */
5
+
6
+ /** Visible window of a carousel at `index`, with arrow availability. */
7
+ export function slideWindow(index, length, visible, loop = false) {
8
+ const count = Math.max(1, Math.min(visible, length))
9
+ const max = Math.max(0, length - count)
10
+ const start = loop ? ((index % length) + length) % length : Math.min(max, Math.max(0, index))
11
+ return { start, end: Math.min(length, start + count), previousDisabled: !loop && start === 0, nextDisabled: !loop && start === max }
12
+ }
13
+
14
+ /** A horizontal intent must clearly dominate before the page scroll is taken over. */
15
+ export function dragIntent(dx, dy) {
16
+ if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return 'pending'
17
+ return Math.abs(dx) > Math.abs(dy) * 1.2 ? 'horizontal' : 'vertical'
18
+ }
19
+
20
+ /** Release decides the step: enough distance, or a quick flick. */
21
+ export function swipeStep(dx, dy, elapsed = Infinity) {
22
+ if (Math.abs(dx) <= Math.abs(dy) * 1.2) return 0
23
+ const flick = elapsed < 300 && Math.abs(dx) >= 24
24
+ return Math.abs(dx) >= 50 || flick ? (dx < 0 ? 1 : -1) : 0
25
+ }
26
+
27
+ /** Motion runs only when the reader allows it, nothing paused it and the tab is visible. */
28
+ export function motionAllowed(reduced, paused, hidden = false) {
29
+ return !reduced && !paused && !hidden
30
+ }
31
+
32
+ /** Clone sets needed on each side of a looping track to cover `perSide` slides. */
33
+ export function loopSets(length, perSide) {
34
+ return Math.max(1, Math.ceil(perSide / Math.max(1, length)))
35
+ }
36
+
37
+ /** The index of the real slide showing the same content as item `index` of a looping track. */
38
+ export function homeIndex(index, length, sets) {
39
+ return sets * length + ((index % length) + length) % length
40
+ }
41
+
42
+ /** Shortest signed distance from item `from` to any copy of real slide `to` in a loop. */
43
+ export function nearestCopy(from, to, length) {
44
+ let delta = ((to - from) % length + length) % length
45
+ if (delta > length / 2) delta -= length
46
+ return delta
47
+ }
48
+
49
+ /** How far a slide has receded: 0 when it is the current slide, 1 from a full step away. */
50
+ export function recede(position, index) {
51
+ return Math.min(1, Math.abs(position - index))
52
+ }
53
+
54
+ /** Few cards: when every card fits side by side (no narrower than `min`), show a still row. */
55
+ export function rowFit(length, natural, gap, room, min) {
56
+ const width = Math.min(natural, (room - gap * (length - 1)) / length)
57
+ return width >= min ? Math.floor(width) : 0
58
+ }
59
+
60
+ /** Whole items that fit in the viewport: the size of one page for page-stepping carousels. */
61
+ export function pageSize(viewport, step) {
62
+ return step > 0 ? Math.max(1, Math.floor((viewport + 1) / step)) : 1
63
+ }
64
+
65
+ /** Arrow step for page-stepping carousels. A loop only slightly longer than a page steps by the hidden remainder, so Next never reads as going backwards. */
66
+ export function arrowStep(page, length, loop) {
67
+ if (!loop) return Math.max(1, page)
68
+ return Math.max(1, Math.min(page, length - page))
69
+ }
70
+
71
+ /** Wrap a position into [0, period). */
72
+ export function wrap(position, period) {
73
+ if (!(period > 0)) return position
74
+ return ((position % period) + period) % period
75
+ }
76
+
77
+ /** Scroll position for a pointer on a scroll bar; `grab` is where the thumb was taken hold of. */
78
+ export function scrubPosition(pointerX, railLeft, railWidth, thumbWidth, grab, range) {
79
+ const travel = railWidth - thumbWidth
80
+ if (travel <= 0 || range <= 0) return 0
81
+ const ratio = Math.min(1, Math.max(0, (pointerX - railLeft - grab) / travel))
82
+ return ratio * range
83
+ }
84
+
85
+ /** Scroll bar thumb geometry, as fractions of the rail, for a viewport over a content range. */
86
+ export function thumbGeometry(position, range, viewport) {
87
+ const total = range + viewport
88
+ if (total <= 0) return { width: 1, left: 0 }
89
+ const width = Math.min(1, viewport / total)
90
+ const left = Math.min(Math.max(0, position), range) / total
91
+ return { width, left }
92
+ }
93
+
94
+ /**
95
+ * Thumb for a looping strip: one window over a repeating period. `left` can run
96
+ * past 1; the part beyond the end is drawn again from the start, so the thumb
97
+ * leaves the right end and re-enters on the left as the loop continues.
98
+ */
99
+ export function loopThumbGeometry(position, period, viewport) {
100
+ if (!(period > 0)) return { width: 1, left: 0 }
101
+ return { width: Math.min(1, viewport / period), left: wrap(position, period) / period }
102
+ }
103
+
104
+ /** Speed factor for a drift easing back in after a pause: 0 to 1 over `duration` ms (smoothstep). */
105
+ export function rampFactor(elapsed, duration = 1200) {
106
+ if (!(duration > 0)) return 1
107
+ const t = Math.min(1, Math.max(0, elapsed / duration))
108
+ return t * t * (3 - 2 * t)
109
+ }
110
+
111
+ /** Delay before autoplay resumes after a reader's interaction. */
112
+ export function resumeDelay(value) {
113
+ const ms = Number(value)
114
+ return Number.isFinite(ms) ? Math.min(10000, Math.max(0, ms)) : 2000
115
+ }
116
+
117
+ /** Momentum after a mouse drag: velocity in px/ms, decaying per 16ms frame. */
118
+ export const MAX_GLIDE_VELOCITY = 2.5
119
+ /** Release velocity in px/ms, capped so a fast fling travels a few cards, not the whole strip. */
120
+ export function releaseVelocity(dx, dt) {
121
+ const velocity = -dx / Math.max(1, dt)
122
+ return Math.max(-MAX_GLIDE_VELOCITY, Math.min(MAX_GLIDE_VELOCITY, velocity))
123
+ }
124
+ export function glideStep(position, velocity, elapsed, friction = 0.94) {
125
+ const dt = Math.min(32, Math.max(0, elapsed))
126
+ return { position: position + velocity * dt, velocity: velocity * Math.pow(friction, dt / 16) }
127
+ }
128
+
129
+ /** Autoplay modes: a discrete step every interval, or a smooth drift in px/s. */
130
+ export const AUTOPLAY_MODES = Object.freeze(['off', 'discrete', 'smooth'])
131
+ export function autoplayConfig({ mode = 'off', interval = 6000, speed = 28 } = {}) {
132
+ return {
133
+ mode: AUTOPLAY_MODES.includes(mode) ? mode : 'off',
134
+ interval: Math.max(2500, Number(interval) || 6000),
135
+ speed: Math.min(400, Math.max(4, Number(speed) || 28)),
136
+ }
137
+ }
138
+
139
+ /** Advance a smooth drift; loops within `period` when one is given. */
140
+ export function driftStep(position, speed, elapsed, period = 0, max = Infinity) {
141
+ const next = position + speed * Math.min(64, Math.max(0, elapsed)) / 1000
142
+ if (period > 0) return wrap(next, period)
143
+ return next >= max ? 0 : next
144
+ }
145
+
146
+ /**
147
+ * Scroll lock (portable-presentation contract): the width a lock must pay back
148
+ * is the scrollbar it hides, measured as the growth of the body's laid-out width.
149
+ * It is zero for overlay scrollbars and for gutters the browser keeps.
150
+ */
151
+ export function scrollLockGap(widthBefore, widthAfter) {
152
+ return Math.max(0, Math.round((widthAfter - widthBefore) * 100) / 100)
153
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Tenant-neutral carousel geometry and interaction styles. Tenants theme only
3
+ * through the --system-carousel-* custom properties; markup contract in
4
+ * docs/reference/ui-primitives.md.
5
+ */
6
+ export const carouselStyles = `
7
+ .system-carousel { position: relative; }
8
+ .system-carousel [data-carousel-viewport] {
9
+ overflow-x: auto;
10
+ overscroll-behavior-inline: contain;
11
+ scroll-snap-type: x mandatory;
12
+ scroll-padding-inline: var(--system-carousel-inset, 0px);
13
+ scrollbar-width: none;
14
+ }
15
+ .system-carousel [data-carousel-viewport]::-webkit-scrollbar { display: none; }
16
+ .system-carousel [data-carousel-track] {
17
+ display: flex;
18
+ gap: var(--system-carousel-gap, 1.25rem);
19
+ padding-inline: var(--system-carousel-inset, 0px);
20
+ }
21
+ .system-carousel [data-carousel-track] > * {
22
+ flex: 0 0 var(--system-carousel-slide, 100%);
23
+ min-width: 0;
24
+ scroll-snap-align: var(--system-carousel-align, start);
25
+ }
26
+ .system-carousel[data-peek] [data-carousel-track] > * { scroll-snap-align: center; }
27
+ .system-carousel[data-drift] [data-carousel-viewport] { scroll-snap-type: none; }
28
+ .system-carousel[data-fit="current"] [data-carousel-viewport] { transition: height 0.3s var(--system-carousel-ease, ease); }
29
+ .system-carousel img { -webkit-user-drag: none; }
30
+ @media (hover: hover) and (pointer: fine) {
31
+ .system-carousel[data-ready][data-overflow="true"] [data-carousel-viewport] { cursor: grab; }
32
+ }
33
+ .system-carousel[data-dragging] [data-carousel-viewport],
34
+ .system-carousel[data-dragging] [data-carousel-viewport] * { cursor: grabbing; user-select: none; -webkit-user-select: none; }
35
+
36
+ .system-carousel[data-peek] [data-carousel-track] > * {
37
+ transform: scale(calc(1 - var(--d, 0) * var(--system-carousel-recede-scale, 0.08)));
38
+ opacity: calc(1 - var(--d, 0) * var(--system-carousel-recede-fade, 0.35));
39
+ }
40
+ .system-carousel[data-row] [data-carousel-track] { justify-content: center; }
41
+ .system-carousel[data-row] [data-carousel-track] > * { flex-basis: var(--slide-fit); transform: none; opacity: 1; }
42
+
43
+ .system-carousel[data-carousel="fade"] [data-carousel-track] { display: grid; }
44
+ .system-carousel[data-carousel="fade"] [data-carousel-track] > * {
45
+ grid-area: 1 / 1;
46
+ transition: opacity var(--system-carousel-fade-ms, 700ms) var(--system-carousel-ease, ease), visibility 0s linear var(--system-carousel-fade-ms, 700ms);
47
+ }
48
+ .system-carousel[data-carousel="fade"][data-ready] [data-carousel-track] > *:not(.is-current) { opacity: 0; visibility: hidden; }
49
+ .system-carousel[data-carousel="fade"][data-ready] [data-carousel-track] > *.is-current { opacity: 1; visibility: visible; transition-delay: 0s; }
50
+
51
+ .system-carousel__controls {
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: var(--system-carousel-controls-justify, center);
55
+ gap: var(--system-carousel-controls-gap, 1.25rem);
56
+ margin-top: var(--system-carousel-controls-offset, 1.5rem);
57
+ }
58
+ .system-carousel__controls[hidden], .system-carousel__controls [hidden] { display: none; }
59
+ .system-carousel__buttons { display: flex; gap: 0.5rem; }
60
+ .system-carousel__button {
61
+ display: grid;
62
+ place-items: center;
63
+ width: var(--system-carousel-button-size, 44px);
64
+ height: var(--system-carousel-button-size, 44px);
65
+ padding: 0;
66
+ border: 1px solid var(--system-carousel-button-border, currentColor);
67
+ border-radius: var(--system-carousel-button-radius, 0);
68
+ background: var(--system-carousel-button-background, transparent);
69
+ color: inherit;
70
+ cursor: pointer;
71
+ transition: background-color 0.24s var(--system-carousel-ease, ease), border-color 0.24s var(--system-carousel-ease, ease), opacity 0.24s var(--system-carousel-ease, ease);
72
+ }
73
+ .system-carousel__button:disabled { opacity: 0.32; cursor: default; }
74
+ .system-carousel__button:active:not(:disabled) { transform: translateY(1px); }
75
+ .system-carousel__button:focus-visible, .system-carousel__page:focus-visible {
76
+ outline: 2px solid var(--system-carousel-focus, currentColor);
77
+ outline-offset: 3px;
78
+ }
79
+ @media (hover: hover) and (pointer: fine) {
80
+ .system-carousel__button:not(:disabled):hover {
81
+ background: var(--system-carousel-button-hover-background, rgba(0, 0, 0, 0.06));
82
+ border-color: var(--system-carousel-button-hover-border, currentColor);
83
+ }
84
+ }
85
+ .system-carousel__toggle .system-carousel__play,
86
+ .system-carousel__toggle[data-state="paused"] .system-carousel__pause { display: none; }
87
+ .system-carousel__toggle[data-state="paused"] .system-carousel__play { display: block; }
88
+
89
+ .system-carousel__scrollbar {
90
+ position: relative;
91
+ width: var(--system-carousel-scrollbar-width, min(260px, 38vw));
92
+ height: 28px;
93
+ overflow: hidden;
94
+ cursor: grab;
95
+ touch-action: none;
96
+ }
97
+ .system-carousel__scrollbar::before, .system-carousel__scrollbar > span {
98
+ content: '';
99
+ position: absolute;
100
+ top: 50%;
101
+ height: var(--system-carousel-scrollbar-thickness, 2px);
102
+ transform: translateY(-50%);
103
+ }
104
+ .system-carousel__scrollbar::before { left: 0; right: 0; background: var(--system-carousel-scrollbar-track, rgba(0, 0, 0, 0.14)); }
105
+ .system-carousel__scrollbar > span { left: 0; width: 25%; background: var(--system-carousel-scrollbar-thumb, currentColor); transition: height 0.16s var(--system-carousel-ease, ease); }
106
+ .system-carousel__scrollbar:hover > span, .system-carousel[data-scrubbing] .system-carousel__scrollbar > span { height: calc(var(--system-carousel-scrollbar-thickness, 2px) * 2); }
107
+ .system-carousel[data-scrubbing] .system-carousel__scrollbar { cursor: grabbing; }
108
+
109
+ .system-carousel__pages { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.25rem; }
110
+ .system-carousel__page { width: 24px; height: 24px; padding: 0; border: 0; background: none; cursor: pointer; display: grid; place-items: center; color: inherit; }
111
+ .system-carousel__page::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: 0.3; transition: opacity 0.24s var(--system-carousel-ease, ease); }
112
+ .system-carousel__page[aria-current="true"]::before { opacity: 1; }
113
+ .system-carousel__page[data-thumb] { width: var(--system-carousel-thumb-width, 72px); height: var(--system-carousel-thumb-height, 90px); opacity: 0.55; background: var(--thumb, none) center / cover; outline-offset: 2px; }
114
+ .system-carousel__page[data-thumb]::before { content: none; }
115
+ .system-carousel__page[data-thumb] img { width: 100%; height: 100%; object-fit: cover; }
116
+ .system-carousel__page[data-thumb][aria-current="true"] { opacity: 1; box-shadow: 0 0 0 1px var(--system-carousel-thumb-current, currentColor); }
117
+ .system-carousel__status {
118
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
119
+ clip: rect(0 0 0 0); white-space: nowrap; border: 0;
120
+ }
121
+
122
+ @media (prefers-reduced-motion: reduce) {
123
+ .system-carousel [data-carousel-track] > *, .system-carousel__button, .system-carousel__scrollbar > span,
124
+ .system-carousel[data-fit="current"] [data-carousel-viewport] { transition: none; }
125
+ .system-carousel[data-peek] [data-carousel-track] > * { transform: none; }
126
+ }
127
+ `
@@ -14,7 +14,9 @@
14
14
  "0.2.0-alpha.13",
15
15
  "0.2.0-alpha.14",
16
16
  "0.2.0-alpha.15",
17
- "0.2.0-alpha.16"
17
+ "0.2.0-alpha.16",
18
+ "0.2.0-alpha.17",
19
+ "0.2.0-alpha.18"
18
20
  ],
19
21
  "status": "active"
20
22
  }