@kolkrabbi/kol-component 0.100.1 → 0.102.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.
|
|
3
|
+
"version": "0.102.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",
|
package/src/index.js
CHANGED
|
@@ -137,12 +137,13 @@ export { default as LoaderOverlay } from './utilities/LoaderOverlay.jsx'
|
|
|
137
137
|
export { default as MediaLibrary, MediaLibraryProvider, useMediaLibrary, MediaPicker, MediaBrowser } from './organisms/MediaLibrary.jsx'
|
|
138
138
|
export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
|
|
139
139
|
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
140
|
-
export { default as SettingsPanel, SettingsRow, SettingsSwitch, SettingsChoice, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
|
|
140
|
+
export { default as SettingsPanel, SettingsSection, SettingsRow, SettingsSwitch, SettingsChoice, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
|
|
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
144
|
export { default as KindPreview } from './molecules/KindPreview.jsx'
|
|
145
145
|
export { kindOf, extOf, isSystemFile, KINDS, KIND_LABEL } from './utilities/mediaKinds.js'
|
|
146
|
+
export { default as markdownToHtml, inlineToHtml } from './utilities/markdownToHtml.js'
|
|
146
147
|
export { default as RecordManager } from './organisms/RecordManager.jsx'
|
|
147
148
|
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
148
149
|
export { default as Table } from './organisms/Table.jsx'
|
|
@@ -4,6 +4,7 @@ import HlsVideo from '../atoms/HlsVideo.jsx'
|
|
|
4
4
|
import CodeBlock from './CodeBlock.jsx'
|
|
5
5
|
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
6
6
|
import { kindOf as defaultKindOf, extOf as defaultExtOf, KIND_LABEL } from '../utilities/mediaKinds.js'
|
|
7
|
+
import markdownToHtml from '../utilities/markdownToHtml.js'
|
|
7
8
|
|
|
8
9
|
/* taxonomy-ok: molecule — nests the DS media atoms + CodeBlock (relative). */
|
|
9
10
|
|
|
@@ -12,8 +13,9 @@ import { kindOf as defaultKindOf, extOf as defaultExtOf, KIND_LABEL } from '../u
|
|
|
12
13
|
* promoted 2026-08-27 (SettingsPanelChromeAndColumnPreview). Before it, anything
|
|
13
14
|
* that was not an image or a video showed a grey box with the word "text".
|
|
14
15
|
* HLS → `HlsVideo` (inert — the DS's background-video atom, preview only),
|
|
15
|
-
* audio → `AudioPlayer`,
|
|
16
|
-
*
|
|
16
|
+
* audio → `AudioPlayer`, markdown → rendered prose in `.kol-prose`
|
|
17
|
+
* (markdownToHtml, KindPreviewMarkdown 2026-08-27), json / yaml / text / code →
|
|
18
|
+
* `CodeBlock` (language by kind or extension), the rest
|
|
17
19
|
* → `AssetPlaceholder`. Images are the caller's (ColumnBrowser keeps its own
|
|
18
20
|
* `<img>` so it can read the dimensions). Text fetches cap at `textLimit`.
|
|
19
21
|
*
|
|
@@ -58,7 +60,7 @@ export default function KindPreview({ o, urlOf = (x) => x.url, poster, kindOf =
|
|
|
58
60
|
const url = urlOf(o)
|
|
59
61
|
const ext = extOf(o.key)
|
|
60
62
|
const name = o.displayKey ?? o.key
|
|
61
|
-
const isText = kind === 'text' || kind === 'code'
|
|
63
|
+
const isText = kind === 'text' || kind === 'code' || kind === 'markdown' || kind === 'json' || kind === 'yaml'
|
|
62
64
|
const { loading, text, error, truncated } = useTextContent(url, isText, textLimit)
|
|
63
65
|
|
|
64
66
|
if (kind === 'playlist') {
|
|
@@ -79,9 +81,18 @@ export default function KindPreview({ o, urlOf = (x) => x.url, poster, kindOf =
|
|
|
79
81
|
if (isText) {
|
|
80
82
|
if (loading) return <span className="kol-mono-12 text-meta">Loading…</span>
|
|
81
83
|
if (error) return <span className="kol-mono-12 text-ui-error">Couldn’t load: {error}</span>
|
|
84
|
+
if (kind === 'markdown') {
|
|
85
|
+
return (
|
|
86
|
+
<div className="max-w-[70ch] max-h-[78vh] overflow-y-auto">
|
|
87
|
+
<div className="kol-prose" dangerouslySetInnerHTML={{ __html: markdownToHtml(text) }} />
|
|
88
|
+
{truncated && <p className="kol-mono-12 text-meta">truncated at {textLimit / 1024} KB</p>}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
const language = kind === 'json' ? 'json' : kind === 'yaml' ? 'yaml' : LANG[ext] || 'text'
|
|
82
93
|
return (
|
|
83
94
|
<div className="max-w-[80ch] max-h-[78vh] overflow-y-auto">
|
|
84
|
-
<CodeBlock code={text} language={
|
|
95
|
+
<CodeBlock code={text} language={language} filename={name} />
|
|
85
96
|
{truncated && <p className="kol-mono-12 text-meta">truncated at {textLimit / 1024} KB</p>}
|
|
86
97
|
</div>
|
|
87
98
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Button from '../atoms/Button.jsx'
|
|
2
|
-
import
|
|
2
|
+
import Dropdown from '../molecules/Dropdown.jsx'
|
|
3
|
+
import SectionLabel from '../atoms/SectionLabel.jsx'
|
|
3
4
|
import ToggleSwitch from '../atoms/ToggleSwitch.jsx'
|
|
4
5
|
import ShellDrawer from '../molecules/ShellDrawer.jsx'
|
|
5
6
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
@@ -21,7 +22,7 @@ import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
|
21
22
|
* where the DS already owned one.
|
|
22
23
|
*
|
|
23
24
|
* The CONTROLS are the DS controls, not the source's word-buttons: a switch
|
|
24
|
-
* row is `ToggleSwitch`, a choice row is `
|
|
25
|
+
* row is `ToggleSwitch`, a choice row is a `Dropdown` (2026-08-27) — the source's
|
|
25
26
|
* segmented variant was flagged by the user as the thing to fix here, not to
|
|
26
27
|
* copy. Sections are the DS `Section` (pass `divided` on every one and the
|
|
27
28
|
* hairline lands between siblings on its own).
|
|
@@ -129,11 +130,26 @@ export function SettingsSwitch({ on = false, onChange, disabled = false, disable
|
|
|
129
130
|
)
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
|
|
133
|
+
/** SettingsSection — a section of the panel: the estate's section label (the DS
|
|
134
|
+
* `SectionLabel`, sm — SettingsPanelEyebrowAndDropdowns, user 2026-08-27: "it's
|
|
135
|
+
* literally SECTION LABEL, SUPER COMMON PATTERN") over its rows.
|
|
136
|
+
* @param {string} label · @param {boolean} divided a hairline above (between sections) */
|
|
137
|
+
export function SettingsSection({ label, divided = false, children, className = '' }) {
|
|
138
|
+
return (
|
|
139
|
+
<div className={`flex flex-col gap-3 ${divided ? 'kol-section--divided' : ''} ${className}`.replace(/\s+/g, ' ').trim()}>
|
|
140
|
+
{label && <SectionLabel text={label} size="sm" />}
|
|
141
|
+
{children}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** SettingsChoice — the row's one-of-N control: the DS Dropdown, sm · primary
|
|
147
|
+
* (SettingsPanelEyebrowAndDropdowns — user: "put the toggles inside a dropdown,
|
|
148
|
+
* because it's super messy like it is"). Options are values or `{ value, label }`;
|
|
149
|
+
* width is the call site's (`className="w-40"`). */
|
|
150
|
+
export function SettingsChoice({ options = [], value, onChange, ariaLabel, className = '' }) {
|
|
135
151
|
const opts = options.map((o) => (o != null && typeof o === 'object' ? o : { value: o, label: String(o) }))
|
|
136
|
-
return <
|
|
152
|
+
return <Dropdown size="sm" variant="primary" value={value} onChange={onChange} options={opts} className={className} aria-label={ariaLabel} />
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
/* THE CONTROL CHIP (SettingsPanelCompliance, user 2026-08-27: "WROOOONG" on the
|
|
@@ -146,15 +162,26 @@ export const chipCls = (on) => `${CHIP_CLS} ${on ? 'kol-control--filled' : 'text
|
|
|
146
162
|
|
|
147
163
|
/**
|
|
148
164
|
* SettingsChipRow — a wrap of toggle chips with optional counts (an
|
|
149
|
-
* allow-list: every chip sets a default, never a gate).
|
|
165
|
+
* allow-list: every chip sets a default, never a gate). `allChip` puts an
|
|
166
|
+
* "all" chip first — the same control chip, filled when every option is on
|
|
167
|
+
* (SettingsPanelEyebrowAndDropdowns — user: "Show all in ghost mode? makes no
|
|
168
|
+
* sense"); `onAll(nextAllOn)` fires when it is clicked.
|
|
150
169
|
* @param {Array} options [{ value, label, count? }]
|
|
151
170
|
* @param {Array|Set} selected the values that are on
|
|
152
171
|
* @param {Function} onToggle (value) => void
|
|
172
|
+
* @param {boolean|string} allChip render the "all" chip (a string = its label)
|
|
173
|
+
* @param {Function} onAll (nextAllOn: boolean) => void
|
|
153
174
|
*/
|
|
154
|
-
export function SettingsChipRow({ options = [], selected = [], onToggle }) {
|
|
175
|
+
export function SettingsChipRow({ options = [], selected = [], onToggle, allChip = false, onAll }) {
|
|
155
176
|
const on = selected instanceof Set ? selected : new Set(selected)
|
|
177
|
+
const allOn = options.length > 0 && options.every((o) => on.has(o.value))
|
|
156
178
|
return (
|
|
157
179
|
<div className="flex flex-wrap gap-1">
|
|
180
|
+
{allChip && (
|
|
181
|
+
<button type="button" aria-pressed={allOn} onClick={() => onAll?.(!allOn)} className={chipCls(allOn)}>
|
|
182
|
+
{typeof allChip === 'string' ? allChip : 'all'}
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
158
185
|
{options.map((o) => (
|
|
159
186
|
<button
|
|
160
187
|
key={String(o.value)}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdownToHtml — a small, escape-first markdown → HTML renderer for
|
|
3
|
+
* `.kol-prose` (KindPreviewMarkdown, kol-r2b2 2026-08-27 — user: "why is it not
|
|
4
|
+
* rendering in preview using kol-prose?"). No dependency: the theme's prose CSS
|
|
5
|
+
* styles bare tags, so bare tags are what this emits. Covers headings,
|
|
6
|
+
* paragraphs, bold / italic / inline code, links and images, bullet and
|
|
7
|
+
* numbered lists, blockquotes, fenced code, rules and pipe tables. Every
|
|
8
|
+
* character is HTML-escaped before any markup is added; link and image URLs
|
|
9
|
+
* allow http(s), mailto, and relative paths only.
|
|
10
|
+
*
|
|
11
|
+
* ponytail: enough for READMEs and notes; a CommonMark engine is the upgrade if
|
|
12
|
+
* nested lists or reference links turn up.
|
|
13
|
+
*/
|
|
14
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
15
|
+
const safeUrl = (u) => (/^(https?:|mailto:|\/|\.\/|\.\.\/|#|[a-z0-9_-]+(\/|\.|$))/i.test(u.trim()) && !/^javascript:/i.test(u.trim()) ? u.trim() : '#')
|
|
16
|
+
|
|
17
|
+
export const inlineToHtml = (raw) => {
|
|
18
|
+
let s = esc(raw)
|
|
19
|
+
s = s.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`)
|
|
20
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `<img alt="${alt}" src="${safeUrl(src)}">`)
|
|
21
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, href) => `<a href="${safeUrl(href)}">${t}</a>`)
|
|
22
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/__([^_]+)__/g, '<strong>$1</strong>')
|
|
23
|
+
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>').replace(/(^|[^_])_([^_\n]+)_/g, '$1<em>$2</em>')
|
|
24
|
+
return s
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function markdownToHtml(markdown = '') {
|
|
28
|
+
const lines = markdown.replace(/\r\n?/g, '\n').split('\n')
|
|
29
|
+
const out = []
|
|
30
|
+
let i = 0
|
|
31
|
+
const para = []
|
|
32
|
+
const flush = () => { if (para.length) { out.push(`<p>${inlineToHtml(para.join(' ').trim())}</p>`); para.length = 0 } }
|
|
33
|
+
/* frontmatter at the very top is skipped */
|
|
34
|
+
if (lines[0]?.trim() === '---') { const end = lines.indexOf('---', 1); if (end > 0) i = end + 1 }
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
const line = lines[i]; const t = line.trim()
|
|
37
|
+
if (t.startsWith('```')) {
|
|
38
|
+
flush(); const lang = t.slice(3).trim(); const buf = []; i++
|
|
39
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) buf.push(lines[i++])
|
|
40
|
+
i++; out.push(`<pre><code${lang ? ` class="language-${esc(lang)}"` : ''}>${esc(buf.join('\n'))}</code></pre>`); continue
|
|
41
|
+
}
|
|
42
|
+
if (!t) { flush(); i++; continue }
|
|
43
|
+
const h = t.match(/^(#{1,6})\s+(.*)$/)
|
|
44
|
+
if (h) { flush(); out.push(`<h${h[1].length}>${inlineToHtml(h[2])}</h${h[1].length}>`); i++; continue }
|
|
45
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(t)) { flush(); out.push('<hr>'); i++; continue }
|
|
46
|
+
if (t.startsWith('>')) {
|
|
47
|
+
flush(); const buf = []
|
|
48
|
+
while (i < lines.length && lines[i].trim().startsWith('>')) buf.push(lines[i++].trim().replace(/^>\s?/, ''))
|
|
49
|
+
out.push(`<blockquote>${markdownToHtml(buf.join('\n'))}</blockquote>`); continue
|
|
50
|
+
}
|
|
51
|
+
if (/^[-*+]\s+/.test(t) || /^\d+[.)]\s+/.test(t)) {
|
|
52
|
+
flush(); const ordered = /^\d+[.)]\s+/.test(t); const items = []
|
|
53
|
+
while (i < lines.length && (/^[-*+]\s+/.test(lines[i].trim()) || /^\d+[.)]\s+/.test(lines[i].trim()))) items.push(lines[i++].trim().replace(/^([-*+]|\d+[.)])\s+/, ''))
|
|
54
|
+
out.push(`<${ordered ? 'ol' : 'ul'}>${items.map((it) => `<li>${inlineToHtml(it)}</li>`).join('')}</${ordered ? 'ol' : 'ul'}>`); continue
|
|
55
|
+
}
|
|
56
|
+
if (t.startsWith('|') && lines[i + 1]?.trim().match(/^\|?\s*:?-{2,}/)) {
|
|
57
|
+
flush(); const cells = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => inlineToHtml(c.trim()))
|
|
58
|
+
const head = cells(t); i += 2; const rows = []
|
|
59
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) rows.push(cells(lines[i++]))
|
|
60
|
+
out.push(`<table><thead><tr>${head.map((c) => `<th>${c}</th>`).join('')}</tr></thead><tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`); continue
|
|
61
|
+
}
|
|
62
|
+
para.push(t); i++
|
|
63
|
+
}
|
|
64
|
+
flush()
|
|
65
|
+
return out.join('\n')
|
|
66
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* code (B2 hands back application/octet-stream), so the extension decides. */
|
|
5
5
|
|
|
6
6
|
const EXT_KINDS = {
|
|
7
|
-
json
|
|
8
|
-
|
|
7
|
+
/* markdown · json · yaml are their own kinds (user ruling 2026-08-27 — "I want
|
|
8
|
+
* md always to show, make it unique"); text is the rest */
|
|
9
|
+
md: 'markdown', json: 'json', yaml: 'yaml', yml: 'yaml',
|
|
10
|
+
txt: 'text', csv: 'text', tsv: 'text', pgn: 'text', xml: 'text', svg: 'image',
|
|
9
11
|
js: 'code', mjs: 'code', cjs: 'code', ts: 'code', jsx: 'code', tsx: 'code',
|
|
10
12
|
css: 'code', html: 'code', sh: 'code', py: 'code',
|
|
11
13
|
m3u8: 'playlist',
|
|
@@ -33,16 +35,17 @@ export function kindOf(o) {
|
|
|
33
35
|
if (ct.startsWith('audio/')) return 'audio'
|
|
34
36
|
const ext = extOf(o.key)
|
|
35
37
|
if (EXT_KINDS[ext]) return EXT_KINDS[ext]
|
|
38
|
+
if (ct === 'text/markdown') return 'markdown'
|
|
39
|
+
if (ct === 'application/json') return 'json'
|
|
36
40
|
if (ct.startsWith('text/')) return 'text'
|
|
37
41
|
if (ct.startsWith('font/')) return 'font'
|
|
38
|
-
if (ct === 'application/json') return 'text'
|
|
39
42
|
return 'other'
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
export const KINDS = ['
|
|
45
|
+
export const KINDS = ['audio', 'video', 'image', 'markdown', 'json', 'yaml', 'text', 'code', 'playlist', 'font', 'archive', 'other']
|
|
43
46
|
|
|
44
47
|
export const KIND_LABEL = {
|
|
45
|
-
image: 'image', video: 'video', audio: 'audio', text: 'text', code: 'code',
|
|
48
|
+
image: 'image', video: 'video', audio: 'audio', markdown: 'markdown', json: 'JSON', yaml: 'YAML', text: 'text', code: 'code',
|
|
46
49
|
playlist: 'HLS', font: 'font', archive: 'archive', segments: 'HLS segments',
|
|
47
50
|
system: 'system', other: 'file',
|
|
48
51
|
}
|