@bearmenu/ui 0.5.2 → 0.6.1
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/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
- package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
- package/dist/chunk-D5QHEHXJ.js +26 -0
- package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
- package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
- package/dist/chunk-RIDXFY6M.js +38 -0
- package/dist/chunk-UUJVXN2O.js +82 -0
- package/dist/chunk-WKMLZDUT.js +122 -0
- package/dist/chunk-XK2ZDQVE.js +122 -0
- package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
- package/dist/chunk-YJ6OBKZJ.js +32 -0
- package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
- package/dist/chunk-ZFNOCP6W.js +106 -0
- package/dist/components/accordion.js +11 -5
- package/dist/components/alert-dialog.js +5 -3
- package/dist/components/alert.d.ts +4 -1
- package/dist/components/alert.js +25 -9
- package/dist/components/avatar.d.ts +17 -2
- package/dist/components/avatar.js +91 -12
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +5 -2
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +39 -0
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +13 -5
- package/dist/components/card.js +2 -2
- package/dist/components/carousel.d.ts +20 -3
- package/dist/components/carousel.js +72 -12
- package/dist/components/checkbox.js +1 -1
- package/dist/components/circular-progress.d.ts +16 -0
- package/dist/components/circular-progress.js +105 -0
- package/dist/components/combobox.d.ts +27 -0
- package/dist/components/combobox.js +102 -0
- package/dist/components/command.d.ts +16 -3
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.d.ts +1 -0
- package/dist/components/currency-input.js +73 -52
- package/dist/components/date-input.d.ts +10 -0
- package/dist/components/date-input.js +40 -0
- package/dist/components/description-list.d.ts +15 -0
- package/dist/components/description-list.js +67 -0
- package/dist/components/dialog.d.ts +13 -2
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +5 -2
- package/dist/components/drawer.js +47 -9
- package/dist/components/dropdown-menu.d.ts +7 -2
- package/dist/components/dropdown-menu.js +22 -6
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.d.ts +22 -0
- package/dist/components/field.js +130 -0
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.js +11 -1
- package/dist/components/haptic-button.js +3 -3
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +92 -0
- package/dist/components/input.d.ts +8 -2
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.js +120 -0
- package/dist/components/kbd.d.ts +13 -0
- package/dist/components/kbd.js +49 -0
- package/dist/components/lazy-markdown.d.ts +3 -1
- package/dist/components/lazy-markdown.js +7 -7
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +92 -83
- package/dist/components/multi-select-menu.d.ts +0 -4
- package/dist/components/multi-select-menu.js +28 -17
- package/dist/components/native-select.d.ts +12 -0
- package/dist/components/native-select.js +49 -0
- package/dist/components/pagination.d.ts +5 -2
- package/dist/components/pagination.js +66 -7
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +4 -4
- package/dist/components/popover.d.ts +7 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/progress.d.ts +5 -1
- package/dist/components/progress.js +17 -3
- package/dist/components/radio-group.js +12 -3
- package/dist/components/rating.d.ts +2 -0
- package/dist/components/rating.js +88 -70
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +116 -0
- package/dist/components/search-bar.d.ts +1 -2
- package/dist/components/search-bar.js +37 -60
- package/dist/components/searchable-select.js +76 -72
- package/dist/components/select.d.ts +5 -1
- package/dist/components/select.js +30 -14
- package/dist/components/selection-bar.d.ts +11 -0
- package/dist/components/selection-bar.js +65 -0
- package/dist/components/separator.js +3 -2
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/slider.js +21 -4
- package/dist/components/sliding-panels.js +9 -6
- package/dist/components/spinner.d.ts +17 -0
- package/dist/components/spinner.js +50 -0
- package/dist/components/stepper.d.ts +18 -0
- package/dist/components/stepper.js +199 -0
- package/dist/components/switch.js +4 -3
- package/dist/components/table.d.ts +23 -5
- package/dist/components/table.js +121 -35
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +37 -9
- package/dist/components/tag-group.d.ts +9 -0
- package/dist/components/tag-group.js +26 -0
- package/dist/components/tag.d.ts +17 -0
- package/dist/components/tag.js +61 -0
- package/dist/components/text.d.ts +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/textarea.js +2 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +16 -2
- package/package.json +45 -26
- package/src/components/accordion.tsx +11 -6
- package/src/components/alert-dialog.tsx +4 -2
- package/src/components/alert.tsx +20 -8
- package/src/components/avatar.tsx +106 -13
- package/src/components/badge.tsx +12 -11
- package/src/components/breadcrumb.tsx +5 -2
- package/src/components/button-group.stories.tsx +52 -0
- package/src/components/button-group.tsx +40 -0
- package/src/components/button.test.tsx +174 -0
- package/src/components/button.tsx +42 -19
- package/src/components/calendar.tsx +2 -2
- package/src/components/card.tsx +55 -13
- package/src/components/carousel.tsx +85 -13
- package/src/components/checkbox.test.tsx +58 -0
- package/src/components/checkbox.tsx +8 -3
- package/src/components/circular-progress.stories.tsx +39 -0
- package/src/components/circular-progress.tsx +102 -0
- package/src/components/collapsible.stories.tsx +7 -3
- package/src/components/combobox.stories.tsx +82 -0
- package/src/components/combobox.test.tsx +118 -0
- package/src/components/combobox.tsx +136 -0
- package/src/components/command.test.tsx +91 -0
- package/src/components/command.tsx +38 -15
- package/src/components/currency-input.test.tsx +63 -0
- package/src/components/currency-input.tsx +84 -66
- package/src/components/date-input.stories.tsx +31 -0
- package/src/components/date-input.tsx +43 -0
- package/src/components/description-list.stories.tsx +62 -0
- package/src/components/description-list.tsx +68 -0
- package/src/components/dialog.test.tsx +84 -0
- package/src/components/dialog.tsx +64 -24
- package/src/components/drawer.tsx +72 -23
- package/src/components/dropdown-menu.test.tsx +180 -0
- package/src/components/dropdown-menu.tsx +31 -6
- package/src/components/field.stories.tsx +36 -0
- package/src/components/field.test.tsx +157 -0
- package/src/components/field.tsx +146 -0
- package/src/components/form.stories.tsx +3 -2
- package/src/components/form.test.tsx +95 -0
- package/src/components/form.tsx +9 -0
- package/src/components/haptic-button.tsx +1 -1
- package/src/components/input-group.stories.tsx +61 -0
- package/src/components/input-group.tsx +99 -0
- package/src/components/input.test.tsx +103 -0
- package/src/components/input.tsx +24 -7
- package/src/components/item.stories.tsx +52 -0
- package/src/components/item.tsx +115 -0
- package/src/components/kbd.stories.tsx +33 -0
- package/src/components/kbd.tsx +46 -0
- package/src/components/lazy-markdown.tsx +10 -7
- package/src/components/multi-select-combobox.test.tsx +101 -0
- package/src/components/multi-select-combobox.tsx +85 -82
- package/src/components/multi-select-menu.tsx +49 -41
- package/src/components/native-select.stories.tsx +52 -0
- package/src/components/native-select.tsx +45 -0
- package/src/components/pagination.tsx +64 -5
- package/src/components/popover.tsx +66 -3
- package/src/components/progress.tsx +27 -4
- package/src/components/radio-group.test.tsx +64 -0
- package/src/components/radio-group.tsx +11 -3
- package/src/components/rating.test.tsx +112 -0
- package/src/components/rating.tsx +97 -82
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +127 -0
- package/src/components/search-bar.stories.tsx +1 -32
- package/src/components/search-bar.tsx +22 -59
- package/src/components/searchable-select.tsx +69 -73
- package/src/components/select.tsx +34 -19
- package/src/components/selection-bar.stories.tsx +37 -0
- package/src/components/selection-bar.tsx +65 -0
- package/src/components/separator.tsx +5 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/skeleton.tsx +2 -2
- package/src/components/slider.test.tsx +45 -0
- package/src/components/slider.tsx +31 -12
- package/src/components/sliding-panels.tsx +8 -5
- package/src/components/spinner.stories.tsx +35 -0
- package/src/components/spinner.tsx +50 -0
- package/src/components/stepper.stories.tsx +71 -0
- package/src/components/stepper.tsx +220 -0
- package/src/components/switch.test.tsx +52 -0
- package/src/components/switch.tsx +4 -3
- package/src/components/table.tsx +140 -35
- package/src/components/tabs.test.tsx +107 -0
- package/src/components/tabs.tsx +70 -23
- package/src/components/tag-group.stories.tsx +39 -0
- package/src/components/tag-group.tsx +28 -0
- package/src/components/tag.stories.tsx +50 -0
- package/src/components/tag.tsx +63 -0
- package/src/components/text.test.tsx +1 -1
- package/src/components/text.tsx +19 -3
- package/src/components/textarea.test.tsx +48 -0
- package/src/components/textarea.tsx +2 -1
- package/src/components/toast.tsx +4 -3
- package/src/components/toggle.tsx +2 -2
- package/src/components/tooltip.tsx +12 -2
- package/src/globals.css +126 -90
- package/dist/chunk-2BMJRXQX.js +0 -80
- package/dist/chunk-4AVS6CV4.js +0 -89
- package/dist/chunk-4UO73SKC.js +0 -26
- package/dist/chunk-CW24PLDW.js +0 -24
- package/dist/chunk-IQASCH2V.js +0 -25
- package/dist/chunk-NKHH23YK.js +0 -31
- package/dist/chunk-QJ54Z7TC.js +0 -83
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { cn } from "../lib/utils";
|
|
5
4
|
import { Star, StarHalf } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
7
|
interface RatingProps {
|
|
8
|
-
value: number;
|
|
9
|
-
max?: number;
|
|
8
|
+
value: number;
|
|
9
|
+
max?: number;
|
|
10
10
|
size?: "sm" | "md" | "lg";
|
|
11
11
|
showCount?: boolean;
|
|
12
12
|
reviewCount?: number;
|
|
13
13
|
readonly?: boolean;
|
|
14
14
|
onRatingChange?: (rating: number) => void;
|
|
15
15
|
className?: string;
|
|
16
|
+
/** Accessible label for the rating control. Defaults to "Rating". */
|
|
17
|
+
label?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
const sizeClasses = {
|
|
21
|
+
sm: "size-3",
|
|
22
|
+
md: "size-4",
|
|
23
|
+
lg: "size-5",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
const textSizeClasses = {
|
|
27
|
+
sm: "text-xs",
|
|
28
|
+
md: "text-sm",
|
|
29
|
+
lg: "text-base",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const FILLED_CLASSES = "fill-amber-400 text-amber-400";
|
|
33
|
+
const EMPTY_CLASSES = "fill-transparent text-muted-foreground/40";
|
|
34
|
+
|
|
18
35
|
const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
|
19
36
|
(
|
|
20
37
|
{
|
|
@@ -26,103 +43,101 @@ const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
|
|
26
43
|
readonly = true,
|
|
27
44
|
onRatingChange,
|
|
28
45
|
className,
|
|
46
|
+
label = "Rating",
|
|
29
47
|
...props
|
|
30
48
|
},
|
|
31
49
|
ref
|
|
32
50
|
) => {
|
|
33
51
|
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
|
|
52
|
+
const displayValue = hoverValue ?? value;
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const textSizeClasses = {
|
|
42
|
-
sm: "text-xs",
|
|
43
|
-
md: "text-sm",
|
|
44
|
-
lg: "text-base",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const handleClick = (rating: number) => {
|
|
48
|
-
if (!readonly && onRatingChange) {
|
|
49
|
-
onRatingChange(rating);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
54
|
+
if (readonly) {
|
|
55
|
+
const fullStars = Math.floor(displayValue);
|
|
56
|
+
const hasHalfStar = displayValue % 1 !== 0;
|
|
57
|
+
const emptyStars = max - fullStars - (hasHalfStar ? 1 : 0);
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
ref={ref}
|
|
62
|
+
role="img"
|
|
63
|
+
aria-label={`${label}: ${value} out of ${max}`}
|
|
64
|
+
className={cn("flex items-center gap-1", className)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center gap-0.5">
|
|
68
|
+
{Array.from({ length: fullStars }).map((_, i) => (
|
|
69
|
+
<Star key={`full-${i}`} className={cn(sizeClasses[size], FILLED_CLASSES)} />
|
|
70
|
+
))}
|
|
71
|
+
{hasHalfStar && <StarHalf className={cn(sizeClasses[size], FILLED_CLASSES)} />}
|
|
72
|
+
{Array.from({ length: emptyStars }).map((_, i) => (
|
|
73
|
+
<Star key={`empty-${i}`} className={cn(sizeClasses[size], EMPTY_CLASSES)} />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
{showCount && reviewCount !== undefined && (
|
|
77
|
+
<span className={cn("ml-1 text-muted-foreground", textSizeClasses[size])}>
|
|
78
|
+
({reviewCount})
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
58
84
|
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
61
|
-
|
|
85
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
86
|
+
if (!onRatingChange) return;
|
|
87
|
+
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
onRatingChange(Math.min(max, value + 1));
|
|
90
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
onRatingChange(Math.max(0, value - 1));
|
|
93
|
+
} else if (e.key === "Home") {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
onRatingChange(0);
|
|
96
|
+
} else if (e.key === "End") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
onRatingChange(max);
|
|
62
99
|
}
|
|
63
100
|
};
|
|
64
101
|
|
|
65
|
-
const displayValue = hoverValue ?? value;
|
|
66
|
-
const fullStars = Math.floor(displayValue);
|
|
67
|
-
const hasHalfStar = displayValue % 1 !== 0;
|
|
68
|
-
const emptyStars = max - fullStars - (hasHalfStar ? 1 : 0);
|
|
69
|
-
|
|
70
102
|
return (
|
|
71
103
|
<div
|
|
72
104
|
ref={ref}
|
|
73
|
-
|
|
105
|
+
role="radiogroup"
|
|
106
|
+
aria-label={label}
|
|
107
|
+
onKeyDown={handleKeyDown}
|
|
108
|
+
className={cn("flex items-center gap-1", className)}
|
|
74
109
|
{...props}
|
|
75
110
|
>
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onMouseLeave={handleMouseLeave}
|
|
104
|
-
/>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{/* Empty stars */}
|
|
108
|
-
{Array.from({ length: emptyStars }).map((_, i) => (
|
|
109
|
-
<Star
|
|
110
|
-
key={`empty-${i}`}
|
|
111
|
-
className={cn(
|
|
112
|
-
sizeClasses[size],
|
|
113
|
-
"text-muted-foreground/30 transition-colors",
|
|
114
|
-
!readonly && "hover:text-primary/50"
|
|
115
|
-
)}
|
|
116
|
-
onClick={() => handleClick(fullStars + (hasHalfStar ? 1 : 0) + i + 1)}
|
|
117
|
-
onMouseEnter={() => handleMouseEnter(fullStars + (hasHalfStar ? 1 : 0) + i + 1)}
|
|
118
|
-
onMouseLeave={handleMouseLeave}
|
|
119
|
-
/>
|
|
120
|
-
))}
|
|
111
|
+
<div className="flex items-center gap-0.5" onMouseLeave={() => setHoverValue(null)}>
|
|
112
|
+
{Array.from({ length: max }).map((_, i) => {
|
|
113
|
+
const starValue = i + 1;
|
|
114
|
+
const isActive = starValue <= displayValue;
|
|
115
|
+
return (
|
|
116
|
+
<button
|
|
117
|
+
key={starValue}
|
|
118
|
+
type="button"
|
|
119
|
+
role="radio"
|
|
120
|
+
aria-checked={value === starValue}
|
|
121
|
+
aria-label={`${starValue} star${starValue === 1 ? "" : "s"}`}
|
|
122
|
+
onClick={() => onRatingChange?.(starValue)}
|
|
123
|
+
onMouseEnter={() => setHoverValue(starValue)}
|
|
124
|
+
className={cn(
|
|
125
|
+
"rounded-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<Star
|
|
129
|
+
className={cn(
|
|
130
|
+
sizeClasses[size],
|
|
131
|
+
isActive ? FILLED_CLASSES : EMPTY_CLASSES,
|
|
132
|
+
"transition-colors"
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
121
138
|
</div>
|
|
122
|
-
|
|
123
|
-
{/* Review count */}
|
|
124
139
|
{showCount && reviewCount !== undefined && (
|
|
125
|
-
<span className={cn("text-muted-foreground
|
|
140
|
+
<span className={cn("ml-1 text-muted-foreground", textSizeClasses[size])}>
|
|
126
141
|
({reviewCount})
|
|
127
142
|
</span>
|
|
128
143
|
)}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ScrollFade } from "./scroll-fade";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/ScrollFade",
|
|
6
|
+
component: ScrollFade,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
"Scroll-position-aware fade overlay. Hides when at the corresponding edge, fades in when there's more content to scroll. Works vertically or horizontally and matches a chosen surface color.",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof ScrollFade>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
const longList = Array.from({ length: 30 });
|
|
22
|
+
const wideList = Array.from({ length: 20 });
|
|
23
|
+
|
|
24
|
+
export const Vertical: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
orientation: "vertical",
|
|
27
|
+
fadeColor: "background",
|
|
28
|
+
size: "md",
|
|
29
|
+
},
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<ScrollFade {...args} className="h-[320px] w-[360px] rounded-lg border" scrollClassName="p-4">
|
|
32
|
+
<ul className="space-y-3">
|
|
33
|
+
{longList.map((_, i) => (
|
|
34
|
+
<li key={i} className="text-sm text-muted-foreground">
|
|
35
|
+
Item #{i + 1} — scroll to reveal the fades at the edges.
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</ScrollFade>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Horizontal: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
orientation: "horizontal",
|
|
46
|
+
fadeColor: "background",
|
|
47
|
+
size: "md",
|
|
48
|
+
},
|
|
49
|
+
render: (args) => (
|
|
50
|
+
<ScrollFade {...args} className="w-[480px] rounded-lg border" scrollClassName="flex gap-3 p-4">
|
|
51
|
+
{wideList.map((_, i) => (
|
|
52
|
+
<div
|
|
53
|
+
key={i}
|
|
54
|
+
className="flex h-24 w-32 shrink-0 items-center justify-center rounded-md bg-muted text-sm"
|
|
55
|
+
>
|
|
56
|
+
Card {i + 1}
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</ScrollFade>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const InsideCard: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
orientation: "vertical",
|
|
66
|
+
fadeColor: "card",
|
|
67
|
+
size: "md",
|
|
68
|
+
},
|
|
69
|
+
render: (args) => (
|
|
70
|
+
<div className="bg-card text-card-foreground w-[360px] rounded-2xl border shadow-md">
|
|
71
|
+
<div className="border-border border-b p-4 font-semibold">Drawer header</div>
|
|
72
|
+
<ScrollFade {...args} className="h-[280px]" scrollClassName="p-4">
|
|
73
|
+
<ul className="space-y-3">
|
|
74
|
+
{longList.map((_, i) => (
|
|
75
|
+
<li key={i} className="text-sm text-muted-foreground">
|
|
76
|
+
Row #{i + 1} — fades use <code>fadeColor="card"</code> to match the surface.
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
</ScrollFade>
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const NoOverflow: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
orientation: "vertical",
|
|
88
|
+
fadeColor: "background",
|
|
89
|
+
size: "md",
|
|
90
|
+
},
|
|
91
|
+
render: (args) => (
|
|
92
|
+
<ScrollFade {...args} className="h-[200px] w-[320px] rounded-lg border" scrollClassName="p-4">
|
|
93
|
+
<p className="text-sm text-muted-foreground">
|
|
94
|
+
Content fits inside the box — both fades stay hidden.
|
|
95
|
+
</p>
|
|
96
|
+
</ScrollFade>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
type Orientation = "vertical" | "horizontal";
|
|
7
|
+
type FadeColor = "background" | "card" | "muted";
|
|
8
|
+
type Size = "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
interface ScrollFadeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
orientation?: Orientation;
|
|
12
|
+
fadeColor?: FadeColor;
|
|
13
|
+
size?: Size;
|
|
14
|
+
/** Class names applied to the inner scrollable element (where overflow lives). */
|
|
15
|
+
scrollClassName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Static maps so Tailwind can statically analyze the class names.
|
|
19
|
+
const fromMap: Record<FadeColor, string> = {
|
|
20
|
+
background: "from-background",
|
|
21
|
+
card: "from-card",
|
|
22
|
+
muted: "from-muted",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sizeMap: Record<Orientation, Record<Size, string>> = {
|
|
26
|
+
vertical: { sm: "h-6", md: "h-12", lg: "h-20" },
|
|
27
|
+
horizontal: { sm: "w-8", md: "w-16", lg: "w-28" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ScrollFade = React.forwardRef<HTMLDivElement, ScrollFadeProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
orientation = "vertical",
|
|
34
|
+
fadeColor = "background",
|
|
35
|
+
size = "md",
|
|
36
|
+
className,
|
|
37
|
+
scrollClassName,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
const innerRef = React.useRef<HTMLDivElement>(null);
|
|
44
|
+
React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);
|
|
45
|
+
|
|
46
|
+
const [atStart, setAtStart] = React.useState(true);
|
|
47
|
+
const [atEnd, setAtEnd] = React.useState(true);
|
|
48
|
+
|
|
49
|
+
const update = React.useCallback(() => {
|
|
50
|
+
const el = innerRef.current;
|
|
51
|
+
if (!el) return;
|
|
52
|
+
if (orientation === "vertical") {
|
|
53
|
+
const overflow = el.scrollHeight - el.clientHeight;
|
|
54
|
+
setAtStart(el.scrollTop <= 1);
|
|
55
|
+
setAtEnd(overflow <= 1 || el.scrollTop >= overflow - 1);
|
|
56
|
+
} else {
|
|
57
|
+
const overflow = el.scrollWidth - el.clientWidth;
|
|
58
|
+
setAtStart(el.scrollLeft <= 1);
|
|
59
|
+
setAtEnd(overflow <= 1 || el.scrollLeft >= overflow - 1);
|
|
60
|
+
}
|
|
61
|
+
}, [orientation]);
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
const el = innerRef.current;
|
|
65
|
+
if (!el) return;
|
|
66
|
+
update();
|
|
67
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
68
|
+
const ro = new ResizeObserver(update);
|
|
69
|
+
ro.observe(el);
|
|
70
|
+
return () => {
|
|
71
|
+
el.removeEventListener("scroll", update);
|
|
72
|
+
ro.disconnect();
|
|
73
|
+
};
|
|
74
|
+
}, [update]);
|
|
75
|
+
|
|
76
|
+
const fromClass = fromMap[fadeColor];
|
|
77
|
+
const fadeSize = sizeMap[orientation][size];
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
"relative isolate flex",
|
|
83
|
+
orientation === "vertical" ? "flex-col" : "flex-row",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
ref={innerRef}
|
|
89
|
+
className={cn(
|
|
90
|
+
"min-h-0 min-w-0 flex-1",
|
|
91
|
+
orientation === "vertical" ? "overflow-y-auto" : "overflow-x-auto",
|
|
92
|
+
scrollClassName
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
<div
|
|
99
|
+
aria-hidden
|
|
100
|
+
className={cn(
|
|
101
|
+
"pointer-events-none absolute z-10 transition-opacity duration-300",
|
|
102
|
+
orientation === "vertical"
|
|
103
|
+
? `inset-x-0 top-0 ${fadeSize} bg-gradient-to-b`
|
|
104
|
+
: `inset-y-0 left-0 ${fadeSize} bg-gradient-to-r`,
|
|
105
|
+
fromClass,
|
|
106
|
+
atStart ? "opacity-0" : "opacity-100"
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
<div
|
|
110
|
+
aria-hidden
|
|
111
|
+
className={cn(
|
|
112
|
+
"pointer-events-none absolute z-10 transition-opacity duration-300",
|
|
113
|
+
orientation === "vertical"
|
|
114
|
+
? `inset-x-0 bottom-0 ${fadeSize} bg-gradient-to-t`
|
|
115
|
+
: `inset-y-0 right-0 ${fadeSize} bg-gradient-to-l`,
|
|
116
|
+
fromClass,
|
|
117
|
+
atEnd ? "opacity-0" : "opacity-100"
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
ScrollFade.displayName = "ScrollFade";
|
|
125
|
+
|
|
126
|
+
export { ScrollFade };
|
|
127
|
+
export type { ScrollFadeProps };
|
|
@@ -20,44 +20,13 @@ export const Default: Story = {
|
|
|
20
20
|
render: () => <SearchBarDemo />,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const Wide: Story = {
|
|
24
24
|
render: () => {
|
|
25
25
|
const [value, setValue] = useState("");
|
|
26
26
|
return (
|
|
27
27
|
<SearchBar
|
|
28
28
|
value={value}
|
|
29
29
|
onChange={setValue}
|
|
30
|
-
variant="editorial"
|
|
31
|
-
placeholder="Search..."
|
|
32
|
-
className="w-[400px]"
|
|
33
|
-
/>
|
|
34
|
-
);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const Primary: Story = {
|
|
39
|
-
render: () => {
|
|
40
|
-
const [value, setValue] = useState("");
|
|
41
|
-
return (
|
|
42
|
-
<SearchBar
|
|
43
|
-
value={value}
|
|
44
|
-
onChange={setValue}
|
|
45
|
-
variant="primary"
|
|
46
|
-
placeholder="Search..."
|
|
47
|
-
className="w-[400px]"
|
|
48
|
-
/>
|
|
49
|
-
);
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const Secondary: Story = {
|
|
54
|
-
render: () => {
|
|
55
|
-
const [value, setValue] = useState("");
|
|
56
|
-
return (
|
|
57
|
-
<SearchBar
|
|
58
|
-
value={value}
|
|
59
|
-
onChange={setValue}
|
|
60
|
-
variant="secondary"
|
|
61
30
|
placeholder="Search..."
|
|
62
31
|
className="w-[400px]"
|
|
63
32
|
/>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ChangeEvent } from "react";
|
|
4
|
-
import { Card } from "./card";
|
|
5
4
|
import { Loader2, Search } from "lucide-react";
|
|
6
5
|
import { cn } from "../lib/utils";
|
|
7
6
|
|
|
@@ -15,7 +14,6 @@ interface SearchBarProps {
|
|
|
15
14
|
autoFocus?: boolean;
|
|
16
15
|
inputAriaLabel?: string;
|
|
17
16
|
size?: "default" | "compact";
|
|
18
|
-
variant?: "primary" | "secondary" | "editorial";
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export function SearchBar({
|
|
@@ -28,49 +26,26 @@ export function SearchBar({
|
|
|
28
26
|
autoFocus = false,
|
|
29
27
|
inputAriaLabel,
|
|
30
28
|
size = "default",
|
|
31
|
-
variant = "editorial",
|
|
32
29
|
}: SearchBarProps) {
|
|
33
30
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
34
|
-
|
|
35
|
-
onChange?.(newValue);
|
|
31
|
+
onChange?.(e.target.value);
|
|
36
32
|
};
|
|
37
33
|
|
|
38
|
-
const Wrapper = variant === "editorial" ? "div" : Card;
|
|
39
|
-
const wrapperClassName =
|
|
40
|
-
variant === "editorial"
|
|
41
|
-
? cn(
|
|
42
|
-
"bg-surface border border-border rounded-[14px] h-12 flex items-center transition-colors focus-within:border-primary",
|
|
43
|
-
className
|
|
44
|
-
)
|
|
45
|
-
: cn(
|
|
46
|
-
"border-2 border-primary-foreground/20 backdrop-blur-xl rounded-lg p-0",
|
|
47
|
-
size === "compact" ? "md:p-1.5" : "md:p-2",
|
|
48
|
-
variant === "primary"
|
|
49
|
-
? "!bg-primary-foreground/10 !text-primary !border-primary/20"
|
|
50
|
-
: "!bg-primary !text-primary-foreground !border-primary/20",
|
|
51
|
-
className
|
|
52
|
-
);
|
|
53
|
-
|
|
54
34
|
return (
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex w-full items-center rounded-[14px] border border-border bg-surface transition-colors focus-within:border-primary",
|
|
38
|
+
size === "compact" ? "h-10" : "h-12",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<div className="relative flex w-full items-center">
|
|
43
|
+
<Search
|
|
58
44
|
className={cn(
|
|
59
|
-
"absolute top-1/2 z-10 -translate-y-1/2",
|
|
60
|
-
size === "compact" ? "left-3.5" : "left-4"
|
|
45
|
+
"pointer-events-none absolute top-1/2 z-10 -translate-y-1/2 text-muted-foreground",
|
|
46
|
+
size === "compact" ? "left-3.5 h-3.5 w-3.5" : "left-4 h-4 w-4"
|
|
61
47
|
)}
|
|
62
|
-
|
|
63
|
-
<Search
|
|
64
|
-
className={cn(
|
|
65
|
-
variant === "editorial"
|
|
66
|
-
? "text-muted-foreground"
|
|
67
|
-
: variant === "primary"
|
|
68
|
-
? "text-primary"
|
|
69
|
-
: "text-primary-foreground",
|
|
70
|
-
size === "compact" ? "h-3.5 w-3.5" : "h-4 w-4"
|
|
71
|
-
)}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
48
|
+
/>
|
|
74
49
|
<input
|
|
75
50
|
type="text"
|
|
76
51
|
placeholder={placeholder}
|
|
@@ -79,35 +54,23 @@ export function SearchBar({
|
|
|
79
54
|
autoFocus={autoFocus}
|
|
80
55
|
aria-label={inputAriaLabel ?? placeholder}
|
|
81
56
|
className={cn(
|
|
82
|
-
"w-full bg-transparent
|
|
57
|
+
"w-full border-0 bg-transparent text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:outline-none",
|
|
83
58
|
size === "compact"
|
|
84
|
-
? "pl-9 pr-3
|
|
85
|
-
: "pl-10 pr-4
|
|
86
|
-
variant === "editorial"
|
|
87
|
-
? "text-foreground placeholder:text-muted-foreground"
|
|
88
|
-
: variant === "primary"
|
|
89
|
-
? "!text-primary placeholder:text-primary/60"
|
|
90
|
-
: "!text-primary-foreground placeholder:text-primary-foreground/60",
|
|
59
|
+
? "rounded-lg py-2.5 pl-9 pr-3 text-base placeholder:text-base md:text-sm md:placeholder:text-sm"
|
|
60
|
+
: "rounded-[14px] py-3 pl-10 pr-4 text-base placeholder:text-base md:text-[14px] md:placeholder:text-[14px]",
|
|
91
61
|
inputClassName
|
|
92
62
|
)}
|
|
93
63
|
/>
|
|
94
64
|
{isSearching && (
|
|
95
|
-
<
|
|
65
|
+
<Loader2
|
|
66
|
+
aria-hidden
|
|
96
67
|
className={cn(
|
|
97
|
-
"absolute top-1/2 -translate-y-1/2",
|
|
98
|
-
size === "compact" ? "right-3.5" : "right-4"
|
|
68
|
+
"absolute top-1/2 -translate-y-1/2 animate-spin text-muted-foreground",
|
|
69
|
+
size === "compact" ? "right-3.5 h-3.5 w-3.5" : "right-4 h-4 w-4"
|
|
99
70
|
)}
|
|
100
|
-
|
|
101
|
-
<Loader2
|
|
102
|
-
className={cn(
|
|
103
|
-
"animate-spin",
|
|
104
|
-
variant === "editorial" ? "text-muted-foreground" : "text-primary/60",
|
|
105
|
-
size === "compact" ? "h-3.5 w-3.5" : "h-4 w-4"
|
|
106
|
-
)}
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
71
|
+
/>
|
|
109
72
|
)}
|
|
110
73
|
</div>
|
|
111
|
-
</
|
|
74
|
+
</div>
|
|
112
75
|
);
|
|
113
76
|
}
|