@kolkrabbi/kol-styleguide 0.9.0 → 0.11.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.9.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "description": "Brand guide components and the brand tool",
6
6
  "license": "MIT",
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@kolkrabbi/kol-framework": "^0.57.0",
24
+ "@kolkrabbi/kol-component": "^0.307.0",
24
25
  "@kolkrabbi/kol-icons": "^0.47.0",
25
- "@kolkrabbi/kol-component": "^0.305.0",
26
- "@kolkrabbi/kol-theme": "^0.191.0"
26
+ "@kolkrabbi/kol-theme": "^0.192.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
@@ -1,67 +1,18 @@
1
- import { useState } from 'react'
2
- import { ContentCollection, FullscreenOverlay, Graphic, GRAPHIC_RAW, Table } from '@kolkrabbi/kol-component'
3
- import PageSection from '@kolkrabbi/kol-framework/src/PageSection.jsx'
4
- import AssetTable from './AssetTable.jsx'
5
- import AssetCard from './AssetCard.jsx'
6
- import { BrandAvatar } from './SocialMocks.jsx'
7
- import { BusinessCardBack, BusinessCardFront, EmailSignature, Envelope, Letterhead } from './StationeryMocks.jsx'
8
- import BrandMark from './BrandMark.jsx'
9
- import { Prose, SectionIndex } from './BrandBook.jsx'
10
- import {
11
- brandSections, brandInfo, STATIONERY_MARKS, BRANDED_ASSET_ROWS, svgDims, downloadRecolored,
12
- } from './brandBook.js'
1
+ import { GuideChapters } from './BrandGuide.jsx'
2
+ import { ASSET_SECTIONS } from './brandBook.js'
13
3
 
14
- /* taxonomy-ok: organism — the brand book's assets page: PageSection chapters over AssetTable, Table, AssetCard and the stationery / social mocks */
15
-
16
- /* The pair the Logos table's Color column swings between. */
17
- const INK_TOKENS = { ink: '--kol-surface-on-primary', surface: '--kol-surface-primary' }
18
-
19
- const TokenName = ({ children }) => (
20
- <code className="kol-helper-12 text-emphasis">{children}</code>
21
- )
22
-
23
- /* a graphic from kol-component's set as an asset row — kol-website's graphicRows, its 80px preview */
24
- const graphicRows = (category, names = []) => names.map((name) => {
25
- const raw = GRAPHIC_RAW[category]?.[name]
26
- return {
27
- id: `${category}/${name}`, name, preview: <Graphic category={category} name={name} />, previewWidth: 80,
28
- path: `packages/component/src/graphics/svg/${category}/${name}.svg`, format: 'SVG', dimensions: svgDims(raw), raw,
29
- onDownload: raw ? (row, ink) => downloadRecolored(row.raw, ink, `${name}.svg`) : undefined,
30
- }
31
- })
32
-
33
- /* square preview tile with its category / name — kol-website's GraphicCard */
34
- function GraphicCard({ category, name, polarity = 'dark' }) {
35
- const bg = polarity === 'dark' ? 'bg-surface-inverse' : 'bg-surface-primary'
36
- return (
37
- <figure>
38
- <div className={`aspect-square flex items-center justify-center rounded-[4px] overflow-hidden ${bg}`}>
39
- <Graphic category={category} name={name} />
40
- </div>
41
- <figcaption className="kol-helper-12 uppercase tracking-wider text-meta mt-2">
42
- {category} / {name}
43
- </figcaption>
44
- </figure>
45
- )
46
- }
47
-
48
- const brandedAssetCols = [
49
- { accessor: 'item', header: 'Item', render: (r) => <TokenName>{r.item}</TokenName> },
50
- { accessor: 'aspect', header: 'Aspect' },
51
- { accessor: 'surface', header: 'Surface' },
52
- { accessor: 'status', header: 'Status', render: (r) => <span className="kol-helper-12 text-meta uppercase tracking-widest">{r.status}</span> },
53
- { accessor: 'note', header: 'Note' },
54
- ]
4
+ /* taxonomy-ok: organism — the brand book's assets page: the guide's chapters over ASSET_SECTIONS */
55
5
 
56
6
  /**
57
7
  * BrandAssets — The brand book's assets page. the ASSETS page of the brand tool: what you download or reproduce. Logos (the
58
- * mark table, with the ink toggle, zoom and recoloured download), Graphics and Patterns (the manifest's `graphics`, names
59
- * from kol-component's set by category, on the same table), the branded-asset register,
60
- * stationery, social post sizes and profile avatars.
8
+ * mark table, with the ink toggle, zoom and recoloured download), Graphics and Patterns (the manifest's `graphics`),
9
+ * the branded-asset register, stationery (and `stationery.letterheadB`), social post sizes (photos or the drawn mocks)
10
+ * and profile avatars.
61
11
  *
62
- * kol-olina's apps/brand `pages/assets/*`, carried class-for-class; the client's marks, contact
63
- * details, templates and avatar grounds come from the manifest (`logos`, `meta`, `stationery.marks`,
64
- * `social`). Sections with nothing to show are left out — see `brandSections`.
12
+ * `BrandGuide`'s chapters over `ASSET_SECTIONS` (0.11.0 — one renderer; the page's own, carried from kol-olina's
13
+ * apps/brand `pages/assets/*`, is in `_tmp/2026-10-28-brand-book-one-renderer/`). The client's marks, contact details,
14
+ * templates and avatar grounds come from the manifest (`logos`, `meta`, `stationery`, `social`, `graphics`); sections
15
+ * with nothing to show are left out.
65
16
  *
66
17
  * @param {Object} brand a brand manifest (`@kolkrabbi/kol-brand-template` schema)
67
18
  * @param {Function} Logo optional mark component, `<Logo variant="wordmark" />`
@@ -69,123 +20,5 @@ const brandedAssetCols = [
69
20
  * the marks themselves when no `Logo` is given
70
21
  */
