@kolkrabbi/kol-component 0.216.0 → 0.218.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 +3 -2
- package/src/atoms/ActionButton.jsx +10 -9
- package/src/atoms/FileIcon.jsx +31 -0
- package/src/atoms/ViewToggle.jsx +35 -17
- package/src/hooks/useMarquee.js +93 -0
- package/src/index.js +3 -0
- package/src/molecules/AudioPreview.jsx +4 -2
- package/src/molecules/AudioSheet.jsx +22 -36
- package/src/molecules/CodeBlock.jsx +4 -2
- package/src/molecules/ContentCard.jsx +13 -0
- package/src/molecules/ContentRow.jsx +9 -0
- package/src/molecules/DocFrontmatter.jsx +1 -1
- package/src/molecules/KindPreview.jsx +91 -5
- package/src/molecules/MediaTile.jsx +29 -0
- package/src/molecules/Modal.jsx +25 -12
- package/src/molecules/PdfPage.jsx +110 -0
- package/src/molecules/PlaybackBar.jsx +16 -8
- package/src/molecules/QuickLookFrame.jsx +88 -0
- package/src/molecules/ShellDrawer.jsx +10 -4
- package/src/molecules/Stepper.jsx +2 -2
- package/src/molecules/VideoSheet.jsx +12 -11
- package/src/organisms/ColumnBrowser.jsx +191 -37
- package/src/organisms/ContentFilters.jsx +29 -13
- package/src/organisms/MediaLibrary.jsx +2 -2
- package/src/organisms/MediaLibraryExplorer.jsx +22 -107
- package/src/organisms/MediaLibraryPages.jsx +1389 -348
- package/src/organisms/SettingsPanel.jsx +16 -6
- package/src/utilities/FullscreenOverlay.jsx +17 -3
- package/src/utilities/Popover.jsx +11 -4
- package/src/utilities/id3.js +3 -0
- package/src/utilities/layerStack.js +18 -0
- package/src/utilities/mediaKinds.js +10 -4
|
@@ -6,6 +6,7 @@ import LabeledControl from '../molecules/LabeledControl.jsx'
|
|
|
6
6
|
import ToggleSwitch from '../atoms/ToggleSwitch.jsx'
|
|
7
7
|
import ShellDrawer from '../molecules/ShellDrawer.jsx'
|
|
8
8
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
9
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* SettingsPanel — a settings surface for the thing you are looking at: you
|
|
@@ -110,7 +111,8 @@ export default function SettingsPanel({
|
|
|
110
111
|
* uppercase label (`kol-helper-10` tracked, meta ink) in a 160px column, the
|
|
111
112
|
* control fills the rest. A switch sits at the far right (`align="end"`, the
|
|
112
113
|
* default); a dropdown fills the row (`align="fill"`). No hint sentences on the
|
|
113
|
-
* page — `hint` rides the control's `
|
|
114
|
+
* page — `hint` rides the control's DS `Tooltip` (a native `title` until
|
|
115
|
+
* 2026-09-22, kol-client-olina). `labelWidth="auto"` passes through
|
|
114
116
|
* to LabeledControl: the label flexes and truncates, the control hugs — for a
|
|
115
117
|
* row living in a column narrower than the 160px label default
|
|
116
118
|
* (SettingsShortcutsComboOverflow, kol-monitor 2026-09-01).
|
|
@@ -118,24 +120,28 @@ export default function SettingsPanel({
|
|
|
118
120
|
export function SettingsRow({ label, hint, align = 'end', labelWidth = 160, children }) {
|
|
119
121
|
return (
|
|
120
122
|
<LabeledControl inline label={typeof label === 'string' ? label.toUpperCase() : label} labelWidth={labelWidth}>
|
|
121
|
-
|
|
123
|
+
{hint
|
|
124
|
+
? <Tooltip label={hint} triggerClassName={`inline-flex w-full ${align === 'fill' ? '' : 'justify-end'}`.trim()}>{children}</Tooltip>
|
|
125
|
+
: <span className={`inline-flex w-full ${align === 'fill' ? '' : 'justify-end'}`.trim()}>{children}</span>}
|
|
122
126
|
</LabeledControl>
|
|
123
127
|
)
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/** SettingsSwitch — the row's on/off control: the DS ToggleSwitch, bare, sm.
|
|
127
|
-
* `disabledHint` rides `
|
|
131
|
+
* `disabledHint` rides a DS `Tooltip` so a switch that cannot act says why;
|
|
132
|
+
* inside a row's `hint` it wins while hovered, as the native `title` did. */
|
|
128
133
|
export function SettingsSwitch({ on = false, onChange, disabled = false, disabledHint, label, title }) {
|
|
129
|
-
|
|
134
|
+
const tip = title ?? (disabled ? disabledHint : undefined)
|
|
135
|
+
const toggle = (
|
|
130
136
|
<ToggleSwitch
|
|
131
137
|
size="sm"
|
|
132
138
|
checked={on}
|
|
133
139
|
onChange={onChange}
|
|
134
140
|
disabled={disabled}
|
|
135
|
-
title={title ?? (disabled ? disabledHint : undefined)}
|
|
136
141
|
aria-label={label}
|
|
137
142
|
/>
|
|
138
143
|
)
|
|
144
|
+
return tip ? <Tooltip label={tip}>{toggle}</Tooltip> : toggle
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
/** LabeledControlSection — a section of LabeledControls: the EYEBROW
|
|
@@ -253,7 +259,11 @@ export function SettingsFooter({ onReset, resetLabel = 'Reset to defaults', chil
|
|
|
253
259
|
<Divider />
|
|
254
260
|
<div className="flex justify-end items-center gap-2">
|
|
255
261
|
{children}
|
|
256
|
-
{onReset &&
|
|
262
|
+
{onReset && (
|
|
263
|
+
<Tooltip label={resetLabel}>
|
|
264
|
+
<IconFrame name="refresh" variant="primary" size="sm" onClick={onReset} aria-label={resetLabel} />
|
|
265
|
+
</Tooltip>
|
|
266
|
+
)}
|
|
257
267
|
</div>
|
|
258
268
|
</div>
|
|
259
269
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { pushLayer, popLayer, isTopLayer } from './layerStack.js'
|
|
2
3
|
import CloseButton from './CloseButton.jsx'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -36,6 +37,11 @@ export default function FullscreenOverlay({
|
|
|
36
37
|
* caller should have to know to put focus in it. A plain `<div ref>` always
|
|
37
38
|
* works. */
|
|
38
39
|
initialFocus,
|
|
40
|
+
/* A DIM BACKDROP INSTEAD OF THE SURFACE (user 2026-09-23, on Quick Look: *"its overlay, not a
|
|
41
|
+
* black background … just dim the background slightly"*). The flat surface is still the default
|
|
42
|
+
* — that is the 2026-08-27 ruling, made because a wash read as a halo around a lightboxed image
|
|
43
|
+
* — so this is opt-in per overlay rather than a reversal for every consumer of the component. */
|
|
44
|
+
scrim = false,
|
|
39
45
|
children,
|
|
40
46
|
}) {
|
|
41
47
|
const sheetRef = useRef(null)
|
|
@@ -44,10 +50,17 @@ export default function FullscreenOverlay({
|
|
|
44
50
|
* the same trap ShellDrawer carries; before this a Tab from the overlay
|
|
45
51
|
* walked into the page underneath). Focus moves into the sheet on open and
|
|
46
52
|
* back to the opener on close. */
|
|
53
|
+
/* `onClose` through a ref: an inline handler is a new function every render, and with it in the
|
|
54
|
+
* deps the effect re-ran on every render — re-pushing this layer to the TOP of the stack and
|
|
55
|
+
* re-stealing focus while a sheet above it was open. */
|
|
56
|
+
const onCloseRef = useRef(onClose)
|
|
57
|
+
onCloseRef.current = onClose
|
|
47
58
|
useEffect(() => {
|
|
48
59
|
if (!open) return
|
|
60
|
+
const layer = pushLayer()
|
|
49
61
|
const onKey = (e) => {
|
|
50
|
-
if (
|
|
62
|
+
if (!isTopLayer(layer)) return
|
|
63
|
+
if (e.key === 'Escape') { onCloseRef.current?.(); return }
|
|
51
64
|
if (e.key !== 'Tab') return
|
|
52
65
|
const sheet = sheetRef.current
|
|
53
66
|
if (!sheet) return
|
|
@@ -72,11 +85,12 @@ export default function FullscreenOverlay({
|
|
|
72
85
|
: sheetRef.current
|
|
73
86
|
target?.focus?.()
|
|
74
87
|
return () => {
|
|
88
|
+
popLayer(layer)
|
|
75
89
|
document.removeEventListener('keydown', onKey)
|
|
76
90
|
document.body.style.overflow = prev
|
|
77
91
|
if (prevFocus instanceof HTMLElement) prevFocus.focus()
|
|
78
92
|
}
|
|
79
|
-
}, [open,
|
|
93
|
+
}, [open, initialFocus])
|
|
80
94
|
|
|
81
95
|
if (!open) return null
|
|
82
96
|
|
|
@@ -90,7 +104,7 @@ export default function FullscreenOverlay({
|
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
return (
|
|
93
|
-
<div className=
|
|
107
|
+
<div className={`kol-overlay${scrim ? ' kol-overlay-scrim' : ''}`} role="dialog" aria-modal="true" onMouseDown={onBackdropClick}>
|
|
94
108
|
<div ref={sheetRef} tabIndex={-1} className="kol-overlay-sheet outline-none">
|
|
95
109
|
{closeButton && (
|
|
96
110
|
/* ONE close idiom (FullscreenOverlayCloseIdiom, kol-chess 2026-09-01,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { createContext, useContext, useState } from 'react'
|
|
2
2
|
import { toneClass } from './tone.js'
|
|
3
3
|
import {
|
|
4
4
|
useFloating,
|
|
@@ -131,6 +131,11 @@ export function usePopover({
|
|
|
131
131
|
* <Button iconOnly="ptrn-checker" ... />
|
|
132
132
|
* </Tooltip>
|
|
133
133
|
*/
|
|
134
|
+
/* A tooltip inside a tooltip's trigger (a disabled switch's reason inside its
|
|
135
|
+
* row's hint) hides the outer one while it is open, and hands it back on leave
|
|
136
|
+
* — the innermost wins, as the native `title` it replaced behaved. */
|
|
137
|
+
const TooltipNest = createContext(null)
|
|
138
|
+
|
|
134
139
|
export function Tooltip({
|
|
135
140
|
label,
|
|
136
141
|
shortcut,
|
|
@@ -140,9 +145,11 @@ export function Tooltip({
|
|
|
140
145
|
triggerClassName = 'inline-flex',
|
|
141
146
|
}) {
|
|
142
147
|
const [open, setOpen] = useState(false)
|
|
148
|
+
const [covered, setCovered] = useState(false)
|
|
149
|
+
const parent = useContext(TooltipNest)
|
|
143
150
|
const popover = usePopover({
|
|
144
|
-
open,
|
|
145
|
-
onOpenChange: setOpen,
|
|
151
|
+
open: open && !covered,
|
|
152
|
+
onOpenChange: (v) => { setOpen(v); parent?.(v) },
|
|
146
153
|
placement,
|
|
147
154
|
offset,
|
|
148
155
|
role: 'tooltip',
|
|
@@ -158,7 +165,7 @@ export function Tooltip({
|
|
|
158
165
|
{...popover.getReferenceProps()}
|
|
159
166
|
className={triggerClassName}
|
|
160
167
|
>
|
|
161
|
-
{children}
|
|
168
|
+
<TooltipNest.Provider value={setCovered}>{children}</TooltipNest.Provider>
|
|
162
169
|
</span>
|
|
163
170
|
<PopoverPanel
|
|
164
171
|
popover={popover}
|
package/src/utilities/id3.js
CHANGED
|
@@ -43,6 +43,9 @@ function apic(tag, version) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function readCover(url) {
|
|
46
|
+
/* only a URL a browser can fetch — a placeholder scheme (a fixture, an unresolved key) is "no
|
|
47
|
+
* cover", not a console error */
|
|
48
|
+
if (!/^(https?:|data:|blob:|\/)/.test(String(url ?? ''))) return Promise.resolve(null)
|
|
46
49
|
if (!covers.has(url)) {
|
|
47
50
|
covers.set(url, (async () => {
|
|
48
51
|
try {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* THE LAYER STACK — which open overlay owns the keyboard (2026-09-23). Every overlay and drawer
|
|
2
|
+
* listened for Escape and Tab on `document`, so with a sheet open over a drawer one Escape closed
|
|
3
|
+
* BOTH, and two focus traps fought over Tab. Each layer pushes itself on open and pops on close;
|
|
4
|
+
* only the top one answers. Order is open order, which is stacking order. */
|
|
5
|
+
const stack = []
|
|
6
|
+
|
|
7
|
+
export function pushLayer() {
|
|
8
|
+
const id = {}
|
|
9
|
+
stack.push(id)
|
|
10
|
+
return id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function popLayer(id) {
|
|
14
|
+
const i = stack.indexOf(id)
|
|
15
|
+
if (i !== -1) stack.splice(i, 1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const isTopLayer = (id) => stack[stack.length - 1] === id
|
|
@@ -11,6 +11,9 @@ const EXT_KINDS = {
|
|
|
11
11
|
js: 'code', mjs: 'code', cjs: 'code', ts: 'code', jsx: 'code', tsx: 'code',
|
|
12
12
|
css: 'code', html: 'code', sh: 'code', py: 'code',
|
|
13
13
|
m3u8: 'playlist',
|
|
14
|
+
/* A PDF IS ITS OWN KIND (user 2026-09-23: *"File I guess is pdf? weird name"*). It fell through
|
|
15
|
+
* to `other`, so the one document type with a real page-one preview was labelled "File". */
|
|
16
|
+
pdf: 'pdf',
|
|
14
17
|
woff: 'font', woff2: 'font', ttf: 'font', otf: 'font',
|
|
15
18
|
zip: 'archive', gz: 'archive', tar: 'archive', rar: 'archive', '7z': 'archive',
|
|
16
19
|
}
|
|
@@ -42,12 +45,15 @@ export function kindOf(o) {
|
|
|
42
45
|
return 'other'
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
export const KINDS = ['audio', 'video', 'image', 'markdown', 'json', 'yaml', 'text', 'code', 'playlist', 'font', 'archive', 'other']
|
|
48
|
+
export const KINDS = ['audio', 'video', 'image', 'pdf', 'markdown', 'json', 'yaml', 'text', 'code', 'playlist', 'font', 'archive', 'other']
|
|
46
49
|
|
|
50
|
+
/* A LABEL IS CAPITALISED, AN ACRONYM IS AN ACRONYM (user 2026-09-22). These read as values in a
|
|
51
|
+
* facts table ("Kind · Markdown") and beside them sit the MIME type and the filename, which are
|
|
52
|
+
* DATA and stay exactly as stored. The filter chips are unaffected — `.kol-tag` uppercases. */
|
|
47
53
|
export const KIND_LABEL = {
|
|
48
|
-
image: '
|
|
49
|
-
playlist: 'HLS', font: '
|
|
50
|
-
system: '
|
|
54
|
+
image: 'Image', video: 'Video', audio: 'Audio', pdf: 'PDF', markdown: 'Markdown', json: 'JSON', yaml: 'YAML', text: 'Text', code: 'Code',
|
|
55
|
+
playlist: 'HLS', font: 'Font', archive: 'Archive', segments: 'HLS segments',
|
|
56
|
+
system: 'System', other: 'File',
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
/* The chips a bucket always shows (user ruling 2026-08-27): media, then the text
|