@kolkrabbi/kol-component 0.100.0 → 0.101.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.101.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
|
@@ -143,6 +143,7 @@ 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,4 @@
|
|
|
1
1
|
import Button from '../atoms/Button.jsx'
|
|
2
|
-
import Tag from '../atoms/Tag.jsx'
|
|
3
2
|
import SegmentedToggle from '../atoms/SegmentedToggle.jsx'
|
|
4
3
|
import ToggleSwitch from '../atoms/ToggleSwitch.jsx'
|
|
5
4
|
import ShellDrawer from '../molecules/ShellDrawer.jsx'
|
|
@@ -137,10 +136,11 @@ export function SettingsChoice({ options = [], value, onChange, ariaLabel }) {
|
|
|
137
136
|
return <SegmentedToggle size="sm" value={value} onChange={onChange} options={opts} ariaLabel={ariaLabel} />
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
/*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
139
|
+
/* THE CONTROL CHIP (SettingsPanelCompliance, user 2026-08-27: "WROOOONG" on the
|
|
140
|
+
* 0.98.0 Tag pills): `.kol-control` sm on kol-mono-12, filled when on, meta ink
|
|
141
|
+
* when off, the count in meta after the label — the SAME string ContentFilters'
|
|
142
|
+
* kind chips, ViewToggle's text variant and the SELECT / FLAT strip wear. No
|
|
143
|
+
* Tag, no uppercase, no inverse. */
|
|
144
144
|
export const CHIP_CLS = 'kol-control kol-control-sm kol-mono-12'
|
|
145
145
|
export const chipCls = (on) => `${CHIP_CLS} ${on ? 'kol-control--filled' : 'text-meta hover:text-emphasis'}`
|
|
146
146
|
|
|
@@ -156,18 +156,16 @@ export function SettingsChipRow({ options = [], selected = [], onToggle }) {
|
|
|
156
156
|
return (
|
|
157
157
|
<div className="flex flex-wrap gap-1">
|
|
158
158
|
{options.map((o) => (
|
|
159
|
-
<
|
|
159
|
+
<button
|
|
160
160
|
key={String(o.value)}
|
|
161
|
-
|
|
162
|
-
hash={false}
|
|
163
|
-
variant={on.has(o.value) ? 'inverse' : 'secondary'}
|
|
164
|
-
active={on.has(o.value)}
|
|
161
|
+
type="button"
|
|
165
162
|
aria-pressed={on.has(o.value)}
|
|
166
163
|
onClick={() => onToggle?.(o.value)}
|
|
164
|
+
className={chipCls(on.has(o.value))}
|
|
167
165
|
>
|
|
168
166
|
{o.label ?? String(o.value)}
|
|
169
167
|
{o.count > 0 && <span className="text-meta"> {o.count}</span>}
|
|
170
|
-
</
|
|
168
|
+
</button>
|
|
171
169
|
))}
|
|
172
170
|
</div>
|
|
173
171
|
)
|
|
@@ -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
|
}
|