Skip to content

Commit 4b7fdaf

Browse files
committed
feat: add Search component
1 parent d3611f4 commit 4b7fdaf

File tree

9 files changed

+290
-0
lines changed

9 files changed

+290
-0
lines changed

src/assets/search-icon.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from "react";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
import { Search } from "./Search";
4+
5+
const meta = {
6+
title: "Components/Search",
7+
component: Search,
8+
parameters: {
9+
layout: "centered",
10+
},
11+
tags: ["autodocs"],
12+
argTypes: {
13+
size: {
14+
control: "select",
15+
options: ["small", "default", "large"],
16+
description: "Size of the search input",
17+
},
18+
showIcon: {
19+
control: "boolean",
20+
description: "Whether to show the search icon",
21+
},
22+
placeholder: {
23+
control: "text",
24+
description: "Placeholder text",
25+
},
26+
disabled: {
27+
control: "boolean",
28+
description: "Whether the input is disabled",
29+
},
30+
},
31+
} satisfies Meta<typeof Search>;
32+
33+
export default meta;
34+
type Story = StoryObj<typeof meta>;
35+
36+
export const Default: Story = {
37+
args: {
38+
placeholder: "Decoding Bitcoin....",
39+
style: { width: "269px" },
40+
},
41+
};
42+
43+
export const Hover: Story = {
44+
args: {
45+
placeholder: "Decoding Bitcoin....",
46+
style: { width: "269px" },
47+
},
48+
parameters: {
49+
pseudo: { hover: true },
50+
},
51+
};
52+
53+
export const Focused: Story = {
54+
args: {
55+
placeholder: "Decoding Bitcoin....",
56+
defaultValue: "Bitc",
57+
autoFocus: true,
58+
style: { width: "269px" },
59+
},
60+
};
61+
62+
export const WithValue: Story = {
63+
args: {
64+
placeholder: "Search...",
65+
defaultValue: "Bitcoin technology",
66+
style: { width: "269px" },
67+
},
68+
};
69+
70+
export const WithoutIcon: Story = {
71+
args: {
72+
placeholder: "Decoding Bitcoin....",
73+
showIcon: false,
74+
style: { width: "269px" },
75+
},
76+
};
77+
78+
export const Disabled: Story = {
79+
args: {
80+
placeholder: "Decoding Bitcoin....",
81+
disabled: true,
82+
style: { width: "269px" },
83+
},
84+
};
85+
86+
export const Small: Story = {
87+
args: {
88+
placeholder: "Search...",
89+
size: "small",
90+
style: { width: "200px" },
91+
},
92+
};
93+
94+
export const Large: Story = {
95+
args: {
96+
placeholder: "Decoding Bitcoin....",
97+
size: "large",
98+
style: { width: "350px" },
99+
},
100+
};
101+
102+
export const FullWidth: Story = {
103+
args: {
104+
placeholder: "Decoding Bitcoin....",
105+
containerClassName: "w-full",
106+
style: { width: "500px" },
107+
},
108+
};
109+
110+
export const CustomStyling: Story = {
111+
args: {
112+
placeholder: "Custom styled search...",
113+
inputClassName: "bg-bdp-hover-primary",
114+
iconClassName: "text-bdp-accent",
115+
style: { width: "300px" },
116+
},
117+
};
118+
119+
export const AllStates: Story = {
120+
args: {
121+
placeholder: "Decoding Bitcoin....",
122+
style: { width: "269px" },
123+
},
124+
parameters: {
125+
docs: {
126+
description: {
127+
story: "This story shows the search component in its default state. You can interact with it to see hover and focus states.",
128+
},
129+
},
130+
},
131+
};
132+

