@kolkrabbi/kol-component 0.186.0 → 0.187.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.187.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/hooks/colorMath.js
CHANGED
|
@@ -104,6 +104,37 @@ export function harmonyColors(hue, harmony, { saturation = 100, lightness = 50 }
|
|
|
104
104
|
return roleOffsets.map((off) => hslToHex(normHue(hue + off), saturation, lightness))
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Re-hue an EXISTING palette to a harmony, preserving each slot's own
|
|
109
|
+
* saturation and lightness.
|
|
110
|
+
*
|
|
111
|
+
* `harmonyColors` builds every role at ONE flat S/L, which is right when a
|
|
112
|
+
* caller has no palette yet and wrong the moment it does: a Light slot and a
|
|
113
|
+
* Dark slot both come back at 50% lightness, so the palette flattens on the
|
|
114
|
+
* first drag of the wheel (kol-fxr, `editor-set-is-behind-its-source`
|
|
115
|
+
* 2026-09-03 — its own wheel emits a hue and re-hues slot by slot, so the
|
|
116
|
+
* package's `colors` payload was half-ignorable).
|
|
117
|
+
*
|
|
118
|
+
* A slot that is `locked`, empty, or has no hex is passed through untouched —
|
|
119
|
+
* locking a colour is the one instruction a re-hue must not overrule.
|
|
120
|
+
*
|
|
121
|
+
* @param {number} hue base hue, 0–360
|
|
122
|
+
* @param {string|object} harmony harmony id or object
|
|
123
|
+
* @param {Array<{hex?: string, locked?: boolean}|string|null>} slots the current palette, in role order
|
|
124
|
+
* @returns {Array} the same shape back, re-hued — strings stay strings, objects keep every other key
|
|
125
|
+
*/
|
|
126
|
+
export function reHueSlots(hue, harmony, slots = []) {
|
|
127
|
+
const { roleOffsets } = harmonyById(harmony)
|
|
128
|
+
return slots.map((slot, i) => {
|
|
129
|
+
const off = roleOffsets[i % roleOffsets.length]
|
|
130
|
+
const hex = typeof slot === 'string' ? slot : slot?.hex
|
|
131
|
+
if (!hex || (typeof slot === 'object' && slot?.locked)) return slot
|
|
132
|
+
const { s, l } = hexToHsl(hex)
|
|
133
|
+
const next = hslToHex(normHue(hue + off), s, l)
|
|
134
|
+
return typeof slot === 'string' ? next : { ...slot, hex: next }
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
/**
|
|
108
139
|
* Deterministic role colors derived from a base hex (S/L taken from the hex).
|
|
109
140
|
* Convenience wrapper over `harmonyColors` for callers holding a color, not
|
|
@@ -3,7 +3,7 @@ import { toneClass, TONE_VARS } from '../utilities/tone.js'
|
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
4
|
import { MenuDropdownItem } from './MenuItem.jsx'
|
|
5
5
|
import { PopoverPanel, usePopover } from '../utilities/Popover.jsx'
|
|
6
|
-
import { indicatorSize } from '../hooks/glyphLadders.js'
|
|
6
|
+
import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Dropdown — trigger IS button chrome (2026-07-08 chrome law).
|
|
@@ -77,6 +77,18 @@ const Dropdown = ({
|
|
|
77
77
|
* while a row is hovered would otherwise leave the consumer previewing
|
|
78
78
|
* forever. It never fires while closed — a closed dropdown has no rows. */
|
|
79
79
|
onOptionHover,
|
|
80
|
+
/* ICON-ONLY TRIGGER — an icon name, or a pre-rendered node dropped in where
|
|
81
|
+
* the glyph goes. The trigger becomes the pinned square (`kol-btn-icon`) at
|
|
82
|
+
* the current size, with no label, no ghost widths and no caret; the panel
|
|
83
|
+
* still sizes to its own rows rather than the trigger. This is what a tool
|
|
84
|
+
* rail needs, and its absence is why kol-fxr's ToolPalette hand-rolls a
|
|
85
|
+
* trigger out of `PopoverPanel` + `usePopover` at a bespoke 36/22 instead of
|
|
86
|
+
* importing anything (`editor-set-is-behind-its-source`, 2026-09-03). */
|
|
87
|
+
iconOnly,
|
|
88
|
+
/* A mark drawn INSIDE the trigger box, over the glyph — the tool-palette
|
|
89
|
+
* corner fold is the case. Kept a slot rather than a boolean so the square
|
|
90
|
+
* has one implementation and the drawing stays the caller's. */
|
|
91
|
+
triggerAdornment,
|
|
80
92
|
defaultOpen = false,
|
|
81
93
|
className = ''
|
|
82
94
|
}) => {
|
|
@@ -163,6 +175,12 @@ const Dropdown = ({
|
|
|
163
175
|
`kol-btn-${resolvedSize}`,
|
|
164
176
|
SIZE_TYPE[resolvedSize],
|
|
165
177
|
'kol-dd-trigger',
|
|
178
|
+
/* ICON-ONLY: the pinned square from Button's own class, so a tool-rail
|
|
179
|
+
* dropdown is the same box as the icon button beside it at every rung
|
|
180
|
+
* (editor-set-is-behind-its-source, kol-fxr 2026-09-03 — its ToolPalette
|
|
181
|
+
* hand-rolls a trigger out of PopoverPanel + usePopover precisely because
|
|
182
|
+
* this mode did not exist). No label, no ghost widths, no caret. */
|
|
183
|
+
iconOnly && 'kol-btn-icon kol-dd-trigger--icon',
|
|
166
184
|
isOpen && 'kol-dd-trigger--open',
|
|
167
185
|
/* the dark chip on a washed plane; the panel continues it (ControlToneInverse, 2026-08-27) */
|
|
168
186
|
toneClass(tone),
|
|
@@ -179,20 +197,33 @@ const Dropdown = ({
|
|
|
179
197
|
aria-expanded={isOpen}
|
|
180
198
|
data-state={isOpen ? 'open' : 'closed'}
|
|
181
199
|
>
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
{iconOnly ? (
|
|
201
|
+
/* the glyph comes from the SOLO ladder — an icon alone in a pinned
|
|
202
|
+
* square — never from a call-site number */
|
|
203
|
+
typeof iconOnly === 'string'
|
|
204
|
+
? <Icon name={iconOnly} size={glyphSize(resolvedSize, true)} />
|
|
205
|
+
: iconOnly
|
|
206
|
+
) : (
|
|
207
|
+
<>
|
|
208
|
+
{/* every option's label rides along hidden so the trigger is as wide
|
|
209
|
+
* as its widest value — the panel matches the trigger's width, so
|
|
210
|
+
* trigger and list stay one piece at every selection */}
|
|
211
|
+
<span className="kol-dd-label">
|
|
212
|
+
<span>{currentOption?.label}</span>
|
|
213
|
+
{options.map((option) => (
|
|
214
|
+
<span key={option.value} className="kol-dd-ghost" aria-hidden="true">
|
|
215
|
+
{option.label}
|
|
216
|
+
</span>
|
|
217
|
+
))}
|
|
190
218
|
</span>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
{/* chrome lives in .kol-dd-caret (trailing edge + open-state flip) —
|
|
220
|
+
* keyed off the trigger's data-state, no inline styles */}
|
|
221
|
+
<Icon name="chevron-down" size={indicatorSize(resolvedSize)} className="kol-dd-caret" />
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
{/* a consumer's own trigger mark — the tool-palette corner fold rides
|
|
225
|
+
here so the square keeps ONE implementation */}
|
|
226
|
+
{triggerAdornment}
|
|
196
227
|
</button>
|
|
197
228
|
|
|
198
229
|
<PopoverPanel
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
-
import { HARMONIES, harmonyById, harmonyColors, normHue } from '../hooks/colorMath.js'
|
|
2
|
+
import { HARMONIES, harmonyById, harmonyColors, normHue, reHueSlots } from '../hooks/colorMath.js'
|
|
3
3
|
|
|
4
4
|
/* taxonomy-ok: a pure-canvas hue + harmony picker in the SpectrumControls
|
|
5
5
|
* color-picker family. It nests no KOL component — its only import is the
|
|
@@ -20,6 +20,16 @@ import { HARMONIES, harmonyById, harmonyColors, normHue } from '../hooks/colorMa
|
|
|
20
20
|
* arrow keys. `colors` is one hex per role offset of the active harmony
|
|
21
21
|
* (see colorMath.harmonyColors), matching the satellite markers 1:1.
|
|
22
22
|
*
|
|
23
|
+
* PASS `slots` IF YOU ALREADY HAVE A PALETTE (2026-09-03,
|
|
24
|
+
* `editor-set-is-behind-its-source`). Without it, `colors` is built at ONE
|
|
25
|
+
* flat `saturation`/`lightness`, so a Light role and a Dark role both come
|
|
26
|
+
* back at 50% — the palette flattens on the first drag, and kol-fxr's editor
|
|
27
|
+
* had to ignore half the payload because its own wheel re-hues slot by slot.
|
|
28
|
+
* With `slots`, every entry keeps its own S/L and only its hue moves, and a
|
|
29
|
+
* `locked` or empty slot is passed through untouched. `onHueChange(hue)` is
|
|
30
|
+
* the same seam with no payload at all, for a caller that owns the derivation
|
|
31
|
+
* outright — which is what fxr's wheel emits.
|
|
32
|
+
*
|
|
23
33
|
* The ring hues, marker outlines and handle halo are literal color math
|
|
24
34
|
* (hsl / #FFFFFF / rgba) on purpose — a spectrum is not themeable, and the
|
|
25
35
|
* markers sit on fully-saturated ring hues, not on the surface (same
|
|
@@ -31,7 +41,9 @@ import { HARMONIES, harmonyById, harmonyColors, normHue } from '../hooks/colorMa
|
|
|
31
41
|
* @param {number} saturation base saturation for emitted colors, 0–100 (default 100)
|
|
32
42
|
* @param {number} lightness base lightness for emitted colors, 0–100 (default 50)
|
|
33
43
|
* @param {Array} harmonies injectable scheme table (default HARMONIES)
|
|
44
|
+
* @param {Array} slots the CURRENT palette in role order (`{hex, locked}` objects or plain hex strings). Given, `colors` re-hues these — each slot keeps its own S/L, locked and empty entries pass through — instead of generating flat ones
|
|
34
45
|
* @param {Function} onChange ({ hue, colors }) => void
|
|
46
|
+
* @param {Function} onHueChange (hue) => void — the payload-free seam, for a caller that derives its own colours
|
|
35
47
|
*/
|
|
36
48
|
|
|
37
49
|
/* Marker outline — white for contrast against the fully-saturated ring hues
|
|
@@ -44,8 +56,10 @@ export default function PaletteHarmonyWheel({
|
|
|
44
56
|
harmony = 'analogous',
|
|
45
57
|
saturation = 100,
|
|
46
58
|
lightness = 50,
|
|
59
|
+
slots,
|
|
47
60
|
harmonies = HARMONIES,
|
|
48
61
|
onChange,
|
|
62
|
+
onHueChange,
|
|
49
63
|
}) {
|
|
50
64
|
const canvasRef = useRef(null)
|
|
51
65
|
const draggingRef = useRef(false)
|
|
@@ -58,10 +72,18 @@ export default function PaletteHarmonyWheel({
|
|
|
58
72
|
)
|
|
59
73
|
|
|
60
74
|
/* Emit next hue + its harmony colors. Held in a ref so the pointer/key
|
|
61
|
-
* handlers stay stable while always seeing the latest props.
|
|
75
|
+
* handlers stay stable while always seeing the latest props.
|
|
76
|
+
*
|
|
77
|
+
* With `slots`, the colours are the CALLER'S palette re-hued — each slot
|
|
78
|
+
* keeping its own saturation and lightness — rather than a fresh flat set.
|
|
79
|
+
* Both fire, so a caller can take the hue and ignore the colours. */
|
|
62
80
|
emitRef.current = (nextHue) => {
|
|
63
81
|
const h = normHue(nextHue)
|
|
64
|
-
|
|
82
|
+
const colors = slots?.length
|
|
83
|
+
? reHueSlots(h, active, slots)
|
|
84
|
+
: harmonyColors(h, active, { saturation, lightness })
|
|
85
|
+
onChange?.({ hue: h, colors })
|
|
86
|
+
onHueChange?.(h)
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
const outerR = size / 2 - 8
|