@ecohouse/ui 0.1.12 → 0.1.14

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.14",
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,
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { colors } from "./colors";
3
- import { buttonTypography } from "./typography";
3
+ import {
4
+ buttonTypography,
5
+ inputTypography,
6
+ selectTypography,
7
+ textareaTypography,
8
+ } from "./typography";
4
9
 
5
10
  describe("design tokens", () => {
6
11
  it("exposes primary and button typography used by Button", () => {
@@ -16,4 +21,17 @@ describe("design tokens", () => {
16
21
  expect(buttonTypography.lg.fontSize).toBe(14);
17
22
  expect(buttonTypography.lg.fontWeight).toBe("600");
18
23
  });
24
+
25
+ it("keeps Input, Select, and Textarea field text aligned", () => {
26
+ const fieldTypography = {
27
+ fontSize: 12,
28
+ fontWeight: "400",
29
+ letterSpacing: 0.36,
30
+ lineHeight: 12,
31
+ };
32
+
33
+ expect(inputTypography.field).toMatchObject(fieldTypography);
34
+ expect(selectTypography.field).toMatchObject(fieldTypography);
35
+ expect(textareaTypography.field).toMatchObject(fieldTypography);
36
+ });
19
37
  });
@@ -117,10 +117,10 @@ export const inputTypography = {
117
117
  },
118
118
  field: {
119
119
  fontFamily: fonts.sans,
120
- fontSize: 14,
120
+ fontSize: 12,
121
121
  fontWeight: "400" as const,
122
- lineHeight: 19,
123
- letterSpacing: 0,
122
+ lineHeight: 12,
123
+ letterSpacing: 0.36,
124
124
  },
125
125
  hint: {
126
126
  fontFamily: fonts.sans,
@@ -131,7 +131,7 @@ export const inputTypography = {
131
131
  },
132
132
  } as const;
133
133
 
134
- /** Select label / field / hint typography — Medium/Regular 12–14, letter-spacing 3%. */
134
+ /** Select label / field / hint typography — Medium/Regular 12, letter-spacing 3%. */
135
135
  export const selectTypography = {
136
136
  label: {
137
137
  fontFamily: fonts.sans,
@@ -142,10 +142,10 @@ export const selectTypography = {
142
142
  },
143
143
  field: {
144
144
  fontFamily: fonts.sans,
145
- fontSize: 14,
145
+ fontSize: 12,
146
146
  fontWeight: "400" as const,
147
- lineHeight: 19,
148
- letterSpacing: 0,
147
+ lineHeight: 12,
148
+ letterSpacing: 0.36,
149
149
  },
150
150
  hint: {
151
151
  fontFamily: fonts.sans,
@@ -221,7 +221,7 @@ export const datePickerTypography = {
221
221
  },
222
222
  } as const;
223
223
 
224
- /** Textarea label / field / hint typography — Medium/Regular 12–14, letter-spacing 3%. */
224
+ /** Textarea label / field / hint typography — Medium/Regular 12, letter-spacing 3%. */
225
225
  export const textareaTypography = {
226
226
  label: {
227
227
  fontFamily: fonts.sans,
@@ -232,10 +232,10 @@ export const textareaTypography = {
232
232
  },
233
233
  field: {
234
234
  fontFamily: fonts.sans,
235
- fontSize: 14,
235
+ fontSize: 12,
236
236
  fontWeight: "400" as const,
237
- lineHeight: 19,
238
- letterSpacing: 0,
237
+ lineHeight: 12,
238
+ letterSpacing: 0.36,
239
239
  },
240
240
  hint: {
241
241
  fontFamily: fonts.sans,
@@ -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
+ }