@kolkrabbi/kol-styleguide 0.18.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/ColorLab.jsx +52 -87
- package/src/ComboLab.jsx +4 -4
- package/src/LockupLab.jsx +34 -2
- package/src/lab/LabWindow.jsx +1 -1
- package/src/lockupLab.js +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-styleguide",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Brand guide components and the brand tool",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@kolkrabbi/kol-
|
|
26
|
-
"@kolkrabbi/kol-theme": "^0.200.2",
|
|
25
|
+
"@kolkrabbi/kol-component": "^0.317.4",
|
|
27
26
|
"@kolkrabbi/kol-framework": "^0.65.0",
|
|
28
|
-
"@kolkrabbi/kol-
|
|
29
|
-
"@kolkrabbi/kol-icons": "^0.53.
|
|
27
|
+
"@kolkrabbi/kol-print": "^0.1.1",
|
|
28
|
+
"@kolkrabbi/kol-icons": "^0.53.5",
|
|
29
|
+
"@kolkrabbi/kol-theme": "^0.200.4"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"src",
|
package/src/ColorLab.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Button, ColorPickerPopover, Dropdown, Icon, IconFrame, PaletteHarmonyWheel, Slider, Tooltip, hexToHsl, hslToHex } from '@kolkrabbi/kol-component'
|
|
2
|
+
import { Button, ColorPickerPopover, Dropdown, Icon, IconFrame, PaletteHarmonyWheel, SettingsSections, Slider, Tooltip, hexToHsl, hslToHex } from '@kolkrabbi/kol-component'
|
|
3
3
|
import { COLOR_RELATIONS, CVD_TYPES, DEFAULT_COLOR_LAB, colorLab, colorLabCss, contrastGrade, contrastRatio, hexToOklch, inkOn, midColor, oklchToHex, paletteFromPixels, rollColorLab, simulateCvd, tintLadder } from './colorLab.js'
|
|
4
4
|
import LabWindow, { CopyAction, LabHistory, useLabValue } from './lab/LabWindow.jsx'
|
|
5
5
|
|
|
@@ -260,7 +260,7 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
260
260
|
const harmony = (
|
|
261
261
|
/* HARMONIES (review 3): the DS's glyphs, each named on hover, the mode named in the label — on the Wheel tab only
|
|
262
262
|
* (review 6 — *"you know there is a tab for wheel. why do you put it on every page?"*) */
|
|
263
|
-
|
|
263
|
+
<div className="flex flex-col gap-3">
|
|
264
264
|
{/* THE EYEBROW IS THE RAIL'S LABEL (review 8 — *"IT WAS PERFECT!!! I was saying why is it only used here"*): the
|
|
265
265
|
* settings rail's section label, now on every label in the rail */}
|
|
266
266
|
<p className="kol-eyebrow text-fg-80">Harmony: {harmonyLabel(c.relation)}</p>
|
|
@@ -276,7 +276,7 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
276
276
|
</Tooltip>
|
|
277
277
|
))}
|
|
278
278
|
</div>
|
|
279
|
-
|
|
279
|
+
</div>
|
|
280
280
|
)
|
|
281
281
|
const sources = [
|
|
282
282
|
{ id: 'base', label: 'Base', icon: 'eyedrop', content: (
|
|
@@ -300,10 +300,12 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
300
300
|
{image
|
|
301
301
|
? <img src={image} alt="" className="absolute inset-0 h-full w-full object-cover" />
|
|
302
302
|
: (
|
|
303
|
-
<span className="relative flex flex-col items-center gap-
|
|
303
|
+
<span className="relative flex flex-col items-center gap-6">
|
|
304
304
|
<Button size="md" tone="inverted" iconLeft="upload" onClick={(e) => { e.stopPropagation(); fileInput.current?.click() }}>Upload your image</Button>
|
|
305
|
-
<span className="
|
|
306
|
-
|
|
305
|
+
<span className="flex flex-col items-center gap-1">
|
|
306
|
+
<span className="kol-mono-12 text-emphasis">Or drag and drop here</span>
|
|
307
|
+
<span className="whitespace-nowrap uppercase kol-mono-10 text-subtle">Files: JPG, PNG or WebP | Size: 40MB</span>
|
|
308
|
+
</span>
|
|
307
309
|
</span>
|
|
308
310
|
)}
|
|
309
311
|
<input ref={fileInput} type="file" accept="image/jpeg,image/png,image/webp" className="hidden" onChange={(e) => { fromFile(e.target.files?.[0]); e.target.value = '' }} />
|
|
@@ -313,8 +315,8 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
313
315
|
* and linked beside them */
|
|
314
316
|
<div className="flex flex-col gap-3">
|
|
315
317
|
<div className="flex items-baseline justify-between gap-3">
|
|
316
|
-
<span className="kol-
|
|
317
|
-
{library && <a href={library.href} target="_blank" rel="noreferrer" className="group inline-flex shrink-0 items-center gap-1 kol-
|
|
318
|
+
<span className="min-w-0 truncate kol-mono-12 text-emphasis">Try one of ours:</span>
|
|
319
|
+
{library && <a href={library.href} target="_blank" rel="noreferrer" className="group inline-flex shrink-0 items-center gap-1 kol-mono-12 text-meta hover:text-emphasis">{library.label}<Icon name="external-link" size={12} className="text-oq-48 group-hover:text-oq-96" /></a>}
|
|
318
320
|
</div>
|
|
319
321
|
<div className="grid grid-cols-4 gap-2">
|
|
320
322
|
{samples.slice(0, 4).map((x) => <SampleTile key={x.src} src={x.src} label={x.label ?? x.src} onPick={() => takeImage(x.src)} />)}
|
|
@@ -323,12 +325,13 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
323
325
|
)}
|
|
324
326
|
</div>
|
|
325
327
|
) },
|
|
326
|
-
{ id: 'wheel', label: 'Wheel', icon: '
|
|
327
|
-
|
|
328
|
+
{ id: 'wheel', label: 'Wheel', icon: 'color-wheel', content: (
|
|
329
|
+
/* the Wheel tab's own air (the user, 2026-10-30: "MORE GAP between tabs, wheel and group"): 24 under the tabs on top of the rail's 16, 40 between the wheel and the harmony group */
|
|
330
|
+
<div className="flex flex-col gap-10 pt-6">
|
|
328
331
|
<WheelFill>{(size) => <PaletteHarmonyWheel size={size} hue={base.h} saturation={base.s} lightness={base.l} harmony={WHEEL_HARMONIES.includes(c.relation) ? c.relation : 'analogous'}
|
|
329
332
|
onHueChange={(hue) => regenerate({ base: hslToHex(hue, base.s, base.l).toUpperCase() })} />}</WheelFill>
|
|
330
333
|
{harmony}
|
|
331
|
-
|
|
334
|
+
</div>
|
|
332
335
|
) },
|
|
333
336
|
]
|
|
334
337
|
/* THE PALETTE SMALL (Adobe's) under every view's stage, Randomise at its right on the palette view (review 5 — *"under
|
|
@@ -338,9 +341,10 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
338
341
|
<div className="flex h-8 w-60 overflow-hidden rounded" aria-label="Palette">
|
|
339
342
|
{c.swatches.map((s) => <span key={s.index} className="flex-1" style={{ background: s.hex }} title={s.hex} />)}
|
|
340
343
|
</div>
|
|
341
|
-
{/* RANDOMISE (review 6, Adobe's shuffle
|
|
342
|
-
*
|
|
343
|
-
|
|
344
|
+
{/* RANDOMISE (review 6, Adobe's shuffle; review 8 — *"WRONG FUCKING BUTTON TONE"*): an explicit tone, so no wrapper's
|
|
345
|
+
* ground repaints it — the inverted button this window already wears on Upload your image. Copy CSS is in the header,
|
|
346
|
+
* where every lab keeps it. His review decides the rung. */}
|
|
347
|
+
{view === 'palette' && <Button tone="inverted" size="sm" iconLeft="shuffle" className="ml-auto" onClick={roll}>Randomise</Button>}
|
|
344
348
|
</div>
|
|
345
349
|
)
|
|
346
350
|
const paletteStage = (
|
|
@@ -407,92 +411,53 @@ export default function ColorLab({ value: valueProp, onChange, samples = [], lib
|
|
|
407
411
|
}
|
|
408
412
|
return best
|
|
409
413
|
}
|
|
410
|
-
/*
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-start gap-2">
|
|
428
|
-
{pairCol('Foreground', fgI, 'fg')}
|
|
429
|
-
<Button variant="nav" size="sm" iconOnly="swap" aria-label="Swap foreground and background" className="mt-6" onClick={() => setPair({ fg: bgI, bg: fgI })} />
|
|
430
|
-
{pairCol('Background', bgI, 'bg')}
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
)
|
|
434
|
-
const summaryTable = (
|
|
435
|
-
<div className="flex flex-col">
|
|
436
|
-
<div className="grid grid-cols-[minmax(0,1fr)_64px_64px] border-b border-oq-16 pb-2 kol-eyebrow text-fg-80"><span>Category</span><span>AA</span><span>AAA</span></div>
|
|
437
|
-
{SUMMARY.map((r) => (
|
|
438
|
-
<div key={r.id} className="grid grid-cols-[minmax(0,1fr)_64px_64px] items-center border-b border-oq-08 py-3 kol-sans-body-02 text-emphasis"><span>{r.label}</span><Verdict ok={r.aa} /><Verdict ok={r.aaa} na={r.aaa === null} /></div>
|
|
439
|
-
))}
|
|
440
|
-
</div>
|
|
441
|
-
)
|
|
442
|
-
/* KOL's text levels: the foreground at every ink stop over the background, each measured */
|
|
443
|
-
const ladder = (
|
|
444
|
-
<div className="flex flex-col gap-2">
|
|
445
|
-
<span className="kol-eyebrow text-fg-80">Text levels</span>
|
|
446
|
-
<div className="flex w-full flex-col rounded p-3" style={{ background: bg }}>
|
|
447
|
-
{c.text.map((x) => {
|
|
448
|
-
const r = contrastRatio(over(fg, bg, x.stop / 100), bg)
|
|
449
|
-
return (
|
|
450
|
-
<span key={x.stop} className="grid grid-cols-[32px_minmax(0,1fr)_auto] items-baseline gap-3 py-0.5" style={{ color: over(fg, bg, x.stop / 100) }}>
|
|
451
|
-
<span className="kol-mono-12">{x.stop}</span>
|
|
452
|
-
<span className="truncate kol-sans-body-03">{x.name ? x.name[0].toUpperCase() + x.name.slice(1) : 'Text'}</span>
|
|
453
|
-
<span className="kol-mono-12">{ratioText(r)} {contrastGrade(r)}</span>
|
|
454
|
-
</span>
|
|
455
|
-
)
|
|
456
|
-
})}
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
|
-
)
|
|
414
|
+
/* THE CONTRAST RAIL ON SETTINGSSECTIONS ROWS (2026-10-30, the user: *"if you need to scroll in this siderail then you
|
|
415
|
+
* are doing something wrong"*, *"we always have used mono sections and labels"*): the same rows the other labs use —
|
|
416
|
+
* label left, control right, the rail's rhythm and its mono — and nothing taller than the rail. The eleven-step ladder
|
|
417
|
+
* is three rows, each grade with the stops it holds. */
|
|
418
|
+
const pairSection = { label: 'Pair', rows: [
|
|
419
|
+
{ id: 'ratio', label: 'Contrast ratio', value: `${ratioText(ratio)} : 1 ${contrastGrade(ratio)}` },
|
|
420
|
+
{ id: 'fg', label: 'Foreground', type: 'choice', value: String(fgI), onChange: (v) => setPair({ fg: Number(v), bg: bgI }), options: colourOptions },
|
|
421
|
+
{ id: 'fgL', label: 'Lightness', type: 'slider', min: 0, max: 100, step: 1, value: lightness(fgI), onChange: (L) => setLightness(fgI, L) },
|
|
422
|
+
{ id: 'bg', label: 'Background', type: 'choice', value: String(bgI), onChange: (v) => setPair({ fg: fgI, bg: Number(v) }), options: colourOptions },
|
|
423
|
+
{ id: 'bgL', label: 'Lightness', type: 'slider', min: 0, max: 100, step: 1, value: lightness(bgI), onChange: (L) => setLightness(bgI, L) },
|
|
424
|
+
{ id: 'swap', label: 'Swap', render: () => <Button variant="nav" size="sm" iconOnly="swap" aria-label="Swap foreground and background" onClick={() => setPair({ fg: bgI, bg: fgI })} /> },
|
|
425
|
+
] }
|
|
426
|
+
const verdict = (ok) => (ok === null ? 'n/a' : ok ? 'pass' : 'fail')
|
|
427
|
+
const wcagSection = { label: 'WCAG', rows: SUMMARY.map((r) => ({ id: r.id, label: r.label, value: `AA ${verdict(r.aa)}, AAA ${verdict(r.aaa)}` })) }
|
|
428
|
+
/* KOL's text levels: the foreground at every ink stop over the background, grouped by the grade each stop earns */
|
|
429
|
+
const levels = c.text.reduce((acc, x) => { const g = contrastGrade(contrastRatio(over(fg, bg, x.stop / 100), bg)); (acc[g] ??= []).push(x.stop); return acc }, {})
|
|
430
|
+
const levelsSection = { label: 'Text levels', rows: ['AAA', 'AA', 'AA large', 'fail'].filter((g) => levels[g]).map((g) => ({ id: g, label: g === 'fail' ? 'Fail' : g, value: levels[g].length > 1 ? `${levels[g][0]} to ${levels[g][levels[g].length - 1]}` : String(levels[g][0]) })) }
|
|
460
431
|
const contrastRail = [
|
|
461
|
-
{ id: 'summary', label: 'Summary', content:
|
|
432
|
+
{ id: 'summary', label: 'Summary', content: <SettingsSections divided sections={[pairSection, wcagSection, levelsSection]} /> },
|
|
462
433
|
{ id: 'suggest', label: 'Suggestions', content: (
|
|
463
|
-
|
|
464
|
-
{
|
|
465
|
-
|
|
466
|
-
<span className="border-b border-oq-16 pb-2 kol-eyebrow text-fg-80">Foreground colors that pass AA</span>
|
|
467
|
-
{suggestions.length === 0 && <span className="py-3 kol-sans-body-02 text-meta">None at this hue. Move the background’s lightness.</span>}
|
|
468
|
-
{suggestions.map((x) => (
|
|
469
|
-
<button key={x.hex} type="button" onClick={() => editAt(fgI, x.hex)} className="grid cursor-pointer grid-cols-[16px_minmax(0,1fr)_auto] items-center gap-3 border-b border-oq-08 py-2 text-left kol-mono-12 text-emphasis hover:bg-oq-04">
|
|
470
|
-
<span className="size-4 rounded-full" style={{ background: x.hex }} /><span>{x.hex}</span><span className="text-meta">{ratioText(x.r)} : 1 {contrastGrade(x.r)}</span>
|
|
471
|
-
</button>
|
|
472
|
-
))}
|
|
473
|
-
</div>
|
|
474
|
-
</>
|
|
434
|
+
<SettingsSections divided sections={[pairSection, { label: 'Foreground colors that pass AA', note: suggestions.length === 0 ? 'None at this hue. Move the background’s lightness.' : undefined, rows: suggestions.length === 0 ? [] : [
|
|
435
|
+
{ id: 'pick', label: 'Use', type: 'choice', value: '', onChange: (hex) => editAt(fgI, hex), options: suggestions.map((x) => ({ value: x.hex, label: `${x.hex} ${ratioText(x.r)} : 1 ${contrastGrade(x.r)}` })) },
|
|
436
|
+
] }]} />
|
|
475
437
|
) },
|
|
476
438
|
{ id: 'ratio', label: 'Set a ratio', content: (
|
|
477
|
-
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
<span className="kol-eyebrow text-fg-80">Target ratio</span>
|
|
481
|
-
<Slider className="w-full" min={1} max={21} step={0.5} value={target ?? Math.round(ratio * 2) / 2} onChange={(t) => { setTarget(t); editAt(fgI, solveFor(t)) }} readout="input" displayWidth={4} readoutPreset="rail" aria-label="Target contrast ratio" />
|
|
482
|
-
<span className="kol-sans-body-02 text-meta">The foreground’s lightness moves until the pair reaches it.</span>
|
|
483
|
-
</div>
|
|
484
|
-
</>
|
|
439
|
+
<SettingsSections divided sections={[pairSection, { label: 'Target', note: 'The foreground’s lightness moves until the pair reaches it.', rows: [
|
|
440
|
+
{ id: 'target', label: 'Ratio', type: 'slider', min: 1, max: 21, step: 0.5, value: target ?? Math.round(ratio * 2) / 2, displayWidth: 4, onChange: (t) => { setTarget(t); editAt(fgI, solveFor(t)) } },
|
|
441
|
+
] }]} />
|
|
485
442
|
) },
|
|
486
443
|
]
|
|
487
444
|
/* THE PREVIEW IN THE PALETTE'S OWN FORMS (review 8 — *"how is this using ANYTHING but the colors?? … show some
|
|
488
445
|
* forms that use the pallette"*): the page in bg and fg, the other colours as forms beside the text; the DS Button */
|
|
489
|
-
|
|
446
|
+
/* KOL's own vector forms (public/kol-vector, the brand's art) in the palette's colours — each a single-fill SVG worn as a
|
|
447
|
+
* CSS mask, so any colour fills it (the user, 2026-10-30: *"find a better SVG image to use, something that actually makes sense"*) */
|
|
448
|
+
const FORMS = [
|
|
449
|
+
{ src: '/kol-vector/form-09.svg', box: { left: '6%', top: '8%', width: '50%', aspectRatio: '163 / 192' } },
|
|
450
|
+
{ src: '/kol-vector/form-05.svg', box: { right: '8%', top: '10%', width: '30%', aspectRatio: '187 / 233' } },
|
|
451
|
+
{ src: '/kol-vector/form-02.svg', box: { right: '12%', bottom: '8%', width: '34%', aspectRatio: '1 / 1' } },
|
|
452
|
+
{ src: '/kol-vector/form-13.svg', box: { left: '8%', bottom: '6%', width: '36%', aspectRatio: '267 / 107' } },
|
|
453
|
+
]
|
|
454
|
+
const masked = (src) => ({ WebkitMaskImage: `url(${src})`, maskImage: `url(${src})`, WebkitMaskRepeat: 'no-repeat', maskRepeat: 'no-repeat', WebkitMaskSize: 'contain', maskSize: 'contain', WebkitMaskPosition: 'center', maskPosition: 'center' })
|
|
490
455
|
const forms = c.colors.filter((_, i) => i !== fgI && i !== bgI)
|
|
491
456
|
const contrastStage = (
|
|
492
457
|
<div className="flex h-full min-h-[240px] flex-col gap-3">
|
|
493
458
|
<div className="grid min-h-0 flex-1 grid-cols-2 grid-rows-[minmax(0,1fr)] overflow-hidden rounded" style={{ background: bg, color: fg }}>
|
|
494
459
|
<div className="relative min-h-0 overflow-hidden">
|
|
495
|
-
{(forms.length ? forms : [fg]).slice(0, 4).map((hex, i) => <span key={`${hex}${i}`} className=
|
|
460
|
+
{(forms.length ? forms : [fg]).slice(0, 4).map((hex, i) => <span key={`${hex}${i}`} className="absolute" style={{ ...FORMS[i].box, background: hex, ...masked(FORMS[i].src) }} />)}
|
|
496
461
|
</div>
|
|
497
462
|
<div className="flex min-h-0 flex-col justify-center gap-6 p-10">
|
|
498
463
|
<span className="kol-sans-heading-01 leading-none">The quick brown fox</span>
|
package/src/ComboLab.jsx
CHANGED
|
@@ -38,13 +38,13 @@ export default function ComboLab({ color, value: valueProp, onChange, logo, clas
|
|
|
38
38
|
const [tab, setTab] = useState('layout')
|
|
39
39
|
const Layout = LAYOUT_COMPONENTS[combo.layout] ?? LAYOUT_COMPONENTS[COMBO_LAYOUTS[0].id]
|
|
40
40
|
const css = comboLabCss(value, c)
|
|
41
|
-
const colourOptions = c.swatches.map((s) => ({ value: String(s.index), label:
|
|
41
|
+
const colourOptions = c.swatches.map((s) => ({ value: String(s.index), label: s.roleLabel }))
|
|
42
42
|
|
|
43
43
|
/* THE SETTINGS RAIL'S ROWS (review 4) */
|
|
44
44
|
const tabs = [
|
|
45
45
|
{ id: 'layout', label: 'Layout', content: (
|
|
46
|
-
<SettingsSections divided sections={[{
|
|
47
|
-
{ id: 'layout', label: '
|
|
46
|
+
<SettingsSections divided sections={[{ rows: [
|
|
47
|
+
{ id: 'layout', label: 'Preset', type: 'choice', value: combo.layout, onChange: (v) => set({ layout: v }), options: COMBO_LAYOUTS.map((l) => ({ value: l.id, label: l.label })) },
|
|
48
48
|
{ id: 'mark', label: logo ? 'Brand mark' : 'Mark slot', type: 'switch', on: !!combo.logo, onChange: (on) => set({ logo: on }) },
|
|
49
49
|
] }]} />
|
|
50
50
|
) },
|
|
@@ -77,7 +77,7 @@ export default function ComboLab({ color, value: valueProp, onChange, logo, clas
|
|
|
77
77
|
}}
|
|
78
78
|
>
|
|
79
79
|
<div className="flex h-full items-center justify-center">
|
|
80
|
-
<div className="w-full max-w-[
|
|
80
|
+
<div className="w-full max-w-[1200px]">
|
|
81
81
|
<Layout palette={combo.palette} logo={combo.logo ? (logo ?? PLAIN_MARK) : null} />
|
|
82
82
|
</div>
|
|
83
83
|
</div>
|
package/src/LockupLab.jsx
CHANGED
|
@@ -120,18 +120,24 @@ export default function LockupLab({ value: valueProp, onChange, mark, font, pres
|
|
|
120
120
|
) },
|
|
121
121
|
{ id: 'ratio', label: 'Ratio', content: (
|
|
122
122
|
<SettingsSections divided sections={[
|
|
123
|
+
/* EVERY TEXT PART GETS THE SAME THREE (the user, 2026-10-30: *"why do you get UPPERCASE toggle for one thing and not another"*):
|
|
124
|
+
* size to the product (the product is the base), weight, case — company, product, sub-category, optional alike */
|
|
123
125
|
{ label: 'Size to product', rows: [
|
|
124
126
|
{ id: 'pr', label: 'Company %', type: 'slider', labelWidth: 112, min: 10, max: 100, step: 2, value: Math.round(l.parentRatio * 100), onChange: (v) => set({ parentRatio: v / 100 }) },
|
|
125
127
|
{ id: 'sr', label: 'Sub-category %', type: 'slider', labelWidth: 112, min: 10, max: 100, step: 2, value: Math.round(l.subRatio * 100), onChange: (v) => set({ subRatio: v / 100 }) },
|
|
128
|
+
{ id: 'or', label: 'Optional %', type: 'slider', labelWidth: 112, min: 10, max: 100, step: 2, value: Math.round(l.optionalRatio * 100), onChange: (v) => set({ optionalRatio: v / 100 }) },
|
|
126
129
|
] },
|
|
127
130
|
{ label: 'Weight', rows: [
|
|
128
131
|
{ id: 'pw', label: 'Company', type: 'slider', labelWidth: 112, min: 100, max: 900, step: 100, value: l.parentWeight, onChange: (v) => set({ parentWeight: v }) },
|
|
129
132
|
{ id: 'w', label: 'Product', type: 'slider', labelWidth: 112, min: 100, max: 900, step: 100, value: l.productWeight, onChange: (v) => set({ productWeight: v }) },
|
|
130
133
|
{ id: 'sw', label: 'Sub-category', type: 'slider', labelWidth: 112, min: 100, max: 900, step: 100, value: l.subWeight, onChange: (v) => set({ subWeight: v }) },
|
|
134
|
+
{ id: 'ow', label: 'Optional', type: 'slider', labelWidth: 112, min: 100, max: 900, step: 100, value: l.optionalWeight, onChange: (v) => set({ optionalWeight: v }) },
|
|
131
135
|
] },
|
|
132
136
|
{ label: 'Case', rows: [
|
|
133
137
|
{ id: 'pc', label: 'Company', type: 'choice', value: l.parentCase, onChange: (v) => set({ parentCase: v }), options: dd(LOCKUP_CASES) },
|
|
134
138
|
{ id: 'c', label: 'Product', type: 'choice', value: l.productCase, onChange: (v) => set({ productCase: v }), options: dd(LOCKUP_CASES) },
|
|
139
|
+
{ id: 'sc', label: 'Sub-category', type: 'choice', value: l.subCase, onChange: (v) => set({ subCase: v }), options: dd(LOCKUP_CASES) },
|
|
140
|
+
{ id: 'oc', label: 'Optional', type: 'choice', value: l.optionalCase, onChange: (v) => set({ optionalCase: v }), options: dd(LOCKUP_CASES) },
|
|
135
141
|
] },
|
|
136
142
|
]} />
|
|
137
143
|
) },
|
|
@@ -168,9 +174,35 @@ export default function LockupLab({ value: valueProp, onChange, mark, font, pres
|
|
|
168
174
|
),
|
|
169
175
|
}}
|
|
170
176
|
>
|
|
171
|
-
|
|
177
|
+
{/* THE LOCKUP FILLS THE STAGE (the user, 2026-10-30: *"WHY IS THE LOGO TINY"*): drawn at 64 and scaled to 80 % of the stage's width or 60 % of its height, whichever binds */}
|
|
178
|
+
<FitStage>
|
|
172
179
|
<BrandLockup value={value} size={64} mark={mark} font={font} />
|
|
173
|
-
</
|
|
180
|
+
</FitStage>
|
|
174
181
|
</LabWindow>
|
|
175
182
|
)
|
|
176
183
|
}
|
|
184
|
+
|
|
185
|
+
/* FitStage — scales its one child to the stage: 80 % of the width, 60 % of the height, whichever binds; remeasured on resize */
|
|
186
|
+
function FitStage({ children }) {
|
|
187
|
+
const outer = useRef(null), inner = useRef(null)
|
|
188
|
+
const [k, setK] = useState(1)
|
|
189
|
+
useLayoutEffect(() => {
|
|
190
|
+
const fit = () => {
|
|
191
|
+
const o = outer.current, i = inner.current
|
|
192
|
+
if (!o || !i) return
|
|
193
|
+
const w = i.offsetWidth, h = i.offsetHeight
|
|
194
|
+
if (!w || !h) return
|
|
195
|
+
setK(Math.max(0.25, Math.min((o.clientWidth * 0.8) / w, (o.clientHeight * 0.6) / h)))
|
|
196
|
+
}
|
|
197
|
+
fit()
|
|
198
|
+
const ro = new ResizeObserver(fit)
|
|
199
|
+
ro.observe(outer.current)
|
|
200
|
+
ro.observe(inner.current)
|
|
201
|
+
return () => ro.disconnect()
|
|
202
|
+
}, [children])
|
|
203
|
+
return (
|
|
204
|
+
<div ref={outer} className="flex h-full min-h-[200px] items-center justify-center overflow-hidden text-emphasis">
|
|
205
|
+
<div ref={inner} className="inline-flex" style={{ transform: `scale(${k})`, transformOrigin: 'center' }}>{children}</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
package/src/lab/LabWindow.jsx
CHANGED
|
@@ -65,7 +65,7 @@ export default function LabWindow({ title, actions, tabs = [], tab: tabProp, onT
|
|
|
65
65
|
<div {...grabProps} className="kol-rail-grab kol-rail-grab--left hidden md:block" />
|
|
66
66
|
{/* THE TABSROW AT THE TOP OF EVERY WINDOW (review 6 — *"why dont we make it a rule at top of each window?"*):
|
|
67
67
|
* one section or many, the rail opens on its tabs */}
|
|
68
|
-
{tabs.length > 0 && <TabsRow
|
|
68
|
+
{tabs.length > 0 && <TabsRow type="mono-light" tabs={tabs.map(({ id, label, icon }) => ({ id, label, icon }))} value={open?.id} onChange={setTab} ariaLabel={`${title} sections`} />}
|
|
69
69
|
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">{open?.content}</div>
|
|
70
70
|
{foot && <div className="flex flex-wrap items-center gap-1">{foot}</div>}
|
|
71
71
|
</aside>
|
package/src/lockupLab.js
CHANGED
|
@@ -22,9 +22,9 @@ export const LOCKUP_PARTS = [
|
|
|
22
22
|
|
|
23
23
|
/* the five lockup types on bh-logo-system-0.svg, in the sheet's order */
|
|
24
24
|
export const LOCKUP_TYPES = [
|
|
25
|
-
{ id: 'full', label: '
|
|
26
|
-
{ id: 'mark-product-sub', label: 'Logo, product, sub
|
|
27
|
-
{ id: 'product-sub', label: 'Product, sub
|
|
25
|
+
{ id: 'full', label: 'Full', parts: ['mark', 'parent', 'product', 'sub', 'optional'] },
|
|
26
|
+
{ id: 'mark-product-sub', label: 'Logo, product, sub', parts: ['mark', 'product', 'sub', 'optional'] },
|
|
27
|
+
{ id: 'product-sub', label: 'Product, sub', parts: ['product', 'sub', 'optional'] },
|
|
28
28
|
{ id: 'mark-product', label: 'Logo, product', parts: ['mark', 'product'] },
|
|
29
29
|
{ id: 'product', label: 'Product', parts: ['product'] },
|
|
30
30
|
]
|
|
@@ -48,9 +48,9 @@ export const LOCKUP_SUB_PRESETS = ['Solutions', 'Services', 'Courts', 'Archives'
|
|
|
48
48
|
export const DEFAULT_LOCKUP_LAB = {
|
|
49
49
|
type: 'full', shape: 'rounded',
|
|
50
50
|
parent: 'Brand', product: 'Product', sub: 'Solutions', optional: '',
|
|
51
|
-
parentRatio: 0.36, subRatio: 0.62,
|
|
52
|
-
parentWeight: 700, productWeight: 700, subWeight: 400,
|
|
53
|
-
parentCase: 'upper', productCase: 'none',
|
|
51
|
+
parentRatio: 0.36, subRatio: 0.62, optionalRatio: 0.62,
|
|
52
|
+
parentWeight: 700, productWeight: 700, subWeight: 400, optionalWeight: 400,
|
|
53
|
+
parentCase: 'upper', productCase: 'none', subCase: 'none', optionalCase: 'none',
|
|
54
54
|
clip: true, grid: false,
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -59,9 +59,9 @@ export function lockupLab(value, size = 64) {
|
|
|
59
59
|
const v = { ...DEFAULT_LOCKUP_LAB, ...value }
|
|
60
60
|
const type = LOCKUP_TYPES.find((t) => t.id === v.type) ?? LOCKUP_TYPES[0]
|
|
61
61
|
const text = { parent: v.parent, product: v.product, sub: v.sub, optional: v.optional }
|
|
62
|
-
const px = { parent: Math.round(size * v.parentRatio), product: size, sub: Math.round(size * v.subRatio), optional: Math.round(size * v.
|
|
63
|
-
const weight = { parent: v.parentWeight, product: v.productWeight, sub: v.subWeight, optional: v.
|
|
64
|
-
const kase = { parent: v.parentCase, product: v.productCase, sub:
|
|
62
|
+
const px = { parent: Math.round(size * v.parentRatio), product: size, sub: Math.round(size * v.subRatio), optional: Math.round(size * v.optionalRatio) }
|
|
63
|
+
const weight = { parent: v.parentWeight, product: v.productWeight, sub: v.subWeight, optional: v.optionalWeight }
|
|
64
|
+
const kase = { parent: v.parentCase, product: v.productCase, sub: v.subCase, optional: v.optionalCase }
|
|
65
65
|
/* the mark spans the text it stands beside: the company and product lines when both show, else the product's cap */
|
|
66
66
|
const markSize = Math.round(type.parts.includes('parent') ? size * (0.78 + v.parentRatio) : size * 0.82)
|
|
67
67
|
const parts = type.parts
|