@kolkrabbi/kol-component 0.108.0 → 0.110.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 +1 -1
- package/src/hooks/useTilt.js +2 -2
- package/src/index.js +1 -1
- package/src/molecules/BentoCard.jsx +4 -185
- package/src/molecules/TiltBento.jsx +191 -0
- package/src/organisms/MediaLibrary.jsx +49 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.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/hooks/useTilt.js
CHANGED
|
@@ -3,8 +3,8 @@ import { useMotionValue, useSpring, useTransform } from 'framer-motion'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Pointer-driven 3D tilt (framer-motion springs) — the ONE tilt hook.
|
|
6
|
-
* Ported from the monorepo's useBentoTiltMotion; TiltCard,
|
|
7
|
-
*
|
|
6
|
+
* Ported from the monorepo's useBentoTiltMotion; the Tilt family — TiltCard,
|
|
7
|
+
* TiltBento — composes this instead of forking their own.
|
|
8
8
|
*
|
|
9
9
|
* Returns `{ ref, style, onMouseMove, onMouseLeave, motionValues }` —
|
|
10
10
|
* spread `ref`/handlers on a `motion.div` and pass `style` to it.
|
package/src/index.js
CHANGED
|
@@ -116,7 +116,7 @@ export { default as Canvas, CanvasFrame, PanViewport, CANVAS_VIRTUAL_W, DEFAULT_
|
|
|
116
116
|
export { default as EditorShell } from './utilities/EditorShell.jsx'
|
|
117
117
|
export { default as GalleryCarousel } from './organisms/GalleryCarousel.jsx'
|
|
118
118
|
export { default as AsciiCursor } from './utilities/AsciiCursor.jsx'
|
|
119
|
-
export { default as BentoCard } from './molecules/
|
|
119
|
+
export { default as TiltBento, default as BentoCard } from './molecules/TiltBento.jsx'
|
|
120
120
|
export { default as Carousel } from './molecules/Carousel.jsx'
|
|
121
121
|
/* EmblaNav — THE prev/next pair. Exported so a consumer building its own embla
|
|
122
122
|
* stage reaches for it instead of re-typing the button markup, which is how the
|
|
@@ -1,187 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { motion } from 'framer-motion'
|
|
3
|
-
import HlsVideo from '../atoms/HlsVideo.jsx'
|
|
4
|
-
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
5
|
-
import Button from '../atoms/Button.jsx'
|
|
6
|
-
import Image from '../atoms/Image.jsx'
|
|
7
|
-
import useTilt from '../hooks/useTilt.js'
|
|
8
|
-
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
9
|
-
|
|
10
|
-
/* taxonomy-ok: nests the same-file Media helper plus DS atoms/molecules
|
|
11
|
-
* (HlsVideo, AssetPlaceholder, Button, Image) it composes — an organism. */
|
|
12
|
-
|
|
13
1
|
/**
|
|
14
|
-
*
|
|
15
|
-
* TiltCard
|
|
16
|
-
*
|
|
17
|
-
* module-load const — fixed on recreate).
|
|
2
|
+
* @deprecated 2026-08-27 — `BentoCard` is `TiltBento` under its old name (the
|
|
3
|
+
* Tilt family: `TiltCard` · `TiltBento` · `useTilt`). Alias kept; drops when no
|
|
4
|
+
* repo imports it (docs/operations/01-release/04-retirements.md).
|
|
18
5
|
*/
|
|
19
|
-
|
|
20
|
-
const [coarse, setCoarse] = useState(
|
|
21
|
-
() => typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
const mq = window.matchMedia('(pointer: coarse)')
|
|
26
|
-
const onChange = () => setCoarse(mq.matches)
|
|
27
|
-
mq.addEventListener('change', onChange)
|
|
28
|
-
return () => mq.removeEventListener('change', onChange)
|
|
29
|
-
}, [])
|
|
30
|
-
|
|
31
|
-
return coarse
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Media — internal, NOT exported. Sniffs `src` by extension and renders the
|
|
36
|
-
* right full-bleed media element: `.m3u8` → HlsVideo, `.mov|.mp4|.webm` →
|
|
37
|
-
* autoplay <video>, any other src → the Image molecule (its own broken-asset
|
|
38
|
-
* fallback), and no src → AssetPlaceholder. Positioning/fit arrive via
|
|
39
|
-
* `className`; Image gets an inline height so its base `h-auto` can't beat the
|
|
40
|
-
* `size-full` cover fill.
|
|
41
|
-
*/
|
|
42
|
-
function Media({ src, poster, className }) {
|
|
43
|
-
if (!src) return <AssetPlaceholder className={className} />
|
|
44
|
-
if (/\.m3u8$/i.test(src)) return <HlsVideo src={src} poster={poster} className={className} />
|
|
45
|
-
if (/\.(mov|mp4|webm)$/i.test(src)) {
|
|
46
|
-
return (
|
|
47
|
-
<video
|
|
48
|
-
src={src}
|
|
49
|
-
poster={poster}
|
|
50
|
-
autoPlay
|
|
51
|
-
muted
|
|
52
|
-
loop
|
|
53
|
-
playsInline
|
|
54
|
-
preload="auto"
|
|
55
|
-
className={className}
|
|
56
|
-
/>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
return <Image src={src} alt="" className={className} style={{ width: '100%', height: '100%' }} />
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* BentoCard — media hover-card for grid/bento walls. Full-bleed auto-detected
|
|
64
|
-
* media (HLS / video / image / placeholder) sits behind a content stack; on a
|
|
65
|
-
* fine pointer, hover reveals a darkening scrim plus subtitle / description /
|
|
66
|
-
* CTA over an always-visible title. No-hover (coarse-pointer) devices show
|
|
67
|
-
* everything statically and drop the media's pointer capture.
|
|
68
|
-
*
|
|
69
|
-
* Motion is gated: the pointer-following 3D tilt (shared `useTilt` framer
|
|
70
|
-
* springs — the monorepo's forked CSS `useBentoTilt` is gone) renders only on
|
|
71
|
-
* a fine pointer with motion allowed; reduced-motion, coarse pointer, or
|
|
72
|
-
* `enableTilt={false}` all fall back to a static card, and reduced-motion also
|
|
73
|
-
* drops the reveal's opacity transition.
|
|
74
|
-
*
|
|
75
|
-
* Zero CMS coupling — flat props. The CTA is a DS Button link (`<a href>`, no
|
|
76
|
-
* router import): `http*`/`mailto` open a new tab; any other href is a
|
|
77
|
-
* same-tab anchor whose `onNavigate(event)` seam lets an SPA intercept
|
|
78
|
-
* (preventDefault + its router) — wired capture-phase so it fires before the
|
|
79
|
-
* default navigation. Title/subtitle/description render exactly as authored;
|
|
80
|
-
* no casing transforms (author strings in their final case at the call site).
|
|
81
|
-
*
|
|
82
|
-
* @param {string} src media source; type auto-detected by extension
|
|
83
|
-
* @param {string} poster poster frame for HLS/video
|
|
84
|
-
* @param {ReactNode} title always-visible heading
|
|
85
|
-
* @param {ReactNode} subtitle hover-revealed line
|
|
86
|
-
* @param {ReactNode} description hover-revealed paragraph
|
|
87
|
-
* @param {string} href CTA target; `http*`/`mailto` → new-tab anchor, else same-tab (onNavigate seam)
|
|
88
|
-
* @param {Function} onNavigate (event) => void — same-tab CTA click seam (SPA intercept)
|
|
89
|
-
* @param {ReactNode} buttonLabel CTA label (no default — author it)
|
|
90
|
-
* @param {ReactNode} bodyContent extra content injected into the stack
|
|
91
|
-
* @param {number} overlayOpacity scrim darkness % over the media, 0 disables (default 60)
|
|
92
|
-
* @param {boolean} alignRight right-align the card (ms-auto) vs fill (size-full)
|
|
93
|
-
* @param {boolean} enableTilt master tilt switch (default true)
|
|
94
|
-
* @param {string} titleClassName title classes
|
|
95
|
-
* @param {string} contentClassName inner content-box classes
|
|
96
|
-
* @param {string} imageClassName media fit/position classes
|
|
97
|
-
* @param {string} contentStackClassName stack layout classes
|
|
98
|
-
* @param {string} className extra classes on the root
|
|
99
|
-
*/
|
|
100
|
-
export default function BentoCard({
|
|
101
|
-
src,
|
|
102
|
-
poster,
|
|
103
|
-
title,
|
|
104
|
-
subtitle,
|
|
105
|
-
description,
|
|
106
|
-
href,
|
|
107
|
-
onNavigate,
|
|
108
|
-
buttonLabel,
|
|
109
|
-
bodyContent = null,
|
|
110
|
-
overlayOpacity = 60,
|
|
111
|
-
alignRight = false,
|
|
112
|
-
enableTilt = true,
|
|
113
|
-
titleClassName = 'kol-sans-heading-01 text-absolute-white',
|
|
114
|
-
contentClassName = 'max-w-[384px]',
|
|
115
|
-
imageClassName = 'object-cover object-center',
|
|
116
|
-
contentStackClassName = 'relative z-20 h-full flex flex-col justify-start items-start gap-4 p-6 md:p-8',
|
|
117
|
-
className = '',
|
|
118
|
-
...rest
|
|
119
|
-
}) {
|
|
120
|
-
const reduced = usePrefersReducedMotion()
|
|
121
|
-
const coarse = useCoarsePointer()
|
|
122
|
-
const tilt = useTilt()
|
|
123
|
-
|
|
124
|
-
const tiltOff = !enableTilt || reduced || coarse
|
|
125
|
-
const Component = tiltOff ? 'div' : motion.div
|
|
126
|
-
const rootStyle = {
|
|
127
|
-
...(tiltOff ? {} : tilt.style),
|
|
128
|
-
backfaceVisibility: 'hidden',
|
|
129
|
-
WebkitBackfaceVisibility: 'hidden',
|
|
130
|
-
}
|
|
131
|
-
const tiltHandlers = tiltOff
|
|
132
|
-
? {}
|
|
133
|
-
: { ref: tilt.ref, onMouseMove: tilt.onMouseMove, onMouseLeave: tilt.onMouseLeave }
|
|
134
|
-
|
|
135
|
-
// Reveal choreography. Coarse (no-hover) devices show everything statically;
|
|
136
|
-
// fine pointers reveal on group-hover. The opacity transition is motion, so
|
|
137
|
-
// reduced-motion drops it (content still reveals, just without the fade).
|
|
138
|
-
const fade = reduced ? '' : 'transition-opacity duration-300'
|
|
139
|
-
const revealClass = `${coarse ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} ${fade}`.trim()
|
|
140
|
-
|
|
141
|
-
const mediaClass =
|
|
142
|
-
`absolute left-0 top-0 size-full rounded overflow-hidden ${imageClassName} ${coarse ? 'pointer-events-none' : ''}`.trim()
|
|
143
|
-
|
|
144
|
-
const isExternal = href && (href.startsWith('http') || href.startsWith('mailto'))
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<Component
|
|
148
|
-
className={`relative group ${alignRight ? 'ms-auto' : 'size-full'} ${className}`.trim()}
|
|
149
|
-
{...tiltHandlers}
|
|
150
|
-
{...rest}
|
|
151
|
-
style={{ ...rootStyle, ...rest.style }}
|
|
152
|
-
>
|
|
153
|
-
<Media src={src} poster={poster} className={mediaClass} />
|
|
154
|
-
|
|
155
|
-
<div className="relative z-10 flex size-full h-full flex-col justify-start items-start text-auto">
|
|
156
|
-
<div className={`relative z-10 ${contentClassName} w-full h-full self-stretch`}>
|
|
157
|
-
{overlayOpacity > 0 && (
|
|
158
|
-
<div
|
|
159
|
-
className={`absolute -inset-1 rounded ${coarse ? 'opacity-60' : 'opacity-0 group-hover:opacity-100'} ${fade} pointer-events-none`.trim()}
|
|
160
|
-
style={{ backgroundColor: `rgba(0, 0, 0, ${overlayOpacity / 100})` }}
|
|
161
|
-
/>
|
|
162
|
-
)}
|
|
163
|
-
<div className={contentStackClassName}>
|
|
164
|
-
{title && <h3 className={titleClassName}>{title}</h3>}
|
|
165
|
-
{subtitle && <p className={`kol-mono-text text-absolute-white ${revealClass}`}>{subtitle}</p>}
|
|
166
|
-
{description && <p className={`kol-mono-12 text-absolute-white pb-6 ${revealClass}`}>{description}</p>}
|
|
167
|
-
{bodyContent}
|
|
168
|
-
{href && (
|
|
169
|
-
<div className={revealClass}>
|
|
170
|
-
<Button
|
|
171
|
-
href={href}
|
|
172
|
-
variant="primary"
|
|
173
|
-
size="sm"
|
|
174
|
-
{...(isExternal
|
|
175
|
-
? { target: '_blank', rel: 'noreferrer noopener' }
|
|
176
|
-
: { onClickCapture: onNavigate })}
|
|
177
|
-
>
|
|
178
|
-
{buttonLabel}
|
|
179
|
-
</Button>
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
185
|
-
</Component>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
6
|
+
export { default } from './TiltBento.jsx'
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { motion } from 'framer-motion'
|
|
3
|
+
import HlsVideo from '../atoms/HlsVideo.jsx'
|
|
4
|
+
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
5
|
+
import Button from '../atoms/Button.jsx'
|
|
6
|
+
import Image from '../atoms/Image.jsx'
|
|
7
|
+
import useTilt from '../hooks/useTilt.js'
|
|
8
|
+
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
9
|
+
|
|
10
|
+
/* taxonomy-ok: nests the same-file Media helper plus DS atoms/molecules
|
|
11
|
+
* (HlsVideo, AssetPlaceholder, Button, Image) it composes — an organism. */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True on coarse-pointer (touch/no-hover) devices. Local to TiltBento, mirrors
|
|
15
|
+
* TiltCard's copy; re-evaluates on device/orientation change via the
|
|
16
|
+
* media-query change event (the monorepo source froze `useIsTouchDevice` in a
|
|
17
|
+
* module-load const — fixed on recreate).
|
|
18
|
+
*/
|
|
19
|
+
function useCoarsePointer() {
|
|
20
|
+
const [coarse, setCoarse] = useState(
|
|
21
|
+
() => typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const mq = window.matchMedia('(pointer: coarse)')
|
|
26
|
+
const onChange = () => setCoarse(mq.matches)
|
|
27
|
+
mq.addEventListener('change', onChange)
|
|
28
|
+
return () => mq.removeEventListener('change', onChange)
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
return coarse
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Media — internal, NOT exported. Sniffs `src` by extension and renders the
|
|
36
|
+
* right full-bleed media element: `.m3u8` → HlsVideo, `.mov|.mp4|.webm` →
|
|
37
|
+
* autoplay <video>, any other src → the Image molecule (its own broken-asset
|
|
38
|
+
* fallback), and no src → AssetPlaceholder. Positioning/fit arrive via
|
|
39
|
+
* `className`; Image gets an inline height so its base `h-auto` can't beat the
|
|
40
|
+
* `size-full` cover fill.
|
|
41
|
+
*/
|
|
42
|
+
function Media({ src, poster, className }) {
|
|
43
|
+
if (!src) return <AssetPlaceholder className={className} />
|
|
44
|
+
if (/\.m3u8$/i.test(src)) return <HlsVideo src={src} poster={poster} className={className} />
|
|
45
|
+
if (/\.(mov|mp4|webm)$/i.test(src)) {
|
|
46
|
+
return (
|
|
47
|
+
<video
|
|
48
|
+
src={src}
|
|
49
|
+
poster={poster}
|
|
50
|
+
autoPlay
|
|
51
|
+
muted
|
|
52
|
+
loop
|
|
53
|
+
playsInline
|
|
54
|
+
preload="auto"
|
|
55
|
+
className={className}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return <Image src={src} alt="" className={className} style={{ width: '100%', height: '100%' }} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* TiltBento — media hover-card for grid/bento walls, the Tilt family's composed
|
|
64
|
+
* tile (was `BentoCard` until 2026-08-27, user ruling: the three tilting things
|
|
65
|
+
* in the estate are ONE prefix family — `TiltCard` the bare frame, `TiltBento`
|
|
66
|
+
* this tile, `useTilt` the one hook; `BentoCard` is the alias on the retirement
|
|
67
|
+
* ledger). Full-bleed auto-detected
|
|
68
|
+
* media (HLS / video / image / placeholder) sits behind a content stack; on a
|
|
69
|
+
* fine pointer, hover reveals a darkening scrim plus subtitle / description /
|
|
70
|
+
* CTA over an always-visible title. No-hover (coarse-pointer) devices show
|
|
71
|
+
* everything statically and drop the media's pointer capture.
|
|
72
|
+
*
|
|
73
|
+
* Motion is gated: the pointer-following 3D tilt (shared `useTilt` framer
|
|
74
|
+
* springs — the monorepo's forked CSS `useBentoTilt` is gone) renders only on
|
|
75
|
+
* a fine pointer with motion allowed; reduced-motion, coarse pointer, or
|
|
76
|
+
* `enableTilt={false}` all fall back to a static card, and reduced-motion also
|
|
77
|
+
* drops the reveal's opacity transition.
|
|
78
|
+
*
|
|
79
|
+
* Zero CMS coupling — flat props. The CTA is a DS Button link (`<a href>`, no
|
|
80
|
+
* router import): `http*`/`mailto` open a new tab; any other href is a
|
|
81
|
+
* same-tab anchor whose `onNavigate(event)` seam lets an SPA intercept
|
|
82
|
+
* (preventDefault + its router) — wired capture-phase so it fires before the
|
|
83
|
+
* default navigation. Title/subtitle/description render exactly as authored;
|
|
84
|
+
* no casing transforms (author strings in their final case at the call site).
|
|
85
|
+
*
|
|
86
|
+
* @param {string} src media source; type auto-detected by extension
|
|
87
|
+
* @param {string} poster poster frame for HLS/video
|
|
88
|
+
* @param {ReactNode} title always-visible heading
|
|
89
|
+
* @param {ReactNode} subtitle hover-revealed line
|
|
90
|
+
* @param {ReactNode} description hover-revealed paragraph
|
|
91
|
+
* @param {string} href CTA target; `http*`/`mailto` → new-tab anchor, else same-tab (onNavigate seam)
|
|
92
|
+
* @param {Function} onNavigate (event) => void — same-tab CTA click seam (SPA intercept)
|
|
93
|
+
* @param {ReactNode} buttonLabel CTA label (no default — author it)
|
|
94
|
+
* @param {ReactNode} bodyContent extra content injected into the stack
|
|
95
|
+
* @param {number} overlayOpacity scrim darkness % over the media, 0 disables (default 60)
|
|
96
|
+
* @param {boolean} alignRight right-align the card (ms-auto) vs fill (size-full)
|
|
97
|
+
* @param {boolean} enableTilt master tilt switch (default true)
|
|
98
|
+
* @param {string} titleClassName title classes
|
|
99
|
+
* @param {string} contentClassName inner content-box classes
|
|
100
|
+
* @param {string} imageClassName media fit/position classes
|
|
101
|
+
* @param {string} contentStackClassName stack layout classes
|
|
102
|
+
* @param {string} className extra classes on the root
|
|
103
|
+
*/
|
|
104
|
+
export default function TiltBento({
|
|
105
|
+
src,
|
|
106
|
+
poster,
|
|
107
|
+
title,
|
|
108
|
+
subtitle,
|
|
109
|
+
description,
|
|
110
|
+
href,
|
|
111
|
+
onNavigate,
|
|
112
|
+
buttonLabel,
|
|
113
|
+
bodyContent = null,
|
|
114
|
+
overlayOpacity = 60,
|
|
115
|
+
alignRight = false,
|
|
116
|
+
enableTilt = true,
|
|
117
|
+
titleClassName = 'kol-sans-heading-01 text-absolute-white',
|
|
118
|
+
contentClassName = 'max-w-[384px]',
|
|
119
|
+
imageClassName = 'object-cover object-center',
|
|
120
|
+
contentStackClassName = 'relative z-20 h-full flex flex-col justify-start items-start gap-4 p-6 md:p-8',
|
|
121
|
+
className = '',
|
|
122
|
+
...rest
|
|
123
|
+
}) {
|
|
124
|
+
const reduced = usePrefersReducedMotion()
|
|
125
|
+
const coarse = useCoarsePointer()
|
|
126
|
+
const tilt = useTilt()
|
|
127
|
+
|
|
128
|
+
const tiltOff = !enableTilt || reduced || coarse
|
|
129
|
+
const Component = tiltOff ? 'div' : motion.div
|
|
130
|
+
const rootStyle = {
|
|
131
|
+
...(tiltOff ? {} : tilt.style),
|
|
132
|
+
backfaceVisibility: 'hidden',
|
|
133
|
+
WebkitBackfaceVisibility: 'hidden',
|
|
134
|
+
}
|
|
135
|
+
const tiltHandlers = tiltOff
|
|
136
|
+
? {}
|
|
137
|
+
: { ref: tilt.ref, onMouseMove: tilt.onMouseMove, onMouseLeave: tilt.onMouseLeave }
|
|
138
|
+
|
|
139
|
+
// Reveal choreography. Coarse (no-hover) devices show everything statically;
|
|
140
|
+
// fine pointers reveal on group-hover. The opacity transition is motion, so
|
|
141
|
+
// reduced-motion drops it (content still reveals, just without the fade).
|
|
142
|
+
const fade = reduced ? '' : 'transition-opacity duration-300'
|
|
143
|
+
const revealClass = `${coarse ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} ${fade}`.trim()
|
|
144
|
+
|
|
145
|
+
const mediaClass =
|
|
146
|
+
`absolute left-0 top-0 size-full rounded overflow-hidden ${imageClassName} ${coarse ? 'pointer-events-none' : ''}`.trim()
|
|
147
|
+
|
|
148
|
+
const isExternal = href && (href.startsWith('http') || href.startsWith('mailto'))
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Component
|
|
152
|
+
className={`relative group ${alignRight ? 'ms-auto' : 'size-full'} ${className}`.trim()}
|
|
153
|
+
{...tiltHandlers}
|
|
154
|
+
{...rest}
|
|
155
|
+
style={{ ...rootStyle, ...rest.style }}
|
|
156
|
+
>
|
|
157
|
+
<Media src={src} poster={poster} className={mediaClass} />
|
|
158
|
+
|
|
159
|
+
<div className="relative z-10 flex size-full h-full flex-col justify-start items-start text-auto">
|
|
160
|
+
<div className={`relative z-10 ${contentClassName} w-full h-full self-stretch`}>
|
|
161
|
+
{overlayOpacity > 0 && (
|
|
162
|
+
<div
|
|
163
|
+
className={`absolute -inset-1 rounded ${coarse ? 'opacity-60' : 'opacity-0 group-hover:opacity-100'} ${fade} pointer-events-none`.trim()}
|
|
164
|
+
style={{ backgroundColor: `rgba(0, 0, 0, ${overlayOpacity / 100})` }}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
<div className={contentStackClassName}>
|
|
168
|
+
{title && <h3 className={titleClassName}>{title}</h3>}
|
|
169
|
+
{subtitle && <p className={`kol-mono-text text-absolute-white ${revealClass}`}>{subtitle}</p>}
|
|
170
|
+
{description && <p className={`kol-mono-12 text-absolute-white pb-6 ${revealClass}`}>{description}</p>}
|
|
171
|
+
{bodyContent}
|
|
172
|
+
{href && (
|
|
173
|
+
<div className={revealClass}>
|
|
174
|
+
<Button
|
|
175
|
+
href={href}
|
|
176
|
+
variant="primary"
|
|
177
|
+
size="sm"
|
|
178
|
+
{...(isExternal
|
|
179
|
+
? { target: '_blank', rel: 'noreferrer noopener' }
|
|
180
|
+
: { onClickCapture: onNavigate })}
|
|
181
|
+
>
|
|
182
|
+
{buttonLabel}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</Component>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
+
import ActionButton from '../atoms/ActionButton.jsx'
|
|
3
4
|
import Button from '../atoms/Button.jsx'
|
|
4
5
|
import Divider from '../atoms/Divider.jsx'
|
|
5
6
|
import Input from '../atoms/Input.jsx'
|
|
6
7
|
import SegmentedToggle from '../atoms/SegmentedToggle.jsx'
|
|
8
|
+
import SizeOrDownload from '../atoms/SizeOrDownload.jsx'
|
|
7
9
|
import ViewToggle from '../atoms/ViewToggle.jsx'
|
|
8
10
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
+
import ContentCard from '../molecules/ContentCard.jsx'
|
|
12
|
+
import ContentRow from '../molecules/ContentRow.jsx'
|
|
11
13
|
import ContentFilters from './ContentFilters.jsx'
|
|
12
14
|
import MediaViewer from './MediaViewer.jsx'
|
|
13
15
|
import { SettingsChipRow, chipCls } from './SettingsPanel.jsx'
|
|
@@ -25,8 +27,12 @@ import { SettingsChipRow, chipCls } from './SettingsPanel.jsx'
|
|
|
25
27
|
* Same contract as kol-dashboards / kol-chess / kol-content.
|
|
26
28
|
*
|
|
27
29
|
* COMPOSED, NOT BUILT. Every part is an existing DS member:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* ContentCard — the grid tile, `variant="default"` (thumb · download in the
|
|
31
|
+
* frame corner · title · date · size-or-download · inline actions)
|
|
32
|
+
* ContentRow — the list row, `variant="default"` (thumb · title · date · size · actions)
|
|
33
|
+
* MediaCard / MediaRow retired 2026-08-26 (ContentSetRetirement);
|
|
34
|
+
* this was the DS's own last composition of them, swapped 2026-08-27
|
|
35
|
+
* onto kol-r2b2 FileList's ContentCard / ContentRow shape.
|
|
30
36
|
* MediaViewer — the lightbox, via its `actions` slot
|
|
31
37
|
* ContentFilters — the PICKER's chrome (search, kind filter, view toggle, N-of-M)
|
|
32
38
|
* FullscreenOverlay — the picker's scrim, dismissal and close button
|
|
@@ -650,7 +656,7 @@ function Toolbar({ viewMode, onViewMode }) {
|
|
|
650
656
|
* picker; `onPick` is the only difference between them. */
|
|
651
657
|
function FilesBody({ files, viewMode, onOpen, onPick }) {
|
|
652
658
|
const { mediaUrl, viewable, prefix, stats, more, pageSize, showMore } = useMediaLibrary()
|
|
653
|
-
const [
|
|
659
|
+
const [, copy] = useCopy()
|
|
654
660
|
|
|
655
661
|
if (files.length === 0) {
|
|
656
662
|
return (
|
|
@@ -680,46 +686,60 @@ function FilesBody({ files, viewMode, onOpen, onPick }) {
|
|
|
680
686
|
</div>
|
|
681
687
|
)
|
|
682
688
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
689
|
+
/* INLINE controls, never labelled Buttons (ContentSetRetirement, 2026-08-27):
|
|
690
|
+
* the card's `actions` float in the plate's corner beside the title, so a
|
|
691
|
+
* labelled Button there sits on the copy — kol-r2b2's column of
|
|
692
|
+
* `.kol-inline-control`s is the shape that fits. The card's download is the
|
|
693
|
+
* frame-corner `control` + the size slot (SizeOrDownload); the row keeps its
|
|
694
|
+
* glyph beside Copy. `Use` (picker only) is the same control wearing `plus`. */
|
|
695
|
+
const actionsFor = (row, form) => (
|
|
696
|
+
<div className={form === 'row' ? 'flex items-center gap-2' : 'flex h-full flex-col items-center justify-between'}>
|
|
697
|
+
{onPick && <ActionButton chrome="inline" size="sm" icon="plus" confirmIcon="check" label="Use" confirmLabel="Used" onAction={() => onPick(row)} />}
|
|
698
|
+
<ActionButton chrome="inline" size="sm" icon="copy" confirmIcon="check" label="Copy URL" confirmLabel="Copied" onAction={() => copy(urlFor(row))} />
|
|
699
|
+
{form === 'row' && (
|
|
700
|
+
<ActionButton chrome="inline" size="sm" icon="download" confirmIcon="check" label="Download" confirmLabel="Downloaded" href={urlFor(row)} />
|
|
701
|
+
)}
|
|
690
702
|
</div>
|
|
691
703
|
)
|
|
692
|
-
|
|
693
|
-
|
|
704
|
+
/* the title voice is the family's ruled default (heading-04 card / heading-05
|
|
705
|
+
* row, truncated by ContentText); the full key rides as the tooltip, as before */
|
|
706
|
+
const nameFor = (row) => <span title={row.key}>{row.displayKey}</span>
|
|
707
|
+
const date = (row) => (row.uploaded ? String(row.uploaded).slice(0, 10) : undefined)
|
|
708
|
+
const size = (row) => formatSize(row.size) || undefined
|
|
694
709
|
|
|
695
710
|
return (
|
|
696
711
|
<>
|
|
697
712
|
{viewMode === 'list' ? (
|
|
698
|
-
<
|
|
713
|
+
<div className="kol-media-list">
|
|
699
714
|
{files.map((row) => (
|
|
700
|
-
<
|
|
715
|
+
<ContentRow
|
|
701
716
|
key={row.key}
|
|
702
|
-
|
|
703
|
-
|
|
717
|
+
variant="default"
|
|
718
|
+
media={thumbFor(row)}
|
|
719
|
+
title={nameFor(row)}
|
|
704
720
|
date={date(row)}
|
|
705
|
-
size={
|
|
706
|
-
actions={actionsFor(row)}
|
|
721
|
+
size={size(row)}
|
|
722
|
+
actions={actionsFor(row, 'row')}
|
|
707
723
|
/>
|
|
708
724
|
))}
|
|
709
|
-
</
|
|
725
|
+
</div>
|
|
710
726
|
) : (
|
|
711
|
-
<
|
|
727
|
+
<div className="kol-media-grid">
|
|
712
728
|
{files.map((row) => (
|
|
713
|
-
<
|
|
729
|
+
<ContentCard
|
|
714
730
|
key={row.key}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
731
|
+
variant="default"
|
|
732
|
+
media={thumbFor(row)}
|
|
733
|
+
control={
|
|
734
|
+
<ActionButton chrome="media" icon="download" confirmIcon="check" label="Download" confirmLabel="Downloaded" href={urlFor(row)} />
|
|
735
|
+
}
|
|
736
|
+
title={nameFor(row)}
|
|
737
|
+
date={date(row)}
|
|
738
|
+
size={size(row) && <SizeOrDownload href={urlFor(row)}>{size(row)}</SizeOrDownload>}
|
|
739
|
+
actions={actionsFor(row, 'card')}
|
|
720
740
|
/>
|
|
721
741
|
))}
|
|
722
|
-
</
|
|
742
|
+
</div>
|
|
723
743
|
)}
|
|
724
744
|
{more > 0 && (
|
|
725
745
|
<button
|