@kolkrabbi/kol-component 0.224.0 → 0.226.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.
@@ -1,5 +1,5 @@
1
- import { createContext, useContext, useState } from 'react'
2
- import { toneClass } from './tone.js'
1
+ import { cloneElement, createContext, isValidElement, useContext, useLayoutEffect, useState, version as reactVersion } from 'react'
2
+ import { toneClass, TONE_VARS } from './tone.js'
3
3
  import {
4
4
  useFloating,
5
5
  autoUpdate,
@@ -136,13 +136,29 @@ export function usePopover({
136
136
  * — the innermost wins, as the native `title` it replaced behaved. */
137
137
  const TooltipNest = createContext(null)
138
138
 
139
- export function Tooltip({
139
+ /* `asChild` (the native-title sweep, 2026-09-26): the tooltip attaches to its one child element
140
+ * instead of wrapping it in a span — for a trigger whose box IS the layout (a grid cell, a rail row,
141
+ * an absolutely placed handle), where an inline-flex wrapper would move it. The child must be an
142
+ * element that takes a ref: an intrinsic tag always does; a component only if it forwards one
143
+ * (React 18 needs forwardRef), so Button / IconFrame keep the wrapper. Its own ref and handlers are
144
+ * kept — the reference props merge over the child's. */
145
+ const childRefOf = (el) => ('ref' in (el.props ?? {}) ? el.props.ref : reactVersion.startsWith('18') ? el.ref : undefined)
146
+
147
+ export function Tooltip(props) {
148
+ /* no label → no tooltip: several call sites pass an optional hint, and an empty panel is worse
149
+ * than none. A separate component so the hooks below never run conditionally. */
150
+ if (props.label == null || props.label === '') return props.children
151
+ return <TooltipOn {...props} />
152
+ }
153
+
154
+ function TooltipOn({
140
155
  label,
141
156
  shortcut,
142
157
  placement = 'bottom',
143
158
  offset = 6,
144
159
  children,
145
160
  triggerClassName = 'inline-flex',
161
+ asChild = false,
146
162
  }) {
147
163
  const [open, setOpen] = useState(false)
148
164
  const [covered, setCovered] = useState(false)
@@ -158,22 +174,56 @@ export function Tooltip({
158
174
  focus: true,
159
175
  })
160
176
 
177
+ /* THE TONE OF WHERE IT IS (user, 2026-09-26: "tooltip should probably use same tone as dropdowns
178
+ * and buttons"). The panel is portalled to body, outside the `kol-tone-*` wrapper that tones the
179
+ * controls around the trigger — Dropdown's problem, solved the same way: on open, copy the
180
+ * trigger's RESOLVED `--kol-tone-*` onto the panel. No tone in effect = nothing copied, and the
181
+ * tooltip keeps its own surface. */
182
+ const [ambient, setAmbient] = useState(null)
183
+ const shown = open && !covered
184
+ useLayoutEffect(() => {
185
+ const ref = popover.refs.reference.current
186
+ /* the SURROUNDINGS' tone, not the trigger's own: an `asChild` trigger can be a variant button
187
+ * (`kol-btn-ghost` carries its own `--kol-tone-*`), so read its parent; the wrapper span has no
188
+ * variant and inherits the ambient values as it is */
189
+ const el = asChild ? ref?.parentElement : ref
190
+ if (!shown || !(el instanceof Element)) return
191
+ const cs = getComputedStyle(el)
192
+ const vars = {}
193
+ for (const v of TONE_VARS) { const val = cs.getPropertyValue(v).trim(); if (val) vars[v] = val }
194
+ setAmbient(vars)
195
+ }, [shown]) // eslint-disable-line react-hooks/exhaustive-deps
196
+
197
+ const child = asChild && isValidElement(children) ? children : null
198
+ const childRef = child ? childRefOf(child) : undefined
199
+ const setRef = (node) => {
200
+ popover.refs.setReference(node)
201
+ if (typeof childRef === 'function') childRef(node)
202
+ else if (childRef) childRef.current = node
203
+ }
161
204
  return (
162
205
  <>
163
- <span
164
- ref={popover.refs.setReference}
165
- {...popover.getReferenceProps()}
166
- className={triggerClassName}
167
- >
168
- <TooltipNest.Provider value={setCovered}>{children}</TooltipNest.Provider>
169
- </span>
206
+ {child ? (
207
+ <TooltipNest.Provider value={setCovered}>
208
+ {cloneElement(child, { ...popover.getReferenceProps(child.props), ref: setRef })}
209
+ </TooltipNest.Provider>
210
+ ) : (
211
+ <span
212
+ ref={popover.refs.setReference}
213
+ {...popover.getReferenceProps()}
214
+ className={triggerClassName}
215
+ >
216
+ <TooltipNest.Provider value={setCovered}>{children}</TooltipNest.Provider>
217
+ </span>
218
+ )}
170
219
  <PopoverPanel
171
220
  popover={popover}
172
221
  focus={false}
173
222
  panel={false}
174
223
  className="kol-tooltip"
224
+ style={ambient ?? undefined}
175
225
  >
176
- <span className="text-emphasis">{label}</span>
226
+ <span>{label}</span>
177
227
  {shortcut && <span className="kol-tooltip-key">{shortcut}</span>}
178
228
  </PopoverPanel>
179
229
  </>
@@ -38,6 +38,7 @@
38
38
  */
39
39
 
40
40
  import { useCallback, useEffect, useRef, useState } from 'react'
41
+ import { Tooltip } from './Popover.jsx'
41
42
 
42
43
  /* ── coordinates ─────────────────────────────────────────────────────────
43
44
  * Pure. No DOM writes. The invariant everything rests on: a cell is a
@@ -442,14 +443,15 @@ export default function QuadrantSync ({
442
443
  className="kol-mono-12 text-fg-96 bg-fg-04 border border-fg-16"
443
444
  style={{ width: 52, padding: '5px 6px', textAlign: 'center', borderRadius: 4 }}
444
445
  />
446
+ <Tooltip label="Set divisions so cells come out square at the current size" asChild>
445
447
  <button
446
448
  type="button" onClick={applySquare}
447
- title="Set divisions so cells come out square at the current size"
448
449
  className="kol-helper-10 text-fg-96 border border-fg-16"
449
450
  style={{ marginLeft: 'auto', padding: '5px 9px', borderRadius: 4, background: 'transparent', cursor: 'pointer' }}
450
451
  >
451
452
  square
452
453
  </button>
454
+ </Tooltip>
453
455
  </div>
454
456
  {aspect && (
455
457
  <div className="kol-mono-10 text-fg-48" style={{ marginTop: 6 }}>