@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 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
+ }
@@ -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
+ }
@@ -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
+ }