71
22
  export default function BrandAssets({ brand, Logo, logoSources }) {
72
- const sections = brandSections(brand, 'assets')
73
- const mark = (id, props) => <BrandMark id={id} Logo={Logo} sources={logoSources} {...props} />
74
- const index = sections.filter((s) => s.nav)
75
- const marks = { ...STATIONERY_MARKS, ...(brand?.stationery?.marks ?? {}) }
76
- const info = brandInfo(brand)
77
- const [zoom, setZoom] = useState(null)
78
-
79
- const logoRows = (brand?.logos ?? []).map((logo) => {
80
- const raw = logoSources?.[logo.id]
81
- return {
82
- id: logo.id,
83
- name: logo.id.replace(/-/g, ' / '),
84
- preview: mark(logo.id),
85
- previewWidth: logo.previewWidth ?? 48,
86
- path: logo.file?.replace(/^\.\//, ''),
87
- format: 'SVG',
88
- dimensions: svgDims(raw),
89
- raw,
90
- onDownload: raw ? (row, ink) => downloadRecolored(row.raw, ink, `${logo.id}.svg`) : undefined,
91
- }
92
- })
93
-
94
- return (
95
- <>
96
- {sections.map((s) => {
97
- const head = { id: s.id, label: s.label, title: s.title, body: s.lede }
98
- switch (s.key) {
99
- case 'assetsOverview': return (
100
- <PageSection key={s.key} {...head}>
101
- <SectionIndex sections={index} />
102
- </PageSection>
103
- )
104
- case 'logos': return (
105
- <PageSection key={s.key} {...head}>
106
- <AssetTable
107
- caption="Logos"
108
- rows={logoRows}
109
- inkTokens={INK_TOKENS}
110
- onPreviewZoom={(row, ink) => setZoom({ row, ink })}
111
- />
112
- <FullscreenOverlay open={!!zoom} onClose={() => setZoom(null)}>
113
- {zoom && (
114
- <span
115
- className="inline-flex items-center p-12"
116
- style={{ color: zoom.ink, width: 'min(60vw, 480px)' }}
117
- >
118
- {zoom.row.preview}
119
- </span>
120
- )}
121
- </FullscreenOverlay>
122
- </PageSection>
123
- )
124
- case 'graphics':
125
- case 'patterns': return (
126
- <PageSection key={s.key} {...head}>
127
- <AssetTable caption={s.title} rows={graphicRows(s.category, brand.graphics[s.category])} inkTokens={INK_TOKENS} />
128
- </PageSection>
129
- )
130
- case 'graphicsPatterns': return (
131
- <PageSection key={s.key} {...head}>
132
- <div className="kol-grid mt-8">
133
- {brand.graphics[s.category].map((name) => <GraphicCard key={name} category={s.category} name={name} />)}
134
- </div>
135
- </PageSection>
136
- )
137
- case 'branded': return (
138
- <PageSection key={s.key} {...head}>
139
- <Table caption="Branded assets" columns={brandedAssetCols} rows={BRANDED_ASSET_ROWS} className="mt-8" />
140
- </PageSection>
141
- )
142
- case 'stationery': return (
143
- <PageSection key={s.key} {...head}>
144
- <div className="kol-grid mt-8">
145
- <div className="col-span-2"><AssetCard><BusinessCardFront mark={mark(marks.card, { className: 'max-h-[70%]' })} /></AssetCard></div>
146
- <div className="col-span-2"><AssetCard><Envelope mark={mark(marks.envelope)} info={info} /></AssetCard></div>
147
- <div className="col-span-2 flex flex-col gap-3">
148
- <AssetCard><BusinessCardBack info={info} /></AssetCard>
149
- <AssetCard><EmailSignature mark={mark(marks.signature)} info={info} /></AssetCard>
150
- </div>
151
- <div className="col-span-2"><AssetCard><Letterhead mark={mark(marks.letter, { style: { height: '100%', width: 'auto' } })} info={info} /></AssetCard></div>
152
- </div>
153
- </PageSection>
154
- )
155
- case 'social': return (
156
- <PageSection key={s.key} {...head}>
157
- <ContentCollection cols={3} className="mt-8 items-start">
158
- {brand.social.templates.map(({ caption, ratio, src }) => (
159
- <AssetCard key={caption ?? src} caption={caption}>
160
- <div className="relative overflow-hidden" style={{ aspectRatio: ratio }}>
161
- <img src={src} alt="" loading="lazy" className="absolute inset-0 w-full h-full object-cover" />
162
- <div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/60 to-transparent" aria-hidden="true" />
163
- <div className="absolute bottom-[6%] left-[6%] w-[42%] text-fg-inverse">
164
- {mark(brand.social.mark ?? 'wordmark', { title: brand?.meta?.name })}
165
- </div>
166
- </div>
167
- </AssetCard>
168
- ))}
169
- </ContentCollection>
170
- </PageSection>
171
- )
172
- case 'profile': return (
173
- <PageSection key={s.key} {...head}>
174
- <div className="kol-grid mt-8 items-start">
175
- {brand.social.avatars.map(({ bg, polarity }, i) => (
176
- <AssetCard key={i}><BrandAvatar mark={mark(marks.avatar, { className: 'w-1/2' })} bg={bg} polarity={polarity} /></AssetCard>
177
- ))}
178
- </div>
179
- </PageSection>
180
- )
181
- default: return (
182
- <PageSection key={s.key} {...head}>
183
- <Prose blocks={s.blocks} />
184
- <Prose blocks={s.after} />
185
- </PageSection>
186
- )
187
- }
188
- })}
189
- </>
190
- )
23
+ return <GuideChapters brand={brand} sections={ASSET_SECTIONS} Logo={Logo} logoSources={logoSources} />
191
24
  }
package/src/BrandBook.jsx CHANGED
@@ -1,255 +1,20 @@
1
- import { ContentCollection } from '@kolkrabbi/kol-component'
2
- import { Icon } from '@kolkrabbi/kol-icons'
3
- import PageHero from '@kolkrabbi/kol-framework/src/PageHero.jsx'
4
- import PageSection from '@kolkrabbi/kol-framework/src/PageSection.jsx'
5
- import LogoCard from './LogoCard.jsx'
6
- import Swatch from './Swatch.jsx'
7
- import BrandMark from './BrandMark.jsx'
8
- import { brandSections, scrollToAnchor } from './brandBook.js'
9
- import { typeSections } from './typeSpecimen.js'
1
+ import { GuideChapters } from './BrandGuide.jsx'
2
+ import { BOOK_SECTIONS } from './brandBook.js'
10
3
 
11
- /* taxonomy-ok: organism — the brand book's identity page: PageHero + PageSection chapters over LogoCard, Swatch and the type specimens */
4
+ /* taxonomy-ok: organism — the brand book's identity page: the guide's chapters over BOOK_SECTIONS */
12
5
 
13
6
  /**
14
7
  * BrandBook — The brand book's identity page. the BRAND page of the brand tool: the identity, one scrolling page of chapters
15
8
  * (About · Tone · Look · Logo · Lockups · Color · Typography) under a hero and a chapter index.
16
9
  *
17
- * kol-olina's apps/brand `pages/brand/*`, carried class-for-class; what was that client's copy now
18
- * comes from the manifest (`brand.book`, `brand.ramps`, `brand.type`, `brand.logos`). Sections with
19
- * nothing to show for the manifest are left out — see `brandSections`.
10
+ * `BrandGuide`'s chapters over `BOOK_SECTIONS` (0.11.0 — one renderer; the book's own, carried from kol-olina's
11
+ * apps/brand `pages/brand/*`, is in `_tmp/2026-10-28-brand-book-one-renderer/`). The copy comes from the manifest
12
+ * (`brand.book`, `brand.ramps`, `brand.type`, `brand.logos`); sections with nothing to show are left out.
20
13
  *
21
14
  * @param {Object} brand a brand manifest (`@kolkrabbi/kol-brand-template` schema)
22
15
  * @param {Function} Logo optional mark component, `<Logo variant="wordmark" />`
23
16
  * @param {Object} logoSources `{ [logoId]: rawSvg }` — drawn when no `Logo` is given
24
17
  */
25
18
  export default function BrandBook({ brand, Logo, logoSources }) {
26
- const sections = brandSections(brand, 'brand')
27
- const mark = (id, props) => <BrandMark id={id} Logo={Logo} sources={logoSources} {...props} />
28
- const index = sections.filter((s) => s.nav)
29
-
30
- return (
31
- <>
32
- {sections.map((s) => {
33
- switch (s.key) {
34
- case 'hero': return <PageHero key={s.key} id={s.id} label={s.label} title={s.title} lede={s.lede} />
35
- case 'overview': return (
36
- <PageSection key={s.key} id={s.id} label={s.label} title={s.title} body={s.lede}>
37
- <SectionIndex sections={index} />
38
- </PageSection>
39
- )
40
- case 'logo': return (
41
- <PageSection key={s.key} id={s.id} label={s.label} title={s.title} body={s.lede}>
42
- <Prose blocks={s.blocks} />
43
- <div className="kol-grid mt-12">
44
- {s.marks.map((id) => <LogoCard key={id} logo={mark(id)} frame={false} />)}
45
- </div>
46
- <Prose blocks={s.after} />
47
- </PageSection>
48
- )
49
- case 'lockups': return (
50
- <PageSection key={s.key} id={s.id} label={s.label} title={s.title} body={s.lede}>
51
- <Prose blocks={s.blocks} />
52
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12">
53
- {s.marks.map((id) => (
54
- <LogoCard
55
- key={id}
56
- logo={mark(id)}
57
- caption={brand?.logos?.find((l) => l.id === id)?.name ?? id}
58
- className="[&_figcaption]:uppercase"
59
- />
60
- ))}
61
- </div>
62
- <Prose blocks={s.after} />
63
- </PageSection>
64
- )
65
- case 'color': return <ColorSection key={s.key} s={s} ramps={brand?.ramps ?? []} />
66
- case 'typography': return <TypographySection key={s.key} s={s} brand={brand} />
67
- default: return (
68
- <PageSection key={s.key} id={s.id} label={s.label} title={s.title} body={s.lede}>
69
- <Prose blocks={s.blocks} />
70
- <Prose blocks={s.after} />
71
- </PageSection>
72
- )
73
- }
74
- })}
75
- </>
76
- )
77
- }
78
-
79
- /** Book copy → `.kol-prose`: a string is a paragraph, `{ h }` a sub-heading, `{ lead, text }` a
80
- * paragraph opening on a bold lead. `head` renders inside the same block, after the copy. */
81
- export function Prose({ blocks, head, className = 'mt-12' }) {
82
- if (!blocks?.length && !head) return null
83
- return (
84
- <div className={`kol-prose ${className}`}>
85
- {(blocks ?? []).map((b, i) => {
86
- if (typeof b === 'string') return <p key={i}>{b}</p>
87
- if (b.h) return <h3 key={i}>{b.h}</h3>
88
- return <p key={i}><strong>{b.lead}</strong> {b.text}</p>
89
- })}
90
- {head}
91
- </div>
92
- )
93
- }
94
-
95
- /** The page's chapters as cards — olina's CategoryIndex, read from the sections instead of a nav
96
- * tree. The links are the sections' anchors; the click scrolls instead of setting the hash. */
97
- export function SectionIndex({ sections }) {
98
- if (!sections.length) return null
99
- return (
100
- <ul className="mt-12 grid gap-2 grid-cols-1 md:grid-cols-2 max-w-panel">
101
- {sections.map((s) => (
102
- <li key={s.id}>
103
- <a
104
- href={`#${s.id}`}
105
- onClick={scrollToAnchor}
106
- className="kol-card kol-card-interactive flex items-center justify-between gap-4 px-4 py-3 no-underline group"
107
- >
108
- <span className="kol-mono-14 text-emphasis">{s.nav}</span>
109
- <Icon name="arrow-right" size={16} className="text-oq-48 group-hover:text-emphasis transition-colors" />
110
- </a>
111
- </li>
112
- ))}
113
- </ul>
114
- )
115
- }
116
-
117
- /* Color — the copy, then one swatch row per ramp under its label and note. The first ramp's
118
- * heading sits in the copy's own prose block (olina's Concept + Greyscale were one block); each
119
- * further ramp opens its own, as kol-website's hue ramps did. */
120
- function ColorSection({ s, ramps }) {
121
- const heading = (r) => <><h3>{r.label}</h3>{r.note && <p>{r.note}</p>}</>
122
- const swatches = (r) => (
123
- <ContentCollection cols={{ sm: 2, md: 5 }} gap={16} className="mt-8">
124
- {(r.stops ?? []).map((st) => (
125
- <Swatch key={st.stop} hex={st.value} name={`${r.id}-${st.stop}`} anchor={st.stop === r.anchor} />
126
- ))}
127
- </ContentCollection>
128
- )
129
- return (
130
- <PageSection id={s.id} label={s.label} title={s.title} body={s.lede}>
131
- {ramps.map((r, i) => (
132
- <div key={r.id ?? i}>
133
- {i === 0
134
- ? <Prose blocks={s.blocks} head={heading(r)} />
135
- : <div className="kol-prose mt-12">{heading(r)}</div>}
136
- {swatches(r)}
137
- </div>
138
- ))}
139
- {!ramps.length && <Prose blocks={s.blocks} />}
140
- <Prose blocks={s.after} />
141
- </PageSection>
142
- )
143
- }
144
-
145
- function TypographySection({ s, brand }) {
146
- return (
147
- <PageSection id={s.id} label={s.label} title={s.title} body={s.lede}>
148
- <Prose blocks={s.blocks} />
149
- {/* Sans italic at display size — real italic cuts, fired by plain font-style: italic. */}
150
- <div className="mt-12 flex flex-col gap-2 py-3 border-b border-fg-08">
151
- <span className="kol-mono-12 text-meta uppercase tracking-wider">
152
- .kol-prose-display-md · sans narrow · roman + italic
153
- </span>
154
- <p className="kol-prose-display-md m-0">
155
- The quick <span className="italic">brown</span> fox jumps over the lazy dog
156
- </p>
157
- </div>
158
-
159
- {typeSections(brand).map((section) => (
160
- <div key={section.id} className="mt-12">
161
- <div className="kol-prose max-w-measure">
162
- <h3>{section.title}</h3>
163
- <p>{section.intro}</p>
164
- {section.reasoning && (
165
- <p className="text-meta italic">{section.reasoning}</p>
166
- )}
167
- </div>
168
- <div className="mt-8 flex flex-col gap-6">
169
- {section.rows.map((row, i) => (
170
- <TypeShowcase key={`${section.id}-${i}`} sectionId={section.id} row={row} />
171
- ))}
172
- </div>
173
- </div>
174
- ))}
175
- <Prose blocks={s.after} />
176
- </PageSection>
177
- )
178
- }
179
-
180
- /* One specimen row, adapted to its section (olina's brand-bits `TypeShowcase`, verbatim). */
181
- function TypeShowcase({ sectionId, row }) {
182
- if (sectionId === 'sans-families') {
183
- return (
184
- <div className="flex flex-col gap-2 py-3 border-b border-fg-08">
185
- <span className="kol-helper-12 text-meta uppercase tracking-wider">{row.token}</span>
186
- <span className="text-emphasis" style={{ fontFamily: `var(${row.token})`, fontSize: 28 }}>
187
- {row.cut}
188
- </span>
189
- <span className="kol-helper-10 text-subtle">{row.role}</span>
190
- </div>
191
- )
192
- }
193
-
194
- if (sectionId === 'sans-atomic') {
195
- const cls = row.cls.replace(/^\./, '')
196
- return (
197
- <div className="flex flex-col gap-2 py-3 border-b border-fg-08">
198
- <span className="kol-helper-12 text-meta uppercase tracking-wider">
199
- {row.cls} · {row.family} · {row.weight}
200
- </span>
201
- <div className={cls}>The quick brown fox jumps over the lazy dog</div>
202
- </div>
203
- )
204
- }
205
-
206
- if (sectionId === 'prose') {
207
- const tag = (row.class.match(/h[1-6]|p|code|pre/) || [])[0]
208
- const word = row.role
209
- let sample
210
- if (tag === 'h1') sample = <h1>{word}</h1>
211
- else if (tag === 'h2') sample = <h2>{word}</h2>
212
- else if (tag === 'h3') sample = <h3>{word}</h3>
213
- else if (tag === 'h4') sample = <h4>{word}</h4>
214
- else if (tag === 'h5') sample = <h5>{word}</h5>
215
- else if (tag === 'h6') sample = <h6>{word}</h6>
216
- else if (tag === 'code') sample = <p>Inline <code>code</code> in body copy.</p>
217
- else if (tag === 'pre') sample = <pre>{`code block\nlines preserved`}</pre>
218
- else sample = <span className={row.class.replace(/^\./, '').replace(/^kol-prose /, '')}>{word}</span>
219
- return (
220
- <div className="flex flex-col gap-2 py-3 border-b border-fg-08">
221
- <span className="kol-helper-12 text-meta uppercase tracking-wider">
222
- {row.class} · {row.family} · {row.weight}
223
- </span>
224
- <div className="kol-prose">{sample}</div>
225
- </div>
226
- )
227
- }
228
-
229
- if (sectionId === 'mono') {
230
- const cls = row.cls.replace(/^\./, '')
231
- return (
232
- <div className="flex flex-col gap-2 py-3 border-b border-fg-08">
233
- <span className="kol-helper-10 text-meta uppercase tracking-wider">
234
- {row.cls} · weight {row.weight} · LH {typeof row.lh === 'number' ? `${row.lh}px` : row.lh} · LS {row.ls}
235
- </span>
236
- <span className={cls}>The quick brown fox jumps over the lazy dog</span>
237
- </div>
238
- )
239
- }
240
-
241
- if (sectionId === 'opacity' && row.name && row.pct !== undefined) {
242
- return (
243
- <div className="flex flex-col gap-2 py-3 border-b border-fg-08">
244
- <span className="kol-helper-12 text-meta uppercase tracking-wider">
245
- .text-{row.name} · {row.pct}% · {row.role}
246
- </span>
247
- <span className={`text-${row.name}`} style={{ fontSize: 18 }}>
248
- The quick brown fox jumps over the lazy dog
249
- </span>
250
- </div>
251
- )
252
- }
253
-
254
- return null
19
+ return <GuideChapters brand={brand} sections={BOOK_SECTIONS} Logo={Logo} logoSources={logoSources} />
255
20
  }
@@ -1,9 +1,11 @@
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). */
1
+ /* THE ONE BRAND RENDERER. kol-branding's guide renderer (src/template/book/BrandBook.jsx + Brand.jsx, 2026-10-24),
2
+ * carried class-for-class for the brand guide outline (BrandGuideOutlineTwoVersions, kol-hq 2026-10-27) — and since
3
+ * 0.11.0 what `BrandBook` and `BrandAssets` draw through too (user 2026-10-28, two contents pages for one thing: "if
4
+ * you can't maintain it, it's too complicated"). The old book's own renderer, with its Swatch palette and KOL-type
5
+ * chapters, is in `_tmp/2026-10-28-brand-book-one-renderer/`. Two things stay in kol-branding because a package cannot
6
+ * carry them: the research line under each chapter (its own citations) and the router on the Contents card. */
5
7
  import { useState } from 'react'
