@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.232.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-search": "^0.2.0",
29
- "@kolkrabbi/kol-markdown": "^0.1.2"
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",
@@ -100,7 +100,6 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
100
100
  role="option"
101
101
  aria-selected={val === value}
102
102
  size="sm"
103
- selected={val === value}
104
103
  className="w-full"
105
104
  onClick={() => {
106
105
  onChange?.(val)
@@ -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
- * selected — the picked item: oq-08
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 a second line under the label
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} fill false = the caller paints the state fills (ColumnBrowser's trail vs
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 media rows
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
- fill: paints = true,
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 lit = active || selected || trail
62
- /* hover takes the active fill — the palette already moves `active` on hover; a list without a
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 ${fill} ${className}`.trim()}
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
- {lead && <span aria-hidden="true" className="flex shrink-0 text-oq-48">{lead}</span>}
75
- <span className="flex flex-1 flex-col min-w-0">
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="kol-mono-12 text-fg-48 truncate">{hint}</span>}
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, glyph and type together. The fills stay this file's CSS (trail vs the
279
- * deepest column's selection is decided by column there, and hover/cursor paint nothing — the
280
- * 2026-09-02 ruling), so `fill={false}`. The `zones` row below is the phone's and keeps its own. */
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
- fill={false}
286
- selected={active}
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="text-oq-32" />}
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, ROW_TYPE } from '../molecules/OptionRow.jsx'
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 only the ramp: box, type, glyph. */
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 ? ' is-selected' : ''}${current ? ' is-current' : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
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="shrink-0 flex items-center justify-center text-oq-48"><Icon name={icon} size={glyphSize(size)} /></span>
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={`${ROW_TYPE[size]} min-w-0 truncate text-fg-default`}>{name.replace(/\/$/, '')}</span>
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="kol-mono-12 text-fg-32 shrink-0 truncate" style={{ width: cols.date }}>{meta}</span>
369
- <span className="kol-mono-12 text-fg-24 shrink-0 truncate" style={{ width: cols.size }}>—</span>
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 ? ' is-selected' : ''}`} style={{ paddingLeft: depth * 20 + 12 }} onClick={onClick} onDoubleClick={onDoubleClick}>
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="shrink-0 flex items-center justify-center text-oq-48 overflow-hidden rounded" style={{ width: glyphSize(size), height: glyphSize(size) }}>
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={`${ROW_TYPE[size]} min-w-0 truncate text-fg-default`}>{o.displayKey ?? o.key.split('/').pop()}</span>
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="kol-mono-12 text-fg-48 shrink-0 truncate" style={{ width: cols.tags }}>{(o.tags ?? []).map((t) => `#${t}`).join(' ')}</span>}
446
- <span className="kol-mono-12 text-fg-32 shrink-0 truncate" style={{ width: cols.date }}>{formatDate?.(o.uploaded)}</span>
447
- <span className="kol-mono-12 text-fg-32 shrink-0 truncate" style={{ width: cols.size }}>{formatSize(o.size)}</span>
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>