@kolkrabbi/kol-component 0.158.0 → 0.159.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.158.0",
3
+ "version": "0.159.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",
@@ -13,7 +13,7 @@ import { glyphSize } from '../hooks/glyphLadders.js'
13
13
  * @param {Object} props
14
14
  * @param {ReactNode} props.children - Button content
15
15
  * @param {'primary'|'secondary'|'accent'|'outline'|'ghost'|'nav'|'danger'|'grey'|'control'} props.variant - Visual variant. `danger` is the destructive treatment (--ui-error fill); `nav` is the chrome rung — transparent, oq-64 ink, one step brighter than `ghost`; `control` is an alias for `ghost` (legacy call-sites).
16
- * @param {'sm'|'md'|'lg'} props.size - Button size (default: 'md')
16
+ * @param {'xs'|'sm'|'md'|'lg'} props.size - Button size (default: 'md'); xs is the panel rung (ControlsXsRung, 2026-09-01) — kol-mono-8 in a 22px shell (20 icon-only), opt-in by prop
17
17
  * @param {string} props.iconLeft - Icon name to display on the left
18
18
  * @param {string} props.iconRight - Icon name to display on the right
19
19
  * @param {string} props.iconLeftHover - Icon to show on hover (left position)
@@ -98,6 +98,8 @@ const Button = ({
98
98
  // Add size class — pairs the padding rule with its mono type class.
99
99
  const sizeClass = size === 'sm'
100
100
  ? 'kol-btn-sm kol-mono-12'
101
+ : size === 'xs'
102
+ ? 'kol-btn-xs kol-mono-8'
101
103
  : size === 'lg'
102
104
  ? 'kol-btn-lg kol-mono-16'
103
105
  : 'kol-btn-md kol-mono-14'
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef, useState } from 'react'
1
2
  import { Icon } from '@kolkrabbi/kol-icons'
2
3
  import { toneClass } from '../utilities/tone.js'
3
4
  import { glyphSize } from '../hooks/glyphLadders.js'
@@ -11,6 +12,13 @@ import { glyphSize } from '../hooks/glyphLadders.js'
11
12
  * secondary, always subordinate to filled)
12
13
  * tone="inverse" — the dark chip (`fg-ab-24`) for a washed plane
13
14
  * (ControlToneInverse, 2026-08-27); same prop on ViewToggle · Dropdown
15
+ * size="xs" — the panel rung (ControlsXsRung, 2026-09-01):
16
+ * kol-mono-8 in a 22px shell; opt-in by prop
17
+ * onCommit — `(trimmed) => void` on blur / Enter (Escape
18
+ * restores): the rack commits a module name or a
19
+ * scope expression, not every keystroke. With it
20
+ * the field keeps a local draft seeded from
21
+ * `value`; `onChange` still fires live if given.
14
22
  * variant="ghost" — legacy alias, resolves to outline
15
23
  * variant="property" — the Figma property field (PropertyField,
16
24
  * 2026-08-12): filled chrome, dim `affordance`
@@ -46,12 +54,13 @@ import { glyphSize } from '../hooks/glyphLadders.js'
46
54
  * Input owns prefix/suffix/icon layout + the inner <input> styling.
47
55
  */
48
56
 
49
- const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
57
+ const SIZE_TYPE = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
50
58
 
