@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.
- package/README.md +15 -0
- package/package.json +43 -0
- package/src/AssetTable.jsx +126 -0
- package/src/ClearspaceDiagram.jsx +54 -0
- package/src/ColorAnatomy.jsx +33 -0
- package/src/ComboLab.jsx +194 -0
- package/src/LogoCard.jsx +90 -0
- package/src/LogoScaling.jsx +95 -0
- package/src/MoodTile.jsx +42 -0
- package/src/TypeBlock.jsx +89 -0
- package/src/comboLayouts.jsx +206 -0
- package/src/comboMath.js +281 -0
- package/src/index.js +28 -0
- package/src/typographyCuts.js +68 -0
package/src/MoodTile.jsx
ADDED
|
@@ -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
|
+
]
|
package/src/comboMath.js
ADDED
|
@@ -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'
|