@ecohouse/ui 0.1.2 → 0.1.4

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.
Files changed (74) hide show
  1. package/dist/index.cjs +1734 -155
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +287 -13
  4. package/dist/index.d.ts +287 -13
  5. package/dist/index.js +1720 -159
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Avatar/Avatar.stories.tsx +133 -0
  9. package/src/components/Avatar/Avatar.tsx +389 -0
  10. package/src/components/Avatar/index.ts +2 -0
  11. package/src/components/Button/Button.tsx +106 -88
  12. package/src/components/Checkbox/Checkbox.stories.tsx +112 -0
  13. package/src/components/Checkbox/Checkbox.tsx +150 -0
  14. package/src/components/Checkbox/index.ts +2 -0
  15. package/src/components/Input/Input.stories.tsx +11 -40
  16. package/src/components/Input/Input.tsx +58 -54
  17. package/src/components/Input/index.ts +1 -1
  18. package/src/components/Select/Select.stories.tsx +196 -0
  19. package/src/components/Select/Select.tsx +851 -0
  20. package/src/components/Select/index.ts +2 -0
  21. package/src/components/Textarea/Textarea.stories.tsx +71 -0
  22. package/src/components/Textarea/Textarea.tsx +241 -0
  23. package/src/components/Textarea/index.ts +2 -0
  24. package/src/components/Toggle/Toggle.stories.tsx +100 -0
  25. package/src/components/Toggle/Toggle.tsx +176 -0
  26. package/src/components/Toggle/index.ts +2 -0
  27. package/src/components/index.ts +16 -1
  28. package/src/icons/Calendar/CalendarIcon.tsx +21 -0
  29. package/src/icons/Calendar/CalendarIcon.web.tsx +27 -0
  30. package/src/icons/Calendar/calendarPath.ts +3 -0
  31. package/src/icons/Calendar/index.ts +1 -0
  32. package/src/icons/Check/CheckIcon.tsx +26 -0
  33. package/src/icons/Check/CheckIcon.web.tsx +32 -0
  34. package/src/icons/Check/checkPath.ts +2 -0
  35. package/src/icons/Check/index.ts +1 -0
  36. package/src/icons/ChevronDown/ChevronDownIcon.tsx +30 -0
  37. package/src/icons/ChevronDown/ChevronDownIcon.web.tsx +36 -0
  38. package/src/icons/ChevronDown/chevronDownPath.ts +2 -0
  39. package/src/icons/ChevronDown/index.ts +1 -0
  40. package/src/icons/Heart/HeartIcon.tsx +23 -0
  41. package/src/icons/Heart/HeartIcon.web.tsx +29 -0
  42. package/src/icons/Heart/heartPath.ts +3 -0
  43. package/src/icons/Heart/index.ts +1 -0
  44. package/src/icons/HelpCircle/HelpCircleIcon.tsx +25 -0
  45. package/src/icons/HelpCircle/HelpCircleIcon.web.tsx +31 -0
  46. package/src/icons/HelpCircle/helpCirclePath.ts +3 -0
  47. package/src/icons/HelpCircle/index.ts +1 -0
  48. package/src/icons/LayoutGrid/LayoutGridIcon.tsx +25 -0
  49. package/src/icons/LayoutGrid/LayoutGridIcon.web.tsx +31 -0
  50. package/src/icons/LayoutGrid/index.ts +1 -0
  51. package/src/icons/LayoutGrid/layoutGridPath.ts +3 -0
  52. package/src/icons/LogOut/LogOutIcon.tsx +21 -0
  53. package/src/icons/LogOut/LogOutIcon.web.tsx +27 -0
  54. package/src/icons/LogOut/index.ts +1 -0
  55. package/src/icons/LogOut/logOutPath.ts +3 -0
  56. package/src/icons/Search/SearchIcon.tsx +37 -0
  57. package/src/icons/Search/SearchIcon.web.tsx +43 -0
  58. package/src/icons/Search/index.ts +1 -0
  59. package/src/icons/Search/searchPath.ts +5 -0
  60. package/src/icons/Settings/SettingsIcon.tsx +21 -0
  61. package/src/icons/Settings/SettingsIcon.web.tsx +27 -0
  62. package/src/icons/Settings/index.ts +1 -0
  63. package/src/icons/Settings/settingsPath.ts +3 -0
  64. package/src/icons/Video/VideoIcon.tsx +24 -0
  65. package/src/icons/Video/VideoIcon.web.tsx +30 -0
  66. package/src/icons/Video/index.ts +1 -0
  67. package/src/icons/Video/videoPath.ts +5 -0
  68. package/src/icons/index.ts +10 -0
  69. package/src/icons/registry.ts +24 -3
  70. package/src/index.ts +31 -3
  71. package/src/theme/colors.ts +1 -0
  72. package/src/theme/index.ts +8 -1
  73. package/src/theme/typography.ts +77 -2
  74. package/src/utils/mergeRefs.ts +20 -0
