@ecohouse/ui 0.1.6 → 0.1.8

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 (34) hide show
  1. package/README.md +13 -12
  2. package/dist/index.cjs +699 -102
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +137 -3
  5. package/dist/index.d.ts +137 -3
  6. package/dist/index.js +690 -103
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/CommentCard/CommentCard.stories.tsx +31 -0
  10. package/src/components/CommentCard/CommentCard.tsx +209 -0
  11. package/src/components/CommentCard/index.ts +2 -0
  12. package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +52 -0
  13. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +287 -0
  14. package/src/components/LanguageSwitcher/index.ts +2 -0
  15. package/src/components/RatingInput/RatingInput.stories.tsx +41 -0
  16. package/src/components/RatingInput/RatingInput.tsx +168 -0
  17. package/src/components/RatingInput/index.ts +2 -0
  18. package/src/components/index.ts +9 -0
  19. package/src/icons/Globe/GlobeIcon.tsx +20 -0
  20. package/src/icons/Globe/GlobeIcon.web.tsx +26 -0
  21. package/src/icons/Globe/globePath.ts +3 -0
  22. package/src/icons/Globe/index.ts +2 -0
  23. package/src/icons/Star/StarIcon.tsx +35 -0
  24. package/src/icons/Star/StarIcon.web.tsx +41 -0
  25. package/src/icons/Star/index.ts +2 -0
  26. package/src/icons/Star/starPath.ts +3 -0
  27. package/src/icons/StarFilled/StarFilledIcon.tsx +28 -0
  28. package/src/icons/StarFilled/StarFilledIcon.web.tsx +34 -0
  29. package/src/icons/StarFilled/index.ts +1 -0
  30. package/src/icons/index.ts +4 -0
  31. package/src/icons/registry.ts +9 -1
  32. package/src/index.ts +22 -1
  33. package/src/theme/index.ts +3 -0
  34. package/src/theme/typography.ts +50 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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,31 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { CommentCard } from "./CommentCard";
