opensidian 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.eslintrc.json +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +35 -0
  4. package/.github/ISSUE_TEMPLATE/question.md +23 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +45 -0
  6. package/.github/README.md +5 -0
  7. package/.github/workflows/cd.yml +44 -0
  8. package/.github/workflows/ci.yml +128 -0
  9. package/.github/workflows/qa.yml +45 -0
  10. package/.planning/PROJECT.md +96 -0
  11. package/.planning/REQUIREMENTS.md +66 -0
  12. package/.planning/ROADMAP.md +129 -0
  13. package/.planning/STATE.md +47 -0
  14. package/.planning/config.json +14 -0
  15. package/CONTRIBUTING.md +232 -0
  16. package/LICENSE +21 -0
  17. package/README.md +244 -0
  18. package/dist/api/auth.d.ts +5 -0
  19. package/dist/api/auth.d.ts.map +1 -0
  20. package/dist/api/auth.js +112 -0
  21. package/dist/api/auth.js.map +1 -0
  22. package/dist/api/routes.d.ts +3 -0
  23. package/dist/api/routes.d.ts.map +1 -0
  24. package/dist/api/routes.js +119 -0
  25. package/dist/api/routes.js.map +1 -0
  26. package/dist/api/themes.d.ts +3 -0
  27. package/dist/api/themes.d.ts.map +1 -0
  28. package/dist/api/themes.js +48 -0
  29. package/dist/api/themes.js.map +1 -0
  30. package/dist/core/graph.d.ts +16 -0
  31. package/dist/core/graph.d.ts.map +1 -0
  32. package/dist/core/graph.js +115 -0
  33. package/dist/core/graph.js.map +1 -0
  34. package/dist/core/markdown.d.ts +21 -0
  35. package/dist/core/markdown.d.ts.map +1 -0
  36. package/dist/core/markdown.js +77 -0
  37. package/dist/core/markdown.js.map +1 -0
  38. package/dist/core/search.d.ts +34 -0
  39. package/dist/core/search.d.ts.map +1 -0
  40. package/dist/core/search.js +159 -0
  41. package/dist/core/search.js.map +1 -0
  42. package/dist/core/sync.d.ts +30 -0
  43. package/dist/core/sync.d.ts.map +1 -0
  44. package/dist/core/sync.js +121 -0
  45. package/dist/core/sync.js.map +1 -0
  46. package/dist/core/vault.d.ts +28 -0
  47. package/dist/core/vault.d.ts.map +1 -0
  48. package/dist/core/vault.js +235 -0
  49. package/dist/core/vault.js.map +1 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +32 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp/cli.d.ts +3 -0
  55. package/dist/mcp/cli.d.ts.map +1 -0
  56. package/dist/mcp/cli.js +7 -0
  57. package/dist/mcp/cli.js.map +1 -0
  58. package/dist/mcp/server.d.ts +18 -0
  59. package/dist/mcp/server.d.ts.map +1 -0
  60. package/dist/mcp/server.js +272 -0
  61. package/dist/mcp/server.js.map +1 -0
  62. package/dist/plugins/host.d.ts +23 -0
  63. package/dist/plugins/host.d.ts.map +1 -0
  64. package/dist/plugins/host.js +104 -0
  65. package/dist/plugins/host.js.map +1 -0
  66. package/dist/plugins/sample-plugin.d.ts +10 -0
  67. package/dist/plugins/sample-plugin.d.ts.map +1 -0
  68. package/dist/plugins/sample-plugin.js +23 -0
  69. package/dist/plugins/sample-plugin.js.map +1 -0
  70. package/dist/server.d.ts +15 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +77 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/shared/types.d.ts +86 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/shared/types.js +2 -0
  77. package/dist/shared/types.js.map +1 -0
  78. package/docker/Dockerfile +37 -0
  79. package/docker/docker-compose.yml +46 -0
  80. package/docs/ARCHITECTURE.md +321 -0
  81. package/futuras_implementacoes.md +0 -0
  82. package/package.json +65 -0
  83. package/scripts/fix-gitignore.ps1 +5 -0
  84. package/scripts/seed-notes.mjs +60 -0
  85. package/src/api/auth.ts +130 -0
  86. package/src/api/routes.ts +133 -0
  87. package/src/api/themes.ts +60 -0
  88. package/src/core/graph.ts +145 -0
  89. package/src/core/markdown.ts +92 -0
  90. package/src/core/search.ts +208 -0
  91. package/src/core/sync.ts +157 -0
  92. package/src/core/vault.ts +286 -0
  93. package/src/index.ts +37 -0
  94. package/src/mcp/cli.ts +7 -0
  95. package/src/mcp/server.ts +296 -0
  96. package/src/plugins/host.ts +120 -0
  97. package/src/plugins/sample-plugin.ts +29 -0
  98. package/src/server.ts +90 -0
  99. package/src/shared/types.ts +92 -0
  100. package/tests/api/routes.test.ts +167 -0
  101. package/tests/core/graph.test.ts +236 -0
  102. package/tests/core/markdown.test.ts +157 -0
  103. package/tests/core/search.test.ts +132 -0
  104. package/tests/core/sync.test.ts +62 -0
  105. package/tests/core/vault.test.ts +162 -0
  106. package/tests/mcp/server.test.ts +118 -0
  107. package/tests/plugins/host.test.ts +165 -0
  108. package/tests/plugins/sample-plugin.test.ts +35 -0
  109. package/tests/server.test.ts +76 -0
  110. package/tsconfig.json +27 -0
  111. package/vite.config.ts +27 -0
  112. package/vitest.config.ts +33 -0
  113. package/web/index.html +13 -0
  114. package/web/package.json +26 -0
  115. package/web/public/favicon.svg +4 -0
  116. package/web/src/App.tsx +63 -0
  117. package/web/src/api/auth.ts +65 -0
  118. package/web/src/api/client.ts +117 -0
  119. package/web/src/api/themes.ts +78 -0
  120. package/web/src/components/GraphView.tsx +139 -0
  121. package/web/src/components/Layout.tsx +74 -0
  122. package/web/src/components/LoginPage.tsx +52 -0
  123. package/web/src/components/NoteEditor.tsx +114 -0
  124. package/web/src/components/NoteList.tsx +95 -0
  125. package/web/src/components/RegisterPage.tsx +58 -0
  126. package/web/src/components/SearchBar.tsx +71 -0
  127. package/web/src/components/SearchPanel.tsx +152 -0
  128. package/web/src/components/ThemeEditor.tsx +129 -0
  129. package/web/src/components/ThemeSelector.tsx +41 -0
  130. package/web/src/components/VaultList.tsx +89 -0
  131. package/web/src/hooks/AuthContext.tsx +57 -0
  132. package/web/src/hooks/ThemeContext.tsx +77 -0
  133. package/web/src/hooks/useWebSocket.ts +34 -0
  134. package/web/src/main.tsx +10 -0
  135. package/web/src/styles/global.css +449 -0
  136. package/web/tsconfig.json +21 -0
  137. package/web/vite.config.ts +19 -0
