@ecohouse/ui 0.1.42 → 0.1.43

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.42",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -0,0 +1,64 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { fn } from "storybook/test";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+ import { colors } from "../../theme";
6
+ import { RadioButton } from "./RadioButton";
7
+
8
+ const meta = {
9
+ title: "Components/RadioButton",
10
+ component: RadioButton,
11
+ args: {
12
+ defaultValue: false,
13
+ disabled: false,
14
+ variant: "default",
15
+ onValueChange: fn(),
16
+ },
17
+ parameters: {
18
+ backgrounds: { default: "black" },
19
+ },
20
+ } satisfies Meta<typeof RadioButton>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {};
26
+
27
+ export const Active: Story = {
28
+ args: { defaultValue: true },
29
+ };
30
+
31
+ export const Disabled: Story = {
32
+ args: { disabled: true },
33
+ };
34
+
35
+ export const WithLabel: Story = {
36
+ args: { defaultValue: true, label: "Option" },
37
+ };
38
+
39
+ function ControlledGroup() {
40
+ const [value, setValue] = useState("first");
41
+
42
+ return (
43
+ <View style={styles.group} accessibilityRole="radiogroup">
44
+ {["first", "second", "third"].map((option) => (
45
+ <RadioButton key={option} value={value === option} onValueChange={() => setValue(option)}>
46
+ <Text style={styles.label}>{option}</Text>
47
+ </RadioButton>
48
+ ))}
49
+ </View>
50
+ );
51
+ }
52
+
53
+ export const Group: Story = {
54
+ render: () => <ControlledGroup />,
55
+ };
56
+
57
+ const styles = StyleSheet.create({
58
+ group: {
59
+ gap: 12,
60
+ },
61
+ label: {
62
+ color: colors.white,
63
+ },
64
+ });
@@ -0,0 +1,135 @@
1
+ import { forwardRef, useMemo, useRef, useState, type ComponentRef, type ReactNode } from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type PressableProps,
8
+ type StyleProp,
9
+ type TextStyle,
10
+ type ViewStyle,
11
+ } from "react-native";
12
+ import { colors, fonts } from "../../theme";
13
+ import { mergeRefs } from "../../utils/mergeRefs";
14
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
15
+
16
+ export type RadioButtonVariant = "default";
17
+
18
+ export interface RadioButtonProps extends Omit<
19
+ PressableProps,
20
+ "onPress" | "disabled" | "style" | "children" | "hitSlop"
21
+ > {
22
+ value?: boolean;
23
+ defaultValue?: boolean;
24
+ onValueChange?: (value: true) => void;
25
+ disabled?: boolean;
26
+ variant?: RadioButtonVariant;
27
+ label?: string;
28
+ children?: ReactNode;
29
+ style?: StyleProp<ViewStyle>;
30
+ labelStyle?: StyleProp<TextStyle>;
31
+ className?: string;
32
+ hitSlop?: PressableProps["hitSlop"];
33
+ }
34
+
35
+ const CONTROL_SIZE = 16;
36
+ const DOT_SIZE = 8;
37
+
38
+ export const RadioButton = forwardRef<ComponentRef<typeof Pressable>, RadioButtonProps>(
39
+ function RadioButton(
40
+ {
41
+ value,
42
+ defaultValue = false,
43
+ onValueChange,
44
+ disabled = false,
45
+ variant: _variant = "default",
46
+ label,
47
+ children,
48
+ style,
49
+ labelStyle,
50
+ className,
51
+ hitSlop = 8,
52
+ accessibilityLabel,
53
+ ...rest
54
+ },
55
+ forwardedRef,
56
+ ) {
57
+ const containerRef = useRef<ComponentRef<typeof Pressable>>(null);
58
+ const isControlled = typeof value === "boolean";
59
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
60
+ const isActive = isControlled ? value : uncontrolledValue;
61
+ const labelContent = children ?? label;
62
+ const resolvedAccessibilityLabel =
63
+ accessibilityLabel ?? (typeof labelContent === "string" ? labelContent : undefined);
64
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
65
+
66
+ useApplyWebClassName(containerRef, className);
67
+
68
+ const handlePress = () => {
69
+ if (disabled || isActive) return;
70
+ if (!isControlled) setUncontrolledValue(true);
71
+ onValueChange?.(true);
72
+ };
73
+
74
+ return (
75
+ <Pressable
76
+ {...rest}
77
+ ref={setContainerRef}
78
+ onPress={handlePress}
79
+ disabled={disabled}
80
+ hitSlop={hitSlop}
81
+ accessibilityRole="radio"
82
+ accessibilityLabel={resolvedAccessibilityLabel}
83
+ accessibilityState={{ selected: isActive, disabled }}
84
+ style={[styles.root, disabled && styles.disabled, style]}
85
+ >
86
+ <View style={styles.control}>{isActive ? <View style={styles.dot} /> : null}</View>
87
+
88
+ {labelContent != null && labelContent !== false ? (
89
+ typeof labelContent === "string" || typeof labelContent === "number" ? (
90
+ <Text style={[styles.label, labelStyle]}>{labelContent}</Text>
91
+ ) : (
92
+ labelContent
93
+ )
94
+ ) : null}
95
+ </Pressable>
96
+ );
97
+ },
98
+ );
99
+
100
+ const styles = StyleSheet.create({
101
+ root: {
102
+ flexDirection: "row",
103
+ alignItems: "center",
104
+ alignSelf: "flex-start",
105
+ gap: 6,
106
+ },
107
+ disabled: {
108
+ opacity: 0.5,
109
+ },
110
+ control: {
111
+ width: CONTROL_SIZE,
112
+ height: CONTROL_SIZE,
113
+ borderRadius: 20,
114
+ borderWidth: 1,
115
+ borderColor: colors.grey700,
116
+ backgroundColor: colors.dark,
117
+ alignItems: "center",
118
+ justifyContent: "center",
119
+ flexShrink: 0,
120
+ },
121
+ dot: {
122
+ width: DOT_SIZE,
123
+ height: DOT_SIZE,
124
+ borderRadius: 43,
125
+ backgroundColor: colors.primary,
126
+ },
127
+ label: {
128
+ flexShrink: 1,
129
+ textAlign: "center",
130
+ fontFamily: fonts.sans,
131
+ fontSize: 12,
132
+ fontWeight: "400",
133
+ color: colors.white,
134
+ },
135
+ });
@@ -0,0 +1,2 @@
1
+ export { RadioButton } from "./RadioButton";
2
+ export type { RadioButtonProps, RadioButtonVariant } from "./RadioButton";
@@ -118,3 +118,6 @@ export type { CommentCardProps } from "./CommentCard";
118
118
 
