@kolkrabbi/kol-workshop 0.21.0 → 0.22.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.21.0",
3
+ "version": "0.22.0",
4
4
  "private": false,
5
5
  "description": "KOL workshop/docs system — a handrolled markdown engine, search, tag system (incl. a d3 tag graph), docs viewer, and docs shell. Sits above @kolkrabbi/kol-{theme,icons,component,framework}; content is consumer-injected (the package never globs docs).",
6
6
  "license": "MIT",
@@ -13,11 +13,11 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "d3": "^7.9.0",
16
- "@kolkrabbi/kol-brand": "^0.1.2",
17
- "@kolkrabbi/kol-component": "^0.34.0",
18
- "@kolkrabbi/kol-icons": "^0.14.0",
19
- "@kolkrabbi/kol-framework": "^0.18.0",
20
- "@kolkrabbi/kol-theme": "^0.34.0"
16
+ "@kolkrabbi/kol-component": "^0.39.0",
17
+ "@kolkrabbi/kol-icons": "^0.16.0",
18
+ "@kolkrabbi/kol-framework": "^0.20.0",
19
+ "@kolkrabbi/kol-theme": "^0.41.0",
20
+ "@kolkrabbi/kol-brand": "^0.1.2"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^18.3.0 || ^19.0.0",
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ExhibitCard — the specimen header inside an exhibit section: what this
3
+ * variant is called, what it does, and (optionally) its technical line and its
4
+ * prop signature. Recreated from kol-website's local `DesCard` (37L), which
5
+ * every showcase page in that repo imported by relative path.
6
+ *
7
+ * Card-level hierarchy, one rung under the section's own PageSection title:
8
+ * name `kol-card-value` · description `kol-mono-12` · details/code `kol-mono-10`.
9
+ * The type comes from the role classes, never from a utility stack at the call
10
+ * site — the 2026-07-30 law that a component's type lives in its own rule.
11
+ *
12
+ * @param {string} name variant name — ".text-auto", "Default state"
13
+ * @param {string} [description] one line on what the variant is for
14
+ * @param {string} [details] technical line — variant trigger, classes used
15
+ * @param {string} [code] the prop signature, rendered as code
16
+ */
17
+ export default function ExhibitCard({ name, description, details, code }) {
18
+ return (
19
+ <div>
20
+ <h4 className="kol-card-value">{name}</h4>
21
+
22
+ {description && <p className="kol-mono-12 text-fg-64 mt-2">{description}</p>}
23
+
24
+ {details && <p className="kol-mono-10 text-fg-48 mt-1">{details}</p>}
25
+
26
+ {code && <code className="block kol-mono-10 text-fg-48 mt-2">{code}</code>}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,69 @@
1
+ import { Link } from 'react-router-dom'
2
+ import { Icon } from '@kolkrabbi/kol-component'
3
+
4
+ /**
5
+ * ExhibitLinkCard — the card an exhibit's landing page uses for each child
6
+ * page. Recreated from kol-website's `OverviewCard`, which carried a standing
7
+ * note on itself: *"vendored verbatim from elder @kol/ui (no DS twin as of
8
+ * kol-dashboards 0.2.0); lobby to the DS or fold into kol-dashboards if a
9
+ * second consumer app appears."* This is that lobby arriving.
10
+ *
11
+ * Internal hrefs render as a router `<Link>`, external ones as a plain anchor —
12
+ * an exhibit's landing grid routinely mixes both (a sub-page beside the live
13
+ * production instance it documents).
14
+ *
15
+ * @param {string} label card title
16
+ * @param {string} [subtitle] one line under the title
17
+ * @param {string} [description] footer line, clamped to one row
18
+ * @param {string} icon glyph name — the corner mark and the empty-state fill
19
+ * @param {string} [image] cover image; replaces the oversized glyph
20
+ * @param {string} href internal path ("/workshop/…") or an absolute URL
21
+ */
22
+ export default function ExhibitLinkCard({
23
+ label,
24
+ subtitle,
25
+ description,
26
+ icon,
27
+ image,
28
+ href,
29
+ target,
30
+ rel,
31
+ className = '',
32
+ }) {
33
+ const internal = href?.startsWith('/')
34
+ const Tag = internal ? Link : 'a'
35
+ const linkProps = internal ? { to: href } : { href, target, rel }
36
+
37
+ return (
38
+ <Tag
39
+ {...linkProps}
40
+ className={`group flex aspect-[4/3] flex-col gap-3 rounded bg-surface-inverse p-4 ${className}`}
41
+ >
42
+ <div className="flex items-start justify-between">
43
+ <div>
44
+ <h3 className="kol-card-kicker">{label}</h3>
45
+ {subtitle && <p className="kol-mono-10 mt-1 italic text-fg-64">{subtitle}</p>}
46
+ </div>
47
+ <Icon name={icon} size={16} />
48
+ </div>
49
+
50
+ <div className="flex flex-1 items-center justify-center overflow-hidden rounded border border-fg-08">
51
+ {image ? (
52
+ <img
53
+ src={image}
54
+ alt={label}
55
+ className="h-full w-full scale-100 object-cover object-center transition-transform duration-300 group-hover:scale-105"
56
+ />
57
+ ) : (
58
+ <Icon
59
+ name={icon}
60
+ size={64}
61
+ className="text-auto transition-transform duration-300 group-hover:scale-105"
62
+ />
63
+ )}
64
+ </div>
65
+
66
+ {description && <p className="kol-card-tag line-clamp-1 text-fg-48">{description}</p>}
67
+ </Tag>
68
+ )
69
+ }
@@ -0,0 +1,69 @@
1
+ import { PageSection } from '@kolkrabbi/kol-framework'
2
+ import ExhibitLinkCard from './ExhibitLinkCard.jsx'
3
+ import useExhibitToc from './useExhibitToc.js'
4
+
5
+ /**
6
+ * ExhibitOverview — an exhibit's landing page: what this section is, then every
7
+ * child page as a card.
8
+ *
9
+ * The overview contract kol-website's DashboardOverview implements by hand —
10
+ * concept in the intro (with room for a single external opt-in, the live
11
+ * instance the exhibit documents), children as a grid below.
12
+ *
13
+ * ```jsx
14
+ * <ExhibitOverview
15
+ * toc={{ links: DOC_LINKS }}
16
+ * label="Scope: Dashboard — Overview"
17
+ * title="Dashboard"
18
+ * body="Modular dashboards, charts and KPI components…"
19
+ * action={<Button variant="primary" href="https://kolkrabbi.io/metrics">Open metrics</Button>}
20
+ * cards={[{ id: 'setup', label: 'Setup', subtitle: '…', icon: 'book-open', href: '/workshop/dashboard/setup' }]}
21
+ * />
22
+ * ```
23
+ *
24
+ * @param {string} title · @param {string} label · @param {string} body the intro section
25
+ * @param {node} [action] one call-to-action under the intro
26
+ * @param {Array} cards child pages: ExhibitLinkCard props, each with an `id`
27
+ * @param {string} [cardsLabel='Pages'] · @param {string} [cardsTitle='Explore'] · @param {string} [cardsBody]
28
+ * @param {object} [toc] ExhibitSidebar props; omit to leave the rail alone
29
+ * @param {string} [id='overview'] anchor id of the intro section
30
+ */
31
+ export default function ExhibitOverview({
32
+ id = 'overview',
33
+ label,
34
+ title,
35
+ body,
36
+ action,
37
+ cards = [],
38
+ cardsId = 'sections',
39
+ cardsLabel = 'Pages',
40
+ cardsTitle = 'Explore',
41
+ cardsBody,
42
+ toc,
43
+ }) {
44
+ useExhibitToc(toc ?? {})
45
+
46
+ return (
47
+ <div>
48
+ <PageSection id={id} label={label} title={title} body={body}>
49
+ {action && <div className="mt-6">{action}</div>}
50
+ </PageSection>
51
+
52
+ {cards.length > 0 && (
53
+ <PageSection id={cardsId} label={cardsLabel} title={cardsTitle} body={cardsBody}>
54
+ {/* auto-fill from a 17.5rem minimum — the reference implementation's
55
+ * grid, so an exhibit's landing reflows the same everywhere. */}
56
+ <div className="mt-8 grid gap-6 grid-cols-[repeat(auto-fill,minmax(17.5rem,1fr))]">
57
+ {cards.map((card) => (
58
+ <ExhibitLinkCard
59
+ key={card.id}
60
+ {...card}
61
+ description={card.description ?? `Open ${card.label} in the workshop`}
62
+ />
63
+ ))}
64
+ </div>
65
+ </PageSection>
66
+ )}
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,80 @@
1
+ import { PageSection } from '@kolkrabbi/kol-framework'
2
+ import ExhibitCard from './ExhibitCard.jsx'
3
+ import useExhibitToc from './useExhibitToc.js'
4
+
5
+ /**
6
+ * ExhibitPage — an exhibit's inner page, declared as CONTENT.
7
+ *
8
+ * The two page shapes kol-website hand-built per section collapse into one
9
+ * component, because they were never structurally different: a showcase page is
10
+ * a list of PageSections whose bodies are specimen grids, and a prose companion
11
+ * is a list of PageSections whose bodies are paragraphs. A section picks its
12
+ * body by which field it carries.
13
+ *
14
+ * ```jsx
15
+ * <ExhibitPage
16
+ * toc={{ links: DOC_LINKS }}
17
+ * sections={[
18
+ * { id: 'intro', label: 'Dashboard', title: 'Dashboard components',
19
+ * body: 'Cards for metrics, charts and data.' },
20
+ *
21
+ * { id: 'metric-cards', label: 'Metric cards', title: 'DashMetricCard',
22
+ * body: 'Label, value, delta.',
23
+ * specimens: [
24
+ * { name: 'KPI variant', description: 'Left border accent.',
25
+ * details: 'Variant: borderColor present', code: 'label value delta borderColor',
26
+ * demo: <DashMetricCard label="GAMES" value="2,847" borderColor="…" /> },
27
+ * ] },
28
+ *
29
+ * { id: 'tracking', label: 'Tracking', title: 'Umami', prose: true,
30
+ * children: <><p>…</p><p>…</p></> },
31
+ * ]}
32
+ * />
33
+ * ```
34
+ *
35
+ * @param {Array} sections the page, in order. Each: `{ id, label, title, body }`
36
+ * plus at most one body shape — `specimens` (a grid of ExhibitCard-headed
37
+ * demos), or `children` (anything), optionally with `prose: true` for the
38
+ * reading measure. `columns` overrides the specimen grid width (default 2).
39
+ * @param {object} [toc] ExhibitSidebar props — `{ links, sections, basePath, docHref }`.
40
+ * Omit entirely to leave the shell's rail alone.
41
+ */
42
+ export default function ExhibitPage({ sections = [], toc }) {
43
+ useExhibitToc(toc ?? {})
44
+
45
+ return (
46
+ <div>
47
+ {sections.map(({ id, label, title, body, specimens, children, prose, columns = 2 }) => (
48
+ <PageSection key={id} id={id} label={label} title={title} body={body}>
49
+ {specimens?.length > 0 && (
50
+ <div
51
+ className="mt-8 grid gap-6 items-start"
52
+ style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
53
+ >
54
+ {specimens.map((s) => (
55
+ <div key={s.name} className="space-y-4">
56
+ <ExhibitCard
57
+ name={s.name}
58
+ description={s.description}
59
+ details={s.details}
60
+ code={s.code}
61
+ />
62
+ {s.demo}
63
+ </div>
64
+ ))}
65
+ </div>
66
+ )}
67
+
68
+ {/* The reading measure is the reference implementation's, kept
69
+ * verbatim — kol-website's prose companions all wrap at 60ch on the
70
+ * body-02 rung. */}
71
+ {children && (
72
+ <div className={prose ? 'mt-8 max-w-[60ch] space-y-6 kol-sans-body-02' : 'mt-8'}>
73
+ {children}
74
+ </div>
75
+ )}
76
+ </PageSection>
77
+ ))}
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,92 @@
1
+ import { Link, useNavigate } from 'react-router-dom'
2
+ import { DocsToc, Icon } from '@kolkrabbi/kol-component'
3
+ import RailSection from '../shell/RailSection.jsx'
4
+ import RailRow from '../shell/RailRow.jsx'
5
+
6
+ /**
7
+ * ExhibitSidebar — the rail block an exhibit page registers into the shell's
8
+ * TOC slot: on-this-page, the section's documentation links, and quick actions.
9
+ * Recreated from kol-website's local `WorkshopSidebarContent`.
10
+ *
11
+ * THE DRIFT FIXED ON RECREATION. The source hand-rolled its own collapsible
12
+ * section: `.shell-sidebar-toggle` AND `.shell-sidebar-label` stacked on one
13
+ * element, inline `paddingRight`/`paddingBottom`/`justifyContent`, and its own
14
+ * chevron at L1. Every one of those is something RailSection exists to prevent
15
+ * — the eyebrow-box law in kol-components-workshop.css says an eyebrow wears
16
+ * ONE box class and sets no y-spacing inline, and the 2026-08-01 ruling says L1
17
+ * draws no chevron (it still collapses; the glyph is simply not there). The
18
+ * rungs come from RailSection now, so this block cannot drift from either rail.
19
+ * `pnpm validate:rails` R3 + R4 assert both halves.
20
+ *
21
+ * Router-agnostic in the same way as WorkshopSidebar: hrefs are built from
22
+ * `basePath`/`docHref`, never from an app singleton.
23
+ *
24
+ * @param {Array} [sections] on-this-page entries for DocsToc: [{ id, title, level }]
25
+ * @param {Array} [links] documentation links: [{ id, label }]
26
+ * @param {string} [basePath] workshop mount point — drives the quick-action hrefs
27
+ * @param {Function} [docHref] `(id) => url`; omit for `${basePath}/docs/${id}`
28
+ * @param {boolean} [actions=true] render the quick-actions rung
29
+ */
30
+ export default function ExhibitSidebar({
31
+ sections = [],
32
+ links = [],
33
+ basePath = '/workshop',
34
+ docHref = (id) => (id ? `${basePath}/docs/${id}` : `${basePath}/docs`),
35
+ actions = true,
36
+ }) {
37
+ const navigate = useNavigate()
38
+
39
+ return (
40
+ <div className="shell-rail-stack-inner">
41
+ {sections.length > 0 && (
42
+ <RailSection level={1} label="On this page" icon={Icon}>
43
+ <DocsToc toc={sections} />
44
+ </RailSection>
45
+ )}
46
+
47
+ {links.length > 0 && (
48
+ <RailSection level={1} label="Documentation" icon={Icon}>
49
+ <nav className="shell-nav-items">
50
+ {links.map(({ id, label }) => (
51
+ <RailRow key={id} to={docHref(id)}>
52
+ {label}
53
+ </RailRow>
54
+ ))}
55
+ </nav>
56
+ </RailSection>
57
+ )}
58
+
59
+ {actions && (
60
+ <RailSection level={1} label="Quick actions" icon={Icon}>
61
+ <div className="space-y-1">
62
+ <button
63
+ type="button"
64
+ className="shell-sidebar-action kol-mono-14 text-body"
65
+ onClick={() => navigate(-1)}
66
+ >
67
+ <Icon name="arrow-left" size={14} />
68
+ Back
69
+ </button>
70
+ <Link to={docHref()} className="shell-sidebar-action kol-mono-14 text-body">
71
+ <Icon name="book-open" size={14} />
72
+ All documentation
73
+ </Link>
74
+ <Link to={basePath} className="shell-sidebar-action kol-mono-14 text-body">
75
+ <Icon name="layout" size={14} />
76
+ Workshop home
77
+ </Link>
78
+ <button
79
+ type="button"
80
+ className="shell-sidebar-action kol-mono-14 text-body"
81
+ onClick={() => navigator.clipboard?.writeText(window.location.pathname)}
82
+ title="Copy page path to clipboard"
83
+ >
84
+ <Icon name="copy" size={14} />
85
+ Copy path
86
+ </button>
87
+ </div>
88
+ </RailSection>
89
+ )}
90
+ </div>
91
+ )
92
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @kolkrabbi/kol-workshop — exhibit sections.
3
+ *
4
+ * The scaffold an exhibit section is built from, so adding the NEXT one is
5
+ * content and nothing else. The shell was already shared; the sections above it
6
+ * were not, and every consumer rebuilt the same landing page, the same
7
+ * specimen-grid page, the same prose companion and the same rail block by hand.
8
+ *
9
+ * ExhibitOverview the landing — concept, one opt-in action, children as cards
10
+ * ExhibitPage an inner page — sections of specimen grids and/or prose
11
+ * ExhibitSidebar the rail block: on-this-page · doc links · quick actions
12
+ * useExhibitToc registers that block into the shell's TOC slot
13
+ * ExhibitCard the specimen header inside a section
14
+ * ExhibitLinkCard the child-page card on a landing grid
15
+ *
16
+ * Content is consumer-injected, like everything else in this package — the
17
+ * exhibit declares cards, specimens and doc links; the package renders them.
18
+ */
19
+ export { default as ExhibitOverview } from './ExhibitOverview.jsx'
20
+ export { default as ExhibitPage } from './ExhibitPage.jsx'
21
+ export { default as ExhibitSidebar } from './ExhibitSidebar.jsx'
22
+ export { default as ExhibitCard } from './ExhibitCard.jsx'
23
+ export { default as ExhibitLinkCard } from './ExhibitLinkCard.jsx'
24
+ export { default as useExhibitToc } from './useExhibitToc.js'
@@ -0,0 +1,32 @@
1
+ import { createElement, useContext, useLayoutEffect } from 'react'
2
+ import { ShellTocContext } from '../shell/ShellLayout.jsx'
3
+ import ExhibitSidebar from './ExhibitSidebar.jsx'
4
+
5
+ /**
6
+ * useExhibitToc — registers an ExhibitSidebar into the shell's TOC slot for as
7
+ * long as the page is mounted, and clears it on the way out.
8
+ *
9
+ * This is the six-line `useContext` + `useLayoutEffect` block that every exhibit
10
+ * page in kol-website copied verbatim. It takes DATA, not a node, on purpose:
11
+ * a caller passing `<Sidebar links={LINKS} />` hands over a fresh element every
12
+ * render, and an effect keyed on that identity would set shell state in a loop.
13
+ * Keying on the CONTENT of the props closes that, so a page can pass an inline
14
+ * array literal — which is what every one of them does.
15
+ *
16
+ * @param {object} props — ExhibitSidebar props: { sections, links, basePath, docHref, actions }
17
+ */
18
+ export default function useExhibitToc(props = {}) {
19
+ const setTocContent = useContext(ShellTocContext)
20
+
21
+ /* Content, not identity — see the note above. */
22
+ const key = JSON.stringify([props.sections ?? [], props.links ?? [], props.basePath, props.actions])
23
+
24
+ useLayoutEffect(() => {
25
+ /* Outside a ShellLayout the context defaults to null; a page rendered
26
+ * standalone (a test, a preview route) must not throw over its rail. */
27
+ if (!setTocContent) return undefined
28
+ setTocContent(createElement(ExhibitSidebar, props))
29
+ return () => setTocContent(null)
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [setTocContent, key])
32
+ }
package/src/index.js CHANGED
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Aggregates everything the package now owns: the shell composition
5
5
  * (ShellLayout + ShellSidebar + their contexts), the docs viewer, the tag
6
- * system, the example compositions, and the pure React-free engine. Duplicated
7
- * chrome (ShellHeader, ShellDrawer, SearchInput, ShellSearchOverlay, DocsToc)
8
- * ships from @kolkrabbi/kol-framework + @kolkrabbi/kol-component — consume it
9
- * from there, not from here.
6
+ * system, the exhibit-section scaffold, the example compositions, and the pure
7
+ * React-free engine. Duplicated chrome (ShellHeader, ShellDrawer, SearchInput,
8
+ * ShellSearchOverlay, DocsToc) ships from @kolkrabbi/kol-framework +
9
+ * @kolkrabbi/kol-component — consume it from there, not from here.
10
10
  */
11
11
  export * from './shell/index.js'
12
12
  export * from './docs/index.js'
13
13
  export * from './tags/index.js'
14
+ export * from './exhibit/index.js'
14
15
  export * from './compositions/index.js'
15
16
  export * from './engine/index.js'