@kolkrabbi/kol-styleguide 0.15.0 → 0.17.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 +9 -8
- package/src/ColorLab.jsx +49 -15
- package/src/ComboLab.jsx +11 -15
- package/src/DocumentIndex.jsx +10 -13
- package/src/DocumentLibrary.jsx +113 -0
- package/src/LockupBuilder.jsx +6 -56
- package/src/LockupLab.jsx +61 -0
- package/src/TypeLab.jsx +108 -0
- package/src/TypeScaleLab.jsx +6 -67
- package/src/colorLab.js +72 -9
- package/src/comboLab.js +34 -0
- package/src/index.js +14 -7
- package/src/lab/index.js +22 -0
- package/src/typeLab.js +125 -0
- package/src/typeScale.js +0 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-styleguide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Brand guide components and the brand tool",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,23 +9,24 @@
|
|
|
9
9
|
"module": "./src/index.js",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.js",
|
|
12
|
+
"./lab": "./src/lab/index.js",
|
|
12
13
|
"./src/*": "./src/*"
|
|
13
14
|
},
|
|
14
15
|
"peerDependencies": {
|
|
15
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
16
|
-
"@kolkrabbi/kol-framework": ">=0.
|
|
16
|
+
"@kolkrabbi/kol-component": ">=0.315.0",
|
|
17
|
+
"@kolkrabbi/kol-framework": ">=0.64.0",
|
|
17
18
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
18
19
|
"@kolkrabbi/kol-print": ">=0.1.0",
|
|
19
|
-
"@kolkrabbi/kol-theme": ">=0.
|
|
20
|
+
"@kolkrabbi/kol-theme": ">=0.198.0",
|
|
20
21
|
"react": "^18.3.0 || ^19.0.0",
|
|
21
22
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@kolkrabbi/kol-component": "^0.
|
|
25
|
-
"@kolkrabbi/kol-framework": "^0.62.4",
|
|
26
|
-
"@kolkrabbi/kol-icons": "^0.52.0",
|
|
25
|
+
"@kolkrabbi/kol-component": "^0.316.0",
|
|
27
26
|
"@kolkrabbi/kol-print": "^0.1.1",
|
|
28
|
-
"@kolkrabbi/kol-
|
|
27
|
+
"@kolkrabbi/kol-framework": "^0.64.0",
|
|
28
|
+
"@kolkrabbi/kol-icons": "^0.52.0",
|
|
29
|
+
"@kolkrabbi/kol-theme": "^0.199.0"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"src",
|
package/src/ColorLab.jsx
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { ColorPickerPopover, Dropdown, HARMONIES, PaletteHarmonyWheel, ToggleSwitch, hslToHex } from '@kolkrabbi/kol-component'
|
|
3
|
-
import { COLOR_MIX, COLOR_ROLES, COLOR_SLOTS, DEFAULT_COLOR_LAB,
|
|
4
|
-
import { fgOn } from './comboMath.js'
|
|
3
|
+
import { COLOR_MIX, COLOR_ROLES, COLOR_SLOTS, DEFAULT_COLOR_LAB, INK_LEVELS, colorLab, inkOn, rgbText } from './colorLab.js'
|
|
5
4
|
|
|
6
5
|
/* taxonomy-ok: organism — the color builder: harmony wheel, roles, slots, palette, ramps and the 60/30/10 strip over one value */
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
* ColorLab —
|
|
8
|
+
* ColorLab — A brand's colour, generated from one base colour.
|
|
10
9
|
*
|
|
11
10
|
* Carried from kol-atlas's Color page (2026-10-20). Six sections over ONE value: Harmony (base +
|
|
12
11
|
* rule on KOL's wheel), Roles (the harmony's five), Brand slots (four, two dark two light), Palette
|
|
13
12
|
* (ten hues 36° apart + blue-grey and grey ramps), Ramps (five blend steps per hue) and Composition
|
|
14
13
|
* (60/30/10). Select a slot or a composition share, then click any swatch to fill it.
|
|
15
14
|
*
|
|
16
|
-
* The math is `colorLab.js` (`
|
|
15
|
+
* The math is `colorLab.js` (`colorLab`, `colorLabCss`), so a consumer can save the value into
|
|
17
16
|
* a brand and emit it as CSS without rendering the lab.
|
|
18
17
|
*
|
|
18
|
+
* A COLOUR DEFINITION (BrandLabToolGaps, kol-atlas 2026-10-29): every swatch's info carries its contrast against the
|
|
19
|
+
* brand's own ink (ratio and WCAG grade); Ink shows the brand's text levels on its light and dark grounds; Dark shows
|
|
20
|
+
* the roles, slots and composition as the dark theme takes them.
|
|
21
|
+
*
|
|
19
22
|
* @param {Object} value `{ base, harmony, blueBase, greyBase, blueLinked, slots, picks }` — controlled; omit for internal
|
|
20
23
|
* @param {Function} onChange `(next) => void`
|
|
21
24
|
* @param {string} className classes on the root
|
|
@@ -26,14 +29,18 @@ const PICKER = '[&>*]:!size-3'
|
|
|
26
29
|
/* ten columns capped so the 5-row ramp fits the window — a swatch is the same size in every section */
|
|
27
30
|
const ROW_MAX = 'calc((100vh - 200px) * 2)'
|
|
28
31
|
const isHex = (hex) => /^#[0-9a-f]{6}/i.test(hex)
|
|
32
|
+
/* a ratio floored, never rounded — 4.497 must not read as 4.5 beside a grade that says it misses AA */
|
|
33
|
+
const ratioText = (r) => (Math.floor(r * 10) / 10).toFixed(1)
|
|
29
34
|
|
|
30
|
-
function Swatch({ hex, name, selected, onClick, info }) {
|
|
35
|
+
function Swatch({ hex, name, selected, onClick, info, inks, ring }) {
|
|
36
|
+
const ink = inks ? inkOn(hex, inks) : null
|
|
31
37
|
return (
|
|
32
|
-
<button type="button" onClick={onClick} title={`${name} ${hex}`} className={`group relative aspect-square cursor-pointer overflow-hidden rounded ${selected ? 'ring-2 ring-[var(--kol-accent-primary)]' : ''}`} style={{ background: hex }}>
|
|
38
|
+
<button type="button" onClick={onClick} title={`${name} ${hex}`} className={`group relative aspect-square cursor-pointer overflow-hidden rounded ${selected ? 'ring-2 ring-[var(--kol-accent-primary)]' : ''}`} style={{ background: hex, boxShadow: ring && !selected ? `inset 0 0 0 1px ${ring}` : undefined }}>
|
|
33
39
|
<span className={`absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-surface-primary px-1.5 py-1 text-left kol-mono-10 leading-tight transition-opacity ${info ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}>
|
|
34
40
|
<span className="truncate text-emphasis">{name}</span>
|
|
35
41
|
<span className="truncate text-meta">{hex}</span>
|
|
36
42
|
<span className="truncate text-meta">{rgbText(hex)}</span>
|
|
43
|
+
{ink && <span className="truncate text-meta">{ratioText(ink.ratio)} · {ink.grade}</span>}
|
|
37
44
|
</span>
|
|
38
45
|
</button>
|
|
39
46
|
)
|
|
@@ -63,9 +70,9 @@ export default function ColorLab({ value: valueProp, onChange, className = '' })
|
|
|
63
70
|
const value = valueProp ?? own
|
|
64
71
|
const set = (patch) => { const next = { ...value, ...patch }; if (valueProp === undefined) setOwn(next); onChange?.(next) }
|
|
65
72
|
const [slot, setSlot] = useState(null)
|
|
66
|
-
const [info, setInfo] = useState({ roles: false, brand: false, palette: false, ramps: false, mix: false })
|
|
73
|
+
const [info, setInfo] = useState({ roles: false, brand: false, palette: false, ramps: false, mix: false, dark: false })
|
|
67
74
|
const infoFor = (k) => ({ info: info[k], onInfo: (v) => setInfo((x) => ({ ...x, [k]: v })) })
|
|
68
|
-
const c =
|
|
75
|
+
const c = colorLab(value)
|
|
69
76
|
|
|
70
77
|
/* a swatch click fills the selected slot (0–3) or composition share (4–6) */
|
|
71
78
|
const assign = (hex) => {
|
|
@@ -83,26 +90,26 @@ export default function ColorLab({ value: valueProp, onChange, className = '' })
|
|
|
83
90
|
<div className="flex flex-wrap items-start gap-x-8 gap-y-4">
|
|
84
91
|
<div className="flex flex-col gap-2"><span className={LABEL}>Base</span><span className="flex h-8 items-center gap-2"><ColorPickerPopover size="sm" className={PICKER} color={c.base} onChange={(hex) => isHex(hex) && set({ base: hex.slice(0, 7).toUpperCase() })} label="Base color" /><span className="kol-mono-10 text-meta">{c.base}</span></span></div>
|
|
85
92
|
<div className="flex flex-col gap-2"><span className={LABEL}>Harmony</span><Dropdown size="sm" value={c.harmony} onChange={(v) => set({ harmony: v })} options={HARMONIES.map((x) => ({ value: x.id, label: x.label }))} /></div>
|
|
86
|
-
<div className="flex flex-col gap-2"><span className={LABEL}>Info</span><span className="flex h-8 items-center"><ToggleSwitch size="sm" className={BARE_SWITCH} checked={Object.values(info).every(Boolean)} onChange={(v) => setInfo({ roles: v, brand: v, palette: v, ramps: v, mix: v })} /></span></div>
|
|
93
|
+
<div className="flex flex-col gap-2"><span className={LABEL}>Info</span><span className="flex h-8 items-center"><ToggleSwitch size="sm" className={BARE_SWITCH} checked={Object.values(info).every(Boolean)} onChange={(v) => setInfo({ roles: v, brand: v, palette: v, ramps: v, mix: v, dark: v })} /></span></div>
|
|
87
94
|
</div>
|
|
88
95
|
<PaletteHarmonyWheel size={220} hue={c.h} harmony={c.harmony} saturation={c.s} lightness={c.l} onHueChange={(hh) => set({ base: hslToHex(hh, c.s, c.l).toUpperCase() })} />
|
|
89
96
|
</Section>
|
|
90
97
|
|
|
91
98
|
<Section title="Roles" {...infoFor('roles')}>
|
|
92
|
-
<Row>{c.roles.map((hex, i) => <Swatch key={i} info={info.roles} hex={hex} name={COLOR_ROLES[i]} onClick={() => assign(hex)} />)}</Row>
|
|
99
|
+
<Row>{c.roles.map((hex, i) => <Swatch inks={c.inks} key={i} info={info.roles} hex={hex} name={COLOR_ROLES[i]} onClick={() => assign(hex)} />)}</Row>
|
|
93
100
|
</Section>
|
|
94
101
|
|
|
95
102
|
<Section title="Brand slots" {...infoFor('brand')}>
|
|
96
|
-
<Row>{c.slots.map((hex, i) => <Swatch key={i} info={info.brand} hex={hex} name={COLOR_SLOTS[i]} selected={slot === i} onClick={() => toggleSlot(i)} />)}</Row>
|
|
103
|
+
<Row>{c.slots.map((hex, i) => <Swatch inks={c.inks} key={i} info={info.brand} hex={hex} name={COLOR_SLOTS[i]} selected={slot === i} onClick={() => toggleSlot(i)} />)}</Row>
|
|
97
104
|
</Section>
|
|
98
105
|
|
|
99
106
|
<Section title="Palette" {...infoFor('palette')} extra={<Switch label="Link blue" checked={c.blueLinked} onChange={(v) => set({ blueLinked: v })} />}>
|
|
100
|
-
<Row>{c.hues.map((x) => <Swatch key={x.name + x.hex} info={info.palette} hex={x.hex} name={x.name} selected={picked(x.hex)} onClick={() => assign(x.hex)} />)}</Row>
|
|
107
|
+
<Row>{c.hues.map((x) => <Swatch inks={c.inks} key={x.name + x.hex} info={info.palette} hex={x.hex} name={x.name} selected={picked(x.hex)} onClick={() => assign(x.hex)} />)}</Row>
|
|
101
108
|
{[['Blue-grey', c.blueGrey, c.blueBase, 'blueBase', true], ['Grey', c.grey, c.greyBase, 'greyBase', false]].map(([row, ramp, rb, key, linkable]) => (
|
|
102
109
|
<div key={row} className="mt-4 flex flex-col gap-2">
|
|
103
110
|
<span className="flex h-4 items-center gap-3"><span className={`${LABEL} w-[120px]`}>{row} base</span>
|
|
104
111
|
{!(linkable && c.blueLinked) && <><ColorPickerPopover size="sm" className={PICKER} color={rb} onChange={(hex) => isHex(hex) && set({ [key]: hex.slice(0, 7).toUpperCase() })} label={`${row} base`} /><span className="kol-mono-10 text-meta">{rb}</span></>}</span>
|
|
105
|
-
<Row>{ramp.map((hex, i) => <Swatch key={row + i} info={info.palette} hex={hex} name={`${row} ${(i + 1) * 100}`} selected={picked(hex)} onClick={() => assign(hex)} />)}</Row>
|
|
112
|
+
<Row>{ramp.map((hex, i) => <Swatch inks={c.inks} key={row + i} info={info.palette} hex={hex} name={`${row} ${(i + 1) * 100}`} selected={picked(hex)} onClick={() => assign(hex)} />)}</Row>
|
|
106
113
|
</div>
|
|
107
114
|
))}
|
|
108
115
|
</Section>
|
|
@@ -111,7 +118,7 @@ export default function ColorLab({ value: valueProp, onChange, className = '' })
|
|
|
111
118
|
<Row>
|
|
112
119
|
{[0, 1, 2, 3, 4].flatMap((step) => c.ramps.map((hue) => {
|
|
113
120
|
const hex = hue.steps[step]
|
|
114
|
-
return <Swatch key={hue.hex + step} info={info.ramps} hex={hex} name={`${hue.name} ${(step + 1) * 100}`} selected={picked(hex)} onClick={() => assign(hex)} />
|
|
121
|
+
return <Swatch inks={c.inks} key={hue.hex + step} info={info.ramps} hex={hex} name={`${hue.name} ${(step + 1) * 100}`} selected={picked(hex)} onClick={() => assign(hex)} />
|
|
115
122
|
}))}
|
|
116
123
|
</Row>
|
|
117
124
|
</Section>
|
|
@@ -120,7 +127,7 @@ export default function ColorLab({ value: valueProp, onChange, className = '' })
|
|
|
120
127
|
<div style={{ maxWidth: ROW_MAX }}>
|
|
121
128
|
<div className="flex h-40 overflow-hidden rounded">
|
|
122
129
|
{COLOR_MIX.map(([share, label], i) => (
|
|
123
|
-
<button key={label} type="button" onClick={() => toggleSlot(4 + i)} title={`${label} ${c.mix[i]}`} className={`flex min-w-0 cursor-pointer flex-col justify-between p-3 text-left transition-colors ${slot === 4 + i ? 'ring-2 ring-inset ring-[var(--kol-accent-primary)]' : ''}`} style={{ flex: share, background: c.mix[i], color:
|
|
130
|
+
<button key={label} type="button" onClick={() => toggleSlot(4 + i)} title={`${label} ${c.mix[i]}`} className={`flex min-w-0 cursor-pointer flex-col justify-between p-3 text-left transition-colors ${slot === 4 + i ? 'ring-2 ring-inset ring-[var(--kol-accent-primary)]' : ''}`} style={{ flex: share, background: c.mix[i], color: inkOn(c.mix[i], c.inks).ink }}>
|
|
124
131
|
<span className="kol-sans-heading-03 leading-none">{share * 10}</span>
|
|
125
132
|
<span className={`truncate kol-mono-10 leading-tight transition-opacity ${info.mix ? 'opacity-100' : 'opacity-0'}`}>{c.mix[i]}</span>
|
|
126
133
|
</button>
|
|
@@ -139,6 +146,33 @@ export default function ColorLab({ value: valueProp, onChange, className = '' })
|
|
|
139
146
|
</div>
|
|
140
147
|
</div>
|
|
141
148
|
</Section>
|
|
149
|
+
|
|
150
|
+
{/* THE BRAND'S INK: its text levels on its own grounds, light and dark, each with its contrast */}
|
|
151
|
+
<Section title="Ink">
|
|
152
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2" style={{ maxWidth: ROW_MAX }}>
|
|
153
|
+
{[['Light', c.slots[2], c.ink.light, c.inkGrades.light], ['Dark', c.dark.slots[2], c.ink.dark, c.inkGrades.dark]].map(([mode, ground, ink, grades]) => (
|
|
154
|
+
<div key={mode} className="flex flex-col gap-2 rounded p-4" style={{ background: ground }}>
|
|
155
|
+
{INK_LEVELS.map(([id]) => (
|
|
156
|
+
<span key={id} className="flex items-baseline justify-between gap-3" style={{ color: ink[id] }}>
|
|
157
|
+
<span className="kol-sans-body-02">{mode} · {id}</span>
|
|
158
|
+
<span className="kol-mono-10">{ink[id]} · {ratioText(grades[id].ratio)} {grades[id].grade}</span>
|
|
159
|
+
</span>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</Section>
|
|
165
|
+
|
|
166
|
+
{/* THE DARK THEME: the slots swapped, each role moved along its ramp until it passes AA on the dark ground */}
|
|
167
|
+
<Section title="Dark" {...infoFor('dark')}>
|
|
168
|
+
<div className="flex flex-col gap-4 rounded p-4" style={{ background: c.dark.slots[2], maxWidth: ROW_MAX }}>
|
|
169
|
+
<Row>{c.dark.roles.map((hex, i) => <Swatch ring={`${c.inks.light}40`} inks={c.inks} key={`dr${i}`} info={info.dark} hex={hex} name={COLOR_ROLES[i]} />)}</Row>
|
|
170
|
+
<Row>{c.dark.slots.map((hex, i) => <Swatch ring={`${c.inks.light}40`} inks={c.inks} key={`ds${i}`} info={info.dark} hex={hex} name={COLOR_SLOTS[i]} />)}</Row>
|
|
171
|
+
<div className="flex h-16 overflow-hidden rounded border" style={{ borderColor: `${c.inks.light}40` }}>
|
|
172
|
+
{COLOR_MIX.map(([share, label], i) => <span key={label} style={{ flex: share, background: c.dark.mix[i] }} title={`${label} ${c.dark.mix[i]}`} />)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</Section>
|
|
142
176
|
</div>
|
|
143
177
|
)
|
|
144
178
|
}
|
package/src/ComboLab.jsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { ViewToggle } from '@kolkrabbi/kol-component'
|
|
3
3
|
import { LAYOUT_COMPONENTS, COMBO_LAYOUTS } from './comboLayouts.jsx'
|
|
4
4
|
import { generatePalette } from './comboMath.js'
|
|
5
|
+
import { DEFAULT_COMBO_LAB } from './comboLab.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ComboLab — the 60 / 30 / 10 color-combination playground.
|
|
@@ -20,6 +21,8 @@ import { generatePalette } from './comboMath.js'
|
|
|
20
21
|
* `palettes` nor `pool`, Randomize derives a fresh palette from
|
|
21
22
|
* the current primary (seed mode).
|
|
22
23
|
* - `logo` — a React node dropped into the logo slots.
|
|
24
|
+
* - `onChange` — `(palette) => void` when Randomize or the Palette toggle picks one (the lab family's
|
|
25
|
+
* one value — `comboLabCss(palette)` emits it; BrandLabToolGaps 2026-10-29).
|
|
23
26
|
*
|
|
24
27
|
* Chrome (borders, type, state) is DS-owned via `.kol-combo-*` +
|
|
25
28
|
* `kol-helper-*` + `text-*` utilities; the only inline values are the palette's
|
|
@@ -28,19 +31,9 @@ import { generatePalette } from './comboMath.js'
|
|
|
28
31
|
* See the monorepo severance notes in lobby/ComboLab.md.
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
|
-
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export const DEFAULT_PALETTE = {
|
|
35
|
-
id: 'kolkrabbi',
|
|
36
|
-
label: 'Kolkrabbi',
|
|
37
|
-
description: 'Yellow hero · rust secondary · navy ink on cream — the canonical composition',
|
|
38
|
-
primary: '#FFCF33',
|
|
39
|
-
secondary: '#AD5038',
|
|
40
|
-
light: '#FAF7F0',
|
|
41
|
-
dark: '#222D3D',
|
|
42
|
-
accent: '#DF760B',
|
|
43
|
-
}
|
|
34
|
+
/* the canonical Kolkrabbi composition is comboLab.js's DEFAULT_COMBO_LAB; DEFAULT_PALETTE is its old name */
|
|
35
|
+
/** @deprecated 2026-10-29 — DEFAULT_COMBO_LAB */
|
|
36
|
+
export const DEFAULT_PALETTE = DEFAULT_COMBO_LAB
|
|
44
37
|
|
|
45
38
|
const ROLE_LABELS = ['Primary', 'Secondary', 'Light', 'Dark', 'Accent']
|
|
46
39
|
|
|
@@ -95,12 +88,15 @@ export default function ComboLab({
|
|
|
95
88
|
logo = null,
|
|
96
89
|
pool,
|
|
97
90
|
mode = 'random',
|
|
91
|
+
onChange,
|
|
98
92
|
className = '',
|
|
99
93
|
}) {
|
|
100
|
-
const initialPalette = palette ?? palettes?.[0] ??
|
|
94
|
+
const initialPalette = palette ?? palettes?.[0] ?? DEFAULT_COMBO_LAB
|
|
101
95
|
|
|
102
96
|
const [layoutId, setLayoutId] = useState(layout ?? layouts[0]?.id ?? COMBO_LAYOUTS[0].id)
|
|
103
|
-
const [active,
|
|
97
|
+
const [active, setOwn] = useState(initialPalette)
|
|
98
|
+
/* the palette reported as it changes, so a draft can keep it (the lab family's one-value contract, 2026-10-29) */
|
|
99
|
+
const setActive = (p) => { setOwn(p); onChange?.(p) }
|
|
104
100
|
const [logoOn, setLogoOn] = useState(Boolean(logo))
|
|
105
101
|
|
|
106
102
|
const LayoutComponent = LAYOUT_COMPONENTS[layoutId] ?? LAYOUT_COMPONENTS[COMBO_LAYOUTS[0].id]
|
package/src/DocumentIndex.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { FileQuickLook, PageHeader, KIND_LABEL } from '@kolkrabbi/kol-component'
|
|
3
3
|
import { Contents } from './BrandGuide.jsx'
|
|
4
4
|
|
|
5
5
|
/* DOCUMENT INDEX — a folder's pages as the brand book's contents cards (BrandGuide's `Contents`, routed), grouped by
|
|
@@ -24,22 +24,19 @@ export default function DocumentIndex({ rows, title, lede, onNavigate }) {
|
|
|
24
24
|
const index = rows.map((r) => (r.file
|
|
25
25
|
? { ...r, href: r.file, meta: r.meta ?? fileLabel(r), onClick: (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; e.preventDefault(); setOpen(files.indexOf(r)) } }
|
|
26
26
|
: r))
|
|
27
|
-
const cur = open === null ? null : files[open]
|
|
28
|
-
const step = (d) => setOpen((i) => (i + d + files.length) % files.length)
|
|
29
27
|
return (
|
|
30
28
|
<div className="kol-page flex flex-col gap-6">
|
|
31
29
|
{title && <PageHeader title={title} subtitle={lede} size="sm" voice="mono" />}
|
|
32
30
|
<Contents index={index} onNavigate={onNavigate} />
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)}
|
|
31
|
+
{/* the DS Quick Look over the folder's files (kol-component FileQuickLook, 2026-10-29 — the composition this file drew
|
|
32
|
+
* by hand: dims, pages ‹ ›, a download) */}
|
|
33
|
+
<FileQuickLook
|
|
34
|
+
files={files.map((f) => ({ key: f.file.split('?')[0], name: f.title, url: f.file, type: f.kind === 'image' ? 'image/' : undefined }))}
|
|
35
|
+
index={open}
|
|
36
|
+
onIndex={setOpen}
|
|
37
|
+
onClose={() => setOpen(null)}
|
|
38
|
+
meta={(x) => fileLabel(files.find((f) => f.file === x.url))}
|
|
39
|
+
/>
|
|
43
40
|
</div>
|
|
44
41
|
)
|
|
45
42
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button, FileQuickLook, Table } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { MarkdownPage, PageSection, libraryLinks } from '@kolkrabbi/kol-framework'
|
|
4
|
+
import DocumentIndex from './DocumentIndex.jsx'
|
|
5
|
+
|
|
6
|
+
/* DOCUMENT LIBRARY — a `documentLibrary` (kol-framework) drawn: a sortable table per folder, the title opening the text,
|
|
7
|
+
* the originals opening in the DS Quick Look, the markdown downloading (DocumentLibraryAndBoardParts, kol-website
|
|
8
|
+
* 2026-10-29 — record's DocumentsIndex · FolderTable · Files · MdDownload, made the DS's so brand and record draw their
|
|
9
|
+
* documents the same way: *"I already asked for tables, and download and file preview"*).
|
|
10
|
+
*
|
|
11
|
+
* <DocumentLibrary library={lib} onNavigate={navigate} urlOf={(f) => f.url} /> every folder, a section each
|
|
12
|
+
* <DocumentLibrary library={lib} folder="press" … /> one folder
|
|
13
|
+
* <DocumentView library={lib} row={row} urlOf={…} resolveLink={…} assetUrl={…} /> the open document
|
|
14
|
+
*
|
|
15
|
+
* DocumentLibrary
|
|
16
|
+
* library `{ rows, folders }` from documentLibrary
|
|
17
|
+
* folder one folder's id; omitted, every folder
|
|
18
|
+
* columns the table's columns — an array of keys (`title · kind · date · lang · assets · markdown`) and/or Table column
|
|
19
|
+
* objects, or `(defaults) => columns` to drop, reorder or add
|
|
20
|
+
* view `table` (default) · `cards` (DocumentIndex's contents cards, for anyone who still wants them)
|
|
21
|
+
* onNavigate (to) => void — a title's click; the link is a real `href` too
|
|
22
|
+
* urlOf (file) => string — an original's URL (default `file.url`; record serves the bucket in prod and a mirror in dev)
|
|
23
|
+
*
|
|
24
|
+
* DocumentFiles — the Assets cell on its own: a button per original, labelled by extension, two of one kind told apart by
|
|
25
|
+
* language (`PDF IS`, `PDF EN`); each opens Quick Look over the row's files. `DocumentMarkdown` — the source as `<stem>.md`. */
|
|
26
|
+
const LANGS = ['is', 'en', 'de']
|
|
27
|
+
const ext = (f) => (f.key ?? f.name ?? '').split('.').pop().toUpperCase()
|
|
28
|
+
export const fileLabel = (files, f) => (files.filter((x) => ext(x) === ext(f)).length > 1
|
|
29
|
+
? `${ext(f)} ${(f.key ?? f.name).split('/').pop().split(/[-.]/).filter((t) => LANGS.includes(t)).join(' ').toUpperCase()}`.trim()
|
|
30
|
+
: ext(f))
|
|
31
|
+
const plain = (e) => !(e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
|
32
|
+
|
|
33
|
+
/* the one Quick Look a page holds: `open(files, index)` and the overlay to render */
|
|
34
|
+
export function useFileQuickLook(urlOf) {
|
|
35
|
+
const [look, setLook] = useState(null)
|
|
36
|
+
const overlay = look && <FileQuickLook files={look.files} index={look.index} onIndex={(index) => setLook({ ...look, index })} onClose={() => setLook(null)} urlOf={urlOf} />
|
|
37
|
+
return [(files, index) => setLook({ files, index }), overlay]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DocumentFiles({ row, onOpen }) {
|
|
41
|
+
if (!row.files?.length) return null
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-wrap gap-1">
|
|
44
|
+
{row.files.map((f, i) => <Button key={f.key} variant="nav" size="sm" onClick={() => onOpen(row.files, i)} title={f.name ?? f.key.split('/').pop()}>{fileLabel(row.files, f)}</Button>)}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DocumentMarkdown = ({ row }) => (
|
|
50
|
+
<Button variant="nav" size="sm" iconOnly="download" href={`data:text/markdown;charset=utf-8,${encodeURIComponent(row.source ?? '')}`} download={`${row.id.split('/').pop()}.md`} aria-label="Download markdown" />
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const DEFAULT_KEYS = ['title', 'kind', 'date', 'lang', 'assets', 'markdown']
|
|
54
|
+
|
|
55
|
+
function columnsFor({ onNavigate, onOpen }) {
|
|
56
|
+
return {
|
|
57
|
+
title: { header: 'Title', accessor: 'title', wrap: true, sortable: true, render: (r) => (
|
|
58
|
+
<a href={r.to} className="underline-offset-2 hover:underline" onClick={onNavigate ? (e) => { if (!plain(e)) return; e.preventDefault(); onNavigate(r.to) } : undefined}>{r.title}</a>
|
|
59
|
+
) },
|
|
60
|
+
kind: { header: 'Kind', accessor: 'kind', sortable: true },
|
|
61
|
+
date: { header: 'Date', accessor: 'date', sortable: true },
|
|
62
|
+
lang: { header: 'Language', accessor: 'lang' },
|
|
63
|
+
assets: { header: 'Assets', accessor: 'files', render: (r) => <DocumentFiles row={r} onOpen={onOpen} /> },
|
|
64
|
+
markdown: { header: 'Markdown', accessor: 'source', render: (r) => <DocumentMarkdown row={r} /> },
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function DocumentLibrary({ library, folder, columns, view = 'table', onNavigate, urlOf = (f) => f.url }) {
|
|
69
|
+
const [open, overlay] = useFileQuickLook(urlOf)
|
|
70
|
+
const named = columnsFor({ onNavigate, onOpen: open })
|
|
71
|
+
const defaults = DEFAULT_KEYS.map((k) => named[k])
|
|
72
|
+
const resolve = (c) => (typeof c === 'string' ? named[c] : c)
|
|
73
|
+
const cols = (typeof columns === 'function' ? columns(defaults) : columns ?? DEFAULT_KEYS).map(resolve).filter(Boolean)
|
|
74
|
+
const folders = folder != null ? library.folders.filter((f) => f.id === folder) : library.folders
|
|
75
|
+
|
|
76
|
+
if (view === 'cards') {
|
|
77
|
+
/* the contents cards: pages as cards in their folder's part, the originals as file cards (DocumentIndex opens them) */
|
|
78
|
+
const rows = folders.flatMap((f) => library.rows.filter((r) => r.folder === f.id).flatMap((r) => [
|
|
79
|
+
{ id: r.id, to: r.to, title: r.title, label: r.kind || undefined, part: f.label },
|
|
80
|
+
...(r.files ?? []).map((x) => ({ id: `${r.id}:${x.key}`, file: urlOf(x), title: x.name ?? x.key.split('/').pop(), part: f.label })),
|
|
81
|
+
]))
|
|
82
|
+
return <DocumentIndex rows={rows} onNavigate={onNavigate} />
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
{folders.map((f) => (
|
|
87
|
+
<PageSection key={f.id || 'root'} id={f.id || 'documents'} label={f.label}>
|
|
88
|
+
<Table columns={cols} rows={library.rows.filter((r) => r.folder === f.id)} width="column" variant="simple" />
|
|
89
|
+
</PageSection>
|
|
90
|
+
))}
|
|
91
|
+
{overlay}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* THE OPEN DOCUMENT: the same Assets and Markdown buttons above `MarkdownPage`, then `actions` — record's DocumentsPage.
|
|
97
|
+
* Every other prop goes to MarkdownPage (`resolveLink`, `assetUrl`, `toc`, `numbered`, …); `resolveLink` defaults to the
|
|
98
|
+
* library's own (`libraryLinks`, kol-framework) when the caller passes `library` and no resolver. */
|
|
99
|
+
export function DocumentView({ row, library, urlOf = (f) => f.url, actions, markdown = true, ...page }) {
|
|
100
|
+
const [open, overlay] = useFileQuickLook(urlOf)
|
|
101
|
+
if (library && !page.resolveLink) page.resolveLink = libraryLinks(library)
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<div className="kol-page flex items-center justify-end gap-1 pb-0">
|
|
105
|
+
<DocumentFiles row={row} onOpen={open} />
|
|
106
|
+
{markdown && <DocumentMarkdown row={row} />}
|
|
107
|
+
{actions}
|
|
108
|
+
</div>
|
|
109
|
+
<MarkdownPage key={row.id} source={row.source} {...page} />
|
|
110
|
+
{overlay}
|
|
111
|
+
</>
|
|
112
|
+
)
|
|
113
|
+
}
|
package/src/LockupBuilder.jsx
CHANGED
|
@@ -1,60 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Input, Label, SegmentedToggle } from '@kolkrabbi/kol-component'
|
|
3
|
-
import Lockup from './Lockup.jsx'
|
|
4
|
-
|
|
5
|
-
/* taxonomy-ok: organism — the live lockup builder: name / descriptor / variant / scale controls beside a Lockup stage, preset rows below */
|
|
1
|
+
import LockupLab from './LockupLab.jsx'
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Carried from kol-atlas `components/lockup/LiveLockup.jsx` (2026-10-20); the parent, the mark, the
|
|
12
|
-
* face and the presets are the brand's, passed in.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} parent the parent brand name
|
|
15
|
-
* @param {ReactNode} mark the mark (see Lockup)
|
|
16
|
-
* @param {string} font CSS font-family for the names
|
|
17
|
-
* @param {Object} presets `{ [level]: string[] }` — e.g. `{ Products: ['Hosting', …] }`
|
|
18
|
-
* @param {string} className classes on the root
|
|
4
|
+
* @deprecated 2026-10-29 — `LockupLab` (the lab family's name, BrandLabToolGaps). Same component, same props; drops when
|
|
5
|
+
* nobody imports it (04-retirements.md).
|
|
6
|
+
* LockupBuilder — A product lockup, generated from a name. The old name of LockupLab.
|
|
19
7
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const Field = ({ label, children }) => <div className="flex flex-col gap-2"><Label>{label}</Label>{children}</div>
|
|
24
|
-
|
|
25
|
-
export default function LockupBuilder({ parent = 'Brand', mark, font, presets = {}, className = '' }) {
|
|
26
|
-
const first = Object.values(presets)[0]?.[0] ?? ''
|
|
27
|
-
const [s, setS] = useState({ product: first, descriptor: '', descriptor2: '', variant: 'horizontal', scale: 'lg' })
|
|
28
|
-
const set = (patch) => setS((p) => ({ ...p, ...patch }))
|
|
29
|
-
const brand = { parent, mark, font }
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div className={`flex flex-col gap-6 ${className}`.trim()}>
|
|
33
|
-
<div className="grid gap-6 md:grid-cols-[300px_minmax(0,1fr)]">
|
|
34
|
-
<div className="flex flex-col gap-5 self-start rounded-lg border border-fg-08 p-5">
|
|
35
|
-
<Field label="Product name"><Input value={s.product} onChange={(e) => set({ product: e.target.value })} placeholder="e.g. Hosting" /></Field>
|
|
36
|
-
<Field label="Descriptor"><Input value={s.descriptor} onChange={(e) => set({ descriptor: e.target.value })} placeholder="e.g. IS, EU" /></Field>
|
|
37
|
-
{s.variant === 'hero' && <Field label="Descriptor 2"><Input value={s.descriptor2} onChange={(e) => set({ descriptor2: e.target.value })} placeholder="e.g. Solution" /></Field>}
|
|
38
|
-
<Field label="Variant"><SegmentedToggle size="sm" value={s.variant} onChange={(v) => set({ variant: v })} options={VARIANTS} /></Field>
|
|
39
|
-
<Field label="Scale"><SegmentedToggle size="sm" value={s.scale} onChange={(v) => set({ scale: v })} options={SCALES} /></Field>
|
|
40
|
-
</div>
|
|
41
|
-
<div className="flex min-h-[320px] items-center justify-center overflow-auto rounded-lg border border-fg-08 text-emphasis">
|
|
42
|
-
<Lockup {...brand} {...s} />
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
{Object.entries(presets).map(([group, names]) => (
|
|
46
|
-
<div key={group}>
|
|
47
|
-
<Label>{group}</Label>
|
|
48
|
-
<div className="mt-2 grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3">
|
|
49
|
-
{names.map((n) => (
|
|
50
|
-
<button key={n} type="button" onClick={() => set({ product: n, descriptor: '', variant: 'horizontal' })}
|
|
51
|
-
className="flex min-h-[96px] cursor-pointer items-center justify-center overflow-hidden rounded-lg border border-fg-08 text-left text-emphasis hover:bg-fg-04">
|
|
52
|
-
<Lockup {...brand} product={n} scale="sm" />
|
|
53
|
-
</button>
|
|
54
|
-
))}
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
)
|
|
8
|
+
export default function LockupBuilder(props) {
|
|
9
|
+
return <LockupLab {...props} />
|
|
60
10
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Input, Label, SegmentedToggle } from '@kolkrabbi/kol-component'
|
|
3
|
+
import Lockup from './Lockup.jsx'
|
|
4
|
+
|
|
5
|
+
/* taxonomy-ok: organism — the lockup generator: name / descriptor / variant / scale controls beside a Lockup stage, preset rows below */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* LockupLab — A product lockup, generated from a name. Was `LockupBuilder` — renamed into the lab family beside TypeLab,
|
|
9
|
+
* ColorLab and ComboLab (BrandLabToolGaps, 2026-10-29); the props are the same. Type a product name, add descriptors, pick a variant and scale; the presets run a
|
|
10
|
+
* brand's naming levels through the same Lockup.
|
|
11
|
+
*
|
|
12
|
+
* Carried from kol-atlas `components/lockup/LiveLockup.jsx` (2026-10-20); the parent, the mark, the
|
|
13
|
+
* face and the presets are the brand's, passed in.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} parent the parent brand name
|
|
16
|
+
* @param {ReactNode} mark the mark (see Lockup)
|
|
17
|
+
* @param {string} font CSS font-family for the names
|
|
18
|
+
* @param {Object} presets `{ [level]: string[] }` — e.g. `{ Products: ['Hosting', …] }`
|
|
19
|
+
* @param {string} className classes on the root
|
|
20
|
+
*/
|
|
21
|
+
const VARIANTS = [{ value: 'horizontal', label: 'Horizontal' }, { value: 'hero', label: 'Hero' }, { value: 'wordmark', label: 'Wordmark' }]
|
|
22
|
+
const SCALES = ['sm', 'md', 'lg', 'xl'].map((s) => ({ value: s, label: s.toUpperCase() }))
|
|
23
|
+
|
|
24
|
+
const Field = ({ label, children }) => <div className="flex flex-col gap-2"><Label>{label}</Label>{children}</div>
|
|
25
|
+
|
|
26
|
+
export default function LockupLab({ parent = 'Brand', mark, font, presets = {}, className = '' }) {
|
|
27
|
+
const first = Object.values(presets)[0]?.[0] ?? ''
|
|
28
|
+
const [s, setS] = useState({ product: first, descriptor: '', descriptor2: '', variant: 'horizontal', scale: 'lg' })
|
|
29
|
+
const set = (patch) => setS((p) => ({ ...p, ...patch }))
|
|
30
|
+
const brand = { parent, mark, font }
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`flex flex-col gap-6 ${className}`.trim()}>
|
|
34
|
+
<div className="grid gap-6 md:grid-cols-[300px_minmax(0,1fr)]">
|
|
35
|
+
<div className="flex flex-col gap-5 self-start rounded-lg border border-fg-08 p-5">
|
|
36
|
+
<Field label="Product name"><Input value={s.product} onChange={(e) => set({ product: e.target.value })} placeholder="e.g. Hosting" /></Field>
|
|
37
|
+
<Field label="Descriptor"><Input value={s.descriptor} onChange={(e) => set({ descriptor: e.target.value })} placeholder="e.g. IS, EU" /></Field>
|
|
38
|
+
{s.variant === 'hero' && <Field label="Descriptor 2"><Input value={s.descriptor2} onChange={(e) => set({ descriptor2: e.target.value })} placeholder="e.g. Solution" /></Field>}
|
|
39
|
+
<Field label="Variant"><SegmentedToggle size="sm" value={s.variant} onChange={(v) => set({ variant: v })} options={VARIANTS} /></Field>
|
|
40
|
+
<Field label="Scale"><SegmentedToggle size="sm" value={s.scale} onChange={(v) => set({ scale: v })} options={SCALES} /></Field>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex min-h-[320px] items-center justify-center overflow-auto rounded-lg border border-fg-08 text-emphasis">
|
|
43
|
+
<Lockup {...brand} {...s} />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{Object.entries(presets).map(([group, names]) => (
|
|
47
|
+
<div key={group}>
|
|
48
|
+
<Label>{group}</Label>
|
|
49
|
+
<div className="mt-2 grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3">
|
|
50
|
+
{names.map((n) => (
|
|
51
|
+
<button key={n} type="button" onClick={() => set({ product: n, descriptor: '', variant: 'horizontal' })}
|
|
52
|
+
className="flex min-h-[96px] cursor-pointer items-center justify-center overflow-hidden rounded-lg border border-fg-08 text-left text-emphasis hover:bg-fg-04">
|
|
53
|
+
<Lockup {...brand} product={n} scale="sm" />
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
package/src/TypeLab.jsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Dropdown, Input } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { DEFAULT_TYPE_LAB, TYPE_CASES, TYPE_FACES, TYPE_INKS, TYPE_RATIOS, TYPE_STEPS, typeLab } from './typeLab.js'
|
|
4
|
+
|
|
5
|
+
/* taxonomy-ok: organism — the type generator: base + ratio controls, a spec row and live sample per step, the roles table */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TypeLab — A brand's type, generated. A base size and a musical ratio set every step; each step's line height is the
|
|
9
|
+
* brand's to tune; sixteen roles sit on the steps, each with its face, weight, tracking, case and ink (`typeLab.js`).
|
|
10
|
+
* Was `TypeScaleLab` (carried from kol-atlas's Typography page, 2026-10-20), which stopped at sizes — renamed into the lab
|
|
11
|
+
* family and grown to a brand's whole type (BrandLabToolGaps, kol-atlas 2026-10-29). The faces are the BRAND's, passed in —
|
|
12
|
+
* the lab never sets KOL's type.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} value `{ base, ratio, leading, roles }` — controlled; omit for internal
|
|
15
|
+
* @param {Function} onChange `(next) => void`
|
|
16
|
+
* @param {Object} faces `{ display, heading, text, mono }` — `{ family, label, weight }` each
|
|
17
|
+
* @param {Object} samples `{ headline, text }` — the sample strings
|
|
18
|
+
* @param {string} className classes on the root
|
|
19
|
+
*/
|
|
20
|
+
const LABEL = 'kol-helper-10 tracking-widest text-meta uppercase'
|
|
21
|
+
const FACES = {
|
|
22
|
+
display: { family: 'var(--kol-font-family-sans-narrow)', label: 'Right Grotesk Narrow', weight: 500 },
|
|
23
|
+
heading: { family: 'var(--kol-font-family-sans-compact)', label: 'Right Grotesk Compact', weight: 500 },
|
|
24
|
+
text: { family: 'var(--kol-font-family-sans)', label: 'Right Grotesk', weight: 400 },
|
|
25
|
+
mono: { family: 'var(--kol-font-family-mono)', label: 'JetBrains Mono', weight: 400 },
|
|
26
|
+
}
|
|
27
|
+
const SAMPLES = {
|
|
28
|
+
headline: 'The quick brown fox jumps over the lazy dog',
|
|
29
|
+
text: 'A modular scale sets every size from one base and one ratio, so the steps keep the same proportion from the smallest note to the largest headline.',
|
|
30
|
+
}
|
|
31
|
+
const INK = { emphasis: 'var(--kol-fg-emphasis)', strong: 'var(--kol-fg-strong)', lede: 'var(--kol-fg-lede)', body: 'var(--kol-fg-body)', meta: 'var(--kol-fg-meta)' }
|
|
32
|
+
const CASE = { none: 'none', upper: 'uppercase', lower: 'lowercase', title: 'capitalize' }
|
|
33
|
+
const opts = (list) => list.map((x) => ({ value: x, label: x }))
|
|
34
|
+
|
|
35
|
+
export default function TypeLab({ value: valueProp, onChange, faces = FACES, samples = SAMPLES, className = '' }) {
|
|
36
|
+
const [own, setOwn] = useState(DEFAULT_TYPE_LAB)
|
|
37
|
+
const value = { ...DEFAULT_TYPE_LAB, ...(valueProp ?? own) }
|
|
38
|
+
const set = (patch) => { const next = { ...value, ...patch }; if (valueProp === undefined) setOwn(next); onChange?.(next) }
|
|
39
|
+
const setRole = (id, patch) => set({ roles: { ...value.roles, [id]: { ...value.roles?.[id], ...patch } } })
|
|
40
|
+
const t = typeLab(value)
|
|
41
|
+
const face = (k) => ({ ...FACES[k], ...faces[k] })
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={`flex flex-col gap-10 ${className}`.trim()}>
|
|
45
|
+
<div className="flex flex-col gap-6">
|
|
46
|
+
<div className="flex max-w-[724px] flex-col gap-2">
|
|
47
|
+
<h3 className="m-0 kol-sans-heading-04 text-emphasis">{t.ratio.label} {t.ratio.note}</h3>
|
|
48
|
+
<p className="m-0 kol-sans-body-02 text-fg-80">{t.ratio.about}</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex flex-wrap items-end gap-x-8 gap-y-4">
|
|
51
|
+
<div className="flex flex-col gap-2"><span className={LABEL}>Base</span><Input type="number" size="sm" unit="px" chars={4} value={value.base} onChange={(e) => { const v = Number(e.target.value); if (v >= 8 && v <= 32) set({ base: v }) }} /></div>
|
|
52
|
+
<div className="flex flex-col gap-2"><span className={LABEL}>Ratio</span><Dropdown size="sm" value={t.ratio.id} onChange={(v) => set({ ratio: v })} options={TYPE_RATIOS.map((x) => ({ value: x.id, label: `${x.label} · ${x.note}` }))} /></div>
|
|
53
|
+
</div>
|
|
54
|
+
{/* the spec rows keep their columns on a phone and scroll sideways */}
|
|
55
|
+
<div className="overflow-x-auto overscroll-x-contain"><div className="min-w-[760px]">
|
|
56
|
+
{t.steps.map((s) => {
|
|
57
|
+
const f = face(s.face)
|
|
58
|
+
return (
|
|
59
|
+
<div key={s.id} className="border-t border-fg-08 py-5">
|
|
60
|
+
<div className="mb-3 grid grid-cols-[120px_minmax(0,1.4fr)_minmax(0,1.8fr)_repeat(3,minmax(0,1fr))_auto] items-center gap-x-6">
|
|
61
|
+
<span className={LABEL}>{s.role}</span>
|
|
62
|
+
<code className="kol-doc-code-inline justify-self-start whitespace-nowrap">--type-{s.id}</code>
|
|
63
|
+
<span className={LABEL}>{f.label}</span>
|
|
64
|
+
<span className={LABEL}>W: {f.weight}</span>
|
|
65
|
+
<span className={LABEL}>S: <span className="normal-case">{s.size}px</span></span>
|
|
66
|
+
<span className={LABEL}>LH: <span className="normal-case">{s.lh}px</span></span>
|
|
67
|
+
{/* the step's line-height factor, the brand's to tune */}
|
|
68
|
+
<Input type="number" size="sm" chars={4} step={0.05} aria-label={`${s.role} line height`} value={s.k}
|
|
69
|
+
onChange={(e) => { const k = Number(e.target.value); if (k >= 0.8 && k <= 2.4) set({ leading: { ...value.leading, [s.id]: k } }) }} />
|
|
70
|
+
</div>
|
|
71
|
+
<p className="m-0 truncate text-emphasis" style={{ fontFamily: f.family, fontWeight: f.weight, fontSize: s.size, lineHeight: `${s.lh}px` }}>
|
|
72
|
+
{s.face === 'text' ? samples.text : samples.headline}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</div></div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<section className="flex flex-col gap-3">
|
|
81
|
+
<h3 className="m-0 kol-eyebrow text-fg-80">Roles</h3>
|
|
82
|
+
<p className="m-0 max-w-[724px] kol-sans-body-03 text-meta">What the brand sets text as: each role on a step, in a face, with its weight, tracking, case and ink.</p>
|
|
83
|
+
<div className="overflow-x-auto overscroll-x-contain"><div className="min-w-[960px]">
|
|
84
|
+
<div className="grid grid-cols-[100px_repeat(6,minmax(0,1fr))_minmax(0,2.2fr)] items-center gap-x-4 border-b border-fg-08 pb-2">
|
|
85
|
+
{['Role', 'Step', 'Face', 'Weight', 'Tracking %', 'Case', 'Ink', 'Sample'].map((h) => <span key={h} className={LABEL}>{h}</span>)}
|
|
86
|
+
</div>
|
|
87
|
+
{t.roles.map((r) => {
|
|
88
|
+
const f = face(r.face)
|
|
89
|
+
return (
|
|
90
|
+
<div key={r.id} className="grid grid-cols-[100px_repeat(6,minmax(0,1fr))_minmax(0,2.2fr)] items-center gap-x-4 border-b border-fg-08 py-2">
|
|
91
|
+
<span className={LABEL}>{r.label}</span>
|
|
92
|
+
<Dropdown size="sm" value={r.step} onChange={(v) => setRole(r.id, { step: v })} options={TYPE_STEPS.map((s) => ({ value: s.id, label: s.role }))} />
|
|
93
|
+
<Dropdown size="sm" value={r.face} onChange={(v) => setRole(r.id, { face: v })} options={opts(TYPE_FACES)} />
|
|
94
|
+
<Input type="number" size="sm" chars={4} step={100} aria-label={`${r.label} weight`} value={r.weight} onChange={(e) => { const w = Number(e.target.value); if (w >= 100 && w <= 900) setRole(r.id, { weight: w }) }} />
|
|
95
|
+
<Input type="number" size="sm" chars={4} step={0.5} aria-label={`${r.label} tracking`} value={Math.round(r.tracking * 1000) / 10} onChange={(e) => { const p = Number(e.target.value); if (p >= -10 && p <= 30) setRole(r.id, { tracking: p / 100 }) }} />
|
|
96
|
+
<Dropdown size="sm" value={r.case} onChange={(v) => setRole(r.id, { case: v })} options={opts(TYPE_CASES)} />
|
|
97
|
+
<Dropdown size="sm" value={r.ink} onChange={(v) => setRole(r.id, { ink: v })} options={opts(TYPE_INKS)} />
|
|
98
|
+
<span className="truncate" style={{ fontFamily: f.family, fontWeight: r.weight, fontSize: Math.min(r.size, 40), lineHeight: 1.2, letterSpacing: `${r.tracking}em`, textTransform: CASE[r.case], color: INK[r.ink] }}>
|
|
99
|
+
{r.label} {r.size}/{r.lh}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</div></div>
|
|
105
|
+
</section>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
package/src/TypeScaleLab.jsx
CHANGED
|
@@ -1,71 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Dropdown, Input } from '@kolkrabbi/kol-component'
|
|
3
|
-
import { DEFAULT_TYPE_SCALE, TYPE_RATIOS, typeScale } from './typeScale.js'
|
|
4
|
-
|
|
5
|
-
/* taxonomy-ok: organism — the type-scale generator: base + ratio controls over a spec row and a live sample per step */
|
|
1
|
+
import TypeLab from './TypeLab.jsx'
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* step (`typeScale.js`); each row is a spec label (role · token · face · weight · size · line height)
|
|
12
|
-
* over a live sample. The faces are the BRAND's, passed in — the lab never sets KOL's type.
|
|
13
|
-
*
|
|
14
|
-
* @param {Object} value `{ base, ratio }` — controlled; omit for internal
|
|
15
|
-
* @param {Function} onChange `(next) => void`
|
|
16
|
-
* @param {Object} faces `{ display, heading, text }` — `{ family, label, weight }` each
|
|
17
|
-
* @param {Object} samples `{ headline, text }` — the sample strings
|
|
18
|
-
* @param {string} className classes on the root
|
|
4
|
+
* @deprecated 2026-10-29 — `TypeLab` (the lab family's name, BrandLabToolGaps), which also sets the roles. Same value
|
|
5
|
+
* (`{ base, ratio }` still works); drops when nobody imports it (04-retirements.md).
|
|
6
|
+
* TypeScaleLab — A brand's type, generated. The old name of TypeLab.
|
|
19
7
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
display: { family: 'var(--kol-font-family-sans-narrow)', label: 'Right Grotesk Narrow', weight: 500 },
|
|
23
|
-
heading: { family: 'var(--kol-font-family-sans-compact)', label: 'Right Grotesk Compact', weight: 500 },
|
|
24
|
-
text: { family: 'var(--kol-font-family-sans)', label: 'Right Grotesk', weight: 400 },
|
|
25
|
-
}
|
|
26
|
-
const SAMPLES = {
|
|
27
|
-
headline: 'The quick brown fox jumps over the lazy dog',
|
|
28
|
-
text: 'A modular scale sets every size from one base and one ratio, so the steps keep the same proportion from the smallest note to the largest headline.',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export default function TypeScaleLab({ value: valueProp, onChange, faces = FACES, samples = SAMPLES, className = '' }) {
|
|
32
|
-
const [own, setOwn] = useState(DEFAULT_TYPE_SCALE)
|
|
33
|
-
const value = valueProp ?? own
|
|
34
|
-
const set = (patch) => { const next = { ...value, ...patch }; if (valueProp === undefined) setOwn(next); onChange?.(next) }
|
|
35
|
-
const r = TYPE_RATIOS.find((x) => x.id === value.ratio) ?? TYPE_RATIOS[3]
|
|
36
|
-
const face = (k) => ({ ...FACES[k], ...faces[k] })
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className={`flex flex-col gap-6 ${className}`.trim()}>
|
|
40
|
-
<div className="flex max-w-[724px] flex-col gap-2">
|
|
41
|
-
<h3 className="m-0 kol-sans-heading-04 text-emphasis">{r.label} {r.note}</h3>
|
|
42
|
-
<p className="m-0 kol-sans-body-02 text-fg-80">{r.about}</p>
|
|
43
|
-
</div>
|
|
44
|
-
<div className="flex flex-wrap items-end gap-x-8 gap-y-4">
|
|
45
|
-
<div className="flex flex-col gap-2"><span className={LABEL}>Base</span><Input type="number" size="sm" unit="px" chars={4} value={value.base} onChange={(e) => { const v = Number(e.target.value); if (v >= 8 && v <= 32) set({ base: v }) }} /></div>
|
|
46
|
-
<div className="flex flex-col gap-2"><span className={LABEL}>Ratio</span><Dropdown size="sm" value={r.id} onChange={(v) => set({ ratio: v })} options={TYPE_RATIOS.map((x) => ({ value: x.id, label: `${x.label} · ${x.note}` }))} /></div>
|
|
47
|
-
</div>
|
|
48
|
-
{/* the spec rows keep their columns on a phone and scroll sideways */}
|
|
49
|
-
<div className="overflow-x-auto overscroll-x-contain"><div className="min-w-[720px]">
|
|
50
|
-
{typeScale(value).map((s) => {
|
|
51
|
-
const f = face(s.face)
|
|
52
|
-
return (
|
|
53
|
-
<div key={s.id} className="border-t border-fg-08 py-5">
|
|
54
|
-
<div className="mb-3 grid grid-cols-[120px_minmax(0,1.6fr)_minmax(0,2fr)_repeat(3,minmax(0,1fr))] items-center gap-x-6">
|
|
55
|
-
<span className={LABEL}>{s.role}</span>
|
|
56
|
-
<code className="kol-doc-code-inline justify-self-start whitespace-nowrap">--type-{s.id}</code>
|
|
57
|
-
<span className={LABEL}>{f.label}</span>
|
|
58
|
-
<span className={LABEL}>W: {f.weight}</span>
|
|
59
|
-
<span className={LABEL}>S: <span className="normal-case">{s.size}px</span></span>
|
|
60
|
-
<span className={LABEL}>LH: <span className="normal-case">{s.lh}px</span></span>
|
|
61
|
-
</div>
|
|
62
|
-
<p className="m-0 truncate text-emphasis" style={{ fontFamily: f.family, fontWeight: f.weight, fontSize: s.size, lineHeight: `${s.lh}px` }}>
|
|
63
|
-
{s.face === 'text' ? samples.text : samples.headline}
|
|
64
|
-
</p>
|
|
65
|
-
</div>
|
|
66
|
-
)
|
|
67
|
-
})}
|
|
68
|
-
</div></div>
|
|
69
|
-
</div>
|
|
70
|
-
)
|
|
8
|
+
export default function TypeScaleLab(props) {
|
|
9
|
+
return <TypeLab {...props} />
|
|
71
10
|
}
|
package/src/colorLab.js
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
* Multiply (darker) of the base over itself at 50%, applied once and twice.
|
|
11
11
|
* Slots four brand slots, two dark and two light, picked from any swatch.
|
|
12
12
|
* Mix 60 / 30 / 10 by area, following Light · Secondary · Primary until a share has its own pick.
|
|
13
|
+
*
|
|
14
|
+
* A COLOUR DEFINITION, NOT ONLY A PALETTE (BrandLabToolGaps, kol-atlas 2026-10-29), and renamed into the lab family
|
|
15
|
+
* (`colorLab` · `colorLabCss` · `DEFAULT_COLOR_LAB`; `deriveColorLab` is its old name):
|
|
16
|
+
* Contrast every colour against the brand's own ink — the Brand black slot on light, the Light slot on dark — with
|
|
17
|
+
* its WCAG ratio and grade (`contrastRatio` · `contrastGrade` · `inkOn`).
|
|
18
|
+
* Ink the brand's text levels, emphasis · strong · lede · body · meta, from its ink at KOL's opacities, for light
|
|
19
|
+
* and for dark — a brand no longer borrows `--kol-fg-*`.
|
|
20
|
+
* Dark the slots swapped (the dark slots become the ground), and each role and composition share moved along its
|
|
21
|
+
* own blend ramp until it passes AA on the dark ground.
|
|
13
22
|
*/
|
|
14
23
|
import { generateHarmony, hexToHsl, hslToHex } from '@kolkrabbi/kol-component'
|
|
15
24
|
|
|
@@ -47,8 +56,41 @@ export const DEFAULT_COLOR_LAB = {
|
|
|
47
56
|
slots: null, picks: [null, null, null],
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
/* ── contrast (WCAG 2) ── */
|
|
60
|
+
const lin = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
|
|
61
|
+
const luminance = (hex) => { const [r, g, b] = toRgb(hex).map(lin); return 0.2126 * r + 0.7152 * g + 0.0722 * b }
|
|
62
|
+
/** the WCAG contrast ratio of two hex colours, 1 … 21 */
|
|
63
|
+
export function contrastRatio(a, b) {
|
|
64
|
+
const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m)
|
|
65
|
+
return (x + 0.05) / (y + 0.05)
|
|
66
|
+
}
|
|
67
|
+
/** `AAA` (7) · `AA` (4.5) · `AA large` (3) · `fail` — the grade for body text, large text at 3 */
|
|
68
|
+
export const contrastGrade = (ratio) => (ratio >= 7 ? 'AAA' : ratio >= 4.5 ? 'AA' : ratio >= 3 ? 'AA large' : 'fail')
|
|
69
|
+
/** the brand's ink that reads best on `hex`, its ratio and grade — `inks` is `{ dark, light }` */
|
|
70
|
+
export function inkOn(hex, inks) {
|
|
71
|
+
const [ink, ratio] = [inks.dark, inks.light].map((i) => [i, contrastRatio(hex, i)]).sort((m, n) => n[1] - m[1])[0]
|
|
72
|
+
return { ink, ratio, grade: contrastGrade(ratio) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* the brand's text levels at KOL's opacities (kol-opacity.css: emphasis full, strong 80, lede 72, body 64, meta 48) */
|
|
76
|
+
export const INK_LEVELS = [['emphasis', 1], ['strong', 0.8], ['lede', 0.72], ['body', 0.64], ['meta', 0.48]]
|
|
77
|
+
const alpha = (hex, a) => (a >= 1 ? hex : `${hex}${Math.round(a * 255).toString(16).padStart(2, '0').toUpperCase()}`)
|
|
78
|
+
const inkRamp = (hex) => Object.fromEntries(INK_LEVELS.map(([id, a]) => [id, alpha(hex, a)]))
|
|
79
|
+
/* the ink at its opacity over its ground, as the eye sees it — what its contrast is measured on */
|
|
80
|
+
const over = (fg, bg, a) => toHex(toRgb(fg).map((c, i) => c * a + toRgb(bg)[i] * (1 - a)))
|
|
81
|
+
const inkContrast = (hex, ground) => Object.fromEntries(INK_LEVELS.map(([id, a]) => {
|
|
82
|
+
const r = contrastRatio(over(hex, ground, a), ground)
|
|
83
|
+
return [id, { ratio: r, grade: contrastGrade(r) }]
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
/* a role on the dark ground: along its own blend ramp from the base toward the lightest, the first step at AA */
|
|
87
|
+
const forDark = (hex, ground) => {
|
|
88
|
+
const ramp = blendRamp(hex)
|
|
89
|
+
return [2, 1, 0].map((i) => ramp[i]).find((x) => contrastRatio(x, ground) >= 4.5) ?? ramp[0]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** everything the lab draws, from its value — `deriveColorLab` before 2026-10-29 */
|
|
93
|
+
export function colorLab(value) {
|
|
52
94
|
const v = { ...DEFAULT_COLOR_LAB, ...value }
|
|
53
95
|
const { h, s, l } = hexToHsl(v.base)
|
|
54
96
|
const hues = Array.from({ length: 10 }, (_, i) => { const hh = (h + i * 36) % 360; return { name: hueName(hh), hex: hslToHex(hh, s, l).toUpperCase() } })
|
|
@@ -59,23 +101,44 @@ export function deriveColorLab(value) {
|
|
|
59
101
|
const roles = generateHarmony(v.base, v.harmony)
|
|
60
102
|
const mix = [slots[2], roles[1], roles[0]].map((hex, i) => v.picks?.[i] ?? hex)
|
|
61
103
|
const ramps = hues.map((x) => ({ ...x, steps: blendRamp(x.hex) }))
|
|
62
|
-
|
|
104
|
+
/* the ink: Brand black on the light ground, Light on the dark one */
|
|
105
|
+
const inks = { dark: slots[1], light: slots[2] }
|
|
106
|
+
const darkSlots = [slots[2], slots[3], slots[0], slots[1]]
|
|
107
|
+
const darkGround = darkSlots[2]
|
|
108
|
+
const dark = {
|
|
109
|
+
slots: darkSlots,
|
|
110
|
+
roles: roles.map((x) => forDark(x, darkGround)),
|
|
111
|
+
mix: [darkGround, forDark(mix[1], darkGround), forDark(mix[2], darkGround)],
|
|
112
|
+
}
|
|
113
|
+
const ink = { light: inkRamp(inks.dark), dark: inkRamp(inks.light) }
|
|
114
|
+
const inkGrades = { light: inkContrast(inks.dark, slots[2]), dark: inkContrast(inks.light, darkGround) }
|
|
115
|
+
return { ...v, h, s, l, hues, blueGrey, grey, slots, roles, mix, ramps, inks, ink, inkGrades, dark }
|
|
63
116
|
}
|
|
117
|
+
/** @deprecated 2026-10-29 — `colorLab` (the lab family's name) */
|
|
118
|
+
export const deriveColorLab = colorLab
|
|
64
119
|
|
|
65
120
|
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
66
121
|
|
|
67
|
-
/** the lab's result as CSS custom properties —
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
/** the lab's result as CSS custom properties — brand-prefixed: the `:root` block (roles, slots, mix, ink, the ramps), then
|
|
123
|
+
* the dark block under `darkSelector` (roles, slots, mix and ink for the dark ground) */
|
|
124
|
+
export function colorLabCss(value, prefix = 'brand', { darkSelector = '[data-theme="dark"]' } = {}) {
|
|
125
|
+
const c = colorLab(value)
|
|
70
126
|
const line = (name, hex) => ` --${prefix}-${name}: ${hex};`
|
|
127
|
+
const themed = (t, ink) => [
|
|
128
|
+
...COLOR_ROLES.map((r, i) => line(`role-${slug(r)}`, t.roles[i])),
|
|
129
|
+
...COLOR_SLOTS.map((r, i) => line(`slot-${slug(r)}`, t.slots[i])),
|
|
130
|
+
...COLOR_MIX.map(([, label], i) => line(`mix-${slug(label).replace(/^\d+-/, '')}`, t.mix[i])),
|
|
131
|
+
...INK_LEVELS.map(([id]) => line(`ink-${id}`, ink[id])),
|
|
132
|
+
]
|
|
71
133
|
return [
|
|
72
134
|
':root {',
|
|
73
|
-
...
|
|
74
|
-
...COLOR_SLOTS.map((r, i) => line(`slot-${slug(r)}`, c.slots[i])),
|
|
75
|
-
...COLOR_MIX.map(([, label], i) => line(`mix-${slug(label).replace(/^\d+-/, '')}`, c.mix[i])),
|
|
135
|
+
...themed(c, c.ink.light),
|
|
76
136
|
...c.ramps.flatMap((x) => x.steps.map((hex, i) => line(`${slug(x.name)}-${(i + 1) * 100}`, hex))),
|
|
77
137
|
...c.blueGrey.map((hex, i) => line(`blue-grey-${(i + 1) * 100}`, hex)),
|
|
78
138
|
...c.grey.map((hex, i) => line(`grey-${(i + 1) * 100}`, hex)),
|
|
79
139
|
'}',
|
|
140
|
+
`${darkSelector} {`,
|
|
141
|
+
...themed(c.dark, c.ink.dark),
|
|
142
|
+
'}',
|
|
80
143
|
].join('\n')
|
|
81
144
|
}
|
package/src/comboLab.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comboLab — the math behind ComboLab in the lab family's shape (BrandLabToolGaps, kol-atlas 2026-10-29 — the user:
|
|
3
|
+
* *"the group of those tools that generate should ship together … and have unified name system"*): `comboLab` makes a
|
|
4
|
+
* palette, `comboLabCss` emits one, `DEFAULT_COMBO_LAB` is where ComboLab starts. The engine is `comboMath.js`'s
|
|
5
|
+
* `generatePalette`, unchanged.
|
|
6
|
+
*
|
|
7
|
+
* A palette is five roles — primary · secondary · light · dark · accent — the 60/30/10 composition's.
|
|
8
|
+
*/
|
|
9
|
+
import { generatePalette } from './comboMath.js'
|
|
10
|
+
|
|
11
|
+
/* Canonical Kolkrabbi composition — yellow-300 / red-200 / cream-100 / blue-400 / orange-300 */
|
|
12
|
+
export const DEFAULT_COMBO_LAB = {
|
|
13
|
+
id: 'kolkrabbi',
|
|
14
|
+
label: 'Kolkrabbi',
|
|
15
|
+
description: 'Yellow hero · rust secondary · navy ink on cream — the canonical composition',
|
|
16
|
+
primary: '#FFCF33',
|
|
17
|
+
secondary: '#AD5038',
|
|
18
|
+
light: '#FAF7F0',
|
|
19
|
+
dark: '#222D3D',
|
|
20
|
+
accent: '#DF760B',
|
|
21
|
+
}
|
|
22
|
+
export const COMBO_ROLES = ['primary', 'secondary', 'light', 'dark', 'accent']
|
|
23
|
+
|
|
24
|
+
/** a new palette: from `pool` (hexes to sample) by `mode` (a GENERATION_MODES id), keeping the roles `locks` marks
|
|
25
|
+
* (booleans in COMBO_ROLES order) from `current`; with no pool, seeded from `base` (default `current.primary`) */
|
|
26
|
+
export function comboLab({ pool = [], mode = 'random', current = DEFAULT_COMBO_LAB, locks = [], base } = {}) {
|
|
27
|
+
const colors = generatePalette(pool, mode, COMBO_ROLES.map((r) => current[r]), locks, pool.length ? undefined : (base ?? current.primary))
|
|
28
|
+
return { ...current, id: 'generated', label: 'Generated', ...Object.fromEntries(COMBO_ROLES.map((r, i) => [r, colors[i] ?? current[r]])) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** a palette as CSS custom properties — `--<prefix>-combo-<role>` */
|
|
32
|
+
export function comboLabCss(palette = DEFAULT_COMBO_LAB, prefix = 'brand') {
|
|
33
|
+
return [':root {', ...COMBO_ROLES.map((r) => ` --${prefix}-combo-${r}: ${palette[r]};`), '}'].join('\n')
|
|
34
|
+
}
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { WEIGHTS, CUTS, CASES, familyFor } from './typographyCuts.js'
|
|
|
15
15
|
export { default as LogoCard } from './LogoCard.jsx'
|
|
16
16
|
export { default as ClearspaceDiagram, hasFramework } from './ClearspaceDiagram.jsx'
|
|
17
17
|
export { default as LogoScaling } from './LogoScaling.jsx'
|
|
18
|
-
export {
|
|
18
|
+
export { DEFAULT_PALETTE } from './ComboLab.jsx'
|
|
19
19
|
export {
|
|
20
20
|
RatioBar, Tower, QuadSplit, CardRow, StripeRow, AppliedCard,
|
|
21
21
|
LAYOUT_COMPONENTS, COMBO_LAYOUTS,
|
|
@@ -27,6 +27,7 @@ export { generatePalette, fgOn, hexToHsl, hslToHex, GENERATION_MODES } from './c
|
|
|
27
27
|
export { default as AssetCard } from './AssetCard.jsx'
|
|
28
28
|
/* a folder's pages as the brand book's contents cards (plan-2026-10-28-document W5.2) */
|
|
29
29
|
export { default as DocumentIndex } from './DocumentIndex.jsx'
|
|
30
|
+
export { default as DocumentLibrary, DocumentView, DocumentFiles, DocumentMarkdown, useFileQuickLook } from './DocumentLibrary.jsx'
|
|
30
31
|
/* print (PdfToolsFromStudio, 2026-10-28): A4 sheets for any content, and markdown to PDF · DOCX · MD on @kolkrabbi/kol-print */
|
|
31
32
|
export { default as A4Pages } from './A4Pages.jsx'
|
|
32
33
|
export { default as PrintStudio } from './PrintStudio.jsx'
|
|
@@ -60,11 +61,17 @@ export { brandSections, brandToc, brandInfo, BRAND_VIEWS, BOOK_SECTIONS, ASSET_S
|
|
|
60
61
|
export { default as BrandGuide } from './BrandGuide.jsx'
|
|
61
62
|
export { guideSections, guideToc } from './guideBook.js'
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
export {
|
|
64
|
+
// THE LAB (2026-10-20; one name system and its own subpath 2026-10-29, BrandLabToolGaps): the generators a brand is MADE
|
|
65
|
+
// with — TypeLab · ColorLab · ComboLab · LockupLab, each `<x>Lab(value)` · `<x>LabCss` · `DEFAULT_<X>_LAB`. The same
|
|
66
|
+
// names ship from `@kolkrabbi/kol-styleguide/lab`.
|
|
67
|
+
export {
|
|
68
|
+
TypeLab, typeLab, typeLabCss, DEFAULT_TYPE_LAB, TYPE_RATIOS, TYPE_STEPS, TYPE_ROLES, TYPE_FACES, TYPE_CASES, TYPE_INKS,
|
|
69
|
+
ColorLab, colorLab, colorLabCss, DEFAULT_COLOR_LAB, COLOR_ROLES, COLOR_SLOTS, COLOR_MIX, INK_LEVELS, contrastRatio, contrastGrade, inkOn, blendRamp, neutralRamp,
|
|
70
|
+
ComboLab, comboLab, comboLabCss, DEFAULT_COMBO_LAB, COMBO_ROLES,
|
|
71
|
+
LockupLab, Lockup, LOCKUP_SCALES,
|
|
72
|
+
} from './lab/index.js'
|
|
73
|
+
// the names before the lab family (deprecated 2026-10-29, 04-retirements.md)
|
|
67
74
|
export { default as TypeScaleLab } from './TypeScaleLab.jsx'
|
|
68
|
-
export { typeScale, typeScaleCss,
|
|
69
|
-
export {
|
|
75
|
+
export { typeScale, typeScaleCss, DEFAULT_TYPE_SCALE } from './typeLab.js'
|
|
76
|
+
export { deriveColorLab } from './colorLab.js'
|
|
70
77
|
export { default as LockupBuilder } from './LockupBuilder.jsx'
|
package/src/lab/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* THE LAB — the generators a brand is MADE with, shipped together under one name system (BrandLabToolGaps, kol-atlas
|
|
2
|
+
* 2026-10-29 — the user: *"the group of those tools that generate should ship together with combolab and logo lockup
|
|
3
|
+
* generator, and have unified name system"*). `@kolkrabbi/kol-styleguide/lab`; the main barrel exports the same names.
|
|
4
|
+
*
|
|
5
|
+
* <X>Lab the generator's UI, controlled by one value
|
|
6
|
+
* <x>Lab(value) everything it draws, from the value
|
|
7
|
+
* <x>LabCss(…) the result as CSS custom properties
|
|
8
|
+
* DEFAULT_<X>_LAB where it starts
|
|
9
|
+
*
|
|
10
|
+
* type TypeLab · typeLab · typeLabCss · DEFAULT_TYPE_LAB
|
|
11
|
+
* colour ColorLab · colorLab · colorLabCss · DEFAULT_COLOR_LAB
|
|
12
|
+
* combo ComboLab · comboLab · comboLabCss · DEFAULT_COMBO_LAB
|
|
13
|
+
* lockup LockupLab (it draws a lockup around the brand's mark; there is no value to emit) */
|
|
14
|
+
export { default as TypeLab } from '../TypeLab.jsx'
|
|
15
|
+
export { typeLab, typeLabCss, DEFAULT_TYPE_LAB, TYPE_RATIOS, TYPE_STEPS, TYPE_ROLES, TYPE_FACES, TYPE_CASES, TYPE_INKS } from '../typeLab.js'
|
|
16
|
+
export { default as ColorLab } from '../ColorLab.jsx'
|
|
17
|
+
export { colorLab, colorLabCss, DEFAULT_COLOR_LAB, COLOR_ROLES, COLOR_SLOTS, COLOR_MIX, INK_LEVELS, contrastRatio, contrastGrade, inkOn, blendRamp, neutralRamp } from '../colorLab.js'
|
|
18
|
+
export { default as ComboLab } from '../ComboLab.jsx'
|
|
19
|
+
export { comboLab, comboLabCss, DEFAULT_COMBO_LAB, COMBO_ROLES } from '../comboLab.js'
|
|
20
|
+
export { GENERATION_MODES } from '../comboMath.js'
|
|
21
|
+
export { default as LockupLab } from '../LockupLab.jsx'
|
|
22
|
+
export { default as Lockup, LOCKUP_SCALES } from '../Lockup.jsx'
|
package/src/typeLab.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* typeLab — the math behind TypeLab: a brand's type, from a base size and a ratio to every role it sets.
|
|
3
|
+
*
|
|
4
|
+
* Carried from kol-atlas `components/graphics/ScaleLab.jsx` (2026-10-20) as `typeScale`; grown into a brand's whole type
|
|
5
|
+
* definition and renamed into the lab family (BrandLabToolGaps, kol-atlas 2026-10-29 — the user: *"the group of those
|
|
6
|
+
* tools that generate should ship together … and have unified name system"*: `TypeLab` · `typeLab` · `typeLabCss` ·
|
|
7
|
+
* `DEFAULT_TYPE_LAB`, beside the colour, combo and lockup labs).
|
|
8
|
+
*
|
|
9
|
+
* Steps base × ratio^n, rounded to a whole px; the line height is the step's factor × size, snapped up to the 4px
|
|
10
|
+
* grid. The factor is 1.2 for display and headings, 1.5 for text, and the brand's to tune (`leading`).
|
|
11
|
+
* Roles what a brand sets text AS — sixteen, each on a step with its face (display · heading · text · mono), weight,
|
|
12
|
+
* tracking, case and ink (the colour lab's text levels). `roles` overrides any of them.
|
|
13
|
+
*
|
|
14
|
+
* The ramp owes nothing to KOL's own type scale — it is a brand's.
|
|
15
|
+
*/
|
|
16
|
+
export const TYPE_RATIOS = [
|
|
17
|
+
{ id: 'M2', label: 'Major 2nd', ratio: 9 / 8, note: '9:8', about: 'The gentlest step, with neighbouring sizes close together. It suits dense interfaces and long documents, where hierarchy comes from weight and spacing more than from size.' },
|
|
18
|
+
{ id: 'm3', label: 'Minor 3rd', ratio: 6 / 5, note: '6:5', about: 'A modest step with clear but quiet contrast between levels. It works for product interfaces and editorial text that need order and calm rather than drama.' },
|
|
19
|
+
{ id: 'M3', label: 'Major 3rd', ratio: 5 / 4, note: '5:4', about: 'A balanced step and a common default for websites. Headings stand apart from body text, while the largest sizes stay practical for headlines and page titles.' },
|
|
20
|
+
{ id: 'P4', label: 'Perfect 4th', ratio: 4 / 3, note: '4:3', about: 'A confident step with strong contrast between levels. It suits marketing pages and brand material, where headlines lead and body text supports them below.' },
|
|
21
|
+
{ id: 'A4', label: 'Augmented 4th', ratio: Math.SQRT2, note: '1:√2', about: 'The ratio of A-series paper, set between a fourth and a fifth. Its large jumps read as deliberate and architectural, suited to print and structured layouts.' },
|
|
22
|
+
{ id: 'P5', label: 'Perfect 5th', ratio: 3 / 2, note: '3:2', about: 'A wide step, with few sizes far apart. It suits posters, decks and expressive layouts, where display type carries the page and body text stays secondary.' },
|
|
23
|
+
{ id: 'G', label: 'Golden ratio', ratio: (1 + Math.sqrt(5)) / 2, note: '1:φ', about: 'The widest step here, where sizes grow fast. It is best kept to display and editorial work, since the text end of the scale thins out quickly.' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/* step n from the base; `face` is which of the brand's faces sets it, `k` the default line-height factor */
|
|
27
|
+
export const TYPE_STEPS = [
|
|
28
|
+
{ id: 'display-1', role: 'Display 1', n: 6, face: 'display', k: 1.2 },
|
|
29
|
+
{ id: 'display-2', role: 'Display 2', n: 5, face: 'display', k: 1.2 },
|
|
30
|
+
{ id: 'heading-1', role: 'Heading 1', n: 4, face: 'display', k: 1.2 },
|
|
31
|
+
{ id: 'heading-2', role: 'Heading 2', n: 3, face: 'heading', k: 1.2 },
|
|
32
|
+
{ id: 'heading-3', role: 'Heading 3', n: 2, face: 'heading', k: 1.2 },
|
|
33
|
+
{ id: 'heading-4', role: 'Heading 4', n: 1, face: 'heading', k: 1.2 },
|
|
34
|
+
{ id: 'body', role: 'Body', n: 0, face: 'text', k: 1.5 },
|
|
35
|
+
{ id: 'small', role: 'Small', n: -1, face: 'text', k: 1.5 },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export const TYPE_FACES = ['display', 'heading', 'text', 'mono']
|
|
39
|
+
export const TYPE_CASES = ['none', 'upper', 'lower', 'title']
|
|
40
|
+
/* the colour lab's ink levels (colorLab `INK_LEVELS`), strongest first */
|
|
41
|
+
export const TYPE_INKS = ['emphasis', 'strong', 'lede', 'body', 'meta']
|
|
42
|
+
|
|
43
|
+
/* THE ROLES (kol-atlas apps/lab roles.css, 2026-10-29): sixteen roles on the eight steps — several share one. Tracking
|
|
44
|
+
* is in em (−0.015 = −1.5 %). */
|
|
45
|
+
export const TYPE_ROLES = [
|
|
46
|
+
{ id: 'display-1', label: 'Display 1', step: 'display-1', face: 'display', weight: 700, tracking: -0.02, case: 'none', ink: 'emphasis' },
|
|
47
|
+
{ id: 'display-2', label: 'Display 2', step: 'display-2', face: 'display', weight: 700, tracking: -0.02, case: 'none', ink: 'emphasis' },
|
|
48
|
+
{ id: 'h1', label: 'Heading 1', step: 'heading-1', face: 'display', weight: 700, tracking: -0.015, case: 'none', ink: 'emphasis' },
|
|
49
|
+
{ id: 'h2', label: 'Heading 2', step: 'heading-2', face: 'heading', weight: 600, tracking: -0.01, case: 'none', ink: 'emphasis' },
|
|
50
|
+
{ id: 'h3', label: 'Heading 3', step: 'heading-3', face: 'heading', weight: 600, tracking: -0.005, case: 'none', ink: 'emphasis' },
|
|
51
|
+
{ id: 'h4', label: 'Heading 4', step: 'heading-4', face: 'heading', weight: 600, tracking: 0, case: 'none', ink: 'emphasis' },
|
|
52
|
+
{ id: 'h5', label: 'Heading 5', step: 'body', face: 'heading', weight: 600, tracking: 0, case: 'none', ink: 'emphasis' },
|
|
53
|
+
{ id: 'h6', label: 'Heading 6', step: 'small', face: 'heading', weight: 600, tracking: 0, case: 'none', ink: 'strong' },
|
|
54
|
+
{ id: 'quote', label: 'Quote', step: 'heading-4', face: 'heading', weight: 400, tracking: 0, case: 'none', ink: 'lede' },
|
|
55
|
+
{ id: 'lead', label: 'Lead', step: 'heading-4', face: 'text', weight: 400, tracking: 0, case: 'none', ink: 'lede' },
|
|
56
|
+
{ id: 'body', label: 'Body', step: 'body', face: 'text', weight: 400, tracking: 0, case: 'none', ink: 'body' },
|
|
57
|
+
{ id: 'small', label: 'Small', step: 'small', face: 'text', weight: 400, tracking: 0, case: 'none', ink: 'body' },
|
|
58
|
+
{ id: 'caption', label: 'Caption', step: 'small', face: 'text', weight: 400, tracking: 0, case: 'none', ink: 'meta' },
|
|
59
|
+
{ id: 'eyebrow', label: 'Eyebrow', step: 'small', face: 'mono', weight: 500, tracking: 0.08, case: 'upper', ink: 'meta' },
|
|
60
|
+
{ id: 'label', label: 'Label', step: 'small', face: 'mono', weight: 500, tracking: 0.04, case: 'upper', ink: 'body' },
|
|
61
|
+
{ id: 'data', label: 'Data', step: 'small', face: 'mono', weight: 400, tracking: 0, case: 'none', ink: 'body' },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
const GRID = 4
|
|
65
|
+
/** the lab's starting value; `leading` (stepId → factor) and `roles` (roleId → partial role) override the defaults */
|
|
66
|
+
export const DEFAULT_TYPE_LAB = { base: 16, ratio: 'P4', leading: {}, roles: {} }
|
|
67
|
+
|
|
68
|
+
/** everything the lab draws, from its value: `{ ...value, ratio, steps, roles }` — steps with `size` and `lh` in px, roles
|
|
69
|
+
* resolved onto their step */
|
|
70
|
+
export function typeLab(value) {
|
|
71
|
+
const v = { ...DEFAULT_TYPE_LAB, ...value }
|
|
72
|
+
const ratio = TYPE_RATIOS.find((x) => x.id === v.ratio) ?? TYPE_RATIOS[3]
|
|
73
|
+
const steps = TYPE_STEPS.map((s) => {
|
|
74
|
+
const k = v.leading?.[s.id] ?? s.k
|
|
75
|
+
const size = Math.round(v.base * ratio.ratio ** s.n)
|
|
76
|
+
return { ...s, k, size, lh: Math.ceil((size * k) / GRID) * GRID }
|
|
77
|
+
})
|
|
78
|
+
const roles = TYPE_ROLES.map((r) => {
|
|
79
|
+
const role = { ...r, ...v.roles?.[r.id] }
|
|
80
|
+
const step = steps.find((s) => s.id === role.step) ?? steps[6]
|
|
81
|
+
return { ...role, size: step.size, lh: step.lh }
|
|
82
|
+
})
|
|
83
|
+
return { ...v, ratio, steps, roles }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const CASE_CSS = { none: 'none', upper: 'uppercase', lower: 'lowercase', title: 'capitalize' }
|
|
87
|
+
|
|
88
|
+
/** the type as CSS: the faces, each step's size and line height, each role's settings as variables, and a `.type-<role>`
|
|
89
|
+
* class per role that reads them. `faces` (`{ display: { family }, … }`) fills the family variables; the ink reads the
|
|
90
|
+
* colour lab's `--<inkPrefix>-ink-<level>` */
|
|
91
|
+
export function typeLabCss(value, { prefix = 'type', faces = value?.faces, inkPrefix = 'brand' } = {}) {
|
|
92
|
+
const t = typeLab(value)
|
|
93
|
+
const v = (name, val) => ` --${prefix}-${name}: ${val};`
|
|
94
|
+
return [
|
|
95
|
+
':root {',
|
|
96
|
+
...TYPE_FACES.filter((f) => faces?.[f]?.family).map((f) => v(`face-${f}`, faces[f].family)),
|
|
97
|
+
...t.steps.flatMap((s) => [v(`${s.id}-size`, `${s.size}px`), v(`${s.id}-lh`, `${s.lh}px`)]),
|
|
98
|
+
...t.roles.flatMap((r) => [
|
|
99
|
+
v(`${r.id}-weight`, r.weight),
|
|
100
|
+
v(`${r.id}-tracking`, `${r.tracking}em`),
|
|
101
|
+
]),
|
|
102
|
+
'}',
|
|
103
|
+
...t.roles.map((r) => [
|
|
104
|
+
`.${prefix}-${r.id} {`,
|
|
105
|
+
` font-family: var(--${prefix}-face-${r.face});`,
|
|
106
|
+
` font-size: var(--${prefix}-${r.step}-size);`,
|
|
107
|
+
` line-height: var(--${prefix}-${r.step}-lh);`,
|
|
108
|
+
` font-weight: var(--${prefix}-${r.id}-weight);`,
|
|
109
|
+
` letter-spacing: var(--${prefix}-${r.id}-tracking);`,
|
|
110
|
+
` text-transform: ${CASE_CSS[r.case] ?? 'none'};`,
|
|
111
|
+
` color: var(--${inkPrefix}-ink-${r.ink});`,
|
|
112
|
+
'}',
|
|
113
|
+
].join('\n')),
|
|
114
|
+
].join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── the names before the lab family (deprecated 2026-10-29, on the retirement ledger) ── */
|
|
118
|
+
/** @deprecated use `typeLab(value).steps` */
|
|
119
|
+
export const typeScale = (value) => typeLab(value).steps
|
|
120
|
+
/** @deprecated use `typeLabCss` — this emits the steps alone, as it did */
|
|
121
|
+
export function typeScaleCss(value, prefix = 'type') {
|
|
122
|
+
return [':root {', ...typeScale(value).flatMap((s) => [` --${prefix}-${s.id}-size: ${s.size}px;`, ` --${prefix}-${s.id}-lh: ${s.lh}px;`]), '}'].join('\n')
|
|
123
|
+
}
|
|
124
|
+
/** @deprecated use DEFAULT_TYPE_LAB */
|
|
125
|
+
export const DEFAULT_TYPE_SCALE = { base: 16, ratio: 'P4' }
|
package/src/typeScale.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* typeScale — a modular type scale from two inputs: a base size and a musical ratio.
|
|
3
|
-
*
|
|
4
|
-
* Carried from kol-atlas `components/graphics/ScaleLab.jsx` (2026-10-20). Each step is base × ratio^n,
|
|
5
|
-
* rounded to a whole px; line heights snap up to the 4px grid (1.2 for display and headings, 1.5 for
|
|
6
|
-
* text). The ramp owes nothing to KOL's own type scale — it is a brand's.
|
|
7
|
-
*/
|
|
8
|
-
export const TYPE_RATIOS = [
|
|
9
|
-
{ id: 'M2', label: 'Major 2nd', ratio: 9 / 8, note: '9:8', about: 'The gentlest step, with neighbouring sizes close together. It suits dense interfaces and long documents, where hierarchy comes from weight and spacing more than from size.' },
|
|
10
|
-
{ id: 'm3', label: 'Minor 3rd', ratio: 6 / 5, note: '6:5', about: 'A modest step with clear but quiet contrast between levels. It works for product interfaces and editorial text that need order and calm rather than drama.' },
|
|
11
|
-
{ id: 'M3', label: 'Major 3rd', ratio: 5 / 4, note: '5:4', about: 'A balanced step and a common default for websites. Headings stand apart from body text, while the largest sizes stay practical for headlines and page titles.' },
|
|
12
|
-
{ id: 'P4', label: 'Perfect 4th', ratio: 4 / 3, note: '4:3', about: 'A confident step with strong contrast between levels. It suits marketing pages and brand material, where headlines lead and body text supports them below.' },
|
|
13
|
-
{ id: 'A4', label: 'Augmented 4th', ratio: Math.SQRT2, note: '1:√2', about: 'The ratio of A-series paper, set between a fourth and a fifth. Its large jumps read as deliberate and architectural, suited to print and structured layouts.' },
|
|
14
|
-
{ id: 'P5', label: 'Perfect 5th', ratio: 3 / 2, note: '3:2', about: 'A wide step, with few sizes far apart. It suits posters, decks and expressive layouts, where display type carries the page and body text stays secondary.' },
|
|
15
|
-
{ id: 'G', label: 'Golden ratio', ratio: (1 + Math.sqrt(5)) / 2, note: '1:φ', about: 'The widest step here, where sizes grow fast. It is best kept to display and editorial work, since the text end of the scale thins out quickly.' },
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
/* step n from the base; `face` is which of the brand's faces sets it, `k` the line-height factor */
|
|
19
|
-
export const TYPE_STEPS = [
|
|
20
|
-
{ id: 'display-1', role: 'Display 1', n: 6, face: 'display', k: 1.2 },
|
|
21
|
-
{ id: 'display-2', role: 'Display 2', n: 5, face: 'display', k: 1.2 },
|
|
22
|
-
{ id: 'heading-1', role: 'Heading 1', n: 4, face: 'display', k: 1.2 },
|
|
23
|
-
{ id: 'heading-2', role: 'Heading 2', n: 3, face: 'heading', k: 1.2 },
|
|
24
|
-
{ id: 'heading-3', role: 'Heading 3', n: 2, face: 'heading', k: 1.2 },
|
|
25
|
-
{ id: 'heading-4', role: 'Heading 4', n: 1, face: 'heading', k: 1.2 },
|
|
26
|
-
{ id: 'body', role: 'Body', n: 0, face: 'text', k: 1.5 },
|
|
27
|
-
{ id: 'small', role: 'Small', n: -1, face: 'text', k: 1.5 },
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
const GRID = 4
|
|
31
|
-
export const DEFAULT_TYPE_SCALE = { base: 16, ratio: 'P4' }
|
|
32
|
-
|
|
33
|
-
/** the steps for `{ base, ratio }` (ratio is a TYPE_RATIOS id) — each with `size` and `lh` in px */
|
|
34
|
-
export function typeScale({ base, ratio } = DEFAULT_TYPE_SCALE) {
|
|
35
|
-
const r = (TYPE_RATIOS.find((x) => x.id === ratio) ?? TYPE_RATIOS[3]).ratio
|
|
36
|
-
return TYPE_STEPS.map((s) => {
|
|
37
|
-
const size = Math.round(base * r ** s.n)
|
|
38
|
-
return { ...s, size, lh: Math.ceil((size * s.k) / GRID) * GRID }
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** the scale as CSS custom properties — `--<prefix>-<step>-size` / `-lh` */
|
|
43
|
-
export function typeScaleCss(value, prefix = 'type') {
|
|
44
|
-
return [':root {', ...typeScale(value).flatMap((s) => [` --${prefix}-${s.id}-size: ${s.size}px;`, ` --${prefix}-${s.id}-lh: ${s.lh}px;`]), '}'].join('\n')
|
|
45
|
-
}
|