src/components/search/Search.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React, { forwardRef, useState } from "react";
2+
import { cn } from "../../utils/cn";
3+
import searchIcon from "../../assets/search-icon.svg";
4+
5+
export interface SearchProps
6+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
7+
containerClassName?: string;
8+
inputClassName?: string;
9+
iconClassName?: string;
10+
showIcon?: boolean;
11+
size?: "small" | "default" | "large";
12+
}
13+
14+
export const Search = forwardRef<HTMLInputElement, SearchProps>(
15+
(
16+
{
17+
containerClassName,
18+
inputClassName,
19+
iconClassName,
20+
showIcon = true,
21+
size = "default",
22+
placeholder = "Decoding Bitcoin....",
23+
className,
24+
onFocus,
25+
onBlur,
26+
...props
27+
},
28+
ref,
29+
) => {
30+
const [isFocused, setIsFocused] = useState(false);
31+
32+
const sizeStyles = {
33+
small: {
34+
container: "h-7",
35+
input: "pl-7 pr-2 py-1 text-sm",
36+
icon: "w-3.5 h-3.5 left-2",
37+
},
38+
default: {
39+
container: "h-8",
40+
input: "pl-8 pr-2.5 py-2 text-base",
41+
icon: "w-4 h-4 left-2",
42+
},
43+
large: {
44+
container: "h-10",
45+
input: "pl-10 pr-3 py-2.5 text-lg",
46+
icon: "w-5 h-5 left-2.5",
47+
},
48+
};
49+
50+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
51+
setIsFocused(true);
52+
onFocus?.(e);
53+
};
54+
55+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
56+
setIsFocused(false);
57+
onBlur?.(e);
58+
};
59+
60+
return (
61+
<div
62+
className={cn(
63+
"relative inline-flex items-center",
64+
sizeStyles[size].container,
65+
containerClassName,
66+
)}
67+
>
68+
{showIcon && (
69+
<img
70+
src={searchIcon}
71+
alt="Search"
72+
className={cn(
73+
"absolute pointer-events-none",
74+
sizeStyles[size].icon,
75+
iconClassName,
76+
)}
77+
style={{ filter: "brightness(0)" }}
78+
/>
79+
)}
80+
<input
81+
ref={ref}
82+
type="text"
83+
placeholder={placeholder}
84+
onFocus={handleFocus}
85+
onBlur={handleBlur}
86+
className={cn(
87+
"w-full h-full rounded-lg",
88+
"font-normal leading-none",
89+
"font-quicksand",
90+
"text-[#6C6C6C] placeholder:text-[#6C6C6C]",
91+
isFocused ? "text-black placeholder:text-[#6C6C6C]" : "",
92+
"border border-[#E1DBD0]",
93+
"outline-none",
94+
"bg-[#EFE9DE]",
95+
"hover:bg-[#E1DBD0]",
96+
isFocused ? "bg-[#E1DBD0]" : "",
97+
"transition-colors duration-200",
98+
showIcon ? sizeStyles[size].input : "px-3 py-2",
99+
inputClassName,
100+
className,
101+
)}
102+
{...props}
103+
/>
104+
</div>
105+
);
106+
},
107+
);
108+
109+
Search.displayName = "Search";
110+

src/components/search/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { Search } from "./Search";
2+
export type { SearchProps } from "./Search";
3+

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./components/footer";
33
export * from "./components/carousel";
44
export * from "./components/select";
55
export * from "./components/banner";
6+
export * from "./components/search";

src/styles/tailwind.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');
2+
13
@tailwind base;
24
@tailwind components;
35
@tailwind utilities;

src/types/images.d.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
declare module '*.png' {
2+
const value: string;
3+
export default value;
4+
}
5+
6+
declare module '*.jpg' {
7+
const value: string;
8+
export default value;
9+
}
10+
11+
declare module '*.jpeg' {
12+
const value: string;
13+
export default value;
14+
}
15+
16+
declare module '*.svg' {
17+
const value: string;
18+
export default value;
19+
}
20+
21+
declare module '*.gif' {
22+
const value: string;
23+
export default value;
24+
}
25+
26+
declare module '*.webp' {
27+
const value: string;
28+
export default value;
29+
}
30+

tailwind.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ module.exports = {
44
content: ['./src/**/*.{js,jsx,ts,tsx}'],
55
theme: {
66
extend: {
7+
fontFamily: {
8+
quicksand: ['Quicksand', 'sans-serif'],
9+
},
710
boxShadowColor: {
811
"dark-light": "rgba(255, 255, 255, 0.05)"
912
},

tsup.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@ export default defineConfig({
1313
injectStyle: false,
1414
async onSuccess() {
1515
await fs.promises.copyFile('src/styles/tailwind.output.css', 'dist/styles.css');
16+
// Copy assets folder
17+
await fs.promises.mkdir('dist/assets', { recursive: true });
18+
const assets = await fs.promises.readdir('src/assets');
19+
for (const asset of assets) {
20+
await fs.promises.copyFile(`src/assets/${asset}`, `dist/assets/${asset}`);
21+
}
1622
},
1723
});

0 commit comments

Comments
 (0)