@kolkrabbi/kol-workshop 0.38.0 → 0.40.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-workshop",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "private": false,
5
5
  "description": "Docs viewer, shell and tag system",
6
6
  "license": "MIT",
@@ -13,12 +13,12 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "d3": "^7.9.0",
16
- "@kolkrabbi/kol-markdown": "^0.1.3",
17
16
  "@kolkrabbi/kol-search": "^0.3.0",
17
+ "@kolkrabbi/kol-markdown": "^0.1.3",
18
18
  "@kolkrabbi/kol-brand": "^0.1.3"
19
19
  },
20
20
  "peerDependencies": {
21
- "@kolkrabbi/kol-component": ">=0.237.0",
21
+ "@kolkrabbi/kol-component": ">=0.266.0",
22
22
  "@kolkrabbi/kol-framework": ">=0.45.0",
23
23
  "@kolkrabbi/kol-theme": ">=0.165.0",
24
24
  "@kolkrabbi/kol-icons": ">=0.18.0",
@@ -28,10 +28,10 @@
28
28
  "react-router-dom": "^6.0.0 || ^7.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@kolkrabbi/kol-component": "^0.239.0",
32
- "@kolkrabbi/kol-icons": "^0.33.0",
33
- "@kolkrabbi/kol-theme": "^0.165.0",
34
- "@kolkrabbi/kol-framework": "^0.49.0"
31
+ "@kolkrabbi/kol-framework": "^0.50.0",
32
+ "@kolkrabbi/kol-icons": "^0.39.0",
33
+ "@kolkrabbi/kol-component": "^0.266.0",
34
+ "@kolkrabbi/kol-theme": "^0.174.0"
35
35
  },
