@ecohouse/ui 0.1.5 → 0.1.7

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 (57) hide show
  1. package/README.md +34 -3
  2. package/dist/index.cjs +946 -102
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +349 -157
  5. package/dist/index.d.ts +349 -157
  6. package/dist/index.js +930 -104
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/Badge/Badge.stories.tsx +52 -0
  10. package/src/components/Badge/Badge.tsx +131 -0
  11. package/src/components/Badge/index.ts +2 -0
  12. package/src/components/CommentCard/CommentCard.stories.tsx +31 -0
  13. package/src/components/CommentCard/CommentCard.tsx +209 -0
  14. package/src/components/CommentCard/index.ts +2 -0
  15. package/src/components/Counter/Counter.stories.tsx +46 -0
  16. package/src/components/Counter/Counter.tsx +135 -0
  17. package/src/components/Counter/index.ts +2 -0
  18. package/src/components/RatingInput/RatingInput.stories.tsx +41 -0
  19. package/src/components/RatingInput/RatingInput.tsx +168 -0
  20. package/src/components/RatingInput/index.ts +2 -0
  21. package/src/components/SegmentedToggle/SegmentedToggle.stories.tsx +63 -0
  22. package/src/components/SegmentedToggle/SegmentedToggle.tsx +221 -0
  23. package/src/components/SegmentedToggle/index.ts +2 -0
  24. package/src/components/StatCard/StatCard.stories.tsx +20 -0
  25. package/src/components/StatCard/StatCard.tsx +61 -0
  26. package/src/components/StatCard/index.ts +2 -0
  27. package/src/components/index.ts +18 -0
  28. package/src/icons/Minus/MinusIcon.tsx +20 -0
  29. package/src/icons/Minus/MinusIcon.web.tsx +19 -0
  30. package/src/icons/Minus/index.ts +1 -0
  31. package/src/icons/Minus/minusPath.ts +1 -0
  32. package/src/icons/Plus/PlusIcon.tsx +20 -0
  33. package/src/icons/Plus/PlusIcon.web.tsx +19 -0
  34. package/src/icons/Plus/index.ts +1 -0
  35. package/src/icons/Plus/plusPath.ts +1 -0
  36. package/src/icons/Sidebar/SidebarIcon.tsx +21 -0
  37. package/src/icons/Sidebar/SidebarIcon.web.tsx +27 -0
  38. package/src/icons/Sidebar/index.ts +1 -0
  39. package/src/icons/Sidebar/sidebarPath.ts +3 -0
  40. package/src/icons/Star/StarIcon.tsx +35 -0
  41. package/src/icons/Star/StarIcon.web.tsx +41 -0
  42. package/src/icons/Star/index.ts +2 -0
  43. package/src/icons/Star/starPath.ts +3 -0
  44. package/src/icons/StarFilled/StarFilledIcon.tsx +28 -0
  45. package/src/icons/StarFilled/StarFilledIcon.web.tsx +34 -0
  46. package/src/icons/StarFilled/index.ts +1 -0
  47. package/src/icons/UsersAlt/UsersAltIcon.tsx +14 -0
  48. package/src/icons/UsersAlt/UsersAltIcon.web.tsx +13 -0
  49. package/src/icons/UsersAlt/index.ts +1 -0
  50. package/src/icons/UsersAlt/usersAltPath.ts +2 -0
  51. package/src/icons/index.ts +7 -0
  52. package/src/icons/registry.ts +18 -2
  53. package/src/index.ts +34 -1
  54. package/src/theme/colors.test.ts +6 -0
  55. package/src/theme/colors.ts +6 -0
  56. package/src/theme/index.ts +6 -0
  57. package/src/theme/typography.ts +77 -0