51
59
  export default function Input({
52
60
  type = 'text',
53
61
  value,
54
62
  onChange,
63
+ onCommit,
55
64
  variant = 'filled',
56
65
  tone = 'default',
57
66
  size = 'md',
@@ -100,6 +109,26 @@ export default function Input({
100
109
  * `${len}ch` hugs the value with no probe element, and stays number-safe
101
110
  * where the HTML `size` attr is ignored (<input type="number">). +2px keeps
102
111
  * the caret from clipping at the end. Tracks `value` → controlled only. */
112
+ /* commit-on-blur/Enter (ControlsXsRung, 2026-09-01 — kol-monitor's panel
113
+ * TextInput collapsing onto this atom): a local draft, seeded from `value`,
114
+ * committed trimmed; Escape restores. Only when `onCommit` is given. */
115
+ const [draft, setDraft] = useState(value ?? '')
116
+ /* the ref mirrors the draft so a blur that lands in the same tick as the
117
+ * last keystroke commits what was typed, not the last RENDER's draft */
118
+ const draftRef = useRef(value ?? '')
119
+ useEffect(() => { if (onCommit) { setDraft(value ?? ''); draftRef.current = value ?? '' } }, [value, onCommit])
120
+ const commitProps = onCommit
121
+ ? {
122
+ value: draft,
123
+ onChange: (e) => { draftRef.current = e.target.value; setDraft(e.target.value); onChange?.(e) },
124
+ onBlur: () => onCommit(String(draftRef.current).trim()),
125
+ onKeyDown: (e) => {
126
+ if (e.key === 'Enter') e.currentTarget.blur()
127
+ if (e.key === 'Escape') { draftRef.current = value ?? ''; setDraft(value ?? ''); e.currentTarget.blur() }
128
+ },
129
+ }
130
+ : null
131
+
103
132
  const propertyLen = Math.max(String(value ?? placeholder ?? '').length, 1)
104
133
 
105
134
  /* Pin inner input height to the typography token's line-height. Without
@@ -108,7 +137,7 @@ export default function Input({
108
137
  * from the font's ascender+descender (font-metric), not strictly from
109
138
  * CSS line-height. Result: kol-control-sm ends up 26.5px instead of 26.
110
139
  * h-4 / h-[18px] / h-[22px] match the kol-mono-12 / -14 / -16 line-heights. */
111
- const heightCls = size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'
140
+ const heightCls = size === 'xs' ? 'h-3' : size === 'sm' ? 'h-4' : size === 'md' ? 'h-[18px]' : 'h-[22px]'
112
141
 
113
142
  const inputCls = [
114
143
  'min-w-0 bg-transparent border-none outline-none text-auto',
@@ -149,9 +178,11 @@ export default function Input({
149
178
  * Controlled + no onChange = deliberate display-only → readOnly. */}
150
179
  <input
151
180
  type={type}
152
- {...(value !== undefined
153
- ? { value: value ?? '', onChange, readOnly: !onChange || undefined }
154
- : { onChange })}
181
+ {...(commitProps
182
+ ? commitProps
183
+ : value !== undefined
184
+ ? { value: value ?? '', onChange, readOnly: !onChange || undefined }
185
+ : { onChange })}
155
186
  placeholder={placeholder}
156
187
  disabled={disabled}
157
188
  spellCheck={false}
@@ -159,6 +190,8 @@ export default function Input({
159
190
  className={inputCls}
160
191
  style={isProperty ? { width: `calc(${propertyLen}ch + 2px)` } : undefined}
161
192
  {...inputProps}
193
+ /* the commit pair LAST, so nothing spread above it can shadow onBlur / onKeyDown */
194
+ {...(commitProps || {})}
162
195
  />
163
196
  {unit !== undefined && (
164
197
  <span aria-hidden="true" className="text-meta shrink-0">{unit}</span>
@@ -24,10 +24,10 @@
24
24
  */
25
25
 
26
26
  /** Icon alone in a pinned square. Pairs with squares 28 · 32 · 36. */
27
- export const SOLO = { sm: 16, md: 20, lg: 24 }
27
+ export const SOLO = { xs: 12, sm: 16, md: 20, lg: 24 }
28
28
 
29
29
  /** Icon beside a label, inside the rung's line box. */
30
- export const ADJACENT = { sm: 14, md: 16, lg: 18 }
30
+ export const ADJACENT = { xs: 10, sm: 14, md: 16, lg: 18 }
31
31
 
32
32
  /**
33
33
  * Indicator glyph — a caret/chevron that DECORATES a control rather than
@@ -37,7 +37,7 @@ export const ADJACENT = { sm: 14, md: 16, lg: 18 }
37
37
  * Dropdown's caret each hand-typed their number (Dropdown took the ADJACENT
38
38
  * rung, the oversize the user called): two transcriptions, the folklore
39
39
  * threshold. */
40
- export const INDICATOR = { sm: 12, md: 14, lg: 16 }
40
+ export const INDICATOR = { xs: 8, sm: 12, md: 14, lg: 16 }
41
41
 
42
42
  /**
43
43
  * Resolve a glyph size. `solo` picks the ladder; `size` indexes it.
@@ -32,7 +32,8 @@ import { indicatorSize } from '../hooks/glyphLadders.js'
32
32
  * (lg ≥1024 / md ≥768 / sm below) is gone.
33
33
  */
34
34
 
35
- const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
35
+ /* xs (ControlsXsRung, 2026-09-01): the panel rung, opt-in by prop — the default stays sm (2026-07-28 law) */
36
+ const SIZE_TYPE = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
36
37
  /* Caret size comes from the INDICATOR ladder (glyphLadders.js) — the private
37
38
  * map that lived here was a transcription of ADJACENT, which is the wrong
38
39
  * ladder for a decoration: it put a caret one rung HEAVIER than the label
@@ -1,8 +1,16 @@
1
1
  /**
2
2
  * Stepper — number input + chevron buttons, built on the .kol-control shell.
3
3
  *
4
- * size="sm" (default) / "md" / "lg" — matched padding + type class.
5
- * Chevron scale follows the size: 8 / 10 / 12 px each, stacked.
4
+ * size="xs" / "sm" (default) / "md" / "lg" — matched padding + type class.
5
+ * Chevron scale follows the size: 6 / 8 / 10 / 12 px each, stacked. xs is the
6
+ * panel rung (ControlsXsRung, kol-monitor 2026-09-01).
7
+ *
8
+ * options — step through a LIST instead of a number range (the rack's
9
+ * ‹ value › Selector, as a variant of this on the ladder; user: "selector
10
+ * could be a variant of ours if we make it follow the size ladder"). `value`
11
+ * is one of the options, the field is read-only, the chevrons step and WRAP
12
+ * at both ends, and `onChange` reports the option in the same event shape
13
+ * as the number path — `{ target: { value: option } }`.
6
14
  *
7
15
  * Loaded from `00-kol/chevron-{up,down}.svg` via the Icon registry — first
8
16
  * stroke icons in the kol curated set. For a plain number input without
@@ -10,8 +18,8 @@
10
18
  */
11
19
  import { Icon } from '@kolkrabbi/kol-icons'
12
20
 
13
- const SIZE_TYPE = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
14
- const CHEVRON_SIZE = { sm: 8, md: 10, lg: 12 }
21
+ const SIZE_TYPE = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }
22
+ const CHEVRON_SIZE = { xs: 6, sm: 8, md: 10, lg: 12 }
15
23
 
16
24
  export default function Stepper({
17
25
  value,
@@ -19,18 +27,27 @@ export default function Stepper({
19
27
  min,
20
28
  max,
21
29
  step = 1,
30
+ options,
22
31
  size = 'sm',
23
32
  className = '',
24
33
  style = {},
25
34
  ...props
26
35
  }) {
36
+ /* the list path: wrap at both ends, report the option */
37
+ const stepList = (dir) => {
38
+ const i = options.indexOf(value)
39
+ const n = i < 0 ? 0 : (i + dir + options.length) % options.length
40
+ onChange?.({ target: { value: options[n] } })
41
+ }
27
42
  const handleIncrement = () => {
43
+ if (options) return stepList(1)
28
44
  const newValue = Number(value) + step
29
45
  if (max !== undefined && newValue > max) return
30
46
  onChange?.({ target: { value: newValue } })
31
47
  }
32
48
 
33
49
  const handleDecrement = () => {
50
+ if (options) return stepList(-1)
34
51
  const newValue = Number(value) - step
35
52
  if (min !== undefined && newValue < min) return
36
53
  onChange?.({ target: { value: newValue } })
@@ -64,12 +81,13 @@ export default function Stepper({
64
81
  return (
65
82
  <div className={shellCls} style={style}>
66
83
  <input
67
- type="number"
84
+ type={options ? 'text' : 'number'}
85
+ readOnly={options ? true : undefined}
68
86
  value={value ?? ''}
69
- onChange={handleInputChange}
70
- min={min}
71
- max={max}
72
- step={step}
87
+ onChange={options ? undefined : handleInputChange}
88
+ min={options ? undefined : min}
89
+ max={options ? undefined : max}
90
+ step={options ? undefined : step}
73
91
  className="w-full min-w-0 bg-transparent border-none outline-none text-auto hide-number-spinners"
74
92
  style={{ paddingRight: `${inputPaddingRight}px` }}
75
93
  {...props}