@kolkrabbi/kol-component 0.35.0 → 0.36.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 +1 -1
- package/src/atoms/Input.jsx +28 -2
- package/src/atoms/SegmentedToggle.jsx +18 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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",
|
package/src/atoms/Input.jsx
CHANGED
|
@@ -9,6 +9,14 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
9
9
|
* treatment (2026-07-08 chrome law: one
|
|
10
10
|
* secondary, always subordinate to filled)
|
|
11
11
|
* variant="ghost" — legacy alias, resolves to outline
|
|
12
|
+
* variant="property" — the Figma property field (PropertyField,
|
|
13
|
+
* 2026-08-12): filled chrome, dim `affordance`
|
|
14
|
+
* (letter or icon node) at a 6px gap, the value
|
|
15
|
+
* HUGS its own length (mono ch-width — number-
|
|
16
|
+
* safe, no `size` attr), and `unit` renders
|
|
17
|
+
* IMMEDIATELY after the value (`0°`, `100%`).
|
|
18
|
+
* Shell fills its cell, content left-packed.
|
|
19
|
+
* Width tracks `value` — controlled usage only.
|
|
12
20
|
*
|
|
13
21
|
* size="sm" / "md" (default) / "lg" — matched padding + type class
|
|
14
22
|
*
|
|
@@ -47,6 +55,8 @@ export default function Input({
|
|
|
47
55
|
prefix,
|
|
48
56
|
suffix,
|
|
49
57
|
slotLeft,
|
|
58
|
+
affordance,
|
|
59
|
+
unit,
|
|
50
60
|
iconLeft,
|
|
51
61
|
iconSize = null,
|
|
52
62
|
placeholder,
|
|
@@ -66,7 +76,9 @@ export default function Input({
|
|
|
66
76
|
const resolvedIconSize = iconSize ?? glyphSize(size)
|
|
67
77
|
|
|
68
78
|
// ghost folds into outline (2026-07-08 chrome law): one secondary treatment.
|
|
69
|
-
|
|
79
|
+
// property rides the filled chrome — it is a behaviour variant, not new paint.
|
|
80
|
+
const isProperty = variant === 'property'
|
|
81
|
+
const resolvedVariant = variant === 'ghost' ? 'outline' : isProperty ? 'filled' : variant
|
|
70
82
|
|
|
71
83
|
const shellCls = [
|
|
72
84
|
'kol-control',
|
|
@@ -74,9 +86,16 @@ export default function Input({
|
|
|
74
86
|
`kol-control-${size}`,
|
|
75
87
|
SIZE_TYPE[size],
|
|
76
88
|
'cursor-text',
|
|
89
|
+
isProperty && 'w-full',
|
|
77
90
|
className,
|
|
78
91
|
].filter(Boolean).join(' ')
|
|
79
92
|
|
|
93
|
+
/* Property width: the shell type is mono, so every glyph is exactly 1ch —
|
|
94
|
+
* `${len}ch` hugs the value with no probe element, and stays number-safe
|
|
95
|
+
* where the HTML `size` attr is ignored (<input type="number">). +2px keeps
|
|
96
|
+
* the caret from clipping at the end. Tracks `value` → controlled only. */
|
|
97
|
+
const propertyLen = Math.max(String(value ?? placeholder ?? '').length, 1)
|
|
98
|
+
|
|
80
99
|
/* Pin inner input height to the typography token's line-height. Without
|
|
81
100
|
* this the `<input>` renders ~0.5px taller than the equivalent <button>
|
|
82
101
|
* or <label> at the same kol-mono-N — Chromium computes input height
|
|
@@ -88,7 +107,7 @@ export default function Input({
|
|
|
88
107
|
const inputCls = [
|
|
89
108
|
'min-w-0 bg-transparent border-none outline-none text-auto',
|
|
90
109
|
heightCls,
|
|
91
|
-
!fixedChars && 'flex-1',
|
|
110
|
+
!fixedChars && !isProperty && 'flex-1',
|
|
92
111
|
/* Balance the dim prefix/suffix visual weight with extra inner padding
|
|
93
112
|
* on the opposite side. Without this the bright value sits closer to
|
|
94
113
|
* the affordance than to the empty edge, reads off-balance. */
|
|
@@ -115,6 +134,9 @@ export default function Input({
|
|
|
115
134
|
{prefix !== undefined && (
|
|
116
135
|
<span aria-hidden="true" className="text-meta pr-1 shrink-0">{prefix}</span>
|
|
117
136
|
)}
|
|
137
|
+
{affordance !== undefined && (
|
|
138
|
+
<span aria-hidden="true" className="text-meta pr-1.5 shrink-0 inline-flex items-center">{affordance}</span>
|
|
139
|
+
)}
|
|
118
140
|
{/* Controlled only when a `value` prop is passed — otherwise stay
|
|
119
141
|
* uncontrolled so prop-less usages (search stubs, quick demos) type
|
|
120
142
|
* normally instead of freezing on a value-without-onChange input.
|
|
@@ -129,8 +151,12 @@ export default function Input({
|
|
|
129
151
|
spellCheck={false}
|
|
130
152
|
size={fixedChars ? chars : undefined}
|
|
131
153
|
className={inputCls}
|
|
154
|
+
style={isProperty ? { width: `calc(${propertyLen}ch + 2px)` } : undefined}
|
|
132
155
|
{...inputProps}
|
|
133
156
|
/>
|
|
157
|
+
{unit !== undefined && (
|
|
158
|
+
<span aria-hidden="true" className="text-meta shrink-0">{unit}</span>
|
|
159
|
+
)}
|
|
134
160
|
{suffix !== undefined && (
|
|
135
161
|
<span aria-hidden="true" className="text-meta pl-1 shrink-0">{suffix}</span>
|
|
136
162
|
)}
|
|
@@ -23,9 +23,17 @@
|
|
|
23
23
|
* the group on the active cell, ←/→ (or ↑/↓) move selection + focus.
|
|
24
24
|
*
|
|
25
25
|
* Props:
|
|
26
|
-
* value — current option value
|
|
26
|
+
* value — current option value. `null`/`undefined` = STATELESS mode
|
|
27
|
+
* (the segmented state law, 2026-08-12): role `group`, no
|
|
28
|
+
* aria-checked, no selected styling — a pure one-shot ACTION
|
|
29
|
+
* strip (canvas alignment, transform cluster); onChange is the
|
|
30
|
+
* action dispatch.
|
|
27
31
|
* onChange — handler (newValue) => void
|
|
28
32
|
* options — [{ value, label, ariaLabel? }]
|
|
33
|
+
* variant — 'default' (shipped chrome: shared outer stroke + dividers,
|
|
34
|
+
* filled active cell) | 'filled' (the state law's tiles: every
|
|
35
|
+
* cell a surface-secondary tile with 1px transparent gaps, NO
|
|
36
|
+
* outline shell; the inset ring marks ONLY the selected cell).
|
|
29
37
|
* size — mirrors Button exactly: 'sm' (26px, mono-12, 4/12 pad) |
|
|
30
38
|
* 'md' (default, 32px, mono-14, 6/16 pad) | 'lg' (40px,
|
|
31
39
|
* mono-16, 8/20 pad). Same cell padding + mono type as the
|
|
@@ -34,11 +42,13 @@
|
|
|
34
42
|
* ariaLabel — accessible name for the group
|
|
35
43
|
* className — additional classes on the outer shell
|
|
36
44
|
*/
|
|
37
|
-
export default function SegmentedToggle({ value, onChange, options = [], size = 'md', ariaLabel, className = '' }) {
|
|
45
|
+
export default function SegmentedToggle({ value, onChange, options = [], variant = 'default', size = 'md', ariaLabel, className = '' }) {
|
|
38
46
|
const cellType = { sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }[size]
|
|
47
|
+
const stateless = value == null
|
|
39
48
|
const focusIdx = Math.max(0, options.findIndex((opt) => opt.value === value))
|
|
40
49
|
|
|
41
50
|
const handleKeyDown = (e) => {
|
|
51
|
+
if (stateless) return // plain button row — Tab moves focus, arrows do nothing
|
|
42
52
|
const dir = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1 }[e.key]
|
|
43
53
|
if (!dir || !options.length) return
|
|
44
54
|
e.preventDefault()
|
|
@@ -49,21 +59,21 @@ export default function SegmentedToggle({ value, onChange, options = [], size =
|
|
|
49
59
|
|
|
50
60
|
return (
|
|
51
61
|
<div
|
|
52
|
-
role=
|
|
62
|
+
role={stateless ? 'group' : 'radiogroup'}
|
|
53
63
|
aria-label={ariaLabel}
|
|
54
64
|
onKeyDown={handleKeyDown}
|
|
55
|
-
className={['kol-seg', size !== 'md' && `kol-seg--${size}`, className].filter(Boolean).join(' ')}
|
|
65
|
+
className={['kol-seg', variant === 'filled' && 'kol-seg--filled', size !== 'md' && `kol-seg--${size}`, className].filter(Boolean).join(' ')}
|
|
56
66
|
>
|
|
57
67
|
{options.map((opt, i) => {
|
|
58
|
-
const isActive = opt.value === value
|
|
68
|
+
const isActive = !stateless && opt.value === value
|
|
59
69
|
return (
|
|
60
70
|
<button
|
|
61
71
|
key={opt.value}
|
|
62
72
|
type="button"
|
|
63
|
-
role=
|
|
64
|
-
aria-checked={isActive}
|
|
73
|
+
role={stateless ? undefined : 'radio'}
|
|
74
|
+
aria-checked={stateless ? undefined : isActive}
|
|
65
75
|
aria-label={opt.ariaLabel}
|
|
66
|
-
tabIndex={i === focusIdx ? 0 : -1}
|
|
76
|
+
tabIndex={stateless ? 0 : (i === focusIdx ? 0 : -1)}
|
|
67
77
|
onClick={() => onChange?.(opt.value)}
|
|
68
78
|
className={['kol-seg-cell', cellType, isActive && 'is-active'].filter(Boolean).join(' ')}
|
|
69
79
|
>
|