@gv-tech/design-system 1.2.0 → 2.0.0

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.
Files changed (39) hide show
  1. package/.github/workflows/release-please.yml +2 -2
  2. package/.release-please-manifest.json +3 -0
  3. package/CHANGELOG.md +74 -0
  4. package/dist/App.d.ts.map +1 -1
  5. package/dist/components/docs/Sidebar.d.ts.map +1 -1
  6. package/dist/components/ui/search.d.ts +16 -0
  7. package/dist/components/ui/search.d.ts.map +1 -0
  8. package/dist/components/ui/search.test.d.ts +2 -0
  9. package/dist/components/ui/search.test.d.ts.map +1 -0
  10. package/dist/index.cjs.js +2 -2
  11. package/dist/index.cjs.js.map +1 -1
  12. package/dist/index.es.js +3 -3
  13. package/dist/index.es.js.map +1 -1
  14. package/dist/pages/components/SearchDocs.d.ts +2 -0
  15. package/dist/pages/components/SearchDocs.d.ts.map +1 -0
  16. package/dist/pages/components/ThemeToggleDocs.d.ts.map +1 -1
  17. package/dist/pages/index.d.ts +1 -0
  18. package/dist/pages/index.d.ts.map +1 -1
  19. package/dist/registry/index.json +14 -0
  20. package/dist/registry/search.json +13 -0
  21. package/dist/registry/search.test.json +13 -0
  22. package/dist/registry/theme-toggle.json +1 -1
  23. package/dist/{vendor-CAF5bxO5.mjs → vendor-BLvpSabH.mjs} +6689 -6623
  24. package/dist/vendor-BLvpSabH.mjs.map +1 -0
  25. package/dist/vendor-n4WFhtJT.js +73 -0
  26. package/dist/vendor-n4WFhtJT.js.map +1 -0
  27. package/package.json +10 -10
  28. package/release-please-config.json +36 -0
  29. package/src/App.tsx +33 -0
  30. package/src/components/docs/Sidebar.tsx +16 -1
  31. package/src/components/ui/search.test.tsx +75 -0
  32. package/src/components/ui/search.tsx +93 -0
  33. package/src/components/ui/theme-toggle.tsx +2 -2
  34. package/src/pages/components/SearchDocs.tsx +194 -0
  35. package/src/pages/components/ThemeToggleDocs.tsx +72 -0
  36. package/src/pages/index.ts +1 -0
  37. package/dist/vendor-CAF5bxO5.mjs.map +0 -1
  38. package/dist/vendor-Hw1BQGd3.js +0 -73
  39. package/dist/vendor-Hw1BQGd3.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gv-tech/design-system",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Garcia Ventures react design system",
5
5
  "repository": "git@github.com:Garcia-Ventures/gvtech-design.git",
6
6
  "license": "MIT",
@@ -83,7 +83,7 @@
83
83
  "@radix-ui/react-toggle": "^1.1.10",
84
84
  "@radix-ui/react-toggle-group": "^1.1.11",
85
85
  "@radix-ui/react-tooltip": "^1.2.8",
86
- "@tailwindcss/postcss": "4",
86
+ "@tailwindcss/postcss": "4.1.18",
87
87
  "class-variance-authority": "^0.7.1",
88
88
  "clsx": "^2.1.1",
89
89
  "cmdk": "^1.1.1",
@@ -91,10 +91,10 @@
91
91
  "embla-carousel-react": "^8.6.0",
92
92
  "lucide-react": "^0.563.0",
93
93
  "next-themes": "^0.4.6",
94
- "react-day-picker": "^9.13.0",
94
+ "react-day-picker": "^9.13.2",
95
95
  "react-hook-form": "^7.71.1",
96
96
  "react-icons": "^5.5.0",
97
- "react-resizable-panels": "^4.6.0",
97
+ "react-resizable-panels": "^4.6.2",
98
98
  "recharts": "2.15.4",
99
99
  "sonner": "^2.0.7",
100
100
  "tailwind-merge": "^3.4.0",
@@ -102,17 +102,17 @@
102
102
  "zod": "^4.3.6"
103
103
  },
