@kolkrabbi/kol-component 0.131.0 → 0.133.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/atoms/RotaryDial.jsx +18 -1
- package/src/index.js +0 -2
- package/src/molecules/Slider.jsx +182 -35
- package/src/molecules/MediaCard.jsx +0 -103
- package/src/molecules/MediaRow.jsx +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.133.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/atoms/RotaryDial.jsx
CHANGED
|
@@ -30,6 +30,7 @@ const snapTo = (v, min, max, step) => {
|
|
|
30
30
|
* @param {number} size dial px size (default 80); derives ring + disc radii
|
|
31
31
|
* @param {boolean} disabled blocks drag + keyboard and dims the control (default false)
|
|
32
32
|
* @param {Function} formatValue optional readout formatter (value: number) => string; default `${value}%`
|
|
33
|
+
* @param {number} defaultValue alt-click the dial resets to this (falls back to `min`) — Slider carries the same gesture
|
|
33
34
|
*/
|
|
34
35
|
export default function RotaryDial({
|
|
35
36
|
label,
|
|
@@ -41,6 +42,7 @@ export default function RotaryDial({
|
|
|
41
42
|
size = 80,
|
|
42
43
|
disabled = false,
|
|
43
44
|
formatValue,
|
|
45
|
+
defaultValue,
|
|
44
46
|
}) {
|
|
45
47
|
const [isDragging, setIsDragging] = useState(false)
|
|
46
48
|
const [localValue, setLocalValue] = useState(value) // visual buffer — updates every move
|
|
@@ -102,6 +104,21 @@ export default function RotaryDial({
|
|
|
102
104
|
onChange?.(next)
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/* alt-click resets — the same gesture Slider carries (SliderDualThumbAndPlayhead,
|
|
108
|
+
* 2026-08-30). The two components share one value-control contract, and a
|
|
109
|
+
* reset that worked on the fader but not the knob would split it. Handled on
|
|
110
|
+
* pointer-down BEFORE the drag starts, so an alt-click never also nudges. */
|
|
111
|
+
const handlePointerDownOrReset = (e) => {
|
|
112
|
+
if (e.altKey) {
|
|
113
|
+
e.preventDefault()
|
|
114
|
+
const next = defaultValue ?? min
|
|
115
|
+
setLocalValue(next)
|
|
116
|
+
onChange?.(next)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
handlePointerDown(e)
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
const outerRadius = size / 2
|
|
106
123
|
const innerRadius = (size * 0.7) / 2
|
|
107
124
|
const strokeWidth = 2
|
|
@@ -124,7 +141,7 @@ export default function RotaryDial({
|
|
|
124
141
|
transform: `rotate(${angle}deg)`,
|
|
125
142
|
willChange: isDragging ? 'transform' : 'auto',
|
|
126
143
|
}}
|
|
127
|
-
onPointerDown={disabled ? undefined :
|
|
144
|
+
onPointerDown={disabled ? undefined : handlePointerDownOrReset}
|
|
128
145
|
onKeyDown={disabled ? undefined : handleKeyDown}
|
|
129
146
|
>
|
|
130
147
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ overflow: 'visible' }}>
|
package/src/index.js
CHANGED
|
@@ -82,8 +82,6 @@ export { default as Dropdown } from './molecules/Dropdown.jsx'
|
|
|
82
82
|
export { default as FieldRow, StatusChip } from './molecules/FieldRow.jsx'
|
|
83
83
|
export { default as FramedMediaBand } from './organisms/FramedMediaBand.jsx'
|
|
84
84
|
export { default as Image } from './atoms/Image.jsx'
|
|
85
|
-
export { default as MediaCard } from './molecules/MediaCard.jsx'
|
|
86
|
-
export { default as MediaRow } from './molecules/MediaRow.jsx'
|
|
87
85
|
|
|
88
86
|
/* content-card system (2026-08-15) — the ruled card/row family:
|
|
89
87
|
* docs/documentation/03-components/06-content-card-system.md */
|
package/src/molecules/Slider.jsx
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import { useEffect, useId, useMemo, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import Input from '../atoms/Input.jsx'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Slider — range slider with label and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
5
|
+
* Slider — range slider with label and a value readout. The LINEAR VARIANT of
|
|
6
|
+
* RotaryDial (atoms/RotaryDial.jsx): both implement the shared value-control
|
|
7
|
+
* contract — `value` / `min` / `max` / `step` / `onChange(next: number)` /
|
|
8
|
+
* `label` / `size` / `disabled` / `formatValue` / `defaultValue`.
|
|
9
9
|
* Controlled; onChange always fires with the plain number.
|
|
10
10
|
*
|
|
11
|
-
* One bare inline row: label · track ·
|
|
12
|
-
* `
|
|
13
|
-
* the only slider.)
|
|
11
|
+
* One bare inline row: label · track · readout. (Bordered `default` and chip
|
|
12
|
+
* `subtle` variants retired 2026-07-08 — minimal is the only single slider.)
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Track color is exposed as the `--kol-slider-track` CSS variable on
|
|
15
|
+
* `.slider-black`; override per-instance via
|
|
16
|
+
* style={{ '--kol-slider-track': '...' }}.
|
|
17
|
+
*
|
|
18
|
+
* DUAL + PLAYHEAD (SliderDualThumbAndPlayhead, kol-mirror 2026-08-30):
|
|
19
|
+
* `variant="dual"` stacks two native ranges on one rail for an in/out pair,
|
|
20
|
+
* with an optional draggable playhead. Built because mirror was maintaining a
|
|
21
|
+
* verbatim copy of this component's CSS at 10 call sites to get it — 8 of which
|
|
22
|
+
* needed nothing else. The clamp lives HERE, not in the consumer: an in-thumb
|
|
23
|
+
* that can cross its out-thumb is a bug every caller would re-fix.
|
|
20
24
|
*
|
|
21
25
|
* @param {Object} props
|
|
22
26
|
* @param {string} props.label - Slider label text
|
|
@@ -31,6 +35,15 @@ import Input from '../atoms/Input.jsx'
|
|
|
31
35
|
* @param {string} props.className - Additional wrapper classes
|
|
32
36
|
* @param {number} props.displayWidth - Width of the value readout, in characters (default: 6)
|
|
33
37
|
* @param {string} props.fontSize - Font size for label/value (e.g., '11px')
|
|
38
|
+
* @param {number} props.defaultValue - alt-click the control resets to this (falls back to `min`). Same contract on RotaryDial
|
|
39
|
+
* @param {'input'|'value'|'none'} props.readout - `input` (default) an editable Input · `value` a plain right-aligned span, RotaryDial's own display-only readout · `none`
|
|
40
|
+
* @param {'minimal'|'dual'} props.variant - `dual` = two thumbs on one rail
|
|
41
|
+
* @param {number} props.value2 - dual only — the out value
|
|
42
|
+
* @param {Function} props.onChange2 - dual only — (next: number) => void for the out thumb
|
|
43
|
+
* @param {string} props.label1 - dual only — replaces the formatted in value above the rail
|
|
44
|
+
* @param {string} props.label2 - dual only — replaces the formatted out value
|
|
45
|
+
* @param {number|null} props.playhead - dual only — marker position in min…max; null renders nothing and attaches no listeners
|
|
46
|
+
* @param {Function} props.onPlayheadChange - dual only — (next: number) => void; omit and the marker is not draggable and the rail does not seek
|
|
34
47
|
*/
|
|
35
48
|
const Slider = ({
|
|
36
49
|
label,
|
|
@@ -45,6 +58,15 @@ const Slider = ({
|
|
|
45
58
|
className = '',
|
|
46
59
|
displayWidth = 6,
|
|
47
60
|
fontSize,
|
|
61
|
+
defaultValue,
|
|
62
|
+
readout = 'input',
|
|
63
|
+
variant = 'minimal',
|
|
64
|
+
value2,
|
|
65
|
+
onChange2,
|
|
66
|
+
label1,
|
|
67
|
+
label2,
|
|
68
|
+
playhead = null,
|
|
69
|
+
onPlayheadChange,
|
|
48
70
|
}) => {
|
|
49
71
|
/* Label ↔ input pairing — the <label> is a sibling of the range input, so
|
|
50
72
|
* without an htmlFor/id pair the visible label confers no accessible name. */
|
|
@@ -64,13 +86,16 @@ const Slider = ({
|
|
|
64
86
|
return decimalPart ? decimalPart.length : 2
|
|
65
87
|
}, [formatValue, step])
|
|
66
88
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return Number(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const fmt = useCallback(
|
|
90
|
+
(v) => {
|
|
91
|
+
if (formatValue) return String(formatValue(v))
|
|
92
|
+
if (decimals && decimals > 0) return Number(v).toFixed(decimals)
|
|
93
|
+
return String(Math.round(v))
|
|
94
|
+
},
|
|
95
|
+
[decimals, formatValue],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const displayValue = useMemo(() => fmt(value), [fmt, value])
|
|
74
99
|
|
|
75
100
|
/* Editable readout — local string state lets the user type intermediate
|
|
76
101
|
* values (e.g. "-" while entering a negative) without clamping mid-keystroke.
|
|
@@ -79,6 +104,39 @@ const Slider = ({
|
|
|
79
104
|
const [editing, setEditing] = useState(false)
|
|
80
105
|
useEffect(() => { if (!editing) setDraft(displayValue) }, [displayValue, editing])
|
|
81
106
|
|
|
107
|
+
/* EVERY hook runs before the dual branch returns. The prior art in kol-mirror
|
|
108
|
+
* called useMemo *after* its early return, so hook order changed with the
|
|
109
|
+
* variant — it survived only because no call site switched variant at
|
|
110
|
+
* runtime. Not carried. */
|
|
111
|
+
const trackRef = useRef(null)
|
|
112
|
+
|
|
113
|
+
const seekTo = useCallback(
|
|
114
|
+
(clientX) => {
|
|
115
|
+
const el = trackRef.current
|
|
116
|
+
if (!onPlayheadChange || !el) return
|
|
117
|
+
const rect = el.getBoundingClientRect()
|
|
118
|
+
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
|
119
|
+
onPlayheadChange(min + ratio * (max - min))
|
|
120
|
+
},
|
|
121
|
+
[min, max, onPlayheadChange],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const handlePlayheadDrag = useCallback(
|
|
125
|
+
(e) => {
|
|
126
|
+
if (!onPlayheadChange) return
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
seekTo(e.clientX)
|
|
129
|
+
const onMove = (me) => seekTo(me.clientX)
|
|
130
|
+
const onUp = () => {
|
|
131
|
+
window.removeEventListener('pointermove', onMove)
|
|
132
|
+
window.removeEventListener('pointerup', onUp)
|
|
133
|
+
}
|
|
134
|
+
window.addEventListener('pointermove', onMove)
|
|
135
|
+
window.addEventListener('pointerup', onUp)
|
|
136
|
+
},
|
|
137
|
+
[onPlayheadChange, seekTo],
|
|
138
|
+
)
|
|
139
|
+
|
|
82
140
|
const commit = () => {
|
|
83
141
|
setEditing(false)
|
|
84
142
|
const parsed = Number(draft)
|
|
@@ -96,8 +154,84 @@ const Slider = ({
|
|
|
96
154
|
if (e.key === 'Escape') { setDraft(displayValue); setEditing(false); e.currentTarget.blur() }
|
|
97
155
|
}
|
|
98
156
|
|
|
157
|
+
/* alt-click anywhere on the control resets — RotaryDial carries the same
|
|
158
|
+
* gesture, so a reset that worked on the knob and not the fader would be the
|
|
159
|
+
* shared value-control contract splitting again. */
|
|
160
|
+
const onAltReset = (e) => {
|
|
161
|
+
if (!e.altKey || !onChange || disabled) return
|
|
162
|
+
e.preventDefault()
|
|
163
|
+
onChange(defaultValue ?? min)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (variant === 'dual') {
|
|
167
|
+
const v1 = value ?? min
|
|
168
|
+
const v2 = value2 ?? max
|
|
169
|
+
const showLabels = label1 != null || label2 != null
|
|
170
|
+
const ratio = max > min ? (playhead - min) / (max - min) : 0
|
|
171
|
+
return (
|
|
172
|
+
<div className={`control-slider gap-3 shadow-none ${className}`}>
|
|
173
|
+
{label && (
|
|
174
|
+
<label
|
|
175
|
+
className={`kol-helper-12 whitespace-nowrap shrink-0 w-fit ${disabled ? 'opacity-50' : ''}`}
|
|
176
|
+
style={fontSize ? { fontSize } : undefined}
|
|
177
|
+
>
|
|
178
|
+
{label}
|
|
179
|
+
</label>
|
|
180
|
+
)}
|
|
181
|
+
<div className="flex-1">
|
|
182
|
+
{showLabels && (
|
|
183
|
+
<div
|
|
184
|
+
className="kol-helper-12 text-fg-32 flex items-center justify-between"
|
|
185
|
+
style={{ marginBottom: '-2px' }}
|
|
186
|
+
>
|
|
187
|
+
<span>{label1 ?? fmt(v1)}</span>
|
|
188
|
+
<span>{label2 ?? fmt(v2)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
<div
|
|
192
|
+
ref={trackRef}
|
|
193
|
+
className="kol-slider-dual"
|
|
194
|
+
style={onPlayheadChange ? { cursor: 'pointer' } : undefined}
|
|
195
|
+
/* the whole rail seeks, not just the marker — hunting a 3px target
|
|
196
|
+
* to scrub is the thing that makes a playhead feel broken */
|
|
197
|
+
onPointerDown={(e) => { if (e.target === e.currentTarget) seekTo(e.clientX) }}
|
|
198
|
+
>
|
|
199
|
+
<div className="kol-slider-dual-rail" />
|
|
200
|
+
{playhead != null && max > min && (
|
|
201
|
+
<div
|
|
202
|
+
className="kol-slider-dual-playhead"
|
|
203
|
+
/* half a thumb in from each end, so the marker lines up with
|
|
204
|
+
* where a thumb CENTRE can actually reach */
|
|
205
|
+
style={{ left: `calc(6px + (100% - 12px) * ${ratio})` }}
|
|
206
|
+
onPointerDown={handlePlayheadDrag}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
<input
|
|
210
|
+
type="range"
|
|
211
|
+
min={min} max={max} step={step} value={v1}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
aria-label={label1 ?? 'In'}
|
|
214
|
+
onChange={(e) => onChange?.(Math.min(Number(e.target.value), v2))}
|
|
215
|
+
className="kol-slider-range kol-slider-range--in"
|
|
216
|
+
style={{ zIndex: 1 }}
|
|
217
|
+
/>
|
|
218
|
+
<input
|
|
219
|
+
type="range"
|
|
220
|
+
min={min} max={max} step={step} value={v2}
|
|
221
|
+
disabled={disabled}
|
|
222
|
+
aria-label={label2 ?? 'Out'}
|
|
223
|
+
onChange={(e) => onChange2?.(Math.max(Number(e.target.value), v1))}
|
|
224
|
+
className="kol-slider-range kol-slider-range--out"
|
|
225
|
+
style={{ zIndex: 2 }}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
99
233
|
return (
|
|
100
|
-
<div className={`control-slider gap-3 shadow-none ${className}`}>
|
|
234
|
+
<div className={`control-slider gap-3 shadow-none ${className}`} onClick={onAltReset}>
|
|
101
235
|
{label && (
|
|
102
236
|
<label
|
|
103
237
|
htmlFor={sliderId}
|
|
@@ -119,20 +253,33 @@ const Slider = ({
|
|
|
119
253
|
className={`slider-black cursor-pointer disabled:cursor-default disabled:opacity-50 ${size ? 'flex-none' : 'flex-1 w-full'}`}
|
|
120
254
|
style={size ? { width: size } : undefined}
|
|
121
255
|
/>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
256
|
+
{readout === 'input' && (
|
|
257
|
+
<Input
|
|
258
|
+
type="text"
|
|
259
|
+
inputMode="decimal"
|
|
260
|
+
variant="filled"
|
|
261
|
+
size="sm"
|
|
262
|
+
chars={displayWidth}
|
|
263
|
+
value={draft}
|
|
264
|
+
disabled={disabled}
|
|
265
|
+
onFocus={(e) => { setEditing(true); e.target.select() }}
|
|
266
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
267
|
+
onBlur={commit}
|
|
268
|
+
onKeyDown={onKeyDown}
|
|
269
|
+
inputClassName="text-center"
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
{/* `value` is RotaryDial's readout, not a second design — a mixer running
|
|
273
|
+
* ~23 faders in a 24px row cannot afford an input chip on every one, and
|
|
274
|
+
* that is why the easy call sites could not move. */}
|
|
275
|
+
{readout === 'value' && (
|
|
276
|
+
<span
|
|
277
|
+
className={`kol-helper-12 shrink-0 w-fit text-right ${disabled ? 'opacity-50' : ''}`}
|
|
278
|
+
style={fontSize ? { fontSize } : undefined}
|
|
279
|
+
>
|
|
280
|
+
{displayValue}
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
136
283
|
</div>
|
|
137
284
|
)
|
|
138
285
|
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { Icon } from '@kolkrabbi/kol-icons'
|
|
2
|
-
|
|
3
|
-
/* taxonomy-ok: nests kol-icons's Icon (a package import the relative-import
|
|
4
|
-
* check can't see) plus the same-file SelectIndicator. */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @deprecated 2026-08-26 — absorbed by `ContentCard variant="default"` in @kolkrabbi/kol-component
|
|
8
|
-
* (the Content Set, 2026-08-15). Step 1 of the retirement wave: this export
|
|
9
|
-
* stays and renders unchanged until the next major, then it is removed.
|
|
10
|
-
* Consumers: swap on your next bump. Map + row-by-row diff:
|
|
11
|
-
* docs/documentation/03-components/06-content-card-system.md.
|
|
12
|
-
*
|
|
13
|
-
* SelectIndicator — passive square check indicator for multi-select rows and
|
|
14
|
-
* cards. `on` = checked. Deliberately NOT ToggleCheckbox: that is a labeled
|
|
15
|
-
* form control with a real <input>, which double-fires inside a click-target
|
|
16
|
-
* card and misleads assistive tech — here the CARD is the toggle, this is
|
|
17
|
-
* only its visual state. Inline CSS vars because `bg-fg-default` isn't a
|
|
18
|
-
* generated Tailwind utility; the checked fill reads from
|
|
19
|
-
* `--kol-surface-on-primary` (solid fg, theme-correct).
|
|
20
|
-
*
|
|
21
|
-
* Shared by MediaCard and MediaRow; not exported from the package barrel.
|
|
22
|
-
*/
|
|
23
|
-
export function SelectIndicator({ on = false }) {
|
|
24
|
-
return (
|
|
25
|
-
<span
|
|
26
|
-
className="w-4 h-4 shrink-0 rounded-sm border flex items-center justify-center transition-colors"
|
|
27
|
-
style={
|
|
28
|
-
on
|
|
29
|
-
? { background: 'var(--kol-surface-on-primary)', borderColor: 'var(--kol-surface-on-primary)' }
|
|
30
|
-
: { background: 'var(--kol-fg-ab-16, rgba(0,0,0,0.15))', borderColor: 'var(--kol-fg-ab-48, rgba(0,0,0,0.4))' }
|
|
31
|
-
}
|
|
32
|
-
aria-hidden="true"
|
|
33
|
-
>
|
|
34
|
-
{on && <Icon name="check" size={11} style={{ color: 'var(--kol-surface-primary)' }} />}
|
|
35
|
-
</span>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* MediaCard — grid tile for one media object: square thumbnail with a
|
|
41
|
-
* top-right download overlay (or a top-left select checkbox in select mode),
|
|
42
|
-
* then name / meta / actions stacked below. The grid-view counterpart to
|
|
43
|
-
* MediaRow (same slot contract).
|
|
44
|
-
*
|
|
45
|
-
* Presentational — the parent supplies rendered slots; no media loading,
|
|
46
|
-
* fetch, or rename logic lives here. Grid track sizing belongs to the parent
|
|
47
|
-
* list; the card just fills its cell.
|
|
48
|
-
*
|
|
49
|
-
* @param {ReactNode} thumb square thumbnail (image/video/placeholder)
|
|
50
|
-
* @param {ReactNode} name name cell (plain text or an inline editor)
|
|
51
|
-
* @param {string} meta one-line secondary text (e.g. "1.2 MB · 2026-06-19")
|
|
52
|
-
* @param {ReactNode} actions row of action buttons (hidden in select mode)
|
|
53
|
-
* @param {string} downloadHref href for the overlay download button (omit to hide)
|
|
54
|
-
* @param {boolean} selectMode selection mode on → checkbox replaces download, whole card toggles
|
|
55
|
-
* @param {boolean} selected this card is selected (stronger border + checked box)
|
|
56
|
-
* @param {Function} onSelect (event) => void — card click in select mode; gets shiftKey for range
|
|
57
|
-
*/
|
|
58
|
-
export default function MediaCard({
|
|
59
|
-
thumb,
|
|
60
|
-
name,
|
|
61
|
-
meta,
|
|
62
|
-
actions,
|
|
63
|
-
downloadHref,
|
|
64
|
-
selectMode = false,
|
|
65
|
-
selected = false,
|
|
66
|
-
onSelect,
|
|
67
|
-
}) {
|
|
68
|
-
return (
|
|
69
|
-
<li
|
|
70
|
-
onClick={selectMode ? onSelect : undefined}
|
|
71
|
-
className={`flex flex-col rounded overflow-hidden border bg-fg-02 ${selectMode ? 'cursor-pointer select-none' : ''}`}
|
|
72
|
-
style={{ borderColor: selected ? 'var(--kol-fg-64)' : 'var(--kol-fg-12)' }}
|
|
73
|
-
>
|
|
74
|
-
<div className="aspect-square relative">
|
|
75
|
-
{thumb}
|
|
76
|
-
{selectMode ? (
|
|
77
|
-
<span
|
|
78
|
-
className="kol-frame-control kol-frame-control--top-left rounded p-1"
|
|
79
|
-
style={{ background: 'var(--kol-fg-ab-12, rgba(0,0,0,0.4))', backdropFilter: 'blur(4px)' }}
|
|
80
|
-
>
|
|
81
|
-
<SelectIndicator on={selected} />
|
|
82
|
-
</span>
|
|
83
|
-
) : downloadHref ? (
|
|
84
|
-
<a
|
|
85
|
-
href={downloadHref}
|
|
86
|
-
aria-label="Download"
|
|
87
|
-
title="Download"
|
|
88
|
-
className="kol-frame-control inline-flex items-center justify-center w-8 h-8 rounded text-emphasis hover:bg-fg-ab-24 transition-colors"
|
|
89
|
-
style={{ background: 'var(--kol-fg-ab-12, rgba(0,0,0,0.4))', backdropFilter: 'blur(4px)' }}
|
|
90
|
-
onClick={(e) => e.stopPropagation()}
|
|
91
|
-
>
|
|
92
|
-
<Icon name="download" size={16} />
|
|
93
|
-
</a>
|
|
94
|
-
) : null}
|
|
95
|
-
</div>
|
|
96
|
-
<div className="p-3 flex flex-col gap-2">
|
|
97
|
-
{name}
|
|
98
|
-
<p className="kol-mono-12 text-fg-48">{meta}</p>
|
|
99
|
-
{!selectMode && actions}
|
|
100
|
-
</div>
|
|
101
|
-
</li>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { SelectIndicator } from './MediaCard.jsx'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @deprecated 2026-08-26 — absorbed by `ContentRow variant="default"` in @kolkrabbi/kol-component
|
|
5
|
-
* (the Content Set, 2026-08-15). Step 1 of the retirement wave: this export
|
|
6
|
-
* stays and renders unchanged until the next major, then it is removed.
|
|
7
|
-
* Consumers: swap on your next bump. Map + row-by-row diff:
|
|
8
|
-
* docs/documentation/03-components/06-content-card-system.md.
|
|
9
|
-
*
|
|
10
|
-
* MediaRow — list row for one media object: optional select checkbox, small
|
|
11
|
-
* thumbnail, name (flex), fixed-width date + size columns, then actions.
|
|
12
|
-
* The list-view counterpart to MediaCard (same slot contract).
|
|
13
|
-
*
|
|
14
|
-
* Presentational — the parent supplies rendered slots; interaction outside
|
|
15
|
-
* select mode lives in the thumb / name / actions slots. Column widths are
|
|
16
|
-
* consumer-tunable via `dateWidth` / `sizeWidth`.
|
|
17
|
-
*
|
|
18
|
-
* @param {ReactNode} thumb small thumbnail (48px square)
|
|
19
|
-
* @param {ReactNode} name name cell (plain text or an inline editor)
|
|
20
|
-
* @param {string} date right-aligned date column
|
|
21
|
-
* @param {string} size right-aligned size column
|
|
22
|
-
* @param {ReactNode} actions row of action buttons (hidden in select mode)
|
|
23
|
-
* @param {string} dateWidth Tailwind width class for the date column
|
|
24
|
-
* @param {string} sizeWidth Tailwind width class for the size column
|
|
25
|
-
* @param {boolean} selectMode selection mode on → checkbox shown, whole row toggles, actions hidden
|
|
26
|
-
* @param {boolean} selected this row is selected (highlight + checked box)
|
|
27
|
-
* @param {Function} onSelect (event) => void — row click in select mode; gets shiftKey for range
|
|
28
|
-
*/
|
|
29
|
-
export default function MediaRow({
|
|
30
|
-
thumb,
|
|
31
|
-
name,
|
|
32
|
-
date,
|
|
33
|
-
size,
|
|
34
|
-
actions,
|
|
35
|
-
dateWidth = 'w-24',
|
|
36
|
-
sizeWidth = 'w-20',
|
|
37
|
-
selectMode = false,
|
|
38
|
-
selected = false,
|
|
39
|
-
onSelect,
|
|
40
|
-
}) {
|
|
41
|
-
return (
|
|
42
|
-
<li
|
|
43
|
-
onClick={selectMode ? onSelect : undefined}
|
|
44
|
-
className={`flex items-center gap-3 py-2 border-b ${selectMode ? 'cursor-pointer select-none' : ''} ${selected ? 'bg-fg-08' : ''}`}
|
|
45
|
-
style={{ borderColor: 'var(--kol-fg-08)' }}
|
|
46
|
-
>
|
|
47
|
-
{selectMode && <SelectIndicator on={selected} />}
|
|
48
|
-
<div className="w-12 h-12 shrink-0 rounded overflow-hidden">{thumb}</div>
|
|
49
|
-
<div className="flex-1 min-w-0">{name}</div>
|
|
50
|
-
<p className={`kol-mono-12 text-fg-32 shrink-0 text-right ${dateWidth}`}>{date}</p>
|
|
51
|
-
<p className={`kol-mono-12 text-fg-48 shrink-0 text-right ${sizeWidth}`}>{size}</p>
|
|
52
|
-
{!selectMode && <div className="shrink-0">{actions}</div>}
|
|
53
|
-
</li>
|
|
54
|
-
)
|
|
55
|
-
}
|