@umituz/web-design-system 5.3.1 → 6.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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/__tests__/productivity-components.test.tsx +135 -0
- package/src/index.ts +84 -10
- package/src/presentation/atoms/CopyButton.tsx +93 -0
- package/src/presentation/atoms/StatusDot.tsx +79 -0
- package/src/presentation/atoms/index.ts +9 -4
- package/src/presentation/molecules/ConfirmButton.tsx +125 -0
- package/src/presentation/molecules/PasswordInput.tsx +43 -0
- package/src/presentation/molecules/SearchInput.tsx +110 -0
- package/src/presentation/molecules/Toolbar.tsx +36 -0
- package/src/presentation/molecules/index.ts +25 -2
- package/src/presentation/organisms/DescriptionList.tsx +113 -0
- package/src/presentation/organisms/StatsGrid.tsx +110 -0
- package/src/presentation/organisms/index.ts +10 -6
- package/src/presentation/templates/CrudPageTemplate.tsx +69 -0
- package/src/presentation/templates/SettingsLayout.tsx +102 -0
- package/src/presentation/templates/index.ts +7 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
export { FormField } from './FormField';
|
|
3
2
|
export type { FormFieldProps } from './FormField';
|
|
4
3
|
|
|
@@ -17,7 +16,18 @@ export type { ChipProps } from './Chip';
|
|
|
17
16
|
export { Toggle } from './Toggle';
|
|
18
17
|
export type { ToggleProps } from './Toggle';
|
|
19
18
|
|
|
20
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
Select,
|
|
21
|
+
SelectGroup,
|
|
22
|
+
SelectValue,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectLabel,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectSeparator,
|
|
28
|
+
SelectScrollUpButton,
|
|
29
|
+
SelectScrollDownButton,
|
|
30
|
+
} from './Select';
|
|
21
31
|
|
|
22
32
|
export { Textarea } from './Textarea';
|
|
23
33
|
export type { TextareaProps } from './Textarea';
|
|
@@ -64,3 +74,16 @@ export type { TagInputProps } from './TagInput';
|
|
|
64
74
|
|
|
65
75
|
export { MarkdownText } from './MarkdownText';
|
|
66
76
|
export type { MarkdownTextProps } from './MarkdownText';
|
|
77
|
+
|
|
78
|
+
// New Productivity Molecules
|
|
79
|
+
export { PasswordInput } from './PasswordInput';
|
|
80
|
+
export type { PasswordInputProps } from './PasswordInput';
|
|
81
|
+
|
|
82
|
+
export { SearchInput } from './SearchInput';
|
|
83
|
+
export type { SearchInputProps } from './SearchInput';
|
|
84
|
+
|
|
85
|
+
export { ConfirmButton } from './ConfirmButton';
|
|
86
|
+
export type { ConfirmButtonProps } from './ConfirmButton';
|
|
87
|
+
|
|
88
|
+
export { Toolbar } from './Toolbar';
|
|
89
|
+
export type { ToolbarProps } from './Toolbar';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card } from './Card';
|
|
3
|
+
import { Badge, type BadgeVariant } from '../atoms/Badge';
|
|
4
|
+
import { CopyButton } from '../atoms/CopyButton';
|
|
5
|
+
import { cn } from '../../infrastructure/utils';
|
|
6
|
+
|
|
7
|
+
export interface DescriptionItem {
|
|
8
|
+
key?: string;
|
|
9
|
+
label: string;
|
|
10
|
+
value: React.ReactNode;
|
|
11
|
+
copyable?: boolean;
|
|
12
|
+
copyText?: string;
|
|
13
|
+
badge?: string;
|
|
14
|
+
badgeVariant?: BadgeVariant;
|
|
15
|
+
hint?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DescriptionListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
19
|
+
title?: React.ReactNode;
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
action?: React.ReactNode;
|
|
22
|
+
items: DescriptionItem[];
|
|
23
|
+
columns?: 1 | 2;
|
|
24
|
+
bordered?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DescriptionList = React.forwardRef<HTMLDivElement, DescriptionListProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
action,
|
|
33
|
+
items,
|
|
34
|
+
columns = 1,
|
|
35
|
+
bordered = true,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref
|
|
40
|
+
) => {
|
|
41
|
+
const content = (
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn('w-full flex flex-col gap-4', className)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{(title || action || description) && (
|
|
48
|
+
<div className="flex items-start justify-between gap-4 pb-3 border-b border-border">
|
|
49
|
+
<div>
|
|
50
|
+
{title && (
|
|
51
|
+
<h3 className="text-base font-semibold text-foreground tracking-tight">
|
|
52
|
+
{title}
|
|
53
|
+
</h3>
|
|
54
|
+
)}
|
|
55
|
+
{description && (
|
|
56
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
57
|
+
{description}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<dl
|
|
66
|
+
className={cn(
|
|
67
|
+
'grid gap-x-6 gap-y-3',
|
|
68
|
+
columns === 2 ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1'
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{items.map((item, idx) => {
|
|
72
|
+
const rawCopyText = item.copyText ?? (typeof item.value === 'string' ? item.value : '');
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
key={item.key ?? idx}
|
|
77
|
+
className="flex items-center justify-between py-2 border-b border-border/60 last:border-0 gap-3"
|
|
78
|
+
>
|
|
79
|
+
<dt className="text-sm font-medium text-muted-foreground shrink-0">
|
|
80
|
+
{item.label}
|
|
81
|
+
{item.hint && (
|
|
82
|
+
<span className="block text-xs font-normal text-muted-foreground/75">
|
|
83
|
+
{item.hint}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</dt>
|
|
87
|
+
<dd className="text-sm font-medium text-foreground flex items-center gap-2 text-right break-all">
|
|
88
|
+
{item.value}
|
|
89
|
+
{item.badge && (
|
|
90
|
+
<Badge variant={item.badgeVariant ?? 'secondary'} size="sm">
|
|
91
|
+
{item.badge}
|
|
92
|
+
</Badge>
|
|
93
|
+
)}
|
|
94
|
+
{item.copyable && rawCopyText && (
|
|
95
|
+
<CopyButton text={rawCopyText} size="xs" />
|
|
96
|
+
)}
|
|
97
|
+
</dd>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</dl>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (bordered) {
|
|
106
|
+
return <Card className="p-5">{content}</Card>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return content;
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
DescriptionList.displayName = 'DescriptionList';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Card } from './Card';
|
|
3
|
+
import { Skeleton } from '../atoms/Skeleton';
|
|
4
|
+
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../infrastructure/utils';
|
|
6
|
+
|
|
7
|
+
export interface StatItem {
|
|
8
|
+
id?: string;
|
|
9
|
+
title: string;
|
|
10
|
+
value: string | number;
|
|
11
|
+
change?: string;
|
|
12
|
+
trend?: 'up' | 'down' | 'neutral';
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StatsGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
stats: StatItem[];
|
|
19
|
+
columns?: 2 | 3 | 4;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const colStyles = {
|
|
24
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
25
|
+
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
26
|
+
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const StatsGrid = React.forwardRef<HTMLDivElement, StatsGridProps>(
|
|
30
|
+
({ stats, columns = 4, loading = false, className, ...props }, ref) => {
|
|
31
|
+
if (loading) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn('grid gap-4 w-full', colStyles[columns], className)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{Array.from({ length: columns }).map((_, i) => (
|
|
39
|
+
<Card key={i} className="p-5 flex flex-col gap-3">
|
|
40
|
+
<Skeleton className="h-4 w-24" />
|
|
41
|
+
<Skeleton className="h-8 w-32" />
|
|
42
|
+
<Skeleton className="h-3 w-40" />
|
|
43
|
+
</Card>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('grid gap-4 w-full', colStyles[columns], className)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{stats.map((stat, idx) => {
|
|
56
|
+
const isUp = stat.trend === 'up';
|
|
57
|
+
const isDown = stat.trend === 'down';
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Card
|
|
61
|
+
key={stat.id ?? idx}
|
|
62
|
+
className="p-5 flex flex-col justify-between transition-shadow hover:shadow-md"
|
|
63
|
+
>
|
|
64
|
+
<div className="flex items-start justify-between gap-2">
|
|
65
|
+
<span className="text-sm font-medium text-muted-foreground truncate">
|
|
66
|
+
{stat.title}
|
|
67
|
+
</span>
|
|
68
|
+
{stat.icon && (
|
|
69
|
+
<div className="size-9 rounded-lg bg-accent/60 flex items-center justify-center text-foreground shrink-0 [&_svg]:size-4">
|
|
70
|
+
{stat.icon}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="mt-3 flex items-baseline justify-between gap-2">
|
|
76
|
+
<div className="text-2xl font-bold tracking-tight text-foreground">
|
|
77
|
+
{stat.value}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{stat.change && (
|
|
81
|
+
<span
|
|
82
|
+
className={cn(
|
|
83
|
+
'inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full',
|
|
84
|
+
isUp && 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
|
85
|
+
isDown && 'bg-rose-500/10 text-rose-600 dark:text-rose-400',
|
|
86
|
+
!isUp && !isDown && 'bg-accent text-muted-foreground'
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{isUp && <TrendingUp className="size-3" />}
|
|
90
|
+
{isDown && <TrendingDown className="size-3" />}
|
|
91
|
+
{!isUp && !isDown && <Minus className="size-3" />}
|
|
92
|
+
<span>{stat.change}</span>
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{stat.description && (
|
|
98
|
+
<p className="mt-2 text-xs text-muted-foreground line-clamp-1">
|
|
99
|
+
{stat.description}
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
102
|
+
</Card>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
StatsGrid.displayName = 'StatsGrid';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
|
|
3
2
|
export type { CardProps, CardHeaderProps } from './Card';
|
|
4
3
|
|
|
@@ -13,7 +12,7 @@ export {
|
|
|
13
12
|
TableRow,
|
|
14
13
|
TableHead,
|
|
15
14
|
TableCell,
|
|
16
|
-
TableCaption
|
|
15
|
+
TableCaption,
|
|
17
16
|
} from './Table';
|
|
18
17
|
export type { TableProps } from './Table';
|
|
19
18
|
|
|
@@ -36,10 +35,10 @@ export {
|
|
|
36
35
|
} from './Dialog';
|
|
37
36
|
|
|
38
37
|
export { CommandPalette } from './CommandPalette';
|
|
38
|
+
export type { CommandPaletteProps, CommandGroup, CommandItem } from './CommandPalette';
|
|
39
39
|
|
|
40
40
|
export { ErrorBoundary } from './ErrorBoundary';
|
|
41
41
|
export type { ErrorBoundaryProps } from './ErrorBoundary';
|
|
42
|
-
export type { CommandPaletteProps, CommandGroup, CommandItem } from './CommandPalette';
|
|
43
42
|
|
|
44
43
|
export { Breadcrumbs } from './Breadcrumb';
|
|
45
44
|
export type { BreadcrumbsProps, BreadcrumbItem } from './Breadcrumb';
|
|
@@ -112,7 +111,7 @@ export {
|
|
|
112
111
|
export { ToggleGroup, ToggleGroupItem } from './ToggleGroup';
|
|
113
112
|
|
|
114
113
|
export { ToastProvider, useToast } from './Toaster';
|
|
115
|
-
export type { ToastKind, ToastOptions } from './Toaster';
|
|
114
|
+
export type { ToastKind, ToastOptions, ToastShorthandOptions } from './Toaster';
|
|
116
115
|
|
|
117
116
|
export { KeyValueList } from './KeyValueList';
|
|
118
117
|
export type { KeyValueListProps, KeyValueItem } from './KeyValueList';
|
|
@@ -149,8 +148,6 @@ export type { PhotoGalleryProps, PhotoTile } from './PhotoGallery';
|
|
|
149
148
|
export { Lightbox } from './Lightbox';
|
|
150
149
|
export type { LightboxProps } from './Lightbox';
|
|
151
150
|
|
|
152
|
-
export type { ToastShorthandOptions } from './Toaster';
|
|
153
|
-
|
|
154
151
|
export { Timeline } from './Timeline';
|
|
155
152
|
export type { TimelineProps, TimelineItem, TimelineTone } from './Timeline';
|
|
156
153
|
|
|
@@ -181,3 +178,10 @@ export type { PlanPickerProps, PlanPickerPlan, PlanPickerCycle } from './PlanPic
|
|
|
181
178
|
|
|
182
179
|
export { CoverArt } from './CoverArt';
|
|
183
180
|
export type { CoverArtProps } from './CoverArt';
|
|
181
|
+
|
|
182
|
+
// New Productivity Organisms
|
|
183
|
+
export { StatsGrid } from './StatsGrid';
|
|
184
|
+
export type { StatsGridProps, StatItem } from './StatsGrid';
|
|
185
|
+
|
|
186
|
+
export { DescriptionList } from './DescriptionList';
|
|
187
|
+
export type { DescriptionListProps, DescriptionItem } from './DescriptionList';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Breadcrumbs, type BreadcrumbItem } from '../organisms/Breadcrumb';
|
|
3
|
+
import { cn } from '../../infrastructure/utils';
|
|
4
|
+
|
|
5
|
+
export interface CrudPageTemplateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
|
+
title: React.ReactNode;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
breadcrumbItems?: BreadcrumbItem[];
|
|
9
|
+
actions?: React.ReactNode;
|
|
10
|
+
stats?: React.ReactNode;
|
|
11
|
+
toolbar?: React.ReactNode;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const CrudPageTemplate = React.forwardRef<HTMLDivElement, CrudPageTemplateProps>(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
breadcrumbItems,
|
|
21
|
+
actions,
|
|
22
|
+
stats,
|
|
23
|
+
toolbar,
|
|
24
|
+
children,
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) => {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn('w-full flex flex-col gap-6 p-4 sm:p-6 lg:p-8', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{/* Breadcrumbs */}
|
|
37
|
+
{breadcrumbItems && breadcrumbItems.length > 0 && (
|
|
38
|
+
<div>
|
|
39
|
+
<Breadcrumbs items={breadcrumbItems} />
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
{/* Page Header */}
|
|
44
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground">
|
|
47
|
+
{title}
|
|
48
|
+
</h1>
|
|
49
|
+
{description && (
|
|
50
|
+
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Optional Stats Grid */}
|
|
57
|
+
{stats && <div>{stats}</div>}
|
|
58
|
+
|
|
59
|
+
{/* Optional Search / Filter Toolbar */}
|
|
60
|
+
{toolbar && <div>{toolbar}</div>}
|
|
61
|
+
|
|
62
|
+
{/* Main Content (Table / List / Cards) */}
|
|
63
|
+
<div className="w-full">{children}</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CrudPageTemplate.displayName = 'CrudPageTemplate';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../infrastructure/utils';
|
|
3
|
+
|
|
4
|
+
export interface SettingsNavItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
description?: string;
|
|
9
|
+
badge?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SettingsLayoutProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'onSelect'> {
|
|
14
|
+
title?: React.ReactNode;
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
navItems: SettingsNavItem[];
|
|
17
|
+
activeId: string;
|
|
18
|
+
onSelect: (id: string) => void;
|
|
19
|
+
actions?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SettingsLayout = React.forwardRef<HTMLDivElement, SettingsLayoutProps>(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
title = 'Ayarlar',
|
|
26
|
+
description,
|
|
27
|
+
navItems,
|
|
28
|
+
activeId,
|
|
29
|
+
onSelect,
|
|
30
|
+
actions,
|
|
31
|
+
children,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref
|
|
36
|
+
) => {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn('w-full max-w-6xl mx-auto flex flex-col gap-6 p-4 sm:p-6 lg:p-8', className)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{/* Header */}
|
|
44
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-4 border-b border-border">
|
|
45
|
+
<div>
|
|
46
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">{title}</h1>
|
|
47
|
+
{description && (
|
|
48
|
+
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Content Layout */}
|
|
55
|
+
<div className="flex flex-col lg:flex-row gap-8 items-start">
|
|
56
|
+
{/* Navigation Sidebar */}
|
|
57
|
+
<nav
|
|
58
|
+
aria-label="Settings navigation"
|
|
59
|
+
className="w-full lg:w-64 shrink-0 flex flex-row lg:flex-col gap-1 overflow-x-auto pb-2 lg:pb-0"
|
|
60
|
+
>
|
|
61
|
+
{navItems.map((item) => {
|
|
62
|
+
const isActive = item.id === activeId;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
key={item.id}
|
|
67
|
+
type="button"
|
|
68
|
+
disabled={item.disabled}
|
|
69
|
+
onClick={() => onSelect(item.id)}
|
|
70
|
+
className={cn(
|
|
71
|
+
'flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-sm font-medium transition-colors text-left shrink-0 whitespace-nowrap lg:whitespace-normal',
|
|
72
|
+
isActive
|
|
73
|
+
? 'bg-primary/10 text-primary font-semibold'
|
|
74
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
|
|
75
|
+
item.disabled && 'opacity-50 pointer-events-none'
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{item.icon && (
|
|
79
|
+
<span className="shrink-0 size-4 flex items-center justify-center [&_svg]:size-4">
|
|
80
|
+
{item.icon}
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
<span className="flex-1 truncate">{item.label}</span>
|
|
84
|
+
{item.badge && (
|
|
85
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-accent text-accent-foreground ml-auto">
|
|
86
|
+
{item.badge}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</nav>
|
|
93
|
+
|
|
94
|
+
{/* Main Panel Content */}
|
|
95
|
+
<main className="flex-1 w-full min-w-0">{children}</main>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
SettingsLayout.displayName = 'SettingsLayout';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
export { Form, FormActions } from './Form';
|
|
3
2
|
export type { FormProps } from './Form';
|
|
4
3
|
|
|
@@ -19,3 +18,10 @@ export type { ProjectSkeletonProps } from './ProjectSkeleton';
|
|
|
19
18
|
|
|
20
19
|
export { ResponsiveContainer } from './ResponsiveContainer';
|
|
21
20
|
export type { ResponsiveContainerProps, ResponsiveSize } from './ResponsiveContainer';
|
|
21
|
+
|
|
22
|
+
// New Productivity Templates
|
|
23
|
+
export { SettingsLayout } from './SettingsLayout';
|
|
24
|
+
export type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout';
|
|
25
|
+
|
|
26
|
+
export { CrudPageTemplate } from './CrudPageTemplate';
|
|
27
|
+
export type { CrudPageTemplateProps } from './CrudPageTemplate';
|