@kolkrabbi/kol-component 0.188.0 → 0.190.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.190.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
37
|
+
"@kolkrabbi/kol-icons": "^0.26.0"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"src",
|
|
@@ -41,8 +41,22 @@ import { usePopover, PopoverPanel } from '../utilities/Popover'
|
|
|
41
41
|
* entry.value on a palette pick
|
|
42
42
|
* label — row label (kol-helper-12); also prefixes aria-labels
|
|
43
43
|
* hideLabel — suppress the visible label (aria keeps it)
|
|
44
|
-
* refs — [{ value, label, hex }]
|
|
45
|
-
*
|
|
44
|
+
* refs — [{ value, label, hex? }] palette entries → popover mode.
|
|
45
|
+
* `hex` may be omitted when `resolveRef` is supplied
|
|
46
|
+
* resolveRef — (value) => hex — the RESOLVER SEAM. Without it, every
|
|
47
|
+
* entry must arrive pre-resolved and a `palette:accent`
|
|
48
|
+
* value cannot be shown at all: the swatch has no hex and
|
|
49
|
+
* the subtitle prints the raw ref. With it, a consumer
|
|
50
|
+
* keeps its own palette and this row renders live against
|
|
51
|
+
* it (editor-set-is-behind-its-source, kol-fxr 2026-09-03 —
|
|
52
|
+
* its ColorField takes `palette` and calls `resolveColor`)
|
|
53
|
+
* autoValue — the THEME state's value, typically a `var(--kol-*)`
|
|
54
|
+
* token that flips with light/dark. Set, the popover
|
|
55
|
+
* offers a Theme button; unset, it does not — a field with
|
|
56
|
+
* no auto value has no theme to fall back to
|
|
57
|
+
* size — control rung for the hex input, 'xs'|'sm'|'md'|'lg'
|
|
58
|
+
* (default 'sm'). A rail renders a dozen of these and the
|
|
59
|
+
* rung is the rail's decision, not each row's
|
|
46
60
|
* locked — lock overlay pinned visible, aria-pressed on the swatch
|
|
47
61
|
* onToggleLock — () => void — swatch click toggles the lock
|
|
48
62
|
* tokenName — resolved token readout (kol-helper-10) → grid mode
|
|
@@ -58,6 +72,9 @@ export default function ColorInputRow({
|
|
|
58
72
|
label,
|
|
59
73
|
hideLabel = false,
|
|
60
74
|
refs,
|
|
75
|
+
resolveRef,
|
|
76
|
+
autoValue,
|
|
77
|
+
size = 'sm',
|
|
61
78
|
locked = false,
|
|
62
79
|
onToggleLock,
|
|
63
80
|
tokenName,
|
|
@@ -67,25 +84,52 @@ export default function ColorInputRow({
|
|
|
67
84
|
className = '',
|
|
68
85
|
}) {
|
|
69
86
|
const hasRefs = Array.isArray(refs) && refs.length > 0
|
|
70
|
-
|
|
87
|
+
/* The popover carries the ref grid AND the quick states, so a field with no
|
|
88
|
+
* palette but an `autoValue` still gets one — that is the whole Theme/None
|
|
89
|
+
* affordance and it has nowhere else to live. */
|
|
90
|
+
const hasPopover = hasRefs || autoValue != null
|
|
91
|
+
const isLockToggle = !hasPopover && typeof onToggleLock === 'function'
|
|
71
92
|
const isGrid = tokenName != null
|
|
72
93
|
const labelVisible = label != null && !hideLabel
|
|
73
94
|
|
|
74
|
-
/*
|
|
75
|
-
*
|
|
95
|
+
/* A value is one of FOUR kinds, and the row has to tell them apart before it
|
|
96
|
+
* can render anything: a literal hex, a palette REF the consumer resolves, a
|
|
97
|
+
* themed `var(--kol-*)` token that flips with light/dark, or null — None.
|
|
98
|
+
* The first port only understood the first and the last. */
|
|
99
|
+
const isVar = typeof value === 'string' && value.startsWith('var(')
|
|
100
|
+
const isNone = value == null
|
|
101
|
+
|
|
102
|
+
/* Display resolution: a refs entry shows its own hex, `resolveRef` resolves
|
|
103
|
+
* anything else the consumer owns (a `palette:` ref, a token), and a bare
|
|
104
|
+
* hex IS the value. `hex` on the entry still wins, so a pre-resolved list
|
|
105
|
+
* needs no resolver and nothing existing moves. */
|
|
106
|
+
const resolve = (v) => {
|
|
107
|
+
if (v == null) return null
|
|
108
|
+
const entry = hasRefs ? refs.find((r) => r.value === v) : undefined
|
|
109
|
+
return entry?.hex ?? resolveRef?.(v) ?? (typeof v === 'string' && v.startsWith('#') ? v : null)
|
|
110
|
+
}
|
|
76
111
|
const activeRef = hasRefs ? refs.find((r) => r.value === value) : undefined
|
|
77
|
-
const displayHex =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
const displayHex = resolve(value)
|
|
113
|
+
/* A themed token renders LIVE in the swatch but has no meaningful hex to
|
|
114
|
+
* print, so the field shows its placeholder rather than a resolved literal
|
|
115
|
+
* the user cannot have typed. */
|
|
116
|
+
const digits = isVar || isNone ? '' : (displayHex ?? '').replace(/^#/, '').toUpperCase()
|
|
117
|
+
const showTransparent = unused || (isNone && !isVar)
|
|
118
|
+
const subtitle = isNone
|
|
119
|
+
? 'None'
|
|
120
|
+
: isVar
|
|
121
|
+
? 'Theme'
|
|
122
|
+
: (activeRef?.label ?? (displayHex ? '#' + digits : String(value)))
|
|
81
123
|
|
|
82
124
|
const [open, setOpen] = useState(false)
|
|
83
125
|
const popover = usePopover({ open, onOpenChange: setOpen, placement: 'bottom-start', offset: 4 })
|
|
84
126
|
|
|
85
|
-
const chip = (
|
|
127
|
+
const chip = (swatchSize) => (
|
|
86
128
|
<ColorSwatch
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
/* a themed token goes STRAIGHT to the swatch — `var(--kol-x)` is a live
|
|
130
|
+
* paint, and resolving it to a literal would freeze it out of the theme */
|
|
131
|
+
hex={showTransparent ? null : (isVar ? value : displayHex)}
|
|
132
|
+
size={swatchSize}
|
|
89
133
|
showTransparent={showTransparent}
|
|
90
134
|
transparentTone={transparentTone}
|
|
91
135
|
hoverable={false}
|
|
@@ -95,7 +139,7 @@ export default function ColorInputRow({
|
|
|
95
139
|
/* Swatch cell — popover trigger (refs), lock toggle (onToggleLock), or a
|
|
96
140
|
* plain preview chip. The lock overlay is a SIBLING of the swatch: inside
|
|
97
141
|
* it, ColorSwatch's overflow-hidden radius clip would cut the glyph off. */
|
|
98
|
-
const swatchCell =
|
|
142
|
+
const swatchCell = hasPopover ? (
|
|
99
143
|
<button
|
|
100
144
|
type="button"
|
|
101
145
|
ref={popover.refs.setReference}
|
|
@@ -134,10 +178,11 @@ export default function ColorInputRow({
|
|
|
134
178
|
const hexInput = (
|
|
135
179
|
<Input
|
|
136
180
|
variant="filled"
|
|
137
|
-
size=
|
|
181
|
+
size={size}
|
|
138
182
|
prefix="#"
|
|
139
183
|
chars={6}
|
|
140
184
|
maxLength={6}
|
|
185
|
+
placeholder={isVar ? 'auto' : '–'}
|
|
141
186
|
value={digits}
|
|
142
187
|
onChange={(e) => onChange?.('#' + e.target.value.replace(/^#/, '').toUpperCase())}
|
|
143
188
|
disabled={disabled}
|
|
@@ -170,28 +215,63 @@ export default function ColorInputRow({
|
|
|
170
215
|
{hexInput}
|
|
171
216
|
</div>
|
|
172
217
|
)}
|
|
173
|
-
{
|
|
218
|
+
{hasPopover && (
|
|
174
219
|
<PopoverPanel
|
|
175
220
|
popover={popover}
|
|
176
221
|
panel={false}
|
|
177
222
|
focus={false}
|
|
178
|
-
className="bg-surface-secondary border border-fg-08 rounded p-2 shadow-lg"
|
|
223
|
+
className="bg-surface-secondary border border-fg-08 rounded p-2 flex flex-col gap-2 shadow-lg"
|
|
179
224
|
style={{ minWidth: 200 }}
|
|
180
225
|
>
|
|
181
|
-
|
|
182
|
-
|
|
226
|
+
{hasRefs && (
|
|
227
|
+
<div className="grid grid-cols-6 gap-1">
|
|
228
|
+
{refs.map((entry) => (
|
|
229
|
+
<ColorSwatch
|
|
230
|
+
key={entry.value}
|
|
231
|
+
hex={entry.hex ?? resolveRef?.(entry.value) ?? null}
|
|
232
|
+
size="fill"
|
|
233
|
+
selected={entry.value === value}
|
|
234
|
+
title={entry.label}
|
|
235
|
+
onClick={() => {
|
|
236
|
+
onChange?.(entry.value)
|
|
237
|
+
setOpen(false)
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
{/* QUICK STATES. Theme (the auto value — a token that flips with
|
|
244
|
+
light/dark) is offered only where the field HAS one; None is
|
|
245
|
+
always available, because clearing a colour is not a palette
|
|
246
|
+
decision. Both were dropped in the first port, which is what left
|
|
247
|
+
`value == null` renderable but unreachable. */}
|
|
248
|
+
<div className="flex items-center gap-2">
|
|
249
|
+
{autoValue != null && (
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
onClick={() => { onChange?.(autoValue); setOpen(false) }}
|
|
253
|
+
aria-pressed={isVar}
|
|
254
|
+
className="flex items-center gap-1.5 kol-helper-12 text-fg-64 rounded px-1.5 h-6 border border-fg-08"
|
|
255
|
+
>
|
|
256
|
+
<ColorSwatch hex={resolve(autoValue) ?? autoValue} size={14} hoverable={false} />
|
|
257
|
+
Theme
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={() => { onChange?.(null); setOpen(false) }}
|
|
263
|
+
aria-pressed={isNone}
|
|
264
|
+
className="flex items-center gap-1.5 kol-helper-12 text-fg-64 rounded px-1.5 h-6 border border-fg-08"
|
|
265
|
+
>
|
|
183
266
|
<ColorSwatch
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
onClick={() => {
|
|
190
|
-
onChange?.(entry.value)
|
|
191
|
-
setOpen(false)
|
|
192
|
-
}}
|
|
267
|
+
hex="#FFFFFF"
|
|
268
|
+
size={14}
|
|
269
|
+
showTransparent
|
|
270
|
+
transparentTone={transparentTone}
|
|
271
|
+
hoverable={false}
|
|
193
272
|
/>
|
|
194
|
-
|
|
273
|
+
None
|
|
274
|
+
</button>
|
|
195
275
|
</div>
|
|
196
276
|
</PopoverPanel>
|
|
197
277
|
)}
|
|
@@ -42,19 +42,15 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
42
42
|
* @param {string} props.className - Additional classes on the trigger
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
/* Corner fold marker
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
/* Corner fold marker — `fold-indicator`, promoted into kol-icon-set-v1
|
|
46
|
+
* 2026-09-03 from kol-fxr's own drawing (editor-set-is-behind-its-source); the
|
|
47
|
+
* comment that stood here said it did not exist yet and should be promoted, so
|
|
48
|
+
* this is that. Lives inside the button so it dims with kol-btn-quiet and
|
|
49
|
+
* inverts with kol-btn-pressed (currentColor). */
|
|
48
50
|
const FoldIndicator = () => (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
height={4}
|
|
53
|
-
viewBox="0 0 4 4"
|
|
54
|
-
className="pointer-events-none absolute right-0.5 bottom-0.5 opacity-70"
|
|
55
|
-
>
|
|
56
|
-
<path d="M4 0v4H0z" fill="currentColor" />
|
|
57
|
-
</svg>
|
|
51
|
+
<span aria-hidden="true" className="pointer-events-none absolute right-0.5 bottom-0.5 opacity-70 inline-flex">
|
|
52
|
+
<Icon name="fold-indicator" size={4} />
|
|
53
|
+
</span>
|
|
58
54
|
)
|
|
59
55
|
|
|
60
56
|
const SplitToolButton = ({
|