@ecohouse/ui 0.1.11 → 0.1.13

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": "@ecohouse/ui",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -52,6 +52,17 @@ export const WithCustomIcon: Story = {
52
52
  },
53
53
  };
54
54
 
55
+ export const WithCustomWidth: Story = {
56
+ args: {
57
+ title: "Button",
58
+ showIcon: true,
59
+ style: { width: 400 },
60
+ },
61
+ parameters: {
62
+ backgrounds: { default: "black" },
63
+ },
64
+ };
65
+
55
66
  export const AllVariants: Story = {
56
67
  render: () => (
57
68
  <View style={storyStyles.grid}>
@@ -140,6 +140,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
140
140
  paddingRight: hasIcon ? metrics.paddingRightWithIcon : metrics.paddingRight,
141
141
  gap: hasIcon ? metrics.gap : 0,
142
142
  },
143
+ hasIcon ? styles.contentWithIcon : styles.contentWithoutIcon,
143
144
  disabled && styles.disabled,
144
145
  style,
145
146
  ];
@@ -203,6 +204,12 @@ const styles = StyleSheet.create({
203
204
  disabled: {
204
205
  opacity: 0.6,
205
206
  },
207
+ contentWithIcon: {
208
+ justifyContent: "space-between",
209
+ },
210
+ contentWithoutIcon: {
211
+ justifyContent: "center",
212
+ },
206
213
  label: {},
207
214
  labelAndroid: {
208
215
  includeFontPadding: false,
@@ -35,6 +35,14 @@ export const Open: Story = {
35
35
  args: { defaultOpen: true },
36
36
  };
37
37
 
38
+ export const IndependentWidths: Story = {
39
+ args: {
40
+ defaultOpen: true,
41
+ style: { width: 240 },
42
+ calendarStyle: { width: 420 },
43
+ },
44
+ };
45
+
38
46
  export const WithValue: Story = {
39
47
  args: {
40
48
  defaultValue: new Date(2026, 8, 15),
@@ -78,6 +78,8 @@ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
78
78
  formatValue?: (date: Date) => string;
79
79
  /** First day of the week column (0 = Sunday, 1 = Monday). Defaults to Monday. */
80
80
  weekStartsOn?: 0 | 1;
81
+ /** Styles the calendar popup independently from the trigger width. */
82
+ calendarStyle?: StyleProp<ViewStyle>;
81
83
  style?: StyleProp<ViewStyle>;
82
84
  className?: string;
83
85
  };
@@ -123,6 +125,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
123
125
  weekdayLabels = DEFAULT_WEEKDAY_LABELS,
124
126
  formatValue = formatDate,
125
127
  weekStartsOn = 1,
128
+ calendarStyle,
126
129
  style,
127
130
  className,
128
131
  accessibilityLabel,
@@ -329,7 +332,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
329
332
  </Pressable>
330
333
 
331
334
  {isOpen ? (
332
- <View style={styles.menu}>
335
+ <View style={[styles.menu, calendarStyle]}>
333
336
  <View style={styles.calendarHeader}>
334
337
  <Pressable
335
338
  onPress={handlePrevMonth}
@@ -536,7 +539,7 @@ const styles = StyleSheet.create({
536
539
  position: "absolute",
537
540
  top: "100%",
538
541
  left: 0,
539
- right: 0,
542
+ width: DEFAULT_WIDTH,
540
543
  marginTop: 8,
541
544
  zIndex: 10,
542
545
  borderRadius: MENU_RADIUS,
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { fn } from "storybook/test";
3
3
  import { View, StyleSheet } from "react-native";
4
+ import { MailIcon } from "../../icons";
4
5
  import { Input } from "./Input";
5
6
 
6
7
  const meta = {
@@ -8,9 +9,7 @@ const meta = {
8
9
  component: Input,
9
10
  args: {
10
11
  label: "Label",
11
- showLabel: true,
12
12
  hint: "Hint",
13
- showHint: true,
14
13
  placeholder: "placeholder",
15
14
  size: "lg",
16
15
  showLeftIcon: true,
@@ -51,8 +50,12 @@ export const WithoutIcons: Story = {
51
50
  args: { showLeftIcon: false, showRightIcon: false },
52
51
  };
53
52
 
53
+ export const CustomIcon: Story = {
54
+ args: { leftIcon: MailIcon },
55
+ };
56
+
54
57
  export const WithoutLabelAndHint: Story = {
55
- args: { showLabel: false, showHint: false },
58
+ render: () => <Input placeholder="placeholder" showLeftIcon showRightIcon />,
56
59
  };
57
60
 
58
61
  export const AllStates: Story = {
@@ -1,4 +1,4 @@
1
- import { forwardRef, useMemo, useRef, useState, type ComponentRef, type ReactNode } from "react";
1
+ import { forwardRef, useMemo, useRef, useState, type ComponentRef } from "react";
2
2
  import {
3
3
  Platform,
4
4
  StyleSheet,
@@ -11,13 +11,14 @@ import {
11
11
  type ViewStyle,
12
12
  type TextInputProps,
13
13
  } from "react-native";
14
- import { ShowIcon } from "../../icons";
15
- import { UserIcon } from "../../icons";
14
+ import { ShowIcon, UserIcon } from "../../icons";
16
15
  import { colors, inputTypography } from "../../theme";
17
16
  import { mergeRefs } from "../../utils/mergeRefs";
17
+ import { renderStatefulIcon, type StatefulIcon } from "../../utils/renderStatefulIcon";
18
18
  import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
19
19
 
20
20
  export type InputSize = "lg" | "sm";
21
+ export type InputIcon = StatefulIcon;
21
22
 
22
23
  export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
23
24
  label?: string;
@@ -29,9 +30,9 @@ export interface InputProps extends Omit<TextInputProps, "style" | "editable"> {
29
30
  error?: boolean;
30
31
  /** External — non-interactive. */
31
32
  disabled?: boolean;
32
- leftIcon?: ReactNode;
33
+ leftIcon?: InputIcon;
33
34
  showLeftIcon?: boolean;
34
- rightIcon?: ReactNode;
35
+ rightIcon?: InputIcon;
35
36
  showRightIcon?: boolean;
36
37
  onRightIconPress?: () => void;
37
38
  /** Accessible name for the right icon button, when `onRightIconPress` is set. */
@@ -147,11 +148,11 @@ function chromeFor(state: VisualState): FieldChrome {
147
148
 
148
149
  export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(function Input(
149
150
  {
150
- label = "Label",
151
+ label,
151
152
  showLabel = true,
152
- hint = "Hint",
153
+ hint,
153
154
  showHint = true,
154
- placeholder = "placeholder",
155
+ placeholder,
155
156
  size = "lg",
156
157
  disabled = false,
157
158
  error = false,
@@ -194,12 +195,14 @@ export const Input = forwardRef<ComponentRef<typeof TextInput>, InputProps>(func
194
195
  const hasLeftIcon = showLeftIcon || leftIcon != null;
195
196
  const hasRightIcon = showRightIcon || rightIcon != null;
196
197
 
197
- const leftIconNode = leftIcon ?? (
198
- <UserIcon size={metrics.iconSize} color={chrome.leftIconColor} />
199
- );
200
- const rightIconNode = rightIcon ?? (
201
- <ShowIcon size={metrics.iconSize} color={chrome.rightIconColor} />
202
- );
198
+ const leftIconNode = renderStatefulIcon(leftIcon, UserIcon, {
199
+ size: metrics.iconSize,
200
+ color: chrome.leftIconColor,
201
+ });
202
+ const rightIconNode = renderStatefulIcon(rightIcon, ShowIcon, {
203
+ size: metrics.iconSize,
204
+ color: chrome.rightIconColor,
205
+ });
203
206
 
204
207
  useApplyWebClassName(rootRef, className);
205
208
  useApplyWebClassName(inputRef, inputClassName);
@@ -1,2 +1,2 @@
1
1
  export { Input } from "./Input";
2
- export type { InputProps, InputSize } from "./Input";
2
+ export type { InputIcon, InputProps, InputSize } from "./Input";
@@ -2,6 +2,7 @@ import { useState } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-vite";
3
3
  import { fn } from "storybook/test";
4
4
  import { View, StyleSheet, Text } from "react-native";
5
+ import { MailIcon } from "../../icons";
5
6
  import { Select, type SelectOption } from "./Select";
6
7
  import { colors } from "../../theme";
7
8
 
@@ -24,7 +25,6 @@ const meta = {
24
25
  component: Select,
25
26
  args: {
26
27
  label: "Label",
27
- showLabel: true,
28
28
  placeholder: "placeholder",
29
29
  options: defaultOptions,
30
30
  showLeftIcon: true,
@@ -82,11 +82,18 @@ export const WithoutLeftIcon: Story = {
82
82
  args: { showLeftIcon: false },
83
83
  };
84
84
 
85
+ export const CustomIcon: Story = {
86
+ args: { leftIcon: MailIcon },
87
+ };
88
+
89
+ export const WithoutLabelAndHint: Story = {
90
+ render: () => <Select options={namedOptions} placeholder="Select a city" />,
91
+ };
92
+
85
93
  export const Error: Story = {
86
94
  args: {
87
95
  error: true,
88
96
  hint: "Hint",
89
- showHint: true,
90
97
  },
91
98
  };
92
99
 
@@ -6,7 +6,6 @@ import {
6
6
  useRef,
7
7
  useState,
8
8
  type ComponentRef,
9
- type ReactNode,
10
9
  } from "react";
11
10
  import {
12
11
  Platform,
@@ -26,6 +25,7 @@ import { Checkbox } from "../Checkbox";
26
25
  import { ChevronDownIcon, SearchIcon, UserIcon } from "../../icons";
27
26
  import { colors, selectTypography } from "../../theme";
28
27
  import { mergeRefs } from "../../utils/mergeRefs";
28
+ import { renderStatefulIcon, type StatefulIcon } from "../../utils/renderStatefulIcon";
29
29
  import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
30
30
 
31
31
  export type SelectOption = {
@@ -33,6 +33,8 @@ export type SelectOption = {
33
33
  value: string;
34
34
  };
35
35
 
36
+ export type SelectIcon = StatefulIcon;
37
+
36
38
  type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
37
39
  label?: string;
38
40
  showLabel?: boolean;
@@ -45,7 +47,7 @@ type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
45
47
  onOpenChange?: (open: boolean) => void;
46
48
  disabled?: boolean;
47
49
  error?: boolean;
48
- leftIcon?: ReactNode;
50
+ leftIcon?: SelectIcon;
49
51
  showLeftIcon?: boolean;
50
52
  showSearch?: boolean;
51
53
  searchPlaceholder?: string;
@@ -244,11 +246,11 @@ function MultiValueChips({ options, disabled, onRemove }: MultiValueChipsProps)
244
246
  export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
245
247
  function Select(props, forwardedRef) {
246
248
  const {
247
- label = "Label",
249
+ label,
248
250
  showLabel = true,
249
251
  hint,
250
- showHint = false,
251
- placeholder = "placeholder",
252
+ showHint = true,
253
+ placeholder,
252
254
  options,
253
255
  open: openProp,
254
256
  defaultOpen = false,
@@ -504,12 +506,10 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
504
506
  setOpen,
505
507
  ]);
506
508
 
507
- const leftIconNode = leftIcon ?? (
508
- <UserIcon
509
- size={ICON_SIZE}
510
- color={error ? colors.red : isOpen ? colors.primary : colors.grey100}
511
- />
512
- );
509
+ const leftIconNode = renderStatefulIcon(leftIcon, UserIcon, {
510
+ size: ICON_SIZE,
511
+ color: error ? colors.red : isOpen ? colors.primary : colors.grey100,
512
+ });
513
513
  const hasLeftIcon = showLeftIcon || leftIcon != null;
514
514
 
515
515
  const triggerBorderColor = error
@@ -1,2 +1,8 @@
1
1
  export { Select } from "./Select";
2
- export type { SelectProps, SelectOption, SelectSingleProps, SelectMultipleProps } from "./Select";
2
+ export type {
3
+ SelectIcon,
4
+ SelectProps,
5
+ SelectOption,
6
+ SelectSingleProps,
7
+ SelectMultipleProps,
8
+ } from "./Select";
@@ -31,10 +31,16 @@ export type {
31
31
  } from "./DatePicker";
32
32
 
33
33
  export { Input } from "./Input";
34
- export type { InputProps, InputSize } from "./Input";
34
+ export type { InputIcon, InputProps, InputSize } from "./Input";
35
35
 
36
36
  export { Select } from "./Select";
37
- export type { SelectOption, SelectProps, SelectSingleProps, SelectMultipleProps } from "./Select";
37
+ export type {
38
+ SelectIcon,
39
+ SelectOption,
40
+ SelectProps,
41
+ SelectSingleProps,
42
+ SelectMultipleProps,
43
+ } from "./Select";
38
44
 
39
45
  export { Textarea } from "./Textarea";
40
46
  export type { TextareaProps } from "./Textarea";
@@ -1,7 +1,9 @@
1
+ import { useState } from "react";
1
2
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { View, Text, StyleSheet } from "react-native";
3
+ import { Pressable, View, Text, StyleSheet } from "react-native";
3
4
  import { colors } from "../theme";
4
5
  import { Icon } from "./Icon";
6
+ import { MenuIcon } from "./Menu";
5
7
  import {
6
8
  getIconsByGroup,
7
9
  iconGroupNames,
@@ -140,6 +142,30 @@ export const Social: Story = {
140
142
  ),
141
143
  };
142
144
 
145
+ function AnimatedMenuPreview() {
146
+ const [open, setOpen] = useState(false);
147
+
148
+ return (
149
+ <View style={styles.menuPreview}>
150
+ <Pressable
151
+ accessibilityLabel={open ? "Close menu" : "Open menu"}
152
+ accessibilityRole="button"
153
+ accessibilityState={{ expanded: open }}
154
+ onPress={() => setOpen((current) => !current)}
155
+ style={styles.menuButton}
156
+ >
157
+ <MenuIcon open={open} size={24} />
158
+ </Pressable>
159
+ <Text style={styles.label}>Click to {open ? "close" : "open"}</Text>
160
+ </View>
161
+ );
162
+ }
163
+
164
+ export const AnimatedMenu: Story = {
165
+ name: "Animated / Menu",
166
+ render: () => <AnimatedMenuPreview />,
167
+ };
168
+
143
169
  const styles = StyleSheet.create({
144
170
  sections: {
145
171
  gap: 32,
@@ -171,4 +197,16 @@ const styles = StyleSheet.create({
171
197
  fontSize: 12,
172
198
  fontFamily: "Noto Sans Armenian",
173
199
  },
200
+ menuPreview: {
201
+ alignItems: "center",
202
+ gap: 12,
203
+ },
204
+ menuButton: {
205
+ width: 48,
206
+ height: 48,
207
+ alignItems: "center",
208
+ justifyContent: "center",
209
+ borderRadius: 24,
210
+ backgroundColor: colors.primary,
211
+ },
174
212
  });
@@ -0,0 +1,30 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { MENU_CLOSE_PATH, MENU_PATH } from "./menuPath";
5
+
6
+ export { MENU_CLOSE_PATH, MENU_PATH } from "./menuPath";
7
+
8
+ export interface MenuIconProps extends IconProps {
9
+ open?: boolean;
10
+ }
11
+
12
+ export function MenuIcon({
13
+ size = 19,
14
+ color = colors.white,
15
+ strokeWidth = 2,
16
+ open = false,
17
+ }: MenuIconProps) {
18
+ return (
19
+ <Svg width={size} height={size} viewBox="0 0 19 19" fill="none">
20
+ <Path
21
+ d={open ? MENU_CLOSE_PATH : MENU_PATH}
22
+ stroke={color}
23
+ strokeWidth={strokeWidth}
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ transform={open ? "translate(1.5 1.5)" : "translate(0 2.5)"}
27
+ />
28
+ </Svg>
29
+ );
30
+ }
@@ -0,0 +1,58 @@
1
+ import { colors } from "../../theme";
2
+ import type { MenuIconProps } from "./MenuIcon";
3
+ import { MENU_CLOSE_PATH, MENU_PATH } from "./menuPath";
4
+
5
+ export function MenuIcon({
6
+ size = 19,
7
+ color = colors.white,
8
+ strokeWidth = 2,
9
+ open = false,
10
+ }: MenuIconProps) {
11
+ const transition = "opacity 180ms ease, transform 180ms ease";
12
+
13
+ return (
14
+ <svg
15
+ width={size}
16
+ height={size}
17
+ viewBox="0 0 19 19"
18
+ fill="none"
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ aria-hidden
21
+ >
22
+ <g transform="translate(0 2.5)">
23
+ <path
24
+ d={MENU_PATH}
25
+ stroke={color}
26
+ strokeWidth={strokeWidth}
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ style={{
30
+ opacity: open ? 0 : 1,
31
+ transform: open ? "rotate(45deg) scale(0.75)" : "rotate(0deg) scale(1)",
32
+ transformBox: "fill-box",
33
+ transformOrigin: "center",
34
+ transition,
35
+ }}
36
+ />
37
+ </g>
38
+ <g transform="translate(1.5 1.5)">
39
+ <path
40
+ d={MENU_CLOSE_PATH}
41
+ stroke={color}
42
+ strokeWidth={strokeWidth}
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ style={{
46
+ opacity: open ? 1 : 0,
47
+ transform: open ? "rotate(0deg) scale(1)" : "rotate(-45deg) scale(0.75)",
48
+ transformBox: "fill-box",
49
+ transformOrigin: "center",
50
+ transition,
51
+ }}
52
+ />
53
+ </g>
54
+ </svg>
55
+ );
56
+ }
57
+
58
+ export { MENU_CLOSE_PATH, MENU_PATH } from "./menuPath";
@@ -0,0 +1,2 @@
1
+ export { MenuIcon, MENU_CLOSE_PATH, MENU_PATH } from "./MenuIcon";
2
+ export type { MenuIconProps } from "./MenuIcon";
@@ -0,0 +1,3 @@
1
+ export const MENU_PATH = "M8 12.6667H17.3333M1 6.83332H17.3333M8 0.999985H17.3333";
2
+
3
+ export const MENU_CLOSE_PATH = "M15 1L1 15M1 1L15 15";
@@ -28,6 +28,8 @@ export { LayoutGridIcon } from "./LayoutGrid";
28
28
  export { LinkedInIcon } from "./LinkedIn";
29
29
  export { LogOutIcon } from "./LogOut";
30
30
  export { MailIcon } from "./Mail";
31
+ export { MenuIcon } from "./Menu";
32
+ export type { MenuIconProps } from "./Menu";
31
33
  export { NavigateIcon } from "./Navigate";
32
34
  export { PhoneIcon } from "./Phone";
33
35
  export { PlusIcon } from "./Plus";
@@ -21,6 +21,7 @@ import { LayoutGridIcon } from "./LayoutGrid";
21
21
  import { LinkedInIcon } from "./LinkedIn";
22
22
  import { LogOutIcon } from "./LogOut";
23
23
  import { MailIcon } from "./Mail";
24
+ import { MenuIcon } from "./Menu";
24
25
  import { MinusIcon } from "./Minus";
25
26
  import { NavigateIcon } from "./Navigate";
26
27
  import { PhoneIcon } from "./Phone";
@@ -60,6 +61,7 @@ export const icons = {
60
61
  linkedin: LinkedInIcon,
61
62
  logOut: LogOutIcon,
62
63
  mail: MailIcon,
64
+ menu: MenuIcon,
63
65
  minus: MinusIcon,
64
66
  navigate: NavigateIcon,
65
67
  phone: PhoneIcon,
@@ -80,7 +82,7 @@ export type IconName = keyof typeof icons;
80
82
  export const iconNames = Object.keys(icons) as IconName[];
81
83
 
82
84
  export const iconGroups = {
83
- navigation: ["arrowRight", "chevronDown", "chevronLeft", "home", "navigate"],
85
+ navigation: ["arrowRight", "chevronDown", "chevronLeft", "home", "menu", "navigate"],
84
86
  actions: [
85
87
  "show",
86
88
  "bell",
package/src/index.ts CHANGED
@@ -36,8 +36,10 @@ export type {
36
36
  DatePickerSingleProps,
37
37
  DatePickerRangeProps,
38
38
  DateRange,
39
+ InputIcon,
39
40
  InputProps,
40
41
  InputSize,
42
+ SelectIcon,
41
43
  SelectOption,
42
44
  SelectProps,
43
45
  SelectSingleProps,
@@ -87,6 +89,7 @@ export {
87
89
  LinkedInIcon,
88
90
  LogOutIcon,
89
91
  MailIcon,
92
+ MenuIcon,
90
93
  MinusIcon,
91
94
  NavigateIcon,
92
95
  PhoneIcon,
@@ -107,6 +110,7 @@ export type {
107
110
  IconGroup,
108
111
  IconComponent,
109
112
  IconByNameProps,
113
+ MenuIconProps,
110
114
  StarIconProps,
111
115
  } from "./icons";
112
116
 
@@ -0,0 +1,25 @@
1
+ import { cloneElement, isValidElement, type ReactNode } from "react";
2
+ import type { IconComponent, IconProps } from "../icons";
3
+
4
+ export type StatefulIcon = ReactNode | IconComponent;
5
+
6
+ export function renderStatefulIcon(
7
+ icon: StatefulIcon | undefined,
8
+ FallbackIcon: IconComponent,
9
+ props: Pick<IconProps, "color" | "size">,
10
+ ) {
11
+ if (icon == null) {
12
+ return <FallbackIcon {...props} />;
13
+ }
14
+
15
+ if (typeof icon === "function") {
16
+ const Icon = icon as IconComponent;
17
+ return <Icon {...props} />;
18
+ }
19
+
20
+ if (isValidElement<IconProps>(icon)) {
21
+ return cloneElement(icon, props);
22
+ }
23
+
24
+ return icon;
25
+ }