@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.
- package/.github/workflows/release-please.yml +2 -2
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +74 -0
- package/dist/App.d.ts.map +1 -1
- package/dist/components/docs/Sidebar.d.ts.map +1 -1
- package/dist/components/ui/search.d.ts +16 -0
- package/dist/components/ui/search.d.ts.map +1 -0
- package/dist/components/ui/search.test.d.ts +2 -0
- package/dist/components/ui/search.test.d.ts.map +1 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +3 -3
- package/dist/index.es.js.map +1 -1
- package/dist/pages/components/SearchDocs.d.ts +2 -0
- package/dist/pages/components/SearchDocs.d.ts.map +1 -0
- package/dist/pages/components/ThemeToggleDocs.d.ts.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/registry/index.json +14 -0
- package/dist/registry/search.json +13 -0
- package/dist/registry/search.test.json +13 -0
- package/dist/registry/theme-toggle.json +1 -1
- package/dist/{vendor-CAF5bxO5.mjs → vendor-BLvpSabH.mjs} +6689 -6623
- package/dist/vendor-BLvpSabH.mjs.map +1 -0
- package/dist/vendor-n4WFhtJT.js +73 -0
- package/dist/vendor-n4WFhtJT.js.map +1 -0
- package/package.json +10 -10
- package/release-please-config.json +36 -0
- package/src/App.tsx +33 -0
- package/src/components/docs/Sidebar.tsx +16 -1
- package/src/components/ui/search.test.tsx +75 -0
- package/src/components/ui/search.tsx +93 -0
- package/src/components/ui/theme-toggle.tsx +2 -2
- package/src/pages/components/SearchDocs.tsx +194 -0
- package/src/pages/components/ThemeToggleDocs.tsx +72 -0
- package/src/pages/index.ts +1 -0
- package/dist/vendor-CAF5bxO5.mjs.map +0 -1
- package/dist/vendor-Hw1BQGd3.js +0 -73
- 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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
112
|
-
"@types/react": "^19.2.
|
|
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.
|
|
115
|
-
"@typescript-eslint/parser": "^8.
|
|
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
|
|
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
|
}
|
package/src/pages/index.ts
CHANGED
|
@@ -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';
|