@geekmidas/studio 0.0.1 → 0.1.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.
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/Users/cerberus/technanimals/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
package/ui/src/App.tsx CHANGED
@@ -15,6 +15,7 @@ export function App() {
15
15
  > | null>(null);
16
16
  const [loading, setLoading] = useState(true);
17
17
  const [error, setError] = useState<string | null>(null);
18
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
18
19
 
19
20
  // Load tables on mount
20
21
  useEffect(() => {
@@ -22,6 +23,10 @@ export function App() {
22
23
  try {
23
24
  const data = await api.getTables();
24
25
  setTables(data.tables);
26
+ // Auto-select first table if none selected
27
+ if (data.tables.length > 0 && !selectedTable) {
28
+ setSelectedTable(data.tables[0].name);
29
+ }
25
30
  } catch (err) {
26
31
  setError(err instanceof Error ? err.message : 'Failed to load tables');
27
32
  } finally {
@@ -69,73 +74,167 @@ export function App() {
69
74
  setSelectedRow(null);
70
75
  }, []);
71
76
 
72
- const handleBack = useCallback(() => {
73
- setSelectedTable(null);
74
- setTableInfo(null);
75
- setSelectedRow(null);
76
- }, []);
77
-
78
77
  return (
79
- <div className="flex flex-col min-h-screen bg-bg-primary font-mono text-slate-100">
78
+ <div className="flex flex-col h-screen bg-studio-bg text-slate-100">
80
79
  {/* Header */}
81
- <header className="bg-bg-secondary border-b border-border px-6 py-4 flex items-center justify-between">
82
- <div className="flex items-center gap-4">
83
- {selectedTable && (
84
- <button
85
- onClick={handleBack}
86
- className="text-slate-400 hover:text-slate-100 transition-colors"
80
+ <header className="bg-studio-surface border-b border-studio-border px-4 py-3 flex items-center justify-between shrink-0">
81
+ <div className="flex items-center gap-3">
82
+ <button
83
+ onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
84
+ className="p-1.5 hover:bg-studio-hover rounded transition-colors"
85
+ title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
86
+ >
87
+ <svg
88
+ className="w-5 h-5 text-slate-400"
89
+ fill="none"
90
+ stroke="currentColor"
91
+ viewBox="0 0 24 24"
87
92
  >
88
- &larr; Back
89
- </button>
90
- )}
91
- <h1 className="text-xl font-semibold flex items-center gap-2">
92
- <span className="text-purple-400">&#128451;</span> Studio
93
- {selectedTable && (
94
- <span className="text-slate-400">/ {selectedTable}</span>
95
- )}
93
+ <path
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ strokeWidth={2}
97
+ d="M4 6h16M4 12h16M4 18h16"
98
+ />
99
+ </svg>
100
+ </button>
101
+ <h1 className="text-lg font-semibold flex items-center gap-2">
102
+ <span className="text-emerald-400">
103
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
104
+ <path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
105
+ </svg>
106
+ </span>
107
+ Studio
96
108
  </h1>
97
109
  </div>
98
- <div className="flex items-center gap-4">
99
- <span className="text-sm text-slate-400">{tables.length} tables</span>
110
+ <div className="flex items-center gap-3">
100
111
  <button
101
112
  onClick={handleRefresh}
102
113
  disabled={loading}
103
- className="px-3 py-1 text-sm bg-bg-tertiary hover:bg-slate-600 rounded transition-colors disabled:opacity-50"
114
+ className="flex items-center gap-2 px-3 py-1.5 text-sm bg-studio-hover hover:bg-studio-active rounded transition-colors disabled:opacity-50"
104
115
  >
105
- {loading ? 'Refreshing...' : 'Refresh'}
116
+ <svg
117
+ className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`}
118
+ fill="none"
119
+ stroke="currentColor"
120
+ viewBox="0 0 24 24"
121
+ >
122
+ <path
123
+ strokeLinecap="round"
124
+ strokeLinejoin="round"
125
+ strokeWidth={2}
126
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
127
+ />
128
+ </svg>
129
+ Refresh
106
130
  </button>
107
131
  </div>
108
132
  </header>
109
133
 
110
134
  {/* Main Content */}
111
135
  <main className="flex-1 flex overflow-hidden">
112
- {error ? (
113
- <div className="flex-1 flex items-center justify-center text-red-400">
114
- {error}
136
+ {/* Sidebar */}
137
+ <aside
138
+ className={`${sidebarCollapsed ? 'w-0' : 'w-64'} bg-studio-surface border-r border-studio-border flex flex-col shrink-0 transition-all duration-200 overflow-hidden`}
139
+ >
140
+ <div className="p-3 border-b border-studio-border">
141
+ <div className="relative">
142
+ <svg
143
+ className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500"
144
+ fill="none"
145
+ stroke="currentColor"
146
+ viewBox="0 0 24 24"
147
+ >
148
+ <path
149
+ strokeLinecap="round"
150
+ strokeLinejoin="round"
151
+ strokeWidth={2}
152
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
153
+ />
154
+ </svg>
155
+ <input
156
+ type="text"
157
+ placeholder="Search tables..."
158
+ className="w-full bg-studio-bg border border-studio-border rounded px-3 py-1.5 pl-9 text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-500/50"
159
+ />
160
+ </div>
161
+ </div>
162
+ <div className="flex-1 overflow-auto">
163
+ {loading && tables.length === 0 ? (
164
+ <div className="p-4 text-center text-slate-500 text-sm">
165
+ Loading...
166
+ </div>
167
+ ) : (
168
+ <TableList
169
+ tables={tables}
170
+ selectedTable={selectedTable}
171
+ onSelect={handleSelectTable}
172
+ />
173
+ )}
115
174
  </div>
116
- ) : loading && tables.length === 0 ? (
117
- <div className="flex-1 flex items-center justify-center text-slate-500">
118
- Loading...
175
+ <div className="p-3 border-t border-studio-border text-xs text-slate-500">
176
+ {tables.length} tables
119
177
  </div>
120
- ) : !selectedTable ? (
121
- <TableList tables={tables} onSelect={handleSelectTable} />
122
- ) : (
123
- <TableView
124
- tableName={selectedTable}
125
- tableInfo={tableInfo}
126
- onRowSelect={setSelectedRow}
178
+ </aside>
179
+
180
+ {/* Main Area */}
181
+ <div className="flex-1 flex flex-col overflow-hidden">
182
+ {error ? (
183
+ <div className="flex-1 flex items-center justify-center text-red-400">
184
+ <div className="text-center">
185
+ <svg
186
+ className="w-12 h-12 mx-auto mb-3 text-red-400/50"
187
+ fill="none"
188
+ stroke="currentColor"
189
+ viewBox="0 0 24 24"
190
+ >
191
+ <path
192
+ strokeLinecap="round"
193
+ strokeLinejoin="round"
194
+ strokeWidth={2}
195
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
196
+ />
197
+ </svg>
198
+ <p>{error}</p>
199
+ </div>
200
+ </div>
201
+ ) : !selectedTable ? (
202
+ <div className="flex-1 flex items-center justify-center text-slate-500">
203
+ <div className="text-center">
204
+ <svg
205
+ className="w-12 h-12 mx-auto mb-3 text-slate-600"
206
+ fill="none"
207
+ stroke="currentColor"
208
+ viewBox="0 0 24 24"
209
+ >
210
+ <path
211
+ strokeLinecap="round"
212
+ strokeLinejoin="round"
213
+ strokeWidth={2}
214
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"
215
+ />
216
+ </svg>
217
+ <p>Select a table to view data</p>
218
+ </div>
219
+ </div>
220
+ ) : (
221
+ <TableView
222
+ tableName={selectedTable}
223
+ tableInfo={tableInfo}
224
+ onRowSelect={setSelectedRow}
225
+ />
226
+ )}
227
+ </div>
228
+
229
+ {/* Row Detail Panel */}
230
+ {selectedRow && tableInfo && (
231
+ <RowDetail
232
+ row={selectedRow}
233
+ columns={tableInfo.columns}
234
+ onClose={() => setSelectedRow(null)}
127
235
  />
128
236
  )}
129
237
  </main>
130
-
131
- {/* Row Detail Panel */}
132
- {selectedRow && tableInfo && (
133
- <RowDetail
134
- row={selectedRow}
135
- columns={tableInfo.columns}
136
- onClose={() => setSelectedRow(null)}
137
- />
138
- )}
139
238
  </div>
140
239
  );
141
240
  }
@@ -0,0 +1,213 @@
1
+ import { useCallback } from 'react';
2
+ import type { ColumnInfo, FilterConfig } from '../types';
3
+
4
+ interface FilterPanelProps {
5
+ columns: ColumnInfo[];
6
+ filters: FilterConfig[];
7
+ onFiltersChange: (filters: FilterConfig[]) => void;
8
+ onClose: () => void;
9
+ }
10
+
11
+ const OPERATORS: { value: string; label: string; types?: string[] }[] = [
12
+ { value: 'eq', label: 'equals' },
13
+ { value: 'neq', label: 'not equals' },
14
+ { value: 'gt', label: 'greater than', types: ['number', 'date', 'datetime'] },
15
+ {
16
+ value: 'gte',
17
+ label: 'greater than or equal',
18
+ types: ['number', 'date', 'datetime'],
19
+ },
20
+ { value: 'lt', label: 'less than', types: ['number', 'date', 'datetime'] },
21
+ {
22
+ value: 'lte',
23
+ label: 'less than or equal',
24
+ types: ['number', 'date', 'datetime'],
25
+ },
26
+ { value: 'like', label: 'contains', types: ['string'] },
27
+ { value: 'ilike', label: 'contains (case insensitive)', types: ['string'] },
28
+ { value: 'is_null', label: 'is null' },
29
+ { value: 'is_not_null', label: 'is not null' },
30
+ ];
31
+
32
+ function getOperatorsForColumn(column: ColumnInfo) {
33
+ return OPERATORS.filter(
34
+ (op) => !op.types || op.types.includes(column.type || 'string'),
35
+ );
36
+ }
37
+
38
+ export function FilterPanel({
39
+ columns,
40
+ filters,
41
+ onFiltersChange,
42
+ onClose,
43
+ }: FilterPanelProps) {
44
+ const addFilter = useCallback(() => {
45
+ const firstColumn = columns[0];
46
+ if (!firstColumn) return;
47
+
48
+ onFiltersChange([
49
+ ...filters,
50
+ { column: firstColumn.name, operator: 'eq', value: '' },
51
+ ]);
52
+ }, [columns, filters, onFiltersChange]);
53
+
54
+ const updateFilter = useCallback(
55
+ (index: number, updates: Partial<FilterConfig>) => {
56
+ const newFilters = [...filters];
57
+ newFilters[index] = { ...newFilters[index], ...updates };
58
+ onFiltersChange(newFilters);
59
+ },
60
+ [filters, onFiltersChange],
61
+ );
62
+
63
+ const removeFilter = useCallback(
64
+ (index: number) => {
65
+ onFiltersChange(filters.filter((_, i) => i !== index));
66
+ },
67
+ [filters, onFiltersChange],
68
+ );
69
+
70
+ const clearAll = useCallback(() => {
71
+ onFiltersChange([]);
72
+ }, [onFiltersChange]);
73
+
74
+ return (
75
+ <div className="filter-panel bg-studio-surface border-b border-studio-border p-4">
76
+ <div className="flex items-center justify-between mb-3">
77
+ <h3 className="text-sm font-medium text-slate-300">Filters</h3>
78
+ <div className="flex items-center gap-2">
79
+ {filters.length > 0 && (
80
+ <button
81
+ onClick={clearAll}
82
+ className="text-xs text-slate-500 hover:text-slate-300 transition-colors"
83
+ >
84
+ Clear all
85
+ </button>
86
+ )}
87
+ <button
88
+ onClick={onClose}
89
+ className="p-1 hover:bg-studio-hover rounded transition-colors"
90
+ >
91
+ <svg
92
+ className="w-4 h-4 text-slate-400"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ viewBox="0 0 24 24"
96
+ >
97
+ <path
98
+ strokeLinecap="round"
99
+ strokeLinejoin="round"
100
+ strokeWidth={2}
101
+ d="M6 18L18 6M6 6l12 12"
102
+ />
103
+ </svg>
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <div className="space-y-2">
109
+ {filters.map((filter, index) => {
110
+ const column = columns.find((c) => c.name === filter.column);
111
+ const operators = column ? getOperatorsForColumn(column) : OPERATORS;
112
+ const needsValue = !['is_null', 'is_not_null'].includes(
113
+ filter.operator,
114
+ );
115
+
116
+ return (
117
+ <div key={index} className="flex items-center gap-2">
118
+ {index > 0 && (
119
+ <span className="text-xs text-slate-500 w-8">and</span>
120
+ )}
121
+ {index === 0 && (
122
+ <span className="text-xs text-slate-500 w-8">Where</span>
123
+ )}
124
+
125
+ {/* Column select */}
126
+ <select
127
+ value={filter.column}
128
+ onChange={(e) =>
129
+ updateFilter(index, { column: e.target.value })
130
+ }
131
+ className="select flex-1 min-w-0"
132
+ >
133
+ {columns.map((col) => (
134
+ <option key={col.name} value={col.name}>
135
+ {col.name}
136
+ </option>
137
+ ))}
138
+ </select>
139
+
140
+ {/* Operator select */}
141
+ <select
142
+ value={filter.operator}
143
+ onChange={(e) =>
144
+ updateFilter(index, { operator: e.target.value })
145
+ }
146
+ className="select w-48"
147
+ >
148
+ {operators.map((op) => (
149
+ <option key={op.value} value={op.value}>
150
+ {op.label}
151
+ </option>
152
+ ))}
153
+ </select>
154
+
155
+ {/* Value input */}
156
+ {needsValue && (
157
+ <input
158
+ type="text"
159
+ value={filter.value}
160
+ onChange={(e) =>
161
+ updateFilter(index, { value: e.target.value })
162
+ }
163
+ placeholder="Enter value..."
164
+ className="input flex-1 min-w-0"
165
+ />
166
+ )}
167
+
168
+ {/* Remove button */}
169
+ <button
170
+ onClick={() => removeFilter(index)}
171
+ className="p-1.5 hover:bg-studio-hover rounded transition-colors text-slate-500 hover:text-slate-300"
172
+ >
173
+ <svg
174
+ className="w-4 h-4"
175
+ fill="none"
176
+ stroke="currentColor"
177
+ viewBox="0 0 24 24"
178
+ >
179
+ <path
180
+ strokeLinecap="round"
181
+ strokeLinejoin="round"
182
+ strokeWidth={2}
183
+ d="M6 18L18 6M6 6l12 12"
184
+ />
185
+ </svg>
186
+ </button>
187
+ </div>
188
+ );
189
+ })}
190
+ </div>
191
+
192
+ <button
193
+ onClick={addFilter}
194
+ className="mt-3 flex items-center gap-2 text-sm text-emerald-400 hover:text-emerald-300 transition-colors"
195
+ >
196
+ <svg
197
+ className="w-4 h-4"
198
+ fill="none"
199
+ stroke="currentColor"
200
+ viewBox="0 0 24 24"
201
+ >
202
+ <path
203
+ strokeLinecap="round"
204
+ strokeLinejoin="round"
205
+ strokeWidth={2}
206
+ d="M12 4v16m8-8H4"
207
+ />
208
+ </svg>
209
+ Add filter
210
+ </button>
211
+ </div>
212
+ );
213
+ }