@kolkrabbi/kol-component 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.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",
@@ -1,9 +1,31 @@
1
1
  import { useEffect, useState } from 'react'
2
2
 
3
+ /**
4
+ * QuantityInput — a compact integer quantity picker with two control layouts.
5
+ *
6
+ * controls="chevron" (default) — value with a stacked up/down chevron pair
7
+ * on the right, inside a full-width field.
8
+ * controls="split" — a − value + pill (minus and plus flank the
9
+ * value). The former `QuantityStepper`.
10
+ *
11
+ * Display-only value (not a text-editable input) — for an editable number
12
+ * field with bump chevrons use `Stepper`. Size auto-adapts to the viewport
13
+ * (sm < 768 ≤ md < 1024 ≤ lg) unless `size` pins it. Controlled: `value` +
14
+ * `(next) => onChange`, clamped to [min, max].
15
+ *
16
+ * @param {number} value current quantity (default 1)
17
+ * @param {Function} onChange (next) => void
18
+ * @param {number} min lower bound (default 1)
19
+ * @param {number} max upper bound (default 99)
20
+ * @param {string} controls 'chevron' (default) | 'split'
21
+ * @param {string} size 'sm' | 'md' | 'lg' — pins the responsive size
22
+ * @param {string} className extra classes on the root
23
+ */
24
+
3
25
  const SIZE_MAP = {
4
26
  sm: { fontSize: 11, paddingY: 12, paddingX: 24, radius: 20, icon: 10 },
5
27
  md: { fontSize: 12, paddingY: 14, paddingX: 24, radius: 22, icon: 12 },
6
- lg: { fontSize: 14, paddingY: 16, paddingX: 24, radius: 24, icon: 14 }
28
+ lg: { fontSize: 14, paddingY: 16, paddingX: 24, radius: 24, icon: 14 },
7
29
  }
8
30
 