6
- import { ColorRamp, ContentCollection, DocsToc, FullscreenOverlay, Table } from '@kolkrabbi/kol-component'
8
+ import { ColorRamp, ContentCollection, DocsToc, FullscreenOverlay, Graphic, GRAPHIC_RAW, Table } from '@kolkrabbi/kol-component'
7
9
  import { Icon } from '@kolkrabbi/kol-icons'
8
10
  import PageHero from '@kolkrabbi/kol-framework/src/PageHero.jsx'
9
11
  import PageSection from '@kolkrabbi/kol-framework/src/PageSection.jsx'
@@ -11,8 +13,8 @@ import LogoCard from './LogoCard.jsx'
11
13
  import AssetTable from './AssetTable.jsx'
12
14
  import AssetCard from './AssetCard.jsx'
13
15
  import BrandMark from './BrandMark.jsx'
14
- import { BrandAvatar } from './SocialMocks.jsx'
15
- import { BusinessCardBack, BusinessCardFront, EmailSignature, Envelope, Letterhead } from './StationeryMocks.jsx'
16
+ import { BrandAvatar, PostEditorial, PostPhoto, StoryPhoto } from './SocialMocks.jsx'
17
+ import { BusinessCardBack, BusinessCardFront, EmailSignature, Envelope, Letterhead, LetterheadB } from './StationeryMocks.jsx'
16
18
  import { brandInfo, STATIONERY_MARKS, BRANDED_ASSET_ROWS, scrollToAnchor, svgDims, downloadRecolored } from './brandBook.js'
