@kolkrabbi/kol-styleguide 0.1.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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * MoodTile — image tile with a centered logo overlay.
3
+ *
4
+ * A cover-cropped photo in a 4:3 frame, an optional centered brand-logo
5
+ * overlay, and an optional caption below. Image and logo are consumer-injected
6
+ * (no app coupling): pass an image `src` / `alt` and a `logo` node.
7
+ *
8
+ * Class contract (chrome lives in @kolkrabbi/kol-theme →
9
+ * packages/framework/kol-framework.css):
10
+ * .kol-mood-tile-frame img — absolute inset-0, object-cover (crop)
11
+ * .kol-mood-tile-overlay .kol-brand-logo — logo box sized to 32% / max 180px
12
+ * .kol-mood-tile-overlay svg — svg fills its wrapper (width 100%)
13
+ *
14
+ * Props:
15
+ * src — image URL.
16
+ * alt — image alt text. Default ''.
17
+ * logo — logo node centered over the image (e.g. a brand <svg> or a
18
+ * logo component). Wrapped in .kol-brand-logo for overlay sizing.
19
+ * overlay — show the logo overlay. Default true (only renders when `logo`
20
+ * is also provided).
21
+ * caption — optional caption string rendered below the frame. Author its
22
+ * casing at the call site (no auto text-transform).
23
+ */
24
+ export default function MoodTile({ src, alt = '', logo, overlay = true, caption }) {
25
+ return (
26
+ <figure className="kol-mood-tile">
27
+ <div className="kol-mood-tile-frame relative rounded-[4px] overflow-hidden aspect-[4/3]">
28
+ <img src={src} alt={alt} loading="lazy" />
29
+ {overlay && logo && (
30
+ <div className="kol-mood-tile-overlay text-emphasis absolute inset-0 flex items-center justify-center pointer-events-none">
31
+ <span className="kol-brand-logo">{logo}</span>
32
+ </div>
33
+ )}
34
+ </div>
35
+ {caption && (
36
+ <figcaption className="kol-helper-12 tracking-wider text-meta mt-2">
37
+ {caption}
38
+ </figcaption>
39
+ )}
40
+ </figure>
41
+ )
42
+ }
@@ -0,0 +1,89 @@
1
+ import { familyFor } from './typographyCuts'
2
+
3
+ /**
4
+ * TypeBlock — typography specimen primitive.
5
+ *
6
+ * Renders one sample string with a fully consumer-controlled type treatment:
7
+ * cut (Right Grotesk width / mono), weight, italic, size, tracking,
8
+ * line-height, case, alignment, colour, and an optional stroke. A pure,
9
+ * stateless render — feed it props, it draws the specimen. Defaults land on
10
+ * Right Grotesk regular so a bare `<TypeBlock />` shows a legible baseline.
11
+ *
12
+ * **Position + colour are the consumer's job.** TypeBlock owns no positioned
13
+ * container, outline, or drag chrome, and inherits colour by default
14
+ * (`currentColor`) so the surrounding surface / theme drives ink. Wrap it in
15
+ * whatever tile, row, or frame the specimen page needs.
16
+ *
17
+ * The sample is intentionally *style-driven*, not a fixed `kol-*` type class —
18
+ * previewing arbitrary cuts / sizes is the whole point. It is kept
19
+ * wrap-capable (`overflow-wrap` + `white-space: pre-wrap`) so multi-line
20
+ * samples flow, honouring the mono wrap-vs-nowrap fault line for wrapping text.
21
+ *
22
+ * Ported from the monorepo apps/brand styleguide TypeBlock (2026-07). The
23
+ * source's contentEditable inline-edit / drag-select behaviour (Type-Lab editor
24
+ * chrome) is dropped — a DS specimen is a presentational render. The source's
25
+ * nested `value` object is flattened to discrete props.
26
+ *
27
+ * Props:
28
+ * text sample string (default 'Right Grotesk')
29
+ * cut Right Grotesk cut key or 'mono' — resolved via familyFor
30
+ * weight numeric font-weight (100–900)
31
+ * italic render the italic style
32
+ * size font-size in px
33
+ * tracking letter-spacing in em
34
+ * lineHeight unitless line-height
35
+ * case CSS text-transform keyword the CONSUMER opts into
36
+ * ('none' | 'uppercase' | 'lowercase' | 'capitalize').
37
+ * Default 'none' — never baked on; the sample is never mutated.
38
+ * align text-align
39
+ * color ink (default 'currentColor' — inherit from surface / theme)
40
+ * strokeWidth outline width in px (0 = none)
41
+ * strokeColor outline colour (required for stroke to render)
42
+ * className extra classes on the sample element
43
+ * style inline-style overrides merged last
44
+ */
45
+ export default function TypeBlock({
46
+ text = 'Right Grotesk',
47
+ cut = 'base',
48
+ weight = 400,
49
+ italic = false,
50
+ size = 48,
51
+ tracking = 0,
52
+ lineHeight = 1,
53
+ case: textCase = 'none',
54
+ align = 'left',
55
+ color = 'currentColor',
56
+ strokeWidth = 0,
57
+ strokeColor,
58
+ className = '',
59
+ style,
60
+ ...rest
61
+ }) {
62
+ const hasStroke = strokeWidth > 0 && Boolean(strokeColor)
63
+ return (
64
+ <div
65
+ className={className}
66
+ style={{
67
+ fontFamily: `'${familyFor(cut)}', 'Right Grotesk', sans-serif`,
68
+ fontWeight: weight,
69
+ fontStyle: italic ? 'italic' : 'normal',
70
+ fontSize: `${size}px`,
71
+ letterSpacing: `${tracking}em`,
72
+ lineHeight,
73
+ textAlign: align,
74
+ color,
75
+ textTransform: textCase,
76
+ WebkitTextStrokeColor: hasStroke ? strokeColor : undefined,
77
+ WebkitTextStrokeWidth: hasStroke ? `${strokeWidth}px` : undefined,
78
+ paintOrder: hasStroke ? 'stroke fill' : undefined,
79
+ minHeight: '1em',
80
+ overflowWrap: 'break-word',
81
+ whiteSpace: 'pre-wrap',
82
+ ...style,
83
+ }}
84
+ {...rest}
85
+ >
86
+ {text}
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * ComboLab — stage layout primitives.
3
+ *
4
+ * Each layout takes `{ palette, logo }` and renders its geometry with the
5
+ * palette's role-colors (primary / secondary / light / dark / accent).
6
+ * Structural CSS is DS-owned under `.kol-combo-*`
7
+ * (packages/theme/kol-components-styleguide.css → currently shipped from
8
+ * packages/framework/kol-framework.css). Inline styles here are strictly
9
+ * data-driven (background + fg-on color from the palette) — the classes carry
10
+ * everything else, so nothing is re-invented.
11
+ *
12
+ * DECOUPLING NOTE: the monorepo original imported a hard `KolLogo` brand
13
+ * component. Here `logo` is a consumer-injected React node — <LogoSlot/>
14
+ * renders whatever the host passes (a logomark, an <img>, an <svg>) and
15
+ * renders nothing when it's absent. No brand-app coupling.
16
+ */
17
+ import { fgOn } from './comboMath.js'
18
+
19
+ /* LogoSlot — renders the injected logo node at a fixed inline width. */
20
+ const LogoSlot = ({ logo, size = 48 }) => {
21
+ if (!logo) return null
22
+ return (
23
+ <span className="kol-combo-logo" style={{ width: size }}>
24
+ {logo}
25
+ </span>
26
+ )
27
+ }
28
+
29
+ /* ────────── 60 / 30 / 10 ────────── */
30
+ export function RatioBar({ palette, logo }) {
31
+ const { primary, secondary, light } = palette
32
+ return (
33
+ <div className="kol-combo-stage kol-combo-frame kol-combo-stage--fill kol-combo-stage--ratio">
34
+ <div className="kol-combo-slab" style={{ background: primary, color: fgOn(primary) }}>
35
+ <span className="kol-combo-label">Primary</span>
36
+ <LogoSlot logo={logo} size={48} />
37
+ <span className="kol-combo-number">10</span>
38
+ </div>
39
+ <div className="kol-combo-slab" style={{ background: secondary, color: fgOn(secondary) }}>
40
+ <span className="kol-combo-label">Secondary</span>
41
+ <span className="kol-combo-number">30</span>
42
+ </div>
43
+ <div className="kol-combo-slab" style={{ background: light, color: fgOn(light) }}>
44
+ <span className="kol-combo-label">Neutral</span>
45
+ <span className="kol-combo-number">60</span>
46
+ </div>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ /* ────────── Tower — 4-band vertical ────────── */
52
+ export function Tower({ palette, logo }) {
53
+ const { primary, secondary, light, dark } = palette
54
+ const band = (bg, label) => (
55
+ <div className="kol-combo-slab kol-combo-slab--end" style={{ background: bg, color: fgOn(bg) }}>
56
+ <span className="kol-combo-label">{label}</span>
57
+ </div>
58
+ )
59
+ return (
60
+ <div className="kol-combo-stage kol-combo-frame kol-combo-stage--tower">
61
+ <div className="kol-combo-slab kol-combo-slab--between" style={{ background: primary, color: fgOn(primary) }}>
62
+ <LogoSlot logo={logo} size={48} />
63
+ <span className="kol-combo-label">Primary</span>
64
+ </div>
65
+ {band(secondary, 'Secondary')}
66
+ {band(light, 'Light')}
67
+ {band(dark, 'Dark')}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ /* ────────── Quad split — 50 / 25 / 25 ────────── */
73
+ export function QuadSplit({ palette, logo }) {
74
+ const { primary, light, dark, accent } = palette
75
+ return (
76
+ <div className="kol-combo-stage kol-combo-frame kol-combo-stage--fill kol-combo-stage--quad">
77
+ <div className="kol-combo-slab kol-combo-slab--between" style={{ background: primary, color: fgOn(primary) }}>
78
+ <LogoSlot logo={logo} size={64} />
79
+ <span className="kol-combo-label">Primary · 50</span>
80
+ </div>
81
+ <div className="kol-combo-quad-col">
82
+ <div className="kol-combo-slab kol-combo-slab--end" style={{ background: light, color: fgOn(light) }}>
83
+ <span className="kol-combo-label">Light · 25</span>
84
+ </div>
85
+ <div className="kol-combo-slab kol-combo-slab--between" style={{ background: dark, color: fgOn(dark) }}>
86
+ <span className="kol-combo-label" style={{ color: accent }}>Accent</span>
87
+ <span className="kol-combo-label">Dark · 25</span>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ /* ────────── Card row — 4 discrete cards ────────── */
95
+ export function CardRow({ palette, logo }) {
96
+ const { primary, secondary, light, dark } = palette
97
+ const card = (bg, label, withLogo = false) => (
98
+ <div
99
+ className={`kol-combo-card kol-combo-frame${withLogo ? ' kol-combo-card--between' : ' kol-combo-card--end'}`}
100
+ style={{ background: bg, color: fgOn(bg) }}
101
+ >
102
+ {withLogo && <LogoSlot logo={logo} size={40} />}
103
+ <span className="kol-combo-label">{label}</span>
104
+ </div>
105
+ )
106
+ return (
107
+ <div className="kol-combo-stage kol-combo-stage--card-row">
108
+ {card(primary, 'Primary', true)}
109
+ {card(secondary, 'Secondary')}
110
+ {card(light, 'Light')}
111
+ {card(dark, 'Dark')}
112
+ </div>
113
+ )
114
+ }
115
+
116
+ /* ────────── Stripe row — Method 01 / 02 proportion bars ────────── */
117
+ export function StripeRow({ palette }) {
118
+ const { primary, secondary, light, dark, accent } = palette
119
+ /* Rule color = dark at ~12% alpha, passed via CSS custom prop so the
120
+ .kol-combo-stripe-neutral class owns the border declarations. */
121
+ const ruleColor = `${dark}20`
122
+ return (
123
+ <div
124
+ className="kol-combo-stage kol-combo-frame kol-combo-stage--stripe-row"
125
+ style={{
126
+ '--stripe-rule': ruleColor,
127
+ '--stripe-accent': accent,
128
+ }}
129
+ >
130
+ {/* Method 01 — single-row proportion bar */}
131
+ <div className="kol-combo-stripe-row">
132
+ <div className="kol-combo-stripe-bar">
133
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--6" style={{ background: primary }} />
134
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--3 kol-combo-stripe-neutral" style={{ background: light }} />
135
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: accent }} />
136
+ <span className="kol-combo-stripe-method">Method 01</span>
137
+ </div>
138
+ </div>
139
+ {/* Method 02 — split-row */}
140
+ <div className="kol-combo-stripe-row">
141
+ <div className="kol-combo-stripe-bar">
142
+ <div className="kol-combo-stripe-group kol-combo-stripe-group--3">
143
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: primary }} />
144
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: secondary }} />
145
+ </div>
146
+ <div className="kol-combo-stripe-group kol-combo-stripe-group--3">
147
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1 kol-combo-stripe-neutral" style={{ background: light }} />
148
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: light }} />
149
+ </div>
150
+ <div className="kol-combo-stripe-group kol-combo-stripe-group--1">
151
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: accent }} />
152
+ <div className="kol-combo-stripe-seg kol-combo-stripe-seg--1" style={{ background: dark }} />
153
+ </div>
154
+ <span className="kol-combo-stripe-method">Method 02</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ )
159
+ }
160
+
161
+ /* ────────── Applied card — brand-application "business card" mockup ────────── */
162
+ export function AppliedCard({ palette, logo }) {
163
+ const { primary, secondary, light, dark, accent } = palette
164
+ return (
165
+ <div className="kol-combo-stage kol-combo-frame kol-combo-stage--applied" style={{ background: light }}>
166
+ {/* Main logo plate — the card front */}
167
+ <div className="kol-combo-applied-plate" style={{ background: primary, color: fgOn(primary) }}>
168
+ <LogoSlot logo={logo} size={72} />
169
+ <span className="kol-combo-label">Front</span>
170
+ </div>
171
+ {/* Right swatch column — surface + accent chips + bands */}
172
+ <div className="kol-combo-applied-col">
173
+ <div className="kol-combo-slab kol-combo-slab--between kol-combo-applied-surface" style={{ background: light, color: fgOn(light) }}>
174
+ <span className="kol-combo-label">Surface</span>
175
+ <div className="kol-combo-applied-accent-strip">
176
+ <span className="kol-combo-applied-accent-chip" style={{ background: secondary }} />
177
+ <span className="kol-combo-applied-accent-chip" style={{ background: accent }} />
178
+ </div>
179
+ </div>
180
+ <div className="kol-combo-applied-band kol-combo-applied-band--lg" style={{ background: secondary }} />
181
+ <div className="kol-combo-applied-band kol-combo-applied-band--sm" style={{ background: dark }} />
182
+ </div>
183
+ </div>
184
+ )
185
+ }
186
+
187
+ /* Layout registry — id → component. */
188
+ export const LAYOUT_COMPONENTS = {
189
+ 'ratio-603010': RatioBar,
190
+ 'tower': Tower,
191
+ 'quad-split': QuadSplit,
192
+ 'card-row': CardRow,
193
+ 'stripe-row': StripeRow,
194
+ 'applied-card': AppliedCard,
195
+ }
196
+
197
+ /* Layout metadata — id + label for pickers. Kept beside the registry so the
198
+ two can't drift. */
199
+ export const COMBO_LAYOUTS = [
200
+ { id: 'ratio-603010', label: '60 / 30 / 10' },
201
+ { id: 'tower', label: 'Tower' },
202
+ { id: 'quad-split', label: 'Quad split' },
203
+ { id: 'card-row', label: 'Card row' },
204
+ { id: 'stripe-row', label: 'Stripe row' },
205
+ { id: 'applied-card', label: 'Applied card' },
206
+ ]
@@ -0,0 +1,281 @@
1
+ /**
2
+ * comboMath — palette engine for <ComboLab/> (plain JS, framework-agnostic).
3
+ *
4
+ * Raided from apps/generator palette mode (colorMath.js + the `fgOn` chooser
5
+ * from palettes.js). Two generation paths:
6
+ * - Pool-based — sample a caller-supplied array of hex, OR pick a random
7
+ * base from it and derive the other slots via HSL math.
8
+ * - Seed-based — slot 0 = a base color, slots 1–4 derived via the mode.
9
+ *
10
+ * DECOUPLING NOTE: the monorepo original resolved its color pools live from
11
+ * CSS custom properties (`resolveCssVar` in pools.js). That resolver is
12
+ * dropped here — it duplicates the DS token layer and forces a DOM dependency
13
+ * on a plain helper. <ComboLab/> is fully data-injected instead: the caller
14
+ * passes literal palettes and/or a `pool` array of hex, so this module never
15
+ * reads the CSS-var cascade.
16
+ */
17
+
18
+ /* ────────── HSL <-> hex ────────── */
19
+
20
+ export function hexToHsl(hex) {
21
+ const r = parseInt(hex.slice(1, 3), 16) / 255
22
+ const g = parseInt(hex.slice(3, 5), 16) / 255
23
+ const b = parseInt(hex.slice(5, 7), 16) / 255
24
+ const max = Math.max(r, g, b)
25
+ const min = Math.min(r, g, b)
26
+ let h = 0, s = 0
27
+ const l = (max + min) / 2
28
+ if (max !== min) {
29
+ const d = max - min
30
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
31
+ switch (max) {
32
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break
33
+ case g: h = (b - r) / d + 2; break
34
+ case b: h = (r - g) / d + 4; break
35
+ }
36
+ h *= 60
37
+ }
38
+ return { h, s: s * 100, l: l * 100 }
39
+ }
40
+
41
+ export function hslToHex(h, s, l) {
42
+ s = Math.max(0, Math.min(100, s)) / 100
43
+ l = Math.max(0, Math.min(100, l)) / 100
44
+ const c = (1 - Math.abs(2 * l - 1)) * s
45
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
46
+ const m = l - c / 2
47
+ let r = 0, g = 0, b = 0
48
+ if (h < 60) { r = c; g = x; b = 0 }
49
+ else if (h < 120) { r = x; g = c; b = 0 }
50
+ else if (h < 180) { r = 0; g = c; b = x }
51
+ else if (h < 240) { r = 0; g = x; b = c }
52
+ else if (h < 300) { r = x; g = 0; b = c }
53
+ else { r = c; g = 0; b = x }
54
+ const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, '0')
55
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
56
+ }
57
+
58
+ /* ────────── Foreground chooser (WCAG-ish luminance flip) ────────── */
59
+
60
+ /**
61
+ * Pick the text color with the best contrast against a background hex.
62
+ * Used by every layout primitive to keep labels legible on any slot color.
63
+ */
64
+ export function fgOn(bg) {
65
+ const hex = bg.replace('#', '')
66
+ const r = parseInt(hex.slice(0, 2), 16)
67
+ const g = parseInt(hex.slice(2, 4), 16)
68
+ const b = parseInt(hex.slice(4, 6), 16)
69
+ const L = (0.299 * r + 0.587 * g + 0.114 * b) / 255
70
+ return L > 0.55 ? '#0E0E11' : '#FAFAFA'
71
+ }
72
+
73
+ /* ────────── sampling helpers ────────── */
74
+
75
+ const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)]
76
+ const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5)
77
+ const fillTo = (arr, n) => {
78
+ if (arr.length >= n) return arr.slice(0, n)
79
+ const out = [...arr]
80
+ while (out.length < n) out.push(pickRandom(arr))
81
+ return out
82
+ }
83
+
84
+ /* ────────── Pool-based modes — pick a random base from pool, derive 6 ────────── */
85
+
86
+ const MODES = {
87
+ random: (pool) => fillTo(shuffle(pool), 6),
88
+
89
+ monochromatic: (pool) => {
90
+ const { h, s } = hexToHsl(pickRandom(pool))
91
+ return [10, 25, 40, 55, 70, 85].map((l) => hslToHex(h, s, l))
92
+ },
93
+
94
+ analogous: (pool) => {
95
+ const { h, s, l } = hexToHsl(pickRandom(pool))
96
+ return [-45, -25, -10, 10, 25, 45].map((o) => hslToHex((h + o + 360) % 360, s, l))
97
+ },
98
+
99
+ complementary: (pool) => {
100
+ const { h, s, l } = hexToHsl(pickRandom(pool))
101
+ return [
102
+ hslToHex(h, s, Math.max(15, l - 25)),
103
+ hslToHex(h, s, l),
104
+ hslToHex(h, s, Math.min(85, l + 25)),
105
+ hslToHex((h + 180) % 360, s, Math.max(15, l - 15)),
106
+ hslToHex((h + 180) % 360, s, l),
107
+ hslToHex((h + 180) % 360, s, Math.min(85, l + 15)),
108
+ ]
109
+ },
110
+
111
+ triadic: (pool) => {
112
+ const { h, s, l } = hexToHsl(pickRandom(pool))
113
+ return [
114
+ hslToHex(h, s, l),
115
+ hslToHex((h + 120) % 360, s, l),
116
+ hslToHex((h + 240) % 360, s, l),
117
+ hslToHex(h, s, Math.min(85, l + 20)),
118
+ hslToHex((h + 120) % 360, s, Math.max(15, l - 20)),
119
+ hslToHex((h + 240) % 360, s, Math.min(85, l + 20)),
120
+ ]
121
+ },
122
+
123
+ doubleComplementary: (pool) => {
124
+ const { h, s, l } = hexToHsl(pickRandom(pool))
125
+ return [
126
+ hslToHex(h, s, l),
127
+ hslToHex((h + 90) % 360, s, l),
128
+ hslToHex((h + 180) % 360, s, l),
129
+ hslToHex((h + 270) % 360, s, l),
130
+ hslToHex(h, s, Math.min(85, l + 20)),
131
+ hslToHex((h + 180) % 360, s, Math.max(15, l - 20)),
132
+ ]
133
+ },
134
+ }
135
+
136
+ /* ────────── Seed-based modes — slot 0 = base, slots 1-5 jittered derivations ────────── */
137
+
138
+ const jitter = (range) => (Math.random() - 0.5) * 2 * range
139
+
140
+ const SEED_MODES = {
141
+ random: (base) => {
142
+ const { h, s, l } = hexToHsl(base)
143
+ return [
144
+ base,
145
+ hslToHex((h + Math.random() * 360) % 360, s, l),
146
+ hslToHex((h + Math.random() * 360) % 360, s, l),
147
+ hslToHex(h, s, Math.max(10, l - 25 + jitter(10))),
148
+ hslToHex(h, s, Math.min(90, l + 25 + jitter(10))),
149
+ hslToHex(h, s * (0.4 + Math.random() * 0.4), l),
150
+ ]
151
+ },
152
+
153
+ monochromatic: (base) => {
154
+ const { h, s, l } = hexToHsl(base)
155
+ const j = 8
156
+ return [
157
+ base,
158
+ hslToHex(h, s, Math.max(10, l - 30 + jitter(j))),
159
+ hslToHex(h, s, Math.max(20, l - 15 + jitter(j))),
160
+ hslToHex(h, s, Math.min(85, l + 15 + jitter(j))),
161
+ hslToHex(h, s, Math.min(95, l + 30 + jitter(j))),
162
+ hslToHex(h, s, Math.min(98, l + 45 + jitter(j))),
163
+ ]
164
+ },
165
+
166
+ analogous: (base) => {
167
+ const { h, s, l } = hexToHsl(base)
168
+ return [
169
+ base,
170
+ hslToHex((h - 30 + jitter(8) + 360) % 360, s, l),
171
+ hslToHex((h - 15 + jitter(8) + 360) % 360, s, l),
172
+ hslToHex((h + 15 + jitter(8)) % 360, s, l),
173
+ hslToHex((h + 30 + jitter(8)) % 360, s, l),
174
+ hslToHex((h + 45 + jitter(8)) % 360, s, l),
175
+ ]
176
+ },
177
+
178
+ complementary: (base) => {
179
+ const { h, s, l } = hexToHsl(base)
180
+ return [
181
+ base,
182
+ hslToHex(h, s, Math.max(15, l - 25 + jitter(8))),
183
+ hslToHex(h, s, Math.min(85, l + 25 + jitter(8))),
184
+ hslToHex((h + 180 + jitter(10)) % 360, s, l),
185
+ hslToHex((h + 180 + jitter(10)) % 360, s, Math.max(15, l - 15 + jitter(8))),
186
+ hslToHex((h + 180 + jitter(10)) % 360, s, Math.min(85, l + 15 + jitter(8))),
187
+ ]
188
+ },
189
+
190
+ triadic: (base) => {
191
+ const { h, s, l } = hexToHsl(base)
192
+ return [
193
+ base,
194
+ hslToHex((h + 120 + jitter(10)) % 360, s, l),
195
+ hslToHex((h + 240 + jitter(10)) % 360, s, l),
196
+ hslToHex(h, s, Math.min(85, l + 20 + jitter(8))),
197
+ hslToHex((h + 120 + jitter(10)) % 360, s, Math.max(15, l - 20 + jitter(8))),
198
+ hslToHex((h + 240 + jitter(10)) % 360, s, Math.min(85, l + 20 + jitter(8))),
199
+ ]
200
+ },
201
+
202
+ doubleComplementary: (base) => {
203
+ const { h, s, l } = hexToHsl(base)
204
+ return [
205
+ base,
206
+ hslToHex((h + 90 + jitter(10)) % 360, s, l),
207
+ hslToHex((h + 180 + jitter(10)) % 360, s, l),
208
+ hslToHex((h + 270 + jitter(10)) % 360, s, l),
209
+ hslToHex(h, s, Math.min(85, l + 20 + jitter(8))),
210
+ hslToHex((h + 180 + jitter(10)) % 360, s, Math.max(15, l - 20 + jitter(8))),
211
+ ]
212
+ },
213
+ }
214
+
215
+ /* Mode metadata for a caller-supplied mode picker (pure data, no CSS). */
216
+ export const GENERATION_MODES = [
217
+ { id: 'random', label: 'Random' },
218
+ { id: 'monochromatic', label: 'Mono' },
219
+ { id: 'analogous', label: 'Analogous' },
220
+ { id: 'complementary', label: 'Complement' },
221
+ { id: 'triadic', label: 'Triadic' },
222
+ { id: 'doubleComplementary', label: 'Double comp' },
223
+ ]
224
+
225
+ /**
226
+ * Pick a 6th (background) color unused by the 5 layout slots. Falls back to an
227
+ * HSL lightness shift when the pool is exhausted.
228
+ */
229
+ function pickBgColor(layoutColors, pool) {
230
+ const used = new Set(layoutColors.map((c) => c.toUpperCase()))
231
+ const candidates = pool.filter((c) => !used.has(c.toUpperCase()))
232
+ if (candidates.length > 0) return pickRandom(candidates)
233
+
234
+ const base = pickRandom(pool)
235
+ const { h, s, l } = hexToHsl(base)
236
+ for (const shift of [-20, 20, -35, 35, -50, 50]) {
237
+ const candidate = hslToHex(h, s, Math.max(5, Math.min(95, l + shift))).toUpperCase()
238
+ if (!used.has(candidate)) return candidate
239
+ }
240
+ return hslToHex(h, s, l > 50 ? 5 : 95)
241
+ }
242
+
243
+ /** Background for a seed-derived palette — a lightness shift off the seed. */
244
+ function pickBgFromSeed(layoutColors, baseColor) {
245
+ const used = new Set(layoutColors.map((c) => c.toUpperCase()))
246
+ const { h, s, l } = hexToHsl(baseColor)
247
+ for (const shift of [-50, 50, -35, 35, -20, 20]) {
248
+ const candidate = hslToHex(h, s, Math.max(5, Math.min(95, l + shift))).toUpperCase()
249
+ if (!used.has(candidate)) return candidate
250
+ }
251
+ return hslToHex(h, s, l > 50 ? 5 : 95)
252
+ }
253
+
254
+ /**
255
+ * Generate 6 colors `[primary, secondary, light, dark, accent, bg]`,
256
+ * preserving any locked slots.
257
+ *
258
+ * - `baseColor` set → seed path: slots derived from the base via SEED_MODES.
259
+ * - `baseColor` null → pool path: sample `pool` via MODES.
260
+ *
261
+ * @param {string[]} pool Hex array to sample (pool path).
262
+ * @param {string} mode Mode id (see GENERATION_MODES).
263
+ * @param {string[]} currentColors Current 6 colors (used for locked slots).
264
+ * @param {boolean[]} locks Per-slot lock flags.
265
+ * @param {string} [baseColor] Seed hex — switches to the seed path.
266
+ */
267
+ export function generatePalette(pool, mode, currentColors, locks, baseColor) {
268
+ let layoutColors
269
+ let bgColor
270
+ if (baseColor) {
271
+ const gen = SEED_MODES[mode] ?? SEED_MODES.random
272
+ layoutColors = gen(baseColor).slice(0, 5)
273
+ bgColor = pickBgFromSeed(layoutColors, baseColor)
274
+ } else {
275
+ const gen = MODES[mode] ?? MODES.random
276
+ layoutColors = gen(pool).slice(0, 5)
277
+ bgColor = pickBgColor(layoutColors, pool)
278
+ }
279
+ const final = [...layoutColors, bgColor]
280
+ return final.map((c, i) => (locks && locks[i] ? currentColors[i] : c))
281
+ }
package/src/index.js ADDED
@@ -0,0 +1,28 @@
1
+ // @kolkrabbi/kol-styleguide — the brand style-guide component set.
2
+ //
3
+ // Visual specimens a brand manual is built from — colour anatomy + the
4
+ // combination lab, logo construction / clearspace / scaling, mood tiles,
5
+ // type blocks, asset spec tables. Raided from the monorepo apps/brand
6
+ // styleguide (2026-07). Styling ships in @kolkrabbi/kol-theme
7
+ // (kol-components-styleguide.css, layer components); data is consumer-injected.
8
+ //
9
+ // Exports are appended as each component lands.
10
+ export { default as MoodTile } from './MoodTile.jsx'
11
+ export { default as ColorAnatomy } from './ColorAnatomy.jsx'
12
+ export { default as TypeBlock } from './TypeBlock.jsx'
13
+ export { default as AssetTable } from './AssetTable.jsx'
14
+ export { WEIGHTS, CUTS, CASES, familyFor } from './typographyCuts.js'
15
+ export { default as LogoCard } from './LogoCard.jsx'
16
+ export { default as ClearspaceDiagram, hasFramework } from './ClearspaceDiagram.jsx'
17
+ export { default as LogoScaling } from './LogoScaling.jsx'
18
+ export { default as ComboLab, DEFAULT_PALETTE } from './ComboLab.jsx'
19
+ export {
20
+ RatioBar, Tower, QuadSplit, CardRow, StripeRow, AppliedCard,
21
+ LAYOUT_COMPONENTS, COMBO_LAYOUTS,
22
+ } from './comboLayouts.jsx'
23
+ export { generatePalette, fgOn, hexToHsl, hslToHex, GENERATION_MODES } from './comboMath.js'
24
+
25
+ // Re-exported styleguide primitives — live in the core packages, surfaced here
26
+ // so a brand guide imports from one place. Not duplicated: same modules.
27
+ export { AssetGrid, FeatureSplit, ProsePreview, SpectrumGrid } from '@kolkrabbi/kol-component'
28
+ export { TypeSample, TypeSpecCard } from '@kolkrabbi/kol-foundry'