flowyml 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,145 @@
1
+ import React, { useState } from 'react';
2
+ import { NavLink, useLocation } from 'react-router-dom';
3
+ import {
4
+ LayoutDashboard,
5
+ PlayCircle,
6
+ FolderKanban,
7
+ FlaskConical,
8
+ Database,
9
+ Settings,
10
+ Trophy,
11
+ Calendar,
12
+ MessageSquare,
13
+ Key,
14
+ Package,
15
+ ChevronLeft,
16
+ ChevronRight,
17
+ Menu
18
+ } from 'lucide-react';
19
+ import { motion, AnimatePresence } from 'framer-motion';
20
+
21
+ const NAV_LINKS = [
22
+ { icon: LayoutDashboard, label: 'Dashboard', path: '/' },
23
+ { icon: FolderKanban, label: 'Projects', path: '/projects' },
24
+ { icon: PlayCircle, label: 'Pipelines', path: '/pipelines' },
25
+ { icon: Calendar, label: 'Schedules', path: '/schedules' },
26
+ { icon: PlayCircle, label: 'Runs', path: '/runs' },
27
+ { icon: Trophy, label: 'Leaderboard', path: '/leaderboard' },
28
+ { icon: Database, label: 'Assets', path: '/assets' },
29
+ { icon: FlaskConical, label: 'Experiments', path: '/experiments' },
30
+ { icon: MessageSquare, label: 'Traces', path: '/traces' },
31
+ ];
32
+
33
+ const SETTINGS_LINKS = [
34
+ { icon: Package, label: 'Plugins', path: '/plugins' },
35
+ { icon: Key, label: 'API Tokens', path: '/tokens' },
36
+ { icon: Settings, label: 'Settings', path: '/settings' },
37
+ ];
38
+
39
+ export function Sidebar({ collapsed, setCollapsed }) {
40
+ const location = useLocation();
41
+
42
+ return (
43
+ <motion.aside
44
+ initial={false}
45
+ animate={{ width: collapsed ? 80 : 256 }}
46
+ className="h-screen bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col shadow-sm z-20 relative"
47
+ >
48
+ {/* Logo Section */}
49
+ <div className="p-6 border-b border-slate-100 dark:border-slate-700 flex items-center gap-3 h-[73px]">
50
+ <div className="w-8 h-8 min-w-[32px] bg-primary-600 rounded-lg flex items-center justify-center shadow-lg shadow-primary-500/30">
51
+ <PlayCircle className="text-white w-5 h-5" />
52
+ </div>
53
+ <AnimatePresence>
54
+ {!collapsed && (
55
+ <motion.h1
56
+ initial={{ opacity: 0, x: -10 }}
57
+ animate={{ opacity: 1, x: 0 }}
58
+ exit={{ opacity: 0, x: -10 }}
59
+ className="text-xl font-bold text-slate-900 dark:text-white tracking-tight whitespace-nowrap overflow-hidden"
60
+ >
61
+ flowyml
62
+ </motion.h1>
63
+ )}
64
+ </AnimatePresence>
65
+ </div>
66
+
67
+ {/* Navigation */}
68
+ <nav className="flex-1 p-4 space-y-1 overflow-y-auto overflow-x-hidden scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700">
69
+ <div className={`px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider transition-opacity duration-200 ${collapsed ? 'opacity-0 h-0' : 'opacity-100'}`}>
70
+ Platform
71
+ </div>
72
+ {NAV_LINKS.map((link) => (
73
+ <NavItem
74
+ key={link.path}
75
+ to={link.path}
76
+ icon={link.icon}
77
+ label={link.label}
78
+ collapsed={collapsed}
79
+ isActive={location.pathname === link.path}
80
+ />
81
+ ))}
82
+
83
+ <div className={`px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mt-4 transition-opacity duration-200 ${collapsed ? 'opacity-0 h-0' : 'opacity-100'}`}>
84
+ Settings
85
+ </div>
86
+ {SETTINGS_LINKS.map((link) => (
87
+ <NavItem
88
+ key={link.path}
89
+ to={link.path}
90
+ icon={link.icon}
91
+ label={link.label}
92
+ collapsed={collapsed}
93
+ isActive={location.pathname === link.path}
94
+ />
95
+ ))}
96
+ </nav>
97
+
98
+ {/* Footer */}
99
+ <div className="p-4 border-t border-slate-100 dark:border-slate-700">
100
+ <div className={`bg-slate-50 dark:bg-slate-900 rounded-lg p-4 border border-slate-100 dark:border-slate-700 transition-all duration-200 ${collapsed ? 'p-2 flex justify-center' : ''}`}>
101
+ {!collapsed ? (
102
+ <>
103
+ <p className="text-xs font-medium text-slate-500 dark:text-slate-400 whitespace-nowrap">flowyml v0.1.0</p>
104
+ <p className="text-xs text-slate-400 dark:text-slate-500 mt-1 whitespace-nowrap">Local Environment</p>
105
+ </>
106
+ ) : (
107
+ <div className="w-2 h-2 rounded-full bg-emerald-500" title="Online" />
108
+ )}
109
+ </div>
110
+ </div>
111
+
112
+ {/* Collapse Toggle */}
113
+ <button
114
+ onClick={() => setCollapsed(!collapsed)}
115
+ className="absolute -right-3 top-20 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-full p-1 shadow-md text-slate-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
116
+ >
117
+ {collapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
118
+ </button>
119
+ </motion.aside>
120
+ );
121
+ }
122
+
123
+ function NavItem({ to, icon: Icon, label, collapsed, isActive }) {
124
+ return (
125
+ <NavLink
126
+ to={to}
127
+ className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all duration-200 group relative ${isActive
128
+ ? 'bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-400 font-medium shadow-sm'
129
+ : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white'
130
+ }`}
131
+ title={collapsed ? label : undefined}
132
+ >
133
+ <span className={`transition-colors flex-shrink-0 ${isActive ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300'
134
+ }`}>
135
+ <Icon size={20} />
136
+ </span>
137
+ {!collapsed && (
138
+ <span className="text-sm whitespace-nowrap overflow-hidden text-ellipsis">{label}</span>
139
+ )}
140
+ {collapsed && isActive && (
141
+ <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-primary-600 rounded-r-full" />
142
+ )}
143
+ </NavLink>
144
+ );
145
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { cn } from '../../utils/cn';
3
+
4
+ const variants = {
5
+ default: "bg-slate-100 text-slate-800",
6
+ primary: "bg-primary-50 text-primary-700 border border-primary-100",
7
+ success: "bg-emerald-50 text-emerald-700 border border-emerald-100",
8
+ warning: "bg-amber-50 text-amber-700 border border-amber-100",
9
+ danger: "bg-rose-50 text-rose-700 border border-rose-100",
10
+ outline: "bg-transparent border border-slate-200 text-slate-600",
11
+ };
12
+
13
+ export function Badge({ className, variant = "default", children, ...props }) {
14
+ return (
15
+ <span
16
+ className={cn(
17
+ "inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ring-black/5",
18
+ variants[variant],
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ </span>
25
+ );
26
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '../../utils/cn';
4
+
5
+ const variants = {
6
+ primary: "bg-primary-600 text-white hover:bg-primary-700 shadow-md shadow-primary-500/20",
7
+ secondary: "bg-white text-slate-900 border border-slate-200 hover:bg-slate-50",
8
+ ghost: "bg-transparent text-slate-600 hover:bg-slate-100",
9
+ danger: "bg-rose-600 text-white hover:bg-rose-700 shadow-md shadow-rose-500/20",
10
+ };
11
+
12
+ const sizes = {
13
+ sm: "h-8 px-3 text-xs",
14
+ md: "h-10 px-4 py-2",
15
+ lg: "h-12 px-8 text-lg",
16
+ icon: "h-10 w-10",
17
+ };
18
+
19
+ export function Button({ className, variant = "primary", size = "md", children, ...props }) {
20
+ return (
21
+ <motion.button
22
+ whileTap={{ scale: 0.98 }}
23
+ className={cn(
24
+ "inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 disabled:pointer-events-none disabled:opacity-50",
25
+ variants[variant],
26
+ sizes[size],
27
+ className
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </motion.button>
33
+ );
34
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { cn } from '../../utils/cn';
4
+
5
+ export function Card({ className, children, hover = true, ...props }) {
6
+ return (
7
+ <motion.div
8
+ whileHover={hover ? { y: -2, boxShadow: "0 10px 30px -10px rgba(0,0,0,0.1)" } : {}}
9
+ transition={{ duration: 0.2 }}
10
+ className={cn(
11
+ "bg-white dark:bg-slate-800 rounded-xl border border-slate-100 dark:border-slate-700 shadow-sm p-6",
12
+ "transition-colors duration-200",
13
+ className
14
+ )}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </motion.div>
19
+ );
20
+ }
21
+
22
+ export function CardHeader({ className, children, ...props }) {
23
+ return (
24
+ <div className={cn("flex flex-col space-y-1.5 mb-4", className)} {...props}>
25
+ {children}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export function CardTitle({ className, children, ...props }) {
31
+ return (
32
+ <h3 className={cn("font-semibold leading-none tracking-tight text-slate-900 dark:text-white", className)} {...props}>
33
+ {children}
34
+ </h3>
35
+ );
36
+ }
37
+
38
+ export function CardContent({ className, children, ...props }) {
39
+ return (
40
+ <div className={cn("", className)} {...props}>
41
+ {children}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,38 @@
1
+ import React, { useState } from 'react';
2
+ import { Copy, Check } from 'lucide-react';
3
+
4
+ export function CodeSnippet({ code, language = 'python', title, className = '' }) {
5
+ const [copied, setCopied] = useState(false);
6
+
7
+ const copyToClipboard = () => {
8
+ navigator.clipboard.writeText(code);
9
+ setCopied(true);
10
+ setTimeout(() => setCopied(false), 2000);
11
+ };
12
+
13
+ return (
14
+ <div className={`relative group ${className}`}>
15
+ {title && (
16
+ <div className="flex items-center justify-between px-4 py-2 bg-slate-800 dark:bg-slate-900 border-b border-slate-700 rounded-t-lg">
17
+ <span className="text-sm font-medium text-slate-300">{title}</span>
18
+ <span className="text-xs text-slate-500 uppercase font-mono">{language}</span>
19
+ </div>
20
+ )}
21
+ <div className="relative">
22
+ <pre className={`p-4 bg-slate-900 dark:bg-slate-950 text-slate-100 text-sm font-mono overflow-x-auto leading-relaxed ${title ? '' : 'rounded-t-lg'} rounded-b-lg`}>
23
+ <code className={`language-${language}`}>{code}</code>
24
+ </pre>
25
+ <button
26
+ onClick={copyToClipboard}
27
+ className="absolute top-3 right-3 p-2 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-slate-200 rounded-md transition-all opacity-0 group-hover:opacity-100"
28
+ title="Copy to clipboard"
29
+ >
30
+ {copied ? <Check size={14} /> : <Copy size={14} />}
31
+ </button>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // Example usage in CodeTab:
38
+ // <CodeSnippet code={sourceCode} language="python" title="Step Source Code" />
@@ -0,0 +1,53 @@
1
+ import React, { useState } from 'react';
2
+ import { ChevronDown, ChevronRight } from 'lucide-react';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+
5
+ export function CollapsibleCard({
6
+ title,
7
+ children,
8
+ icon,
9
+ badge,
10
+ defaultOpen = false,
11
+ className = '',
12
+ headerClassName = ''
13
+ }) {
14
+ const [isOpen, setIsOpen] = useState(defaultOpen);
15
+
16
+ return (
17
+ <div className={`border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-white dark:bg-slate-800 ${className}`}>
18
+ <button
19
+ onClick={() => setIsOpen(!isOpen)}
20
+ className={`w-full flex items-center justify-between p-4 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors ${headerClassName}`}
21
+ >
22
+ <div className="flex items-center gap-3">
23
+ {icon && <span className="text-slate-500">{icon}</span>}
24
+ <h3 className="text-base font-semibold text-slate-900 dark:text-white">{title}</h3>
25
+ {badge && <span>{badge}</span>}
26
+ </div>
27
+ <div className="flex items-center gap-2">
28
+ {isOpen ? (
29
+ <ChevronDown size={18} className="text-slate-400" />
30
+ ) : (
31
+ <ChevronRight size={18} className="text-slate-400" />
32
+ )}
33
+ </div>
34
+ </button>
35
+
36
+ <AnimatePresence>
37
+ {isOpen && (
38
+ <motion.div
39
+ initial={{ height: 0, opacity: 0 }}
40
+ animate={{ height: 'auto', opacity: 1 }}
41
+ exit={{ height: 0, opacity: 0 }}
42
+ transition={{ duration: 0.2 }}
43
+ className="overflow-hidden"
44
+ >
45
+ <div className="p-4 pt-0 border-t border-slate-100 dark:border-slate-700">
46
+ {children}
47
+ </div>
48
+ </motion.div>
49
+ )}
50
+ </AnimatePresence>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,175 @@
1
+ import React, { useState } from 'react';
2
+ import { Search, LayoutGrid, List, Table as TableIcon, ArrowUpDown, Filter } from 'lucide-react';
3
+ import { Button } from './Button';
4
+
5
+ export function DataView({
6
+ title,
7
+ subtitle,
8
+ actions,
9
+ items = [],
10
+ columns = [],
11
+ renderGrid,
12
+ renderList,
13
+ searchPlaceholder = "Search...",
14
+ initialView = 'grid',
15
+ emptyState,
16
+ loading = false
17
+ }) {
18
+ const [view, setView] = useState(initialView);
19
+ const [searchQuery, setSearchQuery] = useState('');
20
+ const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
21
+
22
+ // Filter items
23
+ const filteredItems = items.filter(item => {
24
+ if (!searchQuery) return true;
25
+ const searchStr = searchQuery.toLowerCase();
26
+ return Object.values(item).some(val =>
27
+ String(val).toLowerCase().includes(searchStr)
28
+ );
29
+ });
30
+
31
+ // Sort items
32
+ const sortedItems = [...filteredItems].sort((a, b) => {
33
+ if (!sortConfig.key) return 0;
34
+
35
+ const aVal = a[sortConfig.key];
36
+ const bVal = b[sortConfig.key];
37
+
38
+ if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
39
+ if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
40
+ return 0;
41
+ });
42
+
43
+ const handleSort = (key) => {
44
+ setSortConfig(current => ({
45
+ key,
46
+ direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
47
+ }));
48
+ };
49
+
50
+ return (
51
+ <div className="space-y-6">
52
+ {/* Header */}
53
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
54
+ <div>
55
+ <h1 className="text-2xl font-bold text-slate-900 dark:text-white">{title}</h1>
56
+ {subtitle && <p className="text-slate-500 dark:text-slate-400 mt-1">{subtitle}</p>}
57
+ </div>
58
+ {actions && <div className="flex gap-2">{actions}</div>}
59
+ </div>
60
+
61
+ {/* Controls */}
62
+ <div className="flex flex-col md:flex-row gap-4 items-center justify-between bg-white dark:bg-slate-800 p-4 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm">
63
+ <div className="relative w-full md:w-96">
64
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 w-4 h-4" />
65
+ <input
66
+ type="text"
67
+ placeholder={searchPlaceholder}
68
+ value={searchQuery}
69
+ onChange={(e) => setSearchQuery(e.target.value)}
70
+ className="w-full pl-10 pr-4 py-2 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-sm text-slate-900 dark:text-white"
71
+ />
72
+ </div>
73
+
74
+ <div className="flex items-center gap-2 w-full md:w-auto justify-end">
75
+ <div className="flex bg-slate-100 dark:bg-slate-900 p-1 rounded-lg border border-slate-200 dark:border-slate-700">
76
+ <button
77
+ onClick={() => setView('grid')}
78
+ className={`p-2 rounded-md transition-all ${view === 'grid' ? 'bg-white dark:bg-slate-800 shadow-sm text-primary-600' : 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}`}
79
+ title="Grid View"
80
+ >
81
+ <LayoutGrid size={18} />
82
+ </button>
83
+ <button
84
+ onClick={() => setView('list')}
85
+ className={`p-2 rounded-md transition-all ${view === 'list' ? 'bg-white dark:bg-slate-800 shadow-sm text-primary-600' : 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}`}
86
+ title="List View"
87
+ >
88
+ <List size={18} />
89
+ </button>
90
+ <button
91
+ onClick={() => setView('table')}
92
+ className={`p-2 rounded-md transition-all ${view === 'table' ? 'bg-white dark:bg-slate-800 shadow-sm text-primary-600' : 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}`}
93
+ title="Table View"
94
+ >
95
+ <TableIcon size={18} />
96
+ </button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ {/* Content */}
102
+ {loading ? (
103
+ <div className="flex justify-center py-12">
104
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500"></div>
105
+ </div>
106
+ ) : sortedItems.length === 0 ? (
107
+ emptyState || (
108
+ <div className="text-center py-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700">
109
+ <div className="flex justify-center mb-4">
110
+ <Search className="w-12 h-12 text-slate-300 dark:text-slate-600" />
111
+ </div>
112
+ <h3 className="text-lg font-medium text-slate-900 dark:text-white">No items found</h3>
113
+ <p className="text-slate-500 dark:text-slate-400 mt-1">
114
+ {searchQuery ? `No results matching "${searchQuery}"` : "Get started by creating a new item."}
115
+ </p>
116
+ </div>
117
+ )
118
+ ) : (
119
+ <div className="animate-in fade-in duration-300">
120
+ {view === 'grid' && (
121
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
122
+ {sortedItems.map((item, idx) => (
123
+ <div key={idx}>{renderGrid(item)}</div>
124
+ ))}
125
+ </div>
126
+ )}
127
+
128
+ {view === 'list' && (
129
+ <div className="space-y-4">
130
+ {sortedItems.map((item, idx) => (
131
+ <div key={idx}>{renderList ? renderList(item) : renderGrid(item)}</div>
132
+ ))}
133
+ </div>
134
+ )}
135
+
136
+ {view === 'table' && (
137
+ <div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
138
+ <div className="overflow-x-auto">
139
+ <table className="w-full text-sm text-left">
140
+ <thead className="text-xs text-slate-500 uppercase bg-slate-50 dark:bg-slate-900/50 border-b border-slate-200 dark:border-slate-700">
141
+ <tr>
142
+ {columns.map((col, idx) => (
143
+ <th
144
+ key={idx}
145
+ className="px-6 py-3 font-medium cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
146
+ onClick={() => col.sortable && handleSort(col.key)}
147
+ >
148
+ <div className="flex items-center gap-2">
149
+ {col.header}
150
+ {col.sortable && <ArrowUpDown size={14} className="text-slate-400" />}
151
+ </div>
152
+ </th>
153
+ ))}
154
+ </tr>
155
+ </thead>
156
+ <tbody className="divide-y divide-slate-200 dark:divide-slate-700">
157
+ {sortedItems.map((item, idx) => (
158
+ <tr key={idx} className="bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
159
+ {columns.map((col, colIdx) => (
160
+ <td key={colIdx} className="px-6 py-4">
161
+ {col.render ? col.render(item) : item[col.key]}
162
+ </td>
163
+ ))}
164
+ </tr>
165
+ ))}
166
+ </tbody>
167
+ </table>
168
+ </div>
169
+ </div>
170
+ )}
171
+ </div>
172
+ )}
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { Button } from './Button';
3
+
4
+ export function EmptyState({
5
+ icon: Icon,
6
+ title,
7
+ description,
8
+ action,
9
+ actionLabel,
10
+ onAction,
11
+ className = ''
12
+ }) {
13
+ return (
14
+ <div className={`flex flex-col items-center justify-center py-16 px-4 text-center ${className}`}>
15
+ <div className="w-20 h-20 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-700 rounded-2xl flex items-center justify-center mb-6 shadow-inner">
16
+ {Icon && <Icon className="text-slate-400 dark:text-slate-500" size={40} />}
17
+ </div>
18
+
19
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">
20
+ {title}
21
+ </h3>
22
+
23
+ {description && (
24
+ <p className="text-slate-500 dark:text-slate-400 max-w-md mb-6">
25
+ {description}
26
+ </p>
27
+ )}
28
+
29
+ {(action || (actionLabel && onAction)) && (
30
+ <div>
31
+ {action || (
32
+ <Button onClick={onAction}>
33
+ {actionLabel}
34
+ </Button>
35
+ )}
36
+ </div>
37
+ )}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ // Card wrapper variant
43
+ export function EmptyStateCard(props) {
44
+ return (
45
+ <div className="bg-slate-50 dark:bg-slate-800/30 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700">
46
+ <EmptyState {...props} />
47
+ </div>
48
+ );
49
+ }