@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/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @kolkrabbi/kol-styleguide
|
|
2
|
+
|
|
3
|
+
The KOL **style-guide / brand-guide** component set — the visual specimens a brand manual is built from. Raided from the monorepo `apps/brand` styleguide and assembled here because a brand guide is a shared capability with its own cadence, distinct from the core UI atoms.
|
|
4
|
+
|
|
5
|
+
Colour anatomy + the **combination lab** (60/30/10 palette × layout × logo, incl. applied-brand mockups), **logo** construction / clearspace / scaling, **mood tiles**, **type blocks**, and **asset spec tables** — plus the colour/type specimen primitives (`ColorRamp`, `ColorSwatch`, `SpectrumGrid`, `TypeSample`, `TypeSpecCard`, `ProsePreview`, `AssetGrid`, `FeatureSplit`) gathered from the core packages.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { ComboLab, MoodTile, LogoCard, TypeBlock } from '@kolkrabbi/kol-styleguide'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Consumer requirements
|
|
12
|
+
|
|
13
|
+
- **CSS** ships in `@kolkrabbi/kol-theme` (`kol-components-styleguide.css`, in the theme aggregate) — this package ships JS only.
|
|
14
|
+
- Tailwind v4 `@source "…/node_modules/@kolkrabbi/kol-styleguide/src"` so utility classes used inside the components are generated (Tailwind skips `node_modules`, or they never generate).
|
|
15
|
+
- Data is consumer-injected — palettes, type scales, logo assets are supplied as flat props.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kolkrabbi/kol-styleguide",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "KOL style-guide / brand-guide component set — the visual specimens a brand manual is built from: color anatomy + combination lab, logo construction / clearspace / scaling, mood tiles, type blocks, and asset spec tables. Raided from the apps/brand styleguide. Sits above @kolkrabbi/kol-{theme,icons,component}.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"module": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@kolkrabbi/kol-component": "0.9.0",
|
|
15
|
+
"@kolkrabbi/kol-icons": "0.5.0",
|
|
16
|
+
"@kolkrabbi/kol-foundry": "0.4.0",
|
|
17
|
+
"@kolkrabbi/kol-theme": "0.7.4"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.3.0 || ^19.0.0",
|
|
21
|
+
"react-dom": "^18.3.0 || ^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"kol",
|
|
29
|
+
"kolkrabbi",
|
|
30
|
+
"design-system",
|
|
31
|
+
"styleguide",
|
|
32
|
+
"brand-guide",
|
|
33
|
+
"specimen"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Tor-Grimsson/kol-ds.git",
|
|
38
|
+
"directory": "packages/styleguide"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Table } from '@kolkrabbi/kol-component'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AssetTable — mark / asset spec table.
|
|
6
|
+
*
|
|
7
|
+
* A thin composition of the DS `Table` (@kolkrabbi/kol-component) that renders
|
|
8
|
+
* a download-oriented asset manifest: preview, name, format, dimensions, and a
|
|
9
|
+
* download control per row. Rows are consumer-injected; the standard column set
|
|
10
|
+
* is built here so a consumer only supplies data.
|
|
11
|
+
*
|
|
12
|
+
* Ported from the monorepo apps/brand styleguide AssetTable (2026-07). All
|
|
13
|
+
* app-specific wiring is dropped: the `import.meta.glob` SVG harvesting, the
|
|
14
|
+
* KolLogo / Graphic loaders, the ink-token toggle, the fullscreen overlay, and
|
|
15
|
+
* the `Blob` recolour-and-download. Download is now a plain per-row seam —
|
|
16
|
+
* pass `href` (a download link) OR `onDownload` (a callback). Preview is a
|
|
17
|
+
* consumer-supplied node.
|
|
18
|
+
*
|
|
19
|
+
* Columns adapt to the data: the Preview column appears only when a row carries
|
|
20
|
+
* a `preview` node, and the Download column only when a row carries `href` or
|
|
21
|
+
* `onDownload`. Name / format / dimensions always show, falling back to an
|
|
22
|
+
* em-dash (DS Table behaviour) when a field is absent.
|
|
23
|
+
*
|
|
24
|
+
* Row shape:
|
|
25
|
+
* { id, name, preview?, format?, dimensions?, href?, filename?, onDownload? }
|
|
26
|
+
* id React key (falls back to array index)
|
|
27
|
+
* name asset name (string or node)
|
|
28
|
+
* preview specimen node rendered in the Preview cell
|
|
29
|
+
* format e.g. 'SVG' · 'PNG @2x'
|
|
30
|
+
* dimensions e.g. '48 × 48'
|
|
31
|
+
* href download URL → renders an <a download>
|
|
32
|
+
* filename download filename for the <a download> attribute
|
|
33
|
+
* onDownload click handler → renders a <button> (takes precedence over href)
|
|
34
|
+
*
|
|
35
|
+
* Props:
|
|
36
|
+
* rows Array<row> (required)
|
|
37
|
+
* caption sr-only <caption> for the table (a11y)
|
|
38
|
+
* variant 'default' | 'simple' → forwarded to DS Table
|
|
39
|
+
* className extra classes on the table wrapper
|
|
40
|
+
* emptyLabel message shown when rows is empty (default 'No assets.')
|
|
41
|
+
*/
|
|
42
|
+
export default function AssetTable({
|
|
43
|
+
rows = [],
|
|
44
|
+
caption,
|
|
45
|
+
variant = 'default',
|
|
46
|
+
className = '',
|
|
47
|
+
emptyLabel = 'No assets.',
|
|
48
|
+
}) {
|
|
49
|
+
if (!rows.length) {
|
|
50
|
+
return <p className="kol-mono-14 text-fg-48">{emptyLabel}</p>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const withId = rows.map((row, i) => ({ ...row, id: row.id ?? i }))
|
|
54
|
+
const hasPreview = withId.some((row) => row.preview != null)
|
|
55
|
+
const hasDownload = withId.some((row) => row.href || row.onDownload)
|
|
56
|
+
|
|
57
|
+
const columns = [
|
|
58
|
+
hasPreview && {
|
|
59
|
+
accessor: 'preview',
|
|
60
|
+
header: 'Preview',
|
|
61
|
+
className: 'kol-table-cell-text',
|
|
62
|
+
style: { minWidth: 160 },
|
|
63
|
+
render: (row) => row.preview ?? null,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
accessor: 'name',
|
|
67
|
+
header: 'Name',
|
|
68
|
+
className: 'kol-table-cell-text',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
accessor: 'format',
|
|
72
|
+
header: 'Format',
|
|
73
|
+
className: 'kol-table-cell-meta',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
accessor: 'dimensions',
|
|
77
|
+
header: 'Dimensions',
|
|
78
|
+
className: 'kol-table-cell-meta',
|
|
79
|
+
},
|
|
80
|
+
hasDownload && {
|
|
81
|
+
accessor: 'download',
|
|
82
|
+
header: 'Download',
|
|
83
|
+
style: { width: 96 },
|
|
84
|
+
render: (row) => <DownloadControl row={row} />,
|
|
85
|
+
},
|
|
86
|
+
].filter(Boolean)
|
|
87
|
+
|
|
88
|
+
return <Table caption={caption} columns={columns} rows={withId} variant={variant} className={className} />
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Per-row download affordance. onDownload → button; href → anchor; else nothing.
|
|
92
|
+
* The DS `download` icon inherits currentColor; aria-label composes the asset
|
|
93
|
+
* name for screen readers (an a11y string, not rendered content). */
|
|
94
|
+
function DownloadControl({ row }) {
|
|
95
|
+
const label = `Download ${typeof row.name === 'string' ? row.name : 'asset'}`
|
|
96
|
+
|
|
97
|
+
if (row.onDownload) {
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => row.onDownload(row)}
|
|
102
|
+
className="inline-flex items-center text-fg-64"
|
|
103
|
+
aria-label={label}
|
|
104
|
+
title="Download"
|
|
105
|
+
>
|
|
106
|
+
<Icon name="download" size={16} />
|
|
107
|
+
</button>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (row.href) {
|
|
112
|
+
return (
|
|
113
|
+
<a
|
|
114
|
+
href={row.href}
|
|
115
|
+
download={row.filename ?? true}
|
|
116
|
+
className="inline-flex items-center text-fg-64"
|
|
117
|
+
aria-label={label}
|
|
118
|
+
title="Download"
|
|
119
|
+
>
|
|
120
|
+
<Icon name="download" size={16} />
|
|
121
|
+
</a>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClearspaceDiagram — layered logo construction overlay.
|
|
3
|
+
*
|
|
4
|
+
* Stacks up to three absolutely-positioned layers inside one box:
|
|
5
|
+
* grid (construction modules) → keyline (x-height / clearspace guides) → logo.
|
|
6
|
+
*
|
|
7
|
+
* All three layers are CONSUMER-INJECTED rendered nodes. The DS bakes in no
|
|
8
|
+
* brand mark and no SVG registry — a generic grid can't align to an unknown
|
|
9
|
+
* mark, so the consumer (who owns the mark) also owns the construction geometry,
|
|
10
|
+
* authored to match that mark's viewBox. This is the asset-agnostic mirror of
|
|
11
|
+
* the monorepo original, which pulled `GRAPHIC_RAW.structure` (diagram-grid-*,
|
|
12
|
+
* diagram-x-*, diagram-logo-*) and injected the raw strings itself.
|
|
13
|
+
*
|
|
14
|
+
* `framework` toggles the two overlay layers; the logo always renders. Each
|
|
15
|
+
* layer fills the box and its descendant <svg> is stretched to fit
|
|
16
|
+
* (`[&_svg]:w-full [&_svg]:h-full`) — inject wrapped nodes (e.g. kol-brand
|
|
17
|
+
* <Asset>, which renders <span><svg/></span>) or pre-sized svg nodes.
|
|
18
|
+
*
|
|
19
|
+
* import { ClearspaceDiagram } from '@kolkrabbi/kol-styleguide'
|
|
20
|
+
* import { Asset } from '@kolkrabbi/kol-brand/svg'
|
|
21
|
+
* <ClearspaceDiagram
|
|
22
|
+
* framework
|
|
23
|
+
* logo={<Asset name="kol-logomark" />}
|
|
24
|
+
* grid={<Asset name="diagram-grid-logomark" />}
|
|
25
|
+
* keyline={<Asset name="diagram-x-logomark" />}
|
|
26
|
+
* />
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const LAYER = 'absolute inset-0 [&_svg]:w-full [&_svg]:h-full'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when both overlay layers are present — the injected-node equivalent of
|
|
33
|
+
* the monorepo `hasFramework(variant)` (which checked the grid + x diagrams
|
|
34
|
+
* existed for a variant). Consumers gate a clearspace toggle on this.
|
|
35
|
+
*/
|
|
36
|
+
export const hasFramework = ({ grid, keyline } = {}) => Boolean(grid && keyline)
|
|
37
|
+
|
|
38
|
+
export default function ClearspaceDiagram({
|
|
39
|
+
logo,
|
|
40
|
+
grid = null,
|
|
41
|
+
keyline = null,
|
|
42
|
+
framework = false,
|
|
43
|
+
className = '',
|
|
44
|
+
}) {
|
|
45
|
+
if (!logo) return null
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={`kol-clearspace-diagram relative w-full h-full ${className}`.trim()}>
|
|
49
|
+
{framework && grid && <span className={LAYER}>{grid}</span>}
|
|
50
|
+
{framework && keyline && <span className={LAYER}>{keyline}</span>}
|
|
51
|
+
<span className={LAYER}>{logo}</span>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorAnatomy — token-composition specimen.
|
|
3
|
+
*
|
|
4
|
+
* A figure that shows a color `sample`, the inline `<code>` token expression
|
|
5
|
+
* that produced it (e.g. `color-mix(in srgb, var(--kol-fg-16), transparent)`
|
|
6
|
+
* or a bare `--kol-fg-48`), and an italic figcaption describing it. Used in
|
|
7
|
+
* the brand/style-guide to document how a composed color is built from tokens.
|
|
8
|
+
*
|
|
9
|
+
* The surviving CSS owns the type/chrome for the two text descendants — it is
|
|
10
|
+
* fully token-based (var(--kol-fg-48), var(--kol-font-family-mono)) — so it is
|
|
11
|
+
* reused as-is rather than re-declared inline (chrome lives in
|
|
12
|
+
* @kolkrabbi/kol-theme → packages/framework/kol-framework.css):
|
|
13
|
+
* .kol-anatomy-sample figcaption — 12px, italic, var(--kol-fg-48)
|
|
14
|
+
* .kol-anatomy-sample code — 11px mono chip, currentColor 18% tint, r3
|
|
15
|
+
*
|
|
16
|
+
* Props:
|
|
17
|
+
* sample — sample node rendered as-is (full control of size/shape). When
|
|
18
|
+
* omitted, a DS <ColorSwatch> is rendered from `hex`.
|
|
19
|
+
* hex — color string for the fallback ColorSwatch (ignored if `sample`).
|
|
20
|
+
* code — token / color-mix expression string, shown in the inline <code>.
|
|
21
|
+
* caption — italic figcaption description. Author its casing at the call site.
|
|
22
|
+
*/
|
|
23
|
+
import { ColorSwatch } from '@kolkrabbi/kol-component'
|
|
24
|
+
|
|
25
|
+
export default function ColorAnatomy({ sample, hex, code, caption }) {
|
|
26
|
+
return (
|
|
27
|
+
<figure className="kol-anatomy-sample flex flex-col items-start gap-2">
|
|
28
|
+
{sample ?? <ColorSwatch hex={hex} size={96} radius="sm" />}
|
|
29
|
+
{code && <code>{code}</code>}
|
|
30
|
+
{caption && <figcaption>{caption}</figcaption>}
|
|
31
|
+
</figure>
|
|
32
|
+
)
|
|
33
|
+
}
|
package/src/ComboLab.jsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ViewToggle } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { LAYOUT_COMPONENTS, COMBO_LAYOUTS } from './comboLayouts.jsx'
|
|
4
|
+
import { generatePalette } from './comboMath.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ComboLab — the 60 / 30 / 10 color-combination playground.
|
|
8
|
+
*
|
|
9
|
+
* A layout picker + logo toggle + randomize over a role-palette
|
|
10
|
+
* (primary / secondary / light / dark / accent), a live stage that renders the
|
|
11
|
+
* selected layout primitive, and a hex readout. The "Applied card" layout is
|
|
12
|
+
* the brand-application ("business card") mockup.
|
|
13
|
+
*
|
|
14
|
+
* Fully data-injected — nothing is read from an editor store:
|
|
15
|
+
* - `palette` — the initial role-palette (defaults to the Kolkrabbi comp).
|
|
16
|
+
* - `palettes` — optional named set; when present a Palette toggle appears
|
|
17
|
+
* and Randomize picks among them.
|
|
18
|
+
* - `pool` — optional hex array; when present Randomize *generates* a new
|
|
19
|
+
* palette by sampling it (see comboMath). With neither
|
|
20
|
+
* `palettes` nor `pool`, Randomize derives a fresh palette from
|
|
21
|
+
* the current primary (seed mode).
|
|
22
|
+
* - `logo` — a React node dropped into the logo slots.
|
|
23
|
+
*
|
|
24
|
+
* Chrome (borders, type, state) is DS-owned via `.kol-combo-*` +
|
|
25
|
+
* `kol-helper-*` + `text-*` utilities; the only inline values are the palette's
|
|
26
|
+
* literal hex, which are content, not tokens.
|
|
27
|
+
*
|
|
28
|
+
* See the monorepo severance notes in lobby/ComboLab.md.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/* Canonical Kolkrabbi composition. Literal hex (this is a plain module with no
|
|
32
|
+
CSS-var resolver) mirroring the DS brand tokens:
|
|
33
|
+
yellow-300 / red-200 / cream-100 / blue-400 / orange-300. */
|
|
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
|
+
}
|
|
44
|
+
|
|
45
|
+
const ROLE_LABELS = ['Primary', 'Secondary', 'Light', 'Dark', 'Accent']
|
|
46
|
+
|
|
47
|
+
const toOptions = (items) => items.map((i) => ({ value: i.id, label: i.label }))
|
|
48
|
+
const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)]
|
|
49
|
+
|
|
50
|
+
/* Pick a random item whose id differs from `currentId` where possible. */
|
|
51
|
+
const pickDifferent = (arr, currentId) => {
|
|
52
|
+
const others = arr.filter((i) => i.id !== currentId)
|
|
53
|
+
return others.length ? pickRandom(others) : arr[0]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* [primary, secondary, light, dark, accent, bg?] → role-palette object. */
|
|
57
|
+
const toPalette = (colors) => ({
|
|
58
|
+
id: 'generated',
|
|
59
|
+
label: 'Generated',
|
|
60
|
+
description: 'Generated combination',
|
|
61
|
+
primary: colors[0],
|
|
62
|
+
secondary: colors[1],
|
|
63
|
+
light: colors[2],
|
|
64
|
+
dark: colors[3],
|
|
65
|
+
accent: colors[4],
|
|
66
|
+
bg: colors[5],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const paletteToColors = (p) => [p.primary, p.secondary, p.light, p.dark, p.accent, p.bg ?? p.light]
|
|
70
|
+
|
|
71
|
+
function Row({ label, children }) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="kol-combo-row">
|
|
74
|
+
<span className="kol-combo-row-label kol-helper-10 text-meta">{label}</span>
|
|
75
|
+
<div className="kol-combo-row-controls">{children}</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SwatchRow({ label, hex }) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="kol-combo-swatch-row">
|
|
83
|
+
<span className="kol-combo-swatch-chip" style={{ background: hex }} />
|
|
84
|
+
<span className="kol-combo-swatch-label kol-helper-12 text-auto">{label}</span>
|
|
85
|
+
<span className="kol-combo-swatch-hex kol-helper-12 text-meta">{hex.toUpperCase()}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function ComboLab({
|
|
91
|
+
palette,
|
|
92
|
+
palettes,
|
|
93
|
+
layout,
|
|
94
|
+
layouts = COMBO_LAYOUTS,
|
|
95
|
+
logo = null,
|
|
96
|
+
pool,
|
|
97
|
+
mode = 'random',
|
|
98
|
+
className = '',
|
|
99
|
+
}) {
|
|
100
|
+
const initialPalette = palette ?? palettes?.[0] ?? DEFAULT_PALETTE
|
|
101
|
+
|
|
102
|
+
const [layoutId, setLayoutId] = useState(layout ?? layouts[0]?.id ?? COMBO_LAYOUTS[0].id)
|
|
103
|
+
const [active, setActive] = useState(initialPalette)
|
|
104
|
+
const [logoOn, setLogoOn] = useState(Boolean(logo))
|
|
105
|
+
|
|
106
|
+
const LayoutComponent = LAYOUT_COMPONENTS[layoutId] ?? LAYOUT_COMPONENTS[COMBO_LAYOUTS[0].id]
|
|
107
|
+
const activeLayout = layouts.find((l) => l.id === layoutId) ?? layouts[0] ?? COMBO_LAYOUTS[0]
|
|
108
|
+
const colorsKey = paletteToColors(active).join('')
|
|
109
|
+
|
|
110
|
+
const randomize = () => {
|
|
111
|
+
setLayoutId(pickDifferent(layouts, layoutId).id)
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(palettes) && palettes.length) {
|
|
114
|
+
setActive(pickDifferent(palettes, active.id))
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const noLocks = [false, false, false, false, false, false]
|
|
119
|
+
const current = paletteToColors(active)
|
|
120
|
+
const colors = Array.isArray(pool) && pool.length
|
|
121
|
+
? generatePalette(pool, mode, current, noLocks) // pool path
|
|
122
|
+
: generatePalette([], mode, current, noLocks, active.primary) // seed path
|
|
123
|
+
setActive(toPalette(colors))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className={`kol-combo-lab ${className}`.trim()}>
|
|
128
|
+
<div className="kol-combo-controls">
|
|
129
|
+
<Row label="Layout">
|
|
130
|
+
<ViewToggle
|
|
131
|
+
viewMode={layoutId}
|
|
132
|
+
onViewChange={setLayoutId}
|
|
133
|
+
options={toOptions(layouts)}
|
|
134
|
+
/>
|
|
135
|
+
</Row>
|
|
136
|
+
|
|
137
|
+
{Array.isArray(palettes) && palettes.length > 0 && (
|
|
138
|
+
<Row label="Palette">
|
|
139
|
+
<ViewToggle
|
|
140
|
+
viewMode={active.id}
|
|
141
|
+
onViewChange={(id) => setActive(palettes.find((p) => p.id === id) ?? active)}
|
|
142
|
+
options={toOptions(palettes)}
|
|
143
|
+
/>
|
|
144
|
+
</Row>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{logo && (
|
|
148
|
+
<Row label="Logo">
|
|
149
|
+
<ViewToggle
|
|
150
|
+
variant="single"
|
|
151
|
+
viewMode={logoOn ? 'on' : 'off'}
|
|
152
|
+
onViewChange={(v) => setLogoOn(v === 'on')}
|
|
153
|
+
options={[
|
|
154
|
+
{ value: 'off', label: 'No logo' },
|
|
155
|
+
{ value: 'on', label: 'Logo' },
|
|
156
|
+
]}
|
|
157
|
+
/>
|
|
158
|
+
</Row>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
<div className="kol-combo-footer">
|
|
162
|
+
<button type="button" className="kol-combo-randomize kol-helper-10" onClick={randomize}>
|
|
163
|
+
↻ Randomize
|
|
164
|
+
</button>
|
|
165
|
+
{active.description && (
|
|
166
|
+
<span className="kol-combo-footer-desc kol-helper-10 text-meta">
|
|
167
|
+
{active.description}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="kol-combo-stage-wrap">
|
|
174
|
+
<div key={`${layoutId}-${colorsKey}`} className="kol-combo-stage-anim">
|
|
175
|
+
<LayoutComponent palette={active} logo={logoOn ? logo : null} />
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className="kol-combo-readout">
|
|
180
|
+
{ROLE_LABELS.map((label) => (
|
|
181
|
+
<SwatchRow key={label} label={label} hex={active[label.toLowerCase()]} />
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<p className="kol-combo-summary kol-helper-10 text-meta">
|
|
186
|
+
<span className="text-emphasis">{activeLayout.label}</span>
|
|
187
|
+
{' × '}
|
|
188
|
+
<span className="text-emphasis">{active.label}</span>
|
|
189
|
+
{' × '}
|
|
190
|
+
<span className="text-emphasis">{logoOn ? 'Logo' : 'No logo'}</span>
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|
package/src/LogoCard.jsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import ClearspaceDiagram, { hasFramework } from './ClearspaceDiagram.jsx'
|
|
3
|
+
import { ToggleSwitch } from '@kolkrabbi/kol-component'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LogoCard — framed logo specimen tile with a clearspace / grid toggle.
|
|
7
|
+
*
|
|
8
|
+
* A brand-manual logo plate: the mark centered in a padded frame, an optional
|
|
9
|
+
* caption, and — when construction overlays are supplied — a toggle that reveals
|
|
10
|
+
* the grid + x-height keyline over the mark.
|
|
11
|
+
*
|
|
12
|
+
* ASSET-AGNOSTIC: the mark and its construction overlays are CONSUMER-INJECTED
|
|
13
|
+
* rendered nodes (`logo`, `grid`, `keyline`). The monorepo original imported a
|
|
14
|
+
* specific `KolLogo` and resolved diagrams from `GRAPHIC_RAW.structure`; this
|
|
15
|
+
* DS version bakes in neither — layering is delegated to <ClearspaceDiagram>,
|
|
16
|
+
* which the consumer feeds with their own <Asset>/<Icon>/svg nodes.
|
|
17
|
+
*
|
|
18
|
+
* import { LogoCard } from '@kolkrabbi/kol-styleguide'
|
|
19
|
+
* import { Asset } from '@kolkrabbi/kol-brand/svg'
|
|
20
|
+
* <LogoCard
|
|
21
|
+
* caption="Logomark"
|
|
22
|
+
* aspect="1 / 1"
|
|
23
|
+
* logo={<Asset name="kol-logomark" />}
|
|
24
|
+
* grid={<Asset name="diagram-grid-logomark" />}
|
|
25
|
+
* keyline={<Asset name="diagram-x-logomark" />}
|
|
26
|
+
* />
|
|
27
|
+
*
|
|
28
|
+
* Caption text is authored in the case it should render — the component applies
|
|
29
|
+
* no text-transform (KOL no-auto-casing rule).
|
|
30
|
+
*/
|
|
31
|
+
export default function LogoCard({
|
|
32
|
+
logo,
|
|
33
|
+
grid = null,
|
|
34
|
+
keyline = null,
|
|
35
|
+
caption,
|
|
36
|
+
backdrop,
|
|
37
|
+
aspect = '2 / 1',
|
|
38
|
+
frame = true,
|
|
39
|
+
toggleLabel = 'Clearspace',
|
|
40
|
+
defaultFramework = true,
|
|
41
|
+
className = '',
|
|
42
|
+
}) {
|
|
43
|
+
const [showFramework, setShowFramework] = useState(defaultFramework)
|
|
44
|
+
const canFramework = hasFramework({ grid, keyline })
|
|
45
|
+
|
|
46
|
+
const figureStyle = aspect ? { aspectRatio: aspect } : undefined
|
|
47
|
+
const frameStyle = frame && backdrop ? { background: backdrop } : undefined
|
|
48
|
+
const frameChrome = frame
|
|
49
|
+
? `rounded-[4px] overflow-hidden p-8 ${backdrop ? '' : 'bg-fg-02 border border-fg-04'}`
|
|
50
|
+
: ''
|
|
51
|
+
const figureSizing = aspect ? '' : 'h-full'
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<figure
|
|
55
|
+
className={`kol-logo-card flex flex-col ${figureSizing} ${className}`.trim()}
|
|
56
|
+
style={figureStyle}
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
className={`kol-logo-card-frame flex-1 min-h-0 flex items-center justify-center ${frameChrome}`.trim()}
|
|
60
|
+
style={frameStyle}
|
|
61
|
+
>
|
|
62
|
+
<ClearspaceDiagram
|
|
63
|
+
logo={logo}
|
|
64
|
+
grid={grid}
|
|
65
|
+
keyline={keyline}
|
|
66
|
+
framework={showFramework && canFramework}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
{(caption || canFramework) && (
|
|
70
|
+
<div className="flex items-center justify-between mt-2">
|
|
71
|
+
{caption ? (
|
|
72
|
+
<figcaption className="kol-helper-12 tracking-wider text-meta">
|
|
73
|
+
{caption}
|
|
74
|
+
</figcaption>
|
|
75
|
+
) : (
|
|
76
|
+
<span />
|
|
77
|
+
)}
|
|
78
|
+
{canFramework && (
|
|
79
|
+
<ToggleSwitch
|
|
80
|
+
variant="bare"
|
|
81
|
+
label={toggleLabel}
|
|
82
|
+
checked={showFramework}
|
|
83
|
+
onToggle={setShowFramework}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</figure>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Fragment, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LogoScaling — the responsive logo scale matrix.
|
|
5
|
+
*
|
|
6
|
+
* A specimen grid: columns = logo variants, rows = pixel steps (128 → 8px).
|
|
7
|
+
* Each cell renders the variant's mark sized to that step, so a brand manual
|
|
8
|
+
* can show at a glance where a lockup stops being legible. A fixed internal
|
|
9
|
+
* canvas is scaled to fit the container (ResizeObserver + transform), keeping
|
|
10
|
+
* every step at an exact px size regardless of viewport.
|
|
11
|
+
*
|
|
12
|
+
* ASSET-AGNOSTIC: variants (columns) are CONSUMER-INJECTED. Each variant supplies
|
|
13
|
+
* a rendered mark `node` that fills its cell (a width-constrained box), so the
|
|
14
|
+
* mark scales with the step. The monorepo original hardcoded four `KolLogo`
|
|
15
|
+
* variants; this DS version bakes in no mark — the consumer passes their own.
|
|
16
|
+
*
|
|
17
|
+
* import { LogoScaling } from '@kolkrabbi/kol-styleguide'
|
|
18
|
+
* import { Asset } from '@kolkrabbi/kol-brand/svg'
|
|
19
|
+
* <LogoScaling
|
|
20
|
+
* variants={[
|
|
21
|
+
* { label: 'Logomark', node: <Asset name="kol-logomark" />, widthMul: 1 },
|
|
22
|
+
* { label: 'Vertical lockup', node: <Asset name="kol-lockup-vert" />, widthMul: 1 },
|
|
23
|
+
* { label: 'Horizontal lockup', node: <Asset name="kol-lockup-hori" />, widthMul: 2.5 },
|
|
24
|
+
* { label: 'Wordmark', node: <Asset name="kol-wordmark" />, widthMul: 2.5 },
|
|
25
|
+
* ]}
|
|
26
|
+
* />
|
|
27
|
+
*
|
|
28
|
+
* Column labels are authored in the case they should render — no text-transform
|
|
29
|
+
* is applied (KOL no-auto-casing rule).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const DEFAULT_STEPS = [128, 96, 64, 48, 40, 32, 24, 16, 12, 8]
|
|
33
|
+
|
|
34
|
+
export default function LogoScaling({
|
|
35
|
+
variants = [],
|
|
36
|
+
steps = DEFAULT_STEPS,
|
|
37
|
+
canvasWidth = 1600,
|
|
38
|
+
canvasHeight = 1000,
|
|
39
|
+
className = '',
|
|
40
|
+
}) {
|
|
41
|
+
const outerRef = useRef(null)
|
|
42
|
+
const [scale, setScale] = useState(1)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const el = outerRef.current
|
|
46
|
+
if (!el) return undefined
|
|
47
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
48
|
+
setScale(entry.contentRect.width / canvasWidth)
|
|
49
|
+
})
|
|
50
|
+
ro.observe(el)
|
|
51
|
+
return () => ro.disconnect()
|
|
52
|
+
}, [canvasWidth])
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={`kol-logo-scaling ${className}`.trim()}>
|
|
56
|
+
<div
|
|
57
|
+
ref={outerRef}
|
|
58
|
+
className="relative w-full overflow-hidden"
|
|
59
|
+
style={{ aspectRatio: `${canvasWidth} / ${canvasHeight}` }}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
className="absolute top-0 left-0 inline-grid gap-x-12 gap-y-8 items-center text-emphasis origin-top-left"
|
|
63
|
+
style={{
|
|
64
|
+
gridTemplateColumns: `auto repeat(${variants.length}, auto)`,
|
|
65
|
+
width: canvasWidth,
|
|
66
|
+
height: canvasHeight,
|
|
67
|
+
transform: `scale(${scale})`,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<div />
|
|
71
|
+
{variants.map((v, i) => (
|
|
72
|
+
<div key={v.label ?? i} className="kol-helper-12 tracking-widest">
|
|
73
|
+
{v.label}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
|
|
77
|
+
{steps.map((px) => (
|
|
78
|
+
<Fragment key={px}>
|
|
79
|
+
<div className="kol-helper-12 text-right">{px}</div>
|
|
80
|
+
{variants.map((v, i) => (
|
|
81
|
+
<div
|
|
82
|
+
key={v.label ?? i}
|
|
83
|
+
className="[&_svg]:w-full [&_svg]:h-auto"
|
|
84
|
+
style={{ width: px * (v.widthMul ?? 1) }}
|
|
85
|
+
>
|
|
86
|
+
{v.node}
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</Fragment>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|