@kolkrabbi/kol-styleguide 0.7.0 → 0.8.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-styleguide",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Brand guide components and the brand tool",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.296.0",
24
- "@kolkrabbi/kol-framework": "^0.53.0",
25
- "@kolkrabbi/kol-icons": "^0.45.0",
26
- "@kolkrabbi/kol-theme": "^0.184.0"
23
+ "@kolkrabbi/kol-framework": "^0.57.0",
24
+ "@kolkrabbi/kol-component": "^0.305.0",
25
+ "@kolkrabbi/kol-icons": "^0.47.0",
26
+ "@kolkrabbi/kol-theme": "^0.191.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
@@ -0,0 +1,313 @@
1
+ /* BrandGuide — kol-branding's guide renderer (src/template/book/BrandBook.jsx + Brand.jsx, 2026-10-24), carried
2
+ * class-for-class for the brand guide outline (BrandGuideOutlineTwoVersions, kol-hq 2026-10-27). Two changes, both
3
+ * because a package cannot carry them: the research line under each chapter (`Source`, kol-branding's own citations)
4
+ * stays there, and the Contents card is an in-page anchor only (no router in this package). */
5
+ import { useState } from 'react'
6
+ import { ColorRamp, ContentCollection, DocsToc, FullscreenOverlay, Table } from '@kolkrabbi/kol-component'
7
+ import { Icon } from '@kolkrabbi/kol-icons'
8
+ import PageHero from '@kolkrabbi/kol-framework/src/PageHero.jsx'
9
+ import PageSection from '@kolkrabbi/kol-framework/src/PageSection.jsx'
10
+ import LogoCard from './LogoCard.jsx'
11
+ import AssetTable from './AssetTable.jsx'
12
+ import AssetCard from './AssetCard.jsx'
13
+ import BrandMark from './BrandMark.jsx'
14
+ import { BrandAvatar } from './SocialMocks.jsx'
15
+ import { BusinessCardBack, BusinessCardFront, EmailSignature, Envelope, Letterhead } from './StationeryMocks.jsx'
16
+ import { brandInfo, STATIONERY_MARKS, BRANDED_ASSET_ROWS, scrollToAnchor, svgDims, downloadRecolored } from './brandBook.js'
17
+ import { guideSections, guideToc, stopHex, contrast } from './guideBook.js'
18
+
19
+ /* taxonomy-ok: organism — a brand guide: PageHero + one PageSection per chapter, each chapter the same anatomy, the chapters on a rail */
20
+
21
+ /**
22
+ * BrandGuide — A brand guide. one scrolling page: the hero, then one chapter per row of the list it is handed, with the
23
+ * chapters on a rail. Every chapter has one anatomy — eyebrow · title · lede · prose · rules · the specimen its `kind`
24
+ * draws · spec table · do / don't · word list · closing prose — and only the specimen differs by kind. The outline is
25
+ * `@kolkrabbi/kol-brand-template`'s `GUIDE_SECTIONS`; its blank copy is `GUIDE_TEMPLATE`.
26
+ *
27
+ * @param {Object} brand a brand manifest (`@kolkrabbi/kol-brand-template` schema; `book` carries the copy)
28
+ * @param {Array} sections the chapter list (see `GUIDE_SECTIONS` for a row)
29
+ * @param {Function} Logo optional mark component, `<Logo variant="wordmark" />`
30
+ * @param {Object} logoSources `{ [logoId]: rawSvg }` — drawn when no `Logo` is given
31
+ * @param {Object} structure `{ [logoId]: { logo, grid, keyline } }` raw svgs — Lockups' clear-space toggle
32
+ * @param {string} className classes on the root
33
+ */
34
+ export default function BrandGuide({ brand, sections, Logo, logoSources, structure, className = '' }) {
35
+ const list = guideSections(brand, sections)
36
+ const mark = (id, props) => <BrandMark id={id} Logo={Logo} sources={logoSources} {...props} />
37
+ const index = list.filter((s) => s.nav)
38
+ return (
39
+ /* `xl:pr-32` — the rail is `position: fixed` on the right edge from xl up, and the pages' grids
40
+ run to the frame's edge; the padding keeps the last column out from under it */
41
+ <div className={`xl:pr-32 ${className}`.trim()}>
42
+ {list.map((s) => (s.kind === 'hero'
43
+ ? <PageHero key={s.key} id={s.id} label={s.label} title={s.title} lede={s.lede} />
44
+ : <Chapter key={s.key} s={s} brand={brand} mark={mark} index={index} logoSources={logoSources} structure={structure} />))}
45
+ <DocsToc variant="rail" toc={guideToc(brand, sections)} onNavigate={scrollToAnchor} className="hidden xl:flex" />
46
+ </div>
47
+ )
48
+ }
49
+
50
+ function Chapter({ s, brand, mark, index, logoSources, structure }) {
51
+ const Specimen = SPECIMENS[s.kind ?? 'prose']
52
+ return (
53
+ <PageSection id={s.id} label={s.label} title={s.title} body={s.lede}>
54
+ <Prose blocks={s.blocks} />
55
+ {s.rules?.length > 0 && <Rules items={s.rules} />}
56
+ {Specimen && <Specimen s={s} brand={brand} mark={mark} index={index} logoSources={logoSources} structure={structure} />}
57
+ {s.spec && <Spec spec={s.spec} />}
58
+ {(s.dos?.length > 0 || s.donts?.length > 0) && <Pair left={{ title: 'Do', items: s.dos }} right={{ title: "Don't", items: s.donts }} />}
59
+ {s.words && <Pair left={{ title: 'Words we use', items: s.words.use }} right={{ title: 'Words we avoid', items: s.words.avoid }} />}
60
+ <Prose blocks={s.after} />
61
+ </PageSection>
62
+ )
63
+ }
64
+
65
+ /** Book copy → `.kol-prose`: a string is a paragraph, `{ h }` a sub-heading, `{ lead, text }` a
66
+ * paragraph opening on a bold lead. `head` renders inside the same block, after the copy. */
67
+ function Prose({ blocks, head, className = 'mt-12' }) {
68
+ if (!blocks?.length && !head) return null
69
+ return (
70
+ <div className={`kol-prose ${className}`}>
71
+ {(blocks ?? []).map((b, i) => {
72
+ if (typeof b === 'string') return <p key={i}>{b}</p>
73
+ if (b.h) return <h3 key={i}>{b.h}</h3>
74
+ return <p key={i}><strong>{b.lead}</strong> {b.text}</p>
75
+ })}
76
+ {head}
77
+ </div>
78
+ )
79
+ }
80
+
81
+ /* The rules, as a list in the prose voice. */
82
+ function Rules({ items }) {
83
+ return <div className="kol-prose mt-8"><ul>{items.map((r, i) => <li key={i}>{r}</li>)}</ul></div>
84
+ }
85
+
86
+ /* Two lists side by side — do / don't, use / avoid. */
87
+ /* Two lists side by side (do / don't, words). A side with no items is left out; the titles are labels, not headings. */
88
+ function Pair({ left, right }) {
89
+ const cols = [left, right].filter((c) => c.items?.length)
90
+ if (!cols.length) return null
91
+ return (
92
+ <div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2 max-w-panel">
93
+ {cols.map(({ title, items }) => (
94
+ <div key={title} className="flex flex-col gap-3">
95
+ <h3 className="kol-prose-label m-0">{title}</h3>
96
+ <div className="kol-prose"><ul>{items.map((t, i) => <li key={i}>{t}</li>)}</ul></div>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ )
101
+ }
102
+
103
+ /* A measured spec — columns and rows from the manifest, on the DS table. */
104
+ function Spec({ spec }) {
105
+ const columns = spec.columns.map((h, i) => ({ accessor: `c${i}`, header: h, wrap: true }))
106
+ const rows = spec.rows.map((r, n) => Object.fromEntries([['id', n], ...r.map((v, i) => [`c${i}`, v])]))
107
+ return <Table variant="simple" caption={spec.caption ?? 'Specification'} columns={columns} rows={rows} className="mt-8" />
108
+ }
109
+
110
+ /* The contents: every indexed chapter as a row, its eyebrow carrying the number; grouped by `part` when the list has parts. */
111
+ function Contents({ index }) {
112
+ if (!index.length) return null
113
+ const parts = [...new Set(index.map((s) => s.part ?? ''))]
114
+ const row = (s) => (
115
+ <li key={s.id}>
116
+ <IndexCard href={`#${s.id}`} onClick={scrollToAnchor} label={s.label} title={s.title ?? s.nav} />
117
+ </li>
118
+ )
119
+ return (
120
+ <div className="mt-12 flex flex-col gap-10 max-w-panel">
121
+ {parts.map((p) => (
122
+ <div key={p || 'all'} className="flex flex-col gap-4">
123
+ {p && <h3 className="kol-prose-label m-0">{p}</h3>}
124
+ <ul className="m-0 grid list-none grid-cols-1 gap-2 p-0 md:grid-cols-2">{index.filter((s) => (s.part ?? '') === p).map(row)}</ul>
125
+ </div>
126
+ ))}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ /* The book's index card — the Contents row: a label, the title, one small line and the arrow (kol-branding's IndexCard,
132
+ * the in-page anchor half). */
133
+ function IndexCard({ href, onClick, label, title, meta }) {
134
+ return (
135
+ <a href={href} onClick={onClick} className="kol-card kol-card-interactive flex items-center justify-between gap-4 px-4 py-3 no-underline group">
136
+ <span className="flex min-w-0 flex-col gap-1">
137
+ {label && <span className="kol-mono-12 text-meta uppercase">{label}</span>}
138
+ <span className="kol-mono-14 text-emphasis">{title}</span>
139
+ {meta && <span className="kol-mono-10 text-meta mt-1">{meta}</span>}
140
+ </span>
141
+ <Icon name="arrow-right" size={16} className="shrink-0 text-oq-48 group-hover:text-emphasis transition-colors" />
142
+ </a>
143
+ )
144
+ }
145
+
146
+ /* ── the specimens, by kind ───────────────────────────────────────────────────────────────────── */
147
+
148
+ function Marks({ s, mark }) {
149
+ return <div className="kol-grid mt-12">{(s.marks ?? []).map((id) => <LogoCard key={id} logo={mark(id)} frame={false} />)}</div>
150
+ }
151
+
152
+ /* A raw svg string as a layer node — ClearspaceDiagram stretches the svg inside to its box. */
153
+ const Raw = ({ svg }) => <span dangerouslySetInnerHTML={{ __html: svg }} />
154
+
155
+ /* With a mark's construction (`structure[id]`) the card draws the padded diagram and offers the clear-space toggle. */
156
+ function Lockups({ s, brand, mark, structure }) {
157
+ return (
158
+ <div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 max-w-panel">
159
+ {(s.marks ?? []).map((id) => {
160
+ const st = structure?.[id]
161
+ return (
162
+ <LogoCard key={id} logo={st ? <Raw svg={st.logo} /> : mark(id)} grid={st && <Raw svg={st.grid} />} keyline={st && <Raw svg={st.keyline} />}
163
+ caption={brand?.logos?.find((l) => l.id === id)?.name ?? id} className="[&_figcaption]:uppercase" />
164
+ )
165
+ })}
166
+ </div>
167
+ )
168
+ }
169
+
170
+ /* Colour — one compact ramp row per family (its name, its role, the anchor dot), then the approved pairs with their contrast. */
171
+ function Color({ s, brand }) {
172
+ const ramps = brand?.ramps ?? []
173
+ return (
174
+ <>
175
+ {/* chips at one fixed height instead of ColorRamp's squares, so a family is a strip, not a screen */}
176
+ <div className="mt-12 max-w-panel [&_.aspect-square]:aspect-auto [&_.aspect-square]:h-14">
177
+ {ramps.map((r) => (
178
+ <ColorRamp key={r.id} label={r.label} note={r.note} anchor={`${r.id}-${r.anchor}`}
179
+ colors={(r.stops ?? []).map((st) => [`${r.id}-${st.stop}`, st.value])} />
180
+ ))}
181
+ </div>
182
+ {s.pairs?.length > 0 && <Pairs pairs={s.pairs} brand={brand} />}
183
+ </>
184
+ )
185
+ }
186
+
187
+ /* Approved text / background pairs, contrast computed from the ramps — the accessibility rule, kept where the values are. */
188
+ function Pairs({ pairs, brand }) {
189
+ const grade = (r) => (r >= 7 ? 'AAA' : r >= 4.5 ? 'AA' : r >= 3 ? 'AA, large text only' : 'fails')
190
+ const rows = pairs.map((p, i) => {
191
+ const [t, b] = [stopHex(brand, p.text), stopHex(brand, p.bg)]
192
+ const r = contrast(t, b)
193
+ return { id: i, text: `${p.text}${t ? ` · ${t}` : ''}`, bg: `${p.bg}${b ? ` · ${b}` : ''}`, ratio: r ? `${r.toFixed(2)} : 1` : '—', passes: r ? grade(r) : '—', use: p.use ?? '' }
194
+ })
195
+ const columns = [{ accessor: 'text', header: 'Text' }, { accessor: 'bg', header: 'Background' }, { accessor: 'ratio', header: 'Contrast' }, { accessor: 'passes', header: 'WCAG 2' }, { accessor: 'use', header: 'Use', wrap: true }]
196
+ return <Table variant="simple" caption="Approved color pairs" columns={columns} rows={rows} className="mt-8" />
197
+ }
198
+
199
+ /* Type — the brand's families, each set in its own cut. The brand's scale is copy (`spec` or `after`), not KOL's system. */
200
+ function Typography({ brand }) {
201
+ const families = brand?.type?.families ?? []
202
+ if (!families.length) return null
203
+ return (
204
+ <div className="mt-12 flex flex-col gap-6">
205
+ {families.map((f) => (
206
+ <div key={f.token} className="flex flex-col gap-2 border-b border-fg-08 py-3">
207
+ <span className="kol-mono-12 text-meta uppercase tracking-wider">{f.token}</span>
208
+ <span className="text-emphasis" style={{ fontFamily: `var(${f.token})`, fontSize: 28 }}>{f.cut}</span>
209
+ <span className="kol-mono-12 text-subtle">{f.role}</span>
210
+ </div>
211
+ ))}
212
+ {brand?.type?.note && <p className="kol-mono-14 text-meta m-0">{brand.type.note}</p>}
213
+ </div>
214
+ )
215
+ }
216
+
217
+ /* The pair the Logos table's Color column swings between. */
218
+ const INK_TOKENS = { ink: '--kol-surface-on-primary', surface: '--kol-surface-primary' }
219
+
220
+ /* The logo asset table — ink toggle, zoom, recoloured download. */
221
+ function LogosTable({ brand, mark, logoSources }) {
222
+ const [zoom, setZoom] = useState(null)
223
+ const rows = (brand?.logos ?? []).map((logo) => {
224
+ const raw = logoSources?.[logo.id]
225
+ return {
226
+ id: logo.id, name: logo.id.replace(/-/g, ' / '), preview: mark(logo.id), previewWidth: logo.previewWidth ?? 48,
227
+ path: logo.file?.replace(/^\.\//, ''), format: 'SVG', dimensions: svgDims(raw), raw,
228
+ onDownload: raw ? (row, ink) => downloadRecolored(row.raw, ink, `${logo.id}.svg`) : undefined,
229
+ }
230
+ })
231
+ return (
232
+ <>
233
+ <AssetTable caption="Logos" rows={rows} inkTokens={INK_TOKENS} onPreviewZoom={(row, ink) => setZoom({ row, ink })} className="mt-8" />
234
+ <FullscreenOverlay open={!!zoom} onClose={() => setZoom(null)}>
235
+ {zoom && <span className="inline-flex items-center p-12" style={{ color: zoom.ink, width: 'min(60vw, 480px)' }}>{zoom.row.preview}</span>}
236
+ </FullscreenOverlay>
237
+ </>
238
+ )
239
+ }
240
+
241
+ const TokenName = ({ children }) => <code className="kol-helper-12 text-emphasis">{children}</code>
242
+ const brandedAssetCols = [
243
+ { accessor: 'item', header: 'Item', render: (r) => <TokenName>{r.item}</TokenName> },
244
+ { accessor: 'aspect', header: 'Aspect' },
245
+ { accessor: 'surface', header: 'Surface' },
246
+ { accessor: 'status', header: 'Status', render: (r) => <span className="kol-helper-12 text-meta uppercase tracking-widest">{r.status}</span> },
247
+ { accessor: 'note', header: 'Note' },
248
+ ]
249
+ function Branded() {
250
+ return <Table variant="simple" caption="Branded assets" columns={brandedAssetCols} rows={BRANDED_ASSET_ROWS} className="mt-8" />
251
+ }
252
+
253
+ /* Stationery — the five mocks with the manifest's marks and contact details. */
254
+ function Stationery({ brand, mark }) {
255
+ const marks = { ...STATIONERY_MARKS, ...(brand?.stationery?.marks ?? {}) }
256
+ const info = brandInfo(brand)
257
+ return (
258
+ <div className="kol-grid mt-8">
259
+ <div className="col-span-2"><AssetCard caption="Business card, front"><div className="aspect-[85/55]"><BusinessCardFront mark={mark(marks.card, { className: 'max-h-[70%]' })} /></div></AssetCard></div>
260
+ <div className="col-span-2"><AssetCard caption="Envelope"><div className="aspect-[220/110]"><Envelope mark={mark(marks.envelope)} info={info} /></div></AssetCard></div>
261
+ <div className="col-span-2 flex flex-col gap-3">
262
+ <AssetCard caption="Business card, back"><div className="aspect-[85/55]"><BusinessCardBack info={info} /></div></AssetCard>
263
+ <AssetCard caption="Email signature"><EmailSignature mark={mark(marks.signature)} info={info} /></AssetCard>
264
+ </div>
265
+ <div className="col-span-2"><AssetCard caption="Letterhead"><div className="aspect-[210/297]"><Letterhead mark={mark(marks.letter, { style: { height: '100%', width: 'auto' } })} info={info} /></div></AssetCard></div>
266
+ </div>
267
+ )
268
+ }
269
+
270
+ /* Social — one frame per template at its ratio; the image when the manifest has one, the mark on a plain ground when not. */
271
+ function Social({ brand, mark }) {
272
+ const templates = brand?.social?.templates ?? []
273
+ if (!templates.length) return null
274
+ return (
275
+ <ContentCollection cols={3} className="mt-8 items-start">
276
+ {templates.map(({ caption, ratio, src }) => (
277
+ <AssetCard key={caption ?? src} caption={caption}>
278
+ <div className={`relative overflow-hidden ${src ? '' : 'bg-fg-08'}`} style={{ aspectRatio: ratio }}>
279
+ {src && <img src={src} alt="" loading="lazy" className="absolute inset-0 h-full w-full object-cover" />}
280
+ {src && <div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/60 to-transparent" aria-hidden="true" />}
281
+ <div className={`absolute bottom-[6%] left-[6%] w-[42%] ${src ? 'text-fg-inverse' : 'text-emphasis'}`}>
282
+ {mark(brand.social.mark ?? 'wordmark', { title: brand?.meta?.name })}
283
+ </div>
284
+ </div>
285
+ </AssetCard>
286
+ ))}
287
+ </ContentCollection>
288
+ )
289
+ }
290
+
291
+ function Profile({ brand, mark }) {
292
+ const avatars = brand?.social?.avatars ?? []
293
+ const marks = { ...STATIONERY_MARKS, ...(brand?.stationery?.marks ?? {}) }
294
+ if (!avatars.length) return null
295
+ return (
296
+ <div className="kol-grid mt-8 items-start">
297
+ {avatars.map(({ bg, polarity }, i) => <AssetCard key={i}><BrandAvatar mark={mark(marks.avatar, { className: 'w-1/2' })} bg={bg} polarity={polarity} /></AssetCard>)}
298
+ </div>
299
+ )
300
+ }
301
+
302
+ const SPECIMENS = {
303
+ contents: Contents,
304
+ logo: Marks,
305
+ lockups: Lockups,
306
+ color: Color,
307
+ typography: Typography,
308
+ 'logos-table': LogosTable,
309
+ branded: Branded,
310
+ stationery: Stationery,
311
+ social: Social,
312
+ profile: Profile,
313
+ }
@@ -0,0 +1,56 @@
1
+ /* The brand guide's chapter model — kol-branding's src/template/book/brandBook.js (2026-10-24), carried for `BrandGuide`
2
+ * (BrandGuideOutlineTwoVersions, 2026-10-27). The book owns no chapter list: it renders the one it is handed —
3
+ * `@kolkrabbi/kol-brand-template`'s `GUIDE_SECTIONS`, or a client's own. The helpers the identity book already
4
+ * exports (brandInfo, the stationery marks, the anchor scroll, the svg helpers) stay in ./brandBook.js. */
5
+
6
+ const hasCopy = (s) => Boolean(s.lede || s.blocks?.length || s.after?.length || s.rules?.length || s.spec || s.dos?.length || s.donts?.length || s.words || s.pairs?.length)
7
+
8
+ /* Whether a chapter has anything to show for this manifest. The specimen kinds need the data they draw;
9
+ * everything else shows when it has copy. */
10
+ export function shows(s, brand) {
11
+ const kind = s.kind ?? 'prose'
12
+ if (kind === 'hero' || kind === 'contents' || kind === 'branded' || kind === 'stationery') return true
13
+ if (kind === 'logos-table') return Boolean(brand?.logos?.length)
14
+ if (kind === 'logo' || kind === 'lockups') return Boolean(brand?.logos?.length) || hasCopy(s)
15
+ if (kind === 'color') return Boolean(brand?.ramps?.length) || hasCopy(s)
16
+ if (kind === 'typography') return Boolean(brand?.type?.families?.length) || hasCopy(s)
17
+ if (kind === 'social') return Boolean(brand?.social?.templates?.length) || hasCopy(s)
18
+ if (kind === 'profile') return Boolean(brand?.social?.avatars?.length) || hasCopy(s)
19
+ return hasCopy(s)
20
+ }
21
+
22
+ /** The guide's chapters for a manifest — the list's rows merged with `brand.book`, the empty ones dropped. */
23
+ export function guideSections(brand, sections = []) {
24
+ const name = brand?.meta?.name
25
+ return sections
26
+ .map((d) => {
27
+ const s = { ...d, ...(brand?.book?.[d.key] ?? {}) }
28
+ if (d.kind === 'hero' && !s.title) s.title = name
29
+ if (d.key === 'about' && !s.title && name) s.title = `About ${name}`
30
+ return s
31
+ })
32
+ .filter((s) => shows(s, brand))
33
+ }
34
+
35
+ /** The rail's entries — `[{ id, label }]`. */
36
+ export const guideToc = (brand, sections) =>
37
+ guideSections(brand, sections).filter((s) => s.nav).map((s) => ({ id: s.id, label: s.nav }))
38
+
39
+ /* The ramp stop a pair names (`teal-300`) → its hex, from the manifest's ramps. */
40
+ export function stopHex(brand, ref) {
41
+ const m = /^(.*)-(\d+)$/.exec(ref ?? '')
42
+ if (!m) return undefined
43
+ return brand?.ramps?.find((r) => r.id === m[1])?.stops?.find((s) => String(s.stop) === m[2])?.value
44
+ }
45
+
46
+ /* WCAG 2 contrast ratio of two hexes — the number a color chapter's approved pairs carry. */
47
+ export function contrast(hexA, hexB) {
48
+ const lum = (hex) => {
49
+ const n = parseInt(hex.replace('#', ''), 16)
50
+ const c = [16, 8, 0].map((sh) => ((n >> sh) & 255) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
51
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
52
+ }
53
+ if (!hexA || !hexB) return undefined
54
+ const [a, b] = [lum(hexA), lum(hexB)]
55
+ return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)
56
+ }
package/src/index.js CHANGED
@@ -50,6 +50,11 @@ export { default as BrandBook } from './BrandBook.jsx'
50
50
  export { default as BrandAssets } from './BrandAssets.jsx'
51
51
  export { brandSections, brandToc, brandInfo, BRAND_VIEWS, BOOK_SECTIONS, ASSET_SECTIONS } from './brandBook.js'
52
52
 
53
+ // The brand guide (2026-10-27): kol-branding's guide renderer over a chapter list it is handed —
54
+ // kol-brand-template's GUIDE_SECTIONS, blank (GUIDE_TEMPLATE) or filled by a client's manifest.
55
+ export { default as BrandGuide } from './BrandGuide.jsx'
56
+ export { guideSections, guideToc } from './guideBook.js'
57
+
53
58
  // The brand lab (2026-10-20): the generators a brand is MADE with — carried from kol-atlas's Assets pages.
54
59
  // Each is controlled by one value, and the math is exported so a brand can be emitted without the UI.
55
60
  export { default as ColorLab } from './ColorLab.jsx'