@@ -0,0 +1,89 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api, Vault } from '../api/client';
4
+
5
+ interface VaultListProps {
6
+ onSelect: (path: string) => void;
7
+ }
8
+
9
+ export default function VaultList({ onSelect }: VaultListProps) {
10
+ const [vaults, setVaults] = useState<Vault[]>([]);
11
+ const [loading, setLoading] = useState(true);
12
+ const [showCreate, setShowCreate] = useState(false);
13
+ const [newName, setNewName] = useState('');
14
+ const [newPath, setNewPath] = useState('');
15
+ const navigate = useNavigate();
16
+
17
+ const load = async () => {
18
+ try {
19
+ const res = await api.vaults.list();
20
+ setVaults(res.vaults);
21
+ } catch (err) {
22
+ console.error('Failed to load vaults:', err);
23
+ } finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+
28
+ useEffect(() => { load(); }, []);
29
+
30
+ const handleCreate = async () => {
31
+ if (!newName || !newPath) return;
32
+ try {
33
+ await api.vaults.create(newName, newPath);
34
+ setShowCreate(false);
35
+ setNewName('');
36
+ setNewPath('');
37
+ await load();
38
+ } catch (err) {
39
+ console.error('Failed to create vault:', err);
40
+ }
41
+ };
42
+
43
+ const selectVault = (vault: Vault) => {
44
+ onSelect(vault.path);
45
+ navigate('/notes');
46
+ };
47
+
48
+ if (loading) return <div className="content-area"><p>Carregando...</p></div>;
49
+
50
+ return (
51
+ <div className="content-area">
52
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
53
+ <h2>Vaults</h2>
54
+ <button className="primary" onClick={() => setShowCreate(true)}>+ Novo Vault</button>
55
+ </div>
56
+
57
+ {vaults.length === 0 ? (
58
+ <div className="empty-state">
59
+ <h2>Nenhum vault ainda</h2>
60
+ <p>Crie um vault para começar a escrever notas</p>
61
+ </div>
62
+ ) : (
63
+ <div className="vault-grid">
64
+ {vaults.map(vault => (
65
+ <div key={vault.path} className="vault-card" onClick={() => selectVault(vault)}>
66
+ <h3>{vault.name}</h3>
67
+ <p>{vault.notes.length} notas</p>
68
+ <p>{new Date(vault.created).toLocaleDateString()}</p>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ )}
73
+
74
+ {showCreate && (
75
+ <div className="modal-overlay" onClick={() => setShowCreate(false)}>
76
+ <div className="modal" onClick={e => e.stopPropagation()}>
77
+ <h2>Novo Vault</h2>
78
+ <input placeholder="Nome" value={newName} onChange={e => setNewName(e.target.value)} style={{ width: '100%', marginBottom: 8 }} />
79
+ <input placeholder="Caminho (ex: ./vaults/meu-vault)" value={newPath} onChange={e => setNewPath(e.target.value)} style={{ width: '100%' }} />
80
+ <div className="modal-actions">
81
+ <button onClick={() => setShowCreate(false)}>Cancelar</button>
82
+ <button className="primary" onClick={handleCreate}>Criar</button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,57 @@
1
+ import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
2
+ import { authApi, AuthUser, getToken, setToken, removeToken } from '../api/auth';
3
+
4
+ interface AuthContextType {
5
+ user: AuthUser | null;
6
+ loading: boolean;
7
+ login: (email: string, password: string) => Promise<void>;
8
+ register: (email: string, password: string, name: string) => Promise<void>;
9
+ logout: () => void;
10
+ }
11
+
12
+ const AuthContext = createContext<AuthContextType>(null!);
13
+
14
+ export function AuthProvider({ children }: { children: ReactNode }) {
15
+ const [user, setUser] = useState<AuthUser | null>(null);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ const token = getToken();
20
+ if (!token) {
21
+ setLoading(false);
22
+ return;
23
+ }
24
+ authApi.me()
25
+ .then(res => setUser(res.user))
26
+ .catch(() => removeToken())
27
+ .finally(() => setLoading(false));
28
+ }, []);
29
+
30
+ const login = useCallback(async (email: string, password: string) => {
31
+ const res = await authApi.login(email, password);
32
+ setToken(res.token);
33
+ setUser(res.user);
34
+ }, []);
35
+
36
+ const register = useCallback(async (email: string, password: string, name: string) => {
37
+ const res = await authApi.register(email, password, name);
38
+ setToken(res.token);
39
+ setUser(res.user);
40
+ }, []);
41
+
42
+ const logout = useCallback(() => {
43
+ authApi.logout().catch(() => {});
44
+ removeToken();
45
+ setUser(null);
46
+ }, []);
47
+
48
+ return (
49
+ <AuthContext.Provider value={{ user, loading, login, register, logout }}>
50
+ {children}
51
+ </AuthContext.Provider>
52
+ );
53
+ }
54
+
55
+ export function useAuth() {
56
+ return useContext(AuthContext);
57
+ }
@@ -0,0 +1,77 @@
1
+ import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
2
+ import { themesApi, BUILT_IN_THEMES, CustomTheme } from '../api/themes';
3
+
4
+ interface ThemeContextType {
5
+ themeName: string;
6
+ currentVariables: Record<string, string>;
7
+ customThemes: CustomTheme[];
8
+ selectTheme: (name: string) => void;
9
+ saveCustomTheme: (theme: CustomTheme) => Promise<void>;
10
+ deleteCustomTheme: (name: string) => Promise<void>;
11
+ }
12
+
13
+ const ThemeContext = createContext<ThemeContextType>(null!);
14
+
15
+ const STORAGE_KEY = 'opensidian_theme';
16
+
17
+ function getStoredTheme(): string {
18
+ return localStorage.getItem(STORAGE_KEY) || 'Claro';
19
+ }
20
+
21
+ function getAllThemes(custom: CustomTheme[]): CustomTheme[] {
22
+ return [...BUILT_IN_THEMES, ...custom];
23
+ }
24
+
25
+ export function ThemeProvider({ children }: { children: ReactNode }) {
26
+ const [themeName, setThemeName] = useState(getStoredTheme);
27
+ const [customThemes, setCustomThemes] = useState<CustomTheme[]>([]);
28
+
29
+ useEffect(() => {
30
+ themesApi.list().then(res => setCustomThemes(res.themes)).catch(() => {});
31
+ }, []);
32
+
33
+ const currentVariables = useCallback(() => {
34
+ const all = getAllThemes(customThemes);
35
+ const found = all.find(t => t.name === themeName);
36
+ return found?.variables || BUILT_IN_THEMES[0].variables;
37
+ }, [themeName, customThemes])();
38
+
39
+ useEffect(() => {
40
+ const root = document.documentElement;
41
+ for (const [key, val] of Object.entries(currentVariables)) {
42
+ root.style.setProperty(key, val as string);
43
+ }
44
+ localStorage.setItem(STORAGE_KEY, themeName);
45
+ }, [themeName, currentVariables]);
46
+
47
+ const selectTheme = useCallback((name: string) => {
48
+ setThemeName(name);
49
+ }, []);
50
+
51
+ const saveCustomTheme = useCallback(async (theme: CustomTheme) => {
52
+ await themesApi.save(theme);
53
+ setCustomThemes(prev => {
54
+ const filtered = prev.filter(t => t.name !== theme.name);
55
+ return [...filtered, theme];
56
+ });
57
+ setThemeName(theme.name);
58
+ }, []);
59
+
60
+ const deleteCustomTheme = useCallback(async (name: string) => {
61
+ await themesApi.delete(name);
62
+ setCustomThemes(prev => prev.filter(t => t.name !== name));
63
+ if (themeName === name) {
64
+ setThemeName('Claro');
65
+ }
66
+ }, [themeName]);
67
+
68
+ return (
69
+ <ThemeContext.Provider value={{ themeName, currentVariables, customThemes, selectTheme, saveCustomTheme, deleteCustomTheme }}>
70
+ {children}
71
+ </ThemeContext.Provider>
72
+ );
73
+ }
74
+
75
+ export function useTheme() {
76
+ return useContext(ThemeContext);
77
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
+
3
+ export function useWebSocket(vaultPath: string) {
4
+ const ws = useRef<WebSocket | null>(null);
5
+ const [connected, setConnected] = useState(false);
6
+
7
+ useEffect(() => {
8
+ if (!vaultPath) return;
9
+ const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
10
+ const url = `${proto}//${window.location.hostname}:3001`;
11
+ const socket = new WebSocket(url);
12
+ ws.current = socket;
13
+
14
+ socket.onopen = () => {
15
+ setConnected(true);
16
+ socket.send(JSON.stringify({ type: 'sync_request', vaultPath }));
17
+ };
18
+
19
+ socket.onclose = () => setConnected(false);
20
+ socket.onerror = () => setConnected(false);
21
+
22
+ return () => {
23
+ socket.close();
24
+ };
25
+ }, [vaultPath]);
26
+
27
+ const send = useCallback((data: unknown) => {
28
+ if (ws.current?.readyState === WebSocket.OPEN) {
29
+ ws.current.send(JSON.stringify(data));
30
+ }
31
+ }, []);
32
+
33
+ return { connected, send };
34
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles/global.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,449 @@
1
+ :root,
2
+ [data-theme='light'] {
3
+ --bg: #ffffff;
4
+ --bg-secondary: #f5f5f5;
5
+ --bg-tertiary: #ebebeb;
6
+ --text: #1a1a1a;
7
+ --text-secondary: #666;
8
+ --border: #ddd;
9
+ --accent: #6c31e0;
10
+ --accent-hover: #5a28c0;
11
+ --danger: #e03e3e;
12
+ --success: #2e7d32;
13
+ --shadow: 0 1px 3px rgba(0,0,0,0.08);
14
+ --radius: 6px;
15
+ }
16
+
17
+ [data-theme='dark'] {
18
+ --bg: #1a1a2e;
19
+ --bg-secondary: #16213e;
20
+ --bg-tertiary: #0f3460;
21
+ --text: #e0e0e0;
22
+ --text-secondary: #999;
23
+ --border: #333;
24
+ --accent: #7c4dff;
25
+ --accent-hover: #651fff;
26
+ --danger: #cf6679;
27
+ --success: #4caf50;
28
+ --shadow: 0 1px 3px rgba(0,0,0,0.3);
29
+ }
30
+
31
+ * { margin: 0; padding: 0; box-sizing: border-box; }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ line-height: 1.6;
38
+ }
39
+
40
+ a { color: var(--accent); text-decoration: none; }
41
+ a:hover { text-decoration: underline; }
42
+
43
+ button {
44
+ cursor: pointer;
45
+ border: 1px solid var(--border);
46
+ border-radius: var(--radius);
47
+ padding: 6px 14px;
48
+ background: var(--bg);
49
+ color: var(--text);
50
+ font-size: 14px;
51
+ }
52
+ button:hover { background: var(--bg-secondary); }
53
+ button.primary {
54
+ background: var(--accent);
55
+ color: #fff;
56
+ border-color: var(--accent);
57
+ }
58
+ button.primary:hover { background: var(--accent-hover); }
59
+
60
+ input, textarea {
61
+ border: 1px solid var(--border);
62
+ border-radius: var(--radius);
63
+ padding: 8px 12px;
64
+ background: var(--bg);
65
+ color: var(--text);
66
+ font-size: 14px;
67
+ outline: none;
68
+ }
69
+ input:focus, textarea:focus { border-color: var(--accent); }
70
+
71
+ .layout {
72
+ display: flex;
73
+ height: 100vh;
74
+ }
75
+
76
+ .sidebar {
77
+ width: 260px;
78
+ min-width: 260px;
79
+ background: var(--bg-secondary);
80
+ border-right: 1px solid var(--border);
81
+ display: flex;
82
+ flex-direction: column;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .sidebar-header {
87
+ padding: 16px;
88
+ border-bottom: 1px solid var(--border);
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ }
93
+
94
+ .sidebar-header h1 {
95
+ font-size: 16px;
96
+ font-weight: 600;
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 8px;
100
+ }
101
+
102
+ .sidebar-nav {
103
+ flex: 1;
104
+ overflow-y: auto;
105
+ padding: 8px;
106
+ }
107
+
108
+ .nav-item {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 8px;
112
+ padding: 8px 12px;
113
+ border-radius: var(--radius);
114
+ color: var(--text);
115
+ font-size: 14px;
116
+ cursor: pointer;
117
+ transition: background 0.15s;
118
+ }
119
+ .nav-item:hover { background: var(--bg-tertiary); text-decoration: none; }
120
+ .nav-item.active { background: var(--accent); color: #fff; }
121
+
122
+ .main-content {
123
+ flex: 1;
124
+ display: flex;
125
+ flex-direction: column;
126
+ overflow: hidden;
127
+ }
128
+
129
+ .toolbar {
130
+ padding: 12px 20px;
131
+ border-bottom: 1px solid var(--border);
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 12px;
135
+ background: var(--bg);
136
+ }
137
+
138
+ .toolbar input {
139
+ flex: 1;
140
+ max-width: 400px;
141
+ }
142
+
143
+ .content-area {
144
+ flex: 1;
145
+ overflow-y: auto;
146
+ padding: 20px;
147
+ }
148
+
149
+ /* Vault Cards */
150
+ .vault-grid {
151
+ display: grid;
152
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
153
+ gap: 16px;
154
+ max-width: 900px;
155
+ }
156
+
157
+ .vault-card {
158
+ background: var(--bg-secondary);
159
+ border: 1px solid var(--border);
160
+ border-radius: var(--radius);
161
+ padding: 20px;
162
+ cursor: pointer;
163
+ transition: border-color 0.15s;
164
+ }
165
+ .vault-card:hover { border-color: var(--accent); }
166
+ .vault-card h3 { font-size: 16px; margin-bottom: 4px; }
167
+ .vault-card p { font-size: 13px; color: var(--text-secondary); }
168
+
169
+ /* Note List */
170
+ .note-list { max-width: 700px; }
171
+ .note-item {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: space-between;
175
+ padding: 12px 16px;
176
+ border-bottom: 1px solid var(--border);
177
+ cursor: pointer;
178
+ transition: background 0.1s;
179
+ }
180
+ .note-item:hover { background: var(--bg-secondary); text-decoration: none; }
181
+ .note-item .note-title { font-weight: 500; }
182
+ .note-item .note-date { font-size: 12px; color: var(--text-secondary); }
183
+
184
+ /* Editor */
185
+ .editor-layout {
186
+ display: flex;
187
+ flex: 1;
188
+ height: 100%;
189
+ gap: 1px;
190
+ background: var(--border);
191
+ }
192
+
193
+ .editor-pane, .preview-pane {
194
+ flex: 1;
195
+ overflow-y: auto;
196
+ background: var(--bg);
197
+ }
198
+
199
+ .editor-pane textarea {
200
+ width: 100%;
201
+ height: 100%;
202
+ border: none;
203
+ border-radius: 0;
204
+ resize: none;
205
+ padding: 20px;
206
+ font-family: 'SF Mono', 'Fira Code', monospace;
207
+ font-size: 14px;
208
+ line-height: 1.7;
209
+ background: var(--bg);
210
+ color: var(--text);
211
+ }
212
+
213
+ .preview-pane {
214
+ padding: 20px;
215
+ line-height: 1.7;
216
+ }
217
+
218
+ .preview-pane h1 { font-size: 24px; margin: 16px 0 8px; }
219
+ .preview-pane h2 { font-size: 20px; margin: 14px 0 6px; }
220
+ .preview-pane h3 { font-size: 17px; margin: 12px 0 4px; }
221
+ .preview-pane p { margin: 8px 0; }
222
+ .preview-pane code {
223
+ background: var(--bg-tertiary);
224
+ padding: 2px 6px;
225
+ border-radius: 4px;
226
+ font-size: 13px;
227
+ }
228
+ .preview-pane pre code {
229
+ display: block;
230
+ padding: 12px;
231
+ overflow-x: auto;
232
+ }
233
+
234
+ /* Graph */
235
+ .graph-container {
236
+ width: 100%;
237
+ height: calc(100vh - 120px);
238
+ }
239
+
240
+ /* Empty state */
241
+ .empty-state {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ justify-content: center;
246
+ height: 60vh;
247
+ color: var(--text-secondary);
248
+ gap: 12px;
249
+ }
250
+
251
+ .empty-state h2 { font-size: 20px; }
252
+ .empty-state p { font-size: 14px; }
253
+
254
+ /* Modal */
255
+ .modal-overlay {
256
+ position: fixed;
257
+ inset: 0;
258
+ background: rgba(0,0,0,0.4);
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ z-index: 100;
263
+ }
264
+
265
+ .modal {
266
+ background: var(--bg);
267
+ border-radius: 8px;
268
+ padding: 24px;
269
+ min-width: 360px;
270
+ box-shadow: 0 4px 20px rgba(0,0,0,0.2);
271
+ }
272
+
273
+ .modal h2 { margin-bottom: 16px; font-size: 18px; }
274
+
275
+ .modal-actions {
276
+ display: flex;
277
+ gap: 8px;
278
+ justify-content: flex-end;
279
+ margin-top: 16px;
280
+ }
281
+
282
+ /* Search results */
283
+ .search-results {
284
+ position: absolute;
285
+ top: 100%;
286
+ left: 0;
287
+ right: 0;
288
+ background: var(--bg);
289
+ border: 1px solid var(--border);
290
+ border-radius: var(--radius);
291
+ box-shadow: var(--shadow);
292
+ max-height: 300px;
293
+ overflow-y: auto;
294
+ z-index: 50;
295
+ }
296
+
297
+ .search-result-item {
298
+ padding: 10px 14px;
299
+ cursor: pointer;
300
+ border-bottom: 1px solid var(--border);
301
+ }
302
+ .search-result-item:hover { background: var(--bg-secondary); }
303
+ .search-result-item .result-title { font-weight: 500; }
304
+ .search-result-item .result-excerpt { font-size: 12px; color: var(--text-secondary); }
305
+
306
+ /* Auth Pages */
307
+ .auth-page {
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ height: 100vh;
312
+ background: var(--bg-secondary);
313
+ }
314
+
315
+ .auth-card {
316
+ background: var(--bg);
317
+ border: 1px solid var(--border);
318
+ border-radius: 8px;
319
+ padding: 32px;
320
+ width: 100%;
321
+ max-width: 380px;
322
+ box-shadow: var(--shadow);
323
+ }
324
+
325
+ .auth-card h2 {
326
+ font-size: 20px;
327
+ margin-bottom: 20px;
328
+ text-align: center;
329
+ }
330
+
331
+ .auth-card form {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 12px;
335
+ }
336
+
337
+ .auth-card input {
338
+ width: 100%;
339
+ }
340
+
341
+ .auth-logo {
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ gap: 8px;
346
+ margin-bottom: 24px;
347
+ }
348
+
349
+ .auth-logo h1 {
350
+ font-size: 22px;
351
+ font-weight: 700;
352
+ }
353
+
354
+ .auth-error {
355
+ color: var(--danger);
356
+ font-size: 13px;
357
+ text-align: center;
358
+ }
359
+
360
+ .auth-link {
361
+ text-align: center;
362
+ font-size: 13px;
363
+ margin-top: 16px;
364
+ color: var(--text-secondary);
365
+ }
366
+
367
+ /* Theme cards */
368
+ .theme-card {
369
+ border: 2px solid var(--border);
370
+ border-radius: 8px;
371
+ padding: 16px;
372
+ cursor: pointer;
373
+ transition: border-color 0.15s;
374
+ min-width: 160px;
375
+ }
376
+ .theme-card:hover { border-color: var(--accent); }
377
+ .theme-card--active { border-color: var(--accent); background: var(--bg-secondary); }
378
+
379
+ .theme-card-preview {
380
+ display: flex;
381
+ gap: 6px;
382
+ align-items: center;
383
+ margin-bottom: 12px;
384
+ }
385
+
386
+ .theme-card-name {
387
+ font-weight: 500;
388
+ font-size: 14px;
389
+ margin-bottom: 8px;
390
+ }
391
+
392
+ /* Theme editor */
393
+ .theme-editor {
394
+ background: var(--bg-secondary);
395
+ border: 1px solid var(--border);
396
+ border-radius: 8px;
397
+ padding: 24px;
398
+ max-width: 600px;
399
+ }
400
+ .theme-editor h3 { margin-bottom: 16px; font-size: 16px; }
401
+
402
+ .theme-editor-grid {
403
+ display: grid;
404
+ grid-template-columns: 1fr 1fr;
405
+ gap: 12px;
406
+ }
407
+
408
+ .theme-editor-field label {
409
+ display: block;
410
+ font-size: 12px;
411
+ color: var(--text-secondary);
412
+ margin-bottom: 4px;
413
+ }
414
+
415
+ /* Search results full page */
416
+ .search-results-list {
417
+ display: flex;
418
+ flex-direction: column;
419
+ gap: 8px;
420
+ }
421
+
422
+ .search-result-full {
423
+ border: 1px solid var(--border);
424
+ border-radius: var(--radius);
425
+ padding: 14px 16px;
426
+ cursor: pointer;
427
+ transition: border-color 0.15s;
428
+ }
429
+ .search-result-full:hover { border-color: var(--accent); }
430
+ .search-result-full .result-title {
431
+ font-weight: 600;
432
+ font-size: 15px;
433
+ margin-bottom: 4px;
434
+ }
435
+ .search-result-full .result-excerpt {
436
+ font-size: 13px;
437
+ color: var(--text-secondary);
438
+ line-height: 1.5;
439
+ }
440
+ .search-result-full .result-excerpt mark {
441
+ background: rgba(108, 49, 224, 0.2);
442
+ color: var(--text);
443
+ padding: 0 2px;
444
+ border-radius: 2px;
445
+ }
446
+
447
+ details summary {
448
+ font-weight: 500;
449
+ }