@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.
- package/package.json +1 -1
- package/src/atoms/ColorSwatch.jsx +6 -2
- package/src/atoms/SelectionOverlay.jsx +3 -1
- package/src/index.js +4 -2
- package/src/molecules/ColorInputRow.jsx +3 -2
- package/src/molecules/Dropdown.jsx +10 -5
- package/src/molecules/SectionText.jsx +5 -1
- package/src/molecules/ShapeDropdown.jsx +3 -2
- package/src/molecules/SplitToolButton.jsx +3 -2
- package/src/molecules/SwatchControls.jsx +3 -1
- package/src/organisms/Canvas.jsx +5 -2
- package/src/organisms/DocumentEditor.jsx +282 -0
- package/src/organisms/KeyframeEditor.jsx +2 -1
- package/src/organisms/LayerStack.jsx +11 -6
- package/src/organisms/MediaLibrary.jsx +1 -1
- package/src/organisms/MediaLibraryExplorer.jsx +67 -3
- package/src/organisms/MediaLibraryPages.jsx +358 -197
- package/src/organisms/SettingsPanel.jsx +5 -1
- package/src/organisms/SettingsSections.jsx +2 -1
- package/src/organisms/SpectrumGrid.jsx +3 -2
- package/src/organisms/TimelineDock.jsx +6 -3
- package/src/organisms/ToolPalette.jsx +6 -3
- package/src/utilities/Popover.jsx +61 -11
- package/src/utilities/QuadrantSync.jsx +3 -1
- package/src/utilities/frontmatter.js +47 -0
- package/src/utilities/localDrafts.js +50 -0
|
@@ -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) => <
|
|
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
|
-
|
|
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={
|
|
92
|
+
onViewChange={setView}
|
|
29
93
|
/>
|
|
30
94
|
)
|
|
31
95
|
}
|