119
119
  export { Range } from "./Range";
120
120
  export type { RangeProps, RangeValue } from "./Range";
121
+
122
+ export { RadioButton } from "./RadioButton";
123
+ export type { RadioButtonProps, RadioButtonVariant } from "./RadioButton";
@@ -0,0 +1,27 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { MOON_STARS_MOON_PATH, MOON_STARS_SPARKLE_PATH } from "./moonStarsPaths";
5
+
6
+ export function MoonStarsIcon({ size = 15, color = colors.primary, strokeWidth = 1.5 }: IconProps) {
7
+ return (
8
+ <Svg width={size} height={size} viewBox="0 0 15 15" fill="none">
9
+ <Path
10
+ d={MOON_STARS_SPARKLE_PATH}
11
+ stroke={color}
12
+ strokeWidth={strokeWidth}
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ />
16
+ <Path
17
+ d={MOON_STARS_MOON_PATH}
18
+ stroke={color}
19
+ strokeWidth={strokeWidth}
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ />
23
+ </Svg>
24
+ );
25
+ }
26
+
27
+ export { MOON_STARS_MOON_PATH, MOON_STARS_PATHS, MOON_STARS_SPARKLE_PATH } from "./moonStarsPaths";
@@ -0,0 +1,33 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { MOON_STARS_MOON_PATH, MOON_STARS_SPARKLE_PATH } from "./moonStarsPaths";
4
+
5
+ export function MoonStarsIcon({ size = 15, color = colors.primary, strokeWidth = 1.5 }: IconProps) {
6
+ return (
7
+ <svg
8
+ width={size}
9
+ height={size}
10
+ viewBox="0 0 15 15"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ aria-hidden
14
+ >
15
+ <path
16
+ d={MOON_STARS_SPARKLE_PATH}
17
+ stroke={color}
18
+ strokeWidth={strokeWidth}
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ />
22
+ <path
23
+ d={MOON_STARS_MOON_PATH}
24
+ stroke={color}
25
+ strokeWidth={strokeWidth}
26
+ strokeLinecap="round"
27
+ strokeLinejoin="round"
28
+ />
29
+ </svg>
30
+ );
31
+ }
32
+
33
+ export { MOON_STARS_MOON_PATH, MOON_STARS_PATHS, MOON_STARS_SPARKLE_PATH } from "./moonStarsPaths";
@@ -0,0 +1,6 @@
1
+ export {
2
+ MoonStarsIcon,
3
+ MOON_STARS_MOON_PATH,
4
+ MOON_STARS_PATHS,
5
+ MOON_STARS_SPARKLE_PATH,
6
+ } from "./MoonStarsIcon";
@@ -0,0 +1,7 @@
1
+ export const MOON_STARS_SPARKLE_PATH =
2
+ "M11.4167 0.75L11.8285 1.57372C12.0055 1.92771 12.094 2.1047 12.2122 2.25808C12.3171 2.39417 12.4392 2.51618 12.5753 2.62109C12.7286 2.73932 12.9056 2.82781 13.2596 3.00481L14.0833 3.41667L13.2596 3.82853C12.9056 4.00552 12.7286 4.09402 12.5753 4.21224C12.4392 4.31715 12.3171 4.43916 12.2122 4.57526C12.094 4.72863 12.0055 4.90563 11.8285 5.25961L11.4167 6.08333L11.0048 5.25961C10.8278 4.90563 10.7393 4.72863 10.6211 4.57526C10.5162 4.43916 10.3942 4.31715 10.2581 4.21224C10.1047 4.09402 9.92771 4.00552 9.57372 3.82853L8.75 3.41667L9.57372 3.00481C9.92771 2.82781 10.1047 2.73932 10.2581 2.62109C10.3942 2.51618 10.5162 2.39417 10.6211 2.25808C10.7393 2.1047 10.8278 1.92771 11.0048 1.57372L11.4167 0.75Z";
3
+
4
+ export const MOON_STARS_MOON_PATH =
5
+ "M13.4167 8.34286C12.5427 9.876 10.893 10.9097 9.00196 10.9097C6.19739 10.9097 3.92383 8.63612 3.92383 5.83155C3.92383 3.94036 4.95766 2.29063 6.491 1.41667C3.26986 1.72208 0.75 4.43461 0.75 7.73569C0.75 11.2414 3.59195 14.0833 7.09766 14.0833C10.3986 14.0833 13.111 11.5637 13.4167 8.34286Z";
6
+
7
+ export const MOON_STARS_PATHS = [MOON_STARS_SPARKLE_PATH, MOON_STARS_MOON_PATH] as const;
@@ -80,6 +80,7 @@ export { MapCollapseIcon, MapExpandIcon } from "./MapControls";
80
80
  export { MenuIcon } from "./Menu";
