total-recall-brain 3.28.2 → 3.30.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.
Files changed (132) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  43. package/src/cli/agent.mjs +1 -1
  44. package/src/cli/app/index.mjs +493 -0
  45. package/src/cli/brain.mjs +33 -3
  46. package/src/cli/command.mjs +80 -41
  47. package/src/cli/harness.mjs +1 -1
  48. package/src/cli/init.mjs +3 -1
  49. package/src/cli/mesh.mjs +118 -12
  50. package/src/cli/plugin/create.mjs +144 -25
  51. package/src/cli/plugin/index.mjs +83 -53
  52. package/src/cli/plugin/install.mjs +10 -127
  53. package/src/cli/plugin/list.mjs +44 -44
  54. package/src/cli/plugin/remove.mjs +23 -30
  55. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  56. package/src/cli/plugin/search.mjs +108 -23
  57. package/src/cli/rebuild.mjs +37 -1
  58. package/src/cli/recall.mjs +17 -2
  59. package/src/cli/research.mjs +4 -6
  60. package/src/cli/share.mjs +2 -0
  61. package/src/core/app-deploy/apply.mjs +386 -0
  62. package/src/core/app-deploy/design-tokens.mjs +217 -0
  63. package/src/core/app-deploy/plan.mjs +249 -0
  64. package/src/core/app-deploy/resolve.mjs +295 -0
  65. package/src/core/app-deploy/source.mjs +337 -0
  66. package/src/core/app-deploy/standalone.mjs +248 -0
  67. package/src/core/app-deploy/upgrade.mjs +256 -0
  68. package/src/core/app-deploy/verify.mjs +213 -0
  69. package/src/core/bound-hosts.mjs +1 -1
  70. package/src/core/brain-registry.mjs +146 -0
  71. package/src/core/brain-state.json +1 -0
  72. package/src/core/daemon-loop.mjs +30 -26
  73. package/src/core/dream.mjs +5 -17
  74. package/src/core/fact-seeker.mjs +53 -368
  75. package/src/core/lan-discovery.mjs +3 -3
  76. package/src/core/mesh-access-sync.mjs +134 -0
  77. package/src/core/mesh-access.mjs +18 -3
  78. package/src/core/mesh-discover.mjs +173 -0
  79. package/src/core/mesh.mjs +131 -32
  80. package/src/core/optimizer.mjs +4 -41
  81. package/src/core/plugin-bundle.mjs +167 -0
  82. package/src/core/plugin-context.mjs +8 -13
  83. package/src/core/plugin-loader.mjs +272 -68
  84. package/src/core/plugin-peers.mjs +163 -0
  85. package/src/core/plugin-public.mjs +85 -0
  86. package/src/core/plugin-runner-child.mjs +24 -0
  87. package/src/core/plugin-runner.mjs +102 -0
  88. package/src/core/plugin-store.mjs +384 -0
  89. package/src/core/plugin-tasks.mjs +212 -0
  90. package/src/core/post-mortem.mjs +40 -9
  91. package/src/core/research-gate.mjs +182 -0
  92. package/src/core/research-queue.mjs +47 -7
  93. package/src/core/research-surface.mjs +211 -0
  94. package/src/core/research.mjs +25 -32
  95. package/src/core/scheduler.mjs +67 -153
  96. package/src/core/schema.mjs +129 -0
  97. package/src/core/search.mjs +12 -1
  98. package/src/core/secret-integration-research.mjs +12 -8
  99. package/src/core/secrets-store.mjs +32 -2
  100. package/src/core/session-watcher.mjs +6 -0
  101. package/src/core/surface.mjs +64 -3
  102. package/src/core/task-executors.mjs +8 -45
  103. package/src/core/vault-cache.mjs +70 -4
  104. package/src/server/api.mjs +21 -0
  105. package/src/server/rest.mjs +2 -0
  106. package/src/server/route-manifest.json +21 -5
  107. package/src/server/routes/collab.mjs +38 -11
  108. package/src/server/routes/extension.mjs +3 -2
  109. package/src/server/routes/memory.mjs +24 -2
  110. package/src/server/routes/mesh.mjs +7 -7
  111. package/src/server/routes/plugins-mesh.mjs +49 -0
  112. package/src/server/routes/plugins.mjs +91 -426
  113. package/src/server/routes/research.mjs +5 -2
  114. package/src/server/routes/share.mjs +2 -0
  115. package/src/server/tools.mjs +29 -2
  116. package/extension/icons/README.md +0 -27
  117. package/extension/lib/preconfigured.js +0 -5
  118. package/extension/popup/popup.css +0 -227
  119. package/extension/popup/popup.html +0 -50
  120. package/extension/popup/popup.js +0 -175
  121. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  122. package/scaffold/.agent/plugins/.gitkeep +0 -0
  123. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  124. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  125. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  126. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  127. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  129. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  131. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  132. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" fill="none">
