@kolkrabbi/kol-component 0.317.0 → 0.317.2

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-component",
3
- "version": "0.317.0",
3
+ "version": "0.317.2",
4
4
  "description": "The core component library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "react-dom": "^18.3.0 || ^19.0.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@kolkrabbi/kol-icons": "^0.53.0"
41
+ "@kolkrabbi/kol-icons": "^0.53.2"
42
42
  },
43
43
  "files": [
44
44
  "src",
@@ -32,7 +32,8 @@ import { RAIL_ROW } from '../hooks/railRow.js'
32
32
  * Every look but `stacked`, which has its phone bar
33
33
  * @param {'underline'|'chip'|'text'|'stacked'} [look='underline']
34
34
  * @param {boolean} [filled=false] - `chip` only: every chip shows at rest (Button `tab-filled`), so the row reads as choices
35
- * underline — this row, as it always was; a tab's `icon` sits before its label
35
+ * underline — this row, as it always was; a tab's `icon` sits before its label. `size="md"` is the sans body
36
+ * face with a 20px glyph in a 48px row (the user's ref, Adobe's tab strip, 2026-10-29)
36
37
  * chip — the active tab a filled chip, the rest quiet (was `TabChips`); `size` is the control rung
37
38
  * text — flat helper-type tabs, the active one bright (was kol-shell's `TabStrip`); `size` is the
38
39
  * helper size (default 14), `tracked` adds 1px tracking
@@ -76,7 +77,7 @@ export default function TabsRow({ tabs: raw = [], value, onChange, onClose, onMi
76
77
  if (look === 'chip') return <Overflow mode={overflow} value={value}><ChipTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 'sm'} filled={filled} ariaLabel={ariaLabel} className={`${wrap ? 'flex-wrap ' : ''}${className ?? ''}`} /></Overflow>
77
78
  if (look === 'text') return <Overflow mode={overflow} value={value}><TextTabs tabs={tabs} value={value} onChange={onChange} size={size ?? 14} tracked={tracked} className={`${wrap ? 'flex-wrap gap-y-2 ' : ''}${className ?? 'gap-6'}`} style={style} /></Overflow>
78
79
  if (look === 'stacked') return <StackedTabs tabs={tabs} value={value} onChange={onChange} placement={placement} shape={shape} collapsed={collapsed} onExpand={onExpand} edge={edge} border={border} shadow={shadow} dock={dock} active={active} foldable={foldable} folded={folded} onFoldChange={onFoldChange} foldSide={foldSide} ariaLabel={ariaLabel} className={className ?? ''} />
79
- return <Overflow mode={overflow} value={value}><UnderlineTabs tabs={tabs} value={value} onChange={onChange} onClose={onClose} onMinimise={onMinimise} wrap={wrap} /></Overflow>
80
+ return <Overflow mode={overflow} value={value}><UnderlineTabs tabs={tabs} value={value} onChange={onChange} onClose={onClose} onMinimise={onMinimise} wrap={wrap} size={size} /></Overflow>
80
81
  }
81
82
 
82
83
  /* `overflow="scroll"`: the row in a sideways scroller that fades its clipped edges and keeps the active tab (`data-active`)
@@ -264,7 +265,10 @@ function StackedTabs({ tabs, value, onChange, placement, shape = 'bar', collapse
264
265
  )
265
266
  }
266
267
 
267
- function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
268
+ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap, size }) {
269
+ /* `size="md"` (2026-10-29, the user's ref — Adobe's tab strip): the sans body face at medium weight, a 20px glyph at a
270
+ * 2 stroke (`.kol-tab-md`, the theme), a 48px row, 24 between tabs — review 8: *"the problem … was thickness and stroke"* */
271
+ const md = size === 'md'
268
272
  const listRef = useRef(null)
269
273
  const activeIdx = tabs.findIndex((t) => t.id === value)
270
274
 
@@ -278,14 +282,14 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
278
282
  }
279
283
 
280
284
  return (
281
- <div className={`flex items-stretch gap-4 ${wrap ? 'min-h-10' : 'h-10'}`}>
285
+ <div className={`flex items-stretch gap-4 ${wrap ? (md ? 'min-h-12' : 'min-h-10') : md ? 'h-12' : 'h-10'}`}>
282
286
  {onClose && (
283
287
  /* the one close, at the rung whose glyph is the 12 this hand-rolled
284
288
  * (2026-09-03) — xs SOLO is 12, so the drawing does not move */
285
289
  <CloseButton onClick={onClose} size="xs" className="self-center" />
286
290
  )}
287
291
 
288
- <div ref={listRef} role="tablist" onKeyDown={onKeyDown} className={`flex items-stretch gap-4${wrap ? ' flex-wrap gap-y-1' : ''}`}>
292
+ <div ref={listRef} role="tablist" onKeyDown={onKeyDown} className={`flex items-stretch ${md ? 'gap-6' : 'gap-4'}${wrap ? ' flex-wrap gap-y-1' : ''}`}>
289
293
  {tabs.map((t, i) => {
290
294
  const isActive = t.id === value
291
295
  return (
@@ -298,7 +302,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
298
302
  tabIndex={isActive || (activeIdx === -1 && i === 0) ? 0 : -1}
299
303
  onClick={() => onChange?.(t.id)}
300
304
  className={[
301
- 'group kol-mono-12 flex items-center gap-2 cursor-pointer border-b-2',
305
+ `group ${md ? 'kol-tab-md kol-sans-body-02 font-medium gap-2.5' : 'kol-mono-12 gap-2'} flex items-center cursor-pointer border-b-2`,
302
306
  isActive
303
307
  ? 'text-emphasis border-fg'
304
308
  : 'text-meta hover:text-emphasis border-transparent',
@@ -306,7 +310,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
306
310
  >
307
311
  {/* a tab may carry its glyph before the label (2026-10-29, Adobe's Base color · Image · Color wheel) */}
308
312
  {/* the glyph in OPAQUE ink (icons take oq, never fg) — full when active, meta otherwise, full under the pointer */}
309
- {t.icon && <Icon name={t.icon} size={16} className={isActive ? 'text-oq-96' : 'text-oq-48 group-hover:text-oq-96'} />}
313
+ {t.icon && <Icon name={t.icon} size={md ? 20 : 16} className={isActive ? 'text-oq-96' : 'text-oq-48 group-hover:text-oq-96'} />}
310
314
  {t.label}
311
315
  </button>
312
316
  )
@@ -4,7 +4,7 @@ import Input from '../atoms/Input.jsx'
4
4
  import Slider from '../molecules/Slider.jsx'
5
5
  import Dropdown from '../molecules/Dropdown.jsx'
6
6
  import { Icon } from '@kolkrabbi/kol-icons'
7
- import { usePopover, PopoverPanel, Tooltip } from '../utilities/Popover.jsx'
7
+ import { usePopover, PopoverPanel } from '../utilities/Popover.jsx'
8
8
  import { HueStrip, SBSquare, AlphaStrip } from './SpectrumControls.jsx'
9
9
  import { hexToHsva, hsvaToHex } from '../hooks/colorMath.js'
10
10
  import { resolveCssColor } from '../hooks/cssVar.js'
@@ -50,6 +50,9 @@ const CHANNELS = {
50
50
  }
51
51
 
52
52
  /* one channel: its letter, a strip painted with the channel's own range at the current colour, a thumb, its number */
53
+ /* brightness — Adobe's ☀ ramp under the hue strip in HEX and RGB: HSV value, black to the colour at full brightness */
54
+ const BRIGHTNESS = { key: 'B', label: 'Brightness', icon: 'brightness', min: 0, max: 100, read: (_, h) => h.val, write: null, hsva: (h, v) => ({ ...h, val: v }) }
55
+
53
56
  function ChannelStrip({ ch, hex, hsva, onHsva }) {
54
57
  const ref = useRef(null)
55
58
  const value = ch.read(hex, hsva)
@@ -63,17 +66,16 @@ function ChannelStrip({ ch, hex, hsva, onHsva }) {
63
66
  const pct = ((value - ch.min) / (ch.max - ch.min)) * 100
64
67
  return (
65
68
  <div className="flex items-center gap-2">
66
- <span className="w-3 kol-mono-12 text-meta">{ch.key}</span>
67
- <div ref={ref} role="slider" aria-label={ch.key} aria-valuemin={ch.min} aria-valuemax={ch.max} aria-valuenow={Math.round(value)} tabIndex={0}
69
+ <span className="flex w-4 shrink-0 items-center kol-mono-12 text-meta">{ch.icon ? <Icon name={ch.icon} size={16} className="text-oq-64" /> : ch.key}</span>
70
+ <div ref={ref} role="slider" aria-label={ch.label ?? ch.key} aria-valuemin={ch.min} aria-valuemax={ch.max} aria-valuenow={Math.round(value)} tabIndex={0}
68
71
  className="relative h-4 flex-1 cursor-pointer rounded-full touch-none" style={{ background: `linear-gradient(to right, ${stops.join(', ')})` }}
69
72
  onPointerDown={(e) => { e.currentTarget.setPointerCapture(e.pointerId); fromPointer(e) }}
70
73
  onPointerMove={(e) => { if (e.currentTarget.hasPointerCapture(e.pointerId)) fromPointer(e) }}
71
74
  onKeyDown={(e) => { const d = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }[e.key]; if (d) { e.preventDefault(); set(value + d * (e.shiftKey ? 10 : 1)) } }}>
72
75
  <span className="absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full" style={{ left: `${pct}%`, boxShadow: '0 0 0 2px #fff, 0 0 0 3px rgba(0,0,0,0.35)', background: hex.slice(0, 7) }} />
73
76
  </div>
74
- <div className="w-14 shrink-0">
75
- <Input size="sm" type="number" value={Math.round(value)} aria-label={`${ch.key} value`} onChange={(e) => { const v = Number(e.target.value); if (!Number.isNaN(v)) set(v) }} />
76
- </div>
77
+ {/* the box hugs three characters by the Input's own `chars` — a utility width is not guaranteed in every consumer (review 8, ref 81) */}
78
+ <Input size="sm" type="number" chars={3} value={Math.round(value)} aria-label={`${ch.label ?? ch.key} value`} onChange={(e) => { const v = Number(e.target.value); if (!Number.isNaN(v)) set(v) }} />
77
79
  </div>
78
80
  )
79
81
  }
@@ -198,36 +200,27 @@ export default function ColorPickerPopover({ color, onChange, defaultValue, size
198
200
  )
199
201
 
200
202
  if (inline) {
201
- /* THE EDITOR COLOUR PANEL'S SHAPE (review 5 — *"did you check the editor color panel"*, design-editor's ColourBody):
202
- * the top row is the swatch, the eyedropper (where the browser has one) and, on the right, the `#` hex at six
203
- * characters and the end control; the mode a full-width Dropdown under it; then the hue strip over the square */
204
- const eyedrop = typeof window !== 'undefined' && 'EyeDropper' in window
205
- const pick = async () => {
206
- try {
207
- const { sRGBHex } = await new window.EyeDropper().open()
208
- const next = hexToHsva(sRGBHex)
209
- if (next) commit({ ...next, alpha: hsva.alpha })
210
- } catch { /* cancelled */ }
211
- }
203
+ /* ADOBE'S BASE COLOR PANEL (review 7, refs 68–72): the label row — the name, the mode as plain text at the right;
204
+ * in HEX, ONE field: the swatch, the hex, the lock (Input's slotLeft · slotRight); the square, THEN the hue strip;
205
+ * the mode's channels under them (RGB · HSB · Lab), and brightness — the ☀ ramp — in HEX and RGB */
206
+ const hexField = (!modes || mode === 'hex') && (
207
+ <Input size="md" prefix="#" maxLength={8} value={draft} onChange={onHex} aria-label={`${label} hex`} width="100%"
208
+ slotLeft={<ColorSwatch hex={isNone ? null : color} size={SWATCH.xs} radius="full" hoverable={false} />}
209
+ slotRight={hexEnd} />
210
+ )
212
211
  return hsva && (
213
212
  <div className={`flex w-full flex-col gap-3 ${className}`.trim()}>
214
- <div className="flex items-center gap-3">
215
- <ColorSwatch hex={isNone ? null : color} size={SWATCH.sm} hoverable={false} className="shrink-0 border border-[var(--kol-border-default)]" />
216
- {eyedrop && (
217
- <Tooltip label="Pick a colour from the screen">
218
- <button type="button" onClick={pick} aria-label="Eyedropper" className="inline-flex cursor-pointer text-oq-64 hover:text-oq-96"><Icon name="eyedrop" size={20} /></button>
219
- </Tooltip>
220
- )}
221
- <span className="ml-auto" />
222
- <Input size="sm" prefix="#" chars={6} maxLength={8} value={draft} onChange={onHex} aria-label={`${label} hex`} />
223
- {hexEnd}
213
+ <div className="flex items-center justify-between gap-3">
214
+ <span className="kol-eyebrow text-fg-80">{title ?? label}</span>
215
+ {modes && <Dropdown variant="nav" size="sm" value={mode} onChange={setMode} options={PICKER_MODES} ariaLabel={`${label} mode`} />}
224
216
  </div>
225
- {modes && <Dropdown size="sm" value={mode} onChange={setMode} options={PICKER_MODES} className="w-full" ariaLabel={`${label} mode`} />}
226
- <HueStrip hue={hsva.hue} onChange={(hue) => commit({ ...hsva, hue })} />
217
+ {hexField}
227
218
  <div className="w-full overflow-hidden rounded-[var(--kol-radius-xs)]" style={{ height: 200 }}>
228
219
  <SBSquare hue={hsva.hue} sat={hsva.sat} val={hsva.val} onChange={(sat, val) => commit({ ...hsva, sat, val })} />
229
220
  </div>
221
+ <HueStrip hue={hsva.hue} onChange={(hue) => commit({ ...hsva, hue })} />
230
222
  {modes && mode !== 'hex' && CHANNELS[mode].map((ch) => <ChannelStrip key={ch.key} ch={ch} hex={hsvaToHex({ ...hsva, alpha: 1 })} hsva={hsva} onHsva={commit} />)}
223
+ {(!modes || mode === 'hex' || mode === 'rgb') && <ChannelStrip ch={BRIGHTNESS} hex={hsvaToHex({ ...hsva, alpha: 1 })} hsva={hsva} onHsva={commit} />}
231
224
  {alpha && <AlphaStrip alpha={hsva.alpha} color={opaque} onChange={(a) => commit({ ...hsva, alpha: a })} />}
232
225
  </div>
233
226
  )