@kolkrabbi/kol-component 0.317.0 → 0.317.1

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.1",
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.1"
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,9 @@ 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, a 20px glyph, a 48px row, 24 between tabs */
270
+ const md = size === 'md'
268
271
  const listRef = useRef(null)
269
272
  const activeIdx = tabs.findIndex((t) => t.id === value)
270
273
 
@@ -278,14 +281,14 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
278
281
  }
279
282
 
280
283
  return (
281
- <div className={`flex items-stretch gap-4 ${wrap ? 'min-h-10' : 'h-10'}`}>
284
+ <div className={`flex items-stretch gap-4 ${wrap ? (md ? 'min-h-12' : 'min-h-10') : md ? 'h-12' : 'h-10'}`}>
282
285
  {onClose && (
283
286
  /* the one close, at the rung whose glyph is the 12 this hand-rolled
284
287
  * (2026-09-03) — xs SOLO is 12, so the drawing does not move */
285
288
  <CloseButton onClick={onClose} size="xs" className="self-center" />
286
289
  )}
287
290
 
288
- <div ref={listRef} role="tablist" onKeyDown={onKeyDown} className={`flex items-stretch gap-4${wrap ? ' flex-wrap gap-y-1' : ''}`}>
291
+ <div ref={listRef} role="tablist" onKeyDown={onKeyDown} className={`flex items-stretch ${md ? 'gap-6' : 'gap-4'}${wrap ? ' flex-wrap gap-y-1' : ''}`}>
289
292
  {tabs.map((t, i) => {
290
293
  const isActive = t.id === value
291
294
  return (
@@ -298,7 +301,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
298
301
  tabIndex={isActive || (activeIdx === -1 && i === 0) ? 0 : -1}
299
302
  onClick={() => onChange?.(t.id)}
300
303
  className={[
301
- 'group kol-mono-12 flex items-center gap-2 cursor-pointer border-b-2',
304
+ `group ${md ? 'kol-sans-body-02 gap-2.5' : 'kol-mono-12 gap-2'} flex items-center cursor-pointer border-b-2`,
302
305
  isActive
303
306
  ? 'text-emphasis border-fg'
304
307
  : 'text-meta hover:text-emphasis border-transparent',
@@ -306,7 +309,7 @@ function UnderlineTabs({ tabs, value, onChange, onClose, onMinimise, wrap }) {
306
309
  >
307
310
  {/* a tab may carry its glyph before the label (2026-10-29, Adobe's Base color · Image · Color wheel) */}
308
311
  {/* 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'} />}
312
+ {t.icon && <Icon name={t.icon} size={md ? 20 : 16} className={isActive ? 'text-oq-96' : 'text-oq-48 group-hover:text-oq-96'} />}
310
313
  {t.label}
311
314
  </button>
312
315
  )
@@ -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,8 +66,8 @@ 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) }}
@@ -72,7 +75,7 @@ function ChannelStrip({ ch, hex, hsva, onHsva }) {
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
77
  <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) }} />
78
+ <Input size="sm" type="number" 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) }} />
76
79
  </div>
77
80
  </div>
78
81
  )
@@ -198,36 +201,27 @@ export default function ColorPickerPopover({ color, onChange, defaultValue, size
198
201
  )
199
202
 
200
203
  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
- }
204
+ /* ADOBE'S BASE COLOR PANEL (review 7, refs 68–72): the label row — the name, the mode as plain text at the right;
205
+ * in HEX, ONE field: the swatch, the hex, the lock (Input's slotLeft · slotRight); the square, THEN the hue strip;
206
+ * the mode's channels under them (RGB · HSB · Lab), and brightness — the ☀ ramp — in HEX and RGB */
207
+ const hexField = (!modes || mode === 'hex') && (
208
+ <Input size="md" prefix="#" maxLength={8} value={draft} onChange={onHex} aria-label={`${label} hex`} width="100%"
209
+ slotLeft={<ColorSwatch hex={isNone ? null : color} size={SWATCH.xs} radius="full" hoverable={false} />}
210
+ slotRight={hexEnd} />
211
+ )
212
212
  return hsva && (
213
213
  <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}
214
+ <div className="flex items-center justify-between gap-3">
215
+ <span className="kol-sans-body-02 text-emphasis">{title ?? label}</span>
216
+ {modes && <Dropdown variant="nav" size="sm" value={mode} onChange={setMode} options={PICKER_MODES} ariaLabel={`${label} mode`} />}
224
217
  </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 })} />
218
+ {hexField}
227
219
  <div className="w-full overflow-hidden rounded-[var(--kol-radius-xs)]" style={{ height: 200 }}>
228
220
  <SBSquare hue={hsva.hue} sat={hsva.sat} val={hsva.val} onChange={(sat, val) => commit({ ...hsva, sat, val })} />
229
221
  </div>
222
+ <HueStrip hue={hsva.hue} onChange={(hue) => commit({ ...hsva, hue })} />
230
223
  {modes && mode !== 'hex' && CHANNELS[mode].map((ch) => <ChannelStrip key={ch.key} ch={ch} hex={hsvaToHex({ ...hsva, alpha: 1 })} hsva={hsva} onHsva={commit} />)}
224
+ {(!modes || mode === 'hex' || mode === 'rgb') && <ChannelStrip ch={BRIGHTNESS} hex={hsvaToHex({ ...hsva, alpha: 1 })} hsva={hsva} onHsva={commit} />}
231
225
  {alpha && <AlphaStrip alpha={hsva.alpha} color={opaque} onChange={(a) => commit({ ...hsva, alpha: a })} />}
232
226
  </div>
233
227
  )