2
+ <!-- Transparent vault arch mark — dark UI friendly -->
3
+ <path d="M 120 300 L 120 180 A 80 80 0 0 1 280 180 L 280 300 Z" fill="#93c5fd"/>
4
+ <path d="M 280 300 L 280 180 A 80 80 0 0 0 200 100 L 350 250 L 350 300 Z" fill="#1e40af" opacity="0.55"/>
5
+ <path d="M 150 300 L 150 180 A 50 50 0 0 1 250 180 L 250 300 Z" fill="#0b1220"/>
6
+ </svg>
@@ -1,99 +1,186 @@
1
- // Brain API Client for Total Recall Chrome Extension
2
- // Shared by background.js, popup, sidepanel, and options page.
3
- // Exposed as `self.BrainClient` namespace (no ES-module bundler required).
1
+ // Total Recall — Brain API client
2
+ // Shared by the service worker, side panel and options page (classic scripts,
3
+ // no bundler), exposed as `self.BrainClient`.
4
+ //
5
+ // Every request carries `x-total-recall-brain` so search, capture, listing and
6
+ // chat all act on the brain the user selected, not silently on the global one.
4
7
 
5
8
  (function () {
9
+ 'use strict';
10
+
6
11
  const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
12
+ const DEFAULT_TIMEOUT_MS = 20000;
13
+ const CHAT_TIMEOUT_MS = 300000; // agent-routed answers with tool use can take minutes
14
+
15
+ class BrainError extends Error {
16
+ /** @param {'offline'|'unauthorized'|'forbidden'|'http'|'timeout'|'unconfigured'} code */
17
+ constructor(code, message, status) {
18
+ super(message);
19
+ this.name = 'BrainError';
20
+ this.code = code;
21
+ this.status = status || 0;
22
+ }
23
+ }
24
+
25
+ function normalizeUrl(raw) {
26
+ const url = String(raw || '').trim().replace(/\/+$/, '');
27
+ return url || DEFAULT_BRAIN_URL;
28
+ }
7
29
 
8
30
  async function getConfig() {
9
- const [localConfig, syncConfig] = await Promise.all([
31
+ const [local, sync] = await Promise.all([
10
32
  chrome.storage.local.get(['brainUrl', 'pat']),
11
- chrome.storage.sync.get(['brainUrl', 'pat'])
33
+ chrome.storage.sync.get(['brainUrl', 'pat', 'activeBrainId']),
12
34
  ]);
13
- const pre = self.PreConfigured || {};
14
- const config = {
15
- brainUrl: localConfig.brainUrl || syncConfig.brainUrl,
16
- pat: localConfig.pat || syncConfig.pat
17
- };
18
35
 
19
- if (syncConfig.pat && !localConfig.pat) {
20
- await chrome.storage.local.set({ pat: syncConfig.pat });
21
- await chrome.storage.sync.remove('pat');
36
+ // PATs belong in extension-local storage only; migrate any legacy synced copy.
37
+ if (sync.pat && !local.pat) {
38
+ await chrome.storage.local.set({ pat: sync.pat });
39
+ local.pat = sync.pat;
22
40
  }
41
+ if (sync.pat) await chrome.storage.sync.remove('pat');
23
42
 
24
43
  return {
25
- brainUrl: config.brainUrl || pre.brainUrl || DEFAULT_BRAIN_URL,
26
- pat: config.pat || pre.pat || ''
44
+ brainUrl: normalizeUrl(local.brainUrl || sync.brainUrl),
45
+ pat: local.pat || '',
46
+ activeBrainId: sync.activeBrainId || 'global',
27
47
  };
28
48
  }
29
49
 
30
- async function brainFetch(path, options = {}) {
31
- const { brainUrl, pat } = await getConfig();
32
- const url = `${brainUrl}${path}`;
33
- const headers = { 'Content-Type': 'application/json', ...options.headers };
34
- if (pat) headers['Authorization'] = `Bearer ${pat}`;
35
-
36
- const res = await fetch(url, { ...options, headers });
37
- if (res.status === 401) {
50
+ function setAuthBadge(broken) {
51
+ if (!chrome.action) return;
52
+ if (broken) {
53
+ chrome.action.setBadgeBackgroundColor({ color: '#f87171' });
38
54
  chrome.action.setBadgeText({ text: '!' });
39
- chrome.action.setBadgeBackgroundColor({ color: '#ef4444' });
40
- throw new Error('Authentication failed — check your PAT token');
55
+ chrome.action.setTitle({ title: 'Total Recall: sign-in needed. Check your access token in Settings.' });
56
+ } else {
57
+ chrome.action.getBadgeText({}).then((text) => {
58
+ if (text === '!') chrome.action.setBadgeText({ text: '' });
59
+ }).catch(() => {});
60
+ chrome.action.setTitle({ title: 'Open Total Recall' });
41
61
  }
42
- if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
43
- return res.json();
44
62
  }
45
63
 
46
- async function share(data) {
47
- return brainFetch('/api/share', { method: 'POST', body: JSON.stringify(data) });
48
- }
64
+ /**
65
+ * @param {string} path
66
+ * @param {{method?: string, body?: any, timeoutMs?: number, brainScoped?: boolean, config?: object}} [opts]
67
+ */
68
+ async function request(path, opts = {}) {
69
+ const config = opts.config || await getConfig();
70
+ const headers = { Accept: 'application/json' };
71
+ if (opts.body !== undefined) headers['Content-Type'] = 'application/json';
72
+ if (config.pat) headers.Authorization = `Bearer ${config.pat}`;
73
+ if (opts.brainScoped !== false && config.activeBrainId && config.activeBrainId !== 'global') {
74
+ headers['x-total-recall-brain'] = config.activeBrainId;
75
+ }
49
76
 
50
- async function search(query, topK = 3) {
51
- return brainFetch('/api/memory/search/semantic', {
52
- method: 'POST',
53
- body: JSON.stringify({ query, top_k: topK })
54
- });
55
- }
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs || DEFAULT_TIMEOUT_MS);
79
+ let res;
80
+ try {
81
+ res = await fetch(`${config.brainUrl}${path}`, {
82
+ method: opts.method || (opts.body !== undefined ? 'POST' : 'GET'),
83
+ headers,
84
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
85
+ signal: controller.signal,
86
+ });
87
+ } catch (err) {
88
+ if (err && err.name === 'AbortError') throw new BrainError('timeout', 'The brain took too long to answer.');
89
+ throw new BrainError('offline', `Can't reach the brain at ${config.brainUrl}.`);
90
+ } finally {
91
+ clearTimeout(timer);
92
+ }
56
93
 
57
- async function healthCheck() {
58
- return brainFetch('/health');
59
- }
94
+ if (res.status === 401) {
95
+ setAuthBadge(true);
96
+ throw new BrainError(config.pat ? 'unauthorized' : 'unconfigured',
97
+ config.pat ? 'The brain rejected your access token.' : 'Add an access token in Settings to connect.', 401);
98
+ }
99
+ if (res.status === 403) {
100
+ throw new BrainError('forbidden', 'Your access token lacks the scope for this action.', 403);
101
+ }
60
102
 
61
- async function chat(messages, options = {}) {
62
- const { brainUrl, pat } = await getConfig();
63
- const url = `${brainUrl}/v1/chat/completions`;
64
- const headers = { 'Content-Type': 'application/json' };
65
- if (pat) headers['Authorization'] = `Bearer ${pat}`;
66
- if (options.sessionId) headers['x-session-id'] = options.sessionId;
67
-
68
- const res = await fetch(url, {
69
- method: 'POST',
70
- headers,
71
- body: JSON.stringify({
72
- messages,
73
- model: options.model || 'gemini',
74
- brainId: options.brainId || 'global',
75
- groundingNodes: options.groundingNodes || []
76
- })
77
- });
78
- if (res.status === 401) throw new Error('Authentication failed — check your PAT token');
79
- if (!res.ok) throw new Error(`Brain API error: ${res.status}`);
80
- return res.json();
103
+ let data = null;
104
+ const text = await res.text();
105
+ if (text) {
106
+ try { data = JSON.parse(text); } catch { data = { raw: text }; }
107
+ }
108
+ if (!res.ok) {
109
+ const detail = (data && (data.error || data.message)) || `HTTP ${res.status}`;
110
+ throw new BrainError('http', detail, res.status);
111
+ }
112
+ setAuthBadge(false);
113
+ return data;
81
114
  }
82
115
 
83
- // Self-heal stale storage: if preconfigured values exist, write them to extension-local storage.
84
- try {
85
- if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
86
- chrome.storage.local.get(['brainUrl', 'pat']).then((config) => {
87
- const pre = self.PreConfigured || {};
88
- if (pre.pat && (!config.pat || config.pat !== pre.pat)) {
89
- chrome.storage.local.set({ brainUrl: pre.brainUrl, pat: pre.pat });
90
- }
116
+ // ---- Endpoints ----------------------------------------------------------
117
+
118
+ const api = {
119
+ health: (config) => request('/health', { config, brainScoped: false }),
120
+ extensionStatus: () => request('/api/extension/status', { brainScoped: false }),
121
+ listBrains: () => request('/api/brains', { brainScoped: false }),
122
+
123
+ async search(query, { topK = 8, includeSessions = false } = {}) {
124
+ try {
125
+ const data = await request('/api/memory/search/semantic', {
126
+ body: { query, top_k: topK, include_sessions: includeSessions },
127
+ });
128
+ return (data && data.results) || [];
129
+ } catch (err) {
130
+ // 503 = embeddings index not built yet: an empty result, not a failure.
131
+ if (err.code === 'http' && err.status === 503) return [];
132
+ throw err;
133
+ }
134
+ },
135
+
136
+ async listRecent(limit = 8) {
137
+ const data = await request(`/api/memory?sort=recent&limit=${limit}`);
138
+ const nodes = (data && data.nodes) || [];
139
+ // Older brains ignore sort=recent; order client-side as a fallback.
140
+ return nodes.slice().sort((a, b) => (Date.parse(b.created) || 0) - (Date.parse(a.created) || 0));
141
+ },
142
+
143
+ async findByUrl(url) {
144
+ const data = await request(`/api/memory?q=${encodeURIComponent(url)}&limit=5`);
145
+ return (data && data.nodes) || [];
146
+ },
147
+
148
+ share: (payload) => request('/api/share', { body: payload }),
149
+
150
+ chat(messages, { groundingNodes } = {}) {
151
+ // No `model` field: the brain routes with the user's runtime config. Naming a
152
+ // model here would override that routing for every extension request.
153
+ return request('/v1/chat/completions', {
154
+ body: { messages, groundingNodes: groundingNodes || [] },
155
+ timeoutMs: CHAT_TIMEOUT_MS,
156
+ }).then((data) => {
157
+ const choice = data && data.choices && data.choices[0];
158
+ return {
159
+ content: (choice && choice.message && choice.message.content) || '',
160
+ model: data && data.model,
161
+ };
91
162
  });
92
- }
93
- } catch (e) {
94
- // Ignore in contexts where storage sync isn't available
95
- }
163
+ },
164
+
165
+ async listResearch() {
166
+ // The server lists pending before in_progress; fetch enough to include every
167
+ // running item even behind a long queue.
168
+ const data = await request('/api/research?limit=500', { brainScoped: false });
169
+ return {
170
+ items: (data && (data.items || data.projects)) || [],
171
+ counts: (data && data.counts) || {},
172
+ };
173
+ },
174
+ queueResearch: (topic, notes) => request('/api/research', { body: { topic, notes: notes || '', via: 'extension' }, brainScoped: false }),
175
+ cancelResearch: (id) => request(`/api/research/${encodeURIComponent(id)}`, { method: 'DELETE', brainScoped: false }),
176
+
177
+ compile: () => request('/api/vault/compile', { method: 'POST', body: {}, timeoutMs: 120000 }),
178
+
179
+ async dashboardUrl(path = '/') {
180
+ const { brainUrl } = await getConfig();
181
+ return `${brainUrl}${path}`;
182
+ },
183
+ };
96
184
 
97
- // Expose as a global namespace for non-module contexts (service worker, popup, etc.)
98
- self.BrainClient = { getConfig, brainFetch, share, search, healthCheck, chat };
185
+ self.BrainClient = { DEFAULT_BRAIN_URL, BrainError, getConfig, normalizeUrl, request, ...api };
99
186
  })();
@@ -0,0 +1,187 @@
1
+ /* Total Recall — extension design tokens.
2
+ * Mirrors the dashboard design system (frontend/src/index.css :root) so the
3
+ * extension reads as the same product. Change them together. */
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+
8
+ --bg-primary: #070b14;
9
+ --bg-secondary: #0c1220;
10
+ --bg-tertiary: #121a2b;
11
+ --bg-elevated: #172033;
12
+ --bg-hover: #1c2740;
13
+ --bg-active: #243352;
14
+
15
+ --accent: #3b82f6;
16
+ --accent-deep: #1e3a8a;
17
+ --accent-glow: rgba(59, 130, 246, 0.28);
18
+ --accent-hover: #60a5fa;
19
+ --accent-muted: rgba(59, 130, 246, 0.14);
20
+ --accent-soft: rgba(147, 197, 253, 0.12);
21
+
22
+ --success: #34d399;
23
+ --success-muted: rgba(52, 211, 153, 0.12);
24
+ --warning: #fbbf24;
25
+ --warning-muted: rgba(251, 191, 36, 0.12);
26
+ --error: #f87171;
27
+ --error-muted: rgba(248, 113, 113, 0.12);
28
+
29
+ --text-primary: #f1f5f9;
30
+ --text-secondary: #94a3b8;
31
+ --text-tertiary: #64748b;
32
+ --text-inverse: #070b14;
33
+
34
+ --border: rgba(148, 163, 184, 0.1);
35
+ --border-hover: rgba(148, 163, 184, 0.22);
36
+ --border-accent: rgba(59, 130, 246, 0.35);
37
+
38
+ --glass: rgba(12, 18, 32, 0.72);
39
+ --glass-border: rgba(148, 163, 184, 0.12);
40
+ --mesh-1: rgba(30, 58, 138, 0.35);
41
+ --mesh-2: rgba(59, 130, 246, 0.12);
42
+
43
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ --font-mono: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
45
+
46
+ --radius-sm: 8px;
47
+ --radius-md: 12px;
48
+ --radius-lg: 18px;
49
+
50
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
51
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35);
52
+ --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.45);
53
+ --shadow-glow: 0 0 40px rgba(59, 130, 246, 0.18);
54
+
55
+ --transition-fast: 140ms cubic-bezier(0.23, 1, 0.32, 1);
56
+ --transition-normal: 220ms cubic-bezier(0.23, 1, 0.32, 1);
57
+ }
58
+
59
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
60
+
61
+ html, body {
62
+ background: var(--bg-primary);
63
+ color: var(--text-primary);
64
+ font-family: var(--font-sans);
65
+ font-size: 13px;
66
+ line-height: 1.5;
67
+ -webkit-font-smoothing: antialiased;
68
+ text-rendering: optimizeLegibility;
69
+ }
70
+
71
+ body {
72
+ background:
73
+ radial-gradient(520px 280px at 0% -8%, var(--mesh-1), transparent 60%),
74
+ radial-gradient(420px 260px at 110% 0%, var(--mesh-2), transparent 55%),
75
+ var(--bg-primary);
76
+ background-attachment: fixed;
77
+ }
78
+
79
+ a { color: var(--accent-hover); text-decoration: none; }
80
+ a:hover { color: var(--text-primary); }
81
+
82
+ button, input, textarea, select { font: inherit; color: inherit; }
83
+ button { cursor: pointer; background: none; border: 0; }
84
+ button:disabled { cursor: default; }
85
+
86
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
87
+
88
+ ::selection { background: var(--accent-muted); color: var(--text-primary); }
89
+
90
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
91
+ ::-webkit-scrollbar-track { background: transparent; }
92
+ ::-webkit-scrollbar-thumb { background: var(--bg-hover); border-radius: 3px; }
93
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
94
+
95
+ .icon { width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
96
+ .icon.sm { width: 14px; height: 14px; }
97
+ .icon.lg { width: 20px; height: 20px; }
98
+
99
+ /* Buttons */
100
+ .btn {
101
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
102
+ height: 32px; padding: 0 12px; border-radius: var(--radius-sm);
103
+ font-size: 12.5px; font-weight: 600; letter-spacing: -0.005em; white-space: nowrap;
104
+ background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border);
105
+ transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
106
+ }
107
+ .btn:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--border-hover); }
108
+ .btn:active:not(:disabled) { transform: translateY(1px); }
109
+ .btn:disabled { opacity: 0.5; }
110
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 6px 18px -8px var(--accent-glow); }
111
+ .btn.primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
112
+ .btn.ghost { background: transparent; border-color: transparent; color: var(--text-secondary); }
113
+ .btn.ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
114
+ .btn.danger { color: var(--error); }
115
+ .btn.danger:hover:not(:disabled) { background: var(--error-muted); border-color: rgba(248, 113, 113, 0.3); }
116
+ .btn.block { width: 100%; }
117
+ .btn.is-busy { position: relative; color: transparent !important; pointer-events: none; }
118
+ .btn.is-busy::after {
119
+ content: ''; position: absolute; width: 14px; height: 14px; border-radius: 50%;
120
+ border: 2px solid rgba(255, 255, 255, 0.35); border-top-color: #fff; animation: spin 0.7s linear infinite;
121
+ }
122
+ .btn:not(.primary).is-busy::after { border-color: var(--border-hover); border-top-color: var(--text-primary); }
123
+
124
+ .icon-btn {
125
+ display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm);
126
+ color: var(--text-secondary); transition: background var(--transition-fast), color var(--transition-fast);
127
+ }
128
+ .icon-btn:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
129
+
130
+ /* Fields */
131
+ .input, .textarea, .select {
132
+ width: 100%; background: var(--bg-secondary); color: var(--text-primary);
133
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
134
+ padding: 8px 10px; font-size: 13px; outline: none;
135
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);
136
+ }
137
+ .input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
138
+ .input:hover, .textarea:hover, .select:hover { border-color: var(--border-hover); }
139
+ .input:focus, .textarea:focus, .select:focus { border-color: var(--border-accent); box-shadow: 0 0 0 3px var(--accent-muted); background: var(--bg-tertiary); }
140
+ .textarea { resize: none; line-height: 1.5; }
141
+ .select { appearance: none; padding-right: 30px; cursor: pointer;
142
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
143
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 14px; }
144
+
145
+ /* Switch */
146
+ .switch { position: relative; display: inline-block; width: 34px; height: 20px; flex: none; }
147
+ .switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; z-index: 1; }
148
+ .switch .track { position: absolute; inset: 0; border-radius: 999px; background: var(--bg-active); border: 1px solid var(--border-hover); transition: background var(--transition-fast), border-color var(--transition-fast); }
149
+ .switch .track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text-secondary); transition: transform var(--transition-normal), background var(--transition-fast); }
150
+ .switch input:checked + .track { background: var(--accent); border-color: var(--accent); }
151
+ .switch input:checked + .track::after { transform: translateX(14px); background: #fff; }
152
+ .switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }
153
+ .switch input:disabled + .track { opacity: 0.45; }
154
+
155
+ /* Chips / badges */
156
+ .chip {
157
+ display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px; border-radius: 999px;
158
+ font-size: 11px; font-weight: 600; letter-spacing: 0.01em;
159
+ background: var(--bg-elevated); color: var(--text-secondary); border: 1px solid var(--border);
160
+ }
161
+ .chip.accent { background: var(--accent-muted); color: var(--accent-hover); border-color: var(--border-accent); }
162
+ .chip.success { background: var(--success-muted); color: var(--success); border-color: rgba(52, 211, 153, 0.25); }
163
+ .chip.warning { background: var(--warning-muted); color: var(--warning); border-color: rgba(251, 191, 36, 0.25); }
164
+ .chip.error { background: var(--error-muted); color: var(--error); border-color: rgba(248, 113, 113, 0.25); }
165
+
166
+ .kbd {
167
+ display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 5px;
168
+ font-family: var(--font-mono); font-size: 10.5px; color: var(--text-tertiary);
169
+ background: var(--bg-tertiary); border: 1px solid var(--border); box-shadow: inset 0 -1px 0 var(--border);
170
+ }
171
+
172
+ .skeleton {
173
+ border-radius: var(--radius-sm);
174
+ background: linear-gradient(90deg, var(--bg-tertiary) 0%, var(--bg-elevated) 50%, var(--bg-tertiary) 100%);
175
+ background-size: 200% 100%; animation: shimmer 1.4s ease-in-out infinite;
176
+ }
177
+
178
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
179
+
180
+ @keyframes spin { to { transform: rotate(360deg); } }
181
+ @keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
182
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
183
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
184
+
185
+ @media (prefers-reduced-motion: reduce) {
186
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
187
+ }