3
+
4
+ const meta = {
5
+ title: "Components/Comment Card",
6
+ component: CommentCard,
7
+ args: {
8
+ userName: "Աննա Սարգսյան",
9
+ date: "Հուլիս 2026",
10
+ commentText:
11
+ "Աննկարագրելի գեղեցիկ և հանգիստ վայր։ QR-ով մուտքն անչափ հարմար էր, իսկ քոթեջի ներսում ամեն ինչ մտածված էր մինչև ամենափոքր մանրուքը։",
12
+ rating: 4,
13
+ imageUrl: "https://i.pravatar.cc/150?img=47",
14
+ },
15
+ parameters: {
16
+ backgrounds: { default: "black" },
17
+ },
18
+ } satisfies Meta<typeof CommentCard>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ export const Default: Story = {};
24
+
25
+ export const WithoutImage: Story = {
26
+ args: { imageUrl: undefined },
27
+ };
28
+
29
+ export const ShortComment: Story = {
30
+ args: { commentText: "Հիանալի վայր և սպասարկում։" },
31
+ };
@@ -0,0 +1,209 @@
1
+ import { forwardRef, useEffect, useMemo, useRef, useState, type ComponentRef } from "react";
2
+ import {
3
+ Image,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type TextStyle,
10
+ type ViewProps,
11
+ type ViewStyle,
12
+ } from "react-native";
13
+ import { UserIcon } from "../../icons";
14
+ import { colors, commentCardTypography } from "../../theme";
15
+ import { mergeRefs } from "../../utils/mergeRefs";
16
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
17
+ import { RatingInput } from "../RatingInput";
18
+
19
+ export interface CommentCardProps extends Omit<ViewProps, "style" | "children"> {
20
+ userName: string;
21
+ date: string;
22
+ commentText: string;
23
+ rating: number;
24
+ imageUrl?: string | null;
25
+ maxCommentLines?: number;
26
+ maxCommentLength?: number;
27
+ readMoreLabel?: string;
28
+ readLessLabel?: string;
29
+ onExpandedChange?: (expanded: boolean) => void;
30
+ style?: StyleProp<ViewStyle>;
31
+ commentStyle?: StyleProp<TextStyle>;
32
+ className?: string;
33
+ }
34
+
35
+ const CARD_WIDTH = 370;
36
+ const CARD_MIN_HEIGHT = 173;
37
+ const AVATAR_SIZE = 44;
38
+
39
+ export const CommentCard = forwardRef<ComponentRef<typeof View>, CommentCardProps>(
40
+ function CommentCard(
41
+ {
42
+ userName,
43
+ date,
44
+ commentText,
45
+ rating,
46
+ imageUrl,
47
+ maxCommentLines = 3,
48
+ maxCommentLength = 111,
49
+ readMoreLabel = "Դիտել ավելին",
50
+ readLessLabel = "Փակել",
51
+ onExpandedChange,
52
+ style,
53
+ commentStyle,
54
+ className,
55
+ ...rest
56
+ },
57
+ forwardedRef,
58
+ ) {
59
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
60
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
61
+ const [imageFailed, setImageFailed] = useState(false);
62
+ const [expanded, setExpanded] = useState(false);
63
+ const [actionHovered, setActionHovered] = useState(false);
64
+ const hasImage = Boolean(imageUrl) && !imageFailed;
65
+ const shouldShowAction = Array.from(commentText).length >= maxCommentLength;
66
+
67
+ useApplyWebClassName(containerRef, className);
68
+
69
+ useEffect(() => {
70
+ setImageFailed(false);
71
+ }, [imageUrl]);
72
+
73
+ useEffect(() => {
74
+ setExpanded(false);
75
+ setActionHovered(false);
76
+ }, [commentText, maxCommentLength, maxCommentLines]);
77
+
78
+ const toggleExpanded = () => {
79
+ const next = !expanded;
80
+ setExpanded(next);
81
+ onExpandedChange?.(next);
82
+ };
83
+
84
+ return (
85
+ <View {...rest} ref={setContainerRef} style={[styles.root, style]}>
86
+ <View style={styles.header}>
87
+ <View style={styles.avatar}>
88
+ {hasImage ? (
89
+ <Image
90
+ source={{ uri: imageUrl ?? undefined }}
91
+ style={styles.avatarImage}
92
+ onError={() => setImageFailed(true)}
93
+ />
94
+ ) : (
95
+ <UserIcon size={20} color={colors.primary} />
96
+ )}
97
+ </View>
98
+
99
+ <View style={styles.identity}>
100
+ <Text numberOfLines={1} style={styles.name}>
101
+ {userName}
102
+ </Text>
103
+ <Text numberOfLines={1} style={styles.date}>
104
+ {date}
105
+ </Text>
106
+ </View>
107
+
108
+ <RatingInput value={rating} readOnly showValue={false} size={16} />
109
+ </View>
110
+
111
+ <Text
112
+ numberOfLines={expanded ? undefined : maxCommentLines}
113
+ style={[styles.comment, commentStyle]}
114
+ >
115
+ {commentText}
116
+ </Text>
117
+
118
+ {shouldShowAction ? (
119
+ <Pressable
120
+ onPress={toggleExpanded}
121
+ accessibilityRole="button"
122
+ accessibilityState={{ expanded }}
123
+ hitSlop={6}
124
+ onHoverIn={() => setActionHovered(true)}
125
+ onHoverOut={() => setActionHovered(false)}
126
+ style={styles.action}
127
+ >
128
+ <Text
129
+ style={[
130
+ styles.actionText,
131
+ expanded && styles.closeActionText,
132
+ actionHovered &&
133
+ (expanded ? styles.closeActionTextHovered : styles.openActionTextHovered),
134
+ ]}
135
+ >
136
+ {expanded ? readLessLabel : readMoreLabel}
137
+ </Text>
138
+ </Pressable>
139
+ ) : null}
140
+ </View>
141
+ );
142
+ },
143
+ );
144
+
145
+ const styles = StyleSheet.create({
146
+ root: {
147
+ width: CARD_WIDTH,
148
+ minHeight: CARD_MIN_HEIGHT,
149
+ padding: 16,
150
+ gap: 12,
151
+ borderRadius: 12,
152
+ backgroundColor: colors.grey750,
153
+ },
154
+ header: {
155
+ flexDirection: "row",
156
+ alignItems: "center",
157
+ gap: 8,
158
+ },
159
+ avatar: {
160
+ width: AVATAR_SIZE,
161
+ height: AVATAR_SIZE,
162
+ flexShrink: 0,
163
+ alignItems: "center",
164
+ justifyContent: "center",
165
+ overflow: "hidden",
166
+ borderRadius: AVATAR_SIZE / 2,
167
+ backgroundColor: colors.grey600,
168
+ },
169
+ avatarImage: {
170
+ width: AVATAR_SIZE,
171
+ height: AVATAR_SIZE,
172
+ },
173
+ identity: {
174
+ minWidth: 0,
175
+ flex: 1,
176
+ gap: 4,
177
+ },
178
+ name: {
179
+ ...commentCardTypography.name,
180
+ color: colors.white,
181
+ },
182
+ date: {
183
+ ...commentCardTypography.date,
184
+ color: colors.grey0,
185
+ },
186
+ comment: {
187
+ ...commentCardTypography.comment,
188
+ color: colors.white,
189
+ },
190
+ action: {
191
+ alignSelf: "flex-start",
192
+ marginTop: "auto",
193
+ minHeight: 16,
194
+ justifyContent: "center",
195
+ },
196
+ actionText: {
197
+ ...commentCardTypography.action,
198
+ color: colors.white,
199
+ },
200
+ closeActionText: {
201
+ color: colors.primary,
202
+ },
203
+ openActionTextHovered: {
204
+ color: colors.primary,
205
+ },
206
+ closeActionTextHovered: {
207
+ color: colors.white,
208
+ },
209
+ });
@@ -0,0 +1,2 @@
1
+ export { CommentCard } from "./CommentCard";
2
+ export type { CommentCardProps } from "./CommentCard";
@@ -0,0 +1,52 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { View } from "react-native";
4
+ import { LanguageSwitcher } from "./LanguageSwitcher";
5
+
6
+ const meta = {
7
+ title: "Components/LanguageSwitcher",
8
+ component: LanguageSwitcher,
9
+ args: {
10
+ defaultValue: "hy",
11
+ },
12
+ parameters: {
13
+ backgrounds: { default: "black" },
14
+ },
15
+ } satisfies Meta<typeof LanguageSwitcher>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {};
21
+
22
+ export const Open: Story = {
23
+ args: { defaultOpen: true },
24
+ };
25
+
26
+ export const Disabled: Story = {
27
+ args: { disabled: true },
28
+ };
29
+
30
+ function ControlledExample() {
31
+ const [value, setValue] = useState("hy");
32
+ return <LanguageSwitcher value={value} onValueChange={setValue} />;
33
+ }
34
+
35
+ export const Controlled: Story = {
36
+ render: () => <ControlledExample />,
37
+ };
38
+
39
+ export const CustomLanguages: Story = {
40
+ render: () => (
41
+ <View style={{ alignItems: "flex-start" }}>
42
+ <LanguageSwitcher
43
+ defaultValue="fr"
44
+ options={[
45
+ { value: "fr", label: "Fra" },
46
+ { value: "de", label: "Deu" },
47
+ { value: "it", label: "Ita" },
48
+ ]}
49
+ />
50
+ </View>
51
+ ),
52
+ };
@@ -0,0 +1,287 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentRef,
9
+ } from "react";
10
+ import {
11
+ Platform,
12
+ Pressable,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ type PressableProps,
17
+ type StyleProp,
18
+ type ViewProps,
19
+ type ViewStyle,
20
+ } from "react-native";
21
+ import { ChevronDownIcon, GlobeIcon } from "../../icons";
22
+ import { colors, fonts, languageSwitcherTypography } from "../../theme";
23
+ import { mergeRefs } from "../../utils/mergeRefs";
24
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
25
+
26
+ export interface LanguageOption {
27
+ /** Stable locale identifier, such as `hy`, `en`, or `ru`. */
28
+ value: string;
29
+ /** Short label shown in the trigger and menu. */
30
+ label: string;
31
+ }
32
+
33
+ export interface LanguageSwitcherProps extends Omit<ViewProps, "style" | "children"> {
34
+ options?: readonly LanguageOption[];
35
+ value?: string;
36
+ defaultValue?: string;
37
+ onValueChange?: (value: string) => void;
38
+ open?: boolean;
39
+ defaultOpen?: boolean;
40
+ onOpenChange?: (open: boolean) => void;
41
+ disabled?: boolean;
42
+ accessibilityLabel?: string;
43
+ style?: StyleProp<ViewStyle>;
44
+ className?: string;
45
+ hitSlop?: PressableProps["hitSlop"];
46
+ }
47
+
48
+ export const defaultLanguageOptions = [
49
+ { value: "hy", label: "Հայ" },
50
+ { value: "en", label: "Eng" },
51
+ { value: "ru", label: "Рус" },
52
+ ] as const satisfies readonly LanguageOption[];
53
+
54
+ const CONTROL_WIDTH = 90;
55
+ const CONTROL_HEIGHT = 35;
56
+ const ICON_SIZE = 16;
57
+
58
+ export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSwitcherProps>(
59
+ function LanguageSwitcher(
60
+ {
61
+ options = defaultLanguageOptions,
62
+ value,
63
+ defaultValue,
64
+ onValueChange,
65
+ open: openProp,
66
+ defaultOpen = false,
67
+ onOpenChange,
68
+ disabled = false,
69
+ accessibilityLabel = "Language",
70
+ style,
71
+ className,
72
+ hitSlop = 8,
73
+ ...rest
74
+ },
75
+ forwardedRef,
76
+ ) {
77
+ const wrapperRef = useRef<ComponentRef<typeof View>>(null);
78
+ const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
79
+ const setWrapperRef = useMemo(() => mergeRefs(wrapperRef, forwardedRef), [forwardedRef]);
80
+ const firstValue = options[0]?.value ?? "";
81
+ const isValueControlled = value !== undefined;
82
+ const isOpenControlled = openProp !== undefined;
83
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? firstValue);
84
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
85
+ const [triggerHovered, setTriggerHovered] = useState(false);
86
+ const [hoveredValue, setHoveredValue] = useState<string | null>(null);
87
+ const selectedValue = isValueControlled ? value : uncontrolledValue;
88
+ const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
89
+ const selectedOption =
90
+ options.find((option) => option.value === selectedValue) ?? options[0] ?? null;
91
+
92
+ useApplyWebClassName(wrapperRef, className?.trim() || undefined);
93
+
94
+ const setOpen = useCallback(
95
+ (next: boolean) => {
96
+ if (disabled || next === isOpen) return;
97
+ if (!isOpenControlled) setUncontrolledOpen(next);
98
+ onOpenChange?.(next);
99
+ },
100
+ [disabled, isOpen, isOpenControlled, onOpenChange],
101
+ );
102
+
103
+ const closeAndFocusTrigger = useCallback(() => {
104
+ setOpen(false);
105
+ const node = triggerRef.current as unknown as { focus?: () => void } | null;
106
+ node?.focus?.();
107
+ }, [setOpen]);
108
+
109
+ useEffect(() => {
110
+ if (Platform.OS !== "web" || !isOpen) return;
111
+
112
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
113
+ const wrapper = wrapperRef.current as unknown as {
114
+ contains?: (node: Node) => boolean;
115
+ } | null;
116
+ const target = event.target;
117
+ if (wrapper?.contains && target instanceof Node && !wrapper.contains(target)) {
118
+ setOpen(false);
119
+ }
120
+ };
121
+ const handleKeyDown = (event: KeyboardEvent) => {
122
+ if (event.key === "Escape") {
123
+ event.preventDefault();
124
+ closeAndFocusTrigger();
125
+ }
126
+ };
127
+
128
+ document.addEventListener("mousedown", handlePointerDown);
129
+ document.addEventListener("touchstart", handlePointerDown);
130
+ document.addEventListener("keydown", handleKeyDown);
131
+ return () => {
132
+ document.removeEventListener("mousedown", handlePointerDown);
133
+ document.removeEventListener("touchstart", handlePointerDown);
134
+ document.removeEventListener("keydown", handleKeyDown);
135
+ };
136
+ }, [closeAndFocusTrigger, isOpen, setOpen]);
137
+
138
+ const selectLanguage = (nextValue: string) => {
139
+ if (nextValue !== selectedValue) {
140
+ if (!isValueControlled) setUncontrolledValue(nextValue);
141
+ onValueChange?.(nextValue);
142
+ }
143
+ closeAndFocusTrigger();
144
+ };
145
+
146
+ return (
147
+ <View
148
+ {...rest}
149
+ ref={setWrapperRef}
150
+ style={[styles.wrapper, isOpen && styles.wrapperOpen, disabled && styles.disabled, style]}
151
+ >
152
+ <Pressable
153
+ ref={triggerRef}
154
+ onPress={() => setOpen(!isOpen)}
155
+ disabled={disabled || options.length === 0}
156
+ hitSlop={hitSlop}
157
+ accessibilityRole="button"
158
+ accessibilityLabel={accessibilityLabel}
159
+ accessibilityState={{ disabled, expanded: isOpen }}
160
+ onHoverIn={() => setTriggerHovered(true)}
161
+ onHoverOut={() => setTriggerHovered(false)}
162
+ style={({ pressed }) => [
163
+ styles.trigger,
164
+ (triggerHovered || pressed || isOpen) && styles.triggerActive,
165
+ ]}
166
+ >
167
+ <GlobeIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
168
+ <Text style={styles.triggerLabel} numberOfLines={1}>
169
+ {selectedOption?.label ?? ""}
170
+ </Text>
171
+ <View style={[styles.chevron, isOpen && styles.chevronOpen]}>
172
+ <ChevronDownIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
173
+ </View>
174
+ </Pressable>
175
+
176
+ {isOpen && options.length > 0 ? (
177
+ <View style={styles.menu} accessibilityRole="menu">
178
+ {options.map((option) => {
179
+ const selected = option.value === selectedValue;
180
+ return (
181
+ <Pressable
182
+ key={option.value}
183
+ onPress={() => selectLanguage(option.value)}
184
+ accessibilityRole="menuitem"
185
+ accessibilityLabel={option.label}
186
+ accessibilityState={{ selected }}
187
+ onHoverIn={() => setHoveredValue(option.value)}
188
+ onHoverOut={() => setHoveredValue(null)}
189
+ style={({ pressed }) => [
190
+ styles.item,
191
+ selected
192
+ ? styles.itemSelected
193
+ : (hoveredValue === option.value || pressed) && styles.itemHovered,
194
+ ]}
195
+ >
196
+ <Text style={[styles.itemLabel, selected && styles.itemLabelSelected]}>
197
+ {option.label}
198
+ </Text>
199
+ </Pressable>
200
+ );
201
+ })}
202
+ </View>
203
+ ) : null}
204
+ </View>
205
+ );
206
+ },
207
+ );
208
+
209
+ const styles = StyleSheet.create({
210
+ wrapper: {
211
+ position: "relative",
212
+ width: CONTROL_WIDTH,
213
+ alignSelf: "flex-start",
214
+ },
215
+ wrapperOpen: {
216
+ zIndex: 20,
217
+ },
218
+ disabled: {
219
+ opacity: 0.6,
220
+ },
221
+ trigger: {
222
+ width: CONTROL_WIDTH,
223
+ height: CONTROL_HEIGHT,
224
+ flexDirection: "row",
225
+ alignItems: "center",
226
+ gap: 8,
227
+ padding: 8,
228
+ borderRadius: 12,
229
+ backgroundColor: "transparent",
230
+ },
231
+ triggerActive: {
232
+ backgroundColor: colors.grey700,
233
+ },
234
+ triggerLabel: {
235
+ ...languageSwitcherTypography,
236
+ ...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
237
+ minWidth: 0,
238
+ flex: 1,
239
+ color: colors.white,
240
+ },
241
+ chevron: {
242
+ width: ICON_SIZE,
243
+ height: ICON_SIZE,
244
+ flexShrink: 0,
245
+ alignItems: "center",
246
+ justifyContent: "center",
247
+ },
248
+ chevronOpen: {
249
+ transform: [{ rotate: "180deg" }],
250
+ },
251
+ menu: {
252
+ position: "absolute",
253
+ top: CONTROL_HEIGHT + 8,
254
+ left: 0,
255
+ width: CONTROL_WIDTH,
256
+ padding: 8,
257
+ gap: 8,
258
+ borderRadius: 12,
259
+ backgroundColor: colors.grey700,
260
+ ...Platform.select({
261
+ web: { boxShadow: `0 8px 24px ${colors.black}73` },
262
+ default: { elevation: 8 },
263
+ }),
264
+ },
265
+ item: {
266
+ height: CONTROL_HEIGHT,
267
+ alignItems: "center",
268
+ justifyContent: "center",
269
+ paddingVertical: 8,
270
+ paddingHorizontal: 12,
271
+ borderRadius: 8,
272
+ },
273
+ itemHovered: {
274
+ backgroundColor: colors.whiteAlpha5,
275
+ },
276
+ itemSelected: {
277
+ backgroundColor: colors.primaryMuted,
278
+ },
279
+ itemLabel: {
280
+ ...languageSwitcherTypography,
281
+ ...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
282
+ color: colors.white,
283
+ },
284
+ itemLabelSelected: {
285
+ color: colors.primary,
286
+ },
287
+ });
@@ -0,0 +1,2 @@
1
+ export { LanguageSwitcher, defaultLanguageOptions } from "./LanguageSwitcher";
2
+ export type { LanguageOption, LanguageSwitcherProps } from "./LanguageSwitcher";
@@ -0,0 +1,41 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { RatingInput } from "./RatingInput";
4
+
5
+ const meta = {
6
+ title: "Components/Rating Input",
7
+ component: RatingInput,
8
+ args: {
9
+ defaultValue: 5,
10
+ showValue: true,
11
+ disabled: false,
12
+ },
13
+ argTypes: {
14
+ onValueChange: { action: "valueChange" },
15
+ },
16
+ parameters: {
17
+ backgrounds: { default: "black" },
18
+ },
19
+ } satisfies Meta<typeof RatingInput>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const WithValue: Story = {};
25
+
26
+ export const WithoutValue: Story = {
27
+ args: { showValue: false },
28
+ };
29
+
30
+ export const ReadOnly: Story = {
31
+ args: { defaultValue: 4, readOnly: true },
32
+ };
33
+
34
+ function ControlledRatingInput() {
35
+ const [value, setValue] = useState(3);
36
+ return <RatingInput value={value} onValueChange={setValue} />;
37
+ }
38
+
39
+ export const Controlled: Story = {
40
+ render: () => <ControlledRatingInput />,
41
+ };