81
81
  export type { MenuIconProps } from "./Menu";
82
82
  export { MenuLinesIcon } from "./MenuLines";
83
+ export { MoonStarsIcon } from "./MoonStars";
83
84
  export { MoreHorizontalIcon } from "./MoreHorizontal";
84
85
  export { NavigateIcon } from "./Navigate";
85
86
  export { PhoneIcon } from "./Phone";
@@ -68,6 +68,7 @@ import { MapCollapseIcon, MapExpandIcon } from "./MapControls";
68
68
  import { MenuIcon } from "./Menu";
69
69
  import { MenuLinesIcon } from "./MenuLines";
70
70
  import { MinusIcon } from "./Minus";
71
+ import { MoonStarsIcon } from "./MoonStars";
71
72
  import { MoreHorizontalIcon } from "./MoreHorizontal";
72
73
  import { NavigateIcon } from "./Navigate";
73
74
  import { PhoneIcon } from "./Phone";
@@ -145,6 +146,7 @@ export const icons = {
145
146
  menu: MenuIcon,
146
147
  menuLines: MenuLinesIcon,
147
148
  minus: MinusIcon,
149
+ moonStars: MoonStarsIcon,
148
150
  moreHorizontal: MoreHorizontalIcon,
149
151
  navigate: NavigateIcon,
150
152
  noSmoking: NoSmokingIcon,
@@ -235,6 +237,7 @@ export const iconGroups = {
235
237
  "star",
236
238
  "starFilled",
237
239
  "minus",
240
+ "moonStars",
238
241
  "moreHorizontal",
239
242
  "plus",
240
243
  "refresh",
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ export {
34
34
  RatingInput,
35
35
  CommentCard,
36
36
  Range,
37
+ RadioButton,
37
38
  } from "./components";
38
39
  export type {
39
40
  AvatarProps,
@@ -94,6 +95,8 @@ export type {
94
95
  CommentCardProps,
95
96
  RangeProps,
96
97
  RangeValue,
98
+ RadioButtonProps,
99
+ RadioButtonVariant,
97
100
  } from "./components";
98
101
 
99
102
  export {
@@ -194,6 +197,7 @@ export {
194
197
  MenuIcon,
195
198
  MenuLinesIcon,
196
199
  MinusIcon,
200
+ MoonStarsIcon,
197
201
  NavigateIcon,
198
202
  PhoneIcon,
199
203
  PlusIcon,