@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.100.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`, text / code `CodeBlock` (language by extension;
16
- * markdown as code the DS has no markdown renderer in this tier), the rest
16
+ * audio → `AudioPlayer`, markdownrendered 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={LANG[ext] || 'text'} filename={name} />
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
- /* 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
+ /* 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
- <Tag
159
+ <button
160
160
  key={String(o.value)}
161
- size="md"
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
- </Tag>
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
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: 'text', yaml: 'text', yml: 'text', txt: 'text', md: 'text', csv: 'text',
8
- tsv: 'text', pgn: 'text', xml: 'text', svg: 'image',
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 = ['image', 'video', 'audio', 'text', 'code', 'playlist', 'font', 'archive', 'other']
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
  }