@tonyarbor/components 0.1.0 → 0.2.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.
@@ -0,0 +1,283 @@
1
+ // src/NumericInput/NumericInput.tsx
2
+ import * as React from "react";
3
+ import { clsx } from "clsx";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ var labelStyles = {
6
+ display: "block",
7
+ fontSize: "13px",
8
+ fontWeight: "600",
9
+ color: "#2f2f2f",
10
+ marginBottom: "4px",
11
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
12
+ };
13
+ var helperTextStyles = {
14
+ fontSize: "13px",
15
+ margin: "0",
16
+ marginTop: "2px",
17
+ color: "#595959",
18
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
19
+ lineHeight: "1.4"
20
+ };
21
+ var errorTextStyles = {
22
+ ...helperTextStyles,
23
+ color: "#a62323",
24
+ // destructive-600
25
+ display: "flex",
26
+ alignItems: "center",
27
+ gap: "4px"
28
+ };
29
+ var containerStyles = {
30
+ base: {
31
+ display: "flex",
32
+ alignItems: "center",
33
+ height: "46px",
34
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
35
+ backgroundColor: "#ffffff",
36
+ borderRadius: "8px",
37
+ transition: "all 0.2s ease-in-out",
38
+ overflow: "hidden"
39
+ },
40
+ states: {
41
+ default: {
42
+ border: "1px solid #d1d1d1"
43
+ // grey-300
44
+ },
45
+ defaultFocus: {
46
+ borderColor: "#3cad51",
47
+ // brand-500
48
+ outline: "3px solid rgba(60, 173, 81, 0.2)"
49
+ },
50
+ error: {
51
+ border: "1px solid #c93232"
52
+ // destructive-500
53
+ },
54
+ errorFocus: {
55
+ borderColor: "#c93232",
56
+ outline: "3px solid rgba(201, 50, 50, 0.2)"
57
+ },
58
+ success: {
59
+ border: "1px solid #16a33d"
60
+ // success-500
61
+ },
62
+ successFocus: {
63
+ borderColor: "#16a33d",
64
+ outline: "3px solid rgba(22, 163, 61, 0.2)"
65
+ },
66
+ disabled: {
67
+ backgroundColor: "#f8f8f8",
68
+ // grey-050
69
+ borderColor: "#efefef",
70
+ // grey-100
71
+ cursor: "not-allowed"
72
+ }
73
+ }
74
+ };
75
+ var buttonStyles = {
76
+ width: "46px",
77
+ height: "100%",
78
+ border: "none",
79
+ background: "transparent",
80
+ cursor: "pointer",
81
+ fontSize: "16px",
82
+ color: "#2f2f2f",
83
+ display: "flex",
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ transition: "background-color 0.2s",
87
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
88
+ userSelect: "none"
89
+ };
90
+ var inputStyles = {
91
+ flex: 1,
92
+ border: "none",
93
+ outline: "none",
94
+ background: "transparent",
95
+ textAlign: "center",
96
+ fontSize: "13px",
97
+ color: "#2f2f2f",
98
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
99
+ padding: "0 8px",
100
+ appearance: "textfield"
101
+ };
102
+ var NumericInput = React.forwardRef(
103
+ ({
104
+ label,
105
+ value,
106
+ onChange,
107
+ min,
108
+ max,
109
+ step = 1,
110
+ state = "default",
111
+ error,
112
+ helperText,
113
+ disabled = false,
114
+ className,
115
+ style,
116
+ "data-testid": dataTestId
117
+ }, ref) => {
118
+ const [isFocused, setIsFocused] = React.useState(false);
119
+ const inputRef = React.useRef(null);
120
+ const inputId = React.useId();
121
+ const helperTextId = React.useId();
122
+ const errorId = React.useId();
123
+ const clampValue = (val) => {
124
+ let clamped = val;
125
+ if (min !== void 0 && clamped < min) clamped = min;
126
+ if (max !== void 0 && clamped > max) clamped = max;
127
+ return clamped;
128
+ };
129
+ const handleIncrement = () => {
130
+ if (disabled) return;
131
+ const currentValue = value ?? 0;
132
+ const newValue = currentValue + step;
133
+ const clampedValue = clampValue(newValue);
134
+ if (clampedValue !== currentValue) {
135
+ onChange?.(clampedValue);
136
+ }
137
+ };
138
+ const handleDecrement = () => {
139
+ if (disabled) return;
140
+ const currentValue = value ?? 0;
141
+ const newValue = currentValue - step;
142
+ const clampedValue = clampValue(newValue);
143
+ if (clampedValue !== currentValue) {
144
+ onChange?.(clampedValue);
145
+ }
146
+ };
147
+ const handleInputChange = (e) => {
148
+ const inputValue = e.target.value;
149
+ if (inputValue === "" || inputValue === "-") {
150
+ onChange?.(void 0);
151
+ return;
152
+ }
153
+ const numValue = parseFloat(inputValue);
154
+ if (!isNaN(numValue)) {
155
+ const clampedValue = clampValue(numValue);
156
+ onChange?.(clampedValue);
157
+ }
158
+ };
159
+ const handleKeyDown = (e) => {
160
+ const allowedKeys = ["Backspace", "Delete", "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "-", "."];
161
+ if (allowedKeys.includes(e.key)) {
162
+ if (e.key === "ArrowUp") {
163
+ e.preventDefault();
164
+ handleIncrement();
165
+ } else if (e.key === "ArrowDown") {
166
+ e.preventDefault();
167
+ handleDecrement();
168
+ }
169
+ return;
170
+ }
171
+ if (!/^[0-9]$/.test(e.key)) {
172
+ e.preventDefault();
173
+ }
174
+ };
175
+ const currentState = error ? "error" : state;
176
+ const stateStyles = containerStyles.states[currentState];
177
+ const focusStyles = currentState === "error" ? containerStyles.states.errorFocus : currentState === "success" ? containerStyles.states.successFocus : containerStyles.states.defaultFocus;
178
+ const containerStyle = {
179
+ ...containerStyles.base,
180
+ ...stateStyles,
181
+ ...isFocused && !disabled && focusStyles,
182
+ ...disabled && containerStyles.states.disabled
183
+ };
184
+ const isDecrementDisabled = disabled || min !== void 0 && value !== void 0 && value <= min;
185
+ const isIncrementDisabled = disabled || max !== void 0 && value !== void 0 && value >= max;
186
+ const getButtonStyle = (isDisabled) => {
187
+ const baseStyle = {
188
+ ...buttonStyles,
189
+ cursor: isDisabled ? "not-allowed" : "pointer",
190
+ color: isDisabled ? "#7e7e7e" : "#2f2f2f"
191
+ };
192
+ if (currentState === "success" && !isDisabled) {
193
+ return {
194
+ ...baseStyle,
195
+ color: "#16a33d",
196
+ // success-500
197
+ border: "2px solid #16a33d",
198
+ borderRadius: "50%",
199
+ width: "32px",
200
+ height: "32px"
201
+ };
202
+ }
203
+ return baseStyle;
204
+ };
205
+ return /* @__PURE__ */ jsxs("div", { className: clsx("arbor-numeric-input-wrapper", className), style, ref, "data-testid": dataTestId, children: [
206
+ label && /* @__PURE__ */ jsx("label", { htmlFor: inputId, style: labelStyles, children: label }),
207
+ /* @__PURE__ */ jsxs("div", { style: containerStyle, children: [
208
+ /* @__PURE__ */ jsx(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ onClick: handleDecrement,
213
+ disabled: isDecrementDisabled,
214
+ style: getButtonStyle(isDecrementDisabled),
215
+ onMouseEnter: (e) => {
216
+ if (!isDecrementDisabled) {
217
+ e.currentTarget.style.backgroundColor = "#f8f8f8";
218
+ e.currentTarget.style.borderRadius = "99px";
219
+ }
220
+ },
221
+ onMouseLeave: (e) => {
222
+ e.currentTarget.style.backgroundColor = "transparent";
223
+ e.currentTarget.style.borderRadius = currentState === "success" ? "50%" : "0";
224
+ },
225
+ "aria-label": "Decrement",
226
+ children: "\u2212"
227
+ }
228
+ ),
229
+ /* @__PURE__ */ jsx(
230
+ "input",
231
+ {
232
+ ref: inputRef,
233
+ id: inputId,
234
+ type: "text",
235
+ inputMode: "numeric",
236
+ value: value !== void 0 ? value : "",
237
+ onChange: handleInputChange,
238
+ onKeyDown: handleKeyDown,
239
+ onFocus: () => setIsFocused(true),
240
+ onBlur: () => setIsFocused(false),
241
+ disabled,
242
+ "aria-invalid": error ? "true" : "false",
243
+ "aria-describedby": error ? errorId : helperText ? helperTextId : void 0,
244
+ style: {
245
+ ...inputStyles,
246
+ color: disabled ? "#7e7e7e" : "#2f2f2f",
247
+ cursor: disabled ? "not-allowed" : "text"
248
+ }
249
+ }
250
+ ),
251
+ /* @__PURE__ */ jsx(
252
+ "button",
253
+ {
254
+ type: "button",
255
+ onClick: handleIncrement,
256
+ disabled: isIncrementDisabled,
257
+ style: getButtonStyle(isIncrementDisabled),
258
+ onMouseEnter: (e) => {
259
+ if (!isIncrementDisabled) {
260
+ e.currentTarget.style.backgroundColor = "#f8f8f8";
261
+ e.currentTarget.style.borderRadius = "99px";
262
+ }
263
+ },
264
+ onMouseLeave: (e) => {
265
+ e.currentTarget.style.backgroundColor = "transparent";
266
+ e.currentTarget.style.borderRadius = currentState === "success" ? "50%" : "0";
267
+ },
268
+ "aria-label": "Increment",
269
+ children: "+"
270
+ }
271
+ )
272
+ ] }),
273
+ error && /* @__PURE__ */ jsx("p", { id: errorId, style: errorTextStyles, children: error }),
274
+ helperText && !error && /* @__PURE__ */ jsx("p", { id: helperTextId, style: helperTextStyles, children: helperText })
275
+ ] });
276
+ }
277
+ );
278
+ NumericInput.displayName = "NumericInput";
279
+
280
+ export {
281
+ NumericInput
282
+ };
283
+ //# sourceMappingURL=chunk-5BUXFTPW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/NumericInput/NumericInput.tsx"],"sourcesContent":["import * as React from 'react';\nimport { clsx } from 'clsx';\n\nexport type NumericInputState = 'default' | 'error' | 'success';\n\nexport interface NumericInputProps {\n /**\n * The label for the numeric input\n */\n label?: string;\n /**\n * The current value\n */\n value?: number;\n /**\n * Callback when value changes\n */\n onChange?: (value: number | undefined) => void;\n /**\n * Minimum allowed value\n */\n min?: number;\n /**\n * Maximum allowed value\n */\n max?: number;\n /**\n * Step increment/decrement value\n * @default 1\n */\n step?: number;\n /**\n * The validation state\n * @default 'default'\n */\n state?: NumericInputState;\n /**\n * Optional error message\n */\n error?: string;\n /**\n * Optional helper text\n */\n helperText?: string;\n /**\n * Whether the input is disabled\n */\n disabled?: boolean;\n /**\n * Custom className\n */\n className?: string;\n /**\n * Custom style\n */\n style?: React.CSSProperties;\n /**\n * Test ID for testing\n */\n 'data-testid'?: string;\n}\n\nconst labelStyles: React.CSSProperties = {\n display: 'block',\n fontSize: '13px',\n fontWeight: '600',\n color: '#2f2f2f',\n marginBottom: '4px',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n};\n\nconst helperTextStyles: React.CSSProperties = {\n fontSize: '13px',\n margin: '0',\n marginTop: '2px',\n color: '#595959',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n lineHeight: '1.4',\n};\n\nconst errorTextStyles: React.CSSProperties = {\n ...helperTextStyles,\n color: '#a62323', // destructive-600\n display: 'flex',\n alignItems: 'center',\n gap: '4px',\n};\n\nconst containerStyles = {\n base: {\n display: 'flex',\n alignItems: 'center',\n height: '46px',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n backgroundColor: '#ffffff',\n borderRadius: '8px',\n transition: 'all 0.2s ease-in-out',\n overflow: 'hidden',\n },\n states: {\n default: {\n border: '1px solid #d1d1d1', // grey-300\n },\n defaultFocus: {\n borderColor: '#3cad51', // brand-500\n outline: '3px solid rgba(60, 173, 81, 0.2)',\n },\n error: {\n border: '1px solid #c93232', // destructive-500\n },\n errorFocus: {\n borderColor: '#c93232',\n outline: '3px solid rgba(201, 50, 50, 0.2)',\n },\n success: {\n border: '1px solid #16a33d', // success-500\n },\n successFocus: {\n borderColor: '#16a33d',\n outline: '3px solid rgba(22, 163, 61, 0.2)',\n },\n disabled: {\n backgroundColor: '#f8f8f8', // grey-050\n borderColor: '#efefef', // grey-100\n cursor: 'not-allowed',\n },\n },\n};\n\nconst buttonStyles: React.CSSProperties = {\n width: '46px',\n height: '100%',\n border: 'none',\n background: 'transparent',\n cursor: 'pointer',\n fontSize: '16px',\n color: '#2f2f2f',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n transition: 'background-color 0.2s',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n userSelect: 'none' as const,\n};\n\nconst inputStyles: React.CSSProperties = {\n flex: 1,\n border: 'none',\n outline: 'none',\n background: 'transparent',\n textAlign: 'center',\n fontSize: '13px',\n color: '#2f2f2f',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n padding: '0 8px',\n appearance: 'textfield' as any,\n};\n\n/**\n * NumericInput component - Arbor Design System\n *\n * A number input with increment/decrement buttons.\n * Enforces numeric rules (min, max, step) and prevents non-numeric input.\n */\nexport const NumericInput = React.forwardRef<HTMLDivElement, NumericInputProps>(\n (\n {\n label,\n value,\n onChange,\n min,\n max,\n step = 1,\n state = 'default',\n error,\n helperText,\n disabled = false,\n className,\n style,\n 'data-testid': dataTestId,\n },\n ref\n ) => {\n const [isFocused, setIsFocused] = React.useState(false);\n const inputRef = React.useRef<HTMLInputElement>(null);\n\n const inputId = React.useId();\n const helperTextId = React.useId();\n const errorId = React.useId();\n\n const clampValue = (val: number): number => {\n let clamped = val;\n if (min !== undefined && clamped < min) clamped = min;\n if (max !== undefined && clamped > max) clamped = max;\n return clamped;\n };\n\n const handleIncrement = () => {\n if (disabled) return;\n const currentValue = value ?? 0;\n const newValue = currentValue + step;\n const clampedValue = clampValue(newValue);\n if (clampedValue !== currentValue) {\n onChange?.(clampedValue);\n }\n };\n\n const handleDecrement = () => {\n if (disabled) return;\n const currentValue = value ?? 0;\n const newValue = currentValue - step;\n const clampedValue = clampValue(newValue);\n if (clampedValue !== currentValue) {\n onChange?.(clampedValue);\n }\n };\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const inputValue = e.target.value;\n\n // Allow empty input\n if (inputValue === '' || inputValue === '-') {\n onChange?.(undefined);\n return;\n }\n\n // Parse as number\n const numValue = parseFloat(inputValue);\n\n // Only update if it's a valid number\n if (!isNaN(numValue)) {\n const clampedValue = clampValue(numValue);\n onChange?.(clampedValue);\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n // Allow: backspace, delete, tab, escape, enter, minus sign, decimal point\n const allowedKeys = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', '-', '.'];\n\n if (allowedKeys.includes(e.key)) {\n // Handle arrow keys for increment/decrement\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n handleIncrement();\n } else if (e.key === 'ArrowDown') {\n e.preventDefault();\n handleDecrement();\n }\n return;\n }\n\n // Only allow numbers\n if (!/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n }\n };\n\n const currentState = error ? 'error' : state;\n const stateStyles = containerStyles.states[currentState];\n const focusStyles =\n currentState === 'error'\n ? containerStyles.states.errorFocus\n : currentState === 'success'\n ? containerStyles.states.successFocus\n : containerStyles.states.defaultFocus;\n\n const containerStyle: React.CSSProperties = {\n ...containerStyles.base,\n ...stateStyles,\n ...(isFocused && !disabled && focusStyles),\n ...(disabled && containerStyles.states.disabled),\n };\n\n const isDecrementDisabled = disabled || (min !== undefined && value !== undefined && value <= min);\n const isIncrementDisabled = disabled || (max !== undefined && value !== undefined && value >= max);\n\n // Button styles for success state (green outlined circles)\n const getButtonStyle = (isDisabled: boolean) => {\n const baseStyle: React.CSSProperties = {\n ...buttonStyles,\n cursor: isDisabled ? 'not-allowed' : 'pointer',\n color: isDisabled ? '#7e7e7e' : '#2f2f2f',\n };\n\n // Add green circle border for success state\n if (currentState === 'success' && !isDisabled) {\n return {\n ...baseStyle,\n color: '#16a33d', // success-500\n border: '2px solid #16a33d',\n borderRadius: '50%',\n width: '32px',\n height: '32px',\n };\n }\n\n return baseStyle;\n };\n\n return (\n <div className={clsx('arbor-numeric-input-wrapper', className)} style={style} ref={ref} data-testid={dataTestId}>\n {label && (\n <label htmlFor={inputId} style={labelStyles}>\n {label}\n </label>\n )}\n\n <div style={containerStyle}>\n <button\n type=\"button\"\n onClick={handleDecrement}\n disabled={isDecrementDisabled}\n style={getButtonStyle(isDecrementDisabled)}\n onMouseEnter={(e) => {\n if (!isDecrementDisabled) {\n e.currentTarget.style.backgroundColor = '#f8f8f8';\n e.currentTarget.style.borderRadius = '99px';\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent';\n e.currentTarget.style.borderRadius = currentState === 'success' ? '50%' : '0';\n }}\n aria-label=\"Decrement\"\n >\n −\n </button>\n\n <input\n ref={inputRef}\n id={inputId}\n type=\"text\"\n inputMode=\"numeric\"\n value={value !== undefined ? value : ''}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n disabled={disabled}\n aria-invalid={error ? 'true' : 'false'}\n aria-describedby={error ? errorId : helperText ? helperTextId : undefined}\n style={{\n ...inputStyles,\n color: disabled ? '#7e7e7e' : '#2f2f2f',\n cursor: disabled ? 'not-allowed' : 'text',\n }}\n />\n\n <button\n type=\"button\"\n onClick={handleIncrement}\n disabled={isIncrementDisabled}\n style={getButtonStyle(isIncrementDisabled)}\n onMouseEnter={(e) => {\n if (!isIncrementDisabled) {\n e.currentTarget.style.backgroundColor = '#f8f8f8';\n e.currentTarget.style.borderRadius = '99px';\n }\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent';\n e.currentTarget.style.borderRadius = currentState === 'success' ? '50%' : '0';\n }}\n aria-label=\"Increment\"\n >\n +\n </button>\n </div>\n\n {error && (\n <p id={errorId} style={errorTextStyles}>\n {error}\n </p>\n )}\n\n {helperText && !error && (\n <p id={helperTextId} style={helperTextStyles}>\n {helperText}\n </p>\n )}\n </div>\n );\n }\n);\n\nNumericInput.displayName = 'NumericInput';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,293 @@
1
+ // src/Combobox/Combobox.tsx
2
+ import * as React from "react";
3
+ import { clsx } from "clsx";
4
+ import { X, ChevronDown, Check } from "lucide-react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var comboboxStyles = {
7
+ base: {
8
+ width: "100%",
9
+ height: "36px",
10
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
11
+ backgroundColor: "#ffffff",
12
+ borderRadius: "8px",
13
+ transition: "all 0.2s ease-in-out",
14
+ outline: "none",
15
+ fontSize: "13px",
16
+ padding: "8px",
17
+ paddingRight: "32px",
18
+ margin: "0",
19
+ display: "flex",
20
+ alignItems: "center",
21
+ cursor: "pointer",
22
+ position: "relative"
23
+ },
24
+ states: {
25
+ default: {
26
+ border: "1px solid #d1d1d1",
27
+ // grey-300
28
+ color: "#2f2f2f"
29
+ // grey-900
30
+ },
31
+ defaultFocus: {
32
+ borderColor: "#3cad51",
33
+ // brand-500
34
+ outline: "3px solid rgba(60, 173, 81, 0.2)"
35
+ },
36
+ error: {
37
+ border: "1px solid #c93232",
38
+ // destructive-500
39
+ color: "#2f2f2f"
40
+ },
41
+ errorFocus: {
42
+ borderColor: "#c93232",
43
+ outline: "3px solid rgba(201, 50, 50, 0.2)"
44
+ },
45
+ success: {
46
+ border: "1px solid #16a33d",
47
+ // success-500
48
+ color: "#2f2f2f"
49
+ },
50
+ successFocus: {
51
+ borderColor: "#16a33d",
52
+ outline: "3px solid rgba(22, 163, 61, 0.2)"
53
+ },
54
+ disabled: {
55
+ backgroundColor: "#f8f8f8",
56
+ // grey-050
57
+ borderColor: "#efefef",
58
+ // grey-100
59
+ color: "#7e7e7e",
60
+ // grey-500
61
+ cursor: "not-allowed"
62
+ }
63
+ }
64
+ };
65
+ var labelStyles = {
66
+ display: "block",
67
+ fontSize: "13px",
68
+ fontWeight: "600",
69
+ color: "#2f2f2f",
70
+ marginBottom: "4px",
71
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
72
+ };
73
+ var helperTextStyles = {
74
+ fontSize: "13px",
75
+ margin: "0",
76
+ marginTop: "2px",
77
+ color: "#595959",
78
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
79
+ lineHeight: "1.4"
80
+ };
81
+ var errorTextStyles = {
82
+ ...helperTextStyles,
83
+ color: "#a62323",
84
+ // destructive-600
85
+ display: "flex",
86
+ alignItems: "center",
87
+ gap: "4px"
88
+ };
89
+ var dropdownStyles = {
90
+ position: "absolute",
91
+ top: "calc(100% + 4px)",
92
+ left: "0",
93
+ right: "0",
94
+ backgroundColor: "#ffffff",
95
+ border: "1px solid #d1d1d1",
96
+ borderRadius: "8px",
97
+ maxHeight: "200px",
98
+ overflowY: "auto",
99
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
100
+ zIndex: 1e3
101
+ };
102
+ var optionStyles = {
103
+ padding: "8px 12px",
104
+ fontSize: "13px",
105
+ cursor: "pointer",
106
+ display: "flex",
107
+ alignItems: "center",
108
+ justifyContent: "space-between",
109
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
110
+ color: "#2f2f2f"
111
+ // grey-900
112
+ };
113
+ var Combobox = React.forwardRef(
114
+ ({
115
+ label,
116
+ placeholder = "default-text",
117
+ options,
118
+ value,
119
+ onChange,
120
+ state = "default",
121
+ error,
122
+ helperText,
123
+ disabled = false,
124
+ className,
125
+ style,
126
+ "data-testid": dataTestId
127
+ }, ref) => {
128
+ const [isOpen, setIsOpen] = React.useState(false);
129
+ const [searchQuery, setSearchQuery] = React.useState("");
130
+ const [isFocused, setIsFocused] = React.useState(false);
131
+ const inputRef = React.useRef(null);
132
+ const dropdownRef = React.useRef(null);
133
+ const wrapperRef = React.useRef(null);
134
+ const comboboxId = React.useId();
135
+ const helperTextId = React.useId();
136
+ const errorId = React.useId();
137
+ const selectedOption = options.find((opt) => opt.value === value);
138
+ const filteredOptions = React.useMemo(() => {
139
+ if (!searchQuery) return options;
140
+ return options.filter(
141
+ (option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())
142
+ );
143
+ }, [options, searchQuery]);
144
+ React.useEffect(() => {
145
+ const handleClickOutside = (event) => {
146
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
147
+ setIsOpen(false);
148
+ setSearchQuery("");
149
+ }
150
+ };
151
+ document.addEventListener("mousedown", handleClickOutside);
152
+ return () => document.removeEventListener("mousedown", handleClickOutside);
153
+ }, []);
154
+ const handleInputChange = (e) => {
155
+ setSearchQuery(e.target.value);
156
+ if (!isOpen) setIsOpen(true);
157
+ };
158
+ const handleSelectOption = (optionValue) => {
159
+ onChange?.(optionValue);
160
+ setIsOpen(false);
161
+ setSearchQuery("");
162
+ inputRef.current?.blur();
163
+ };
164
+ const handleClear = (e) => {
165
+ e.stopPropagation();
166
+ onChange?.(void 0);
167
+ setSearchQuery("");
168
+ inputRef.current?.focus();
169
+ };
170
+ const handleInputFocus = () => {
171
+ setIsFocused(true);
172
+ if (!disabled) setIsOpen(true);
173
+ };
174
+ const handleInputBlur = () => {
175
+ setIsFocused(false);
176
+ setTimeout(() => {
177
+ if (!wrapperRef.current?.contains(document.activeElement)) {
178
+ setSearchQuery("");
179
+ }
180
+ }, 200);
181
+ };
182
+ const currentState = error ? "error" : state;
183
+ const stateStyles = comboboxStyles.states[currentState];
184
+ const focusStyles = currentState === "error" ? comboboxStyles.states.errorFocus : currentState === "success" ? comboboxStyles.states.successFocus : comboboxStyles.states.defaultFocus;
185
+ const inputWrapperStyle = {
186
+ ...comboboxStyles.base,
187
+ ...stateStyles,
188
+ ...isFocused && !disabled && focusStyles,
189
+ ...disabled && comboboxStyles.states.disabled
190
+ };
191
+ const displayValue = selectedOption ? selectedOption.label : "";
192
+ const showPlaceholder = !selectedOption && !searchQuery && !isFocused;
193
+ return /* @__PURE__ */ jsxs("div", { className: clsx("arbor-combobox-wrapper", className), style, ref: wrapperRef, "data-testid": dataTestId, children: [
194
+ label && /* @__PURE__ */ jsx("label", { htmlFor: comboboxId, style: labelStyles, children: label }),
195
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, ref, children: [
196
+ /* @__PURE__ */ jsxs("div", { style: inputWrapperStyle, children: [
197
+ /* @__PURE__ */ jsx(
198
+ "input",
199
+ {
200
+ ref: inputRef,
201
+ id: comboboxId,
202
+ type: "text",
203
+ value: searchQuery || displayValue,
204
+ onChange: handleInputChange,
205
+ onFocus: handleInputFocus,
206
+ onBlur: handleInputBlur,
207
+ placeholder: showPlaceholder ? placeholder : "",
208
+ disabled,
209
+ "aria-invalid": error ? "true" : "false",
210
+ "aria-describedby": error ? errorId : helperText ? helperTextId : void 0,
211
+ "aria-expanded": isOpen,
212
+ "aria-autocomplete": "list",
213
+ role: "combobox",
214
+ style: {
215
+ border: "none",
216
+ outline: "none",
217
+ background: "transparent",
218
+ flex: 1,
219
+ fontSize: "13px",
220
+ padding: 0,
221
+ margin: 0,
222
+ color: showPlaceholder ? "#7e7e7e" : "#2f2f2f",
223
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
224
+ }
225
+ }
226
+ ),
227
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "4px", position: "absolute", right: "8px" }, children: [
228
+ selectedOption && !disabled && /* @__PURE__ */ jsx(
229
+ "button",
230
+ {
231
+ type: "button",
232
+ onClick: handleClear,
233
+ style: {
234
+ border: "none",
235
+ background: "transparent",
236
+ padding: "2px",
237
+ cursor: "pointer",
238
+ display: "flex",
239
+ alignItems: "center",
240
+ color: "#7e7e7e"
241
+ },
242
+ "aria-label": "Clear selection",
243
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
244
+ }
245
+ ),
246
+ currentState === "success" && !isOpen && /* @__PURE__ */ jsx(Check, { size: 16, color: "#16a33d" }),
247
+ /* @__PURE__ */ jsx(
248
+ ChevronDown,
249
+ {
250
+ size: 16,
251
+ color: "#7e7e7e",
252
+ style: {
253
+ transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
254
+ transition: "transform 0.2s"
255
+ }
256
+ }
257
+ )
258
+ ] })
259
+ ] }),
260
+ isOpen && !disabled && /* @__PURE__ */ jsx("div", { ref: dropdownRef, style: dropdownStyles, children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("div", { style: { ...optionStyles, color: "#7e7e7e", cursor: "default" }, children: "No options found" }) : filteredOptions.map((option) => /* @__PURE__ */ jsxs(
261
+ "div",
262
+ {
263
+ onClick: () => handleSelectOption(option.value),
264
+ onMouseDown: (e) => e.preventDefault(),
265
+ style: {
266
+ ...optionStyles,
267
+ backgroundColor: option.value === value ? "#f8f8f8" : "transparent"
268
+ },
269
+ onMouseEnter: (e) => {
270
+ e.currentTarget.style.backgroundColor = "#f8f8f8";
271
+ },
272
+ onMouseLeave: (e) => {
273
+ e.currentTarget.style.backgroundColor = option.value === value ? "#f8f8f8" : "transparent";
274
+ },
275
+ children: [
276
+ /* @__PURE__ */ jsx("span", { children: option.label }),
277
+ option.value === value && /* @__PURE__ */ jsx(Check, { size: 16, color: "#16a33d" })
278
+ ]
279
+ },
280
+ option.value
281
+ )) })
282
+ ] }),
283
+ error && /* @__PURE__ */ jsx("p", { id: errorId, style: errorTextStyles, role: "alert", children: error }),
284
+ !error && helperText && /* @__PURE__ */ jsx("p", { id: helperTextId, style: helperTextStyles, children: helperText })
285
+ ] });
286
+ }
287
+ );
288
+ Combobox.displayName = "Combobox";
289
+
290
+ export {
291
+ Combobox
292
+ };
293
+ //# sourceMappingURL=chunk-7OWLBYNM.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Combobox/Combobox.tsx"],"sourcesContent":["import * as React from 'react';\nimport { clsx } from 'clsx';\nimport { X, ChevronDown, Check } from 'lucide-react';\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n};\n\nexport type ComboboxState = 'default' | 'error' | 'success';\n\nexport interface ComboboxProps {\n /**\n * The label for the combobox\n */\n label?: string;\n /**\n * The placeholder text\n */\n placeholder?: string;\n /**\n * The available options\n */\n options: ComboboxOption[];\n /**\n * The selected value\n */\n value?: string;\n /**\n * Callback when value changes\n */\n onChange?: (value: string | undefined) => void;\n /**\n * The validation state\n * @default 'default'\n */\n state?: ComboboxState;\n /**\n * Optional error message\n */\n error?: string;\n /**\n * Optional helper text\n */\n helperText?: string;\n /**\n * Whether the combobox is disabled\n */\n disabled?: boolean;\n /**\n * Custom className\n */\n className?: string;\n /**\n * Custom style\n */\n style?: React.CSSProperties;\n /**\n * Test ID for testing\n */\n 'data-testid'?: string;\n}\n\nconst comboboxStyles = {\n base: {\n width: '100%',\n height: '36px',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n backgroundColor: '#ffffff',\n borderRadius: '8px',\n transition: 'all 0.2s ease-in-out',\n outline: 'none',\n fontSize: '13px',\n padding: '8px',\n paddingRight: '32px',\n margin: '0',\n display: 'flex',\n alignItems: 'center',\n cursor: 'pointer',\n position: 'relative' as const,\n },\n states: {\n default: {\n border: '1px solid #d1d1d1', // grey-300\n color: '#2f2f2f', // grey-900\n },\n defaultFocus: {\n borderColor: '#3cad51', // brand-500\n outline: '3px solid rgba(60, 173, 81, 0.2)',\n },\n error: {\n border: '1px solid #c93232', // destructive-500\n color: '#2f2f2f',\n },\n errorFocus: {\n borderColor: '#c93232',\n outline: '3px solid rgba(201, 50, 50, 0.2)',\n },\n success: {\n border: '1px solid #16a33d', // success-500\n color: '#2f2f2f',\n },\n successFocus: {\n borderColor: '#16a33d',\n outline: '3px solid rgba(22, 163, 61, 0.2)',\n },\n disabled: {\n backgroundColor: '#f8f8f8', // grey-050\n borderColor: '#efefef', // grey-100\n color: '#7e7e7e', // grey-500\n cursor: 'not-allowed',\n },\n },\n};\n\nconst labelStyles: React.CSSProperties = {\n display: 'block',\n fontSize: '13px',\n fontWeight: '600',\n color: '#2f2f2f',\n marginBottom: '4px',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n};\n\nconst helperTextStyles: React.CSSProperties = {\n fontSize: '13px',\n margin: '0',\n marginTop: '2px',\n color: '#595959',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n lineHeight: '1.4',\n};\n\nconst errorTextStyles: React.CSSProperties = {\n ...helperTextStyles,\n color: '#a62323', // destructive-600\n display: 'flex',\n alignItems: 'center',\n gap: '4px',\n};\n\nconst dropdownStyles: React.CSSProperties = {\n position: 'absolute',\n top: 'calc(100% + 4px)',\n left: '0',\n right: '0',\n backgroundColor: '#ffffff',\n border: '1px solid #d1d1d1',\n borderRadius: '8px',\n maxHeight: '200px',\n overflowY: 'auto',\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',\n zIndex: 1000,\n};\n\nconst optionStyles: React.CSSProperties = {\n padding: '8px 12px',\n fontSize: '13px',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n color: '#2f2f2f', // grey-900\n};\n\n/**\n * Combobox component - Arbor Design System\n *\n * A searchable select/autocomplete component with filtering.\n * Supports labels, validation states, and helper text.\n */\nexport const Combobox = React.forwardRef<HTMLDivElement, ComboboxProps>(\n (\n {\n label,\n placeholder = 'default-text',\n options,\n value,\n onChange,\n state = 'default',\n error,\n helperText,\n disabled = false,\n className,\n style,\n 'data-testid': dataTestId,\n },\n ref\n ) => {\n const [isOpen, setIsOpen] = React.useState(false);\n const [searchQuery, setSearchQuery] = React.useState('');\n const [isFocused, setIsFocused] = React.useState(false);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const dropdownRef = React.useRef<HTMLDivElement>(null);\n const wrapperRef = React.useRef<HTMLDivElement>(null);\n\n const comboboxId = React.useId();\n const helperTextId = React.useId();\n const errorId = React.useId();\n\n const selectedOption = options.find((opt) => opt.value === value);\n\n const filteredOptions = React.useMemo(() => {\n if (!searchQuery) return options;\n return options.filter((option) =>\n option.label.toLowerCase().includes(searchQuery.toLowerCase())\n );\n }, [options, searchQuery]);\n\n // Close dropdown when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (\n wrapperRef.current &&\n !wrapperRef.current.contains(event.target as Node)\n ) {\n setIsOpen(false);\n setSearchQuery('');\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setSearchQuery(e.target.value);\n if (!isOpen) setIsOpen(true);\n };\n\n const handleSelectOption = (optionValue: string) => {\n onChange?.(optionValue);\n setIsOpen(false);\n setSearchQuery('');\n inputRef.current?.blur();\n };\n\n const handleClear = (e: React.MouseEvent) => {\n e.stopPropagation();\n onChange?.(undefined);\n setSearchQuery('');\n inputRef.current?.focus();\n };\n\n const handleInputFocus = () => {\n setIsFocused(true);\n if (!disabled) setIsOpen(true);\n };\n\n const handleInputBlur = () => {\n setIsFocused(false);\n // Delay to allow click on option\n setTimeout(() => {\n if (!wrapperRef.current?.contains(document.activeElement)) {\n setSearchQuery('');\n }\n }, 200);\n };\n\n const currentState = error ? 'error' : state;\n const stateStyles = comboboxStyles.states[currentState];\n const focusStyles =\n currentState === 'error'\n ? comboboxStyles.states.errorFocus\n : currentState === 'success'\n ? comboboxStyles.states.successFocus\n : comboboxStyles.states.defaultFocus;\n\n const inputWrapperStyle: React.CSSProperties = {\n ...comboboxStyles.base,\n ...stateStyles,\n ...(isFocused && !disabled && focusStyles),\n ...(disabled && comboboxStyles.states.disabled),\n };\n\n const displayValue = selectedOption ? selectedOption.label : '';\n const showPlaceholder = !selectedOption && !searchQuery && !isFocused;\n\n return (\n <div className={clsx('arbor-combobox-wrapper', className)} style={style} ref={wrapperRef} data-testid={dataTestId}>\n {label && (\n <label htmlFor={comboboxId} style={labelStyles}>\n {label}\n </label>\n )}\n\n <div style={{ position: 'relative' }} ref={ref}>\n <div style={inputWrapperStyle}>\n <input\n ref={inputRef}\n id={comboboxId}\n type=\"text\"\n value={searchQuery || displayValue}\n onChange={handleInputChange}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n placeholder={showPlaceholder ? placeholder : ''}\n disabled={disabled}\n aria-invalid={error ? 'true' : 'false'}\n aria-describedby={error ? errorId : helperText ? helperTextId : undefined}\n aria-expanded={isOpen}\n aria-autocomplete=\"list\"\n role=\"combobox\"\n style={{\n border: 'none',\n outline: 'none',\n background: 'transparent',\n flex: 1,\n fontSize: '13px',\n padding: 0,\n margin: 0,\n color: showPlaceholder ? '#7e7e7e' : '#2f2f2f',\n fontFamily: \"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n }}\n />\n\n <div style={{ display: 'flex', alignItems: 'center', gap: '4px', position: 'absolute', right: '8px' }}>\n {selectedOption && !disabled && (\n <button\n type=\"button\"\n onClick={handleClear}\n style={{\n border: 'none',\n background: 'transparent',\n padding: '2px',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n color: '#7e7e7e',\n }}\n aria-label=\"Clear selection\"\n >\n <X size={16} />\n </button>\n )}\n\n {currentState === 'success' && !isOpen && (\n <Check size={16} color=\"#16a33d\" />\n )}\n\n <ChevronDown\n size={16}\n color=\"#7e7e7e\"\n style={{\n transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',\n transition: 'transform 0.2s',\n }}\n />\n </div>\n </div>\n\n {isOpen && !disabled && (\n <div ref={dropdownRef} style={dropdownStyles}>\n {filteredOptions.length === 0 ? (\n <div style={{ ...optionStyles, color: '#7e7e7e', cursor: 'default' }}>\n No options found\n </div>\n ) : (\n filteredOptions.map((option) => (\n <div\n key={option.value}\n onClick={() => handleSelectOption(option.value)}\n onMouseDown={(e) => e.preventDefault()} // Prevent blur\n style={{\n ...optionStyles,\n backgroundColor: option.value === value ? '#f8f8f8' : 'transparent',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = '#f8f8f8';\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor =\n option.value === value ? '#f8f8f8' : 'transparent';\n }}\n >\n <span>{option.label}</span>\n {option.value === value && <Check size={16} color=\"#16a33d\" />}\n </div>\n ))\n )}\n </div>\n )}\n </div>\n\n {error && (\n <p id={errorId} style={errorTextStyles} role=\"alert\">\n {error}\n </p>\n )}\n {!error && helperText && (\n <p id={helperTextId} style={helperTextStyles}>\n {helperText}\n </p>\n )}\n </div>\n );\n }\n);\n\nCombobox.displayName = 'Combobox';\n"],"mappings":"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","names":[]}