@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.188.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.25.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 }] pre-resolved palette entries →
45
- * popover mode
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
- const isLockToggle = !hasRefs && typeof onToggleLock === 'function'
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
- /* Display resolution: when the current value is a refs entry, the swatch and
75
- * input show the entry's pre-resolved hex; otherwise value IS the hex. */
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 = activeRef?.hex ?? value ?? null
78
- const digits = (displayHex ?? '').replace(/^#/, '').toUpperCase()
79
- const showTransparent = unused || displayHex == null
80
- const subtitle = displayHex == null ? 'None' : (activeRef?.label ?? '#' + digits)
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 = (size) => (
127
+ const chip = (swatchSize) => (
86
128
  <ColorSwatch
87
- hex={showTransparent ? null : displayHex}
88
- size={size}
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 = hasRefs ? (
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="sm"
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
- {hasRefs && (
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
- <div className="grid grid-cols-6 gap-1">
182
- {refs.map((entry) => (
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
- key={entry.value}
185
- hex={entry.hex}
186
- size="fill"
187
- selected={entry.value === value}
188
- title={entry.label}
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. `tool-fold-indicator` doesn't exist in kol-icons yet —
46
- * icon-set candidate; inline until promoted. Lives inside the button so it
47
- * dims with kol-btn-quiet and inverts with kol-btn-pressed (currentColor). */
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
- <svg
50
- aria-hidden="true"
51
- width={4}
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 = ({