@doppelgangerdev/doppelganger 0.2.2

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 (63) hide show
  1. package/.dockerignore +9 -0
  2. package/.github/workflows/docker-publish.yml +59 -0
  3. package/CODE_OF_CONDUCT.md +28 -0
  4. package/CONTRIBUTING.md +42 -0
  5. package/Dockerfile +44 -0
  6. package/LICENSE +163 -0
  7. package/README.md +133 -0
  8. package/TERMS.md +16 -0
  9. package/THIRD_PARTY_LICENSES.md +3502 -0
  10. package/agent.js +1240 -0
  11. package/headful.js +171 -0
  12. package/index.html +21 -0
  13. package/n8n-nodes-doppelganger/LICENSE +201 -0
  14. package/n8n-nodes-doppelganger/README.md +42 -0
  15. package/n8n-nodes-doppelganger/package-lock.json +6128 -0
  16. package/n8n-nodes-doppelganger/package.json +36 -0
  17. package/n8n-nodes-doppelganger/src/credentials/DoppelgangerApi.credentials.ts +35 -0
  18. package/n8n-nodes-doppelganger/src/index.ts +4 -0
  19. package/n8n-nodes-doppelganger/src/nodes/Doppelganger/Doppelganger.node.ts +147 -0
  20. package/n8n-nodes-doppelganger/src/nodes/Doppelganger/icon.png +0 -0
  21. package/n8n-nodes-doppelganger/tsconfig.json +14 -0
  22. package/package.json +45 -0
  23. package/postcss.config.js +6 -0
  24. package/public/icon.png +0 -0
  25. package/public/novnc.html +151 -0
  26. package/public/styles.css +86 -0
  27. package/scrape.js +389 -0
  28. package/server.js +875 -0
  29. package/src/App.tsx +722 -0
  30. package/src/components/AuthScreen.tsx +95 -0
  31. package/src/components/CodeEditor.tsx +70 -0
  32. package/src/components/DashboardScreen.tsx +133 -0
  33. package/src/components/EditorScreen.tsx +1519 -0
  34. package/src/components/ExecutionDetailScreen.tsx +115 -0
  35. package/src/components/ExecutionsScreen.tsx +156 -0
  36. package/src/components/LoadingScreen.tsx +26 -0
  37. package/src/components/NotFoundScreen.tsx +34 -0
  38. package/src/components/RichInput.tsx +68 -0
  39. package/src/components/SettingsScreen.tsx +228 -0
  40. package/src/components/Sidebar.tsx +61 -0
  41. package/src/components/app/CenterAlert.tsx +44 -0
  42. package/src/components/app/CenterConfirm.tsx +33 -0
  43. package/src/components/app/EditorLoader.tsx +89 -0
  44. package/src/components/editor/ActionPalette.tsx +79 -0
  45. package/src/components/editor/JsonEditorPane.tsx +71 -0
  46. package/src/components/editor/ResultsPane.tsx +641 -0
  47. package/src/components/editor/actionCatalog.ts +23 -0
  48. package/src/components/settings/AgentAiPanel.tsx +105 -0
  49. package/src/components/settings/ApiKeyPanel.tsx +68 -0
  50. package/src/components/settings/CookiesPanel.tsx +154 -0
  51. package/src/components/settings/LayoutPanel.tsx +46 -0
  52. package/src/components/settings/ScreenshotsPanel.tsx +64 -0
  53. package/src/components/settings/SettingsHeader.tsx +28 -0
  54. package/src/components/settings/StoragePanel.tsx +35 -0
  55. package/src/index.css +287 -0
  56. package/src/main.tsx +13 -0
  57. package/src/types.ts +114 -0
  58. package/src/utils/syntaxHighlight.ts +140 -0
  59. package/start-vnc.sh +52 -0
  60. package/tailwind.config.js +22 -0
  61. package/tsconfig.json +39 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.mts +27 -0
