ui-arreya-components 0.0.15 → 0.0.16
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/.github/workflows/npm-publish.yml +35 -0
- package/README.md +8 -0
- package/dist/styles.css +1 -1
- package/dist/ui.cjs.js +185 -62
- package/dist/ui.es.js +22593 -4815
- package/dist/ui.umd.js +186 -63
- package/package.json +2 -2
- package/scripts/build-index-ts.sh +16 -0
- package/scripts/template.sh +5 -5
- package/src/components/feature/graph-card.stories.tsx +138 -0
- package/src/components/feature/graph-card.tsx +113 -0
- package/src/components/feature/header.stories.tsx +1 -1
- package/src/components/feature/header.tsx +2 -2
- package/src/components/feature/search-bar.stories.tsx +35 -0
- package/src/components/feature/search-bar.tsx +141 -0
- package/src/components/feature/wizard.stories.tsx +199 -0
- package/src/components/feature/wizard.tsx +278 -0
- package/src/components/ui/sheet.tsx +0 -2
- package/src/components/ui/sonner.tsx +2 -2
- package/src/index.css +124 -0
- package/src/index.ts +51 -51
- package/tailwind.config.js +0 -2
- package/src/components/index.ts +0 -1
- package/src/components/ui/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui-arreya-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"build-storybook": "concurrently 'npm run build-storybook:css' 'storybook build'",
|
|
25
25
|
"build-storybook:css": "tailwindcss -m -i ./src/styles/tailwind.css -o ./src/index.css",
|
|
26
26
|
"prepublish": "npm run build",
|
|
27
|
-
"
|
|
27
|
+
"test": "echo 'setup unit testing'"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18.0.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Starting fresh
|
|
4
|
+
rm src/index.ts
|
|
5
|
+
|
|
6
|
+
# Add Components
|
|
7
|
+
for import in `ls src/components/ui/ | grep -v '.stories.' | cut -f1 -d\.`;
|
|
8
|
+
do
|
|
9
|
+
echo 'export * from "./components/ui/'$import'"' >> src/index.ts
|
|
10
|
+
done
|
|
11
|
+
|
|
12
|
+
# Add Features
|
|
13
|
+
for import in `ls src/components/feature/ | grep -v '.stories.' | cut -f1 -d\.`;
|
|
14
|
+
do
|
|
15
|
+
echo 'export * from "./components/feature/'${import}'"' >> src/index.ts
|
|
16
|
+
done
|
package/scripts/template.sh
CHANGED
|
@@ -13,21 +13,21 @@ COMPONENT_OR_FEATURE_NAME=$2
|
|
|
13
13
|
case $TEMPLATE_TYPE in
|
|
14
14
|
feature)
|
|
15
15
|
echo "Creating feature template..."
|
|
16
|
-
mkdir -p src/components/
|
|
16
|
+
mkdir -p src/components/feature/${COMPONENT_OR_FEATURE_NAME}
|
|
17
17
|
|
|
18
|
-
cat <<EOF > src/components/
|
|
18
|
+
cat <<EOF > src/components/feature/${COMPONENT_OR_FEATURE_NAME}/index.ts
|
|
19
19
|
export * from './${COMPONENT_OR_FEATURE_NAME}'
|
|
20
20
|
EOF
|
|
21
21
|
|
|
22
|
-
cat <<EOF >> src/components/
|
|
22
|
+
cat <<EOF >> src/components/feature/index.ts
|
|
23
23
|
export * from './${COMPONENT_OR_FEATURE_NAME}'
|
|
24
24
|
EOF
|
|
25
25
|
|
|
26
26
|
cat <<EOF >> src/components/index.ts
|
|
27
|
-
export * from './
|
|
27
|
+
export * from './feature/${COMPONENT_OR_FEATURE_NAME}'
|
|
28
28
|
EOF
|
|
29
29
|
|
|
30
|
-
touch src/components/
|
|
30
|
+
touch src/components/feature/${COMPONENT_OR_FEATURE_NAME}/{${COMPONENT_OR_FEATURE_NAME}.stories.tsx,${COMPONENT_OR_FEATURE_NAME}.tsx}
|
|
31
31
|
;;
|
|
32
32
|
component)
|
|
33
33
|
echo "Creating component template"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { GraphCard } from "./graph-card"
|
|
3
|
+
import { Area, AreaChart, ResponsiveContainer } from "recharts"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof GraphCard> = {
|
|
6
|
+
title: "Feature/GraphCard",
|
|
7
|
+
component: GraphCard,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {
|
|
13
|
+
title: {
|
|
14
|
+
control: "text",
|
|
15
|
+
description: "The title of the graph card",
|
|
16
|
+
},
|
|
17
|
+
description: {
|
|
18
|
+
control: "text",
|
|
19
|
+
description: "Optional description text below the title",
|
|
20
|
+
},
|
|
21
|
+
value: {
|
|
22
|
+
control: "text",
|
|
23
|
+
description: "The primary value to display (e.g., total, average)",
|
|
24
|
+
},
|
|
25
|
+
change: {
|
|
26
|
+
control: "number",
|
|
27
|
+
description: "Percentage change to display",
|
|
28
|
+
},
|
|
29
|
+
changeType: {
|
|
30
|
+
control: { type: "select" },
|
|
31
|
+
options: ["increase", "decrease", "neutral"],
|
|
32
|
+
description: "Type of change (affects color and icon)",
|
|
33
|
+
},
|
|
34
|
+
changeLabel: {
|
|
35
|
+
control: "text",
|
|
36
|
+
description: "Label to display after the change value",
|
|
37
|
+
},
|
|
38
|
+
isLoading: {
|
|
39
|
+
control: "boolean",
|
|
40
|
+
description: "Whether to show a loading state",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default meta
|
|
46
|
+
type Story = StoryObj<typeof GraphCard>
|
|
47
|
+
|
|
48
|
+
// Simple example with minimal data
|
|
49
|
+
export const Simple: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
title: "Monthly Sales",
|
|
52
|
+
description: "Total sales for the current month",
|
|
53
|
+
value: "$12,345",
|
|
54
|
+
chart: (
|
|
55
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
56
|
+
<AreaChart
|
|
57
|
+
data={[
|
|
58
|
+
{ month: "Jan", value: 1000 },
|
|
59
|
+
{ month: "Feb", value: 2000 },
|
|
60
|
+
{ month: "Mar", value: 1500 },
|
|
61
|
+
{ month: "Apr", value: 3000 },
|
|
62
|
+
{ month: "May", value: 2500 },
|
|
63
|
+
{ month: "Jun", value: 4000 },
|
|
64
|
+
]}
|
|
65
|
+
>
|
|
66
|
+
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="hsl(var(--primary) / 0.2)" />
|
|
67
|
+
</AreaChart>
|
|
68
|
+
</ResponsiveContainer>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Example with positive change
|
|
74
|
+
export const PositiveChange: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
title: "Website Traffic",
|
|
77
|
+
description: "Unique visitors per day",
|
|
78
|
+
value: "12,456",
|
|
79
|
+
change: 23.5,
|
|
80
|
+
changeType: "increase",
|
|
81
|
+
changeLabel: "vs last week",
|
|
82
|
+
chart: (
|
|
83
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
84
|
+
<AreaChart
|
|
85
|
+
data={[
|
|
86
|
+
{ day: "Mon", value: 1000 },
|
|
87
|
+
{ day: "Tue", value: 2000 },
|
|
88
|
+
{ day: "Wed", value: 3000 },
|
|
89
|
+
{ day: "Thu", value: 2500 },
|
|
90
|
+
{ day: "Fri", value: 4000 },
|
|
91
|
+
{ day: "Sat", value: 3500 },
|
|
92
|
+
{ day: "Sun", value: 4500 },
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="hsl(var(--primary) / 0.2)" />
|
|
96
|
+
</AreaChart>
|
|
97
|
+
</ResponsiveContainer>
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Example with negative change
|
|
103
|
+
export const NegativeChange: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
title: "Conversion Rate",
|
|
106
|
+
description: "Percentage of visitors who make a purchase",
|
|
107
|
+
value: "3.2%",
|
|
108
|
+
change: 5.7,
|
|
109
|
+
changeType: "decrease",
|
|
110
|
+
changeLabel: "vs last month",
|
|
111
|
+
chart: (
|
|
112
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
113
|
+
<AreaChart
|
|
114
|
+
data={[
|
|
115
|
+
{ week: "W1", value: 4.5 },
|
|
116
|
+
{ week: "W2", value: 4.2 },
|
|
117
|
+
{ week: "W3", value: 3.8 },
|
|
118
|
+
{ week: "W4", value: 3.5 },
|
|
119
|
+
{ week: "W5", value: 3.2 },
|
|
120
|
+
]}
|
|
121
|
+
>
|
|
122
|
+
<Area type="monotone" dataKey="value" stroke="hsl(var(--destructive))" fill="hsl(var(--destructive) / 0.2)" />
|
|
123
|
+
</AreaChart>
|
|
124
|
+
</ResponsiveContainer>
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Loading state
|
|
130
|
+
export const Loading: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
title: "Revenue",
|
|
133
|
+
description: "Total revenue this quarter",
|
|
134
|
+
value: "$143,245",
|
|
135
|
+
isLoading: true,
|
|
136
|
+
chart: <div />, // This won't be shown in loading state
|
|
137
|
+
},
|
|
138
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
3
|
+
import { ChartContainer } from "@/components/ui/chart"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { ArrowDownIcon, ArrowUpIcon, MoreHorizontal } from "lucide-react"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
8
|
+
|
|
9
|
+
export interface GraphCardProps {
|
|
10
|
+
title: string
|
|
11
|
+
description?: string
|
|
12
|
+
chart?: React.ReactNode | any
|
|
13
|
+
value?: string | number
|
|
14
|
+
previousValue?: string | number
|
|
15
|
+
change?: number
|
|
16
|
+
changeType?: "increase" | "decrease" | "neutral"
|
|
17
|
+
changeLabel?: string
|
|
18
|
+
className?: string
|
|
19
|
+
footerContent?: React.ReactNode
|
|
20
|
+
menuItems?: Array<{ label: string; onClick: () => void }>
|
|
21
|
+
chartConfig?: Record<string, any>
|
|
22
|
+
isLoading?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function GraphCard({
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
chart,
|
|
29
|
+
value,
|
|
30
|
+
previousValue,
|
|
31
|
+
change,
|
|
32
|
+
changeType,
|
|
33
|
+
changeLabel,
|
|
34
|
+
className,
|
|
35
|
+
footerContent,
|
|
36
|
+
menuItems,
|
|
37
|
+
chartConfig,
|
|
38
|
+
isLoading = false,
|
|
39
|
+
}: GraphCardProps) {
|
|
40
|
+
const renderChangeIndicator = () => {
|
|
41
|
+
if (changeType === "increase") {
|
|
42
|
+
return <ArrowUpIcon className="h-4 w-4 text-emerald-500" />
|
|
43
|
+
} else if (changeType === "decrease") {
|
|
44
|
+
return <ArrowDownIcon className="h-4 w-4 text-red-500" />
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getChangeTextColor = () => {
|
|
50
|
+
if (changeType === "increase") return "text-emerald-500"
|
|
51
|
+
if (changeType === "decrease") return "text-red-500"
|
|
52
|
+
return "text-muted-foreground"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Card className={cn("overflow-hidden", className)}>
|
|
57
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
58
|
+
<div>
|
|
59
|
+
<CardTitle className="text-base font-medium">{title}</CardTitle>
|
|
60
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
61
|
+
</div>
|
|
62
|
+
{menuItems && menuItems.length > 0 && (
|
|
63
|
+
<DropdownMenu>
|
|
64
|
+
<DropdownMenuTrigger asChild>
|
|
65
|
+
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
66
|
+
<span className="sr-only">Open menu</span>
|
|
67
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
68
|
+
</Button>
|
|
69
|
+
</DropdownMenuTrigger>
|
|
70
|
+
<DropdownMenuContent align="end">
|
|
71
|
+
{menuItems.map((item, index) => (
|
|
72
|
+
<DropdownMenuItem key={index} onClick={item.onClick}>
|
|
73
|
+
{item.label}
|
|
74
|
+
</DropdownMenuItem>
|
|
75
|
+
))}
|
|
76
|
+
</DropdownMenuContent>
|
|
77
|
+
</DropdownMenu>
|
|
78
|
+
)}
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent className="pb-2">
|
|
81
|
+
{value && (
|
|
82
|
+
<div className="flex items-center justify-between">
|
|
83
|
+
<div className="space-y-1">
|
|
84
|
+
<p className="text-2xl font-bold">{value}</p>
|
|
85
|
+
{(change !== undefined || previousValue) && (
|
|
86
|
+
<div className="flex items-center text-xs">
|
|
87
|
+
{renderChangeIndicator()}
|
|
88
|
+
<span className={cn("ml-1", getChangeTextColor())}>
|
|
89
|
+
{change !== undefined && `${Math.abs(change)}%`}
|
|
90
|
+
{previousValue && !change && `from ${previousValue}`}
|
|
91
|
+
</span>
|
|
92
|
+
{changeLabel && <span className="ml-1 text-muted-foreground">{changeLabel}</span>}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
<div className={cn("mt-4", value ? "h-[180px]" : "h-[240px]")}>
|
|
99
|
+
{isLoading ? (
|
|
100
|
+
<div className="flex h-full items-center justify-center">
|
|
101
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
|
102
|
+
</div>
|
|
103
|
+
) : (chartConfig && chart) ? (
|
|
104
|
+
<ChartContainer config={chartConfig}>{ chart }</ChartContainer>
|
|
105
|
+
) : (
|
|
106
|
+
chart
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</CardContent>
|
|
110
|
+
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
|
111
|
+
</Card>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@/components/ui/breadcrumb"
|
|
10
10
|
import { ChevronRight, Home } from "lucide-react"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type BreadcrumbItem = {
|
|
13
13
|
label: string
|
|
14
14
|
href?: string
|
|
15
15
|
isCurrentPage?: boolean
|
|
@@ -26,7 +26,7 @@ interface HeaderProps {
|
|
|
26
26
|
export function Header({ title, breadcrumbItems = [], showHomeLink = true, className = "", children }: HeaderProps) {
|
|
27
27
|
return (
|
|
28
28
|
<header className={`border-b pb-4 ${className}`}>
|
|
29
|
-
<div className="container mx-auto
|
|
29
|
+
<div className="container mx-auto p-4">
|
|
30
30
|
{(breadcrumbItems.length > 0 || showHomeLink) && (
|
|
31
31
|
<Breadcrumb className="mb-2">
|
|
32
32
|
<BreadcrumbList>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { SearchBar } from "@/components/feature/search-bar"
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Feature/SearchBar",
|
|
6
|
+
component: SearchBar,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
} satisfies Meta<typeof SearchBar>
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof meta>
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
options: [
|
|
19
|
+
"First",
|
|
20
|
+
"Second",
|
|
21
|
+
"Third"
|
|
22
|
+
],
|
|
23
|
+
search: [
|
|
24
|
+
"How to use React hooks",
|
|
25
|
+
"Tailwind CSS tutorial",
|
|
26
|
+
"Next.js documentation",
|
|
27
|
+
"JavaScript best practices",
|
|
28
|
+
"TypeScript interfaces",
|
|
29
|
+
"CSS Grid layout examples",
|
|
30
|
+
"React performance optimization",
|
|
31
|
+
"State management in React",
|
|
32
|
+
],
|
|
33
|
+
submit: (search, option) => { console.log(`pressed ${search} ${option}`) },
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useRef,
|
|
5
|
+
} from 'react'
|
|
6
|
+
|
|
7
|
+
import { Search, ChevronDown } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
type SearchBarProps = {
|
|
10
|
+
search: string[],
|
|
11
|
+
options?: string[]
|
|
12
|
+
submit: (search:string, option?: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SearchBar = ({ search, options, submit }: SearchBarProps): JSX.Element => {
|
|
16
|
+
const firstDropdownOption = (options && options.length > 0) ? options[0] : ""
|
|
17
|
+
|
|
18
|
+
const [isOptionsOpen, setIsOptionsOpen] = useState(false)
|
|
19
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
20
|
+
const [searchValue, setSearchValue] = useState("")
|
|
21
|
+
const [selectedOption, setSelectedOption] = useState(firstDropdownOption)
|
|
22
|
+
|
|
23
|
+
const optionsDropdownRef = useRef<HTMLDivElement>(null)
|
|
24
|
+
const searchDropdownRef = useRef<HTMLDivElement>(null)
|
|
25
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
26
|
+
|
|
27
|
+
// TODO if no dropdownOptions make it a regular search bar
|
|
28
|
+
|
|
29
|
+
// Filtered search options based on input
|
|
30
|
+
const filteredOptions = searchValue
|
|
31
|
+
? search.filter((option) => option?.toLowerCase().includes(searchValue.toLowerCase()))
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
const onSubmit = () => {
|
|
35
|
+
submit(searchValue, selectedOption)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Close dropdowns when clicking outside
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
function handleClickOutside(event: MouseEvent) {
|
|
41
|
+
if (optionsDropdownRef.current && !optionsDropdownRef.current.contains(event.target as Node)) {
|
|
42
|
+
setIsOptionsOpen(false)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
searchDropdownRef.current &&
|
|
47
|
+
!searchDropdownRef.current.contains(event.target as Node) &&
|
|
48
|
+
event.target !== searchInputRef.current
|
|
49
|
+
|
|
50
|
+
) {
|
|
51
|
+
setIsSearchOpen(false)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
56
|
+
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
// Handle search input changes
|
|
60
|
+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
61
|
+
const value = e.target.value
|
|
62
|
+
setSearchValue(value)
|
|
63
|
+
setIsSearchOpen(value.length > 0)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle search option selection
|
|
67
|
+
const handleSelectSearchOption = (option: string) => {
|
|
68
|
+
setSearchValue(option)
|
|
69
|
+
setIsSearchOpen(false)
|
|
70
|
+
searchInputRef.current?.focus()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex max-w-xl rounded-lg border border-blue-100 bg-white">
|
|
75
|
+
<div className="relative flex-1" ref={searchDropdownRef}>
|
|
76
|
+
<input
|
|
77
|
+
ref={searchInputRef}
|
|
78
|
+
type="search"
|
|
79
|
+
placeholder="Search..."
|
|
80
|
+
value={searchValue}
|
|
81
|
+
onChange={handleSearchChange}
|
|
82
|
+
onFocus={() => searchValue && setIsSearchOpen(true)}
|
|
83
|
+
className="h-9 w-full border-0 bg-transparent px-3 py-2 outline-none"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{isSearchOpen && filteredOptions.length > 0 && (
|
|
87
|
+
<div className="absolute left-0 top-full z-10 mt-1 w-full rounded-md border border-gray-200 bg-white py-1 shadow-lg">
|
|
88
|
+
{filteredOptions.map((option) => (
|
|
89
|
+
<button
|
|
90
|
+
key={option}
|
|
91
|
+
className="flex w-full px-4 py-2 text-left text-sm hover:bg-gray-50"
|
|
92
|
+
onClick={() => handleSelectSearchOption(option)}
|
|
93
|
+
>
|
|
94
|
+
{option}
|
|
95
|
+
</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="flex items-center">
|
|
102
|
+
<div className="h-5 w-[1px] bg-slate-200" />
|
|
103
|
+
<div className="relative" ref={optionsDropdownRef}>
|
|
104
|
+
<button
|
|
105
|
+
onClick={() => setIsOptionsOpen(!isOptionsOpen)}
|
|
106
|
+
className="flex h-9 items-center gap-1 px-3 text-sm font-normal text-gray-700 hover:bg-gray-50"
|
|
107
|
+
>
|
|
108
|
+
{selectedOption}
|
|
109
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{isOptionsOpen && (
|
|
113
|
+
<div className="absolute right-0 top-full z-10 mt-1 w-48 rounded-md border border-gray-200 bg-white py-1 shadow-lg">
|
|
114
|
+
{options?.map((option) => (
|
|
115
|
+
<button
|
|
116
|
+
key={option}
|
|
117
|
+
className="flex w-full px-4 py-2 text-left text-sm hover:bg-gray-50"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
setSelectedOption(option)
|
|
120
|
+
setIsOptionsOpen(false)
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{option}
|
|
124
|
+
</button>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
<div className="h-5 w-[1px] bg-slate-200" />
|
|
130
|
+
<button
|
|
131
|
+
className="flex h-9 w-9 items-center justify-center text-gray-500 hover:bg-gray-50"
|
|
132
|
+
onClick={onSubmit}
|
|
133
|
+
>
|
|
134
|
+
<Search className="h-2 w-2" />
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
SearchBar.displayName = "SearchBar"
|