@kolkrabbi/kol-component 0.223.0 → 0.225.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.
@@ -3,7 +3,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Button from '../atoms/Button.jsx'
4
4
  import Input from '../atoms/Input.jsx'
5
5
  import { MenuDropdownItem, MenuDropdownNest } from '../molecules/MenuItem.jsx'
6
- import { usePopover, PopoverPanel } from '../utilities/Popover.jsx'
6
+ import { usePopover, PopoverPanel, Tooltip } from '../utilities/Popover.jsx'
7
7
  import { rowLabelForLayer, findLayerDeep } from '../hooks/layerTree.js'
8
8
 
9
9
  /* DS icon names for the engine's types — every one a shipped v1 glyph. A
@@ -51,11 +51,11 @@ function useShiftClickHandlers(onSelect, onShiftSelect) {
51
51
 
52
52
  function Chevron({ IconC, collapsed, onToggle, title }) {
53
53
  return (
54
+ <Tooltip label={title} asChild>
54
55
  <button
55
56
  type="button"
56
57
  onClick={onToggle}
57
58
  aria-expanded={!collapsed}
58
- title={title}
59
59
  className="kol-layer-stack-collapse"
60
60
  >
61
61
  <IconC
@@ -64,6 +64,7 @@ function Chevron({ IconC, collapsed, onToggle, title }) {
64
64
  style={{ transform: collapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transition: 'transform 150ms' }}
65
65
  />
66
66
  </button>
67
+ </Tooltip>
67
68
  )
68
69
  }
69
70
 
@@ -160,24 +161,26 @@ function LayerRow({
160
161
  </span>
161
162
  </button>
162
163
  )}
164
+ <Tooltip label={layer.visible ? 'Hide' : 'Show'} asChild>
163
165
  <button
164
166
  type="button"
165
167
  onClick={onToggleVisibility}
166
- title={layer.visible ? 'Hide' : 'Show'}
167
168
  aria-pressed={!layer.visible}
168
169
  className={`kol-layer-stack-toggle kol-layer-stack-toggle--eye${active || !layer.visible ? ' is-pinned' : ''}`}
169
170
  >
170
171
  <IconC name={layer.visible ? 'eye-on' : 'eye-off'} size={12} />
171
172
  </button>
173
+ </Tooltip>
174
+ <Tooltip label={layer.locked ? 'Unlock' : 'Lock'} asChild>
172
175
  <button
173
176
  type="button"
174
177
  onClick={onToggleLock}
175
- title={layer.locked ? 'Unlock' : 'Lock'}
176
178
  aria-pressed={!!layer.locked}
177
179
  className={`kol-layer-stack-toggle kol-layer-stack-toggle--lock${active || layer.locked ? ' is-pinned' : ''}${layer.locked ? ' is-on' : ''}`}
178
180
  >
179
181
  <IconC name={layer.locked ? 'lock' : 'unlock'} size={12} />
180
182
  </button>
183
+ </Tooltip>
181
184
  </div>
182
185
  </div>
183
186
  )
@@ -419,16 +422,17 @@ export default function LayerStack({
419
422
  * consumer's keymap. */}
420
423
  {onGroup && layerSelectionCount >= 2 && (
421
424
  <div className="mt-auto flex items-center gap-2 px-3 h-10 border-t border-fg-08">
425
+ <Tooltip label={`Group ${layerSelectionCount} selected layers`}>
422
426
  <Button
423
427
  iconComponent={IconC}
424
428
  variant="primary"
425
429
  size="sm"
426
430
  iconLeft="layers"
427
431
  onClick={() => onGroup(layerSelectedIds)}
428
- title={`Group ${layerSelectionCount} selected layers`}
429
432
  >
430
433
  Group {layerSelectionCount}
431
434
  </Button>
435
+ </Tooltip>
432
436
  </div>
433
437
  )}
434
438
  </div>