@@ -0,0 +1,168 @@
1
+ import { forwardRef, useMemo, useRef, useState, type ComponentRef } from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type PressableProps,
8
+ type StyleProp,
9
+ type TextStyle,
10
+ type ViewProps,
11
+ type ViewStyle,
12
+ } from "react-native";
13
+ import { StarFilledIcon, StarIcon } from "../../icons";
14
+ import { colors, ratingTypography } from "../../theme";
15
+ import { mergeRefs } from "../../utils/mergeRefs";
16
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
17
+
18
+ const STAR_COUNT = 5;
19
+ const BASE_ITEM_SIZE = 16;
20
+ const ICON_WIDTH_RATIO = 12.57 / BASE_ITEM_SIZE;
21
+ const ICON_HEIGHT_RATIO = 11.98 / BASE_ITEM_SIZE;
22
+ const STAR_GAP_RATIO = 2 / BASE_ITEM_SIZE;
23
+
24
+ export interface RatingInputProps extends Omit<ViewProps, "style" | "children"> {
25
+ value?: number;
26
+ defaultValue?: number;
27
+ onValueChange?: (value: number) => void;
28
+ showValue?: boolean;
29
+ precision?: number;
30
+ /** Star item size. Glyph dimensions and default spacing scale with it. */
31
+ size?: number;
32
+ /** Overrides the proportional spacing between star items. */
33
+ starGap?: number;
34
+ readOnly?: boolean;
35
+ disabled?: boolean;
36
+ style?: StyleProp<ViewStyle>;
37
+ valueStyle?: StyleProp<TextStyle>;
38
+ className?: string;
39
+ hitSlop?: PressableProps["hitSlop"];
40
+ accessibilityLabel?: string;
41
+ }
42
+
43
+ function clamp(value: number) {
44
+ return Math.max(0, Math.min(value, STAR_COUNT));
45
+ }
46
+
47
+ export const RatingInput = forwardRef<ComponentRef<typeof View>, RatingInputProps>(
48
+ function RatingInput(
49
+ {
50
+ value,
51
+ defaultValue = 0,
52
+ onValueChange,
53
+ showValue = true,
54
+ precision = 1,
55
+ size = 20,
56
+ starGap,
57
+ readOnly = false,
58
+ disabled = false,
59
+ style,
60
+ valueStyle,
61
+ className,
62
+ hitSlop = 6,
63
+ accessibilityLabel,
64
+ ...rest
65
+ },
66
+ forwardedRef,
67
+ ) {
68
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
69
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
70
+ const isControlled = value !== undefined;
71
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => clamp(defaultValue));
72
+ const selectedValue = clamp(isControlled ? value : uncontrolledValue);
73
+ const selectedStarCount = Math.round(selectedValue);
74
+ const iconWidth = size * ICON_WIDTH_RATIO;
75
+ const iconHeight = size * ICON_HEIGHT_RATIO;
76
+ const resolvedStarGap = starGap ?? size * STAR_GAP_RATIO;
77
+ const resolvedAccessibilityLabel =
78
+ accessibilityLabel ?? (readOnly ? `${selectedValue} out of ${STAR_COUNT} stars` : "Rating");
79
+
80
+ useApplyWebClassName(containerRef, className);
81
+
82
+ const selectValue = (nextValue: number) => {
83
+ if (disabled || nextValue === selectedValue) return;
84
+ if (!isControlled) setUncontrolledValue(nextValue);
85
+ onValueChange?.(nextValue);
86
+ };
87
+
88
+ return (
89
+ <View
90
+ {...rest}
91
+ ref={setContainerRef}
92
+ style={[styles.root, disabled && styles.disabled, style]}
93
+ >
94
+ {showValue ? (
95
+ <Text numberOfLines={1} style={[styles.value, valueStyle]}>
96
+ {selectedValue.toFixed(precision)}
97
+ </Text>
98
+ ) : null}
99
+
100
+ <View
101
+ accessibilityRole={readOnly ? "image" : "radiogroup"}
102
+ accessibilityLabel={resolvedAccessibilityLabel}
103
+ accessibilityState={{ disabled }}
104
+ style={[styles.stars, { gap: resolvedStarGap }]}
105
+ >
106
+ {Array.from({ length: STAR_COUNT }, (_, index) => {
107
+ const starValue = index + 1;
108
+ const isSelected = starValue <= selectedStarCount;
109
+ const icon = isSelected ? (
110
+ <StarFilledIcon size={iconWidth} height={iconHeight} />
111
+ ) : (
112
+ <StarIcon size={iconWidth} height={iconHeight} />
113
+ );
114
+ const itemStyle = [styles.starItem, { width: size, height: size }];
115
+
116
+ if (readOnly) {
117
+ return (
118
+ <View key={starValue} style={itemStyle}>
119
+ {icon}
120
+ </View>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <Pressable
126
+ key={starValue}
127
+ disabled={disabled}
128
+ hitSlop={hitSlop}
129
+ onPress={() => selectValue(starValue)}
130
+ accessibilityRole="radio"
131
+ accessibilityLabel={`${starValue} star${starValue === 1 ? "" : "s"}`}
132
+ accessibilityState={{ checked: isSelected, disabled }}
133
+ style={itemStyle}
134
+ >
135
+ {icon}
136
+ </Pressable>
137
+ );
138
+ })}
139
+ </View>
140
+ </View>
141
+ );
142
+ },
143
+ );
144
+
145
+ const styles = StyleSheet.create({
146
+ root: {
147
+ flexDirection: "row",
148
+ alignItems: "center",
149
+ flexWrap: "nowrap",
150
+ alignSelf: "flex-start",
151
+ gap: 16,
152
+ },
153
+ stars: {
154
+ flexDirection: "row",
155
+ alignItems: "center",
156
+ },
157
+ starItem: {
158
+ alignItems: "center",
159
+ justifyContent: "center",
160
+ },
161
+ value: {
162
+ ...ratingTypography,
163
+ color: colors.white,
164
+ },
165
+ disabled: {
166
+ opacity: 0.5,
167
+ },
168
+ });
@@ -0,0 +1,2 @@
1
+ export { RatingInput } from "./RatingInput";
2
+ export type { RatingInputProps } from "./RatingInput";
@@ -0,0 +1,63 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { LayoutGridIcon } from "../../icons";
5
+ import { SegmentedToggle } from "./SegmentedToggle";
6
+
7
+ const optionsWithIcons = [
8
+ { value: "grid", label: "Label", icon: LayoutGridIcon },
9
+ { value: "list", label: "Label", icon: LayoutGridIcon },
10
+ ] as const;
11
+
12
+ const optionsWithoutIcons = [
13
+ { value: "first", label: "Label" },
14
+ { value: "second", label: "Label" },
15
+ ] as const;
16
+
17
+ const meta = {
18
+ title: "Components/SegmentedToggle",
19
+ component: SegmentedToggle,
20
+ args: {
21
+ options: optionsWithIcons,
22
+ defaultValue: "grid",
23
+ },
24
+ parameters: {
25
+ backgrounds: { default: "black" },
26
+ },
27
+ } satisfies Meta<typeof SegmentedToggle>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const WithIcons: Story = {};
33
+
34
+ export const WithoutIcons: Story = {
35
+ args: { options: optionsWithoutIcons, defaultValue: "first" },
36
+ };
37
+
38
+ export const ContentWidth: Story = {
39
+ args: { fullWidth: false },
40
+ };
41
+
42
+ export const In400PixelContainer: Story = {
43
+ render: () => (
44
+ <View style={storyStyles.container}>
45
+ <SegmentedToggle options={optionsWithIcons} defaultValue="grid" />
46
+ </View>
47
+ ),
48
+ };
49
+
50
+ function ControlledSegmentedToggle() {
51
+ const [value, setValue] = useState("grid");
52
+ return <SegmentedToggle options={optionsWithIcons} value={value} onValueChange={setValue} />;
53
+ }
54
+
55
+ export const Controlled: Story = {
56
+ render: () => <ControlledSegmentedToggle />,
57
+ };
58
+
59
+ const storyStyles = StyleSheet.create({
60
+ container: {
61
+ width: 400,
62
+ },
63
+ });
@@ -0,0 +1,221 @@
1
+ import { forwardRef, useEffect, useMemo, useRef, useState, type ComponentRef } from "react";
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type PressableProps,
10
+ type StyleProp,
11
+ type ViewProps,
12
+ type ViewStyle,
13
+ } from "react-native";
14
+ import type { IconComponent } from "../../icons";
15
+ import { colors, segmentedToggleTypography } from "../../theme";
16
+ import { mergeRefs } from "../../utils/mergeRefs";
17
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
18
+
19
+ export interface SegmentedToggleOption {
20
+ value: string;
21
+ label: string;
22
+ icon?: IconComponent;
23
+ }
24
+
25
+ export interface SegmentedToggleProps extends Omit<ViewProps, "style" | "children"> {
26
+ options: readonly [SegmentedToggleOption, SegmentedToggleOption];
27
+ value?: string;
28
+ defaultValue?: string;
29
+ onValueChange?: (value: string) => void;
30
+ fullWidth?: boolean;
31
+ disabled?: boolean;
32
+ style?: StyleProp<ViewStyle>;
33
+ className?: string;
34
+ hitSlop?: PressableProps["hitSlop"];
35
+ }
36
+
37
+ const ICON_SIZE = 20;
38
+ const ANIMATION_DURATION = 200;
39
+
40
+ export const SegmentedToggle = forwardRef<ComponentRef<typeof View>, SegmentedToggleProps>(
41
+ function SegmentedToggle(
42
+ {
43
+ options,
44
+ value,
45
+ defaultValue = options[0].value,
46
+ onValueChange,
47
+ fullWidth = true,
48
+ disabled = false,
49
+ style,
50
+ className,
51
+ hitSlop,
52
+ onLayout,
53
+ ...rest
54
+ },
55
+ forwardedRef,
56
+ ) {
57
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
58
+ const isControlled = value !== undefined;
59
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
60
+ const selectedValue = isControlled ? value : uncontrolledValue;
61
+ const selectedIndex = Math.max(
62
+ 0,
63
+ options.findIndex(({ value: optionValue }) => optionValue === selectedValue),
64
+ );
65
+ const resolvedClassName = className?.trim() || undefined;
66
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
67
+ const progress = useRef(new Animated.Value(selectedIndex)).current;
68
+ const [contentWidths, setContentWidths] = useState<[number, number]>([0, 0]);
69
+ const [containerWidth, setContainerWidth] = useState(0);
70
+ const availableOptionWidth = Math.max(0, (containerWidth - 12) / 2);
71
+ const widestContentWidth = Math.max(...contentWidths);
72
+ const contentBasedOptionWidth = widestContentWidth > 0 ? widestContentWidth + 24 * 2 : 0;
73
+ const optionWidth = fullWidth ? availableOptionWidth : contentBasedOptionWidth;
74
+ const indicatorTranslateX = progress.interpolate({
75
+ inputRange: [0, 1],
76
+ outputRange: [0, optionWidth + 4],
77
+ });
78
+
79
+ useApplyWebClassName(containerRef, resolvedClassName);
80
+
81
+ useEffect(() => {
82
+ const animation = Animated.timing(progress, {
83
+ toValue: selectedIndex,
84
+ duration: ANIMATION_DURATION,
85
+ easing: Easing.out(Easing.cubic),
86
+ useNativeDriver: true,
87
+ });
88
+
89
+ animation.start();
90
+ return () => animation.stop();
91
+ }, [progress, selectedIndex]);
92
+
93
+ const selectOption = (nextValue: string) => {
94
+ if (disabled || nextValue === selectedValue) return;
95
+ if (!isControlled) setUncontrolledValue(nextValue);
96
+ onValueChange?.(nextValue);
97
+ };
98
+
99
+ return (
100
+ <View
101
+ {...rest}
102
+ ref={setContainerRef}
103
+ onLayout={(event) => {
104
+ const nextWidth = event.nativeEvent.layout.width;
105
+ setContainerWidth((currentWidth) =>
106
+ currentWidth === nextWidth ? currentWidth : nextWidth,
107
+ );
108
+ onLayout?.(event);
109
+ }}
110
+ style={[
111
+ styles.base,
112
+ fullWidth ? styles.fullWidth : styles.contentWidth,
113
+ disabled && styles.disabled,
114
+ style,
115
+ ]}
116
+ accessibilityRole="radiogroup"
117
+ >
118
+ {optionWidth > 0 ? (
119
+ <Animated.View
120
+ pointerEvents="none"
121
+ style={[
122
+ styles.activeIndicator,
123
+ { width: optionWidth, transform: [{ translateX: indicatorTranslateX }] },
124
+ ]}
125
+ />
126
+ ) : null}
127
+ {options.map(({ value: optionValue, label, icon: Icon }, index) => {
128
+ const selected = optionValue === selectedValue;
129
+ const iconColor = selected ? colors.primary : colors.whiteAlpha20;
130
+ const labelColor = selected ? colors.whiteAlpha86 : colors.whiteAlpha40;
131
+
132
+ return (
133
+ <Pressable
134
+ key={optionValue}
135
+ onPress={() => selectOption(optionValue)}
136
+ disabled={disabled}
137
+ hitSlop={hitSlop}
138
+ accessibilityRole="radio"
139
+ accessibilityLabel={label}
140
+ accessibilityState={{ selected, disabled }}
141
+ style={[
142
+ styles.option,
143
+ fullWidth ? styles.optionFullWidth : optionWidth > 0 && { minWidth: optionWidth },
144
+ ]}
145
+ >
146
+ <View
147
+ onLayout={(event) => {
148
+ const nextWidth = event.nativeEvent.layout.width;
149
+ setContentWidths((currentWidths) => {
150
+ if (currentWidths[index] === nextWidth) return currentWidths;
151
+ const nextWidths: [number, number] = [currentWidths[0], currentWidths[1]];
152
+ nextWidths[index] = nextWidth;
153
+ return nextWidths;
154
+ });
155
+ }}
156
+ style={styles.optionContent}
157
+ >
158
+ {Icon ? <Icon size={ICON_SIZE} color={iconColor} /> : null}
159
+ <Text numberOfLines={1} style={[styles.label, { color: labelColor }]}>
160
+ {label}
161
+ </Text>
162
+ </View>
163
+ </Pressable>
164
+ );
165
+ })}
166
+ </View>
167
+ );
168
+ },
169
+ );
170
+
171
+ const styles = StyleSheet.create({
172
+ base: {
173
+ height: 44,
174
+ flexDirection: "row",
175
+ padding: 4,
176
+ gap: 4,
177
+ borderRadius: 60,
178
+ backgroundColor: colors.grey700,
179
+ overflow: "hidden",
180
+ },
181
+ fullWidth: {
182
+ width: "100%",
183
+ alignSelf: "stretch",
184
+ },
185
+ contentWidth: {
186
+ alignSelf: "flex-start",
187
+ },
188
+ disabled: {
189
+ opacity: 0.6,
190
+ },
191
+ option: {
192
+ zIndex: 1,
193
+ flexDirection: "row",
194
+ alignItems: "center",
195
+ justifyContent: "center",
196
+ paddingHorizontal: 24,
197
+ borderRadius: 60,
198
+ },
199
+ optionContent: {
200
+ maxWidth: "100%",
201
+ flexDirection: "row",
202
+ alignItems: "center",
203
+ gap: 12,
204
+ },
205
+ optionFullWidth: {
206
+ flex: 1,
207
+ minWidth: 0,
208
+ },
209
+ activeIndicator: {
210
+ position: "absolute",
211
+ top: 4,
212
+ left: 4,
213
+ height: 36,
214
+ backgroundColor: colors.grey600,
215
+ borderRadius: 60,
216
+ },
217
+ label: {
218
+ ...segmentedToggleTypography,
219
+ flexShrink: 1,
220
+ },
221
+ });
@@ -0,0 +1,2 @@
1
+ export { SegmentedToggle } from "./SegmentedToggle";
2
+ export type { SegmentedToggleOption, SegmentedToggleProps } from "./SegmentedToggle";
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { UsersAltIcon } from "../../icons";
3
+ import { StatCard } from "./StatCard";
4
+
5
+ const meta = {
6
+ title: "Components/StatCard",
7
+ component: StatCard,
8
+ args: {
9
+ label: "Label",
10
+ icon: UsersAltIcon,
11
+ },
12
+ parameters: {
13
+ backgrounds: { default: "black" },
14
+ },
15
+ } satisfies Meta<typeof StatCard>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {};
@@ -0,0 +1,61 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type ViewProps,
8
+ type ViewStyle,
9
+ } from "react-native";
10
+ import type { IconComponent } from "../../icons";
11
+ import { colors, statCardTypography } from "../../theme";
12
+ import { mergeRefs } from "../../utils/mergeRefs";
13
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
14
+
15
+ export interface StatCardProps extends Omit<ViewProps, "style" | "children"> {
16
+ label: string;
17
+ icon: IconComponent;
18
+ style?: StyleProp<ViewStyle>;
19
+ className?: string;
20
+ }
21
+
22
+ const ICON_SIZE = 28;
23
+
24
+ export const StatCard = forwardRef<ComponentRef<typeof View>, StatCardProps>(function StatCard(
25
+ { label, icon: Icon, style, className, accessibilityLabel, ...rest },
26
+ forwardedRef,
27
+ ) {
28
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
29
+ const resolvedClassName = className?.trim() || undefined;
30
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
31
+
32
+ useApplyWebClassName(containerRef, resolvedClassName);
33
+
34
+ return (
35
+ <View
36
+ {...rest}
37
+ ref={setContainerRef}
38
+ style={[styles.base, style]}
39
+ accessibilityRole="text"
40
+ accessibilityLabel={accessibilityLabel ?? label}
41
+ >
42
+ <Icon size={ICON_SIZE} color={colors.white} />
43
+ <Text style={styles.label}>{label}</Text>
44
+ </View>
45
+ );
46
+ });
47
+
48
+ const styles = StyleSheet.create({
49
+ base: {
50
+ width: 204.5,
51
+ minHeight: 114,
52
+ padding: 24,
53
+ gap: 16,
54
+ borderRadius: 20,
55
+ backgroundColor: colors.grey750,
56
+ },
57
+ label: {
58
+ ...statCardTypography,
59
+ color: colors.whiteAlpha86,
60
+ },
61
+ });
@@ -0,0 +1,2 @@
1
+ export { StatCard } from "./StatCard";
2
+ export type { StatCardProps } from "./StatCard";
@@ -1,6 +1,18 @@
1
1
  export { Avatar } from "./Avatar";
