@kolkrabbi/kol-component 0.97.3 → 0.99.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 -3
- package/src/atoms/Tag.jsx +3 -1
- package/src/index.js +2 -0
- package/src/molecules/ContentCard.jsx +6 -3
- package/src/molecules/ContentText.jsx +3 -1
- package/src/molecules/KindPreview.jsx +93 -0
- package/src/molecules/ShellDrawer.jsx +1 -1
- package/src/organisms/ColumnBrowser.jsx +25 -17
- package/src/organisms/SettingsPanel.jsx +21 -14
- package/src/utilities/mediaKinds.js +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.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",
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"framer-motion": "^12.0.0",
|
|
25
25
|
"gsap": "^3.13.0",
|
|
26
26
|
"hls.js": "^1.6.0",
|
|
27
|
-
"opentype.js": "^1.3.4",
|
|
27
|
+
"opentype.js": "^1.3.4 || ^2.0.0",
|
|
28
28
|
"react": "^18.3.0 || ^19.0.0",
|
|
29
29
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
30
30
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
33
|
+
"@kolkrabbi/kol-icons": "^0.20.0"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
package/src/atoms/Tag.jsx
CHANGED
|
@@ -53,7 +53,8 @@ export default function Tag({
|
|
|
53
53
|
icon,
|
|
54
54
|
onRemove,
|
|
55
55
|
onClick,
|
|
56
|
-
className = ''
|
|
56
|
+
className = '',
|
|
57
|
+
...props
|
|
57
58
|
}) {
|
|
58
59
|
const isInteractive = !!(onClick || onRemove)
|
|
59
60
|
const Element = isInteractive ? 'button' : 'span'
|
|
@@ -92,6 +93,7 @@ export default function Tag({
|
|
|
92
93
|
|
|
93
94
|
return (
|
|
94
95
|
<Element
|
|
96
|
+
{...props}
|
|
95
97
|
type={isInteractive ? 'button' : undefined}
|
|
96
98
|
className={classes}
|
|
97
99
|
onClick={onClick}
|
package/src/index.js
CHANGED
|
@@ -141,6 +141,8 @@ export { default as SettingsPanel, SettingsRow, SettingsSwitch, SettingsChoice,
|
|
|
141
141
|
export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
|
|
142
142
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
143
143
|
export { default as ColumnBrowser } from './organisms/ColumnBrowser.jsx'
|
|
144
|
+
export { default as KindPreview } from './molecules/KindPreview.jsx'
|
|
145
|
+
export { kindOf, extOf, isSystemFile, KINDS, KIND_LABEL } from './utilities/mediaKinds.js'
|
|
144
146
|
export { default as RecordManager } from './organisms/RecordManager.jsx'
|
|
145
147
|
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
146
148
|
export { default as Table } from './organisms/Table.jsx'
|
|
@@ -44,7 +44,8 @@ const BOX = {
|
|
|
44
44
|
* like I've said that before" — the ListingCardThumbBorder ruling): the selected
|
|
45
45
|
* state reads from the checked ToggleCheckbox, not a fg-64 border */
|
|
46
46
|
default: { layout: 'stack', border: null, bg: 'var(--kol-fg-02)', pad: 'var(--kol-pad-card-sm)' },
|
|
47
|
-
|
|
47
|
+
/* frameHover: the frame steps fg-04 → fg-16 on hover (ShellHomeSystem, 2026-08-27 — the tertiary fill alone drowned the fg-04 border) */
|
|
48
|
+
catalog: { layout: 'fill-card', border: 'var(--kol-fg-04)', bg: 'var(--kol-fg-04)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, plateBg: 'var(--kol-surface-primary)', frameHover: 'var(--kol-fg-16)' },
|
|
48
49
|
/* flip: PrintGridCard's 3D turn on `isFlipped` → `selected` (ContentRowsAndPrintCard, 2026-08-27) */
|
|
49
50
|
print: { layout: 'fill-card', border: null, bg: 'var(--kol-surface-secondary)', pad: 'var(--kol-pad-card-sm) var(--kol-pad-card-md)', plateTop: true, flip: true },
|
|
50
51
|
article: { layout: 'stack', border: null, bg: null, pad: '0', mediaGap: 'var(--kol-spacing-4)' },
|
|
@@ -196,7 +197,8 @@ export default function ContentCard({
|
|
|
196
197
|
frame: frame ?? MEDIA[variant]?.frame ?? false,
|
|
197
198
|
ring: ring ?? MEDIA[variant]?.ring ?? false,
|
|
198
199
|
borderHover: MEDIA[variant]?.borderHover ?? false,
|
|
199
|
-
|
|
200
|
+
/* a catalog card with REAL media is image-led: it zooms (ShellHomeSystem) */
|
|
201
|
+
zoom: zoom ?? (isHero ? 'hero' : variant === 'catalog' ? media != null : MEDIA[variant]?.zoom ?? false),
|
|
200
202
|
fade: MEDIA[variant]?.fade ?? false,
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -301,7 +303,7 @@ export default function ContentCard({
|
|
|
301
303
|
|
|
302
304
|
const common = {
|
|
303
305
|
'data-tags': isHero && Array.isArray(text.tags) && text.tags.length ? text.tags.join(' ') : undefined,
|
|
304
|
-
className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${className}`.trim(),
|
|
306
|
+
className: `kol-card group flex ${box.layout === 'canvas' && reveal != null ? 'has-reveal' : ''} ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${interactive && box.frameHover ? 'kol-content-hover-frame' : ''} ${className}`.trim(),
|
|
305
307
|
style: {
|
|
306
308
|
/* same reason as ContentRow: rest colours are PROPERTIES, because an
|
|
307
309
|
* inline background/borderColor outranks the hover class and the step
|
|
@@ -309,6 +311,7 @@ export default function ContentCard({
|
|
|
309
311
|
'--kol-card-bg': box.bg ?? undefined,
|
|
310
312
|
'--kol-card-border': box.border ? (selected ? 'var(--kol-fg-64)' : box.border) : undefined,
|
|
311
313
|
'--kol-content-hover-bg': hoverBg ?? undefined,
|
|
314
|
+
'--kol-content-hover-border': box.frameHover ?? undefined,
|
|
312
315
|
aspectRatio: box.height || expanded ? undefined : (box.layout !== 'stack' ? r : undefined),
|
|
313
316
|
height: box.height,
|
|
314
317
|
gridColumn: expanded ? 'span 2' : undefined,
|
|
@@ -52,7 +52,9 @@ const RAMP = {
|
|
|
52
52
|
row: { title: 'kol-sans-heading-05 text-emphasis truncate', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta' },
|
|
53
53
|
},
|
|
54
54
|
catalog: {
|
|
55
|
-
|
|
55
|
+
/* detail truncates (ShellHomeSystem, 2026-08-27): a wrapping blurb set three
|
|
56
|
+
* cards' plates at three heights and misaligned the media */
|
|
57
|
+
card: { title: 'kol-mono-14 text-emphasis', detail: 'kol-mono-10 text-meta truncate' },
|
|
56
58
|
row: { title: 'kol-mono-12 text-emphasis', detail: 'kol-mono-10 text-meta' },
|
|
57
59
|
},
|
|
58
60
|
print: {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import AudioPlayer from '../atoms/AudioPlayer.jsx'
|
|
3
|
+
import HlsVideo from '../atoms/HlsVideo.jsx'
|
|
4
|
+
import CodeBlock from './CodeBlock.jsx'
|
|
5
|
+
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
6
|
+
import { kindOf as defaultKindOf, extOf as defaultExtOf, KIND_LABEL } from '../utilities/mediaKinds.js'
|
|
7
|
+
|
|
8
|
+
/* taxonomy-ok: molecule — nests the DS media atoms + CodeBlock (relative). */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* KindPreview — a preview for any kind of file: kol-r2b2's `KindPreview.jsx`,
|
|
12
|
+
* promoted 2026-08-27 (SettingsPanelChromeAndColumnPreview). Before it, anything
|
|
13
|
+
* that was not an image or a video showed a grey box with the word "text".
|
|
14
|
+
* HLS → `HlsVideo` (inert — the DS's background-video atom, preview only),
|
|
15
|
+
* audio → `AudioPlayer`, text / code → `CodeBlock` (language by extension;
|
|
16
|
+
* markdown as code — the DS has no markdown renderer in this tier), the rest
|
|
17
|
+
* → `AssetPlaceholder`. Images are the caller's (ColumnBrowser keeps its own
|
|
18
|
+
* `<img>` so it can read the dimensions). Text fetches cap at `textLimit`.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} o the object — `{ key, contentType?, displayKey?, segmentCount? }`
|
|
21
|
+
* @param {Function} urlOf (o) => string — the object's public URL (default: `o.url`)
|
|
22
|
+
* @param {string} poster a poster URL for HLS (the sibling image)
|
|
23
|
+
* @param {Function} kindOf · extOf classification seams (defaults: the DS mediaKinds)
|
|
24
|
+
* @param {Object} kindLabel kind → label
|
|
25
|
+
* @param {number} textLimit bytes of text fetched for a preview (default 200 KB)
|
|
26
|
+
*/
|
|
27
|
+
const TEXT_LIMIT = 200 * 1024
|
|
28
|
+
const LANG = {
|
|
29
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
|
|
30
|
+
ts: 'typescript', tsx: 'typescript', json: 'json', yaml: 'yaml', yml: 'yaml',
|
|
31
|
+
css: 'css', html: 'html', sh: 'bash', py: 'python', md: 'markdown',
|
|
32
|
+
csv: 'text', tsv: 'text', pgn: 'text', txt: 'text', xml: 'xml',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function useTextContent(url, enabled, limit) {
|
|
36
|
+
const [result, setResult] = useState({ url: null, text: '', error: null, truncated: false })
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!enabled || !url) return undefined
|
|
39
|
+
let cancelled = false
|
|
40
|
+
const controller = new AbortController()
|
|
41
|
+
fetch(url, { signal: controller.signal })
|
|
42
|
+
.then(async (res) => {
|
|
43
|
+
if (!res.ok) throw new Error(`${res.status}`)
|
|
44
|
+
const size = Number(res.headers.get('content-length') || 0)
|
|
45
|
+
const body = await res.text()
|
|
46
|
+
if (cancelled) return
|
|
47
|
+
const truncated = body.length > limit || size > limit
|
|
48
|
+
setResult({ url, text: truncated ? body.slice(0, limit) : body, error: null, truncated })
|
|
49
|
+
})
|
|
50
|
+
.catch((e) => { if (!cancelled && e.name !== 'AbortError') setResult({ url, text: '', error: e.message, truncated: false }) })
|
|
51
|
+
return () => { cancelled = true; controller.abort() }
|
|
52
|
+
}, [url, enabled, limit])
|
|
53
|
+
return { ...result, loading: enabled && result.url !== url }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function KindPreview({ o, urlOf = (x) => x.url, poster, kindOf = defaultKindOf, extOf = defaultExtOf, kindLabel = KIND_LABEL, textLimit = TEXT_LIMIT }) {
|
|
57
|
+
const kind = kindOf(o)
|
|
58
|
+
const url = urlOf(o)
|
|
59
|
+
const ext = extOf(o.key)
|
|
60
|
+
const name = o.displayKey ?? o.key
|
|
61
|
+
const isText = kind === 'text' || kind === 'code'
|
|
62
|
+
const { loading, text, error, truncated } = useTextContent(url, isText, textLimit)
|
|
63
|
+
|
|
64
|
+
if (kind === 'playlist') {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col items-center gap-2">
|
|
67
|
+
<HlsVideo src={url} poster={poster} className="max-w-full max-h-[70vh] rounded" />
|
|
68
|
+
<span className="kol-mono-12 text-meta">HLS stream · playback is preview-only, no controls</span>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (kind === 'audio') {
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex justify-center p-8">
|
|
75
|
+
<AudioPlayer src={url} label={name} className="items-center w-[420px] max-w-full" />
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
if (isText) {
|
|
80
|
+
if (loading) return <span className="kol-mono-12 text-meta">Loading…</span>
|
|
81
|
+
if (error) return <span className="kol-mono-12 text-ui-error">Couldn’t load: {error}</span>
|
|
82
|
+
return (
|
|
83
|
+
<div className="max-w-[80ch] max-h-[78vh] overflow-y-auto">
|
|
84
|
+
<CodeBlock code={text} language={LANG[ext] || 'text'} filename={name} />
|
|
85
|
+
{truncated && <p className="kol-mono-12 text-meta">truncated at {textLimit / 1024} KB</p>}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
if (kind === 'segments') {
|
|
90
|
+
return <AssetPlaceholder category="HLS" name={`${o.segmentCount} segments`} note="STREAM CHUNKS" className="w-[420px] max-w-full" />
|
|
91
|
+
}
|
|
92
|
+
return <AssetPlaceholder category={kindLabel[kind] || 'file'} name={name} note={ext ? ext.toUpperCase() : 'FILE'} className="w-[420px] max-w-full" />
|
|
93
|
+
}
|
|
@@ -151,7 +151,7 @@ export default function ShellDrawer({
|
|
|
151
151
|
tabIndex={-1}
|
|
152
152
|
className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4 outline-none md:px-5 lg:px-6 ${backdrop ? 'shadow-2xl' : ''} ${
|
|
153
153
|
side === 'right' ? 'right-0 border-l' : 'left-0 border-r'
|
|
154
|
-
} border-
|
|
154
|
+
} border-oq-08 ${width == null ? 'w-full' : ''} ${motionPanel} ${className}`}
|
|
155
155
|
style={width != null ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
|
|
156
156
|
>
|
|
157
157
|
{/* closeSide="start": the reference sets the × glyph ~9px deeper than
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
+
import KindPreview from '../molecules/KindPreview.jsx'
|
|
4
|
+
import { kindOf as dsKindOf, KIND_LABEL as DS_KIND_LABEL } from '../utilities/mediaKinds.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* ColumnBrowser — Finder-style Miller columns over a flat key space (kol-r2b2's
|
|
@@ -41,17 +43,13 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
41
43
|
* @param {Object} kindLabel kind → label shown when there is no visual preview
|
|
42
44
|
* @param {Function} formatSize (bytes) => string
|
|
43
45
|
* @param {Function} partition (objects, level) => { folders: string[], files: object[] }
|
|
46
|
+
* @param {Function} renderPreview (file) => ReactNode — replaces the preview column's media frame (the facts stay); without it images render the organism's <img>, everything else the DS KindPreview
|
|
44
47
|
* @param {string} className extra classes on the browser
|
|
45
48
|
*/
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (ct.startsWith('video/')) return 'video'
|
|
51
|
-
if (ct.startsWith('audio/')) return 'audio'
|
|
52
|
-
return 'file'
|
|
53
|
-
}
|
|
54
|
-
const DEFAULT_KIND_LABEL = { image: 'image', video: 'video', audio: 'audio', file: 'file' }
|
|
50
|
+
/* the DS kinds (mediaKinds — kol-r2b2's classification, promoted 2026-08-27) */
|
|
51
|
+
const defaultKindOf = dsKindOf
|
|
52
|
+
const DEFAULT_KIND_LABEL = DS_KIND_LABEL
|
|
55
53
|
const defaultFormatSize = (bytes) => {
|
|
56
54
|
if (bytes == null) return '—'
|
|
57
55
|
if (bytes < 1024) return `${bytes} B`
|
|
@@ -74,7 +72,7 @@ const defaultPartition = (objects, prefix) => {
|
|
|
74
72
|
const isImage = (o) => (o.contentType || '').startsWith('image/')
|
|
75
73
|
|
|
76
74
|
/* kol-icons has no `audio` glyph yet — audio rows wear `file` until it does */
|
|
77
|
-
const COL_ICON = { image: 'image', video: 'video', audio: 'file' }
|
|
75
|
+
const COL_ICON = { image: 'image', video: 'video', audio: 'file', playlist: 'video' }
|
|
78
76
|
|
|
79
77
|
function Row({ icon, label, active, cursor = false, trailing, onClick, muted = false }) {
|
|
80
78
|
return (
|
|
@@ -97,7 +95,7 @@ function Row({ icon, label, active, cursor = false, trailing, onClick, muted = f
|
|
|
97
95
|
)
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
function Preview({ o, urlOf, kindOf, kindLabel, formatSize }) {
|
|
98
|
+
function Preview({ o, urlOf, kindOf, kindLabel, formatSize, renderPreview }) {
|
|
101
99
|
// Pixel size comes from the loaded image itself — the bucket stores none.
|
|
102
100
|
const [dims, setDims] = useState(null)
|
|
103
101
|
const src = isImage(o) ? urlOf?.(o) : null
|
|
@@ -110,8 +108,12 @@ function Preview({ o, urlOf, kindOf, kindLabel, formatSize }) {
|
|
|
110
108
|
]
|
|
111
109
|
return (
|
|
112
110
|
<div className="kol-column-browser-preview w-[320px] shrink-0 overflow-y-auto p-4 flex flex-col gap-4">
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
{/* the media frame: an image is the organism's own <img> (it reads the
|
|
112
|
+
* dimensions); anything else is `renderPreview(o)` or the DS KindPreview
|
|
113
|
+
* (video · audio · code · text — SettingsPanelChromeAndColumnPreview,
|
|
114
|
+
* 2026-08-27). Dimensions also come off any <img> a custom node loads. */}
|
|
115
|
+
{src && !renderPreview ? (
|
|
116
|
+
<div className="w-full aspect-square bg-fg-04 rounded flex items-center justify-center overflow-hidden">
|
|
115
117
|
<img
|
|
116
118
|
src={src}
|
|
117
119
|
alt=""
|
|
@@ -119,10 +121,15 @@ function Preview({ o, urlOf, kindOf, kindLabel, formatSize }) {
|
|
|
119
121
|
loading="lazy"
|
|
120
122
|
onLoad={(e) => setDims({ w: e.target.naturalWidth, h: e.target.naturalHeight })}
|
|
121
123
|
/>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
<div
|
|
127
|
+
className="kol-column-browser-media w-full min-h-[160px] max-h-[60vh] overflow-auto rounded bg-fg-04 flex items-center justify-center"
|
|
128
|
+
onLoadCapture={(e) => { if (e.target?.tagName === 'IMG') setDims({ w: e.target.naturalWidth, h: e.target.naturalHeight }) }}
|
|
129
|
+
>
|
|
130
|
+
{renderPreview ? renderPreview(o) : <KindPreview o={o} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} />}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
126
133
|
<p className="kol-mono-12 text-fg-default break-all">{o.displayKey ?? o.key}</p>
|
|
127
134
|
<dl className="flex flex-col gap-1">
|
|
128
135
|
{facts.map(([k, v]) => (
|
|
@@ -148,6 +155,7 @@ export default function ColumnBrowser({
|
|
|
148
155
|
kindLabel = DEFAULT_KIND_LABEL,
|
|
149
156
|
formatSize = defaultFormatSize,
|
|
150
157
|
partition = defaultPartition,
|
|
158
|
+
renderPreview,
|
|
151
159
|
className = '',
|
|
152
160
|
}) {
|
|
153
161
|
const [picked, setPicked] = useState(null)
|
|
@@ -310,7 +318,7 @@ export default function ColumnBrowser({
|
|
|
310
318
|
</ul>
|
|
311
319
|
)
|
|
312
320
|
})}
|
|
313
|
-
{shown && <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} />}
|
|
321
|
+
{shown && <Preview key={shown.key} o={shown} urlOf={urlOf} kindOf={kindOf} kindLabel={kindLabel} formatSize={formatSize} renderPreview={renderPreview} />}
|
|
314
322
|
</div>
|
|
315
323
|
)
|
|
316
324
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Button from '../atoms/Button.jsx'
|
|
2
|
+
import Tag from '../atoms/Tag.jsx'
|
|
2
3
|
import SegmentedToggle from '../atoms/SegmentedToggle.jsx'
|
|
3
4
|
import ToggleSwitch from '../atoms/ToggleSwitch.jsx'
|
|
4
5
|
import ShellDrawer from '../molecules/ShellDrawer.jsx'
|
|
@@ -54,13 +55,17 @@ export default function SettingsPanel({
|
|
|
54
55
|
}) {
|
|
55
56
|
const header = (
|
|
56
57
|
<div className="flex min-w-0 flex-col">
|
|
57
|
-
{
|
|
58
|
-
|
|
58
|
+
{/* THE APP REGISTER (SettingsPanelChromeAndColumnPreview, kol-r2b2 2026-08-27 —
|
|
59
|
+
* user: "all wrong font styles"): the collection beside the panel is the
|
|
60
|
+
* reference — helper-14 uppercase titles, the eyebrow for section labels,
|
|
61
|
+
* mono-12 rows and hints. No mono-10 anywhere. */}
|
|
62
|
+
{title && <span className="kol-helper-14 uppercase text-emphasis">{title}</span>}
|
|
63
|
+
{subtitle && <span className="kol-mono-12 text-meta">{subtitle}</span>}
|
|
59
64
|
</div>
|
|
60
65
|
)
|
|
61
66
|
const body = (
|
|
62
67
|
<>
|
|
63
|
-
{intro && <p className="kol-mono-
|
|
68
|
+
{intro && <p className="kol-mono-12 text-meta">{intro}</p>}
|
|
64
69
|
<div className="flex flex-col gap-5">{children}</div>
|
|
65
70
|
{footer && <div className="mt-2">{footer}</div>}
|
|
66
71
|
</>
|
|
@@ -102,8 +107,8 @@ export function SettingsRow({ label, hint, children }) {
|
|
|
102
107
|
return (
|
|
103
108
|
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-6 py-1">
|
|
104
109
|
<div className="flex min-w-0 flex-col">
|
|
105
|
-
<span className="kol-mono-12 text-
|
|
106
|
-
{hint && <span className="kol-mono-
|
|
110
|
+
<span className="kol-mono-12 text-emphasis">{label}</span>
|
|
111
|
+
{hint && <span className="kol-mono-12 text-meta">{hint}</span>}
|
|
107
112
|
</div>
|
|
108
113
|
<div className="shrink-0">{children}</div>
|
|
109
114
|
</div>
|
|
@@ -132,10 +137,10 @@ export function SettingsChoice({ options = [], value, onChange, ariaLabel }) {
|
|
|
132
137
|
return <SegmentedToggle size="sm" value={value} onChange={onChange} options={opts} ariaLabel={ariaLabel} />
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
/* The
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
140
|
+
/* The chips are the DS `Tag` ("the tag ticket" — SettingsPanelChromeAndColumnPreview,
|
|
141
|
+
* user 2026-08-27: same chrome as Tag everywhere else, not a third look): `md`
|
|
142
|
+
* (helper-12), `inverse` when on, `secondary` when off, as a button. The old
|
|
143
|
+
* `.kol-control` string stays exported for consumers that typed it. */
|
|
139
144
|
export const CHIP_CLS = 'kol-control kol-control-sm kol-mono-12'
|
|
140
145
|
export const chipCls = (on) => `${CHIP_CLS} ${on ? 'kol-control--filled' : 'text-meta hover:text-emphasis'}`
|
|
141
146
|
|
|
@@ -151,16 +156,18 @@ export function SettingsChipRow({ options = [], selected = [], onToggle }) {
|
|
|
151
156
|
return (
|
|
152
157
|
<div className="flex flex-wrap gap-1">
|
|
153
158
|
{options.map((o) => (
|
|
154
|
-
<
|
|
159
|
+
<Tag
|
|
155
160
|
key={String(o.value)}
|
|
156
|
-
|
|
161
|
+
size="md"
|
|
162
|
+
hash={false}
|
|
163
|
+
variant={on.has(o.value) ? 'inverse' : 'secondary'}
|
|
164
|
+
active={on.has(o.value)}
|
|
157
165
|
aria-pressed={on.has(o.value)}
|
|
158
166
|
onClick={() => onToggle?.(o.value)}
|
|
159
|
-
className={chipCls(on.has(o.value))}
|
|
160
167
|
>
|
|
161
168
|
{o.label ?? String(o.value)}
|
|
162
169
|
{o.count > 0 && <span className="text-meta"> {o.count}</span>}
|
|
163
|
-
</
|
|
170
|
+
</Tag>
|
|
164
171
|
))}
|
|
165
172
|
</div>
|
|
166
173
|
)
|
|
@@ -170,7 +177,7 @@ export function SettingsChipRow({ options = [], selected = [], onToggle }) {
|
|
|
170
177
|
export function SettingsFooter({ customised = false, onReset, resetLabel = 'Reset to defaults' }) {
|
|
171
178
|
return (
|
|
172
179
|
<div className="flex items-center justify-between">
|
|
173
|
-
<span className="kol-mono-
|
|
180
|
+
<span className="kol-mono-12 text-meta">{customised ? 'customised' : 'defaults'}</span>
|
|
174
181
|
{onReset && <Button variant="ghost" size="sm" onClick={onReset}>{resetLabel}</Button>}
|
|
175
182
|
</div>
|
|
176
183
|
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Media classification — kol-r2b2's `lib/media.js` kinds, promoted 2026-08-27
|
|
2
|
+
* (SettingsPanelChromeAndColumnPreview) so ColumnBrowser and KindPreview
|
|
3
|
+
* classify the same way the app does. contentType is unreliable for text /
|
|
4
|
+
* code (B2 hands back application/octet-stream), so the extension decides. */
|
|
5
|
+
|
|
6
|
+
const EXT_KINDS = {
|
|
7
|
+
json: 'text', yaml: 'text', yml: 'text', txt: 'text', md: 'text', csv: 'text',
|
|
8
|
+
tsv: 'text', pgn: 'text', xml: 'text', svg: 'image',
|
|
9
|
+
js: 'code', mjs: 'code', cjs: 'code', ts: 'code', jsx: 'code', tsx: 'code',
|
|
10
|
+
css: 'code', html: 'code', sh: 'code', py: 'code',
|
|
11
|
+
m3u8: 'playlist',
|
|
12
|
+
woff: 'font', woff2: 'font', ttf: 'font', otf: 'font',
|
|
13
|
+
zip: 'archive', gz: 'archive', tar: 'archive', rar: 'archive', '7z': 'archive',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extOf(key = '') {
|
|
17
|
+
const base = key.slice(key.lastIndexOf('/') + 1)
|
|
18
|
+
const dot = base.lastIndexOf('.')
|
|
19
|
+
return dot === -1 ? '' : base.slice(dot + 1).toLowerCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SYSTEM_NAMES = new Set(['.ds_store', 'thumbs.db', 'desktop.ini', '.bzempty'])
|
|
23
|
+
export function isSystemFile(key = '') {
|
|
24
|
+
return SYSTEM_NAMES.has(key.slice(key.lastIndexOf('/') + 1).toLowerCase())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function kindOf(o) {
|
|
28
|
+
if (o.forcedKind) return o.forcedKind
|
|
29
|
+
if (isSystemFile(o.key)) return 'system'
|
|
30
|
+
const ct = o.contentType || ''
|
|
31
|
+
if (ct.startsWith('image/')) return 'image'
|
|
32
|
+
if (ct.startsWith('video/')) return 'video'
|
|
33
|
+
if (ct.startsWith('audio/')) return 'audio'
|
|
34
|
+
const ext = extOf(o.key)
|
|
35
|
+
if (EXT_KINDS[ext]) return EXT_KINDS[ext]
|
|
36
|
+
if (ct.startsWith('text/')) return 'text'
|
|
37
|
+
if (ct.startsWith('font/')) return 'font'
|
|
38
|
+
if (ct === 'application/json') return 'text'
|
|
39
|
+
return 'other'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const KINDS = ['image', 'video', 'audio', 'text', 'code', 'playlist', 'font', 'archive', 'other']
|
|
43
|
+
|
|
44
|
+
export const KIND_LABEL = {
|
|
45
|
+
image: 'image', video: 'video', audio: 'audio', text: 'text', code: 'code',
|
|
46
|
+
playlist: 'HLS', font: 'font', archive: 'archive', segments: 'HLS segments',
|
|
47
|
+
system: 'system', other: 'file',
|
|
48
|
+
}
|