@kolkrabbi/kol-component 0.123.0 → 0.125.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.123.0",
3
+ "version": "0.125.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",
@@ -47,6 +47,33 @@ const Dropdown = ({
47
47
  size,
48
48
  variant = 'primary',
49
49
  tone = 'default',
50
+ /* DropdownGhostWidthAndListHeight (kol-mirror 2026-08-28 — user, on the
51
+ * studio's 30-option Source picker in a ~300px shelf: "1 it's way too tall,
52
+ * 2 it's not fitting"):
53
+ * `maxRows` how many rows the panel shows before the list scrolls
54
+ * (default 10). Popover's size middleware clamps the panel to
55
+ * the VIEWPORT, which does nothing for a 30-row slab opened at
56
+ * the top of a tall page — and its inline maxHeight cannot be
57
+ * overridden from a consumer stylesheet, so the ceiling is ours.
58
+ * `rowHeight` the row pitch (number = px, or any CSS length) for a chrome
59
+ * whose own rows are shorter than the h-8 rung. Inline on the
60
+ * row, because `h-8` is a utility and a rule cannot out-rank it.
61
+ * Both write variables on the panel that `.kol-dd-list` reads. */
62
+ maxRows = 10,
63
+ rowHeight,
64
+ /* `onOptionHover(value | null)` (DropdownOptionHoverPreview, kol-mirror
65
+ * 2026-08-28): the pointer entering a row reports its value, leaving reports
66
+ * `null` — so a picker over a VISUAL setting can preview the hovered option
67
+ * live and revert on leave. mirror's blend-mode picker applies the hovered
68
+ * mode to the composite: you scrub 16 modes against the actual image instead
69
+ * of committing to each one to look at it. The same case is every picker over
70
+ * easing curves, palettes, filters, fonts — the DS owns the panel, only the
71
+ * consumer knows what to preview, so this reports and nothing else.
72
+ *
73
+ * `null` fires ON CLOSE too, and that half is load-bearing: a panel dismissed
74
+ * while a row is hovered would otherwise leave the consumer previewing
75
+ * forever. It never fires while closed — a closed dropdown has no rows. */
76
+ onOptionHover,
50
77
  defaultOpen = false,
51
78
  className = ''
52
79
  }) => {
@@ -82,6 +109,21 @@ const Dropdown = ({
82
109
  setIsOpen(false)
83
110
  }
84
111
 
112
+ /* the hover report, and the close that ends it. `hovered` is a ref, not
113
+ * state — the preview is the consumer's business and re-rendering the panel
114
+ * on every row crossed would be a render per pointermove for nothing. */
115
+ const hovered = useRef(null)
116
+ const reportHover = (v) => {
117
+ if (hovered.current === v) return
118
+ hovered.current = v
119
+ onOptionHover?.(v)
120
+ }
121
+ useEffect(() => {
122
+ if (isOpen || hovered.current == null) return
123
+ hovered.current = null
124
+ onOptionHover?.(null)
125
+ }, [isOpen]) // eslint-disable-line react-hooks/exhaustive-deps
126
+
85
127
  const currentOption = options.find((opt) => opt.value === value) || options[0]
86
128
 
87
129
  /* A clamped list (Popover caps the panel to the viewport) can open with the
@@ -108,7 +150,7 @@ const Dropdown = ({
108
150
  ].filter(Boolean).join(' ')
109
151
 
110
152
  return (
111
- <div className={`relative inline-block align-middle ${className}`}>
153
+ <div className={`kol-dd-root relative inline-block align-middle ${className}`}>
112
154
  <button
113
155
  ref={popover.refs.setReference}
114
156
  {...popover.getReferenceProps()}
@@ -139,6 +181,10 @@ const Dropdown = ({
139
181
  panel={false}
140
182
  focus={false}
141
183
  className={`kol-dd-panel kol-dd-panel--${resolvedVariant} ${toneClass(tone)}`.trim()}
184
+ style={{
185
+ '--kol-dd-max-rows': maxRows ?? 10,
186
+ ...(rowHeight != null ? { '--kol-dd-row-h': typeof rowHeight === 'number' ? `${rowHeight}px` : rowHeight } : null),
187
+ }}
142
188
  >
143
189
  {(resolvedVariant === 'primary' || resolvedVariant === 'grey') && <div className="kol-dd-div" />}
144
190
 
@@ -155,6 +201,9 @@ const Dropdown = ({
155
201
  * all — and this was the last one left, the option row's ink
156
202
  * brighten. The check mark is what marks the current value. */
157
203
  hover={false}
204
+ height={rowHeight}
205
+ onPointerEnter={onOptionHover ? () => reportHover(option.value) : undefined}
206
+ onPointerLeave={onOptionHover ? () => reportHover(null) : undefined}
158
207
  onClick={() => handleSelect(option)}
159
208
  shortcut={isActive ? <Icon name="check" size={11} /> : undefined}
160
209
  >
@@ -79,6 +79,11 @@ export function MenuItem({
79
79
  /**
80
80
  * MenuDropdownItem — action row inside a MenuItem's dropdown panel.
81
81
  *
82
+ * `height` (default none — the `h-8` rung) — an inline row height, for a panel
83
+ * in a chrome whose own rows are shorter (DropdownGhostWidthAndListHeight,
84
+ * kol-mirror 2026-08-28: its controls sit in 24px rows and the DS row was a
85
+ * third taller). Inline because `h-8` is a utility and a rule cannot out-rank it.
86
+ *
82
87
  * `hover` (default true) — `false` drops the ink brighten, for a panel ruled
83
88
  * to carry NO hover state at all (user 2026-08-28, on `Dropdown`: "delete any
84
89
  * hover state on dropdowns"). Scoped to the caller: a MenuItem's own menu keeps
@@ -94,14 +99,17 @@ export function MenuItem({
94
99
  * - children — main label, flex-1.
95
100
  * - shortcut — trailing content (text shortcut hint, ✓ marker, or icon).
96
101
  */
97
- export function MenuDropdownItem({ onClick, disabled, prefix, iconLeft, shortcut, hover = true, children }) {
102
+ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disabled, prefix, iconLeft, shortcut, hover = true, height, children }) {
98
103
  return (
99
104
  <button
100
105
  type="button"
101
106
  data-menu-item
102
107
  onClick={onClick}
108
+ onPointerEnter={onPointerEnter}
109
+ onPointerLeave={onPointerLeave}
103
110
  disabled={disabled}
104
111
  role="menuitem"
112
+ style={height != null ? { height } : undefined}
105
113
  className={`w-full kol-helper-12 px-3 h-8 shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
106
114
  >
107
115
  {prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}