36
36
  "files": [
37
37
  "src",
@@ -137,17 +137,34 @@ const CASED_VALUE_FIELDS = new Set(['type', 'status'])
137
137
  /* a doc's lifecycle on Badge's status tones (W17): live reads success, in progress warning, gone error */
138
138
  const STATUS_TONE = { active: 'success', canonical: 'info', draft: 'warning', archived: 'error', superseded: 'error', deprecated: 'error' }
139
139
 
140
- /** Contract order first, then anything else the doc carries, alphabetically. */
141
- const orderFields = (metadata) => {
140
+ /** Contract order first, then anything else the doc carries, alphabetically. A consumer's
141
+ * `fields[key].order` places a key at that index of the list; `hidden` drops it. */
142
+ const orderFields = (metadata, fields = {}) => {
142
143
  const present = Object.keys(metadata).filter(
143
- (k) => !HIDDEN.has(k) && metadata[k] != null && metadata[k] !== '' &&
144
+ (k) => !(fields[k]?.hidden ?? HIDDEN.has(k)) && metadata[k] != null && metadata[k] !== '' &&
144
145
  !(Array.isArray(metadata[k]) && metadata[k].length === 0)
145
146
  )
146
- const known = FIELD_ORDER.filter((k) => present.includes(k))
147
- const rest = present.filter((k) => !FIELD_ORDER.includes(k)).sort()
148
- return [...known, ...rest]
147
+ const placed = present.filter((k) => Number.isFinite(fields[k]?.order))
148
+ const known = FIELD_ORDER.filter((k) => present.includes(k) && !placed.includes(k))
149
+ const rest = present.filter((k) => !FIELD_ORDER.includes(k) && !placed.includes(k)).sort()
150
+ const list = [...known, ...rest]
151
+ for (const k of placed.sort((a, b) => fields[a].order - fields[b].order)) list.splice(Math.min(fields[k].order, list.length), 0, k)
152
+ return list
149
153
  }
150
154
 
155
+ /* A VALUE THAT IS A URL IS A LINK (reader-takes-field-config-and-page-actions, kol-website
156
+ * 2026-10-05): `url`/`repo` printed as plain text. Printed without the protocol, in the body's
157
+ * link idiom, opening a new tab as the body's external links do (render-tokens). */
158
+ const URL_RE = /^https?:\/\//
159
+ const LINK_CLS = 'text-emphasis underline decoration-fg-16 underline-offset-2 hover:decoration-current break-all'
160
+ const printValue = (v) => {
161
+ const s = String(v)
162
+ return URL_RE.test(s) ? <a href={s} target="_blank" rel="noreferrer" className={LINK_CLS}>{s.replace(URL_RE, '').replace(/\/$/, '')}</a> : s
163
+ }
164
+ /* …and a key with no icon entry takes this one, so the label column keeps one left edge whatever
165
+ * a document carries (the 2026-08-01 rule above, made true for keys this file has never seen) */
166
+ const DEFAULT_ICON = 'hash-01'
167
+
151
168
  /* Every field rendered as a date. `updated`/`created`/`verified` are the
152
169
  * kol-docs names; `date`/`modified` are the sample dialect's. */
153
170
  const DATE_FIELDS = new Set(['updated', 'created', 'verified', 'date', 'modified'])
@@ -159,7 +176,13 @@ const formatDate = (dateStr) => {
159
176
  return d.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }).replace(/\//g, '.')
160
177
  }
161
178
 
162
- const DocsFrontmatter = ({ metadata, docId }) => {
179
+ /**
180
+ * @param {Object} [fields] per-key config from the consumer, merged over this file's tables:
181
+ * `{ [key]: { label, icon, order, hidden, render(value, metadata) } }`
182
+ * (2026-10-05, kol-website /workshop). No config is the output as before,
183
+ * apart from URL values as links and the default icon.
184
+ */
185
+ const DocsFrontmatter = ({ metadata, docId, fields: config = {} }) => {
163
186
  const { openTagMode } = useTagMode()
164
187
  /* THE PANEL COLLAPSES, THE PAGE SCROLLS (user call 2026-08-09). A long
165
188
  * frontmatter (Dropdown: classes + reuses + tokens) made the panel the
@@ -170,7 +193,7 @@ const DocsFrontmatter = ({ metadata, docId }) => {
170
193
  const [expanded, setExpanded] = useState(false)
171
194
  const [overflowing, setOverflowing] = useState(false)
172
195
 
173
- const fields = metadata ? orderFields(metadata) : []
196
+ const fields = metadata ? orderFields(metadata, config) : []
174
197
 
175
198
  useLayoutEffect(() => {
176
199
  const el = bodyRef.current
@@ -190,16 +213,17 @@ const DocsFrontmatter = ({ metadata, docId }) => {
190
213
  >
191
214
  {fields.map((key) => {
192
215
  const value = metadata[key]
193
- const icon = FIELD_ICONS[key]
216
+ const cfg = config[key] ?? {}
217
+ const icon = cfg.icon ?? FIELD_ICONS[key] ?? DEFAULT_ICON
194
218
 
195
219
  return (
196
220
  <div key={key} className="docs-frontmatter-row">
197
221
  <span className="docs-frontmatter-key kol-helper-12 text-meta">
198
222
  {icon && <Icon name={icon} size={14} className="text-oq-48" />}
199
- {FIELD_LABELS[key] ?? humanise(key)}
223
+ {cfg.label ?? FIELD_LABELS[key] ?? humanise(key)}
200
224
  </span>
201
225
  <span className="docs-frontmatter-value kol-mono-12 text-strong">
202
- {key === 'tags' && Array.isArray(value) ? (
226
+ {cfg.render ? cfg.render(value, metadata) : key === 'tags' && Array.isArray(value) ? (
203
227
  /* `naked`, the SAME Tag rendering the rail's own tag list uses
204
228
  * (DocReaderSidebar). This was the default filled variant, so
205
229
  * the one concept rendered as a solid pill here and as plain
@@ -248,11 +272,11 @@ const DocsFrontmatter = ({ metadata, docId }) => {
248
272
  * and a glyph restating it is decoration. */
249
273
  <span className="docs-frontmatter-list">
250
274
  {value.map((item, i) => (
251
- <span key={i} className="break-all">{String(item)}</span>
275
+ <span key={i} className="break-all">{printValue(item)}</span>
252
276
  ))}
253
277
  </span>
254
278
  ) : (
255
- String(value)
279
+ printValue(value)
256
280
  )}
257
281
  </span>
258
282
  </div>
@@ -77,6 +77,11 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
77
77
  * (2026-09-30, the names audit: every level has a home).
78
78
  * @param {boolean} [props.showFrontmatter=true] the frontmatter panel; a home hides it by
79
79
  * default and the consumer toggles it.
80
+ * @param {Object} [props.fields] per-key frontmatter config, handed to DocsFrontmatter:
81
+ * `{ [key]: { label, icon, order, hidden, render } }`
82
+ * @param {node} [props.actions] the page's own actions (a button to the live app),
83
+ * rendered under the title and before the intro; absent,
84
+ * nothing (2026-10-05, kol-website /workshop)
80
85
  */
81
86
  const DocumentationReader = ({
82
87
  inventory = [],
@@ -85,6 +90,8 @@ const DocumentationReader = ({
85
90
  routes = {},
86
91
  docId: docIdProp,
87
92
  showFrontmatter = true,
93
+ fields,
94
+ actions,
88
95
  /* false → render the article only and leave the right rail to the page (a HOME sits on top of
89
96
  * a page whose sections the rail must list — 2026-09-30, "This page (0)" on every home) */
90
97
  rail = true,
@@ -365,10 +372,11 @@ const DocumentationReader = ({
365
372
 
366
373
  return (
367
374
  <DocsArticle>
368
- {showFrontmatter && <DocsFrontmatter metadata={doc.metadata} docId={docId} />}
375
+ {showFrontmatter && <DocsFrontmatter metadata={doc.metadata} docId={docId} fields={fields} />}
369
376
  {docTitle && (
370
377
  <h1 id="page-top" className="kol-doc-heading scroll-mt-20">{docTitle}</h1>
371
378
  )}
379
+ {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
372
380
  {/* Render intro blocks (excluding H1 which is docTitle) */}
373
381
  {introBlocks.filter(b => b.type !== 'heading1').map((block, index) =>
374
382
  renderBlock(block, `intro-${block.type}-${index}`)
@@ -31,7 +31,7 @@ import { Link, NavLink } from 'react-router-dom'
31
31
  * @param {string} [to] route → NavLink, active resolved by the router. A `to` carrying a `#` is a
32
32
  * SECTION of a page (2026-10-01): the router would light every section of
33
33
  * the open page at once, so it is a plain Link and `active` decides
34
- * @param {string} [href] plain anchor (hash links: on-this-page)
34
+ * @param {string} [href] plain anchor (hash links: on-this-page); an `http(s)` href opens a new tab
35
35
  * @param {Function} [onClick] no `to`/`href` → a <button>
36
36
  * @param {boolean} [active] forced active state (hash links, buttons)
37
37
  * @param {node} [icon] leading glyph
@@ -80,8 +80,11 @@ export default function RailRow({
80
80
  )
81
81
  }
82
82
  if (href) {
83
+ /* a link that leaves the site opens a new tab, as the body's do (render-tokens, 2026-10-02);
84
+ hash and same-site links stay in it (2026-10-05, kol-website) */
85
+ const external = /^https?:\/\//.test(href) ? { target: '_blank', rel: 'noreferrer' } : null
83
86
  return (
84
- <a href={href} className={cls(active)} onClick={onNavigate}>
87
+ <a href={href} className={cls(active)} onClick={onNavigate} {...external}>
85
88
  {inner}
86
89
  </a>
87
90
  )
@@ -60,7 +60,7 @@ const hueOf = (tag) => { const c = getTagColor(tag); return c === 'dark' ? undef
60
60
  * the scroll spy; both rails must agree on it)
61
61
  * @param {Array} related [{ id, href, label }] — label is the target's
62
62
  * own SHORT title, never a wikilink display text
63
- * @param {Array} actions [{ id, label, icon, to?, onClick? }]
63
+ * @param {Array} actions [{ id, label, icon, to?, href?, onClick? }] — `href` for an action that leaves the site
64
64
  * @param {Array} topTags [{ tag, count }] — the way in, not a dump
65
65
  * @param {Array} tags [string] — this page's own tags
66
66
  * @param {Function} renderTag (tag) => node — the chip renderer, injected so
@@ -179,7 +179,7 @@ export default function RightRail({
179
179
 
180
180
  {group('actions', 'Quick actions', allActions.length,
181
181
  allActions.map((a) => (
182
- <RailRow key={a.id} to={a.to} onClick={a.onClick} icon={a.icon}>
182
+ <RailRow key={a.id} to={a.to} href={a.href} onClick={a.onClick} icon={a.icon}>
183
183
  {a.label}
184
184
  </RailRow>
185
185
  ))
@@ -203,7 +203,7 @@ export default function RightRail({
203
203
  {tagViews.length > 0 && (
204
204
  <nav className="shell-nav-items">
205
205
  {tagViews.map((a) => (
206
- <RailRow key={a.id} to={a.to} onClick={a.onClick} icon={a.icon}>{a.label}</RailRow>
206
+ <RailRow key={a.id} to={a.to} href={a.href} onClick={a.onClick} icon={a.icon}>{a.label}</RailRow>
207
207
  ))}
208
208
  </nav>
209
209
  )}
@@ -2,7 +2,7 @@ import { createContext, useContext, useMemo, useRef, useState, useEffect, Suspen
2
2
  import { Outlet, Link, useLocation, useNavigate, useNavigationType } from 'react-router-dom'
3
3
  import { ShellHeader } from '@kolkrabbi/kol-framework'
4
4
  import ShellSidebar, { RAIL_FOLD_EVENT, ShellRailModeContext } from './ShellSidebar.jsx'
5
- import { FullscreenOverlay, IconFrame, ShellDrawer, ShellSearchOverlay, ShortcutsOverlay, SettingsPanel, SettingsSections, SettingsSwitch, Tooltip, useDragResize } from '@kolkrabbi/kol-component'
5
+ import { FullscreenOverlay, IconFrame, Rail, CommandOverlay, ShortcutsOverlay, SettingsPanel, SettingsSections, SettingsSwitch, Tooltip, useDragResize } from '@kolkrabbi/kol-component'
6
6
  import { createIndex, search } from '@kolkrabbi/kol-search'
7
7
  import { useTagMode } from '../tags/TagModeContext.jsx'
8
8
  import TagModeOverlay from '../tags/TagModeOverlay.jsx'
@@ -602,7 +602,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
602
602
 
603
603
  {/* `open`, not `isOpen` — the old prop name silently kept the drawer
604
604
  * shut, so there was no navigation at all below lg. */}
605
- <ShellDrawer
605
+ <Rail
606
606
  open={isNavDrawerOpen}
607
607
  onClose={() => setIsNavDrawerOpen(false)}
608
608
  >
@@ -610,13 +610,13 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
610
610
  ? renderSidebar({ activeRoute, onNavigate: () => setIsNavDrawerOpen(false) })
611
611
  : <ShellSidebar routes={routes} basePath={basePath} onNavigate={() => setIsNavDrawerOpen(false)} />
612
612
  }
613
- </ShellDrawer>
613
+ </Rail>
614
614
 
615
615
  {/* The overlay owns no filtering — the shell holds the query and
616
616
  * feeds it results (the pre-0.12 API `isOpen/routes/items` matched
617
617
  * nothing, so ⌘K rendered an empty box and searchItems was dead).
618
618
  * Selecting routes to item.href; matchSearchItems is the engine's. */}
619
- <ShellSearchOverlay
619
+ <CommandOverlay
620
620
  open={isSearchOpen}
621
621
  onClose={closeSearch}
622
622
  query={searchQuery}
@@ -658,7 +658,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
658
658
  }}
659
659
  >
660
660
  {tagMode.isProvided && tagMode.expanded ? <TagModeOverlay /> : null}
661
- </ShellSearchOverlay>
661
+ </CommandOverlay>
662
662
 
663
663
  {isShortcutsOpen && <ShortcutsOverlay shortcuts={SHORTCUTS} onClose={() => setIsShortcutsOpen(false)} />}
664
664