@kolkrabbi/kol-component 0.232.0 → 0.233.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.233.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",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"embla-carousel-react": "^8.6.0",
|
|
26
26
|
"pdfjs-dist": "^6.3.289",
|
|
27
27
|
"react-syntax-highlighter": "^16.1.1",
|
|
28
|
-
"@kolkrabbi/kol-
|
|
29
|
-
"@kolkrabbi/kol-
|
|
28
|
+
"@kolkrabbi/kol-markdown": "^0.1.2",
|
|
29
|
+
"@kolkrabbi/kol-search": "^0.2.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -28,7 +28,7 @@ export default function MediaTile({ preview, name, selected = false, markKey, on
|
|
|
28
28
|
onContextMenu={onContextMenu}
|
|
29
29
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); onDoubleClick?.(e) } }}>
|
|
30
30
|
<div className="kol-media-tile-thumb">{preview}</div>
|
|
31
|
-
<span className="kol-media-tile-name">{name}</span>
|
|
31
|
+
<span className="kol-media-tile-name kol-item-name">{name}</span>
|
|
32
32
|
<RowMenuButton variant="grey" onOpen={onContextMenu} className="absolute top-1 right-1" />
|
|
33
33
|
</div>
|
|
34
34
|
)
|
|
@@ -7,26 +7,26 @@
|
|
|
7
7
|
* transparent ring, so a row lines up with the field or button beside it at the same size.
|
|
8
8
|
*
|
|
9
9
|
* States, strongest first:
|
|
10
|
+
* selected — the picked item: the tone's PRESSED fill (white in dark, as the grid's picked name)
|
|
10
11
|
* active — the keyboard cursor / hover: the tone fill, full ink
|
|
11
|
-
*
|
|
12
|
-
* trail — on the path to the picked item (a browser's parent folders): oq-04
|
|
12
|
+
* trail — on the path to the picked item (a browser's parent folders): oq-08
|
|
13
13
|
*
|
|
14
14
|
* Renders no role — the caller owns the list semantics (`role="option"`, `aria-selected`, ids)
|
|
15
15
|
* and passes them through `...rest`.
|
|
16
16
|
*
|
|
17
17
|
* @param {'xs'|'sm'|'md'|'lg'} size the control ramp step
|
|
18
|
+
* @param {string} type a type class instead of the ramp's (file lists: `kol-item-name`)
|
|
18
19
|
* @param {string} icon kol-icons name, drawn in oq (icons-use-oq law)
|
|
19
20
|
* @param {ReactNode} leading anything else in the icon column (a check, a thumb) — wins over icon
|
|
20
21
|
* @param {ReactNode} label
|
|
21
|
-
* @param {ReactNode} hint
|
|
22
|
+
* @param {ReactNode} hint secondary text — beside the label, right-aligned on its baseline
|
|
22
23
|
* @param {ReactNode} trailing right-aligned (a shortcut, a count, a menu button)
|
|
23
24
|
* @param {string} as element to render (`div` · `button` · `li`)
|
|
24
|
-
* @param {boolean}
|
|
25
|
-
* deepest-selection is decided in CSS by column); the ink still follows
|
|
25
|
+
* @param {boolean} hover false = hover paints nothing (the media browser's ruling, 2026-09-02)
|
|
26
26
|
* @param {boolean} muted a row that is there but not the point (fg-48)
|
|
27
27
|
*
|
|
28
|
-
* `ROW_HEIGHT` / `ROW_TYPE` are exported for a row that cannot take this anatomy (the
|
|
29
|
-
* view's table row: twisty, drag hit, columns) — it still sits on the same ramp.
|
|
28
|
+
* `ROW_HEIGHT` / `ROW_TYPE` / `ROW_FILL` are exported for a row that cannot take this anatomy (the
|
|
29
|
+
* media rows view's table row: twisty, drag hit, columns) — it still sits on the same ramp and fills.
|
|
30
30
|
*/
|
|
31
31
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
32
32
|
import { glyphSize } from '../hooks/glyphLadders.js'
|
|
@@ -41,13 +41,24 @@ export const ROW_HEIGHT = {
|
|
|
41
41
|
}
|
|
42
42
|
export const ROW_TYPE = { xs: 'kol-mono-12', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-14' }
|
|
43
43
|
|
|
44
|
+
const TONE = 'bg-[var(--kol-tone-bg,var(--kol-surface-secondary))]'
|
|
45
|
+
/* SELECTED IS THE TONE'S PRESSED STATE (user 2026-09-30: *"use white as focused item like grid
|
|
46
|
+
* already does"*) — `--kol-tone-pressed-*`, the fill a pressed button takes; outside a tone, the
|
|
47
|
+
* same surface-on-primary / surface-primary pair the grid's picked name paints. */
|
|
48
|
+
export const ROW_FILL = {
|
|
49
|
+
selected: 'bg-[var(--kol-tone-pressed-bg,var(--kol-surface-on-primary))] text-[var(--kol-tone-pressed-fg,var(--kol-surface-primary))]',
|
|
50
|
+
active: `${TONE} text-fg`,
|
|
51
|
+
trail: 'bg-oq-08 text-fg',
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
export default function OptionRow({
|
|
45
55
|
as: Tag = 'div',
|
|
46
56
|
size = 'md',
|
|
57
|
+
type,
|
|
47
58
|
active = false,
|
|
48
59
|
selected = false,
|
|
49
60
|
trail = false,
|
|
50
|
-
|
|
61
|
+
hover = true,
|
|
51
62
|
muted = false,
|
|
52
63
|
icon,
|
|
53
64
|
leading,
|
|
@@ -58,23 +69,21 @@ export default function OptionRow({
|
|
|
58
69
|
children,
|
|
59
70
|
...rest
|
|
60
71
|
}) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
* keyboard cursor gets the same look for free. `fill={false}` rows paint nothing (the columns) */
|
|
64
|
-
const bg = !paints ? '' : active
|
|
65
|
-
? 'bg-[var(--kol-tone-bg,var(--kol-surface-secondary))]'
|
|
66
|
-
: `${selected ? 'bg-oq-08' : trail ? 'bg-oq-04' : ''} hover:bg-[var(--kol-tone-bg,var(--kol-surface-secondary))]`
|
|
67
|
-
const fill = `${bg} ${lit ? 'text-fg' : muted ? 'text-fg-48' : 'text-fg-80'}`
|
|
72
|
+
const state = selected ? ROW_FILL.selected : active ? ROW_FILL.active : trail ? ROW_FILL.trail
|
|
73
|
+
: `${muted ? 'text-fg-48' : 'text-fg-80'}${hover ? ` hover:${TONE}` : ''}`
|
|
68
74
|
const lead = leading ?? (icon && <Icon name={icon} size={glyphSize(size)} />)
|
|
69
75
|
return (
|
|
70
76
|
<Tag
|
|
71
|
-
className={`flex items-center gap-2 px-4 border border-transparent ${ROW_HEIGHT[size] ?? ROW_HEIGHT.md} rounded-[var(--kol-radius-sm)] cursor-pointer text-left ${ROW_TYPE[size] ?? ROW_TYPE.md} transition-colors ${
|
|
77
|
+
className={`flex items-center gap-2 px-4 border border-transparent ${ROW_HEIGHT[size] ?? ROW_HEIGHT.md} rounded-[var(--kol-radius-sm)] cursor-pointer text-left ${type ?? ROW_TYPE[size] ?? ROW_TYPE.md} transition-colors ${state} ${className}`.trim()}
|
|
72
78
|
{...rest}
|
|
73
79
|
>
|
|
74
|
-
{
|
|
75
|
-
<span
|
|
80
|
+
{/* the glyph takes the row's ink when selected — a grey mark on a white fill reads as a hole */}
|
|
81
|
+
{lead && <span aria-hidden="true" className={`flex shrink-0${selected ? '' : ' text-oq-48'}`}>{lead}</span>}
|
|
82
|
+
{/* ONE LINE (user 2026-09-30): the hint sits beside the label on its baseline, right-aligned,
|
|
83
|
+
* so the names read as one column and a long name never pushes the path around */}
|
|
84
|
+
<span className="flex flex-1 items-baseline gap-3 min-w-0">
|
|
76
85
|
<span className="truncate">{label ?? children}</span>
|
|
77
|
-
{hint && <span className=
|
|
86
|
+
{hint && <span className={`kol-mono-12 ml-auto truncate${selected ? ' opacity-60' : ' text-fg-48'}`}>{hint}</span>}
|
|
78
87
|
</span>
|
|
79
88
|
{trailing && <span className="flex shrink-0 items-center">{trailing}</span>}
|
|
80
89
|
</Tag>
|
|
@@ -249,7 +249,7 @@ const ZONE_BOX = 44
|
|
|
249
249
|
* without the stack: the columns pass none, so a desktop row is byte-identical
|
|
250
250
|
* to what it was. */
|
|
251
251
|
function Row({
|
|
252
|
-
icon, label, active, cursor = false, trailing, onClick, muted = false, size = 'md',
|
|
252
|
+
icon, label, active, trail = false, cursor = false, trailing, onClick, muted = false, size = 'md',
|
|
253
253
|
indent = 0, meta, zones = false, thumb, onDisclose, disclosed, onContextMenu, drop, dropFiles, markKey,
|
|
254
254
|
}) {
|
|
255
255
|
const [over, setOver] = useState(false)
|
|
@@ -275,15 +275,18 @@ function Row({
|
|
|
275
275
|
},
|
|
276
276
|
} : {}
|
|
277
277
|
/* THE DESKTOP ROW IS AN OptionRow (2026-09-30) — the palette's row, on the control ramp, so a size
|
|
278
|
-
* setting scales height
|
|
279
|
-
*
|
|
280
|
-
*
|
|
278
|
+
* setting scales height and glyph; the name keeps ONE type in every view (`kol-item-name`, the
|
|
279
|
+
* grid's), as Finder does. The deepest selection takes the pressed (white) fill, the folders on the
|
|
280
|
+
* way to it the trail; hover and the bare cursor paint nothing (the 2026-09-02 ruling). The `zones`
|
|
281
|
+
* row below is the phone's and keeps its own. */
|
|
281
282
|
if (!zones) return (
|
|
282
283
|
<OptionRow
|
|
283
284
|
as="li"
|
|
284
285
|
size={size}
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
type="kol-item-name"
|
|
287
|
+
hover={false}
|
|
288
|
+
selected={active && !trail}
|
|
289
|
+
trail={active && trail}
|
|
287
290
|
muted={muted && !cursor}
|
|
288
291
|
icon={icon}
|
|
289
292
|
label={label}
|
|
@@ -1039,8 +1042,11 @@ export default function ColumnBrowser({
|
|
|
1039
1042
|
label={f.replace(/\/$/, '')}
|
|
1040
1043
|
markKey={level + f}
|
|
1041
1044
|
active={f === activeFolder || isPicked(level + f)}
|
|
1045
|
+
/* a folder on the path is the TRAIL unless it is the deepest pick — the column after
|
|
1046
|
+
* it is its own contents with nothing chosen, and no file is showing */
|
|
1047
|
+
trail={f === activeFolder && !isPicked(level + f) && (!!shown || k < levels.length - 2)}
|
|
1042
1048
|
cursor={cursorActive && cursor.col === k && cursor.idx === i}
|
|
1043
|
-
trailing={<Icon name="chevron-right" size={12} className=
|
|
1049
|
+
trailing={<Icon name="chevron-right" size={12} className={f === activeFolder && !shown && k === levels.length - 2 ? '' : 'text-oq-32'} />}
|
|
1044
1050
|
onContextMenu={onRowContextMenu && ((e) => onRowContextMenu(e, { type: 'folder', path: level + f }))}
|
|
1045
1051
|
drop={dragFor?.(level + f)}
|
|
1046
1052
|
dropFiles={onDropFiles ? (list) => onDropFiles(list, level + f) : undefined}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import formatSize from '../utilities/formatSize.js'
|
|
3
|
-
import { ROW_HEIGHT,
|
|
3
|
+
import { ROW_HEIGHT, ROW_FILL } from '../molecules/OptionRow.jsx'
|
|
4
4
|
import { glyphSize } from '../hooks/glyphLadders.js'
|
|
5
5
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
6
6
|
import Button from '../atoms/Button.jsx'
|
|
@@ -303,7 +303,9 @@ function TagEditor({ tags = [], onChange, suggestions = [] }) {
|
|
|
303
303
|
|
|
304
304
|
/* THE ROWS VIEW'S STEP ON THE CONTROL RAMP (user 2026-09-30) — the same `rowSize` the columns take,
|
|
305
305
|
* handed down by context rather than threaded through every subtree level. These rows keep their
|
|
306
|
-
* own anatomy (twisty, drag hit, table columns) and take
|
|
306
|
+
* own anatomy (twisty, drag hit, table columns) and take the ramp's box and glyph, OptionRow's
|
|
307
|
+
* fills (picked = the pressed white, the folder you are in = the trail) and the one item type,
|
|
308
|
+
* `kol-item-name`, that the columns and the grid use. */
|
|
307
309
|
const RowSize = createContext('md')
|
|
308
310
|
|
|
309
311
|
/* `depth` indents a row under the folder it was expanded from; `expanded`/`onToggle` draw the
|
|
@@ -341,7 +343,7 @@ function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, e
|
|
|
341
343
|
return (
|
|
342
344
|
/* THE COLUMN BROWSER'S ROW (user 2026-09-22): `kol-column-browser-row` carries the 4px pill
|
|
343
345
|
* inset, the selected fill and the drop fill; no divider, as the columns have none. */
|
|
344
|
-
<li onContextMenu={onContextMenu} {...dropProps} data-marquee-key={markKey} data-hit-zone data-drop-over={over || undefined} className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ?
|
|
346
|
+
<li onContextMenu={onContextMenu} {...dropProps} data-marquee-key={markKey} data-hit-zone data-drop-over={over || undefined} className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ? ` is-selected ${ROW_FILL.selected}` : ''}${current ? ' is-current' : ''}${current && !selected ? ` ${ROW_FILL.trail}` : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
|
|
345
347
|
{onToggle ? (
|
|
346
348
|
<button type="button" aria-label={expanded ? `Collapse ${name}` : `Expand ${name}`} aria-expanded={!!expanded}
|
|
347
349
|
onClick={(e) => { e.stopPropagation(); onToggle() }}
|
|
@@ -353,11 +355,11 @@ function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, e
|
|
|
353
355
|
{/* THE FOLDER GLYPH IS THE ONE ICON LEFT, so it fills its box (user 2026-09-23: *"only folder
|
|
354
356
|
should use icon, and it should be bigger"*). Every other kind now renders its own bytes. */}
|
|
355
357
|
<span data-hit {...hitDrag} className="kol-row-hit flex items-center gap-3 min-w-0">
|
|
356
|
-
<span className=
|
|
358
|
+
<span className={`shrink-0 flex items-center justify-center${selected ? '' : ' text-oq-48'}`}><Icon name={icon} size={glyphSize(size)} /></span>
|
|
357
359
|
{/* NO TRAILING SLASH (user 2026-09-22). `partition` hands folders back as `audio/` because a
|
|
358
360
|
slash is what marks one in a flat key space; that is storage's spelling, not a label, and
|
|
359
361
|
`ColumnBrowser` has always stripped it. The glyph says "folder". */}
|
|
360
|
-
<span className={
|
|
362
|
+
<span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{name.replace(/\/$/, '')}</span>
|
|
361
363
|
</span>
|
|
362
364
|
<span className="flex-1" />
|
|
363
365
|
{/* THE FOLDER'S COUNT SITS IN THE DATE COLUMN and the size column takes a dash — the columns
|
|
@@ -365,8 +367,8 @@ function FolderRow({ name, icon = 'folder', onClick, onDoubleClick, depth = 0, e
|
|
|
365
367
|
promised a click that opens, and a click now SELECTS (opening is a double-click). */}
|
|
366
368
|
{cols && <>
|
|
367
369
|
{cols.tags != null && <span className="shrink-0" style={{ width: cols.tags }} />}
|
|
368
|
-
<span className=
|
|
369
|
-
<span className=
|
|
370
|
+
<span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.date }}>{meta}</span>
|
|
371
|
+
<span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-24"} shrink-0 truncate`} style={{ width: cols.size }}>—</span>
|
|
370
372
|
</>}
|
|
371
373
|
<RowMenuButton onOpen={onContextMenu} className="-my-1 -mr-1" />
|
|
372
374
|
</li>
|
|
@@ -430,21 +432,21 @@ function FileRow({ o, onClick, onDoubleClick, depth = 0, formatDate, thumb, cols
|
|
|
430
432
|
const size = useContext(RowSize)
|
|
431
433
|
const dragProps = drag ? { draggable: true, onDragStart: (e) => { e.stopPropagation(); drag.onDragStart(e, o.key) } } : {}
|
|
432
434
|
return (
|
|
433
|
-
<li onContextMenu={onContextMenu} data-marquee-key={markKey ?? o.key} data-hit-zone className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ?
|
|
435
|
+
<li onContextMenu={onContextMenu} data-marquee-key={markKey ?? o.key} data-hit-zone className={`kol-column-browser-row flex items-center gap-3 border border-transparent px-3 ${ROW_HEIGHT[size]} transition-colors${selected ? ` is-selected ${ROW_FILL.selected}` : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
|
|
434
436
|
<span className="w-4 shrink-0" />
|
|
435
437
|
{/* the hit — icon and name, as in the folder row above */}
|
|
436
438
|
<span data-hit {...dragProps} className="kol-row-hit flex items-center gap-3 min-w-0">
|
|
437
|
-
<span className=
|
|
439
|
+
<span className={`shrink-0 flex items-center justify-center overflow-hidden rounded${selected ? '' : ' text-oq-48'}`} style={{ width: glyphSize(size), height: glyphSize(size) }}>
|
|
438
440
|
{/* a file with no thumbnail draws its glyph at the FOLDER's size — one icon column (user 2026-09-25) */}
|
|
439
441
|
{thumb ?? <Icon name={kindOf(o) === 'image' ? 'image' : 'file'} size={glyphSize(size)} />}
|
|
440
442
|
</span>
|
|
441
|
-
<span className={
|
|
443
|
+
<span className={`kol-item-name min-w-0 truncate${selected ? '' : ' text-fg-default'}`}>{o.displayKey ?? o.key.split('/').pop()}</span>
|
|
442
444
|
</span>
|
|
443
445
|
<span className="flex-1" />
|
|
444
446
|
{cols && <>
|
|
445
|
-
{cols.tags != null && <span className=
|
|
446
|
-
<span className=
|
|
447
|
-
<span className=
|
|
447
|
+
{cols.tags != null && <span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-48"} shrink-0 truncate`} style={{ width: cols.tags }}>{(o.tags ?? []).map((t) => `#${t}`).join(' ')}</span>}
|
|
448
|
+
<span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.date }}>{formatDate?.(o.uploaded)}</span>
|
|
449
|
+
<span className={`kol-item-name ${selected ? "opacity-60" : "text-fg-32"} shrink-0 truncate`} style={{ width: cols.size }}>{formatSize(o.size)}</span>
|
|
448
450
|
</>}
|
|
449
451
|
<RowMenuButton onOpen={onContextMenu} className="-my-1 -mr-1" />
|
|
450
452
|
</li>
|