@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-styleguide",
3
- "version": "0.18.2",
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-print": "^0.1.1",
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-component": "^0.317.2",
29
- "@kolkrabbi/kol-icons": "^0.53.2"
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-2">
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="kol-sans-body-02 text-emphasis">Or drag and drop here</span>
306
- <span className="whitespace-nowrap kol-sans-body-03 text-meta">File must be JPEG, JPG, PNG or WebP and up to 40MB</span>
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-sans-body-02 text-emphasis">Don’t have an image? Try one of ours:</span>
317
- {library && <a href={library.href} target="_blank" rel="noreferrer" className="group inline-flex shrink-0 items-center gap-1 kol-sans-body-02 text-meta hover:text-emphasis">{library.label}<Icon name="external-link" size={12} className="text-oq-48 group-hover:text-oq-96" /></a>}
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: 'clr-mono', content: (
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): the nav rung, like the header's controls; Copy CSS is in the header, where
342
- * every lab keeps it */}
343
- {view === 'palette' && <Button variant="nav" size="sm" iconLeft="shuffle" className="ml-auto" onClick={roll}>Randomise</Button>}
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
- /* the pair, Adobe's: Foreground and Background side by side, swap between them, each colour's lightness under it */
411
- const pairCol = (label, i, key) => (
412
- <div className="flex min-w-0 flex-col gap-2">
413
- <span className="kol-eyebrow text-fg-80">{label}</span>
414
- <div className="flex items-center gap-2">
415
- <span className="size-4 shrink-0 rounded-full" style={{ background: c.colors[i] }} />
416
- <Dropdown size="sm" value={String(i)} onChange={(v) => setPair({ fg: fgI, bg: bgI, [key]: Number(v) })} options={colourOptions} className="min-w-0 flex-1" ariaLabel={`${label} colour`} />
417
- </div>
418
- <Slider className="w-full" min={0} max={100} step={1} value={lightness(i)} onChange={(L) => setLightness(i, L)} readout="input" displayWidth={3} readoutPreset="rail" aria-label={`${label} lightness`} />
419
- </div>
420
- )
421
- const pairHead = (
422
- <div className="flex flex-col gap-4">
423
- <div className="flex items-center justify-between gap-3">
424
- <span className="kol-eyebrow text-fg-80">Contrast ratio</span>
425
- <span className={`inline-flex items-center gap-1 rounded px-2 py-0.5 kol-mono-12 ${ratio >= 4.5 ? 'bg-oq-08 text-emphasis' : 'bg-oq-04 text-meta'}`}><Icon name={ratio >= 4.5 ? 'check' : 'alert-triangle'} size={12} className={ratio >= 4.5 ? 'text-oq-96' : 'text-oq-48'} />{ratioText(ratio)} : 1</span>
426
- </div>
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: <>{pairHead}{summaryTable}{ladder}</> },
432
+ { id: 'summary', label: 'Summary', content: <SettingsSections divided sections={[pairSection, wcagSection, levelsSection]} /> },
462
433
  { id: 'suggest', label: 'Suggestions', content: (
463
- <>
464
- {pairHead}
465
- <div className="flex flex-col">
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
- {pairHead}
479
- <div className="flex flex-col gap-2">
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
- const FORMS = ['left-[8%] top-[10%] w-[52%] aspect-square rounded-full', 'right-[8%] bottom-[8%] w-[38%] aspect-square rounded-[12%]', 'left-[6%] bottom-[12%] h-[14%] w-[48%] rounded-full', 'right-[10%] top-[8%] h-[9%] w-[32%] rounded']
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={`absolute ${FORMS[i]}`} style={{ background: hex }} />)}
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: `${s.index + 1} ${s.roleLabel} ${s.hex}` }))
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={[{ label: 'Layout', rows: [
47
- { id: 'layout', label: 'Layout', type: 'choice', value: combo.layout, onChange: (v) => set({ layout: v }), options: COMBO_LAYOUTS.map((l) => ({ value: l.id, label: l.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-[720px]">
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
- <div className="flex h-full min-h-[200px] items-center justify-center overflow-hidden text-emphasis">
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
- </div>
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
+ }
@@ -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 size="md" tabs={tabs.map(({ id, label, icon }) => ({ id, label, icon }))} value={open?.id} onChange={setTab} ariaLabel={`${title} sections`} />}
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: 'Logo, company, product, sub-category', parts: ['mark', 'parent', 'product', 'sub', 'optional'] },
26
- { id: 'mark-product-sub', label: 'Logo, product, sub-category', parts: ['mark', 'product', 'sub', 'optional'] },
27
- { id: 'product-sub', label: 'Product, sub-category', parts: ['product', 'sub', 'optional'] },
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.subRatio) }
63
- const weight = { parent: v.parentWeight, product: v.productWeight, sub: v.subWeight, optional: v.subWeight }
64
- const kase = { parent: v.parentCase, product: v.productCase, sub: 'none', optional: 'none' }
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