@kolkrabbi/kol-component 0.40.0 → 0.42.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.js
CHANGED
|
@@ -100,6 +100,10 @@ export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
|
|
|
100
100
|
export { default as AsciiCursor } from './utilities/AsciiCursor.jsx'
|
|
101
101
|
export { default as BentoCard } from './molecules/BentoCard.jsx'
|
|
102
102
|
export { default as Carousel } from './molecules/Carousel.jsx'
|
|
103
|
+
/* EmblaNav — THE prev/next pair. Exported so a consumer building its own embla
|
|
104
|
+
* stage reaches for it instead of re-typing the button markup, which is how the
|
|
105
|
+
* three in-package copies (and kol-website's CarouselNavigation) happened. */
|
|
106
|
+
export { default as EmblaNav } from './molecules/EmblaNav.jsx'
|
|
103
107
|
export { default as ContentFilters } from './organisms/ContentFilters.jsx'
|
|
104
108
|
export { default as CtaGlobal } from './organisms/CtaGlobal.jsx'
|
|
105
109
|
export { default as ErrorBoundary } from './utilities/ErrorBoundary.jsx'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Children, useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import useEmblaCarousel from 'embla-carousel-react'
|
|
3
|
+
import EmblaNav from './EmblaNav.jsx'
|
|
3
4
|
|
|
4
5
|
export default function Carousel({ children, options = { align: 'start', loop: false, dragFree: true, containScroll: 'trimSnaps' }, className = '' }) {
|
|
5
6
|
const [emblaRef, emblaApi] = useEmblaCarousel(options)
|
|
@@ -29,22 +30,12 @@ export default function Carousel({ children, options = { align: 'start', loop: f
|
|
|
29
30
|
))}
|
|
30
31
|
</div>
|
|
31
32
|
</div>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
disabled={!canPrev}
|
|
39
|
-
>‹</button>
|
|
40
|
-
<button
|
|
41
|
-
type="button"
|
|
42
|
-
className="kol-embla-btn border border-fg-16 hover:border-fg-32 text-auto"
|
|
43
|
-
aria-label="Next"
|
|
44
|
-
onClick={() => emblaApi?.scrollNext()}
|
|
45
|
-
disabled={!canNext}
|
|
46
|
-
>›</button>
|
|
47
|
-
</div>
|
|
33
|
+
<EmblaNav
|
|
34
|
+
onPrev={() => emblaApi?.scrollPrev()}
|
|
35
|
+
onNext={() => emblaApi?.scrollNext()}
|
|
36
|
+
canPrev={canPrev}
|
|
37
|
+
canNext={canNext}
|
|
38
|
+
/>
|
|
48
39
|
</div>
|
|
49
40
|
)
|
|
50
41
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EmblaNav — THE prev/next pair for an embla carousel.
|
|
5
|
+
*
|
|
6
|
+
* WHY IT EXISTS. Three components hand-wrote the same
|
|
7
|
+
* `.kol-embla-btn border border-fg-16 hover:border-fg-32 text-auto` string with
|
|
8
|
+
* their own aria labels, and two of them used the literal text glyphs `‹` and
|
|
9
|
+
* `›` as the arrows — in a design system that ships a chevron icon set. The
|
|
10
|
+
* FeaturedCarousel reconciliation (2026-08-15) found the consumer's fork had
|
|
11
|
+
* built its own `CarouselNavigation` for precisely that reason: it wanted real
|
|
12
|
+
* icons. Rather than fold that file in beside three copies of the markup, the
|
|
13
|
+
* markup became one component. Same failure RailSection fixed for rails — a
|
|
14
|
+
* class is vocabulary, not grammar.
|
|
15
|
+
*
|
|
16
|
+
* MediaViewer keeps its own chips deliberately: they are absolutely positioned
|
|
17
|
+
* over an inverse-tier scrim, which is a different control, not this one.
|
|
18
|
+
*
|
|
19
|
+
* @param {Function} onPrev · @param {Function} onNext
|
|
20
|
+
* @param {boolean} [canPrev=true] · @param {boolean} [canNext=true] disabled state
|
|
21
|
+
* @param {number} [size=16] chevron size
|
|
22
|
+
* @param {'stack'|'inline'} [placement='stack'] `inline` drops the top gap and
|
|
23
|
+
* the end-alignment for a header row; `stack` sits under the viewport
|
|
24
|
+
* @param {string} [prevLabel='Previous'] · @param {string} [nextLabel='Next']
|
|
25
|
+
*/
|
|
26
|
+
export default function EmblaNav({
|
|
27
|
+
onPrev,
|
|
28
|
+
onNext,
|
|
29
|
+
canPrev = true,
|
|
30
|
+
canNext = true,
|
|
31
|
+
size = 16,
|
|
32
|
+
placement = 'stack',
|
|
33
|
+
prevLabel = 'Previous',
|
|
34
|
+
nextLabel = 'Next',
|
|
35
|
+
className = '',
|
|
36
|
+
}) {
|
|
37
|
+
const btn = 'kol-embla-btn border border-fg-16 hover:border-fg-32 text-auto'
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={`kol-embla-controls ${placement === 'inline' ? 'is-inline' : ''} ${className}`.trim()}
|
|
42
|
+
>
|
|
43
|
+
<button type="button" className={btn} aria-label={prevLabel} onClick={onPrev} disabled={!canPrev}>
|
|
44
|
+
<Icon name="chevron-left" size={size} />
|
|
45
|
+
</button>
|
|
46
|
+
<button type="button" className={btn} aria-label={nextLabel} onClick={onNext} disabled={!canNext}>
|
|
47
|
+
<Icon name="chevron-right" size={size} />
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -16,12 +16,37 @@
|
|
|
16
16
|
* `text-transform: uppercase` was dropped per KOL casing rules; author those
|
|
17
17
|
* strings in their final case at the call site.
|
|
18
18
|
*
|
|
19
|
+
* THE SECTION ANATOMY, ONCE (SectionSplit ruling, 2026-08-15). The brief asked
|
|
20
|
+
* for a new `SectionSplit` — media slot beside kicker/heading/body/actions,
|
|
21
|
+
* with a flip — and named eleven hand-built kol-website sections as evidence.
|
|
22
|
+
* That is this component: the anatomy already shipped here, and a second one
|
|
23
|
+
* beside it would be the very duplication the brief exists to end. It grew the
|
|
24
|
+
* three things it genuinely lacked instead — `flip`, `titleSize`, `mediaAspect`.
|
|
25
|
+
*
|
|
26
|
+
* ON THREADING PER-SITE TYPE CLASSES — the brief's named "core design
|
|
27
|
+
* question". The answer is that you do not thread one: a consumer picks a ROLE
|
|
28
|
+
* (`titleSize`), and the component emits exactly one type class for the
|
|
29
|
+
* heading. Passing `kol-sans-heading-01` in alongside `.kol-feature-split-pull`
|
|
30
|
+
* would put two equal-specificity rules on one element and let sheet load order
|
|
31
|
+
* decide the winner — the failure ARCHITECTURE §5 records, and the 2026-07-30
|
|
32
|
+
* law that a component's type lives in its own rule. `columnClassName` and
|
|
33
|
+
* friends remain, for LAYOUT.
|
|
34
|
+
*
|
|
19
35
|
* @param {ReactNode} kicker mono eyebrow above the title (accent color)
|
|
20
36
|
* @param {ReactNode} title display pull headline; `<em>` renders as the italic accent
|
|
37
|
+
* @param {'pull'|'display-01'|'display-02'|'display-03'|'heading-01'|'heading-02'|'heading-03'|'heading-04'|'heading-05'} [titleSize='pull']
|
|
38
|
+
* which type ROLE the heading wears — one class, never stacked
|
|
21
39
|
* @param {ReactNode} body lede paragraph
|
|
22
40
|
* @param {{num: ReactNode, label: string}[]} meta stats strip (mutually exclusive with `ctas`)
|
|
23
41
|
* @param {ReactNode} ctas button row (mutually exclusive with `meta`)
|
|
24
|
-
* @param {ReactNode} media image/video for the visual
|
|
42
|
+
* @param {ReactNode} media image/video/interactive node for the visual
|
|
43
|
+
* column; omit for the text-only section (the /CONNECT band shape)
|
|
44
|
+
* @param {string} [mediaAspect='4/5'] aspect ratio of the media frame
|
|
45
|
+
* @param {boolean} [mediaHover=false] zoom the media on frame hover — the same
|
|
46
|
+
* 1.03 / 300ms / reduced-motion-safe treatment CardFeatureItem uses, so the
|
|
47
|
+
* estate has one motion vocabulary instead of a per-section re-decision
|
|
48
|
+
* @param {boolean} [flip=false] media first, text second — the order swaps at
|
|
49
|
+
* both widths, so the stacked layout leads with the media too
|
|
25
50
|
* @param {ReactNode} caption mono caption + gradient veil over the media
|
|
26
51
|
* @param {string} bgImage URL for an inline cover background on the section
|
|
27
52
|
* @param {boolean} fullBleed span the full viewport width (100vw breakout)
|
|
@@ -29,13 +54,29 @@
|
|
|
29
54
|
* @param {string} innerClassName extra classes on the grid wrapper
|
|
30
55
|
* @param {string} columnClassName extra classes on the text column
|
|
31
56
|
*/
|
|
57
|
+
const TITLE_ROLE = {
|
|
58
|
+
pull: 'kol-feature-split-pull',
|
|
59
|
+
'display-01': 'kol-sans-display-01',
|
|
60
|
+
'display-02': 'kol-sans-display-02',
|
|
61
|
+
'display-03': 'kol-sans-display-03',
|
|
62
|
+
'heading-01': 'kol-sans-heading-01',
|
|
63
|
+
'heading-02': 'kol-sans-heading-02',
|
|
64
|
+
'heading-03': 'kol-sans-heading-03',
|
|
65
|
+
'heading-04': 'kol-sans-heading-04',
|
|
66
|
+
'heading-05': 'kol-sans-heading-05',
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
export default function FeatureSplit({
|
|
33
70
|
kicker,
|
|
34
71
|
title,
|
|
72
|
+
titleSize = 'pull',
|
|
35
73
|
body,
|
|
36
74
|
meta,
|
|
37
75
|
ctas,
|
|
38
76
|
media,
|
|
77
|
+
mediaAspect = '4/5',
|
|
78
|
+
mediaHover = false,
|
|
79
|
+
flip = false,
|
|
39
80
|
caption,
|
|
40
81
|
bgImage,
|
|
41
82
|
fullBleed = false,
|
|
@@ -53,9 +94,14 @@ export default function FeatureSplit({
|
|
|
53
94
|
style={sectionStyle}
|
|
54
95
|
>
|
|
55
96
|
<div className={`max-w-[1200px] mx-auto grid grid-cols-1 min-[901px]:grid-cols-2 items-center gap-[clamp(48px,6vw,96px)] ${innerClassName}`.trim()}>
|
|
56
|
-
|
|
97
|
+
{/* `order` rather than `flex-row-reverse`: the grid is one column below
|
|
98
|
+
* 901px, and DOM order is what decides the stack there. Reversing a row
|
|
99
|
+
* would flip the wide layout and leave the narrow one text-first. */}
|
|
100
|
+
<div
|
|
101
|
+
className={`flex flex-col gap-4 max-w-[640px] ${flip ? 'order-2' : ''} ${columnClassName}`.replace(/\s+/g, ' ').trim()}
|
|
102
|
+
>
|
|
57
103
|
{kicker && <span className="kol-feature-split-kicker">{kicker}</span>}
|
|
58
|
-
{title && <h1 className=
|
|
104
|
+
{title && <h1 className={TITLE_ROLE[titleSize] ?? TITLE_ROLE.pull}>{title}</h1>}
|
|
59
105
|
{body && <p className="kol-feature-split-body">{body}</p>}
|
|
60
106
|
{meta && meta.length > 0 && (
|
|
61
107
|
<div className="kol-feature-split-meta flex flex-wrap gap-y-7 gap-x-12 pt-4">
|
|
@@ -70,7 +116,10 @@ export default function FeatureSplit({
|
|
|
70
116
|
{ctas && <div className="flex flex-wrap gap-4 pt-2">{ctas}</div>}
|
|
71
117
|
</div>
|
|
72
118
|
{media && (
|
|
73
|
-
<div
|
|
119
|
+
<div
|
|
120
|
+
className={`kol-feature-split-visual relative rounded-[var(--kol-radius-sm)] overflow-hidden ${mediaHover ? 'is-hoverable' : ''} ${flip ? 'order-1' : ''}`.replace(/\s+/g, ' ').trim()}
|
|
121
|
+
style={{ aspectRatio: mediaAspect }}
|
|
122
|
+
>
|
|
74
123
|
{media}
|
|
75
124
|
{caption && <div className="kol-feature-split-visual-veil" aria-hidden="true" />}
|
|
76
125
|
{caption && <span className="kol-feature-split-visual-caption">{caption}</span>}
|
|
@@ -2,6 +2,7 @@ import { isValidElement, useCallback, useEffect, useState } from 'react'
|
|
|
2
2
|
import useEmblaCarousel from 'embla-carousel-react'
|
|
3
3
|
import Image from '../atoms/Image.jsx'
|
|
4
4
|
import HlsVideo from '../atoms/HlsVideo.jsx'
|
|
5
|
+
import EmblaNav from '../molecules/EmblaNav.jsx'
|
|
5
6
|
import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -85,17 +86,42 @@ function SlideMedia({ media, onEnded, onTimeUpdate }) {
|
|
|
85
86
|
* CTA is a plain anchor styled with the DS button classes plus an `onNavigate`
|
|
86
87
|
* seam (router-agnostic — call `preventDefault` inside it for SPA nav).
|
|
87
88
|
*
|
|
88
|
-
*
|
|
89
|
+
* RECONCILED 2026-08-15 against kol-website's fork (231L vs 259L, 458 diff
|
|
90
|
+
* lines). They were never a fork — two different engines. The fork ran
|
|
91
|
+
* framer-motion `AnimatePresence` over an index, which means **no drag at all**;
|
|
92
|
+
* this one runs embla, so the canon call went to the engine here, and with it
|
|
93
|
+
* the `{ media }` descriptor, OverlayGlassPanel, and the progress ring. Five
|
|
94
|
+
* capabilities crossed the other way — `children`, `fullWidth`, `rounded`, the
|
|
95
|
+
* `show*` visibility toggles and `subtitle` — plus the header nav placement.
|
|
96
|
+
* Deliberately NOT carried: the foundry title coupling (a size ramp keyed on
|
|
97
|
+
* the literal strings 'Málrómur'/'Tröllatunga' and a per-typeface inline
|
|
98
|
+
* `fontFamily`), `kol-label-mono-xs` (a deleted legacy family), and the hidden
|
|
99
|
+
* block that eagerly preloaded every slide image — embla plus `loading="eager"`
|
|
100
|
+
* on the visible slide covers that without fetching a whole gallery up front.
|
|
101
|
+
*
|
|
102
|
+
* @param {Array} items slides: `{ media: { src, kind: 'image'|'video', poster, srcSet, alt }, title, subtitle, description, href, ctaLabel, titleClassName, descriptionClassName, showTitle, showDescription, showCta }`
|
|
89
103
|
* @param {string} sectionLabel header label (default 'Featured')
|
|
90
104
|
* @param {string} ctaLabel default CTA copy; per-item `ctaLabel` overrides (default 'Learn more')
|
|
91
105
|
* @param {string} height slide-frame height class (default 'h-[440px] md:h-[640px]')
|
|
92
106
|
* @param {Function} renderTitle custom title renderer `(item) => ReactNode`; wins over the default
|
|
93
107
|
* @param {string} titleClassName default title class; per-item `titleClassName` overrides
|
|
108
|
+
* @param {string} descriptionClassName default description class; per-item overrides
|
|
94
109
|
* @param {boolean} showHeader show the label + `n / total` counter bar (default true)
|
|
110
|
+
* @param {boolean} showTitle global title visibility; per-item `showTitle` overrides (default true)
|
|
111
|
+
* @param {boolean} showDescription global description visibility; per-item overrides (default true)
|
|
112
|
+
* @param {boolean} showCta global CTA visibility; per-item `showCta` overrides (default true)
|
|
113
|
+
* @param {boolean} fullWidth drop the section's own vertical padding — the
|
|
114
|
+
* slide frame spans its container (default false)
|
|
115
|
+
* @param {boolean} rounded frame border + radius (default true)
|
|
116
|
+
* @param {'stack'|'header'} navPosition prev/next under the viewport, or in the
|
|
117
|
+
* header row beside the counter (default 'stack')
|
|
95
118
|
* @param {boolean} autoPlay auto-advance: timer for image slides, `ended` for video slides (default false)
|
|
96
119
|
* @param {number} autoPlayInterval image-slide timer in ms (default 5000)
|
|
97
120
|
* @param {Function} onNavigate `(href, event) => void` CTA click seam for SPA routing
|
|
98
121
|
* @param {Object} options embla options passthrough (default `{ align: 'center', loop: true }`)
|
|
122
|
+
* @param {node} children a STATIC overlay pinned over the stage — it does
|
|
123
|
+
* not travel with the slides (pointer-events pass
|
|
124
|
+
* through except on the child itself)
|
|
99
125
|
* @param {string} className extra classes on the section
|
|
100
126
|
*/
|
|
101
127
|
export default function FeaturedCarousel({
|
|
@@ -105,11 +131,19 @@ export default function FeaturedCarousel({
|
|
|
105
131
|
height = 'h-[440px] md:h-[640px]',
|
|
106
132
|
renderTitle,
|
|
107
133
|
titleClassName = '',
|
|
134
|
+
descriptionClassName = '',
|
|
108
135
|
showHeader = true,
|
|
136
|
+
showTitle = true,
|
|
137
|
+
showDescription = true,
|
|
138
|
+
showCta = true,
|
|
139
|
+
fullWidth = false,
|
|
140
|
+
rounded = true,
|
|
141
|
+
navPosition = 'stack',
|
|
109
142
|
autoPlay = false,
|
|
110
143
|
autoPlayInterval = 5000,
|
|
111
144
|
onNavigate,
|
|
112
145
|
options = { align: 'center', loop: true },
|
|
146
|
+
children,
|
|
113
147
|
className = '',
|
|
114
148
|
}) {
|
|
115
149
|
const [emblaRef, emblaApi] = useEmblaCarousel(options)
|
|
@@ -175,17 +209,32 @@ export default function FeaturedCarousel({
|
|
|
175
209
|
|
|
176
210
|
const showProgress = autoPlay && items.length > 1
|
|
177
211
|
|
|
212
|
+
const nav = (
|
|
213
|
+
<EmblaNav
|
|
214
|
+
onPrev={() => emblaApi?.scrollPrev()}
|
|
215
|
+
onNext={() => emblaApi?.scrollNext()}
|
|
216
|
+
canPrev={canPrev}
|
|
217
|
+
canNext={canNext}
|
|
218
|
+
placement={navPosition === 'header' ? 'inline' : 'stack'}
|
|
219
|
+
prevLabel="Previous slide"
|
|
220
|
+
nextLabel="Next slide"
|
|
221
|
+
/>
|
|
222
|
+
)
|
|
223
|
+
|
|
178
224
|
return (
|
|
179
225
|
<section
|
|
180
|
-
className={`kol-featured-carousel w-full ${className}`.trim()}
|
|
226
|
+
className={`kol-featured-carousel w-full ${fullWidth ? '' : 'py-16'} ${className}`.trim()}
|
|
181
227
|
onMouseEnter={autoPlay ? () => setPaused(true) : undefined}
|
|
182
228
|
onMouseLeave={autoPlay ? () => setPaused(false) : undefined}
|
|
183
229
|
>
|
|
184
230
|
{showHeader && (
|
|
185
|
-
<div className="mb-6 flex items-center gap-4">
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
231
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
232
|
+
<div className="flex items-center gap-4">
|
|
233
|
+
{/* migrated off the deleted legacy kol-label-* family — label renders as authored */}
|
|
234
|
+
<span className="kol-helper-12 text-auto">{sectionLabel}</span>
|
|
235
|
+
<span className="kol-mono-12 text-fg-64">{selectedIndex + 1} / {items.length}</span>
|
|
236
|
+
</div>
|
|
237
|
+
{navPosition === 'header' && nav}
|
|
189
238
|
</div>
|
|
190
239
|
)}
|
|
191
240
|
|
|
@@ -206,7 +255,9 @@ export default function FeaturedCarousel({
|
|
|
206
255
|
const active = i === selectedIndex
|
|
207
256
|
return (
|
|
208
257
|
<div key={i} className="kol-embla-slide">
|
|
209
|
-
<div
|
|
258
|
+
<div
|
|
259
|
+
className={`relative overflow-hidden bg-surface-secondary ${rounded ? 'rounded border border-fg-08' : ''} ${height}`.replace(/\s+/g, ' ')}
|
|
260
|
+
>
|
|
210
261
|
<SlideMedia
|
|
211
262
|
media={item.media}
|
|
212
263
|
onEnded={autoPlay && items.length > 1 && active ? advance : undefined}
|
|
@@ -214,11 +265,18 @@ export default function FeaturedCarousel({
|
|
|
214
265
|
/>
|
|
215
266
|
<div className="relative z-10 flex h-full w-full items-center justify-center p-6">
|
|
216
267
|
<OverlayGlassPanel maxWidth="max-w-[600px]">
|
|
217
|
-
{renderTitleNode(item)}
|
|
218
|
-
{item.
|
|
219
|
-
<
|
|
268
|
+
{(item.showTitle ?? showTitle) && renderTitleNode(item)}
|
|
269
|
+
{item.subtitle && (
|
|
270
|
+
<span className="kol-mono-10 text-fg-64">{item.subtitle}</span>
|
|
220
271
|
)}
|
|
221
|
-
{item.
|
|
272
|
+
{(item.showDescription ?? showDescription) && item.description && (
|
|
273
|
+
<p
|
|
274
|
+
className={`kol-mono-12 text-auto max-w-[600px] ${item.descriptionClassName || descriptionClassName}`.trim()}
|
|
275
|
+
>
|
|
276
|
+
{item.description}
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
{(item.showCta ?? showCta) && item.href && (
|
|
222
280
|
<a
|
|
223
281
|
href={item.href}
|
|
224
282
|
onClick={onNavigate ? (e) => onNavigate(item.href, e) : undefined}
|
|
@@ -235,24 +293,18 @@ export default function FeaturedCarousel({
|
|
|
235
293
|
})}
|
|
236
294
|
</div>
|
|
237
295
|
</div>
|
|
238
|
-
</div>
|
|
239
296
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<button
|
|
249
|
-
type="button"
|
|
250
|
-
className="kol-embla-btn border border-fg-16 hover:border-fg-32 text-auto"
|
|
251
|
-
aria-label="Next"
|
|
252
|
-
onClick={() => emblaApi?.scrollNext()}
|
|
253
|
-
disabled={!canNext}
|
|
254
|
-
>›</button>
|
|
297
|
+
{/* Pinned over the stage, OUTSIDE the viewport — a caption or badge
|
|
298
|
+
* that must not travel with the slides. Clicks pass through except
|
|
299
|
+
* on the child itself. */}
|
|
300
|
+
{children && (
|
|
301
|
+
<div className="pointer-events-none absolute inset-0 z-20 overflow-hidden">
|
|
302
|
+
<div className="pointer-events-auto w-full">{children}</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
255
305
|
</div>
|
|
306
|
+
|
|
307
|
+
{navPosition !== 'header' && nav}
|
|
256
308
|
</div>
|
|
257
309
|
</section>
|
|
258
310
|
)
|