2
2
  export type { AvatarProps, AvatarMenuItem } from "./Avatar";
3
3
 
4
+ export { Badge } from "./Badge";
5
+ export type { BadgeProps, BadgeVariant } from "./Badge";
6
+
7
+ export { Counter } from "./Counter";
8
+ export type { CounterProps } from "./Counter";
9
+
10
+ export { SegmentedToggle } from "./SegmentedToggle";
11
+ export type { SegmentedToggleOption, SegmentedToggleProps } from "./SegmentedToggle";
12
+
13
+ export { StatCard } from "./StatCard";
14
+ export type { StatCardProps } from "./StatCard";
15
+
4
16
  export { Button } from "./Button";
5
17
  export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button";
6
18
 
@@ -26,3 +38,9 @@ export type { TextareaProps } from "./Textarea";
26
38
 
27
39
  export { Toggle } from "./Toggle";
28
40
  export type { ToggleProps } from "./Toggle";
41
+
42
+ export { RatingInput } from "./RatingInput";
43
+ export type { RatingInputProps } from "./RatingInput";
44
+
45
+ export { CommentCard } from "./CommentCard";
46
+ export type { CommentCardProps } from "./CommentCard";
@@ -0,0 +1,20 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { MINUS_PATH } from "./minusPath";
5
+
6
+ export { MINUS_PATH } from "./minusPath";
7
+
8
+ export function MinusIcon({ size = 16, color = colors.white, strokeWidth = 2 }: IconProps) {
9
+ return (
10
+ <Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
11
+ <Path
12
+ d={MINUS_PATH}
13
+ stroke={color}
14
+ strokeWidth={strokeWidth}
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ />
18
+ </Svg>
19
+ );
20
+ }
@@ -0,0 +1,19 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { MINUS_PATH } from "./minusPath";
4
+
5
+ export { MINUS_PATH } from "./minusPath";
6
+
7
+ export function MinusIcon({ size = 16, color = colors.white, strokeWidth = 2 }: IconProps) {
8
+ return (
9
+ <svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden>
10
+ <path
11
+ d={MINUS_PATH}
12
+ stroke={color}
13
+ strokeWidth={strokeWidth}
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ />
17
+ </svg>
18
+ );
19
+ }
@@ -0,0 +1 @@
1
+ export { MinusIcon, MINUS_PATH } from "./MinusIcon";
@@ -0,0 +1 @@
1
+ export const MINUS_PATH = "M3.33337 8H12.6667";
@@ -0,0 +1,20 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { PLUS_PATH } from "./plusPath";
5
+
6
+ export { PLUS_PATH } from "./plusPath";
7
+
8
+ export function PlusIcon({ size = 16, color = colors.white, strokeWidth = 2 }: IconProps) {
9
+ return (
10
+ <Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
11
+ <Path
12
+ d={PLUS_PATH}
13
+ stroke={color}
14
+ strokeWidth={strokeWidth}
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ />
18
+ </Svg>
19
+ );
20
+ }
@@ -0,0 +1,19 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { PLUS_PATH } from "./plusPath";
4
+
5
+ export { PLUS_PATH } from "./plusPath";
6
+
7
+ export function PlusIcon({ size = 16, color = colors.white, strokeWidth = 2 }: IconProps) {
8
+ return (
9
+ <svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden>
10
+ <path
11
+ d={PLUS_PATH}
12
+ stroke={color}
13
+ strokeWidth={strokeWidth}
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ />
17
+ </svg>
18
+ );
19
+ }
@@ -0,0 +1 @@
1
+ export { PlusIcon, PLUS_PATH } from "./PlusIcon";
@@ -0,0 +1 @@
1
+ export const PLUS_PATH = "M8.00004 3.33325V12.6666M3.33337 7.99992H12.6667";
@@ -0,0 +1,21 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { SIDEBAR_PATH } from "./sidebarPath";
3
+ import { colors } from "../../theme";
4
+ import type { IconProps } from "../types";
5
+
6
+ export { SIDEBAR_PATH } from "./sidebarPath";
7
+
8
+ /** Native — react-native-svg (peer dependency). */
9
+ export function SidebarIcon({ size = 20, color = colors.primary, strokeWidth = 2 }: IconProps) {
10
+ return (
11
+ <Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
12
+ <Path
13
+ d={SIDEBAR_PATH}
14
+ stroke={color}
15
+ strokeWidth={strokeWidth}
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </Svg>
20
+ );
21
+ }