17
19
  import { guideSections, guideToc, stopHex, contrast } from './guideBook.js'
18
20
 
@@ -32,21 +34,27 @@ import { guideSections, guideToc, stopHex, contrast } from './guideBook.js'
32
34
  * @param {string} className classes on the root
33
35
  */
34
36
  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
37
  return (
39
38
  /* `xl:pr-32` — the rail is `position: fixed` on the right edge from xl up, and the pages' grids
40
39
  run to the frame's edge; the padding keeps the last column out from under it */
41
40
  <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} />))}
41
+ <GuideChapters brand={brand} sections={sections} Logo={Logo} logoSources={logoSources} structure={structure} />
45
42
  <DocsToc variant="rail" toc={guideToc(brand, sections)} onNavigate={scrollToAnchor} className="hidden xl:flex" />
46
43
  </div>
47
44
  )
48
45
  }
49
46
 
47
+ /** GuideChapters — the chapters alone, no rail: the hero, then one Chapter per row that shows for this manifest.
48
+ * `BrandBook` and `BrandAssets` are this over `BOOK_SECTIONS` and `ASSET_SECTIONS`; `Brand` puts the rail beside them. */
49
+ export function GuideChapters({ brand, sections, Logo, logoSources, structure }) {
50
+ const list = guideSections(brand, sections)
51
+ const mark = (id, props) => <BrandMark id={id} Logo={Logo} sources={logoSources} {...props} />
52
+ const index = list.filter((s) => s.nav)
53
+ return list.map((s) => (s.kind === 'hero'
54
+ ? <PageHero key={s.key} id={s.id} label={s.label} title={s.title} lede={s.lede} />
55
+ : <Chapter key={s.key} s={s} brand={brand} mark={mark} index={index} logoSources={logoSources} structure={structure} />))
56
+ }
57
+
50
58
  function Chapter({ s, brand, mark, index, logoSources, structure }) {
51
59
  const Specimen = SPECIMENS[s.kind ?? 'prose']
52
60
  return (
@@ -263,17 +271,37 @@ function Stationery({ brand, mark }) {
263
271
  <AssetCard caption="Email signature"><EmailSignature mark={mark(marks.signature)} info={info} /></AssetCard>
264
272
  </div>
265
273
  <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>
274
+ {/* the second letterhead, its own row (kol-website's Stationery page): `stationery.letterheadB: true` */}
275
+ {brand?.stationery?.letterheadB && (
276
+ <div className="col-span-2">
277
+ <AssetCard caption="Letterhead, A4 [B]"><LetterheadB mark={mark(marks.letter, { style: { display: 'inline-block', height: 48, width: 'auto' } })} info={info} /></AssetCard>
278
+ </div>
279
+ )}
266
280
  </div>
267
281
  )
268
282
  }
269
283
 
270
- /* Social — one frame per template at its ratio; the image when the manifest has one, the mark on a plain ground when not. */
284
+ /* the drawn social mocks a template may name instead of a photo (kol-website's Social page, 2026-10-14) */
285
+ const SOCIAL_MOCKS = { PostPhoto, PostEditorial, StoryPhoto }
286
+
287
+ /* Social — one frame per template at its ratio: a drawn mock when the row names one (`mock` — a SOCIAL_MOCKS name or a
288
+ * node; it carries the brand's logomark itself, so no overlay; `ratio` boxes one that does not size itself), the image
289
+ * when it has `src`, the mark on a plain ground when it has neither. */
271
290
  function Social({ brand, mark }) {
272
291
  const templates = brand?.social?.templates ?? []
292
+ const marks = { ...STATIONERY_MARKS, ...(brand?.stationery?.marks ?? {}) }
273
293
  if (!templates.length) return null
274
294
  return (
275
295
  <ContentCollection cols={3} className="mt-8 items-start">
276
- {templates.map(({ caption, ratio, src }) => (
296
+ {templates.map(({ caption, ratio, src, mock }) => mock ? (
297
+ <AssetCard key={caption ?? String(mock)} caption={caption}>
298
+ {(() => {
299
+ const Mock = typeof mock === 'string' ? SOCIAL_MOCKS[mock] : null
300
+ const node = Mock ? <Mock mark={mark(marks.avatar)} /> : mock
301
+ return ratio ? <div className="overflow-hidden rounded-sm" style={{ aspectRatio: ratio }}>{node}</div> : node
302
+ })()}
303
+ </AssetCard>
304
+ ) : (
277
305
  <AssetCard key={caption ?? src} caption={caption}>
278
306
  <div className={`relative overflow-hidden ${src ? '' : 'bg-fg-08'}`} style={{ aspectRatio: ratio }}>
279
307
  {src && <img src={src} alt="" loading="lazy" className="absolute inset-0 h-full w-full object-cover" />}
@@ -299,8 +327,41 @@ function Profile({ brand, mark }) {
299
327
  )
300
328
  }
301
329
 
330
+ /* a graphic from kol-component's set as an asset row — kol-website's graphicRows, its 80px preview */
331
+ const graphicRows = (category, names = []) => names.map((name) => {
332
+ const raw = GRAPHIC_RAW[category]?.[name]
333
+ return {
334
+ id: `${category}/${name}`, name, preview: <Graphic category={category} name={name} />, previewWidth: 80,
335
+ path: `packages/component/src/graphics/svg/${category}/${name}.svg`, format: 'SVG', dimensions: svgDims(raw), raw,
336
+ onDownload: raw ? (row, ink) => downloadRecolored(row.raw, ink, `${name}.svg`) : undefined,
337
+ }
338
+ })
339
+
340
+ /* Graphics · Patterns — the manifest's `graphics[category]` names on the asset table, ink toggle and recoloured download */
341
+ function Graphics({ s, brand }) {
342
+ return <AssetTable caption={s.title} rows={graphicRows(s.category, brand?.graphics?.[s.category])} inkTokens={INK_TOKENS} className="mt-8" />
343
+ }
344
+
345
+ /* the same names as square tiles with their category / name — kol-website's GraphicCard grid */
346
+ function GraphicCards({ s, brand }) {
347
+ return (
348
+ <div className="kol-grid mt-8">
349
+ {(brand?.graphics?.[s.category] ?? []).map((name) => (
350
+ <figure key={name}>
351
+ <div className="aspect-square flex items-center justify-center rounded-[4px] overflow-hidden bg-surface-inverse">
352
+ <Graphic category={s.category} name={name} />
353
+ </div>
354
+ <figcaption className="kol-helper-12 uppercase tracking-wider text-meta mt-2">{s.category} / {name}</figcaption>
355
+ </figure>
356
+ ))}
357
+ </div>
358
+ )
359
+ }
360
+
302
361
  const SPECIMENS = {
303
362
  contents: Contents,
363
+ graphics: Graphics,
364
+ 'graphics-cards': GraphicCards,
304
365
  logo: Marks,
305
366
  lockups: Lockups,
306
367
  color: Color,
package/src/brandBook.js CHANGED
@@ -8,42 +8,49 @@
8
8
  * left for its manifest to set; anything that was the CLIENT's copy (About, Tone, Look, the logo concept,
9
9
  * the color concept) is not here — it comes from the manifest's `book` field, section by section
10
10
  * (`@kolkrabbi/kol-brand-template` schema). A manifest key overrides the default of the same name.
11
+ *
12
+ * Since 0.11.0 the two pages render through `BrandGuide`'s chapters (user 2026-10-28: one renderer) — this file is the
13
+ * two lists and the manifest helpers; the model is ./guideBook.js.
11
14
  */
15
+ import { guideSections, guideToc } from './guideBook.js'
16
+
17
+ /* `nav` is the sidebar / rail label; a section without one (the hero, the overviews) is not indexed. `kind` names the
18
+ * specimen BrandGuide draws (its SPECIMENS table); a row without one is prose and shows only with copy in the manifest.
19
+ * `part` groups the overview's cards — Brand's three, the groups the site rail already uses (apps/site-hub, 2026-10-24). */
20
+ const OVERVIEW = 'Brand overview', LOGOS = 'Logos', FOUNDATIONS = 'Foundations'
12
21
 
13
- /* `nav` is the sidebar / rail label; a section without one (the hero, the overviews) is not indexed.
14
- * `copy` marks a section that is ONLY its copy — with none in the manifest it is not rendered. */
15
22
  export const BOOK_SECTIONS = [
16
- { key: 'hero', id: 'hero', label: 'brand guidelines', lede: 'Client-facing identity guidelines — chapter-structured for handoff. Mirrors the shape of the deliverable PDF.' },
17
- { key: 'overview', id: 'brand-overview', label: 'Overview', title: 'Chapters', lede: 'Each chapter is its own page. They read in order, but every one stands alone for handoff.' },
18
- { key: 'about', id: 'about', nav: 'About', label: '01 — about', copy: true },
19
- { key: 'messages', id: 'key-messages', nav: 'Key messages', label: '01 — about · key messages', title: 'Key messages', copy: true },
20
- { key: 'tone', id: 'voice', nav: 'Tone', label: '02 — tone', title: 'Tone', copy: true },
21
- { key: 'look', id: 'look', nav: 'Look', label: '03 — look', title: 'Look', copy: true },
22
- { key: 'logo', id: 'logos-concept', nav: 'Logo', label: '04 — logos · concept', title: 'The mark', marks: ['wordmark', 'logomark'] },
23
- { key: 'lockups', id: 'logos-types', nav: 'Lockups', label: '05 — logos · types', title: 'Marks and lockups', lede: 'Logomark, wordmark, and two primary lockups.', marks: ['logomark', 'wordmark', 'lockup-hori', 'lockup-vert'] },
24
- { key: 'color', id: 'color', nav: 'Color', label: '06 — color', title: 'Palette' },
25
- { key: 'typography', id: 'typography', nav: 'Typography', label: '07 — typography', title: 'Type' },
23
+ { key: 'hero', id: 'hero', kind: 'hero', label: 'brand guidelines', lede: 'Client-facing identity guidelines — chapter-structured for handoff. Mirrors the shape of the deliverable PDF.' },
24
+ { key: 'overview', id: 'brand-overview', kind: 'contents', label: 'Overview', title: 'Chapters', lede: 'Each chapter is its own page. They read in order, but every one stands alone for handoff.' },
25
+ { key: 'about', id: 'about', part: OVERVIEW, nav: 'About', label: '01 — about' },
26
+ { key: 'messages', id: 'key-messages', part: OVERVIEW, nav: 'Key messages', label: '01 — about · key messages', title: 'Key messages' },
27
+ { key: 'tone', id: 'voice', part: OVERVIEW, nav: 'Tone', label: '02 — tone', title: 'Tone' },
28
+ { key: 'look', id: 'look', part: OVERVIEW, nav: 'Look', label: '03 — look', title: 'Look' },
29
+ { key: 'logo', id: 'logos-concept', part: LOGOS, kind: 'logo', nav: 'Logo', label: '04 — logos · concept', title: 'The mark', marks: ['wordmark', 'logomark'] },
30
+ { key: 'lockups', id: 'logos-types', part: LOGOS, kind: 'lockups', nav: 'Lockups', label: '05 — logos · types', title: 'Marks and lockups', lede: 'Logomark, wordmark, and two primary lockups.', marks: ['logomark', 'wordmark', 'lockup-hori', 'lockup-vert'] },
31
+ { key: 'color', id: 'color', part: FOUNDATIONS, kind: 'color', nav: 'Color', label: '06 — color', title: 'Palette' },
32
+ { key: 'typography', id: 'typography', part: FOUNDATIONS, kind: 'typography', nav: 'Typography', label: '07 — typography', title: 'Type' },
26
33
  ]
27
34
 
28
35
  export const ASSET_SECTIONS = [
29
- { key: 'assetsOverview', id: 'assets-overview', label: 'Assets', title: 'Downloads and reproduction', lede: 'Everything you download or reproduce. Brand documents the identity; this holds the files and the specs for making them.' },
30
- { key: 'logos', id: 'logos', nav: 'Logos', label: '01 — logos', title: 'Logos' },
36
+ { key: 'assetsOverview', id: 'assets-overview', kind: 'contents', label: 'Assets', title: 'Downloads and reproduction', lede: 'Everything you download or reproduce. Brand documents the identity; this holds the files and the specs for making them.' },
37
+ { key: 'logos', id: 'logos', kind: 'logos-table', nav: 'Logos', label: '01 — logos', title: 'Logos' },
31
38
  /* kol-website's Assets page (BrandAssetsGraphicsAndPatterns, 2026-10-27): the manifest's `graphics` — names from
32
39
  kol-component's set by category, `{ abstract: [...], patterns: [...] }`. The Patterns page there carried a second
33
40
  block, the cards (`graphics-patterns`); it rides under Patterns, off the rail, as it did. Ledes named repo paths,
34
41
  so they are the manifest's to set, as Logos' is. */
35
- { key: 'graphics', id: 'graphics', nav: 'Graphics', label: '02 — graphics', title: 'Graphics', category: 'abstract' },
36
- { key: 'patterns', id: 'patterns', nav: 'Patterns', label: '03 — patterns', title: 'Patterns', category: 'patterns' },
37
- { key: 'graphicsPatterns', id: 'graphics-patterns', label: '16 — graphics · patterns', title: 'Patterns', category: 'patterns' },
38
- { key: 'branded', id: 'branded-assets', nav: 'Branded', label: '04 — branded', title: 'Branded assets' },
39
- { key: 'stationery', id: 'assets-stationery', nav: 'Stationery', label: '08 — assets · stationery', title: 'Stationery', lede: 'Standard correspondence — business card, envelope, letterhead, email signature. Quiet typography, generous space, monochrome restraint.' },
40
- { key: 'labels', id: 'labels', nav: 'Labels', label: '09 — labels', title: 'Labels', copy: true },
41
- { key: 'packaging', id: 'packaging', nav: 'Packaging', label: '10 — packaging', title: 'Packaging', copy: true },
42
- { key: 'mockups', id: 'mockups', nav: 'Mockups', label: '11 — mockups', title: 'Mockups', copy: true },
43
- { key: 'social', id: 'social-sizes', nav: 'Social', label: '12 — social · sizes', title: 'Post sizes', lede: 'One template at each of the three Instagram aspect ratios — square feed (1:1), portrait feed (4:5), and story / reel (9:16). Editorial photography, restrained typography, a deliberate cadence.' },
44
- { key: 'profile', id: 'social-profile', nav: 'Profile', label: '13 — social · profile', title: 'Profile', lede: 'Avatar treatment for profile pictures across platforms — round-cropped on burgundy, signature centered.' },
45
- { key: 'generators', id: 'generators', nav: 'Generators', label: '14 — generators', title: 'Generators', copy: true },
46
- { key: 'deck', id: 'slide-deck', nav: 'Slide deck', label: '15 — slide deck', title: 'Slide deck', copy: true },
42
+ { key: 'graphics', id: 'graphics', kind: 'graphics', category: 'abstract', nav: 'Graphics', label: '02 — graphics', title: 'Graphics' },
43
+ { key: 'patterns', id: 'patterns', kind: 'graphics', category: 'patterns', nav: 'Patterns', label: '03 — patterns', title: 'Patterns' },
44
+ { key: 'graphicsPatterns', id: 'graphics-patterns', kind: 'graphics-cards', category: 'patterns', label: '16 — graphics · patterns', title: 'Patterns' },
45
+ { key: 'branded', id: 'branded-assets', kind: 'branded', nav: 'Branded', label: '04 — branded', title: 'Branded assets' },
46
+ { key: 'stationery', id: 'assets-stationery', kind: 'stationery', nav: 'Stationery', label: '08 — assets · stationery', title: 'Stationery', lede: 'Standard correspondence — business card, envelope, letterhead, email signature. Quiet typography, generous space, monochrome restraint.' },
47
+ { key: 'labels', id: 'labels', nav: 'Labels', label: '09 — labels', title: 'Labels' },
48
+ { key: 'packaging', id: 'packaging', nav: 'Packaging', label: '10 — packaging', title: 'Packaging' },
49
+ { key: 'mockups', id: 'mockups', nav: 'Mockups', label: '11 — mockups', title: 'Mockups' },
50
+ { key: 'social', id: 'social-sizes', kind: 'social', nav: 'Social', label: '12 — social · sizes', title: 'Post sizes', lede: 'One template at each of the three Instagram aspect ratios — square feed (1:1), portrait feed (4:5), and story / reel (9:16). Editorial photography, restrained typography, a deliberate cadence.' },
51
+ { key: 'profile', id: 'social-profile', kind: 'profile', nav: 'Profile', label: '13 — social · profile', title: 'Profile', lede: 'Avatar treatment for profile pictures across platforms — round-cropped on burgundy, signature centered.' },
52
+ { key: 'generators', id: 'generators', nav: 'Generators', label: '14 — generators', title: 'Generators' },
53
+ { key: 'deck', id: 'slide-deck', nav: 'Slide deck', label: '15 — slide deck', title: 'Slide deck' },
47
54
  ]
48
55
 
49
56
  /* the two pages, for a view switch */
@@ -52,40 +59,11 @@ export const BRAND_VIEWS = [
52
59
  { value: 'assets', label: 'Assets' },
53
60
  ]
54
61
 
55
- const hasCopy = (s) => Boolean(s.lede || s.blocks?.length || s.after?.length)
56
-
57
- /* Whether a section has anything to show for this manifest. Copy-only sections need copy; the
58
- * specimen sections need the data they render. */
59
- function shows(s, brand) {
60
- if (s.copy) return hasCopy(s)
61
- if (s.key === 'color') return Boolean(brand?.ramps?.length) || hasCopy(s)
62
- if (s.key === 'logos') return Boolean(brand?.logos?.length)
63
- if (s.category) return Boolean(brand?.graphics?.[s.category]?.length)
64
- /* the marks sections draw the manifest's logos — their default lede is not enough to show empty frames */
65
- if (s.key === 'logo' || s.key === 'lockups') return Boolean(brand?.logos?.length || s.blocks?.length || s.after?.length)
66
- if (s.key === 'social') return Boolean(brand?.social?.templates?.length)
67
- if (s.key === 'profile') return Boolean(brand?.social?.avatars?.length)
68
- return true
69
- }
70
-
71
- /** One page's sections for a manifest — defaults merged with `brand.book`, the empty ones dropped.
72
- * `[{ key, id, nav?, label, title, lede, blocks, after, marks }]` */
73
- export function brandSections(brand, page = 'brand') {
74
- const defaults = page === 'assets' ? ASSET_SECTIONS : BOOK_SECTIONS
75
- const name = brand?.meta?.name
76
- return defaults
77
- .map((d) => {
78
- const s = { ...d, ...(brand?.book?.[d.key] ?? {}) }
79
- if (d.key === 'hero' && !s.title) s.title = name
80
- if (d.key === 'about' && !s.title && name) s.title = `About ${name}`
81
- return s
82
- })
83
- .filter((s) => shows(s, brand))
84
- }
62
+ /** One page's sections for a manifest — the page's list on the guide's one model (guideBook.js), the empty ones dropped. */
63
+ export const brandSections = (brand, page = 'brand') => guideSections(brand, page === 'assets' ? ASSET_SECTIONS : BOOK_SECTIONS)
85
64
 
86
65
  /** A page's rail / index entries — `[{ id, label }]`. */
87
- export const brandToc = (brand, page) =>
88
- brandSections(brand, page).filter((s) => s.nav).map((s) => ({ id: s.id, label: s.nav }))
66
+ export const brandToc = (brand, page) => guideToc(brand, page === 'assets' ? ASSET_SECTIONS : BOOK_SECTIONS)
89
67
 
90
68
  /** The manifest's `meta` in the shape the stationery mocks read (`DEFAULT_BRAND_INFO`). A field the
91
69
  * manifest does not carry is an EMPTY STRING, so the mocks render it blank rather than falling back
package/src/guideBook.js CHANGED
@@ -1,21 +1,26 @@
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. */
1
+ /* The chapter model behind every brand page — kol-branding's src/template/book/brandBook.js (2026-10-24), carried for
2
+ * `BrandGuide` (2026-10-27) and, since 0.11.0, the one model `BrandBook` and `BrandAssets` read too (the book had its own
3
+ * `shows` and renderer beside this one; user 2026-10-28: two looks for one thing). A page is a chapter list — `BOOK_SECTIONS`,
4
+ * `ASSET_SECTIONS`, kol-brand-template's `GUIDE_SECTIONS`, a client's own — merged with the manifest's `book`. The manifest
5
+ * helpers (brandInfo, the stationery marks, the anchor scroll, the svg helpers) stay in ./brandBook.js. */
5
6
 
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
+ /* copy beyond the lede — what makes a specimen chapter worth drawing without its data (the brand book's rule, olina:
8
+ * "their default lede is not enough to show empty frames") */
9
+ const hasBody = (s) => Boolean(s.blocks?.length || s.after?.length || s.rules?.length || s.spec || s.dos?.length || s.donts?.length || s.words || s.pairs?.length)
10
+ const hasCopy = (s) => Boolean(s.lede) || hasBody(s)
7
11
 
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. */
12
+ /* Whether a chapter has anything to show for this manifest. The specimen kinds need the data they draw or body copy;
13
+ * a prose row shows with any copy, the lede included. */
10
14
  export function shows(s, brand) {
15
+ if (s.category) return Boolean(brand?.graphics?.[s.category]?.length)
11
16
  const kind = s.kind ?? 'prose'
12
17
  if (kind === 'hero' || kind === 'contents' || kind === 'branded' || kind === 'stationery') return true
13
18
  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
+ if (kind === 'logo' || kind === 'lockups') return Boolean(brand?.logos?.length) || hasBody(s)
20
+ if (kind === 'color') return Boolean(brand?.ramps?.length) || hasBody(s)
21
+ if (kind === 'typography') return Boolean(brand?.type?.families?.length) || hasBody(s)
22
+ if (kind === 'social') return Boolean(brand?.social?.templates?.length) || hasBody(s)
23
+ if (kind === 'profile') return Boolean(brand?.social?.avatars?.length) || hasBody(s)
19
24
  return hasCopy(s)
20
25
  }
21
26
 
@@ -1,166 +0,0 @@
1
- /**
2
- * typeSpecimen — the brand book's Typography chapter as data.
3
- *
4
- * kol-olina's apps/brand `data/typography.js`, verbatim, minus its two brand-specific tables: the
5
- * FAMILIES come from the manifest (`type.families`, `type.note`), and the cuts table was never
6
- * rendered by the chapter. Everything else describes the KOL type system — classes, prose, the mono
7
- * split, the reading hierarchy — which every brand on the system shares, because its type rides the
8
- * family tokens.
9
- */
10
-
11
- const sansDisplayRows = [
12
- { cls: '.kol-sans-display-01', tokenName: '--kol-text-display-01', family: 'sans-narrow', weight: 600 },
13
- { cls: '.kol-sans-display-02', tokenName: '--kol-text-display-02', family: 'sans-narrow', weight: 600 },
14
- ]
15
-
16
- const sansHeadingRows = [
17
- { cls: '.kol-sans-heading-01', tokenName: '--kol-text-heading-01', family: 'sans-narrow', weight: 500 },
18
- { cls: '.kol-sans-heading-02', tokenName: '--kol-text-heading-02', family: 'sans-compact', weight: 500 },
19
- { cls: '.kol-sans-heading-03', tokenName: '--kol-text-heading-03', family: 'sans-compact', weight: 500 },
20
- { cls: '.kol-sans-heading-04', tokenName: '--kol-text-heading-04', family: 'sans-compact', weight: 500 },
21
- { cls: '.kol-sans-heading-05', tokenName: '--kol-text-heading-05', family: 'sans-compact', weight: 500 },
22
- ]
23
-
24
- const sansBodyRows = [
25
- { cls: '.kol-sans-body-01', tokenName: '--kol-text-body-01', family: 'sans (base)', weight: 400 },
26
- { cls: '.kol-sans-body-02', tokenName: '--kol-text-body-02', family: 'sans (base)', weight: 400 },
27
- { cls: '.kol-sans-body-03', tokenName: '--kol-text-body-03', family: 'sans (base)', weight: 400 },
28
- ]
29
-
30
- const proseRows = [
31
- { role: 'Display', class: '.kol-prose-display', family: 'sans-narrow', weight: 500 },
32
- { role: 'Display (md)',class: '.kol-prose-display-md', family: 'sans-narrow', weight: 500 },
33
- { role: 'Title', class: '.kol-prose-title', family: 'sans-narrow', weight: 500 },
34
- { role: 'H1', class: '.kol-prose h1', family: 'sans-narrow', weight: 500, tokenName: '--kol-text-heading-01' },
35
- { role: 'H2', class: '.kol-prose h2', family: 'sans-compact', weight: 400, tokenName: '--kol-text-heading-03' },
36
- { role: 'H3', class: '.kol-prose h3', family: 'sans-compact', weight: 400, tokenName: '--kol-text-heading-04' },
37
- { role: 'H4', class: '.kol-prose h4', family: 'sans-compact', weight: 400, tokenName: '--kol-text-heading-05' },
38
- { role: 'H5', class: '.kol-prose h5', family: 'sans-compact', weight: 500, tokenName: '--kol-text-heading-06' },
39
- { role: 'H6', class: '.kol-prose h6', family: 'sans-compact', weight: 500, tokenName: '--kol-text-heading-06' },
40
- { role: 'Lede', class: '.kol-prose-lede', family: 'sans-compact', weight: 400 },
41
- { role: 'Tagline', class: '.kol-prose-tagline', family: 'sans-narrow', weight: 500 },
42
- { role: 'Body', class: '.kol-prose p', family: 'sans (base)', weight: 300 },
43
- { role: 'Quote', class: '.kol-prose blockquote p', family: 'sans (base)', weight: 600 },
44
- { role: 'Cite', class: '.kol-prose blockquote cite',family: 'sans (base)', weight: 500 },
45
- { role: 'Label', class: '.kol-prose-label', family: 'mono', weight: 500 },
46
- { role: 'Code (inline)',class: '.kol-prose code', family: 'mono', weight: 400 },
47
- { role: 'Code (block)', class: '.kol-prose pre', family: 'mono', weight: 400 },
48
- ]
49
-
50
- const monoRows = [
51
- { cls: '.kol-mono-20', size: 20, lh: 26, weight: 400, ls: 'normal' },
52
- { cls: '.kol-mono-16', size: 16, lh: 22, weight: 400, ls: 'normal' },
53
- { cls: '.kol-mono-14', size: 14, lh: 18, weight: 400, ls: 'normal' },
54
- { cls: '.kol-mono-12', size: 12, lh: 16, weight: 400, ls: 'normal' },
55
- { cls: '.kol-mono-10', size: 10, lh: 14, weight: 400, ls: 'normal' },
56
- { cls: '.kol-mono-8', size: 8, lh: 12, weight: 400, ls: 'normal' },
57
- ]
58
-
59
- const helperRows = [
60
- { cls: '.kol-helper-20', size: 20, lh: '1', weight: 500, ls: '0.06em' },
61
- { cls: '.kol-helper-16', size: 16, lh: '1', weight: 500, ls: '0.06em' },
62
- { cls: '.kol-helper-14', size: 14, lh: '1', weight: 500, ls: '0.06em' },
63
- { cls: '.kol-helper-12', size: 12, lh: '1', weight: 500, ls: '0.06em' },
64
- { cls: '.kol-helper-10', size: 10, lh: '1', weight: 500, ls: '0.10em' },
65
- { cls: '.kol-helper-8', size: 8, lh: '1', weight: 500, ls: '0.10em' },
66
- ]
67
-
68
- const opacityDescriptorRows = [
69
- { name: 'subtle', pct: 24, token: '--kol-fg-subtle', role: 'Dividers, disabled hints, faintest visible' },
70
- { name: 'meta', pct: 48, token: '--kol-fg-meta', role: 'Labels, eyebrows, captions' },
71
- { name: 'body', pct: 64, token: '--kol-fg-body', role: 'Running copy, links' },
72
- { name: 'strong', pct: 80, token: '--kol-fg-strong', role: 'Emphasized body, <strong>' },
73
- { name: 'emphasis', pct: 100, token: '--kol-fg-emphasis', role: 'Headings, max ink' },
74
- ]
75
-
76
- const SYSTEM_SECTIONS = [
77
- {
78
- id: 'sans-atomic',
79
- title: 'Sans atomic classes',
80
- intro:
81
- "Display, heading, body — explicit per-element classes for chrome / " +
82
- "page sections. Distinct from .kol-prose, which cascades into HTML " +
83
- "elements for rich-text containers.",
84
- reasoning:
85
- "Numbered tokens (01–06), not t-shirt sizes. Numbers describe a fixed " +
86
- "position in the scale (01 = anchor, ascending = smaller); t-shirt names " +
87
- "carry no ordering, you have to remember which is bigger. 01 = largest " +
88
- "follows HTML's H1 = biggest convention. Class prefix is family-first " +
89
- "(.kol-sans-) to mirror mono naming and read naturally — \"the sans " +
90
- "system, display role, stop 01\". H1 uses Narrow (condensed for impact); " +
91
- "H2–H5 use Compact (slightly wider, mid-tier); body uses base. Atomic body " +
92
- "weight is 400 (regular) for chrome; prose body is 300 (light) for " +
93
- "editorial reading copy — different defaults per context.",
94
- rows: [...sansDisplayRows, ...sansHeadingRows, ...sansBodyRows],
95
- },
96
- {
97
- id: 'prose',
98
- title: 'Prose container',
99
- intro:
100
- ".kol-prose is a rich-text container that cascades to native HTML " +
101
- "elements (h1–h6, p, ul, blockquote, code, pre). Wrap CMS / blog / " +
102
- "markdown output in it; the container styles every element via " +
103
- "descendant selectors.",
104
- reasoning:
105
- "H2 intentionally skips heading-02 (40px) and uses heading-03 (32px). " +
106
- "48 → 40 reads as \"same family\"; 48 → 32 reads as \"structural break\" " +
107
- "— better visual rhythm at H1 → H2. H6 ships uppercase with tracking " +
108
- "by default since H6 is rare and benefits from a label-like rendering. " +
109
- "Code (inline + block) consumes the mono token so a future swap cascades " +
110
- "into prose code automatically.",
111
- rows: proseRows,
112
- },
113
- {
114
- id: 'mono',
115
- title: 'Mono',
116
- intro:
117
- "JetBrains Mono. Two scales at the same px stops, distinguished by " +
118
- "intent + visual weight. .kol-mono-N for multi-line body / values " +
119
- "(weight 400, with leading). .kol-helper-N for single-line labels " +
120
- "(weight 500, line-height 1, letter-spaced).",
121
- reasoning:
122
- "Same px size, different weight = visible hierarchy without size-jumps. " +
123
- "Common pattern: a label sits above a control or value at the same stop " +
124
- "— label is helper (medium), control text is mono (regular). Both 12px, " +
125
- "but the label \"reads first\" by weight. Tight UI grids stay compact " +
126
- "without further compressing sizes. Px-literal suffixes (10/12/14/16/20) " +
127
- "instead of numbered indices because mono is utility chrome where exact " +
128
- "px matters — the number IS the value, no decoder ring. Wider letter-" +
129
- "spacing (0.10em) on the smallest two stops is a typographic principle: " +
130
- "sub-12px mono needs more tracking to stay legible.",
131
- rows: [...monoRows, ...helperRows],
132
- },
133
- {
134
- id: 'opacity',
135
- title: 'Reading hierarchy (descriptors)',
136
- intro:
137
- "Five-stop ink hierarchy: subtle / meta / body / strong / emphasis. " +
138
- "Aliases over the numeric --kol-fg-NN ramp (UI Colors / Opacity " +
139
- "primitives). Pick descriptors when intent matters; pick numeric stops " +
140
- "when exact percentage matters. Text-only — bg / border / ring " +
141
- "descriptor families retired 2026-04-30 (consumers used numeric stops).",
142
- reasoning:
143
- "Five stops anchor the typography reading hierarchy. The retired " +
144
- "`mute` (32%) sat awkwardly between subtle (24%, dividers) and meta " +
145
- "(48%, labels) and had zero component consumers. Descriptors are now " +
146
- "text-only — for surface tints, borders, and rings, reach for the " +
147
- "numeric --kol-fg-NN family (bg-fg-08, border-fg-12, etc.).",
148
- rows: opacityDescriptorRows,
149
- },
150
- ]
151
-
152
- /** The chapter's sections for a manifest: its families first (when it declares any), then the
153
- * system's. `[{ id, title, intro, reasoning?, rows }]` */
154
- export function typeSections(brand) {
155
- const families = brand?.type?.families ?? []
156
- const head = families.length ? [{
157
- id: 'sans-families',
158
- title: 'Family tokens',
159
- intro:
160
- "Three sans cuts and one mono. Edit a family token to swap any cut " +
161
- "without touching consumers.",
162
- reasoning: brand?.type?.note,
163
- rows: families.map(({ token, role, cut }) => ({ token, role, cut })),
164
- }] : []
165
- return [...head, ...SYSTEM_SECTIONS]
166
- }