@@ -1,4 +1,4 @@
1
- import { useRef, useState, type ComponentRef, type ReactNode } from "react";
1
+ import { forwardRef, useMemo, useRef, useState, type ComponentRef, type ReactNode } from "react";
2
2
  import {
3
3
  Platform,
4
4
  StyleSheet,
@@ -14,9 +14,9 @@ import {
14
14
  import { ShowIcon } from "../../icons";
15
15
  import { UserIcon } from "../../icons";
16
16
  import { colors, inputTypography } from "../../theme";
17
+ import { mergeRefs } from "../../utils/mergeRefs";
17
18
  import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
18
19
 
19
- export type InputState = "default" | "filled" | "active" | "error" | "disabled";
20
20
  export type InputSize = "lg" | "sm";
21
21
 
22
22
  export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
@@ -24,15 +24,18 @@ export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
24
24
  showLabel?: boolean;
25
25
  hint?: string;
26
26
  showHint?: boolean;
27
- state?: InputState;
28
27
  size?: InputSize;
29
- disabled?: boolean;
28
+ /** External — validation / form error. */
30
29
  error?: boolean;
30
+ /** External — non-interactive. */
31
+ disabled?: boolean;
31
32
  leftIcon?: ReactNode;
32
33
  showLeftIcon?: boolean;
33
34
  rightIcon?: ReactNode;
34
35
  showRightIcon?: boolean;
35
36
  onRightIconPress?: () => void;
37
+ /** Accessible name for the right icon button, when `onRightIconPress` is set. */
38
+ rightIconAccessibilityLabel?: string;
36
39
  style?: StyleProp<ViewStyle>;
37
40
  fieldStyle?: StyleProp<ViewStyle>;
38
41
  inputStyle?: StyleProp<TextStyle>;
@@ -40,6 +43,8 @@ export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
40
43
  inputClassName?: string;
41
44
  }
42
45
 
46
+ type VisualState = "default" | "filled" | "active" | "error" | "disabled";
47
+
43
48
  type FieldChrome = {
44
49
  borderColor: string;
45
50
  backgroundColor: string;
@@ -74,20 +79,18 @@ const sizeConfig = {
74
79
  },
75
80
  } as const satisfies Record<InputSize, Record<string, unknown>>;
76
81
 
77
- function resolveState({
78
- state,
82
+ /** default / filled / active come from value + focus; error / disabled from props. */
83
+ function resolveVisualState({
79
84
  disabled,
80
85
  error,
81
86
  focused,
82
87
  hasValue,
83
88
  }: {
84
- state?: InputState;
85
- disabled?: boolean;
86
- error?: boolean;
89
+ disabled: boolean;
90
+ error: boolean;
87
91
  focused: boolean;
88
92
  hasValue: boolean;
89
- }): InputState {
90
- if (state) return state;
93
+ }): VisualState {
91
94
  if (disabled) return "disabled";
92
95
  if (error) return "error";
93
96
  if (focused) return "active";
@@ -95,7 +98,7 @@ function resolveState({
95
98
  return "default";
96
99
  }
97
100
 
98
- function chromeFor(state: InputState): FieldChrome {
101
+ function chromeFor(state: VisualState): FieldChrome {
99
102
  const base: FieldChrome = {
100
103
  borderColor: colors.grey700,
101
104
  backgroundColor: colors.grey700,
@@ -142,35 +145,40 @@ function chromeFor(state: InputState): FieldChrome {
142
145
  }
143
146
  }
144
147
 
145
- export function Input({
146
- label = "Label",
147
- showLabel = true,
148
- hint = "Hint",
149
- showHint = true,
150
- placeholder = "placeholder",
151
- state: stateProp,
152
- size = "lg",
153
- disabled = false,
154
- error = false,
155
- value,
156
- defaultValue,
157
- onChangeText,
158
- onFocus,
159
- onBlur,
160
- leftIcon,
161
- showLeftIcon = false,
162
- rightIcon,
163
- showRightIcon = false,
164
- onRightIconPress,
165
- style,
166
- fieldStyle,
167
- inputStyle,
168
- className,
169
- inputClassName,
170
- ...textInputProps
171
- }: InputProps) {
148
+ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(function Input(
149
+ {
150
+ label = "Label",
151
+ showLabel = true,
152
+ hint = "Hint",
153
+ showHint = true,
154
+ placeholder = "placeholder",
155
+ size = "lg",
156
+ disabled = false,
157
+ error = false,
158
+ value,
159
+ defaultValue,
160
+ onChangeText,
161
+ onFocus,
162
+ onBlur,
163
+ leftIcon,
164
+ showLeftIcon = false,
165
+ rightIcon,
166
+ showRightIcon = false,
167
+ onRightIconPress,
168
+ rightIconAccessibilityLabel,
169
+ style,
170
+ fieldStyle,
171
+ inputStyle,
172
+ className,
173
+ inputClassName,
174
+ accessibilityLabel,
175
+ ...textInputProps
176
+ },
177
+ forwardedRef,
178
+ ) {
172
179
  const rootRef = useRef<ComponentRef<typeof View>>(null);
173
180
  const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
181
+ const setInputRef = useMemo(() => mergeRefs(inputRef, forwardedRef), [forwardedRef]);
174
182
  const [focused, setFocused] = useState(false);
175
183
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
176
184
  const metrics = sizeConfig[size];
@@ -179,15 +187,9 @@ export function Input({
179
187
  const currentValue = isControlled ? value : uncontrolledValue;
180
188
  const hasValue = currentValue.length > 0;
181
189
 
182
- const resolvedState = resolveState({
183
- state: stateProp,
184
- disabled,
185
- error,
186
- focused,
187
- hasValue,
188
- });
189
- const chrome = chromeFor(resolvedState);
190
- const isDisabled = resolvedState === "disabled" || disabled;
190
+ const visualState = resolveVisualState({ disabled, error, focused, hasValue });
191
+ const chrome = chromeFor(visualState);
192
+ const resolvedAccessibilityLabel = accessibilityLabel ?? (showLabel ? undefined : label);
191
193
 
192
194
  const hasLeftIcon = showLeftIcon || leftIcon != null;
193
195
  const hasRightIcon = showRightIcon || rightIcon != null;
@@ -203,7 +205,7 @@ export function Input({
203
205
  useApplyWebClassName(inputRef, inputClassName);
204
206
 
205
207
  return (
206
- <View ref={rootRef} style={[styles.root, isDisabled && styles.disabled, style]}>
208
+ <View ref={rootRef} style={[styles.root, disabled && styles.disabled, style]}>
207
209
  {showLabel && label ? (
208
210
  <Text style={[styles.label, { color: chrome.labelColor }]}>{label}</Text>
209
211
  ) : null}
@@ -232,11 +234,11 @@ export function Input({
232
234
  ) : null}
233
235
 
234
236
  <TextInput
235
- ref={inputRef}
237
+ ref={setInputRef}
236
238
  {...textInputProps}
237
239
  value={isControlled ? value : undefined}
238
240
  defaultValue={isControlled ? undefined : defaultValue}
239
- editable={!isDisabled}
241
+ editable={!disabled}
240
242
  placeholder={placeholder}
241
243
  placeholderTextColor={chrome.placeholderColor}
242
244
  onChangeText={(text) => {
@@ -259,16 +261,18 @@ export function Input({
259
261
  Platform.OS === "android" ? styles.inputAndroid : null,
260
262
  inputStyle,
261
263
  ]}
262
- accessibilityState={{ disabled: isDisabled }}
264
+ accessibilityLabel={resolvedAccessibilityLabel}
265
+ accessibilityState={{ disabled }}
263
266
  />
264
267
 
265
268
  {hasRightIcon ? (
266
269
  onRightIconPress ? (
267
270
  <TouchableOpacity
268
271
  onPress={onRightIconPress}
269
- disabled={isDisabled}
272
+ disabled={disabled}
270
273
  style={[styles.iconSlot, { width: metrics.iconSize, height: metrics.iconSize }]}
271
274
  accessibilityRole="button"
275
+ accessibilityLabel={rightIconAccessibilityLabel}
272
276
  hitSlop={8}
273
277
  >
274
278
  {rightIconNode}
@@ -286,7 +290,7 @@ export function Input({
286
290
  ) : null}
287
291
  </View>
288
292
  );
289
- }
293
+ });
290
294
 
291
295
  const styles = StyleSheet.create({
292
296
  root: {
@@ -1,2 +1,2 @@
1
1
  export { Input } from "./Input";
2
- export type { InputProps, InputSize, InputState } from "./Input";
2
+ export type { InputProps, InputSize } from "./Input";
@@ -0,0 +1,196 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { fn } from "storybook/test";
4
+ import { View, StyleSheet, Text } from "react-native";
5
+ import { Select, type SelectOption } from "./Select";
6
+ import { colors } from "../../theme";
7
+
8
+ const defaultOptions: SelectOption[] = [
9
+ { label: "Select Item", value: "1" },
10
+ { label: "Select Item", value: "2" },
11
+ { label: "Select Item", value: "3" },
12
+ { label: "Select Item", value: "4" },
13
+ ];
14
+
15
+ const namedOptions: SelectOption[] = [
16
+ { label: "Yerevan", value: "yerevan" },
17
+ { label: "Gyumri", value: "gyumri" },
18
+ { label: "Vanadzor", value: "vanadzor" },
19
+ { label: "Dilijan", value: "dilijan" },
20
+ ];
21
+
22
+ const meta = {
23
+ title: "Components/Select",
24
+ component: Select,
25
+ args: {
26
+ label: "Label",
27
+ showLabel: true,
28
+ placeholder: "placeholder",
29
+ options: defaultOptions,
30
+ showLeftIcon: true,
31
+ showSearch: true,
32
+ defaultOpen: false,
33
+ disabled: false,
34
+ multiple: false,
35
+ onValueChange: fn(),
36
+ onOpenChange: fn(),
37
+ },
38
+ argTypes: {
39
+ multiple: { control: "boolean" },
40
+ onValueChange: { action: "valueChange" },
41
+ onOpenChange: { action: "openChange" },
42
+ },
43
+ parameters: {
44
+ backgrounds: { default: "black" },
45
+ },
46
+ } satisfies Meta<typeof Select>;
47
+
48
+ export default meta;
49
+ type Story = StoryObj<typeof meta>;
50
+
51
+ export const Default: Story = {};
52
+
53
+ export const Open: Story = {
54
+ args: { defaultOpen: true },
55
+ };
56
+
57
+ export const WithValue: Story = {
58
+ args: {
59
+ options: namedOptions,
60
+ defaultValue: "gyumri",
61
+ },
62
+ };
63
+
64
+ export const Multiple: Story = {
65
+ args: {
66
+ multiple: true,
67
+ options: namedOptions,
68
+ defaultValue: ["yerevan", "dilijan"],
69
+ defaultOpen: true,
70
+ },
71
+ };
72
+
73
+ export const WithoutSearch: Story = {
74
+ args: {
75
+ showSearch: false,
76
+ defaultOpen: true,
77
+ options: namedOptions,
78
+ },
79
+ };
80
+
81
+ export const WithoutLeftIcon: Story = {
82
+ args: { showLeftIcon: false },
83
+ };
84
+
85
+ export const Error: Story = {
86
+ args: {
87
+ error: true,
88
+ hint: "Hint",
89
+ showHint: true,
90
+ },
91
+ };
92
+
93
+ export const Disabled: Story = {
94
+ args: { disabled: true, defaultValue: "1" },
95
+ };
96
+
97
+ /** Verifies that a disabled Select stays non-interactive even when forced open. */
98
+ export const DisabledOpen: Story = {
99
+ args: {
100
+ disabled: true,
101
+ defaultOpen: true,
102
+ options: namedOptions,
103
+ defaultValue: "yerevan",
104
+ },
105
+ };
106
+
107
+ function ControlledSelect() {
108
+ const [value, setValue] = useState<string | undefined>();
109
+ const [open, setOpen] = useState(false);
110
+
111
+ return (
112
+ <View style={storyStyles.column}>
113
+ <Select
114
+ label="Label"
115
+ placeholder="placeholder"
116
+ options={namedOptions}
117
+ value={value}
118
+ onValueChange={setValue}
119
+ open={open}
120
+ onOpenChange={setOpen}
121
+ />
122
+ <Text style={storyStyles.meta}>
123
+ value: {value ?? "—"} · open: {String(open)}
124
+ </Text>
125
+ </View>
126
+ );
127
+ }
128
+
129
+ function ControlledMultipleSelect() {
130
+ const [value, setValue] = useState<string[]>(["gyumri"]);
131
+ const [open, setOpen] = useState(true);
132
+
133
+ return (
134
+ <View style={storyStyles.column}>
135
+ <Select
136
+ multiple
137
+ label="Cities"
138
+ placeholder="Select cities"
139
+ options={namedOptions}
140
+ value={value}
141
+ onValueChange={setValue}
142
+ open={open}
143
+ onOpenChange={setOpen}
144
+ />
145
+ <Text style={storyStyles.meta}>{value.join(", ") || "—"}</Text>
146
+ </View>
147
+ );
148
+ }
149
+
150
+ export const Controlled: Story = {
151
+ render: () => <ControlledSelect />,
152
+ };
153
+
154
+ export const ControlledMultiple: Story = {
155
+ render: () => <ControlledMultipleSelect />,
156
+ };
157
+
158
+ export const AllStates: Story = {
159
+ render: () => (
160
+ <View style={storyStyles.row}>
161
+ <Select label="Single" placeholder="placeholder" options={namedOptions} />
162
+ <Select
163
+ label="Single open"
164
+ placeholder="placeholder"
165
+ options={namedOptions}
166
+ defaultOpen
167
+ defaultValue="yerevan"
168
+ />
169
+ <Select
170
+ multiple
171
+ label="Multiple"
172
+ placeholder="Select cities"
173
+ options={namedOptions}
174
+ defaultValue={["yerevan", "gyumri"]}
175
+ defaultOpen
176
+ />
177
+ </View>
178
+ ),
179
+ };
180
+
181
+ const storyStyles = StyleSheet.create({
182
+ column: {
183
+ gap: 12,
184
+ alignItems: "flex-start",
185
+ },
186
+ row: {
187
+ flexDirection: "row",
188
+ flexWrap: "wrap",
189
+ gap: 24,
190
+ alignItems: "flex-start",
191
+ },
192
+ meta: {
193
+ color: colors.grey100,
194
+ fontSize: 12,
195
+ },
196
+ });