@@ -0,0 +1,105 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Bot, Save, Eye, EyeOff } from 'lucide-react';
3
+
4
+ interface AgentAiPanelProps {
5
+ apiKey: string | null;
6
+ model: string;
7
+ enabled: boolean;
8
+ loading: boolean;
9
+ saving: boolean;
10
+ onSave: (next: { apiKey: string; model: string; enabled: boolean }) => void;
11
+ }
12
+
13
+ const AgentAiPanel: React.FC<AgentAiPanelProps> = ({ apiKey, model, enabled, loading, saving, onSave }) => {
14
+ const [localKey, setLocalKey] = useState(apiKey || '');
15
+ const [localModel, setLocalModel] = useState(model || 'gpt-5-nano');
16
+ const [visible, setVisible] = useState(false);
17
+ const [localEnabled, setLocalEnabled] = useState(enabled);
18
+
19
+ useEffect(() => {
20
+ setLocalKey(apiKey || '');
21
+ }, [apiKey]);
22
+
23
+ useEffect(() => {
24
+ setLocalModel(model || 'gpt-5-nano');
25
+ }, [model]);
26
+
27
+ useEffect(() => {
28
+ setLocalEnabled(enabled);
29
+ }, [enabled]);
30
+
31
+ const submit = () => {
32
+ onSave({
33
+ apiKey: localKey.trim(),
34
+ model: localModel.trim() || 'gpt-5-nano',
35
+ enabled: localEnabled
36
+ });
37
+ };
38
+
39
+ return (
40
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
41
+ <div className="flex items-center gap-4 mb-2">
42
+ <div className="w-10 h-10 rounded-2xl bg-white/5 flex items-center justify-center text-gray-400"><Bot className="w-5 h-5" /></div>
43
+ <div>
44
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">Agent AI</h3>
45
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Bring your own OpenAI key</p>
46
+ </div>
47
+ </div>
48
+
49
+ <div className="space-y-4">
50
+ <label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">OpenAI API Key</label>
51
+ <div className="flex items-center gap-3 rounded-2xl bg-black/40 border border-white/10 px-4 py-2">
52
+ <input
53
+ type={visible ? 'text' : 'password'}
54
+ value={loading ? 'Loading...' : localKey}
55
+ onChange={(e) => setLocalKey(e.target.value)}
56
+ disabled={loading}
57
+ placeholder="sk-..."
58
+ className="flex-1 bg-transparent text-[10px] text-blue-200/80 font-mono focus:outline-none"
59
+ />
60
+ <button
61
+ onClick={() => setVisible((prev) => !prev)}
62
+ className="p-2 rounded-xl border border-white/10 text-white/70 hover:text-white hover:bg-white/5 transition-all"
63
+ title={visible ? 'Hide key' : 'Show key'}
64
+ aria-label={visible ? 'Hide key' : 'Show key'}
65
+ type="button"
66
+ >
67
+ {visible ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
68
+ </button>
69
+ </div>
70
+ <label className="text-[9px] font-bold text-gray-400 uppercase tracking-[0.2em]">Model</label>
71
+ <input
72
+ type="text"
73
+ value={localModel}
74
+ onChange={(e) => setLocalModel(e.target.value)}
75
+ placeholder="gpt-5-nano"
76
+ className="w-full bg-white/[0.05] border border-white/10 rounded-2xl px-4 py-2 text-[10px] text-white font-mono focus:outline-none focus:border-white/30 transition-all"
77
+ />
78
+ <label className="flex items-center gap-3 p-3 rounded-2xl bg-white/[0.02] border border-white/5 hover:bg-white/[0.05] transition-all cursor-pointer group">
79
+ <input
80
+ type="checkbox"
81
+ checked={localEnabled}
82
+ onChange={(e) => setLocalEnabled(e.target.checked)}
83
+ className="w-4 h-4 rounded border-white/20 bg-transparent"
84
+ />
85
+ <span className="text-[9px] font-bold text-gray-500 uppercase tracking-widest group-hover:text-white">Enable AI workflow generation</span>
86
+ </label>
87
+ <div className="flex gap-3">
88
+ <button
89
+ onClick={submit}
90
+ disabled={saving || loading}
91
+ className="flex-1 px-6 py-3 rounded-2xl text-[9px] font-bold uppercase tracking-widest bg-white text-black hover:scale-105 transition-all disabled:opacity-60 disabled:hover:scale-100 inline-flex items-center justify-center gap-2"
92
+ >
93
+ <Save className="w-4 h-4" />
94
+ Save AI Settings
95
+ </button>
96
+ </div>
97
+ <p className="text-[9px] text-gray-600 uppercase tracking-widest">
98
+ Used by Agent Tasks to generate workflow blocks from steps.
99
+ </p>
100
+ </div>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default AgentAiPanel;
@@ -0,0 +1,68 @@
1
+ import { useState } from 'react';
2
+ import { Copy, Database, Eye, EyeOff } from 'lucide-react';
3
+
4
+ interface ApiKeyPanelProps {
5
+ apiKey: string | null;
6
+ loading: boolean;
7
+ saving: boolean;
8
+ onRegenerate: () => void;
9
+ onCopy: () => void;
10
+ }
11
+
12
+ const ApiKeyPanel: React.FC<ApiKeyPanelProps> = ({ apiKey, loading, saving, onRegenerate, onCopy }) => {
13
+ const [visible, setVisible] = useState(false);
14
+
15
+ return (
16
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
17
+ <div className="flex items-center gap-4 mb-2">
18
+ <div className="w-10 h-10 rounded-2xl bg-white/5 flex items-center justify-center text-gray-400"><Database className="w-5 h-5" /></div>
19
+ <div>
20
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">API Key</h3>
21
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Manage task API access</p>
22
+ </div>
23
+ </div>
24
+ <div className="flex flex-col gap-4">
25
+ <div className="rounded-2xl bg-black/40 border border-white/10 px-4 py-3 font-mono text-[10px] text-blue-200/80 break-all min-h-[44px] flex items-center justify-between gap-3">
26
+ <span className={apiKey && !visible ? 'text-[14px] leading-none' : undefined}>
27
+ {loading
28
+ ? 'Loading...'
29
+ : (apiKey
30
+ ? (visible ? apiKey : '••••••••••••••••••••••••••••••••••••••••')
31
+ : 'No API key set')}
32
+ </span>
33
+ <button
34
+ onClick={() => setVisible((prev) => !prev)}
35
+ disabled={!apiKey}
36
+ className="p-2 rounded-xl border border-white/10 text-white/70 hover:text-white hover:bg-white/5 transition-all disabled:opacity-50"
37
+ title={visible ? 'Hide key' : 'Show key'}
38
+ aria-label={visible ? 'Hide key' : 'Show key'}
39
+ >
40
+ {visible ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
41
+ </button>
42
+ </div>
43
+ <div className="flex gap-3">
44
+ <button
45
+ onClick={onRegenerate}
46
+ disabled={saving}
47
+ className="flex-1 px-6 py-3 rounded-2xl text-[9px] font-bold uppercase tracking-widest bg-white text-black hover:scale-105 transition-all disabled:opacity-60 disabled:hover:scale-100"
48
+ >
49
+ {apiKey ? 'Rotate Key' : 'Generate Key'}
50
+ </button>
51
+ <button
52
+ onClick={onCopy}
53
+ disabled={!apiKey}
54
+ className="flex-1 px-6 py-3 rounded-2xl text-[9px] font-bold uppercase tracking-widest border border-white/10 text-white hover:bg-white/5 transition-all disabled:opacity-50 inline-flex items-center justify-center gap-2"
55
+ >
56
+ <Copy className="w-4 h-4" />
57
+ Copy Key
58
+ </button>
59
+ </div>
60
+ <p className="text-[9px] text-gray-600 uppercase tracking-widest">
61
+ Use this key in `key`, `x-api-key`, or `Authorization: Bearer` headers.
62
+ </p>
63
+ </div>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default ApiKeyPanel;
@@ -0,0 +1,154 @@
1
+ import { useState } from 'react';
2
+ import { Database } from 'lucide-react';
3
+
4
+ interface CookieEntry {
5
+ name: string;
6
+ value: string;
7
+ domain?: string;
8
+ path?: string;
9
+ expires?: number;
10
+ }
11
+
12
+ interface CookiesPanelProps {
13
+ cookies: CookieEntry[];
14
+ originsCount: number;
15
+ loading: boolean;
16
+ onClear: () => void;
17
+ onDelete: (cookie: { name: string; domain?: string; path?: string }) => void;
18
+ }
19
+
20
+ const CookiesPanel: React.FC<CookiesPanelProps> = ({ cookies, originsCount, loading, onClear, onDelete }) => {
21
+ const [expandedCookies, setExpandedCookies] = useState<Record<string, boolean>>({});
22
+ const [decodedCookies, setDecodedCookies] = useState<Record<string, boolean>>({});
23
+
24
+ const cookieKey = (cookie: CookieEntry) => {
25
+ return `${cookie.name}|${cookie.domain || ''}|${cookie.path || ''}|${cookie.expires || ''}`;
26
+ };
27
+
28
+ const isMostlyPrintable = (value: string) => {
29
+ if (!value) return false;
30
+ let printable = 0;
31
+ for (let i = 0; i < value.length; i += 1) {
32
+ const code = value.charCodeAt(i);
33
+ if (code === 9 || code === 10 || code === 13 || (code >= 32 && code <= 126)) {
34
+ printable += 1;
35
+ }
36
+ }
37
+ return printable / value.length >= 0.85;
38
+ };
39
+
40
+ const decodeCookieValue = (value: string) => {
41
+ if (!value) return null;
42
+ if (/%[0-9A-Fa-f]{2}/.test(value)) {
43
+ try {
44
+ const decoded = decodeURIComponent(value);
45
+ if (decoded !== value && isMostlyPrintable(decoded)) {
46
+ return { value: decoded, kind: 'URL' as const };
47
+ }
48
+ } catch {
49
+ // Ignore invalid URI sequences
50
+ }
51
+ }
52
+ if (/^[A-Za-z0-9+/=]+$/.test(value) && value.length >= 12 && value.length % 4 === 0) {
53
+ try {
54
+ const decoded = atob(value);
55
+ if (decoded && isMostlyPrintable(decoded)) {
56
+ return { value: decoded, kind: 'Base64' as const };
57
+ }
58
+ } catch {
59
+ // Ignore invalid base64
60
+ }
61
+ }
62
+ return null;
63
+ };
64
+
65
+ const toggleCookie = (cookie: CookieEntry) => {
66
+ const key = cookieKey(cookie);
67
+ setExpandedCookies((prev) => ({ ...prev, [key]: !prev[key] }));
68
+ };
69
+
70
+ const toggleDecodedCookie = (cookie: CookieEntry) => {
71
+ const key = cookieKey(cookie);
72
+ setDecodedCookies((prev) => ({ ...prev, [key]: !prev[key] }));
73
+ };
74
+
75
+ return (
76
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
77
+ <div className="flex items-center justify-between">
78
+ <div className="flex items-center gap-4">
79
+ <div className="w-10 h-10 rounded-2xl bg-white/5 flex items-center justify-center text-gray-400"><Database className="w-5 h-5" /></div>
80
+ <div>
81
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">Cookies</h3>
82
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Browser storage state</p>
83
+ </div>
84
+ </div>
85
+ <button
86
+ onClick={onClear}
87
+ className="px-4 py-2 text-[9px] font-bold uppercase tracking-widest rounded-xl bg-yellow-500/5 border border-yellow-500/10 text-yellow-400 hover:bg-yellow-500/10 transition-all"
88
+ >
89
+ Clear Cookies
90
+ </button>
91
+ </div>
92
+ {loading && <div className="text-[9px] text-gray-500 uppercase tracking-widest">Loading data...</div>}
93
+ {!loading && cookies.length === 0 && (
94
+ <div className="text-[9px] text-gray-600 uppercase tracking-widest">No cookies found.</div>
95
+ )}
96
+ <div className="space-y-3">
97
+ {cookies.map((cookie) => {
98
+ const key = cookieKey(cookie);
99
+ const isExpanded = !!expandedCookies[key];
100
+ const value = cookie.value || '';
101
+ const decodedCandidate = decodeCookieValue(value);
102
+ const showDecoded = !!decodedCandidate && !!decodedCookies[key];
103
+ const fullValue = showDecoded && decodedCandidate ? decodedCandidate.value : value;
104
+ const displayValue = isExpanded || fullValue.length <= 120
105
+ ? fullValue
106
+ : `${fullValue.slice(0, 120)}...`;
107
+ return (
108
+ <div key={key} className="bg-white/5 border border-white/10 rounded-2xl p-4 space-y-3">
109
+ <div className="flex items-center justify-between gap-4">
110
+ <div className="min-w-0">
111
+ <div className="text-[10px] font-bold text-white uppercase tracking-widest truncate">{cookie.name}</div>
112
+ <div className="text-[8px] text-gray-500 uppercase tracking-[0.2em]">
113
+ {(cookie.domain || 'local')} | {(cookie.path || '/')}
114
+ {cookie.expires ? ` | ${new Date(cookie.expires * 1000).toLocaleString()}` : ''}
115
+ </div>
116
+ </div>
117
+ <div className="flex items-center gap-2">
118
+ {decodedCandidate && (
119
+ <button
120
+ onClick={() => toggleDecodedCookie(cookie)}
121
+ className="px-3 py-2 text-[9px] font-bold uppercase tracking-widest rounded-xl border border-white/10 text-white/70 hover:text-white hover:bg-white/5 transition-all"
122
+ >
123
+ {showDecoded ? 'Show Raw' : `Decode ${decodedCandidate.kind}`}
124
+ </button>
125
+ )}
126
+ <button
127
+ onClick={() => onDelete(cookie)}
128
+ className="px-3 py-2 text-[9px] font-bold uppercase tracking-widest rounded-xl bg-red-500/5 border border-red-500/10 text-red-400 hover:bg-red-500/10 transition-all"
129
+ >
130
+ Delete
131
+ </button>
132
+ </div>
133
+ </div>
134
+ <div
135
+ onClick={() => toggleCookie(cookie)}
136
+ className="cursor-pointer rounded-xl bg-black/40 border border-white/10 px-3 py-2 font-mono text-[10px] text-blue-200/80 whitespace-pre-wrap break-words"
137
+ title="Click to expand/collapse"
138
+ >
139
+ {displayValue || '(empty)'}
140
+ </div>
141
+ </div>
142
+ );
143
+ })}
144
+ </div>
145
+ {!loading && originsCount > 0 && (
146
+ <div className="pt-2 text-[8px] text-gray-600 uppercase tracking-widest">
147
+ Origins stored: {originsCount}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ };
153
+
154
+ export default CookiesPanel;
@@ -0,0 +1,46 @@
1
+ interface LayoutPanelProps {
2
+ splitPercent: number;
3
+ onChange: (value: number) => void;
4
+ onReset: () => void;
5
+ }
6
+
7
+ const LayoutPanel: React.FC<LayoutPanelProps> = ({ splitPercent, onChange, onReset }) => {
8
+ const safeSplit = Number.isFinite(splitPercent) ? Math.min(75, Math.max(25, splitPercent)) : 30;
9
+ return (
10
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
11
+ <div className="flex items-center justify-between">
12
+ <div>
13
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">Layout</h3>
14
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Default editor split</p>
15
+ </div>
16
+ <button
17
+ onClick={onReset}
18
+ className="px-4 py-2 border border-white/10 text-[9px] font-bold rounded-xl uppercase tracking-widest text-white/70 hover:text-white hover:bg-white/5 transition-all"
19
+ >
20
+ Reset 30/70
21
+ </button>
22
+ </div>
23
+ <div className="space-y-3">
24
+ <div className="flex items-center justify-between text-[9px] font-bold uppercase tracking-widest text-gray-500">
25
+ <span>Editor</span>
26
+ <span>{safeSplit}%</span>
27
+ </div>
28
+ <input
29
+ type="range"
30
+ min={25}
31
+ max={75}
32
+ step={5}
33
+ value={safeSplit}
34
+ onChange={(e) => onChange(Math.min(75, Math.max(25, Number(e.target.value))))}
35
+ className="w-full accent-white"
36
+ />
37
+ <div className="flex items-center justify-between text-[9px] font-bold uppercase tracking-widest text-gray-500">
38
+ <span>Preview</span>
39
+ <span>{100 - safeSplit}%</span>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default LayoutPanel;
@@ -0,0 +1,64 @@
1
+ import { Image as ImageIcon } from 'lucide-react';
2
+
3
+ interface ScreenshotEntry {
4
+ name: string;
5
+ url: string;
6
+ size: number;
7
+ modified: number;
8
+ }
9
+
10
+ interface ScreenshotsPanelProps {
11
+ screenshots: ScreenshotEntry[];
12
+ loading: boolean;
13
+ onRefresh: () => void;
14
+ onDelete: (name: string) => void;
15
+ }
16
+
17
+ const ScreenshotsPanel: React.FC<ScreenshotsPanelProps> = ({ screenshots, loading, onRefresh, onDelete }) => {
18
+ return (
19
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
20
+ <div className="flex items-center justify-between">
21
+ <div className="flex items-center gap-4">
22
+ <div className="w-10 h-10 rounded-2xl bg-white/5 flex items-center justify-center text-gray-400"><ImageIcon className="w-5 h-5" /></div>
23
+ <div>
24
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">Screenshots</h3>
25
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Stored captures</p>
26
+ </div>
27
+ </div>
28
+ <button
29
+ onClick={onRefresh}
30
+ className="px-4 py-2 border border-white/10 text-[9px] font-bold rounded-xl uppercase tracking-widest text-white hover:bg-white/5 transition-all"
31
+ >
32
+ Refresh
33
+ </button>
34
+ </div>
35
+ {loading && <div className="text-[9px] text-gray-500 uppercase tracking-widest">Loading data...</div>}
36
+ {!loading && screenshots.length === 0 && (
37
+ <div className="text-[9px] text-gray-600 uppercase tracking-widest">No screenshots found.</div>
38
+ )}
39
+ <div className="space-y-3">
40
+ {screenshots.map((shot) => (
41
+ <div key={shot.name} className="bg-white/5 border border-white/10 rounded-2xl p-4 flex items-center gap-4">
42
+ <div className="w-16 h-16 bg-black rounded-xl overflow-hidden shrink-0 border border-white/10">
43
+ <img src={shot.url} className="w-full h-full object-cover" />
44
+ </div>
45
+ <div className="flex-1 min-w-0">
46
+ <div className="text-[10px] font-bold text-white uppercase tracking-widest truncate">{shot.name}</div>
47
+ <div className="text-[8px] text-gray-500 uppercase tracking-[0.2em]">
48
+ {new Date(shot.modified).toLocaleString()} | {(shot.size / 1024).toFixed(1)} KB
49
+ </div>
50
+ </div>
51
+ <button
52
+ onClick={() => onDelete(shot.name)}
53
+ className="px-4 py-2 text-[9px] font-bold uppercase tracking-widest rounded-xl bg-red-500/5 border border-red-500/10 text-red-400 hover:bg-red-500/10 transition-all"
54
+ >
55
+ Delete
56
+ </button>
57
+ </div>
58
+ ))}
59
+ </div>
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default ScreenshotsPanel;
@@ -0,0 +1,28 @@
1
+ interface SettingsHeaderProps {
2
+ tab: 'system' | 'data';
3
+ onTabChange: (tab: 'system' | 'data') => void;
4
+ }
5
+
6
+ const SettingsHeader: React.FC<SettingsHeaderProps> = ({ tab, onTabChange }) => {
7
+ return (
8
+ <div className="flex items-end justify-between mb-8">
9
+ <div className="space-y-2">
10
+ <p className="text-[10px] font-bold text-purple-500 uppercase tracking-[0.4em]">System</p>
11
+ <h2 className="text-4xl font-bold tracking-tighter text-white">Settings</h2>
12
+ </div>
13
+ <div className="flex bg-white/5 rounded-xl p-1 border border-white/5">
14
+ {(['system', 'data'] as const).map((t) => (
15
+ <button
16
+ key={t}
17
+ onClick={() => onTabChange(t)}
18
+ className={`px-4 py-2 text-[9px] font-bold uppercase tracking-widest rounded-lg transition-all ${tab === t ? 'bg-white text-black' : 'text-gray-500 hover:text-white'}`}
19
+ >
20
+ {t}
21
+ </button>
22
+ ))}
23
+ </div>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ export default SettingsHeader;
@@ -0,0 +1,35 @@
1
+ import { Trash2 } from 'lucide-react';
2
+
3
+ interface StoragePanelProps {
4
+ onClearStorage: (type: 'screenshots' | 'cookies') => void;
5
+ }
6
+
7
+ const StoragePanel: React.FC<StoragePanelProps> = ({ onClearStorage }) => {
8
+ return (
9
+ <div className="glass-card p-8 rounded-[40px] space-y-6">
10
+ <div className="flex items-center gap-4 mb-2">
11
+ <div className="w-10 h-10 rounded-2xl bg-white/5 flex items-center justify-center text-gray-400"><Trash2 className="w-5 h-5" /></div>
12
+ <div>
13
+ <h3 className="text-sm font-bold text-white uppercase tracking-widest">Storage</h3>
14
+ <p className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">Manage stored data</p>
15
+ </div>
16
+ </div>
17
+ <div className="flex gap-4">
18
+ <button
19
+ onClick={() => onClearStorage('screenshots')}
20
+ className="flex-1 px-6 py-4 bg-red-500/5 border border-red-500/10 text-red-400 rounded-2xl text-[9px] font-bold uppercase tracking-widest hover:bg-red-500/10 transition-all"
21
+ >
22
+ Clear Screenshots
23
+ </button>
24
+ <button
25
+ onClick={() => onClearStorage('cookies')}
26
+ className="flex-1 px-6 py-4 bg-yellow-500/5 border border-yellow-500/10 text-yellow-400 rounded-2xl text-[9px] font-bold uppercase tracking-widest hover:bg-yellow-500/10 transition-all"
27
+ >
28
+ Reset Cookies
29
+ </button>
30
+ </div>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default StoragePanel;