From 4b7fdaf525add133b141f5f5c5d25edb680b6492 Mon Sep 17 00:00:00 2001 From: jrakibi Date: Sat, 25 Oct 2025 12:12:30 +0100 Subject: [PATCH 1/5] feat: add Search component --- src/assets/search-icon.svg | 3 + src/components/search/Search.stories.tsx | 132 +++++++++++++++++++++++ src/components/search/Search.tsx | 110 +++++++++++++++++++ src/components/search/index.tsx | 3 + src/index.ts | 1 + src/styles/tailwind.css | 2 + src/types/images.d.ts | 30 ++++++ tailwind.config.js | 3 + tsup.config.ts | 6 ++ 9 files changed, 290 insertions(+) create mode 100644 src/assets/search-icon.svg create mode 100644 src/components/search/Search.stories.tsx create mode 100644 src/components/search/Search.tsx create mode 100644 src/components/search/index.tsx create mode 100644 src/types/images.d.ts diff --git a/src/assets/search-icon.svg b/src/assets/search-icon.svg new file mode 100644 index 0000000..82b0ec1 --- /dev/null +++ b/src/assets/search-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/search/Search.stories.tsx b/src/components/search/Search.stories.tsx new file mode 100644 index 0000000..c08e89e --- /dev/null +++ b/src/components/search/Search.stories.tsx @@ -0,0 +1,132 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Search } from "./Search"; + +const meta = { + title: "Components/Search", + component: Search, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + size: { + control: "select", + options: ["small", "default", "large"], + description: "Size of the search input", + }, + showIcon: { + control: "boolean", + description: "Whether to show the search icon", + }, + placeholder: { + control: "text", + description: "Placeholder text", + }, + disabled: { + control: "boolean", + description: "Whether the input is disabled", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: "Decoding Bitcoin....", + style: { width: "269px" }, + }, +}; + +export const Hover: Story = { + args: { + placeholder: "Decoding Bitcoin....", + style: { width: "269px" }, + }, + parameters: { + pseudo: { hover: true }, + }, +}; + +export const Focused: Story = { + args: { + placeholder: "Decoding Bitcoin....", + defaultValue: "Bitc", + autoFocus: true, + style: { width: "269px" }, + }, +}; + +export const WithValue: Story = { + args: { + placeholder: "Search...", + defaultValue: "Bitcoin technology", + style: { width: "269px" }, + }, +}; + +export const WithoutIcon: Story = { + args: { + placeholder: "Decoding Bitcoin....", + showIcon: false, + style: { width: "269px" }, + }, +}; + +export const Disabled: Story = { + args: { + placeholder: "Decoding Bitcoin....", + disabled: true, + style: { width: "269px" }, + }, +}; + +export const Small: Story = { + args: { + placeholder: "Search...", + size: "small", + style: { width: "200px" }, + }, +}; + +export const Large: Story = { + args: { + placeholder: "Decoding Bitcoin....", + size: "large", + style: { width: "350px" }, + }, +}; + +export const FullWidth: Story = { + args: { + placeholder: "Decoding Bitcoin....", + containerClassName: "w-full", + style: { width: "500px" }, + }, +}; + +export const CustomStyling: Story = { + args: { + placeholder: "Custom styled search...", + inputClassName: "bg-bdp-hover-primary", + iconClassName: "text-bdp-accent", + style: { width: "300px" }, + }, +}; + +export const AllStates: Story = { + args: { + placeholder: "Decoding Bitcoin....", + style: { width: "269px" }, + }, + parameters: { + docs: { + description: { + story: "This story shows the search component in its default state. You can interact with it to see hover and focus states.", + }, + }, + }, +}; + diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx new file mode 100644 index 0000000..fb24922 --- /dev/null +++ b/src/components/search/Search.tsx @@ -0,0 +1,110 @@ +import React, { forwardRef, useState } from "react"; +import { cn } from "../../utils/cn"; +import searchIcon from "../../assets/search-icon.svg"; + +export interface SearchProps + extends Omit, "size"> { + containerClassName?: string; + inputClassName?: string; + iconClassName?: string; + showIcon?: boolean; + size?: "small" | "default" | "large"; +} + +export const Search = forwardRef( + ( + { + containerClassName, + inputClassName, + iconClassName, + showIcon = true, + size = "default", + placeholder = "Decoding Bitcoin....", + className, + onFocus, + onBlur, + ...props + }, + ref, + ) => { + const [isFocused, setIsFocused] = useState(false); + + const sizeStyles = { + small: { + container: "h-7", + input: "pl-7 pr-2 py-1 text-sm", + icon: "w-3.5 h-3.5 left-2", + }, + default: { + container: "h-8", + input: "pl-8 pr-2.5 py-2 text-base", + icon: "w-4 h-4 left-2", + }, + large: { + container: "h-10", + input: "pl-10 pr-3 py-2.5 text-lg", + icon: "w-5 h-5 left-2.5", + }, + }; + + const handleFocus = (e: React.FocusEvent) => { + setIsFocused(true); + onFocus?.(e); + }; + + const handleBlur = (e: React.FocusEvent) => { + setIsFocused(false); + onBlur?.(e); + }; + + return ( +
+ {showIcon && ( + Search + )} + +
+ ); + }, +); + +Search.displayName = "Search"; + diff --git a/src/components/search/index.tsx b/src/components/search/index.tsx new file mode 100644 index 0000000..1f72604 --- /dev/null +++ b/src/components/search/index.tsx @@ -0,0 +1,3 @@ +export { Search } from "./Search"; +export type { SearchProps } from "./Search"; + diff --git a/src/index.ts b/src/index.ts index 20e69ed..73fc2e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from "./components/footer"; export * from "./components/carousel"; export * from "./components/select"; export * from "./components/banner"; +export * from "./components/search"; diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 80a32ab..2be0c50 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/src/types/images.d.ts b/src/types/images.d.ts new file mode 100644 index 0000000..895a1f4 --- /dev/null +++ b/src/types/images.d.ts @@ -0,0 +1,30 @@ +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} + +declare module '*.jpeg' { + const value: string; + export default value; +} + +declare module '*.svg' { + const value: string; + export default value; +} + +declare module '*.gif' { + const value: string; + export default value; +} + +declare module '*.webp' { + const value: string; + export default value; +} + diff --git a/tailwind.config.js b/tailwind.config.js index 1fcda69..e41effb 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,6 +4,9 @@ module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: { + fontFamily: { + quicksand: ['Quicksand', 'sans-serif'], + }, boxShadowColor: { "dark-light": "rgba(255, 255, 255, 0.05)" }, diff --git a/tsup.config.ts b/tsup.config.ts index 1fc1773..dc22d43 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -13,5 +13,11 @@ export default defineConfig({ injectStyle: false, async onSuccess() { await fs.promises.copyFile('src/styles/tailwind.output.css', 'dist/styles.css'); + // Copy assets folder + await fs.promises.mkdir('dist/assets', { recursive: true }); + const assets = await fs.promises.readdir('src/assets'); + for (const asset of assets) { + await fs.promises.copyFile(`src/assets/${asset}`, `dist/assets/${asset}`); + } }, }); \ No newline at end of file From c05f2a48dc2385bc7aca2eefb70907d09ce15f05 Mon Sep 17 00:00:00 2001 From: jrakibi Date: Sun, 26 Oct 2025 19:44:17 +0100 Subject: [PATCH 2/5] add PropTypes validation and fix linter errors (#43) * add PropTypes validation and fix linter errors * run formatter * fix typo --- package.json | 2 ++ src/components/search/Search.stories.tsx | 5 ++-- src/components/search/Search.tsx | 12 ++++++++ src/components/search/index.tsx | 1 - src/components/select/useSelectNavigate.tsx | 4 +-- src/stories/Configure.mdx | 33 ++++++++++++--------- src/styles/tailwind.css | 2 +- src/types/images.d.ts | 13 ++++---- yarn.lock | 9 ++++++ 9 files changed, 53 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index eb4a90f..8f027c4 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@types/eslint": "^9", + "@types/prop-types": "^15", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.4.0", @@ -84,6 +85,7 @@ "jest-environment-jsdom": "^29.7.0", "postcss": "^8.4.38", "prettier": "^3.3.3", + "prop-types": "^15.8.1", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", "semantic-release": "^24.0.0", diff --git a/src/components/search/Search.stories.tsx b/src/components/search/Search.stories.tsx index c08e89e..583e27c 100644 --- a/src/components/search/Search.stories.tsx +++ b/src/components/search/Search.stories.tsx @@ -1,4 +1,3 @@ -import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { Search } from "./Search"; @@ -124,9 +123,9 @@ export const AllStates: Story = { parameters: { docs: { description: { - story: "This story shows the search component in its default state. You can interact with it to see hover and focus states.", + story: + "This story shows the search component in its default state. You can interact with it to see hover and focus states.", }, }, }, }; - diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx index fb24922..27c117a 100644 --- a/src/components/search/Search.tsx +++ b/src/components/search/Search.tsx @@ -1,4 +1,5 @@ import React, { forwardRef, useState } from "react"; +import PropTypes from "prop-types"; import { cn } from "../../utils/cn"; import searchIcon from "../../assets/search-icon.svg"; @@ -108,3 +109,14 @@ export const Search = forwardRef( Search.displayName = "Search"; +Search.propTypes = { + containerClassName: PropTypes.string, + inputClassName: PropTypes.string, + iconClassName: PropTypes.string, + showIcon: PropTypes.bool, + size: PropTypes.oneOf(["small", "default", "large"]), + placeholder: PropTypes.string, + className: PropTypes.string, + onFocus: PropTypes.func, + onBlur: PropTypes.func, +}; diff --git a/src/components/search/index.tsx b/src/components/search/index.tsx index 1f72604..35e06d6 100644 --- a/src/components/search/index.tsx +++ b/src/components/search/index.tsx @@ -1,3 +1,2 @@ export { Search } from "./Search"; export type { SearchProps } from "./Search"; - diff --git a/src/components/select/useSelectNavigate.tsx b/src/components/select/useSelectNavigate.tsx index 844ab1b..f33cab2 100644 --- a/src/components/select/useSelectNavigate.tsx +++ b/src/components/select/useSelectNavigate.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { isInViewport } from "../../utils/navigation"; -type ChekboxNavigateProps = { +type CheckboxNavigateProps = { checkboxContainer: React.MutableRefObject | null; searchEl: React.MutableRefObject | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -12,7 +12,7 @@ const useCheckboxNavigate = ({ checkboxContainer, searchEl, options, -}: ChekboxNavigateProps) => { +}: CheckboxNavigateProps) => { const checkboxNavIndex = useRef(null); const [currentNavigateCheckbox, setcurrentNavigateCheckbox] = useState(""); diff --git a/src/stories/Configure.mdx b/src/stories/Configure.mdx index 5157090..717600a 100644 --- a/src/stories/Configure.mdx +++ b/src/stories/Configure.mdx @@ -15,21 +15,23 @@ import Accessibility from "./assets/accessibility.png"; import Theming from "./assets/theming.png"; import AddonLibrary from "./assets/addon-library.png"; -export const RightArrow = () => ( + - - + > + + +); @@ -38,6 +40,7 @@ export const RightArrow = () =>
@@ -84,6 +87,7 @@ export const RightArrow = () =>
@@ -203,6 +207,7 @@ export const RightArrow = () => Discover tutorials
+