104
104
  "devDependencies": {
105
- "@eng618/prettier-config": "^2.6.0",
105
+ "@eng618/prettier-config": "^2.7.0",
106
106
  "@gv-tech/eslint-config": "^0.1.8",
107
107
  "@testing-library/dom": "^10.4.1",
108
108
  "@testing-library/jest-dom": "^6.9.1",
109
109
  "@testing-library/react": "^16.3.2",
110
110
  "@testing-library/user-event": "^14.6.1",
111
- "@types/node": "^25.2.0",
112
- "@types/react": "^19.2.13",
111
+ "@types/node": "^25.2.3",
112
+ "@types/react": "^19.2.14",
113
113
  "@types/react-dom": "^19.2.3",
114
- "@typescript-eslint/eslint-plugin": "^8.54.0",
115
- "@typescript-eslint/parser": "^8.54.0",
114
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
115
+ "@typescript-eslint/parser": "^8.55.0",
116
116
  "@vitejs/plugin-react-swc": "^4.2.3",
117
117
  "axe-core": "^4.11.1",
118
118
  "eslint": "^10.0.0",
@@ -129,7 +129,7 @@
129
129
  "react": "^19.2.4",
130
130
  "react-dom": "^19.2.4",
131
131
  "react-is": "^19.2.4",
132
- "tailwindcss": "4",
132
+ "tailwindcss": "4.1.18",
133
133
  "tsx": "^4.21.0",
134
134
  "typescript": "^5.9.3",
135
135
  "vite": "7.3.1",
@@ -0,0 +1,36 @@
1
+ {
2
+ "packages": {
3
+ ".": {
4
+ "release-type": "node",
5
+ "changelog-sections": [
6
+ {
7
+ "type": "feat",
8
+ "section": "Features"
9
+ },
10
+ {
11
+ "type": "fix",
12
+ "section": "Bug Fixes"
13
+ },
14
+ {
15
+ "type": "docs",
16
+ "section": "Documentation",
17
+ "hidden": false
18
+ },
19
+ {
20
+ "type": "perf",
21
+ "section": "Performance Improvements"
22
+ },
23
+ {
24
+ "type": "refactor",
25
+ "section": "Code Refactoring",
26
+ "hidden": true
27
+ },
28
+ {
29
+ "type": "chore",
30
+ "section": "Miscellaneous Chores",
31
+ "hidden": true
32
+ }
33
+ ]
34
+ }
35
+ }
36
+ }
package/src/App.tsx CHANGED
@@ -11,6 +11,15 @@ import {
11
11
  BreadcrumbSeparator,
12
12
  } from './components/ui/breadcrumb';
13
13
  import { ScrollArea } from './components/ui/scroll-area';
14
+ import {
15
+ CommandEmpty,
16
+ CommandGroup,
17
+ CommandInput,
18
+ CommandItem,
19
+ CommandList,
20
+ Search,
21
+ SearchTrigger,
22
+ } from './components/ui/search';
14
23
  import { Toaster as SonnerToaster } from './components/ui/sonner';
15
24
  import { ThemeToggle } from './components/ui/theme-toggle';
16
25
  import { Toaster } from './components/ui/toaster';
@@ -50,6 +59,7 @@ import {
50
59
  RadioGroupDocs,
51
60
  ResizableDocs,
52
61
  ScrollAreaDocs,
62
+ SearchDocs,
53
63
  SelectDocs,
54
64
  SeparatorDocs,
55
65
  SheetDocs,
@@ -69,6 +79,7 @@ import {
69
79
 
70
80
  function App() {
71
81
  const [activeItem, setActiveItem] = React.useState('getting-started');
82
+ const [searchOpen, setSearchOpen] = React.useState(false);
72
83
 
73
84
  const renderContent = () => {
74
85
  switch (activeItem) {
@@ -167,6 +178,8 @@ function App() {
167
178
  return <ContextMenuDocs />;
168
179
  case 'command':
169
180
  return <CommandDocs />;
181
+ case 'search':
182
+ return <SearchDocs />;
170
183
  case 'sheet':
171
184
  return <SheetDocs />;
172
185
  case 'drawer':
@@ -218,6 +231,26 @@ function App() {
218
231
  </BreadcrumbList>
219
232
  </Breadcrumb>
220
233
  <div className="flex items-center gap-2">
234
+ <Search open={searchOpen} onOpenChange={setSearchOpen}>
235
+ <CommandInput placeholder="Type a command or search..." />
236
+ <CommandList>
237
+ <CommandEmpty>No results found.</CommandEmpty>
238
+ <CommandGroup heading="Components">
239
+ {navItems.map((item) => (
240
+ <CommandItem
241
+ key={item.id}
242
+ onSelect={() => {
243
+ setActiveItem(item.id);
244
+ setSearchOpen(false);
245
+ }}
246
+ >
247
+ {item.label}
248
+ </CommandItem>
249
+ ))}
250
+ </CommandGroup>
251
+ </CommandList>
252
+ </Search>
253
+ <SearchTrigger onClick={() => setSearchOpen(true)} />
221
254
  <ThemeToggle variant="ternary" />
222
255
  </div>
223
256
  </header>
@@ -93,6 +93,7 @@ export const navItems: NavItem[] = [
93
93
  { id: 'dropdown-menu', label: 'Dropdown Menu', category: 'overlay' },
94
94
  { id: 'context-menu', label: 'Context Menu', category: 'overlay' },
95
95
  { id: 'command', label: 'Command', category: 'overlay' },
96
+ { id: 'search', label: 'Search', category: 'overlay' },
96
97
  { id: 'sheet', label: 'Sheet', category: 'overlay' },
97
98
  { id: 'drawer', label: 'Drawer', category: 'overlay' },
98
99
 
@@ -107,6 +108,15 @@ export const navItems: NavItem[] = [
107
108
 
108
109
  export function Sidebar({ activeItem, onItemSelect }: SidebarProps) {
109
110
  const categories = Object.keys(categoryConfig) as ComponentCategory[];
111
+ const [expandedCategories, setExpandedCategories] = React.useState<string[]>(['getting-started', 'forms']);
112
+
113
+ // Ensure the category of the active item is expanded
114
+ React.useEffect(() => {
115
+ const activeNavItem = navItems.find((item) => item.id === activeItem);
116
+ if (activeNavItem && !expandedCategories.includes(activeNavItem.category)) {
117
+ setExpandedCategories((prev) => [...prev, activeNavItem.category]);
118
+ }
119
+ }, [activeItem, expandedCategories]);
110
120
 
111
121
  return (
112
122
  <div className="w-64 border-r bg-muted/50 flex flex-col h-full">
@@ -116,7 +126,12 @@ export function Sidebar({ activeItem, onItemSelect }: SidebarProps) {
116
126
  </div>
117
127
  <ScrollArea className="flex-1">
118
128
  <nav className="p-2">
119
- <Accordion type="multiple" defaultValue={['getting-started', 'forms']} className="w-full space-y-1">
129
+ <Accordion
130
+ type="multiple"
131
+ value={expandedCategories}
132
+ onValueChange={setExpandedCategories}
133
+ className="w-full space-y-1"
134
+ >
120
135
  {categories.map((category) => {
121
136
  const config = categoryConfig[category];
122
137
  const items = navItems.filter((item) => item.category === category);
@@ -0,0 +1,75 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import * as React from 'react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Search, SearchTrigger } from './search';
5
+
6
+ // Mock CommandDialog since it uses Radix Dialog which might need a portal
7
+ vi.mock('@/components/ui/dialog', () => ({
8
+ Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => (open ? <div>{children}</div> : null),
9
+ DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
10
+ DialogPortal: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
11
+ DialogOverlay: () => null,
12
+ }));
13
+
14
+ describe('Search', () => {
15
+ it('renders search trigger', () => {
16
+ render(<SearchTrigger />);
17
+ expect(screen.getByText('Search docs...')).toBeInTheDocument();
18
+ });
19
+
20
+ it('opens search dialog when trigger is clicked (controlled)', () => {
21
+ const onOpenChange = vi.fn();
22
+ render(
23
+ <Search open={false} onOpenChange={onOpenChange}>
24
+ <CommandInput placeholder="Search..." />
25
+ </Search>,
26
+ );
27
+
28
+ // The dialog should be closed initially
29
+ expect(screen.queryByPlaceholderText('Search...')).not.toBeInTheDocument();
30
+ });
31
+
32
+ it('responds to keyboard shortcuts', () => {
33
+ render(
34
+ <Search>
35
+ <CommandInput placeholder="Search..." />
36
+ <CommandList>
37
+ <CommandItem>Result 1</CommandItem>
38
+ </CommandList>
39
+ </Search>,
40
+ );
41
+
42
+ // Simulate Cmd+K
43
+ fireEvent.keyDown(document, { key: 'k', metaKey: true });
44
+
45
+ // Check if dialog content is visible
46
+ expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
47
+ });
48
+
49
+ it('filters results correctly', () => {
50
+ render(
51
+ <Search open={true}>
52
+ <CommandInput placeholder="Search..." />
53
+ <CommandList>
54
+ <CommandEmpty>No results.</CommandEmpty>
55
+ <CommandGroup heading="Components">
56
+ <SearchItem>Button</SearchItem>
57
+ <SearchItem>Input</SearchItem>
58
+ </CommandGroup>
59
+ </CommandList>
60
+ </Search>,
61
+ );
62
+
63
+ const input = screen.getByPlaceholderText('Search...');
64
+ fireEvent.change(input, { target: { value: 'But' } });
65
+
66
+ expect(screen.getByText('Button')).toBeInTheDocument();
67
+ // cmdk removes non-matching items from the DOM
68
+ expect(screen.queryByText('Input')).not.toBeInTheDocument();
69
+ });
70
+ });
71
+
72
+ // Helper component for testing
73
+ function SearchItem({ children }: { children: React.ReactNode }) {
74
+ return <CommandItem>{children}</CommandItem>;
75
+ }
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ CommandDialog,
6
+ CommandEmpty,
7
+ CommandGroup,
8
+ CommandInput,
9
+ CommandItem,
10
+ CommandList,
11
+ } from '@/components/ui/command';
12
+ import { cn } from '@/lib/utils';
13
+ import { Search as SearchIcon } from 'lucide-react';
14
+ import * as React from 'react';
15
+
16
+ export interface SearchProps {
17
+ children?: React.ReactNode;
18
+ open?: boolean;
19
+ onOpenChange?: (open: boolean) => void;
20
+ }
21
+
22
+ export function Search({ children, open: customOpen, onOpenChange }: SearchProps) {
23
+ const [open, setOpen] = React.useState(false);
24
+
25
+ const isControlled = customOpen !== undefined;
26
+ const isOpen = isControlled ? customOpen : open;
27
+
28
+ const setIsOpen = React.useCallback(
29
+ (value: boolean | ((prev: boolean) => boolean)) => {
30
+ if (isControlled) {
31
+ const nextValue = typeof value === 'function' ? value(isOpen) : value;
32
+ onOpenChange?.(nextValue);
33
+ } else {
34
+ setOpen(value);
35
+ }
36
+ },
37
+ [isControlled, isOpen, onOpenChange],
38
+ );
39
+
40
+ React.useEffect(() => {
41
+ const down = (e: KeyboardEvent) => {
42
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
43
+ e.preventDefault();
44
+ setIsOpen((prev) => !prev);
45
+ }
46
+ };
47
+
48
+ document.addEventListener('keydown', down);
49
+ return () => document.removeEventListener('keydown', down);
50
+ }, [setIsOpen]);
51
+
52
+ return (
53
+ <CommandDialog open={isOpen} onOpenChange={setIsOpen}>
54
+ {children}
55
+ </CommandDialog>
56
+ );
57
+ }
58
+
59
+ export interface SearchTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
60
+ className?: string;
61
+ placeholder?: string;
62
+ variant?: 'default' | 'compact';
63
+ }
64
+
65
+ export const SearchTrigger = React.forwardRef<HTMLButtonElement, SearchTriggerProps>(
66
+ ({ className, placeholder = 'Search docs...', variant = 'default', ...props }, ref) => {
67
+ return (
68
+ <Button
69
+ variant="outline"
70
+ className={cn(
71
+ 'relative h-9 text-sm text-muted-foreground transition-all transition-colors',
72
+ variant === 'default'
73
+ ? 'w-full justify-start sm:pr-12 md:w-40 lg:w-64'
74
+ : 'w-9 justify-center px-0 sm:w-24 sm:justify-start sm:px-3 sm:pr-12',
75
+ className,
76
+ )}
77
+ ref={ref}
78
+ {...props}
79
+ >
80
+ <span className="inline-flex items-center gap-2">
81
+ <SearchIcon className="h-4 w-4 shrink-0" />
82
+ {variant === 'default' && <span className="truncate">{placeholder}</span>}
83
+ </span>
84
+ <kbd className="pointer-events-none absolute right-1.5 top-1.5 hidden h-6 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
85
+ <span className="text-xs">⌘</span>K
86
+ </kbd>
87
+ </Button>
88
+ );
89
+ },
90
+ );
91
+ SearchTrigger.displayName = 'SearchTrigger';
92
+
93
+ export { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList };
@@ -72,7 +72,7 @@ export function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, cl
72
72
  return (
73
73
  <DropdownMenu>
74
74
  <DropdownMenuTrigger asChild>
75
- <Button variant="ghost" size="icon" className={cn('h-9 w-9', className)}>
75
+ <Button variant="ghost" size="icon" className={cn('relative h-9 w-9', className)}>
76
76
  <IconToggle />
77
77
  </Button>
78
78
  </DropdownMenuTrigger>
@@ -98,7 +98,7 @@ export function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, cl
98
98
  <Button
99
99
  variant="ghost"
100
100
  size="icon"
101
- className={cn('h-9 w-9', className)}
101
+ className={cn('relative h-9 w-9', className)}
102
102
  onClick={() => handleThemeChange(currentTheme === 'dark' ? 'light' : 'dark')}
103
103
  aria-label="Toggle theme"
104
104
  >
@@ -0,0 +1,194 @@
1
+ import { ComponentSection, ComponentShowcase } from '@/components/docs/ComponentShowcase';
2
+ import { PropsTable } from '@/components/docs/PropsTable';
3
+ import {
4
+ CommandEmpty,
5
+ CommandGroup,
6
+ CommandInput,
7
+ CommandItem,
8
+ CommandList,
9
+ Search,
10
+ SearchTrigger,
11
+ } from '@/components/ui/search';
12
+ import * as React from 'react';
13
+ import { toast } from 'sonner';
14
+
15
+ export function SearchDocs() {
16
+ const [open, setOpen] = React.useState(false);
17
+
18
+ return (
19
+ <ComponentSection title="Search" description="A searchable command palette for navigation and actions.">
20
+ <ComponentShowcase
21
+ title="Quick Search"
22
+ description="A shortcut-ready search trigger for headers. Press Cmd+K or Ctrl+K to open."
23
+ code={`<Search>
24
+ <CommandInput placeholder="Type a command or search..." />
25
+ <CommandList>
26
+ <CommandEmpty>No results found.</CommandEmpty>
27
+ <CommandGroup heading="Suggestions">
28
+ <CommandItem>Calendar</CommandItem>
29
+ <CommandItem>Search Emoji</CommandItem>
30
+ <CommandItem>Calculator</CommandItem>
31
+ </CommandGroup>
32
+ </CommandList>
33
+ </Search>
34
+ <SearchTrigger onClick={() => setOpen(true)} placeholder="Search documentation..." />`}
35
+ >
36
+ <div className="flex items-center gap-4">
37
+ <Search open={open} onOpenChange={setOpen}>
38
+ <CommandInput placeholder="Type a command or search..." />
39
+ <CommandList>
40
+ <CommandEmpty>No results found.</CommandEmpty>
41
+ <CommandGroup heading="Suggestions">
42
+ <CommandItem
43
+ onSelect={() => {
44
+ toast.success('Selected Calendar');
45
+ setOpen(false);
46
+ }}
47
+ >
48
+ Calendar
49
+ </CommandItem>
50
+ <CommandItem
51
+ onSelect={() => {
52
+ toast.success('Selected Search Emoji');
53
+ setOpen(false);
54
+ }}
55
+ >
56
+ Search Emoji
57
+ </CommandItem>
58
+ <CommandItem
59
+ onSelect={() => {
60
+ toast.success('Selected Calculator');
61
+ setOpen(false);
62
+ }}
63
+ >
64
+ Calculator
65
+ </CommandItem>
66
+ </CommandGroup>
67
+ </CommandList>
68
+ </Search>
69
+ <SearchTrigger onClick={() => setOpen(true)} placeholder="Search documentation..." />
70
+ <p className="text-sm text-muted-foreground">Try clicking the trigger or pressing ⌘K</p>
71
+ </div>
72
+ </ComponentShowcase>
73
+
74
+ <ComponentShowcase
75
+ title="Compact Variant"
76
+ description="A smaller version of the trigger, ideal for dense headers or mobile-first layouts."
77
+ code={`<SearchTrigger variant="compact" />`}
78
+ >
79
+ <div className="flex items-center gap-4">
80
+ <SearchTrigger variant="compact" onClick={() => setOpen(true)} />
81
+ <p className="text-sm text-muted-foreground">Compact trigger showing only icon and shortcut</p>
82
+ </div>
83
+ </ComponentShowcase>
84
+
85
+ <div className="space-y-4">
86
+ <h3 className="text-xl font-semibold">Search Props</h3>
87
+ <PropsTable
88
+ props={[
89
+ {
90
+ name: 'open',
91
+ type: 'boolean',
92
+ required: false,
93
+ description: 'Whether the search dialog is open.',
94
+ },
95
+ {
96
+ name: 'onOpenChange',
97
+ type: '(open: boolean) => void',
98
+ required: false,
99
+ description: 'Event handler called when the open state changes.',
100
+ },
101
+ {
102
+ name: 'children',
103
+ type: 'ReactNode',
104
+ required: false,
105
+ description: 'The search content (CommandInput, CommandList, etc.).',
106
+ },
107
+ ]}
108
+ />
109
+ </div>
110
+
111
+ <div className="mt-8 space-y-4">
112
+ <h3 className="text-xl font-semibold">SearchTrigger Props</h3>
113
+ <PropsTable
114
+ props={[
115
+ {
116
+ name: 'className',
117
+ type: 'string',
118
+ required: false,
119
+ description: 'Additional CSS classes to apply.',
120
+ },
121
+ {
122
+ name: 'onClick',
123
+ type: 'MouseEventHandler',
124
+ required: false,
125
+ description: 'Click event handler to trigger the search.',
126
+ },
127
+ {
128
+ name: 'placeholder',
129
+ type: 'string',
130
+ defaultValue: '"Search docs..."',
131
+ required: false,
132
+ description: 'The placeholder text to display in the trigger.',
133
+ },
134
+ {
135
+ name: 'variant',
136
+ type: '"default" | "compact"',
137
+ defaultValue: '"default"',
138
+ required: false,
139
+ description: 'The visual style of the trigger.',
140
+ },
141
+ ]}
142
+ />
143
+ </div>
144
+
145
+ <div className="mt-12 space-y-6">
146
+ <div>
147
+ <h3 className="text-xl font-semibold">Integrations</h3>
148
+ <p className="mt-2 text-muted-foreground">
149
+ The Search component is designed to be highly composable, making it easy to integrate with external search
150
+ providers like Algolia, ElasticSearch, or custom APIs.
151
+ </p>
152
+ </div>
153
+
154
+ <div className="rounded-lg border bg-muted/50 p-6">
155
+ <h4 className="font-medium">External Provider Pattern</h4>
156
+ <p className="mt-1 text-sm text-muted-foreground">
157
+ You can use the `onValueChange` prop of `CommandInput` to trigger external searches and dynamically render
158
+ `CommandGroup` and `CommandItem` components with the results.
159
+ </p>
160
+ <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-xs">
161
+ <code>{`const [results, setResults] = React.useState([]);
162
+
163
+ const handleSearch = async (query) => {
164
+ const data = await algoliaIndex.search(query);
165
+ setResults(data.hits);
166
+ };
167
+
168
+ return (
169
+ <Search>
170
+ <CommandInput
171
+ placeholder="Search docs..."
172
+ onValueChange={handleSearch}
173
+ />
174
+ <CommandList>
175
+ {results.length > 0 ? (
176
+ <CommandGroup heading="Results">
177
+ {results.map((hit) => (
178
+ <CommandItem key={hit.objectID}>
179
+ {hit.title}
180
+ </CommandItem>
181
+ ))}
182
+ </CommandGroup>
183
+ ) : (
184
+ <CommandEmpty>No results found.</CommandEmpty>
185
+ )}
186
+ </CommandList>
187
+ </Search>
188
+ );`}</code>
189
+ </pre>
190
+ </div>
191
+ </div>
192
+ </ComponentSection>
193
+ );
194
+ }
@@ -1,4 +1,5 @@
1
1
  import { ComponentSection, ComponentShowcase } from '@/components/docs/ComponentShowcase';
2
+ import { PropsTable } from '@/components/docs/PropsTable';
2
3
  import { ThemeToggle } from '@/components/ui/theme-toggle';
3
4
  import { useState } from 'react';
4
5
 
@@ -45,6 +46,77 @@ export function ThemeToggleDocs() {
45
46
  <p className="text-sm font-medium">Current Selection: {customTheme}</p>
46
47
  </div>
47
48
  </ComponentShowcase>
49
+
50
+ <div className="space-y-4">
51
+ <h3 className="text-xl font-semibold">ThemeToggle Props</h3>
52
+ <PropsTable
53
+ props={[
54
+ {
55
+ name: 'variant',
56
+ type: '"binary" | "ternary"',
57
+ defaultValue: '"binary"',
58
+ required: false,
59
+ description:
60
+ "The toggle behavior. 'binary' switches between light/dark, while 'ternary' includes system.",
61
+ },
62
+ {
63
+ name: 'onThemeChange',
64
+ type: '(theme: string) => void',
65
+ required: false,
66
+ description: 'Optional callback for custom theme management logic.',
67
+ },
68
+ {
69
+ name: 'customTheme',
70
+ type: 'string',
71
+ required: false,
72
+ description: 'Overrides the internal theme detection (useful for previews or external control).',
73
+ },
74
+ {
75
+ name: 'className',
76
+ type: 'string',
77
+ required: false,
78
+ description: 'Additional CSS classes for the toggle button.',
79
+ },
80
+ ]}
81
+ />
82
+ </div>
83
+
84
+ <div className="mt-12 space-y-6">
85
+ <div>
86
+ <h3 className="text-xl font-semibold">Integration</h3>
87
+ <p className="mt-2 text-muted-foreground">
88
+ The `ThemeToggle` component is built to be flexible and works seamlessly with `next-themes` by default, but
89
+ it can also be used in a fully controlled manner with any theme provider or custom state.
90
+ </p>
91
+ </div>
92
+
93
+ <div className="grid gap-6 md:grid-cols-2">
94
+ <div className="rounded-lg border bg-muted/50 p-6">
95
+ <h4 className="font-medium text-foreground">With next-themes</h4>
96
+ <p className="mt-1 text-sm text-muted-foreground">
97
+ Simply drop the component anywhere. It will automatically detect the `ThemeProvider` and handle switching.
98
+ </p>
99
+ <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-xs">
100
+ <code>{`<ThemeProvider attribute="class">
101
+ <ThemeToggle />
102
+ </ThemeProvider>`}</code>
103
+ </pre>
104
+ </div>
105
+
106
+ <div className="rounded-lg border bg-muted/50 p-6">
107
+ <h4 className="font-medium text-foreground">Custom Provider</h4>
108
+ <p className="mt-1 text-sm text-muted-foreground">
109
+ Pass your own theme state and change handler to integrate with custom logic or external storage.
110
+ </p>
111
+ <pre className="mt-4 overflow-x-auto rounded-md bg-background p-4 text-xs">
112
+ <code>{`<ThemeToggle
113
+ customTheme={myTheme}
114
+ onThemeChange={(t) => updateMyTheme(t)}
115
+ />`}</code>
116
+ </pre>
117
+ </div>
118
+ </div>
119
+ </div>
48
120
  </ComponentSection>
49
121
  );
50
122
  }
@@ -34,6 +34,7 @@ export { ProgressDocs } from './components/ProgressDocs';
34
34
  export { RadioGroupDocs } from './components/RadioGroupDocs';
35
35
  export { ResizableDocs } from './components/ResizableDocs';
36
36
  export { ScrollAreaDocs } from './components/ScrollAreaDocs';
37
+ export { SearchDocs } from './components/SearchDocs';
37
38
  export { SelectDocs } from './components/SelectDocs';
38
39
  export { SeparatorDocs } from './components/SeparatorDocs';
39
40
  export { SheetDocs } from './components/SheetDocs';