@kolkrabbi/kol-component 0.237.0 → 0.238.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.237.0",
3
+ "version": "0.238.0",
4
4
  "description": "The core component library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,7 @@
8
8
  "module": "./src/index.js",
9
9
  "exports": {
10
10
  ".": "./src/index.js",
11
+ "./atoms/RotaryDial": "./src/molecules/Knob.jsx",
11
12
  "./atoms/*": "./src/atoms/*.jsx",
12
13
  "./molecules/*": "./src/molecules/*.jsx",
13
14
  "./organisms/*": "./src/organisms/*.jsx",
@@ -25,8 +26,8 @@
25
26
  "embla-carousel-react": "^8.6.0",
26
27
  "pdfjs-dist": "^6.3.289",
27
28
  "react-syntax-highlighter": "^16.1.1",
28
- "@kolkrabbi/kol-search": "^0.3.0",
29
- "@kolkrabbi/kol-markdown": "^0.1.2"
29
+ "@kolkrabbi/kol-markdown": "^0.1.2",
30
+ "@kolkrabbi/kol-search": "^0.3.0"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@kolkrabbi/kol-icons": ">=0.22.0",
@@ -159,8 +159,11 @@ export default function Input({
159
159
  * from the font's ascender+descender (font-metric), not strictly from
160
160
  * CSS line-height. Result: kol-control-sm ends up 26.5px instead of 26.
161
161
  * h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights; on touch every size is
162
- * the 16 / 22 rung (D5, 2026-09-29 — see SearchInput) */
163
- const heightCls = `${size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'} pointer-coarse:h-[22px]`
162
+ * the 16 / 22 rung (D5, 2026-09-29 — see SearchInput) — except inside a hardware panel
163
+ * (`.kol-hw-panel`, 2026-10-02): a module's field keeps its desk height on touch, as the rest of
164
+ * the plate does (kol-components-controls.css). The type still goes to 16px there, so iOS does
165
+ * not zoom into it. */
166
+ const heightCls = `${size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'} pointer-coarse:not-[.kol-hw-panel_*]:h-[22px]`
164
167
 
165
168
  const inputCls = [
166
169
  'min-w-0 bg-transparent border-none outline-none text-auto',
@@ -36,6 +36,19 @@ import { glyphSize, indicatorSize } from '../hooks/glyphLadders.js'
36
36
 
37
37
  /* xs (ControlsXsRung, 2026-09-01): the panel rung, opt-in by prop — the default stays sm (2026-07-28 law) */
38
38
  const SIZE_TYPE = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
39
+ /* THE LIST IS THE TRIGGER'S, CONTINUED (2026-10-02 — user: *"the list has different padding then the
40
+ * button"*). Two things were off at every size but one. The rows were set in `kol-helper-12`
41
+ * whatever the trigger wore, and the helper is wider — "Newest first" fitted its trigger and was
42
+ * cut off in its own row. And the rows sat 4px in from the trigger's text, because the list pads
43
+ * itself by 4 on top of the row's padding. So: the row wears the trigger's face, and its side
44
+ * padding is the trigger's (8 · 12 · 16 · 20) less the list's 4. */
45
+ const ROW_PAD = { xs: 'px-1', sm: 'px-2', md: 'px-3', lg: 'px-4' }
46
+ /* …AND THE TRIGGER'S HEIGHT (same day — user: *"is it always the same regardless of size?"*). It was:
47
+ * 32px rows at sm · md · lg whatever the trigger stood (26 · 32 · 40), 20 at xs. A row is the
48
+ * trigger's box now, on the control ramp (22 · 26 · 32 · 40 — `--kol-ctl-*`, so the touch rung and
49
+ * a hardware panel move rows and trigger together). */
50
+ const ROW_H = { xs: 'h-[var(--kol-ctl-xs)]', sm: 'h-[var(--kol-ctl-sm)]', md: 'h-[var(--kol-ctl-md)]', lg: 'h-[var(--kol-ctl-lg)]' }
51
+ const ROW = { xs: `kol-mono-8 ${ROW_PAD.xs} ${ROW_H.xs}`, sm: `kol-mono-12 ${ROW_PAD.sm} ${ROW_H.sm}`, md: `kol-mono-14 ${ROW_PAD.md} ${ROW_H.md}`, lg: `kol-mono-16 ${ROW_PAD.lg} ${ROW_H.lg}` }
39
52
  /* Caret size comes from the INDICATOR ladder (glyphLadders.js) — the private
40
53
  * map that lived here was a transcription of ADJACENT, which is the wrong
41
54
  * ladder for a decoration: it put a caret one rung HEAVIER than the label
@@ -95,6 +108,11 @@ const Dropdown = ({
95
108
  * has one implementation and the drawing stays the caller's. */
96
109
  triggerAdornment,
97
110
  defaultOpen = false,
111
+ /* `stayOpen` — a click outside does not close the list (the trigger and Esc still do). For a
112
+ * surface where the open list is the thing being looked at while other controls are worked:
113
+ * the showcase's preview, with its toolbar pickers beside it (user 2026-10-02: *"can we
114
+ * disclude the preview from that behaviour?"*). */
115
+ stayOpen = false,
98
116
  className = ''
99
117
  }) => {
100
118
  const [isOpen, setIsOpen] = useState(defaultOpen)
@@ -107,12 +125,26 @@ const Dropdown = ({
107
125
  * ambient case is the one only this can serve. */
108
126
  const triggerRef = useRef(null)
109
127
  const [ambient, setAmbient] = useState(null)
128
+ /* …AND SO IS A ZOOM (2026-10-02, kol-monitor's rack — user: *"what about the dropdown in general?
129
+ * its kinda wrong no"*). A trigger inside a zoomed surface (the rack at 80%) is drawn smaller,
130
+ * but the portalled list was not: it kept its full-size type inside a panel pinned to the
131
+ * trigger's SHRUNKEN width, so the names were cut off — 18 of monitor's 40 presets at 80%, all
132
+ * of them at 50%. The trigger's scale is its drawn width over its layout width; the list's
133
+ * content takes the same zoom, so trigger and list stay one piece at any scale. */
134
+ const [scale, setScale] = useState(1)
135
+ const [inPanel, setInPanel] = useState(false)
110
136
  useLayoutEffect(() => {
111
137
  if (!isOpen || !triggerRef.current) return
112
138
  const cs = getComputedStyle(triggerRef.current)
113
139
  const vars = {}
114
140
  for (const v of TONE_VARS) { const val = cs.getPropertyValue(v).trim(); if (val) vars[v] = val }
115
141
  setAmbient(vars)
142
+ const laid = triggerRef.current.offsetWidth
143
+ const s = laid ? triggerRef.current.getBoundingClientRect().width / laid : 1
144
+ setScale(Math.abs(s - 1) > 0.01 ? s : 1)
145
+ /* and the panel it opened from: a hardware plate keeps its desk sizes on touch, and the
146
+ * portalled list would otherwise take the touch rung its trigger skipped */
147
+ setInPanel(!!triggerRef.current.closest('.kol-hw-panel'))
116
148
  }, [isOpen])
117
149
 
118
150
  // sm everywhere unless explicitly overridden (see docblock size law).
@@ -132,6 +164,7 @@ const Dropdown = ({
132
164
  flip: false,
133
165
  matchReferenceWidth: true,
134
166
  role: 'listbox',
167
+ dismiss: stayOpen ? { outsidePress: false } : true,
135
168
  })
136
169
 
137
170
  /* Width belongs to the CALL SITE (2026-08-09 user call — "width without any
@@ -241,21 +274,25 @@ const Dropdown = ({
241
274
  popover={popover}
242
275
  panel={false}
243
276
  focus={false}
244
- className={`kol-dd-panel ${resolvedVariant ? `kol-dd-panel--${resolvedVariant}` : ''} ${toneClass(tone)}`.replace(/\s+/g, ' ').trim()}
277
+ className={`kol-dd-panel ${resolvedVariant ? `kol-dd-panel--${resolvedVariant}` : ''} ${toneClass(tone)} ${inPanel ? 'kol-hw-panel' : ''}`.replace(/\s+/g, ' ').trim()}
245
278
  style={{
246
279
  ...ambient,
247
280
  '--kol-dd-max-rows': maxRows ?? 10,
248
- /* xs rows are 20px; the panel's max-height reads the row height */
249
- ...(resolvedSize === 'xs' && rowHeight == null ? { '--kol-dd-row-h': '20px' } : null),
281
+ /* a row is the trigger's height; the panel's max-height reads it */
282
+ ...(rowHeight == null ? { '--kol-dd-row-h': `var(--kol-ctl-${resolvedSize})` } : null),
250
283
  ...(rowHeight != null ? { '--kol-dd-row-h': typeof rowHeight === 'number' ? `${rowHeight}px` : rowHeight } : null),
251
284
  }}
252
285
  >
253
- {(resolvedVariant === 'primary' || resolvedVariant === 'grey') && <div className="kol-dd-div" />}
286
+ {/* THE HAIRLINE UNDER THE TRIGGER, ALWAYS (2026-10-02 — user: *"I see it sometimes and sometimes
287
+ * not"*). It was drawn only for `variant="primary" | "grey"`: a dropdown toned by `tone=`
288
+ * got none, and an outline one lost its divider too — the panel sits 1px over the trigger's
289
+ * bottom border, which was meant to be it. One rule: an open dropdown has the line. */}
290
+ <div className="kol-dd-div" />
254
291
 
255
- <div ref={listRef} className="kol-dd-list" role="listbox">
292
+ <div ref={listRef} className="kol-dd-list" role="listbox" style={scale !== 1 ? { zoom: scale } : undefined}>
256
293
  {options.map((option, i) => {
257
294
  if (option?.divider) return <MenuDropdownDivider key={`divider-${i}`} />
258
- if (option?.heading) return <div key={`heading-${i}`} className={`${resolvedSize === 'xs' ? 'kol-mono-8 px-2 h-5' : 'kol-helper-12 px-3 h-8'} flex shrink-0 items-center text-meta`}>{option.heading}</div>
295
+ if (option?.heading) return <div key={`heading-${i}`} className={`${resolvedSize === 'xs' ? 'kol-mono-8' : 'kol-helper-12'} ${ROW_H[resolvedSize] ?? ROW_H.sm} ${ROW_PAD[resolvedSize] ?? ROW_PAD.sm} flex shrink-0 items-center text-meta`}>{option.heading}</div>
259
296
  const isActive = option.value === currentOption?.value
260
297
  return (
261
298
  <MenuDropdownItem
@@ -268,6 +305,7 @@ const Dropdown = ({
268
305
  * brighten. The check mark is what marks the current value. */
269
306
  hover={false}
270
307
  size={resolvedSize}
308
+ rowClass={ROW[resolvedSize] ?? ROW.sm}
271
309
  height={rowHeight}
272
310
  onPointerEnter={onOptionHover ? () => reportHover(option.value) : undefined}
273
311
  onPointerLeave={onOptionHover ? () => reportHover(null) : undefined}
@@ -122,7 +122,10 @@ export function MenuItem({
122
122
  * stack reserves the widest label in its own face, and a row set in a different
123
123
  * one measured 3px wider and clipped the selected row beside its check. */
124
124
  const ROW_BY_SIZE = { xs: 'kol-mono-8 px-2 h-5', sm: 'kol-helper-12 px-3 h-8', md: 'kol-helper-12 px-3 h-8', lg: 'kol-helper-12 px-3 h-8' }
125
- export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disabled, prefix, iconLeft, shortcut, hover = true, height, size = 'sm', children }) {
125
+ /* `rowClass` — the row's type, side padding and height, whole, in place of the size's own
126
+ * (Dropdown passes it, 2026-10-02: its rows wear the trigger's face and sit on the trigger's
127
+ * text edge; a menu's rows keep `ROW_BY_SIZE`). */
128
+ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disabled, prefix, iconLeft, shortcut, hover = true, height, size = 'sm', rowClass, children }) {
126
129
  return (
127
130
  <button
128
131
  type="button"
@@ -133,7 +136,7 @@ export function MenuDropdownItem({ onClick, onPointerEnter, onPointerLeave, disa
133
136
  disabled={disabled}
134
137
  role="menuitem"
135
138
  style={height != null ? { height } : undefined}
136
- className={`kol-menu-btn w-full ${ROW_BY_SIZE[size] ?? ROW_BY_SIZE.sm} shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
139
+ className={`kol-menu-btn w-full ${rowClass ?? ROW_BY_SIZE[size] ?? ROW_BY_SIZE.sm} shrink-0 inline-flex items-center gap-2 text-body ${hover ? 'hover:text-emphasis' : ''} disabled:opacity-40 disabled:cursor-not-allowed text-left`}
137
140
  >
138
141
  {prefix && <span className="shrink-0 inline-flex items-center">{prefix}</span>}
139
142
  {iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center">{iconLeft}</span>}
@@ -90,6 +90,9 @@ export function HighlightMatch({ label, query, ranges }) {
90
90
  * @param {Function} [onOpenResults] (query) => void — ⌘/Ctrl+Enter: every hit, on the consumer's
91
91
  * own results surface. Plain Enter keeps its meaning.
92
92
  * @param {string} [resultsLabel] the footer's second line for ⌘Enter (e.g. `All results`)
93
+ * @param {string} [selectLabel] the footer's Enter line while a row is highlighted (default
94
+ * `Go to page`) — a modal whose rows are not pages says what
95
+ * Enter does instead (the rack's module search: `Add module`)
93
96
  */
94
97
  export default function ShellSearchOverlay({
95
98
  open,
@@ -112,6 +115,7 @@ export default function ShellSearchOverlay({
112
115
  enterLabel,
113
116
  onOpenResults,
114
117
  resultsLabel = 'All results',
118
+ selectLabel = 'Go to page',
115
119
  }) {
116
120
  const panelRef = useRef(null)
117
121
  const listRef = useRef(null)
@@ -289,7 +293,7 @@ export default function ShellSearchOverlay({
289
293
  {(results.length > 0 || enterLabel) && (
290
294
  <p className="flex items-center gap-2 kol-helper-12 text-fg-48 mx-2 px-4 border-x border-transparent">
291
295
  <Kbd icon="corner-down-left" />
292
- {results.length > 0 ? 'Go to page' : enterLabel}
296
+ {results.length > 0 ? selectLabel : enterLabel}
293
297
  </p>
294
298
  )}
295
299
  {/* THE RESULTS LINE IS A LINK (user 2026-10-01: *"make 'all results...' at the bottom
@@ -123,7 +123,9 @@ export function usePopover({
123
123
  useClick(data.context, { enabled: click }),
124
124
  useHover(data.context, { enabled: hover, delay: hoverDelay, move: false }),
125
125
  useFocus(data.context, { enabled: focus }),
126
- useDismiss(data.context, { enabled: dismiss }),
126
+ /* `dismiss` may be an object of floating-ui's own options — `{ outsidePress: false }` keeps Esc
127
+ * and drops the outside click (Dropdown `stayOpen`) */
128
+ useDismiss(data.context, typeof dismiss === 'object' && dismiss ? { enabled: true, ...dismiss } : { enabled: dismiss }),
127
129
  useRole(data.context, { role }),
128
130
  ])
129
131