From 0d98535936c2967b6c597766c82b206f151f791a Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 6 Oct 2025 14:25:11 -0600 Subject: [PATCH] search modal content --- .../search/SearchModal/SearchEmptyState.tsx | 13 +++ .../ui/search/SearchModal/SearchFooter.tsx | 42 ++++++++ .../ui/search/SearchModal/SearchInput.tsx | 23 +++++ .../ui/search/SearchModal/SearchModal.tsx | 5 + .../SearchModalContent.stories.tsx | 65 +++++++++++++ .../search/SearchModal/SearchModalContent.tsx | 51 ++++++++++ .../SearchModal/SearchModalContentItems.tsx | 96 +++++++++++++++++++ .../components/ui/search/SearchModal/index.ts | 1 + .../search/SearchModal/search-modal.types.ts | 48 ++++++++++ 9 files changed, 344 insertions(+) create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchEmptyState.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchFooter.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchInput.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchModal.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchModalContent.stories.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchModalContent.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/SearchModalContentItems.tsx create mode 100644 apps/web/src/components/ui/search/SearchModal/index.ts create mode 100644 apps/web/src/components/ui/search/SearchModal/search-modal.types.ts diff --git a/apps/web/src/components/ui/search/SearchModal/SearchEmptyState.tsx b/apps/web/src/components/ui/search/SearchModal/SearchEmptyState.tsx new file mode 100644 index 000000000..5ff449a58 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchEmptyState.tsx @@ -0,0 +1,13 @@ +import { Command } from 'cmdk'; +import React from 'react'; +import type { SearchModalContentProps } from './search-modal.types'; + +export const SearchEmptyState: React.FC> = React.memo( + ({ emptyState }) => { + return ( + + {emptyState} + + ); + } +); diff --git a/apps/web/src/components/ui/search/SearchModal/SearchFooter.tsx b/apps/web/src/components/ui/search/SearchModal/SearchFooter.tsx new file mode 100644 index 000000000..024bb4977 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchFooter.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import ArrowsOppositeDirectionY from '@/components/ui/icons/NucleoIconOutlined/arrows-opposite-direction-y'; +import CommandIcon from '@/components/ui/icons/NucleoIconOutlined/command'; +import ReturnKey from '@/components/ui/icons/NucleoIconOutlined/return-key'; + +export const SearchFooter: React.FC = React.memo(() => { + const footerItems = [ + { + text: 'Select', + icons: [], + }, + { + text: 'Open', + icons: [], + }, + { + text: 'Open in new tab', + icons: [, ], + }, + ]; + + return ( +
+ {footerItems.map((item, index) => ( + + ))} +
+ ); +}); + +const FooterItem = ({ text, icons }: { text: string; icons: React.ReactNode[] }) => { + return ( +
+
+ {icons.map((icon, index) => ( +
{icon}
+ ))} +
+ {text} +
+ ); +}; diff --git a/apps/web/src/components/ui/search/SearchModal/SearchInput.tsx b/apps/web/src/components/ui/search/SearchModal/SearchInput.tsx new file mode 100644 index 000000000..968ac7b2e --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchInput.tsx @@ -0,0 +1,23 @@ +import { Command } from 'cmdk'; +import React from 'react'; +import { cn } from '@/lib/utils'; +import type { SearchModalContentProps } from './search-modal.types'; + +export const SearchInput: React.FC< + Pick & { + searchValue: string; + } +> = React.memo(({ searchValue, onSearchChange, placeholder, filterContent }) => { + return ( +
+ + {filterContent} +
+ ); +}); diff --git a/apps/web/src/components/ui/search/SearchModal/SearchModal.tsx b/apps/web/src/components/ui/search/SearchModal/SearchModal.tsx new file mode 100644 index 000000000..56c5db33d --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchModal.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const SearchModal = () => { + return
SearchModal
; +}; diff --git a/apps/web/src/components/ui/search/SearchModal/SearchModalContent.stories.tsx b/apps/web/src/components/ui/search/SearchModal/SearchModalContent.stories.tsx new file mode 100644 index 000000000..836e490fc --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchModalContent.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { SearchModalContent } from './SearchModalContent'; +import type { SearchItem } from './search-modal.types'; + +const meta: Meta = { + title: 'UI/Search/SearchModalContent', + component: SearchModalContent, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const mockSearchItems: SearchItem[] = [ + { + icon: '🔍', + label: 'Search Result 1', + secondaryLabel: 'This is a secondary label', + value: 'result-1', + keywords: ['search', 'result', 'example'], + type: 'item', + }, + { + icon: '📄', + label: 'Document', + secondaryLabel: 'A document file', + value: 'document-1', + keywords: ['document', 'file', 'pdf'], + type: 'item', + }, + { + icon: '📊', + label: 'Dashboard', + secondaryLabel: 'Analytics dashboard', + value: 'dashboard-1', + keywords: ['dashboard', 'analytics', 'charts'], + type: 'item', + }, + ...Array.from({ length: 10 }).map((_, index) => ({ + icon: '📊', + label: `Dashboard ${index}`, + secondaryLabel: `Analytics dashboard ${index}`, + value: `testing-${index}`, + keywords: ['dashboard', 'analytics', 'charts'], + type: 'item' as const, + })), +]; + +export const Default: Story = { + args: { + searchItems: mockSearchItems, + onSearchChange: fn(), + onSelect: fn(), + onViewSearchItem: fn(), + defaulSearchValue: 'it', + emptyState: 'No results found', + placeholder: 'Search for something', + filterContent:
Filter
, + filterDropdownContent:
Filter Dropdown
, + }, +}; diff --git a/apps/web/src/components/ui/search/SearchModal/SearchModalContent.tsx b/apps/web/src/components/ui/search/SearchModal/SearchModalContent.tsx new file mode 100644 index 000000000..d2f350463 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchModalContent.tsx @@ -0,0 +1,51 @@ +import { Command } from 'cmdk'; +import React, { useState } from 'react'; +import { SearchEmptyState } from './SearchEmptyState'; +import { SearchFooter } from './SearchFooter'; +import { SearchInput } from './SearchInput'; +import { SearchModalContentItems } from './SearchModalContentItems'; +import type { SearchItem, SearchModalContentProps } from './search-modal.types'; + +export const SearchModalContent = ({ + searchItems, + onSearchChange, + onSelect, + onViewSearchItem, + emptyState, + defaulSearchValue = '', + filterContent, + placeholder, + filterDropdownContent, + loading, + secondaryContent, + openSecondaryContent, +}: SearchModalContentProps) => { + const [searchValue, setSearchValue] = useState(defaulSearchValue); + + const onSearchChangePreflight = (searchValue: string) => { + setSearchValue(searchValue); + onSearchChange(searchValue); + }; + + return ( + + +
+ + + + + ); +}; diff --git a/apps/web/src/components/ui/search/SearchModal/SearchModalContentItems.tsx b/apps/web/src/components/ui/search/SearchModal/SearchModalContentItems.tsx new file mode 100644 index 000000000..81526b052 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/SearchModalContentItems.tsx @@ -0,0 +1,96 @@ +import { Command } from 'cmdk'; +import { cn } from '@/lib/utils'; +import type { + SearchItem, + SearchItemGroup, + SearchItemSeperator, + SearchItems, + SearchModalContentProps, +} from './search-modal.types'; + +export const SearchModalContentItems = ({ + searchItems, + onSelect, + onViewSearchItem, +}: Pick, 'searchItems' | 'onSelect' | 'onViewSearchItem'>) => { + return ( + + {searchItems.map((item, index) => ( + + ))} + + ); +}; + +const keyExtractor = (item: SearchItems, index: number): string => { + if (item.type === 'item') { + return String(item.value); + } + return item.type + index; +}; + +const ItemsSelecter = ({ item }: { item: SearchItems }) => { + const type = item.type; + if (type === 'item') { + return ; + } + + if (type === 'group') { + return ; + } + + if (type === 'seperator') { + return ; + } + + const _exhaustiveCheck: never = type; + + return null; +}; + +const SearchItemComponent = ({ + value, + label, + secondaryLabel, + tertiaryLabel, + icon, + onSelect, + loading, + disabled, +}: SearchItem) => { + return ( + { + onSelect?.(); + }} + > + {label} + + ); +}; + +const SearchItemGroupComponent = ({ + item, +}: { + item: SearchItemGroup; +}) => { + return ( + + {item.items.map((item, index) => ( + + ))} + + ); +}; + +const SearchItemSeperatorComponent = ({ item }: { item: SearchItemSeperator }) => { + return ; +}; diff --git a/apps/web/src/components/ui/search/SearchModal/index.ts b/apps/web/src/components/ui/search/SearchModal/index.ts new file mode 100644 index 000000000..14051dc54 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/index.ts @@ -0,0 +1 @@ +export * from './SearchModal'; diff --git a/apps/web/src/components/ui/search/SearchModal/search-modal.types.ts b/apps/web/src/components/ui/search/SearchModal/search-modal.types.ts new file mode 100644 index 000000000..5a56ba907 --- /dev/null +++ b/apps/web/src/components/ui/search/SearchModal/search-modal.types.ts @@ -0,0 +1,48 @@ +export type SearchItem = { + icon?: React.ReactNode; + label: string | React.ReactNode; + secondaryLabel?: string | React.ReactNode; //displayed below the label + tertiaryLabel?: string | React.ReactNode; //displayed to the right of the label + value: T; + keywords?: string[]; + meta?: M; + onSelect?: () => void; + loading?: boolean; + disabled?: boolean; + type: 'item'; +}; + +export type SearchItemGroup = { + label: string | React.ReactNode; + items: SearchItem[]; + type: 'group'; +}; + +export type SearchItemSeperator = { + type: 'seperator'; +}; + +export type SearchItems = + | SearchItem + | SearchItemGroup + | SearchItemSeperator; + +export type SearchModalContentProps = { + defaulSearchValue?: string; + filterDropdownContent?: React.ReactNode; + filterContent?: React.ReactNode; + searchItems: SearchItems[]; + onSearchChange: (searchValue: string) => void; + onSelect: (item: SearchItem) => void; + onViewSearchItem: (item: SearchItem) => void; + emptyState?: React.ReactNode | string; + placeholder?: string; + loading?: boolean; + secondaryContent?: React.ReactNode | null; + openSecondaryContent?: boolean; //if undefined it will close and open with the secondary content +}; + +export type SearchModalProps = SearchModalContentProps & { + open: boolean; + onClose: () => void; +};