@pattern-stack/frontend-patterns 0.0.3 → 0.0.4
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/dist/index.es.js +1 -1
- package/dist/index.js +1 -0
- package/package.json +5 -3
- package/src/App.css +42 -0
- package/src/App.tsx +54 -0
- package/src/__tests__/README.md +221 -0
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
- package/src/__tests__/atoms/ui/button.test.tsx +68 -0
- package/src/__tests__/atoms/utils/simple.test.ts +18 -0
- package/src/__tests__/atoms/utils/utils.test.ts +77 -0
- package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
- package/src/__tests__/setup.ts +51 -0
- package/src/__tests__/utils.tsx +123 -0
- package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
- package/src/atoms/composed/Accordion/index.ts +1 -0
- package/src/atoms/composed/Alert/Alert.tsx +132 -0
- package/src/atoms/composed/Alert/index.ts +1 -0
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
- package/src/atoms/composed/Breadcrumb/index.ts +1 -0
- package/src/atoms/composed/Chart/Chart.tsx +425 -0
- package/src/atoms/composed/Chart/index.ts +2 -0
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
- package/src/atoms/composed/ColorSwatch/index.ts +1 -0
- package/src/atoms/composed/DarkModeToggle.tsx +66 -0
- package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
- package/src/atoms/composed/DataBadge/index.ts +1 -0
- package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
- package/src/atoms/composed/DataTable/index.ts +2 -0
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
- package/src/atoms/composed/DateTimePicker/index.ts +2 -0
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
- package/src/atoms/composed/DetailedCard/index.ts +2 -0
- package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
- package/src/atoms/composed/EmptyState/index.ts +1 -0
- package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
- package/src/atoms/composed/FileUpload/index.ts +2 -0
- package/src/atoms/composed/FormField/FormField.tsx +92 -0
- package/src/atoms/composed/FormField/index.ts +1 -0
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
- package/src/atoms/composed/GlobalSearch/index.ts +1 -0
- package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
- package/src/atoms/composed/IconBadge/index.ts +2 -0
- package/src/atoms/composed/Modal/Modal.tsx +223 -0
- package/src/atoms/composed/Modal/index.ts +2 -0
- package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
- package/src/atoms/composed/ProgressBar/index.ts +1 -0
- package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
- package/src/atoms/composed/StatCard/index.ts +1 -0
- package/src/atoms/composed/StyleGuide.tsx +717 -0
- package/src/atoms/composed/Toast/Toast.tsx +219 -0
- package/src/atoms/composed/Toast/index.ts +1 -0
- package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
- package/src/atoms/composed/Tooltip/index.ts +1 -0
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
- package/src/atoms/composed/UserAvatar/index.ts +1 -0
- package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
- package/src/atoms/composed/UserMenu/index.ts +1 -0
- package/src/atoms/composed/index.ts +29 -0
- package/src/atoms/hooks/useApi.ts +80 -0
- package/src/atoms/hooks/useHealth.ts +17 -0
- package/src/atoms/index.ts +13 -0
- package/src/atoms/services/api/client.ts +134 -0
- package/src/atoms/services/auth-service.ts +248 -0
- package/src/atoms/services/health.ts +15 -0
- package/src/atoms/services/index.ts +3 -0
- package/src/atoms/shared/config/constants.ts +17 -0
- package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
- package/src/atoms/shared/config/environment.ts +10 -0
- package/src/atoms/shared/index.ts +4 -0
- package/src/atoms/shared/styles/color-palettes.css +566 -0
- package/src/atoms/types/auth.ts +62 -0
- package/src/atoms/types/generated.ts +1469 -0
- package/src/atoms/types/index.ts +4 -0
- package/src/atoms/types/loading.ts +28 -0
- package/src/atoms/ui/Badge.tsx +30 -0
- package/src/atoms/ui/ErrorBoundary.tsx +59 -0
- package/src/atoms/ui/Select.tsx +53 -0
- package/src/atoms/ui/Switch.tsx +42 -0
- package/src/atoms/ui/Tabs.tsx +118 -0
- package/src/atoms/ui/avatar.tsx +48 -0
- package/src/atoms/ui/button.tsx +70 -0
- package/src/atoms/ui/card.tsx +76 -0
- package/src/atoms/ui/dropdown-menu.tsx +199 -0
- package/src/atoms/ui/index.ts +39 -0
- package/src/atoms/ui/input.tsx +23 -0
- package/src/atoms/ui/label.tsx +23 -0
- package/src/atoms/ui/skeleton.tsx +13 -0
- package/src/atoms/ui/spinner.tsx +49 -0
- package/src/atoms/ui/table.tsx +116 -0
- package/src/atoms/utils/animations.ts +135 -0
- package/src/atoms/utils/tooltip-helpers.ts +140 -0
- package/src/atoms/utils/utils.ts +9 -0
- package/src/features/auth/components/LoginForm.tsx +168 -0
- package/src/features/auth/components/LogoutButton.tsx +19 -0
- package/src/features/auth/components/ProtectedRoute.tsx +60 -0
- package/src/features/auth/components/index.ts +4 -0
- package/src/features/auth/hooks/index.ts +2 -0
- package/src/features/auth/hooks/useAuth.tsx +205 -0
- package/src/features/auth/hooks/usePermissions.ts +35 -0
- package/src/features/auth/index.ts +2 -0
- package/src/features/index.ts +2 -0
- package/src/index.css +704 -0
- package/src/index.ts +13 -0
- package/src/main.tsx +48 -0
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +75 -0
- package/src/molecules/forms/SearchInput.tsx +259 -0
- package/src/molecules/forms/index.ts +4 -0
- package/src/molecules/index.ts +4 -0
- package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
- package/src/molecules/layout/AppHeader/index.ts +1 -0
- package/src/molecules/layout/AppLayout.tsx +29 -0
- package/src/molecules/layout/PageTemplate.tsx +87 -0
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
- package/src/molecules/layout/SectionHeader/index.ts +1 -0
- package/src/molecules/layout/ShowcaseSection.tsx +57 -0
- package/src/molecules/layout/Sidebar.tsx +144 -0
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
- package/src/molecules/layout/SidebarButton/index.ts +1 -0
- package/src/molecules/layout/SidebarContext.tsx +31 -0
- package/src/molecules/layout/index.ts +7 -0
- package/src/molecules/navigation/NavMenu.tsx +188 -0
- package/src/molecules/navigation/Pagination.tsx +172 -0
- package/src/molecules/navigation/index.ts +4 -0
- package/src/organisms/index.ts +5 -0
- package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
- package/src/organisms/showcase/index.ts +1 -0
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
- package/src/pages/AdminShowcase/index.tsx +3 -0
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
- package/src/pages/ComponentShowcase/index.tsx +188 -0
- package/src/pages/index.ts +2 -0
- package/src/templates/AuthTemplate.tsx +216 -0
- package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
- package/src/templates/DashboardTemplate.tsx +232 -0
- package/src/templates/DataTemplate.tsx +319 -0
- package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
- package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
- package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
- package/src/templates/admin/index.ts +29 -0
- package/src/templates/factory.tsx +169 -0
- package/src/templates/index.ts +37 -0
- package/src/vite-env.d.ts +1 -0
package/src/main.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
5
|
+
import { AuthProvider } from './features/auth'
|
|
6
|
+
import { SidebarProvider } from './molecules/layout'
|
|
7
|
+
import './index.css'
|
|
8
|
+
import App from './App.tsx'
|
|
9
|
+
|
|
10
|
+
// Suppress browser extension errors in development
|
|
11
|
+
if (import.meta.env.DEV) {
|
|
12
|
+
const originalError = console.error;
|
|
13
|
+
console.error = (...args) => {
|
|
14
|
+
const message = args[0];
|
|
15
|
+
if (
|
|
16
|
+
typeof message === 'string' &&
|
|
17
|
+
(message.includes('Deprecated API for given entry type') ||
|
|
18
|
+
message.includes('message channel closed before a response was received') ||
|
|
19
|
+
message.includes('Extension context invalidated'))
|
|
20
|
+
) {
|
|
21
|
+
return; // Suppress extension-related errors
|
|
22
|
+
}
|
|
23
|
+
originalError(...args);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const queryClient = new QueryClient({
|
|
28
|
+
defaultOptions: {
|
|
29
|
+
queries: {
|
|
30
|
+
retry: 1,
|
|
31
|
+
refetchOnWindowFocus: false,
|
|
32
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
createRoot(document.getElementById('root')!).render(
|
|
38
|
+
<StrictMode>
|
|
39
|
+
<QueryClientProvider client={queryClient}>
|
|
40
|
+
<AuthProvider>
|
|
41
|
+
<SidebarProvider>
|
|
42
|
+
<App />
|
|
43
|
+
</SidebarProvider>
|
|
44
|
+
</AuthProvider>
|
|
45
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
46
|
+
</QueryClientProvider>
|
|
47
|
+
</StrictMode>,
|
|
48
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '../../atoms/utils/utils';
|
|
3
|
+
|
|
4
|
+
export interface FormGroupProps {
|
|
5
|
+
/** Group title */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Group description */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Group content */
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/** Visual variant */
|
|
12
|
+
variant?: 'default' | 'card' | 'section';
|
|
13
|
+
/** Additional CSS classes */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Category-based styling */
|
|
16
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const FormGroup: React.FC<FormGroupProps> = ({
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
children,
|
|
23
|
+
variant = 'default',
|
|
24
|
+
className,
|
|
25
|
+
category
|
|
26
|
+
}) => {
|
|
27
|
+
const variantClasses = {
|
|
28
|
+
default: 'space-y-4',
|
|
29
|
+
card: 'space-y-4 p-6 bg-card border border-border rounded-lg',
|
|
30
|
+
section: 'space-y-6 pb-6 border-b border-border last:border-b-0 last:pb-0'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const titleClasses = {
|
|
34
|
+
default: 'text-lg font-semibold text-foreground',
|
|
35
|
+
card: 'text-lg font-semibold text-foreground',
|
|
36
|
+
section: 'text-xl font-semibold text-foreground'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
variantClasses[variant],
|
|
43
|
+
category && variant === 'card' && `border-category-${category}/20`,
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
data-component-name="FormGroup"
|
|
47
|
+
>
|
|
48
|
+
{/* Header */}
|
|
49
|
+
{(title || description) && (
|
|
50
|
+
<div className="space-y-1">
|
|
51
|
+
{title && (
|
|
52
|
+
<h3
|
|
53
|
+
className={cn(
|
|
54
|
+
titleClasses[variant],
|
|
55
|
+
category && `text-category-${category}`
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{title}
|
|
59
|
+
</h3>
|
|
60
|
+
)}
|
|
61
|
+
{description && (
|
|
62
|
+
<p className="text-sm text-muted-foreground">
|
|
63
|
+
{description}
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{/* Content */}
|
|
70
|
+
<div className="space-y-4">
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Search, X, Loader2 } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../atoms/utils/utils';
|
|
4
|
+
import { Input } from '../../atoms/ui/input';
|
|
5
|
+
import { Button } from '../../atoms/ui/button';
|
|
6
|
+
import { getAnimationClasses, animationPresets } from '../../atoms/utils/animations';
|
|
7
|
+
|
|
8
|
+
export interface SearchResult {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
category?: string;
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SearchInputProps {
|
|
17
|
+
/** Search placeholder text */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Current search value */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Value change handler */
|
|
22
|
+
onChange?: (value: string) => void;
|
|
23
|
+
/** Search results */
|
|
24
|
+
results?: SearchResult[];
|
|
25
|
+
/** Loading state */
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
/** Result selection handler */
|
|
28
|
+
onSelect?: (result: SearchResult) => void;
|
|
29
|
+
/** Search debounce delay in ms */
|
|
30
|
+
debounceMs?: number;
|
|
31
|
+
/** Whether to show clear button */
|
|
32
|
+
clearable?: boolean;
|
|
33
|
+
/** Additional CSS classes */
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Category-based styling */
|
|
36
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
37
|
+
/** Maximum results to show */
|
|
38
|
+
maxResults?: number;
|
|
39
|
+
/** No results message */
|
|
40
|
+
noResultsText?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SearchInput: React.FC<SearchInputProps> = ({
|
|
44
|
+
placeholder = 'Search...',
|
|
45
|
+
value = '',
|
|
46
|
+
onChange,
|
|
47
|
+
results = [],
|
|
48
|
+
loading = false,
|
|
49
|
+
onSelect,
|
|
50
|
+
debounceMs = 300,
|
|
51
|
+
clearable = true,
|
|
52
|
+
className,
|
|
53
|
+
category = 1,
|
|
54
|
+
maxResults = 10,
|
|
55
|
+
noResultsText = 'No results found'
|
|
56
|
+
}) => {
|
|
57
|
+
const [internalValue, setInternalValue] = useState(value);
|
|
58
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
59
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
60
|
+
|
|
61
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
62
|
+
const resultsRef = useRef<HTMLDivElement>(null);
|
|
63
|
+
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
64
|
+
|
|
65
|
+
const displayResults = results.slice(0, maxResults);
|
|
66
|
+
const showResults = isOpen && (displayResults.length > 0 || (internalValue && !loading));
|
|
67
|
+
|
|
68
|
+
// Debounced search
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (debounceRef.current) {
|
|
71
|
+
clearTimeout(debounceRef.current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
debounceRef.current = setTimeout(() => {
|
|
75
|
+
if (onChange) onChange(internalValue);
|
|
76
|
+
}, debounceMs);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
if (debounceRef.current) {
|
|
80
|
+
clearTimeout(debounceRef.current);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, [internalValue, onChange, debounceMs]);
|
|
84
|
+
|
|
85
|
+
// Sync external value
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
setInternalValue(value);
|
|
88
|
+
}, [value]);
|
|
89
|
+
|
|
90
|
+
// Keyboard navigation
|
|
91
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
92
|
+
if (!showResults) return;
|
|
93
|
+
|
|
94
|
+
switch (e.key) {
|
|
95
|
+
case 'ArrowDown':
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
setHighlightedIndex(prev =>
|
|
98
|
+
prev < displayResults.length - 1 ? prev + 1 : prev
|
|
99
|
+
);
|
|
100
|
+
break;
|
|
101
|
+
case 'ArrowUp':
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setHighlightedIndex(prev => prev > 0 ? prev - 1 : prev);
|
|
104
|
+
break;
|
|
105
|
+
case 'Enter':
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
if (highlightedIndex >= 0 && displayResults[highlightedIndex]) {
|
|
108
|
+
handleSelect(displayResults[highlightedIndex]);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'Escape':
|
|
112
|
+
setIsOpen(false);
|
|
113
|
+
setHighlightedIndex(-1);
|
|
114
|
+
inputRef.current?.blur();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleSelect = (result: SearchResult) => {
|
|
120
|
+
onSelect?.(result);
|
|
121
|
+
setIsOpen(false);
|
|
122
|
+
setHighlightedIndex(-1);
|
|
123
|
+
inputRef.current?.blur();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleClear = () => {
|
|
127
|
+
setInternalValue('');
|
|
128
|
+
onChange?.('');
|
|
129
|
+
setIsOpen(false);
|
|
130
|
+
inputRef.current?.focus();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
+
const newValue = e.target.value;
|
|
135
|
+
setInternalValue(newValue);
|
|
136
|
+
setIsOpen(true);
|
|
137
|
+
setHighlightedIndex(-1);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleFocus = () => {
|
|
141
|
+
if (internalValue) {
|
|
142
|
+
setIsOpen(true);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleBlur = () => {
|
|
147
|
+
// Delay closing to allow for result clicks
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
if (!resultsRef.current?.contains(document.activeElement)) {
|
|
150
|
+
setIsOpen(false);
|
|
151
|
+
setHighlightedIndex(-1);
|
|
152
|
+
}
|
|
153
|
+
}, 150);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={cn('relative w-full', className)} data-component-name="SearchInput">
|
|
158
|
+
{/* Input container */}
|
|
159
|
+
<div className="relative">
|
|
160
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
|
161
|
+
{loading ? (
|
|
162
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
163
|
+
) : (
|
|
164
|
+
<Search className="w-4 h-4" />
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<Input
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
type="text"
|
|
171
|
+
value={internalValue}
|
|
172
|
+
onChange={handleInputChange}
|
|
173
|
+
onKeyDown={handleKeyDown}
|
|
174
|
+
onFocus={handleFocus}
|
|
175
|
+
onBlur={handleBlur}
|
|
176
|
+
placeholder={placeholder}
|
|
177
|
+
className={cn(
|
|
178
|
+
'pl-10',
|
|
179
|
+
clearable && internalValue && 'pr-10',
|
|
180
|
+
showResults && `ring-2 ring-category-${category}/20`
|
|
181
|
+
)}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
{clearable && internalValue && (
|
|
185
|
+
<Button
|
|
186
|
+
type="button"
|
|
187
|
+
variant="ghost"
|
|
188
|
+
size="sm"
|
|
189
|
+
onClick={handleClear}
|
|
190
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
|
|
191
|
+
>
|
|
192
|
+
<X className="w-4 h-4" />
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Results dropdown */}
|
|
198
|
+
{showResults && (
|
|
199
|
+
<div
|
|
200
|
+
ref={resultsRef}
|
|
201
|
+
className={cn(
|
|
202
|
+
'absolute top-full left-0 right-0 z-50 mt-1',
|
|
203
|
+
'bg-popover border border-border rounded-md shadow-lg',
|
|
204
|
+
'max-h-64 overflow-y-auto',
|
|
205
|
+
getAnimationClasses(animationPresets.card)
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
{displayResults.length > 0 ? (
|
|
209
|
+
<div className="py-1">
|
|
210
|
+
{displayResults.map((result, index) => (
|
|
211
|
+
<button
|
|
212
|
+
key={result.id}
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => handleSelect(result)}
|
|
215
|
+
className={cn(
|
|
216
|
+
'w-full text-left px-3 py-2 text-sm',
|
|
217
|
+
'hover:bg-muted/50 focus:bg-muted/50',
|
|
218
|
+
'transition-colors duration-150',
|
|
219
|
+
index === highlightedIndex && [
|
|
220
|
+
`bg-category-${category}/10`,
|
|
221
|
+
`text-category-${category}`
|
|
222
|
+
]
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
{result.icon && (
|
|
227
|
+
<span className="flex-shrink-0 text-muted-foreground">
|
|
228
|
+
{result.icon}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
231
|
+
<div className="flex-1 min-w-0">
|
|
232
|
+
<div className="font-medium truncate">
|
|
233
|
+
{result.label}
|
|
234
|
+
</div>
|
|
235
|
+
{result.description && (
|
|
236
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
237
|
+
{result.description}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
{result.category && (
|
|
242
|
+
<span className="text-xs text-muted-foreground">
|
|
243
|
+
{result.category}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</button>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
) : internalValue && !loading ? (
|
|
251
|
+
<div className="px-3 py-2 text-sm text-muted-foreground">
|
|
252
|
+
{noResultsText}
|
|
253
|
+
</div>
|
|
254
|
+
) : null}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Form molecules - reusable form patterns and workflows
|
|
2
|
+
// Note: FormField moved to @/atoms/composed as it is a business UI component
|
|
3
|
+
export { FormGroup, type FormGroupProps } from './FormGroup';
|
|
4
|
+
export { SearchInput, type SearchInputProps, type SearchResult } from './SearchInput';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { GlobalSearch, UserMenu } from '../../../atoms/composed';
|
|
3
|
+
import { cn } from '../../../atoms/utils/utils';
|
|
4
|
+
|
|
5
|
+
interface AppHeaderProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const AppHeader: React.FC<AppHeaderProps> = ({ className }) => {
|
|
10
|
+
return (
|
|
11
|
+
<header className={cn(
|
|
12
|
+
"fixed top-0 left-0 right-0 z-50",
|
|
13
|
+
"bg-card/95 backdrop-blur-sm border-b border-border",
|
|
14
|
+
"supports-[backdrop-filter]:bg-card/95",
|
|
15
|
+
className
|
|
16
|
+
)}>
|
|
17
|
+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
18
|
+
<div className="flex h-16 items-center justify-between">
|
|
19
|
+
{/* App Title */}
|
|
20
|
+
<div className="flex items-center">
|
|
21
|
+
<h1 className="text-xl font-bold text-foreground">
|
|
22
|
+
Data Trust Navigator
|
|
23
|
+
</h1>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
{/* Centered Global Search */}
|
|
27
|
+
<div className="flex-1 max-w-2xl mx-8">
|
|
28
|
+
<GlobalSearch
|
|
29
|
+
className="w-full"
|
|
30
|
+
placeholder="Search models, tests, jobs, and components..."
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* User Menu */}
|
|
35
|
+
<div className="flex items-center space-x-4">
|
|
36
|
+
<UserMenu category={8} />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AppHeader } from './AppHeader';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Outlet } from 'react-router-dom';
|
|
2
|
+
import { Sidebar } from './Sidebar';
|
|
3
|
+
import { AppHeader } from './AppHeader';
|
|
4
|
+
import { useSidebar } from './SidebarContext';
|
|
5
|
+
import { cn } from '../../atoms/utils/utils';
|
|
6
|
+
|
|
7
|
+
export const AppLayout = () => {
|
|
8
|
+
const { isExpanded } = useSidebar();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen bg-background">
|
|
12
|
+
{/* Header spans full width */}
|
|
13
|
+
<AppHeader />
|
|
14
|
+
|
|
15
|
+
{/* Sidebar positioned fixed */}
|
|
16
|
+
<Sidebar />
|
|
17
|
+
|
|
18
|
+
{/* Main content with proper spacing for fixed sidebar */}
|
|
19
|
+
<main
|
|
20
|
+
className={cn(
|
|
21
|
+
"pt-16 min-h-screen transition-all duration-300 ease-in-out",
|
|
22
|
+
isExpanded ? 'ml-64' : 'ml-16'
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Outlet />
|
|
26
|
+
</main>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '../../atoms/utils/utils';
|
|
3
|
+
import { getAnimationClasses } from '../../atoms/utils/animations';
|
|
4
|
+
|
|
5
|
+
export interface PageTemplateProps {
|
|
6
|
+
/** Main page title */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Optional subtitle/description */
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
/** Optional icon element to display with title */
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
/** Optional action buttons/controls for the header */
|
|
13
|
+
actions?: React.ReactNode;
|
|
14
|
+
/** Page content */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
/** Additional CSS classes for the container */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Whether to apply animation to the page content */
|
|
19
|
+
animated?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PageTemplate: React.FC<PageTemplateProps> = ({
|
|
23
|
+
title,
|
|
24
|
+
subtitle,
|
|
25
|
+
icon,
|
|
26
|
+
actions,
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
animated = true
|
|
30
|
+
}) => {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
'min-h-screen bg-background',
|
|
35
|
+
animated && getAnimationClasses({ type: 'subtle', timing: 'slow' }),
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
data-component-name="PageTemplate"
|
|
39
|
+
>
|
|
40
|
+
{/* Page Header */}
|
|
41
|
+
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
|
|
42
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
43
|
+
<div className="flex items-center justify-between py-6 sm:py-8">
|
|
44
|
+
<div className="flex items-center space-x-4 min-w-0 flex-1">
|
|
45
|
+
{icon && (
|
|
46
|
+
<div className="flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded bg-primary/10 text-primary">
|
|
47
|
+
{icon}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<div className="min-w-0 flex-1">
|
|
52
|
+
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-foreground tracking-tight">
|
|
53
|
+
{title}
|
|
54
|
+
</h1>
|
|
55
|
+
{subtitle && (
|
|
56
|
+
<p className="mt-2 text-base sm:text-lg text-muted-foreground max-w-3xl">
|
|
57
|
+
{subtitle}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{actions && (
|
|
64
|
+
<div className="flex-shrink-0 ml-4">
|
|
65
|
+
<div className="flex items-center space-x-3">
|
|
66
|
+
{actions}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
{/* Page Content */}
|
|
75
|
+
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-12">
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
'space-y-8',
|
|
79
|
+
animated && 'animate-fade-in'
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</div>
|
|
84
|
+
</main>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '../../../atoms/utils/utils';
|
|
3
|
+
import { DataBadge } from '../../../atoms/composed/DataBadge';
|
|
4
|
+
|
|
5
|
+
export interface SectionHeaderProps {
|
|
6
|
+
/** Main heading text */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Optional description text */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Optional subtitle text */
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
/** Size variant */
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
/** Additional CSS classes */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Optional icon element */
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
/** Optional badge configuration */
|
|
19
|
+
badge?: {
|
|
20
|
+
text: string;
|
|
21
|
+
variant?: 'category' | 'status';
|
|
22
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
23
|
+
status?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const SectionHeader: React.FC<SectionHeaderProps> = ({
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
subtitle,
|
|
31
|
+
size = 'md',
|
|
32
|
+
className,
|
|
33
|
+
icon,
|
|
34
|
+
badge
|
|
35
|
+
}) => {
|
|
36
|
+
const titleSizes = {
|
|
37
|
+
sm: 'text-lg',
|
|
38
|
+
md: 'text-xl',
|
|
39
|
+
lg: 'text-2xl'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const spacingSizes = {
|
|
43
|
+
sm: 'space-y-1',
|
|
44
|
+
md: 'space-y-2',
|
|
45
|
+
lg: 'space-y-3'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
'text-center',
|
|
52
|
+
spacingSizes[size],
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
data-component-name="SectionHeader"
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center justify-center gap-3">
|
|
58
|
+
{icon}
|
|
59
|
+
<h3 className={cn('font-semibold text-foreground', titleSizes[size])}>
|
|
60
|
+
{title}
|
|
61
|
+
</h3>
|
|
62
|
+
{badge && (
|
|
63
|
+
<DataBadge
|
|
64
|
+
variant={badge.variant || 'status'}
|
|
65
|
+
status={badge.status}
|
|
66
|
+
category={badge.category}
|
|
67
|
+
size="sm"
|
|
68
|
+
>
|
|
69
|
+
{badge.text}
|
|
70
|
+
</DataBadge>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{subtitle && (
|
|
75
|
+
<p className="text-muted-foreground text-sm font-medium">
|
|
76
|
+
{subtitle}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{description && (
|
|
81
|
+
<p className="text-muted-foreground text-sm max-w-2xl mx-auto">
|
|
82
|
+
{description}
|
|
83
|
+
</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SectionHeader, type SectionHeaderProps } from './SectionHeader';
|