@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchDocs.d.ts","sourceRoot":"","sources":["../../../src/pages/components/SearchDocs.tsx"],"names":[],"mappings":"AAcA,wBAAgB,UAAU,4CAmLzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeToggleDocs.d.ts","sourceRoot":"","sources":["../../../src/pages/components/ThemeToggleDocs.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ThemeToggleDocs.d.ts","sourceRoot":"","sources":["../../../src/pages/components/ThemeToggleDocs.tsx"],"names":[],"mappings":"AAKA,wBAAgB,eAAe,4CAoH9B"}
|
package/dist/pages/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { ProgressDocs } from './components/ProgressDocs';
|
|
|
31
31
|
export { RadioGroupDocs } from './components/RadioGroupDocs';
|
|
32
32
|
export { ResizableDocs } from './components/ResizableDocs';
|
|
33
33
|
export { ScrollAreaDocs } from './components/ScrollAreaDocs';
|
|
34
|
+
export { SearchDocs } from './components/SearchDocs';
|
|
34
35
|
export { SelectDocs } from './components/SelectDocs';
|
|
35
36
|
export { SeparatorDocs } from './components/SeparatorDocs';
|
|
36
37
|
export { SheetDocs } from './components/SheetDocs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pages/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pages/index.ts"],"names":[],"mappings":"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"}
|
package/dist/registry/index.json
CHANGED
|
@@ -433,6 +433,20 @@
|
|
|
433
433
|
],
|
|
434
434
|
"type": "registry:ui"
|
|
435
435
|
},
|
|
436
|
+
{
|
|
437
|
+
"name": "search.test",
|
|
438
|
+
"files": [
|
|
439
|
+
"ui/search.test.tsx"
|
|
440
|
+
],
|
|
441
|
+
"type": "registry:ui"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"name": "search",
|
|
445
|
+
"files": [
|
|
446
|
+
"ui/search.tsx"
|
|
447
|
+
],
|
|
448
|
+
"type": "registry:ui"
|
|
449
|
+
},
|
|
436
450
|
{
|
|
437
451
|
"name": "select.test",
|
|
438
452
|
"files": [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"registryDependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "ui/search.tsx",
|
|
9
|
+
"content": "'use client';\n\nimport { Button } from '@/components/ui/button';\nimport {\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '@/components/ui/command';\nimport { cn } from '@/lib/utils';\nimport { Search as SearchIcon } from 'lucide-react';\nimport * as React from 'react';\n\nexport interface SearchProps {\n children?: React.ReactNode;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nexport function Search({ children, open: customOpen, onOpenChange }: SearchProps) {\n const [open, setOpen] = React.useState(false);\n\n const isControlled = customOpen !== undefined;\n const isOpen = isControlled ? customOpen : open;\n\n const setIsOpen = React.useCallback(\n (value: boolean | ((prev: boolean) => boolean)) => {\n if (isControlled) {\n const nextValue = typeof value === 'function' ? value(isOpen) : value;\n onOpenChange?.(nextValue);\n } else {\n setOpen(value);\n }\n },\n [isControlled, isOpen, onOpenChange],\n );\n\n React.useEffect(() => {\n const down = (e: KeyboardEvent) => {\n if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n setIsOpen((prev) => !prev);\n }\n };\n\n document.addEventListener('keydown', down);\n return () => document.removeEventListener('keydown', down);\n }, [setIsOpen]);\n\n return (\n <CommandDialog open={isOpen} onOpenChange={setIsOpen}>\n {children}\n </CommandDialog>\n );\n}\n\nexport interface SearchTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n className?: string;\n placeholder?: string;\n variant?: 'default' | 'compact';\n}\n\nexport const SearchTrigger = React.forwardRef<HTMLButtonElement, SearchTriggerProps>(\n ({ className, placeholder = 'Search docs...', variant = 'default', ...props }, ref) => {\n return (\n <Button\n variant=\"outline\"\n className={cn(\n 'relative h-9 text-sm text-muted-foreground transition-all transition-colors',\n variant === 'default'\n ? 'w-full justify-start sm:pr-12 md:w-40 lg:w-64'\n : 'w-9 justify-center px-0 sm:w-24 sm:justify-start sm:px-3 sm:pr-12',\n className,\n )}\n ref={ref}\n {...props}\n >\n <span className=\"inline-flex items-center gap-2\">\n <SearchIcon className=\"h-4 w-4 shrink-0\" />\n {variant === 'default' && <span className=\"truncate\">{placeholder}</span>}\n </span>\n <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\">\n <span className=\"text-xs\">⌘</span>K\n </kbd>\n </Button>\n );\n },\n);\nSearchTrigger.displayName = 'SearchTrigger';\n\nexport { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList };\n",
|
|
10
|
+
"type": "registry:ui"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search.test",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"registryDependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "ui/search.test.tsx",
|
|
9
|
+
"content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport * as React from 'react';\nimport { describe, expect, it, vi } from 'vitest';\nimport { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Search, SearchTrigger } from './search';\n\n// Mock CommandDialog since it uses Radix Dialog which might need a portal\nvi.mock('@/components/ui/dialog', () => ({\n Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => (open ? <div>{children}</div> : null),\n DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n DialogPortal: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n DialogOverlay: () => null,\n}));\n\ndescribe('Search', () => {\n it('renders search trigger', () => {\n render(<SearchTrigger />);\n expect(screen.getByText('Search docs...')).toBeInTheDocument();\n });\n\n it('opens search dialog when trigger is clicked (controlled)', () => {\n const onOpenChange = vi.fn();\n render(\n <Search open={false} onOpenChange={onOpenChange}>\n <CommandInput placeholder=\"Search...\" />\n </Search>,\n );\n\n // The dialog should be closed initially\n expect(screen.queryByPlaceholderText('Search...')).not.toBeInTheDocument();\n });\n\n it('responds to keyboard shortcuts', () => {\n render(\n <Search>\n <CommandInput placeholder=\"Search...\" />\n <CommandList>\n <CommandItem>Result 1</CommandItem>\n </CommandList>\n </Search>,\n );\n\n // Simulate Cmd+K\n fireEvent.keyDown(document, { key: 'k', metaKey: true });\n\n // Check if dialog content is visible\n expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();\n });\n\n it('filters results correctly', () => {\n render(\n <Search open={true}>\n <CommandInput placeholder=\"Search...\" />\n <CommandList>\n <CommandEmpty>No results.</CommandEmpty>\n <CommandGroup heading=\"Components\">\n <SearchItem>Button</SearchItem>\n <SearchItem>Input</SearchItem>\n </CommandGroup>\n </CommandList>\n </Search>,\n );\n\n const input = screen.getByPlaceholderText('Search...');\n fireEvent.change(input, { target: { value: 'But' } });\n\n expect(screen.getByText('Button')).toBeInTheDocument();\n // cmdk removes non-matching items from the DOM\n expect(screen.queryByText('Input')).not.toBeInTheDocument();\n });\n});\n\n// Helper component for testing\nfunction SearchItem({ children }: { children: React.ReactNode }) {\n return <CommandItem>{children}</CommandItem>;\n}\n",
|
|
10
|
+
"type": "registry:ui"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": "ui/theme-toggle.tsx",
|
|
9
|
-
"content": "import { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { cn } from '@/lib/utils';\nimport { Moon, Sun, SunMoon } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nexport interface ThemeToggleProps {\n /**\n * The mode of the theme toggle. 'binary' allows toggling between light and dark. 'ternary' allows choosing between\n * light, dark, and system.\n *\n * @default 'binary'\n */\n variant?: 'binary' | 'ternary';\n /** Optional callback when the theme changes. */\n onThemeChange?: (theme: string) => void;\n /** Optional current theme value for external control. */\n customTheme?: string;\n /** Optional className for the button. */\n className?: string;\n}\n\nexport function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, className }: ThemeToggleProps) {\n const { theme: nextTheme, setTheme: setNextTheme, resolvedTheme } = useTheme();\n\n // Use customTheme if provided, otherwise fallback to next-themes\n const currentTheme = customTheme ?? nextTheme;\n\n // Determine the effective theme for icon rendering\n const effectiveTheme = customTheme ? customTheme : resolvedTheme;\n const isDark = effectiveTheme === 'dark';\n const isSystem = currentTheme === 'system';\n\n const handleThemeChange = (newTheme: string) => {\n if (onThemeChange) {\n onThemeChange(newTheme);\n } else {\n setNextTheme(newTheme);\n }\n };\n\n const IconToggle = () => (\n <>\n <Sun\n className={cn(\n 'h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && !isDark ? 'rotate-0 scale-100' : '-rotate-90 scale-0',\n )}\n />\n <Moon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && isDark ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <SunMoon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n isSystem ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <span className=\"sr-only\">Toggle theme</span>\n </>\n );\n\n if (variant === 'ternary') {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className={cn('h-9 w-9', className)}>\n <IconToggle />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => handleThemeChange('light')}>\n <Sun className=\"mr-2 h-4 w-4\" />\n <span>Light</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('dark')}>\n <Moon className=\"mr-2 h-4 w-4\" />\n <span>Dark</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('system')}>\n <SunMoon className=\"mr-2 h-4 w-4\" />\n <span>System</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('h-9 w-9', className)}\n onClick={() => handleThemeChange(currentTheme === 'dark' ? 'light' : 'dark')}\n aria-label=\"Toggle theme\"\n >\n <IconToggle />\n </Button>\n );\n}\n",
|
|
9
|
+
"content": "import { Button } from '@/components/ui/button';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { cn } from '@/lib/utils';\nimport { Moon, Sun, SunMoon } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nexport interface ThemeToggleProps {\n /**\n * The mode of the theme toggle. 'binary' allows toggling between light and dark. 'ternary' allows choosing between\n * light, dark, and system.\n *\n * @default 'binary'\n */\n variant?: 'binary' | 'ternary';\n /** Optional callback when the theme changes. */\n onThemeChange?: (theme: string) => void;\n /** Optional current theme value for external control. */\n customTheme?: string;\n /** Optional className for the button. */\n className?: string;\n}\n\nexport function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, className }: ThemeToggleProps) {\n const { theme: nextTheme, setTheme: setNextTheme, resolvedTheme } = useTheme();\n\n // Use customTheme if provided, otherwise fallback to next-themes\n const currentTheme = customTheme ?? nextTheme;\n\n // Determine the effective theme for icon rendering\n const effectiveTheme = customTheme ? customTheme : resolvedTheme;\n const isDark = effectiveTheme === 'dark';\n const isSystem = currentTheme === 'system';\n\n const handleThemeChange = (newTheme: string) => {\n if (onThemeChange) {\n onThemeChange(newTheme);\n } else {\n setNextTheme(newTheme);\n }\n };\n\n const IconToggle = () => (\n <>\n <Sun\n className={cn(\n 'h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && !isDark ? 'rotate-0 scale-100' : '-rotate-90 scale-0',\n )}\n />\n <Moon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n !isSystem && isDark ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <SunMoon\n className={cn(\n 'absolute h-[1.2rem] w-[1.2rem] transition-all',\n isSystem ? 'rotate-0 scale-100' : 'rotate-90 scale-0',\n )}\n />\n <span className=\"sr-only\">Toggle theme</span>\n </>\n );\n\n if (variant === 'ternary') {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className={cn('relative h-9 w-9', className)}>\n <IconToggle />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => handleThemeChange('light')}>\n <Sun className=\"mr-2 h-4 w-4\" />\n <span>Light</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('dark')}>\n <Moon className=\"mr-2 h-4 w-4\" />\n <span>Dark</span>\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => handleThemeChange('system')}>\n <SunMoon className=\"mr-2 h-4 w-4\" />\n <span>System</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn('relative h-9 w-9', className)}\n onClick={() => handleThemeChange(currentTheme === 'dark' ? 'light' : 'dark')}\n aria-label=\"Toggle theme\"\n >\n <IconToggle />\n </Button>\n );\n}\n",
|
|
10
10
|
"type": "registry:ui"
|
|
11
11
|
}
|
|
12
12
|
]
|