@@ -469,6 +473,7 @@ export function AddLayerButton({
469
473
  return (
470
474
  <>
471
475
  <span ref={popover.refs.setReference} {...popover.getReferenceProps()} className="inline-flex">
476
+ <Tooltip label="Add layer">
472
477
  <Button
473
478
  iconComponent={IconC}
474
479
  variant="primary"
@@ -476,8 +481,8 @@ export function AddLayerButton({
476
481
  quiet
477
482
  iconOnly="plus"
478
483
  aria-label="Add layer"
479
- title="Add layer"
480
484
  />
485
+ </Tooltip>
481
486
  </span>
482
487
  <PopoverPanel popover={popover} className="py-1" style={{ width: menuWidth }}>
483
488
  {types.map((t) => {
@@ -708,7 +708,7 @@ function FilesBody({ files, viewMode, onOpen, onPick }) {
708
708
  )
709
709
  /* the title voice is the family's ruled default (heading-04 card / heading-05
710
710
  * row, truncated by ContentText); the full key rides as the tooltip, as before */
711
- const nameFor = (row) => <span title={row.key}>{row.displayKey}</span>
711
+ const nameFor = (row) => <Tooltip label={row.key} asChild><span>{row.displayKey}</span></Tooltip>
712
712
  const date = (row) => (row.uploaded ? String(row.uploaded).slice(0, 10) : undefined)
713
713
  const size = (row) => formatSize(row.size) || undefined
714
714
 
@@ -1,5 +1,7 @@
1
- import { useState } from 'react'
1
+ import { useEffect, useRef, useState } from 'react'
2
2
  import { MediaLibraryBrowse } from './MediaLibraryPages.jsx'
3
+ import { useModal } from '../molecules/Modal.jsx'
4
+ import useMediaQuery from '../hooks/useMediaQuery.js'
3
5
 
4
6
  /**
5
7
  * MediaLibraryExplorer — the media surface, and since 2026-09-22 a thin name for it.
@@ -17,15 +19,77 @@ import { MediaLibraryBrowse } from './MediaLibraryPages.jsx'
17
19
  * @param {'columns'|'rows'|'grid'|'list'} view controlled view; omit and the page reads its own setting
18
20
  * @param {Function} onViewChange (view) => void
19
21
  * @param {'columns'|'rows'|'grid'|'list'} defaultView first view when uncontrolled and nothing is stored
22
+ *
23
+ * THE TOOL'S OWN BEHAVIOUR, opt-in (media parity, 2026-09-26 — user: "media in shell is just media in
24
+ * shell.. so there shouldnt really be a reason to diff"). apps/media carried these in its App.jsx, so
25
+ * the same tool one shell over had none of them. They are the product's, so they live with it:
26
+ *
27
+ * @param {boolean} keys the view keys — B browse · F files · R rows · C columns · G grid — and N
28
+ * (new folder, when `fileActions.createFolder` is given), plus K → `onKinds`.
29
+ * Ignored while typing and with ⌘ / Ctrl / ⌥ held
30
+ * @param {boolean} phoneTabs below 768 the surface is three tabs — Browse · Files · Kinds (Kinds only
31
+ * with `onKinds`); a tab picks the view, Kinds opens the overview and leaves
32
+ * the surface where it was
33
+ * @param {Function} onKinds opens the app's file-formats overview (K, the Kinds tab)
20
34
  */
21
- export default function MediaLibraryExplorer({ view, onViewChange, defaultView, ...pageProps }) {
35
+ const TABS = [
36
+ { value: 'browse', label: 'Browse', icon: 'folder' },
37
+ { value: 'files', label: 'Files', icon: 'view-list' },
38
+ { value: 'kinds', label: 'Kinds', icon: 'grid' },
39
+ ]
40
+ /* a key → the view it shows and the phone tab that view belongs to (apps/media's map, verbatim) */
41
+ const VIEW_KEYS = { b: ['columns', 'browse'], f: ['list', 'files'], r: ['rows', 'browse'], c: ['columns', 'browse'], g: ['grid', 'files'] }
42
+
43
+ export default function MediaLibraryExplorer({ view, onViewChange, defaultView, keys = false, phoneTabs = false, onKinds, ...pageProps }) {
22
44
  const [ownView, setOwnView] = useState(view ?? defaultView)
23
45
  const current = view ?? ownView
46
+ const setView = (v) => { setOwnView(v); onViewChange?.(v) }
47
+ const [tab, setTab] = useState('browse')
48
+ const phone = useMediaQuery('(max-width: 767px)')
49
+ const modal = useModal()
50
+
51
+ /* the latest props for the key handler, without re-binding it every render */
52
+ const live = useRef()
53
+ live.current = { onKinds, prefix: pageProps.prefix ?? '', createFolder: pageProps.fileActions?.createFolder, modal }
54
+ useEffect(() => {
55
+ if (!keys) return undefined
56
+ const onKey = (e) => {
57
+ if (e.metaKey || e.ctrlKey || e.altKey) return
58
+ const el = e.target
59
+ if (el?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el?.tagName ?? '')) return
60
+ const { onKinds: kinds, prefix, createFolder, modal: m } = live.current
61
+ if (VIEW_KEYS[e.key]) {
62
+ e.preventDefault()
63
+ const [v, t] = VIEW_KEYS[e.key]
64
+ setOwnView(v); onViewChange?.(v); setTab(t)
65
+ } else if (e.key === 'k' && kinds) {
66
+ e.preventDefault(); kinds()
67
+ } else if (e.key === 'n' && createFolder) {
68
+ e.preventDefault()
69
+ m.prompt(`New folder in ${prefix || 'the bucket root'}:`, '', { okLabel: 'Create' }).then((name) => {
70
+ if (name?.trim()) createFolder(`${prefix}${name.trim().replace(/^\/+|\/+$/g, '')}/`)
71
+ })
72
+ }
73
+ }
74
+ window.addEventListener('keydown', onKey)
75
+ return () => window.removeEventListener('keydown', onKey)
76
+ }, [keys]) // eslint-disable-line react-hooks/exhaustive-deps
77
+
78
+ const onTabChange = (v) => {
79
+ if (v === 'kinds') { onKinds?.(); return }
80
+ setTab(v)
81
+ setView(v === 'files' ? 'list' : 'columns')
82
+ }
83
+ const tabProps = phoneTabs && phone
84
+ ? { tabs: onKinds ? TABS : TABS.filter((t) => t.value !== 'kinds'), activeTab: tab, onTabChange }
85
+ : {}
86
+
24
87
  return (
25
88
  <MediaLibraryBrowse
26
89
  {...pageProps}
90
+ {...tabProps}
27
91
  view={current}
28
- onViewChange={(v) => { setOwnView(v); onViewChange?.(v) }}
92
+ onViewChange={setView}
29
93
  />
30
94
  )
31
95
  }