@ecohouse/ui 0.1.12 → 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.12",
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)",
@@ -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";
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,
@@ -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
+ }