@kolkrabbi/kol-styleguide 0.4.0 → 0.5.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 +8 -6
- package/src/Brand.jsx +42 -0
- package/src/BrandAssets.jsx +152 -0
- package/src/BrandBook.jsx +255 -0
- package/src/BrandMark.jsx +27 -0
- package/src/Swatch.jsx +34 -21
- package/src/brandBook.js +126 -0
- package/src/index.js +7 -0
- package/src/typeSpecimen.js +166 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-styleguide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "KOL style-guide / brand-guide component set — the visual specimens a brand manual is built from: color anatomy + combination lab, logo construction / clearspace / scaling, mood tiles, type blocks, and asset spec tables. Raided from the apps/brand styleguide. Sits above @kolkrabbi/kol-{theme,icons,component}.",
|
|
5
|
+
"description": "KOL style-guide / brand-guide component set — the visual specimens a brand manual is built from: color anatomy + combination lab, logo construction / clearspace / scaling, mood tiles, type blocks, and asset spec tables. Raided from the apps/brand styleguide. Also the brand tool itself — `Brand` (BrandBook ⇄ BrandAssets) renders a whole brand book from one brand manifest. Sits above @kolkrabbi/kol-{theme,icons,component,framework}.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
@@ -11,16 +11,18 @@
|
|
|
11
11
|
".": "./src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
14
|
+
"@kolkrabbi/kol-component": ">=0.188.0",
|
|
15
|
+
"@kolkrabbi/kol-framework": ">=0.39.0",
|
|
15
16
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
16
17
|
"@kolkrabbi/kol-theme": ">=0.53.0",
|
|
17
18
|
"react": "^18.3.0 || ^19.0.0",
|
|
18
19
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@kolkrabbi/kol-component": "^0.
|
|
22
|
-
"@kolkrabbi/kol-
|
|
23
|
-
"@kolkrabbi/kol-
|
|
22
|
+
"@kolkrabbi/kol-component": "^0.225.0",
|
|
23
|
+
"@kolkrabbi/kol-framework": "^0.44.0",
|
|
24
|
+
"@kolkrabbi/kol-icons": "^0.27.1",
|
|
25
|
+
"@kolkrabbi/kol-theme": "^0.151.0"
|
|
24
26
|
},
|
|
25
27
|
"files": [
|
|
26
28
|
"src",
|
package/src/Brand.jsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { DocsToc, ViewToggle } from '@kolkrabbi/kol-component'
|
|
3
|
+
import BrandBook from './BrandBook.jsx'
|
|
4
|
+
import BrandAssets from './BrandAssets.jsx'
|
|
5
|
+
import { BRAND_VIEWS, brandToc, scrollToAnchor } from './brandBook.js'
|
|
6
|
+
|
|
7
|
+
/* taxonomy-ok: organism — the brand tool: BrandBook ⇄ BrandAssets under a view switch, with the page's section rail */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Brand — the whole brand tool: the brand book's two pages over one manifest, BRAND (the identity)
|
|
11
|
+
* and ASSETS (what you download or reproduce), a switch between them, and the page's sections on a
|
|
12
|
+
* rail. ONE component so every app that carries a brand renders the same tool (apps/brand alone,
|
|
13
|
+
* media-shell's Brand tab) — kol-notes' `Notes` shape.
|
|
14
|
+
*
|
|
15
|
+
* The manifest is `@kolkrabbi/kol-brand-template`'s schema; its `book` field carries the copy. The
|
|
16
|
+
* marks come from `Logo` (a component taking `variant`) or `logoSources` (id → raw SVG, which the
|
|
17
|
+
* Logos table also needs for dimensions and downloads — pass it either way).
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} brand the manifest
|
|
20
|
+
* @param {Function} Logo optional `<Logo variant="wordmark" />`
|
|
21
|
+
* @param {Object} logoSources `{ [logoId]: rawSvg }`
|
|
22
|
+
* @param {'brand'|'assets'} view · onViewChange controlled page; or internal
|
|
23
|
+
* @param {string} className classes on the root
|
|
24
|
+
*/
|
|
25
|
+
export default function Brand({ brand, Logo, logoSources, view: viewProp, onViewChange, className = '' }) {
|
|
26
|
+
const [viewState, setViewState] = useState('brand')
|
|
27
|
+
const view = viewProp ?? viewState
|
|
28
|
+
const setView = (v) => { if (viewProp === undefined) setViewState(v); onViewChange?.(v) }
|
|
29
|
+
const Page = view === 'assets' ? BrandAssets : BrandBook
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
/* `xl:pr-32` — the rail is `position: fixed` on the right edge from xl up, and the pages' grids
|
|
33
|
+
run to the frame's edge; the padding keeps the last column out from under it */
|
|
34
|
+
<div className={`xl:pr-32 ${className}`.trim()}>
|
|
35
|
+
<div className="kol-page flex justify-end pt-6 pb-0">
|
|
36
|
+
<ViewToggle variant="text" options={BRAND_VIEWS} viewMode={view} onViewChange={setView} />
|
|
37
|
+
</div>
|
|
38
|
+
<Page brand={brand} Logo={Logo} logoSources={logoSources} />
|
|
39
|
+
<DocsToc key={view} variant="rail" toc={brandToc(brand, view)} onNavigate={scrollToAnchor} className="hidden xl:flex" />
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ContentCollection, FullscreenOverlay, 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 { ProfileAvatar } 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'
|
|
13
|
+
|
|
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
|
+
const brandedAssetCols = [
|
|
24
|
+
{ accessor: 'item', header: 'Item', render: (r) => <TokenName>{r.item}</TokenName> },
|
|
25
|
+
{ accessor: 'aspect', header: 'Aspect' },
|
|
26
|
+
{ accessor: 'surface', header: 'Surface' },
|
|
27
|
+
{ accessor: 'status', header: 'Status', render: (r) => <span className="kol-helper-12 text-meta uppercase tracking-widest">{r.status}</span> },
|
|
28
|
+
{ accessor: 'note', header: 'Note' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* BrandAssets — the ASSETS page of the brand tool: what you download or reproduce. Logos (the
|
|
33
|
+
* mark table, with the ink toggle, zoom and recoloured download), the branded-asset register,
|
|
34
|
+
* stationery, social post sizes and profile avatars.
|
|
35
|
+
*
|
|
36
|
+
* kol-olina's apps/brand `pages/assets/*`, carried class-for-class; the client's marks, contact
|
|
37
|
+
* details, templates and avatar grounds come from the manifest (`logos`, `meta`, `stationery.marks`,
|
|
38
|
+
* `social`). Sections with nothing to show are left out — see `brandSections`.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} brand a brand manifest (`@kolkrabbi/kol-brand-template` schema)
|
|
41
|
+
* @param {Function} Logo optional mark component, `<Logo variant="wordmark" />`
|
|
42
|
+
* @param {Object} logoSources `{ [logoId]: rawSvg }` — the table's dimensions and downloads, and
|
|
43
|
+
* the marks themselves when no `Logo` is given
|
|
44
|
+
*/
|
|
45
|
+
export default function BrandAssets({ brand, Logo, logoSources }) {
|
|
46
|
+
const sections = brandSections(brand, 'assets')
|
|
47
|
+
const mark = (id, props) => <BrandMark id={id} Logo={Logo} sources={logoSources} {...props} />
|
|
48
|
+
const index = sections.filter((s) => s.nav)
|
|
49
|
+
const marks = { ...STATIONERY_MARKS, ...(brand?.stationery?.marks ?? {}) }
|
|
50
|
+
const info = brandInfo(brand)
|
|
51
|
+
const [zoom, setZoom] = useState(null)
|
|
52
|
+
|
|
53
|
+
const logoRows = (brand?.logos ?? []).map((logo) => {
|
|
54
|
+
const raw = logoSources?.[logo.id]
|
|
55
|
+
return {
|
|
56
|
+
id: logo.id,
|
|
57
|
+
name: logo.id.replace(/-/g, ' / '),
|
|
58
|
+
preview: mark(logo.id),
|
|
59
|
+
previewWidth: logo.previewWidth ?? 48,
|
|
60
|
+
path: logo.file?.replace(/^\.\//, ''),
|
|
61
|
+
format: 'SVG',
|
|
62
|
+
dimensions: svgDims(raw),
|
|
63
|
+
raw,
|
|
64
|
+
onDownload: raw ? (row, ink) => downloadRecolored(row.raw, ink, `${logo.id}.svg`) : undefined,
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
{sections.map((s) => {
|
|
71
|
+
const head = { id: s.id, label: s.label, title: s.title, body: s.lede }
|
|
72
|
+
switch (s.key) {
|
|
73
|
+
case 'assetsOverview': return (
|
|
74
|
+
<PageSection key={s.key} {...head}>
|
|
75
|
+
<SectionIndex sections={index} />
|
|
76
|
+
</PageSection>
|
|
77
|
+
)
|
|
78
|
+
case 'logos': return (
|
|
79
|
+
<PageSection key={s.key} {...head}>
|
|
80
|
+
<AssetTable
|
|
81
|
+
caption="Logos"
|
|
82
|
+
rows={logoRows}
|
|
83
|
+
inkTokens={INK_TOKENS}
|
|
84
|
+
onPreviewZoom={(row, ink) => setZoom({ row, ink })}
|
|
85
|
+
/>
|
|
86
|
+
<FullscreenOverlay open={!!zoom} onClose={() => setZoom(null)}>
|
|
87
|
+
{zoom && (
|
|
88
|
+
<span
|
|
89
|
+
className="inline-flex items-center p-12"
|
|
90
|
+
style={{ color: zoom.ink, width: 'min(60vw, 480px)' }}
|
|
91
|
+
>
|
|
92
|
+
{zoom.row.preview}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</FullscreenOverlay>
|
|
96
|
+
</PageSection>
|
|
97
|
+
)
|
|
98
|
+
case 'branded': return (
|
|
99
|
+
<PageSection key={s.key} {...head}>
|
|
100
|
+
<Table caption="Branded assets" columns={brandedAssetCols} rows={BRANDED_ASSET_ROWS} className="mt-8" />
|
|
101
|
+
</PageSection>
|
|
102
|
+
)
|
|
103
|
+
case 'stationery': return (
|
|
104
|
+
<PageSection key={s.key} {...head}>
|
|
105
|
+
<div className="kol-grid mt-8">
|
|
106
|
+
<div className="col-span-2"><AssetCard><BusinessCardFront mark={mark(marks.card, { className: 'max-h-[70%]' })} /></AssetCard></div>
|
|
107
|
+
<div className="col-span-2"><AssetCard><Envelope mark={mark(marks.envelope)} info={info} /></AssetCard></div>
|
|
108
|
+
<div className="col-span-2 flex flex-col gap-3">
|
|
109
|
+
<AssetCard><BusinessCardBack info={info} /></AssetCard>
|
|
110
|
+
<AssetCard><EmailSignature mark={mark(marks.signature)} info={info} /></AssetCard>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="col-span-2"><AssetCard><Letterhead mark={mark(marks.letter, { style: { height: '100%', width: 'auto' } })} info={info} /></AssetCard></div>
|
|
113
|
+
</div>
|
|
114
|
+
</PageSection>
|
|
115
|
+
)
|
|
116
|
+
case 'social': return (
|
|
117
|
+
<PageSection key={s.key} {...head}>
|
|
118
|
+
<ContentCollection cols={3} className="mt-8 items-start">
|
|
119
|
+
{brand.social.templates.map(({ caption, ratio, src }) => (
|
|
120
|
+
<AssetCard key={caption ?? src} caption={caption}>
|
|
121
|
+
<div className="relative overflow-hidden" style={{ aspectRatio: ratio }}>
|
|
122
|
+
<img src={src} alt="" loading="lazy" className="absolute inset-0 w-full h-full object-cover" />
|
|
123
|
+
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-black/60 to-transparent" aria-hidden="true" />
|
|
124
|
+
<div className="absolute bottom-[6%] left-[6%] w-[42%] text-fg-inverse">
|
|
125
|
+
{mark(brand.social.mark ?? 'wordmark', { title: brand?.meta?.name })}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</AssetCard>
|
|
129
|
+
))}
|
|
130
|
+
</ContentCollection>
|
|
131
|
+
</PageSection>
|
|
132
|
+
)
|
|
133
|
+
case 'profile': return (
|
|
134
|
+
<PageSection key={s.key} {...head}>
|
|
135
|
+
<div className="kol-grid mt-8 items-start">
|
|
136
|
+
{brand.social.avatars.map(({ bg, polarity }, i) => (
|
|
137
|
+
<AssetCard key={i}><ProfileAvatar mark={mark(marks.avatar, { className: 'w-1/2' })} bg={bg} polarity={polarity} /></AssetCard>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</PageSection>
|
|
141
|
+
)
|
|
142
|
+
default: return (
|
|
143
|
+
<PageSection key={s.key} {...head}>
|
|
144
|
+
<Prose blocks={s.blocks} />
|
|
145
|
+
<Prose blocks={s.after} />
|
|
146
|
+
</PageSection>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
})}
|
|
150
|
+
</>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
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'
|
|
10
|
+
|
|
11
|
+
/* taxonomy-ok: organism — the brand book's identity page: PageHero + PageSection chapters over LogoCard, Swatch and the type specimens */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BrandBook — the BRAND page of the brand tool: the identity, one scrolling page of chapters
|
|
15
|
+
* (About · Tone · Look · Logo · Lockups · Color · Typography) under a hero and a chapter index.
|
|
16
|
+
*
|
|
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`.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} brand a brand manifest (`@kolkrabbi/kol-brand-template` schema)
|
|
22
|
+
* @param {Function} Logo optional mark component, `<Logo variant="wordmark" />`
|
|
23
|
+
* @param {Object} logoSources `{ [logoId]: rawSvg }` — drawn when no `Logo` is given
|
|
24
|
+
*/
|
|
25
|
+
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
|
+
/* Colour — 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-helper-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
|
|
255
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrandMark — a logo by id, for the brand tool. Two sources, in order:
|
|
3
|
+
*
|
|
4
|
+
* 1. `Logo` — the consumer's own mark component (`<Logo variant="wordmark" />`), e.g. an svgr
|
|
5
|
+
* loader. It wins when given, so an app that already renders its marks keeps that DOM.
|
|
6
|
+
* 2. `sources` — `{ [id]: rawSvg }`. The markup is injected so `currentColor` cascades in; the
|
|
7
|
+
* same strings feed the Logos table's dimensions and recoloured downloads. The wrapper fills
|
|
8
|
+
* its box and the svg fills the wrapper, so a sized parent (a LogoCard layer) fits the mark
|
|
9
|
+
* and an unsized one takes its height from the viewBox.
|
|
10
|
+
*
|
|
11
|
+
* Neither holds the id → nothing renders (a missing mark is a gap, not a crash).
|
|
12
|
+
*/
|
|
13
|
+
export default function BrandMark({ id, Logo, sources, className = '', style, title }) {
|
|
14
|
+
if (Logo) return <Logo variant={id} className={className} style={style} title={title} />
|
|
15
|
+
const raw = sources?.[id]
|
|
16
|
+
if (!raw) return null
|
|
17
|
+
return (
|
|
18
|
+
<span
|
|
19
|
+
className={`kol-logo block w-full h-full [&>svg]:block [&>svg]:w-full [&>svg]:h-full ${className}`.trim()}
|
|
20
|
+
style={style}
|
|
21
|
+
role={title ? 'img' : undefined}
|
|
22
|
+
aria-label={title || undefined}
|
|
23
|
+
aria-hidden={title ? undefined : true}
|
|
24
|
+
dangerouslySetInnerHTML={{ __html: raw }}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
package/src/Swatch.jsx
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { ColorSwatch } from '@kolkrabbi/kol-component'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Swatch — the DOCUMENTED swatch:
|
|
5
|
-
* optional canonical-anchor dot.
|
|
4
|
+
* Swatch — the DOCUMENTED swatch: a specimen bar over its meta row (name, hex),
|
|
5
|
+
* with an optional canonical-anchor dot.
|
|
6
6
|
*
|
|
7
|
-
* `ColorSwatch` (kol-component) is the chip alone — a fixed-size pressable
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `ColorSwatch` (kol-component) is the chip alone — a fixed-size pressable atom
|
|
8
|
+
* for paint bars and inspectors. A colour PAGE needs the specimen plus its label
|
|
9
|
+
* and value, which is the form both brand apps had built locally
|
|
10
10
|
* (`brand-book-mocks-two-consumers`, 2026-09-03: 23 lines, byte-identical in
|
|
11
11
|
* kol-client-olina and kol-website). This composes the atom rather than
|
|
12
12
|
* redrawing it, per the ticket.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* `.kol-swatch-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `kol-components-
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* GEOMETRY IS CARRIED THROUGH. The forks wore `.kol-swatch` /
|
|
15
|
+
* `.kol-swatch-chip` / `.kol-swatch-meta`, and those rules are real — they ship
|
|
16
|
+
* in **`@kolkrabbi/kol-framework`** (`kol-framework.css`, the "Swatch / Ramp
|
|
17
|
+
* (color specimens)" block), not in kol-theme: a 6px-gap column, a **96px-tall**
|
|
18
|
+
* chip at radius 4, and a row-wrapped meta line on the baseline. They are
|
|
19
|
+
* stragglers — `.kol-mood-tile-*` left that same block for
|
|
20
|
+
* `kol-theme/kol-components-styleguide.css` on 2026-07-10 and these did not.
|
|
21
|
+
*
|
|
22
|
+
* So this component draws that geometry ITSELF rather than wearing the classes.
|
|
23
|
+
* kol-framework is not a peer of kol-styleguide and must not become one — a
|
|
24
|
+
* component whose look depends on another package's stylesheet being imported
|
|
25
|
+
* is the defect `styleguide-barrel-is-unimportable` was filed about. A
|
|
26
|
+
* consumer swapping its fork onto this gets the same render with no new
|
|
27
|
+
* install, and `height` is the seam if a page wants a different specimen depth.
|
|
23
28
|
*
|
|
24
29
|
* <Swatch hex="#131316" name="Ink" anchor />
|
|
25
30
|
*
|
|
@@ -27,16 +32,20 @@ import { ColorSwatch } from '@kolkrabbi/kol-component'
|
|
|
27
32
|
* copy; `name` renders exactly as authored.
|
|
28
33
|
*
|
|
29
34
|
* @param {string} hex - The colour, e.g. '#131316' (required — it is both the paint and the printed value)
|
|
30
|
-
* @param {ReactNode} name - Label
|
|
35
|
+
* @param {ReactNode} name - Label beside the value; omitted, only the hex renders
|
|
31
36
|
* @param {boolean} anchor - Mark this stop as the palette's canonical anchor — a difference-blended dot centred on the chip (default: false)
|
|
32
|
-
* @param {number|
|
|
37
|
+
* @param {number|string} height - Specimen depth, the forks' `.kol-swatch-chip` height (default: 96)
|
|
33
38
|
* @param {string} className - Extra classes on the wrapper
|
|
34
39
|
*/
|
|
35
|
-
export default function Swatch({ hex, name, anchor = false,
|
|
40
|
+
export default function Swatch({ hex, name, anchor = false, height = 96, className = '' }) {
|
|
36
41
|
return (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
/* gap 6 — .kol-swatch */
|
|
43
|
+
<div className={`flex flex-col ${className}`.trim()} style={{ gap: 6 }}>
|
|
44
|
+
{/* .kol-swatch-chip: full column width, 96 tall, radius 4. ColorSwatch's
|
|
45
|
+
'stretch' size fills this box, so the atom owns the paint and the
|
|
46
|
+
frame while the specimen depth stays the page's decision. */}
|
|
47
|
+
<div className="relative w-full" style={{ height }}>
|
|
48
|
+
<ColorSwatch hex={hex} size="stretch" radius="sm" />
|
|
40
49
|
{anchor && (
|
|
41
50
|
<span
|
|
42
51
|
aria-label="Canonical anchor"
|
|
@@ -51,7 +60,11 @@ export default function Swatch({ hex, name, anchor = false, size = 'fill', class
|
|
|
51
60
|
</span>
|
|
52
61
|
)}
|
|
53
62
|
</div>
|
|
54
|
-
|
|
63
|
+
{/* .kol-swatch-meta: row, space-between, baseline, wrapping */}
|
|
64
|
+
<div
|
|
65
|
+
className="kol-helper-10 flex flex-row flex-wrap items-baseline justify-between"
|
|
66
|
+
style={{ gap: '4px 12px' }}
|
|
67
|
+
>
|
|
55
68
|
{name && <span className="text-meta">{name}</span>}
|
|
56
69
|
<span className="text-strong font-semibold">{hex.toUpperCase()}</span>
|
|
57
70
|
</div>
|
package/src/brandBook.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brandBook — the brand tool's section registry and the manifest helpers behind it.
|
|
3
|
+
*
|
|
4
|
+
* The brand book as it ran in kol-olina's apps/brand (itself kol-website's apps/brand, copied
|
|
5
|
+
* 2026-09-02): two scrolling pages — BRAND documents the identity, ASSETS holds what you download
|
|
6
|
+
* or reproduce — each a stack of sections. The section ids, eyebrows, titles and ledes below are
|
|
7
|
+
* that app's, verbatim; anything that was the CLIENT's copy (About, Tone, Look, the logo concept,
|
|
8
|
+
* the colour concept) is not here — it comes from the manifest's `book` field, section by section
|
|
9
|
+
* (`@kolkrabbi/kol-brand-template` schema). A manifest key overrides the default of the same name.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* `nav` is the sidebar / rail label; a section without one (the hero, the overviews) is not indexed.
|
|
13
|
+
* `copy` marks a section that is ONLY its copy — with none in the manifest it is not rendered. */
|
|
14
|
+
export const BOOK_SECTIONS = [
|
|
15
|
+
{ key: 'hero', id: 'hero', label: 'brand guidelines', lede: 'Client-facing identity guidelines — chapter-structured for handoff. Mirrors the shape of the deliverable PDF.' },
|
|
16
|
+
{ 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.' },
|
|
17
|
+
{ key: 'about', id: 'about', nav: 'About', label: '01 — about', copy: true },
|
|
18
|
+
{ key: 'tone', id: 'voice', nav: 'Tone', label: '02 — tone', title: 'Tone', copy: true },
|
|
19
|
+
{ key: 'look', id: 'look', nav: 'Look', label: '03 — look', title: 'Look', copy: true },
|
|
20
|
+
{ key: 'logo', id: 'logos-concept', nav: 'Logo', label: '04 — logos · concept', title: 'The mark', marks: ['wordmark', 'logomark'] },
|
|
21
|
+
{ 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'] },
|
|
22
|
+
{ key: 'color', id: 'color', nav: 'Color', label: '06 — color', title: 'Palette' },
|
|
23
|
+
{ key: 'typography', id: 'typography', nav: 'Typography', label: '07 — typography', title: 'Type' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export const ASSET_SECTIONS = [
|
|
27
|
+
{ 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.' },
|
|
28
|
+
{ key: 'logos', id: 'logos', nav: 'Logos', label: '01 — logos', title: 'Logos', lede: 'Every mark in src/brand/logos/svg/. Click a row to open the overlay; toggle the color dot to swap ink vs surface; download recolored on the fly.' },
|
|
29
|
+
{ key: 'branded', id: 'branded-assets', nav: 'Branded', label: '04 — branded', title: 'Branded assets', lede: 'Stationery. Mocks live in src/components/styleguide/StationeryMocks.jsx and render in /styleguide chapter 6.' },
|
|
30
|
+
{ 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.' },
|
|
31
|
+
{ 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.' },
|
|
32
|
+
{ 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.' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
/* the two pages, for a view switch */
|
|
36
|
+
export const BRAND_VIEWS = [
|
|
37
|
+
{ value: 'brand', label: 'Brand' },
|
|
38
|
+
{ value: 'assets', label: 'Assets' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const hasCopy = (s) => Boolean(s.lede || s.blocks?.length || s.after?.length)
|
|
42
|
+
|
|
43
|
+
/* Whether a section has anything to show for this manifest. Copy-only sections need copy; the
|
|
44
|
+
* specimen sections need the data they render. */
|
|
45
|
+
function shows(s, brand) {
|
|
46
|
+
if (s.copy) return hasCopy(s)
|
|
47
|
+
if (s.key === 'color') return Boolean(brand?.ramps?.length) || hasCopy(s)
|
|
48
|
+
if (s.key === 'logos') return Boolean(brand?.logos?.length)
|
|
49
|
+
if (s.key === 'social') return Boolean(brand?.social?.templates?.length)
|
|
50
|
+
if (s.key === 'profile') return Boolean(brand?.social?.avatars?.length)
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One page's sections for a manifest — defaults merged with `brand.book`, the empty ones dropped.
|
|
55
|
+
* `[{ key, id, nav?, label, title, lede, blocks, after, marks }]` */
|
|
56
|
+
export function brandSections(brand, page = 'brand') {
|
|
57
|
+
const defaults = page === 'assets' ? ASSET_SECTIONS : BOOK_SECTIONS
|
|
58
|
+
const name = brand?.meta?.name
|
|
59
|
+
return defaults
|
|
60
|
+
.map((d) => {
|
|
61
|
+
const s = { ...d, ...(brand?.book?.[d.key] ?? {}) }
|
|
62
|
+
if (d.key === 'hero' && !s.title) s.title = name
|
|
63
|
+
if (d.key === 'about' && !s.title && name) s.title = `About ${name}`
|
|
64
|
+
return s
|
|
65
|
+
})
|
|
66
|
+
.filter((s) => shows(s, brand))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A page's rail / index entries — `[{ id, label }]`. */
|
|
70
|
+
export const brandToc = (brand, page) =>
|
|
71
|
+
brandSections(brand, page).filter((s) => s.nav).map((s) => ({ id: s.id, label: s.nav }))
|
|
72
|
+
|
|
73
|
+
/** The manifest's `meta` in the shape the stationery mocks read (`DEFAULT_BRAND_INFO`). A field the
|
|
74
|
+
* manifest does not carry is an EMPTY STRING, so the mocks render it blank rather than falling back
|
|
75
|
+
* to their placeholder values. */
|
|
76
|
+
export function brandInfo(brand) {
|
|
77
|
+
const m = brand?.meta ?? {}
|
|
78
|
+
const loc = m.location ?? {}
|
|
79
|
+
return {
|
|
80
|
+
identity: { founder: m.founder ?? '', role: m.role ?? '', established: m.founded ?? '', name: m.name ?? '', nameShort: m.nameShort ?? '' },
|
|
81
|
+
contact: { email: m.email ?? '', phone: m.phone ?? '', web: (m.url ?? '').replace(/^https?:\/\//, '').replace(/\/$/, '') },
|
|
82
|
+
social: { instagram: m.socials?.instagram ?? '', youtube: m.socials?.youtube ?? '', tiktok: m.socials?.tiktok ?? '' },
|
|
83
|
+
studio: { street: loc.street ?? '', postcode: loc.postcode ?? '', country: loc.country ?? '', city: loc.city ?? '', locShort: loc.locShort ?? '' },
|
|
84
|
+
legal: { entity: m.legalName ?? '', kt: '', vat: '' },
|
|
85
|
+
labels: { madeIn: '', handmade: '', handBy: '', manifesto: '' },
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* which logo each stationery mock carries — the variants olina's brandBook.jsx drew */
|
|
90
|
+
export const STATIONERY_MARKS = { card: 'lockup-hori', envelope: 'logomark', letter: 'wordmark', signature: 'logomark', avatar: 'logomark' }
|
|
91
|
+
|
|
92
|
+
/* The Branded table — the five stationery pieces the mocks draw. */
|
|
93
|
+
export const BRANDED_ASSET_ROWS = [
|
|
94
|
+
{ item: 'Business card · front', aspect: '85.6 / 53.85 (ID-1)', surface: 'paper', status: 'mocked', note: 'StationeryMocks.BusinessCardFront' },
|
|
95
|
+
{ item: 'Business card · back', aspect: '85.6 / 53.85', surface: 'ink', status: 'mocked', note: 'StationeryMocks.BusinessCardBack' },
|
|
96
|
+
{ item: 'Envelope · DL', aspect: '220 / 110', surface: 'paper', status: 'mocked', note: 'StationeryMocks.Envelope' },
|
|
97
|
+
{ item: 'Letterhead · A4', aspect: '210 / 297', surface: 'paper', status: 'mocked', note: 'StationeryMocks.Letterhead' },
|
|
98
|
+
{ item: 'Email signature', aspect: 'free', surface: 'paper', status: 'mocked', note: 'StationeryMocks.EmailSignature' },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
/** An in-page anchor click that scrolls instead of setting the hash — the host may route on the hash
|
|
102
|
+
* (media-shell does; apps/brand keeps its page there), so an anchor must not replace it. */
|
|
103
|
+
export function scrollToAnchor(e) {
|
|
104
|
+
e.preventDefault()
|
|
105
|
+
const id = e.currentTarget.getAttribute('href')?.slice(1)
|
|
106
|
+
if (id) document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* viewBox → "W × H" */
|
|
110
|
+
export const svgDims = (raw) => {
|
|
111
|
+
const m = /viewBox="[\d.-]+\s+[\d.-]+\s+([\d.]+)\s+([\d.]+)"/.exec(raw ?? '')
|
|
112
|
+
return m ? `${Math.round(+m[1])} × ${Math.round(+m[2])}` : undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* `currentColor` → the resolved ink the table hands over, into a Blob, so a white mark downloads white. */
|
|
116
|
+
export function downloadRecolored(raw, color, filename) {
|
|
117
|
+
const blob = new Blob([raw.replace(/currentColor/gi, color)], { type: 'image/svg+xml' })
|
|
118
|
+
const url = URL.createObjectURL(blob)
|
|
119
|
+
const a = document.createElement('a')
|
|
120
|
+
a.href = url
|
|
121
|
+
a.download = filename
|
|
122
|
+
document.body.appendChild(a)
|
|
123
|
+
a.click()
|
|
124
|
+
a.remove()
|
|
125
|
+
URL.revokeObjectURL(url)
|
|
126
|
+
}
|
package/src/index.js
CHANGED
|
@@ -42,3 +42,10 @@ export {
|
|
|
42
42
|
// that wanted one logo card could not build. Import them from their own homes:
|
|
43
43
|
// AssetGrid, FeatureSplit, ProsePreview, SpectrumGrid → @kolkrabbi/kol-component
|
|
44
44
|
// TypeSample, TypeSpecCard → @kolkrabbi/kol-foundry
|
|
45
|
+
|
|
46
|
+
// The brand tool (brand-as-a-tool, 2026-09-27): the brand book's two pages over one manifest —
|
|
47
|
+
// kol-olina's apps/brand, carried in. `Brand` is the tool; the pages render alone too.
|
|
48
|
+
export { default as Brand } from './Brand.jsx'
|
|
49
|
+
export { default as BrandBook } from './BrandBook.jsx'
|
|
50
|
+
export { default as BrandAssets } from './BrandAssets.jsx'
|
|
51
|
+
export { brandSections, brandToc, brandInfo, BRAND_VIEWS, BOOK_SECTIONS, ASSET_SECTIONS } from './brandBook.js'
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
}
|