9
31
  const QuantityInput = ({
@@ -11,33 +33,21 @@ const QuantityInput = ({
11
33
  onChange,
12
34
  min = 1,
13
35
  max = 99,
36
+ controls = 'chevron',
14
37
  size,
15
- className = ''
38
+ className = '',
16
39
  }) => {
17
40
  const [resolvedSize, setResolvedSize] = useState('md')
18
41
  const [componentWidth, setComponentWidth] = useState('180px')
19
42
 
20
43
  useEffect(() => {
21
44
  const determineSize = () => {
22
- if (size) {
23
- setResolvedSize(size)
24
- return
25
- }
26
-
27
- if (typeof window === 'undefined') {
28
- setResolvedSize('md')
29
- return
30
- }
31
-
32
- if (window.innerWidth >= 1024) {
33
- setResolvedSize('lg')
34
- } else if (window.innerWidth >= 768) {
35
- setResolvedSize('md')
36
- } else {
37
- setResolvedSize('sm')
38
- }
45
+ if (size) { setResolvedSize(size); return }
46
+ if (typeof window === 'undefined') { setResolvedSize('md'); return }
47
+ if (window.innerWidth >= 1024) setResolvedSize('lg')
48
+ else if (window.innerWidth >= 768) setResolvedSize('md')
49
+ else setResolvedSize('sm')
39
50
  }
40
-
41
51
  determineSize()
42
52
  window.addEventListener('resize', determineSize)
43
53
  return () => window.removeEventListener('resize', determineSize)
@@ -46,14 +56,9 @@ const QuantityInput = ({
46
56
  useEffect(() => {
47
57
  const updateWidth = () => {
48
58
  if (typeof window === 'undefined') return
49
-
50
- if (window.innerWidth >= 1024) {
51
- setComponentWidth('180px')
52
- } else if (window.innerWidth >= 768) {
53
- setComponentWidth('140px')
54
- } else {
55
- setComponentWidth('100px')
56
- }
59
+ if (window.innerWidth >= 1024) setComponentWidth('180px')
60
+ else if (window.innerWidth >= 768) setComponentWidth('140px')
61
+ else setComponentWidth('100px')
57
62
  }
58
63
  updateWidth()
59
64
  window.addEventListener('resize', updateWidth)
@@ -61,108 +66,65 @@ const QuantityInput = ({
61
66
  }, [])
62
67
 
63
68
  const metrics = SIZE_MAP[resolvedSize] || SIZE_MAP.md
69
+ const increment = () => { if (value < max) onChange?.(value + 1) }
70
+ const decrement = () => { if (value > min) onChange?.(value - 1) }
64
71
 
65
- const increment = () => {
66
- if (value < max) onChange?.(value + 1)
67
- }
68
-
69
- const decrement = () => {
70
- if (value > min) onChange?.(value - 1)
72
+ /* controls="split" — the − value + pill (formerly QuantityStepper). */
73
+ if (controls === 'split') {
74
+ const btn = (disabled) => ({
75
+ padding: `${metrics.paddingY}px 12px`,
76
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
77
+ backgroundColor: 'transparent', border: 'none',
78
+ color: 'var(--kol-surface-on-primary)',
79
+ cursor: disabled ? 'not-allowed' : 'pointer',
80
+ opacity: disabled ? 0.3 : 1,
81
+ transition: 'opacity 0.15s',
82
+ fontFamily: 'var(--kol-font-family-mono)',
83
+ fontSize: `${metrics.fontSize}px`, lineHeight: '120%',
84
+ })
85
+ return (
86
+ <div
87
+ className={`flex items-center justify-center ${className}`}
88
+ style={{
89
+ width: componentWidth, minWidth: componentWidth,
90
+ border: '1px solid var(--kol-border-default)',
91
+ borderRadius: `${metrics.radius}px`,
92
+ backgroundColor: 'var(--kol-surface-primary)',
93
+ }}
94
+ >
95
+ <button type="button" onClick={decrement} disabled={value <= min} style={btn(value <= min)} aria-label="Decrease quantity">−</button>
96
+ <span style={{ minWidth: '24px', textAlign: 'center', color: 'var(--kol-surface-on-primary)', fontFamily: 'var(--kol-font-family-mono)', fontSize: `${metrics.fontSize}px`, lineHeight: '120%' }}>{value}</span>
97
+ <button type="button" onClick={increment} disabled={value >= max} style={btn(value >= max)} aria-label="Increase quantity">+</button>
98
+ </div>
99
+ )
71
100
  }
72
101
 
102
+ /* controls="chevron" (default) — value + stacked chevron pair on the right. */
73
103
  return (
74
- <div
75
- className={`relative block ${className}`}
76
- >
77
- {/* Border wrapper - EXPLICIT 42.5px height */}
104
+ <div className={`relative block ${className}`}>
78
105
  <div
79
106
  className="w-full flex items-center"
80
107
  style={{
81
- position: 'relative',
82
- height: '42.5px',
108
+ position: 'relative', height: '42.5px',
83
109
  border: '1px solid var(--kol-border-default)',
84
110
  borderRadius: `${metrics.radius}px`,
85
111
  backgroundColor: 'var(--kol-surface-primary)',
86
112
  color: 'var(--kol-surface-on-primary)',
87
- paddingLeft: `${metrics.paddingX}px`,
88
- paddingRight: `${metrics.paddingX}px`,
89
- fontSize: `${metrics.fontSize}px`,
90
- lineHeight: '120%',
91
- fontFamily: 'var(--kol-font-family-mono)',
92
- boxSizing: 'border-box'
113
+ paddingLeft: `${metrics.paddingX}px`, paddingRight: `${metrics.paddingX}px`,
114
+ fontSize: `${metrics.fontSize}px`, lineHeight: '120%',
115
+ fontFamily: 'var(--kol-font-family-mono)', boxSizing: 'border-box',
93
116
  }}
94
117
  >
95
118
  <span>{value}</span>
96
-
97
- {/* Chevrons - absolute, OUTSIDE the padded content */}
98
- <div
99
- style={{
100
- position: 'absolute',
101
- right: `${metrics.paddingX}px`,
102
- top: '50%',
103
- transform: 'translateY(-50%)',
104
- display: 'flex',
105
- flexDirection: 'column'
106
- }}
107
- >
108
- <button
109
- type="button"
110
- onClick={increment}
111
- disabled={value >= max}
112
- style={{
113
- backgroundColor: 'transparent',
114
- border: 'none',
115
- padding: '0',
116
- cursor: value >= max ? 'not-allowed' : 'pointer',
117
- opacity: value >= max ? 0.3 : 1,
118
- lineHeight: 0,
119
- display: 'block'
120
- }}
121
- aria-label="Increase quantity"
122
- >
123
- <svg
124
- width={metrics.icon}
125
- height={metrics.icon}
126
- viewBox="0 0 12 12"
127
- fill="none"
128
- >
129
- <path
130
- d="m3 7 3-3 3 3"
131
- stroke="currentColor"
132
- strokeWidth="1.25"
133
- strokeLinecap="round"
134
- strokeLinejoin="round"
135
- />
119
+ <div style={{ position: 'absolute', right: `${metrics.paddingX}px`, top: '50%', transform: 'translateY(-50%)', display: 'flex', flexDirection: 'column' }}>
120
+ <button type="button" onClick={increment} disabled={value >= max} style={{ backgroundColor: 'transparent', border: 'none', padding: '0', cursor: value >= max ? 'not-allowed' : 'pointer', opacity: value >= max ? 0.3 : 1, lineHeight: 0, display: 'block' }} aria-label="Increase quantity">
121
+ <svg width={metrics.icon} height={metrics.icon} viewBox="0 0 12 12" fill="none">
122
+ <path d="m3 7 3-3 3 3" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
136
123
  </svg>
137
124
  </button>
138
- <button
139
- type="button"
140
- onClick={decrement}
141
- disabled={value <= min}
142
- style={{
143
- backgroundColor: 'transparent',
144
- border: 'none',
145
- padding: '0',
146
- cursor: value <= min ? 'not-allowed' : 'pointer',
147
- opacity: value <= min ? 0.3 : 1,
148
- lineHeight: 0,
149
- display: 'block'
150
- }}
151
- aria-label="Decrease quantity"
152
- >
153
- <svg
154
- width={metrics.icon}
155
- height={metrics.icon}
156
- viewBox="0 0 12 12"
157
- fill="none"
158
- >
159
- <path
160
- d="m3 5 3 3 3-3"
161
- stroke="currentColor"
162
- strokeWidth="1.25"
163
- strokeLinecap="round"
164
- strokeLinejoin="round"
165
- />
125
+ <button type="button" onClick={decrement} disabled={value <= min} style={{ backgroundColor: 'transparent', border: 'none', padding: '0', cursor: value <= min ? 'not-allowed' : 'pointer', opacity: value <= min ? 0.3 : 1, lineHeight: 0, display: 'block' }} aria-label="Decrease quantity">
126
+ <svg width={metrics.icon} height={metrics.icon} viewBox="0 0 12 12" fill="none">
127
+ <path d="m3 5 3 3 3-3" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
166
128
  </svg>
167
129
  </button>
168
130
  </div>
package/src/index.js CHANGED
@@ -41,7 +41,6 @@ export { usePopover, PopoverPanel, Tooltip } from './atoms/Popover.jsx'
41
41
  export { default as PriceDisplay } from './atoms/PriceDisplay.jsx'
42
42
  export { default as ProsePreview } from './atoms/ProsePreview.jsx'
43
43
  export { default as QuantityInput } from './atoms/QuantityInput.jsx'
44
- export { default as QuantityStepper } from './atoms/QuantityStepper.jsx'
45
44
  export { default as RotaryDial } from './atoms/RotaryDial.jsx'
46
45
  export { default as SearchInput } from './atoms/SearchInput.jsx'
47
46
  export { default as Section } from './atoms/Section.jsx'
@@ -10,34 +10,11 @@ import { MenuItem as MenuTrigger } from './MenuItem.jsx'
10
10
  * fixed positioning, MenuItem on floating-ui (portal, auto-flip, focus
11
11
  * management). One implementation now: MenuItem. This alias keeps existing
12
12
  * call-sites working; migrate imports to MenuItem. Removal in the next major.
13
+ *
14
+ * Compose the rows with the exported `MenuDropdownItem` / `MenuDropdownDivider`
15
+ * / `MenuDropdownNest` from MenuItem.jsx — this file no longer ships its own
16
+ * duplicate row/divider components (they were dead: never barrel-exported).
13
17
  */
14
18
  export function MenuPopover(props) {
15
19
  return <MenuTrigger {...props} />
16
20
  }
17
-
18
- /**
19
- * MenuItem — action row inside a MenuPopover. Renders as a button so it
20
- * picks up disabled state, focus, and keyboard activation. The popover
21
- * closes automatically when an item is clicked (via the wrapper's
22
- * delegate click — the data-menu-item attr marks rows for that match).
23
- */
24
- export function MenuItem({ onClick, disabled, shortcut, iconLeft, children }) {
25
- return (
26
- <button
27
- type="button"
28
- data-menu-item
29
- onClick={onClick}
30
- disabled={disabled}
31
- role="menuitem"
32
- className="w-full kol-helper-12 px-3 h-8 inline-flex items-center gap-2 text-meta hover:text-emphasis hover:bg-fg-08 disabled:opacity-40 disabled:cursor-not-allowed text-left"
33
- >
34
- {iconLeft && <span className="shrink-0 w-4 inline-flex items-center justify-center text-meta">{iconLeft}</span>}
35
- <span className="flex-1">{children}</span>
36
- {shortcut && <span className="kol-helper-10 text-subtle shrink-0">{shortcut}</span>}
37
- </button>
38
- )
39
- }
40
-
41
- export function MenuDivider() {
42
- return <div className="border-t border-fg-08 my-1" />
43
- }
@@ -1,149 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- // Match Dropdown SIZE_MAP for consistent height
4
- const SIZE_MAP = {
5
- sm: { fontSize: 11, paddingY: 12, paddingX: 24, radius: 20 },
6
- md: { fontSize: 12, paddingY: 14, paddingX: 24, radius: 22 },
7
- lg: { fontSize: 14, paddingY: 16, paddingX: 24, radius: 24 }
8
- }
9
-
10
- const QuantityStepper = ({
11
- value = 1,
12
- onChange,
13
- min = 1,
14
- max = 10,
15
- size,
16
- className = ''
17
- }) => {
18
- const [resolvedSize, setResolvedSize] = useState('md')
19
- const [componentWidth, setComponentWidth] = useState('180px')
20
-
21
- // Responsive size
22
- useEffect(() => {
23
- const determineSize = () => {
24
- if (size) {
25
- setResolvedSize(size)
26
- return
27
- }
28
-
29
- if (typeof window === 'undefined') {
30
- setResolvedSize('md')
31
- return
32
- }
33
-
34
- if (window.innerWidth >= 1024) {
35
- setResolvedSize('lg')
36
- } else if (window.innerWidth >= 768) {
37
- setResolvedSize('md')
38
- } else {
39
- setResolvedSize('sm')
40
- }
41
- }
42
-
43
- determineSize()
44
- window.addEventListener('resize', determineSize)
45
- return () => window.removeEventListener('resize', determineSize)
46
- }, [size])
47
-
48
- // Width management - match Dropdown
49
- useEffect(() => {
50
- const updateWidth = () => {
51
- if (typeof window === 'undefined') return
52
-
53
- if (window.innerWidth >= 1024) {
54
- setComponentWidth('180px')
55
- } else if (window.innerWidth >= 768) {
56
- setComponentWidth('140px')
57
- } else {
58
- setComponentWidth('100px')
59
- }
60
- }
61
- updateWidth()
62
- window.addEventListener('resize', updateWidth)
63
- return () => window.removeEventListener('resize', updateWidth)
64
- }, [])
65
-
66
- const metrics = SIZE_MAP[resolvedSize] || SIZE_MAP.md
67
-
68
- const decrement = () => {
69
- if (value > min) onChange?.(value - 1)
70
- }
71
-
72
- const increment = () => {
73
- if (value < max) onChange?.(value + 1)
74
- }
75
-
76
- const buttonStyle = {
77
- padding: `${metrics.paddingY}px 12px`,
78
- display: 'flex',
79
- alignItems: 'center',
80
- justifyContent: 'center',
81
- backgroundColor: 'transparent',
82
- border: 'none',
83
- color: 'var(--kol-surface-on-primary)',
84
- cursor: 'pointer',
85
- transition: 'opacity 0.15s',
86
- fontFamily: 'var(--kol-font-family-mono)',
87
- fontSize: `${metrics.fontSize}px`,
88
- lineHeight: '120%'
89
- }
90
-
91
- const disabledStyle = {
92
- opacity: 0.3,
93
- cursor: 'not-allowed'
94
- }
95
-
96
- return (
97
- <div
98
- className={`flex items-center justify-center ${className}`}
99
- style={{
100
- width: componentWidth,
101
- minWidth: componentWidth,
102
- border: '1px solid var(--kol-border-default)',
103
- borderRadius: `${metrics.radius}px`,
104
- backgroundColor: 'var(--kol-surface-primary)'
105
- }}
106
- >
107
- <button
108
- type="button"
109
- onClick={decrement}
110
- disabled={value <= min}
111
- style={{
112
- ...buttonStyle,
113
- ...(value <= min ? disabledStyle : {})
114
- }}
115
- aria-label="Decrease quantity"
116
- >
117
-
118
- </button>
119
-
120
- <span
121
- style={{
122
- minWidth: '24px',
123
- textAlign: 'center',
124
- color: 'var(--kol-surface-on-primary)',
125
- fontFamily: 'var(--kol-font-family-mono)',
126
- fontSize: `${metrics.fontSize}px`,
127
- lineHeight: '120%'
128
- }}
129
- >
130
- {value}
131
- </span>
132
-
133
- <button
134
- type="button"
135
- onClick={increment}
136
- disabled={value >= max}
137
- style={{
138
- ...buttonStyle,
139
- ...(value >= max ? disabledStyle : {})
140
- }}
141
- aria-label="Increase quantity"
142
- >
143
- +
144
- </button>
145
- </div>
146
- )
147
- }
148
-
149
- export default QuantityStepper