nx-kv 0.1.1

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 (48) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -0
  4. package/assets/nx-kv/SKILL.md +99 -0
  5. package/assets/nx-kv/references/agent-workflow.md +178 -0
  6. package/assets/nx-kv/references/todo-commands.md +193 -0
  7. package/bin/cli.mjs +10 -0
  8. package/package.json +57 -0
  9. package/src/core/config.js +94 -0
  10. package/src/core/errors.js +81 -0
  11. package/src/core/frontmatter.js +65 -0
  12. package/src/core/fstree.js +74 -0
  13. package/src/core/kvapi.js +201 -0
  14. package/src/core/open.js +26 -0
  15. package/src/core/paths.js +46 -0
  16. package/src/core/prompt.js +60 -0
  17. package/src/index.js +9 -0
  18. package/src/modules/auth/index.js +98 -0
  19. package/src/modules/auth/view.jsx +78 -0
  20. package/src/modules/bundled/index.js +51 -0
  21. package/src/modules/bundled/service.js +80 -0
  22. package/src/modules/group/index.js +199 -0
  23. package/src/modules/group/view.jsx +159 -0
  24. package/src/modules/system/index.js +153 -0
  25. package/src/modules/todo/index.js +280 -0
  26. package/src/modules/todo/service.js +524 -0
  27. package/src/modules/todo/view.jsx +280 -0
  28. package/src/runtime/api.js +108 -0
  29. package/src/runtime/cli.js +299 -0
  30. package/src/runtime/registry.js +56 -0
  31. package/src/runtime/server.js +74 -0
  32. package/src/runtime/spec.js +147 -0
  33. package/src/web/frontend/App.jsx +74 -0
  34. package/src/web/frontend/api/client.js +39 -0
  35. package/src/web/frontend/components/CliHints.jsx +25 -0
  36. package/src/web/frontend/components/ui.jsx +197 -0
  37. package/src/web/frontend/index.html +13 -0
  38. package/src/web/frontend/main.jsx +17 -0
  39. package/src/web/frontend/registry.js +12 -0
  40. package/src/web/frontend/store.jsx +74 -0
  41. package/src/web/frontend/style.css +368 -0
  42. package/src/web/public/assets/CliHints-3H3KOoDZ.js +1 -0
  43. package/src/web/public/assets/index-BnWex75m.css +1 -0
  44. package/src/web/public/assets/index-jhTtYCb9.js +10 -0
  45. package/src/web/public/assets/view-CrL-DSJu.js +1 -0
  46. package/src/web/public/assets/view-Du7Wv_-h.js +1 -0
  47. package/src/web/public/assets/view-S0N5g6v1.js +2 -0
  48. package/src/web/public/index.html +13 -0
@@ -0,0 +1,74 @@
1
+ // 全局面板状态:bootstrap 数据 + 会话级选择。
2
+ // 关键约定:凡是「刷新后不该丢」的用户选择(当前视图、中心/项目路径、多选勾选)
3
+ // 一律走 persist 读写 localStorage——UI 状态持久化是模板的硬标准,不是可选项。
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
5
+ import { api } from './api/client.js';
6
+
7
+ const LS_KEY = 'nx-kv-ui';
8
+
9
+ // 只持久化「选择」,不持久化「数据」——数据永远从 /api 拉,避免陈旧缓存。
10
+ const DEFAULT_UI = {
11
+ view: 'todo', // 当前 tab
12
+ topic: '', // 清单页的主题筛选(空 = 全部)
13
+ };
14
+ // 刻意**不**在这里存当前工作空间:它的真相源是本机配置(~/.nx-kv/config.json),
15
+ // 存两份就会出现「面板显示 A 组、实际写进 B 组」这类不一致。
16
+
17
+ function loadUi() {
18
+ try {
19
+ const raw = localStorage.getItem(LS_KEY);
20
+ if (!raw) return { ...DEFAULT_UI };
21
+ return { ...DEFAULT_UI, ...JSON.parse(raw) };
22
+ } catch {
23
+ return { ...DEFAULT_UI };
24
+ }
25
+ }
26
+
27
+ const StoreCtx = createContext(null);
28
+
29
+ export function StoreProvider({ children }) {
30
+ const [boot, setBoot] = useState(null); // /api/bootstrap 结果
31
+ const [bundled, setBundled] = useState(null);
32
+ const [ui, setUi] = useState(loadUi);
33
+
34
+ // UI 选择变化即落盘(同步、廉价、无版本迁移问题——字段级合并已兜底)
35
+ useEffect(() => {
36
+ try { localStorage.setItem(LS_KEY, JSON.stringify(ui)); } catch { /* 隐私模式等场景静默降级 */ }
37
+ }, [ui]);
38
+
39
+ const refreshBoot = useCallback(async () => {
40
+ const b = await api('/api/bootstrap');
41
+ setBoot(b);
42
+ return b;
43
+ }, []);
44
+
45
+ const refreshBundled = useCallback(async () => {
46
+ setBundled(await api('/api/bundled').catch(() => null));
47
+ }, []);
48
+
49
+ useEffect(() => { refreshBoot(); refreshBundled(); }, [refreshBoot, refreshBundled]);
50
+
51
+ // 支持对象 patch 与函数式 patch(函数式用于「基于最新 state 修剪」场景,避免闭包旧值覆盖)
52
+ const patchUi = useCallback((patch) => {
53
+ setUi((u) => (typeof patch === 'function' ? { ...u, ...patch(u) } : { ...u, ...patch }));
54
+ }, []);
55
+ const toggleSel = useCallback((key, name) => {
56
+ setUi((u) => {
57
+ const set = new Set(u[key]);
58
+ if (set.has(name)) set.delete(name); else set.add(name);
59
+ return { ...u, [key]: [...set] };
60
+ });
61
+ }, []);
62
+
63
+ const value = useMemo(
64
+ () => ({ boot, bundled, ui, patchUi, toggleSel, refreshBoot, refreshBundled }),
65
+ [boot, bundled, ui, patchUi, toggleSel, refreshBoot, refreshBundled]
66
+ );
67
+ return <StoreCtx.Provider value={value}>{children}</StoreCtx.Provider>;
68
+ }
69
+
70
+ export function useStore() {
71
+ const ctx = useContext(StoreCtx);
72
+ if (!ctx) throw new Error('useStore 必须在 StoreProvider 内使用');
73
+ return ctx;
74
+ }
@@ -0,0 +1,368 @@
1
+ /* nx-kv 面板样式:黑白单色、无 emoji。
2
+ 视觉取向:圆角 + 浅底 + 少线条——用背景层次和留白做区分,不用硬边框和网格线。 */
3
+ :root {
4
+ --ink: #14161a;
5
+ --paper: #fff;
6
+ --soft: #f5f6f7;
7
+ --soft-2: #ebedf0;
8
+ --mid: #8b9096;
9
+ --radius: 12px;
10
+ --radius-sm: 8px;
11
+ --shadow: 0 1px 2px rgba(20, 22, 26, 0.06), 0 6px 16px rgba(20, 22, 26, 0.04);
12
+ }
13
+
14
+ * { box-sizing: border-box; }
15
+
16
+ body {
17
+ margin: 0;
18
+ font-family: system-ui, "Segoe UI", "Microsoft YaHei", sans-serif;
19
+ font-size: 13.5px;
20
+ line-height: 1.55;
21
+ color: var(--ink);
22
+ background: #fbfbfc;
23
+ -webkit-font-smoothing: antialiased;
24
+ }
25
+
26
+ /* ---------- 顶栏 ---------- */
27
+ header {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 20px;
31
+ padding: 12px 20px;
32
+ background: var(--paper);
33
+ box-shadow: 0 1px 0 var(--soft-2);
34
+ position: sticky;
35
+ top: 0;
36
+ z-index: 5;
37
+ }
38
+ .brand { font-weight: 800; letter-spacing: 0.4px; }
39
+ .brand .sub { margin-left: 8px; font-weight: 400; color: var(--mid); font-size: 12px; }
40
+
41
+ /* 分段控件:整块浅底 + 圆角滑块,取代带边框的直角 tab */
42
+ nav { display: inline-flex; gap: 2px; padding: 3px; background: var(--soft); border-radius: 999px; }
43
+ .tab {
44
+ font: inherit;
45
+ border: 0;
46
+ background: transparent;
47
+ color: var(--mid);
48
+ padding: 5px 16px;
49
+ border-radius: 999px;
50
+ cursor: pointer;
51
+ transition: background 0.15s, color 0.15s;
52
+ }
53
+ .tab:hover { color: var(--ink); }
54
+ .tab.active { background: var(--paper); color: var(--ink); font-weight: 600; box-shadow: var(--shadow); }
55
+
56
+ .meta { margin-left: auto; color: var(--mid); font-family: ui-monospace, Consolas, monospace; font-size: 11.5px; }
57
+
58
+ main { padding: 18px 20px 28px; max-width: 1240px; margin: 0 auto; }
59
+ .panel { display: none; }
60
+ .panel.active { display: block; }
61
+
62
+ /* ---------- 工具栏 ---------- */
63
+ .toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
64
+ .toolbar label { color: var(--mid); font-size: 12px; }
65
+ .sep { width: 1px; height: 20px; background: var(--soft-2); margin: 0 4px; }
66
+ .grow { max-width: 340px; }
67
+
68
+ input, select {
69
+ font: inherit;
70
+ color: var(--ink);
71
+ background: var(--paper);
72
+ border: 1px solid var(--soft-2);
73
+ border-radius: var(--radius-sm);
74
+ padding: 6px 10px;
75
+ transition: border-color 0.15s, box-shadow 0.15s;
76
+ }
77
+ input::placeholder { color: #b3b8bd; }
78
+ input:focus, select:focus {
79
+ outline: none;
80
+ border-color: var(--ink);
81
+ box-shadow: 0 0 0 3px rgba(20, 22, 26, 0.07);
82
+ }
83
+
84
+ /* ---------- 按钮:圆角、浅底、无硬边框 ---------- */
85
+ .btn {
86
+ font: inherit;
87
+ cursor: pointer;
88
+ padding: 6px 12px;
89
+ border: 0;
90
+ border-radius: var(--radius-sm);
91
+ background: var(--ink);
92
+ color: var(--paper);
93
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
94
+ }
95
+ .btn:hover { opacity: 0.86; }
96
+ .btn.ghost { background: var(--soft); color: var(--ink); }
97
+ .btn.ghost:hover { background: var(--soft-2); opacity: 1; }
98
+ .btn.small { padding: 3px 9px; font-size: 12px; border-radius: 7px; }
99
+ .btn:disabled { opacity: 0.4; cursor: default; }
100
+ .btn.danger { background: #b3261e; color: #fff; }
101
+
102
+ /* ---------- toast / 对话框(取代浏览器原生弹窗) ---------- */
103
+ .toast-stack {
104
+ position: fixed;
105
+ left: 50%;
106
+ bottom: 28px;
107
+ transform: translateX(-50%);
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ gap: 8px;
112
+ z-index: 60;
113
+ pointer-events: none;
114
+ }
115
+ .toast {
116
+ background: var(--ink);
117
+ color: var(--paper);
118
+ font-size: 12.5px;
119
+ padding: 9px 16px;
120
+ border-radius: 10px;
121
+ box-shadow: 0 8px 24px rgba(20, 22, 26, 0.25);
122
+ max-width: 72vw;
123
+ white-space: pre-wrap;
124
+ word-break: break-all;
125
+ }
126
+
127
+ .dlg {
128
+ position: fixed;
129
+ inset: 0;
130
+ background: rgba(251, 251, 252, 0.86);
131
+ backdrop-filter: blur(2px);
132
+ display: flex;
133
+ align-items: flex-start;
134
+ justify-content: center;
135
+ padding: 96px 16px;
136
+ z-index: 50;
137
+ }
138
+ .dlg-box {
139
+ background: var(--paper);
140
+ border-radius: var(--radius);
141
+ box-shadow: 0 12px 40px rgba(20, 22, 26, 0.16);
142
+ width: min(460px, 94vw);
143
+ padding: 16px 18px;
144
+ }
145
+ .dlg-title { font-weight: 700; margin-bottom: 6px; }
146
+ .dlg-msg { color: var(--ink); white-space: pre-wrap; word-break: break-all; margin-bottom: 6px; }
147
+ .dlg-input {
148
+ width: 100%;
149
+ margin: 8px 0 4px;
150
+ font-family: ui-monospace, Consolas, monospace;
151
+ font-size: 12.5px;
152
+ }
153
+ .dlg-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
154
+
155
+ /* ---------- 卡片与列表 ---------- */
156
+ .card {
157
+ background: var(--paper);
158
+ border-radius: var(--radius);
159
+ padding: 10px 12px;
160
+ box-shadow: var(--shadow);
161
+ margin-bottom: 12px;
162
+ }
163
+ .card.hidden { display: none; }
164
+ .colhead { display: flex; align-items: baseline; gap: 8px; margin: 2px 2px 8px; }
165
+ h3 { font-size: 13px; margin: 0; font-weight: 700; }
166
+ .cols { display: flex; gap: 14px; align-items: flex-start; }
167
+ .col { flex: 1; min-width: 0; }
168
+ .list { padding: 4px; }
169
+
170
+ /* 行:靠 hover 底色区分,不用分隔线;内容多时允许换行 */
171
+ .row {
172
+ display: flex;
173
+ flex-wrap: wrap;
174
+ align-items: center;
175
+ gap: 8px 10px;
176
+ padding: 8px 10px;
177
+ border-radius: 10px;
178
+ transition: background 0.12s;
179
+ }
180
+ .row:hover { background: var(--soft); }
181
+ .row + .row { margin-top: 2px; }
182
+
183
+ /* 清单页行内元素:id / 主题 / 正文 / 时间 / 备注 */
184
+ .row .tid {
185
+ color: var(--mid);
186
+ font-family: ui-monospace, Consolas, monospace;
187
+ font-size: 11.5px;
188
+ min-width: 30px;
189
+ text-align: right;
190
+ flex-shrink: 0;
191
+ }
192
+ .row .topic {
193
+ font-size: 12px;
194
+ font-weight: 600;
195
+ background: var(--soft-2);
196
+ border-radius: 5px;
197
+ padding: 1px 6px;
198
+ white-space: nowrap;
199
+ flex-shrink: 0;
200
+ }
201
+ .row .text {
202
+ flex: 1;
203
+ min-width: 0; /* 关键:没有它,长文本会把整个 flex 行撑破 */
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+ .row .time { color: var(--mid); font-size: 11.5px; white-space: nowrap; flex-shrink: 0; }
209
+ .row .note {
210
+ flex-basis: 100%; /* 强制换行到自己的行 */
211
+ color: var(--mid);
212
+ font-size: 11.5px;
213
+ padding-left: 40px;
214
+ word-break: break-word;
215
+ }
216
+
217
+ /* 登录页:行内表单 */
218
+ .row-inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
219
+ .row .name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
220
+ .row .desc { color: var(--mid); font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
221
+ .row .acts { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
222
+
223
+ /* 平台小按钮(平行、可勾选) */
224
+ .plats { display: flex; gap: 4px; flex-wrap: wrap; }
225
+ .pill {
226
+ font: inherit;
227
+ font-size: 11px;
228
+ line-height: 1;
229
+ cursor: pointer;
230
+ padding: 5px 8px;
231
+ border: 0;
232
+ border-radius: 999px;
233
+ background: var(--soft);
234
+ color: var(--mid);
235
+ transition: background 0.12s, color 0.12s;
236
+ }
237
+ .pill:hover { background: var(--soft-2); color: var(--ink); }
238
+ .pill.on { background: var(--ink); color: var(--paper); font-weight: 600; }
239
+ /* 形态区分:实体 = 实心(默认 on 样式);软链接 = 虚线描边 */
240
+ .pill.on.lnk {
241
+ background: var(--paper);
242
+ color: var(--ink);
243
+ border: 1.5px dashed var(--ink);
244
+ padding: 4px 6.5px;
245
+ }
246
+
247
+ /* 徽标 */
248
+ .tag {
249
+ font-size: 11px;
250
+ padding: 2px 8px;
251
+ border-radius: 999px;
252
+ background: var(--soft);
253
+ color: var(--mid);
254
+ white-space: nowrap;
255
+ }
256
+ .tag.strong { background: var(--ink); color: var(--paper); }
257
+
258
+ /* ---------- 表格:去掉网格线,只用极浅的行分隔 ---------- */
259
+ table { width: 100%; border-collapse: collapse; }
260
+ th, td { text-align: left; padding: 9px 10px; vertical-align: top; }
261
+ th { color: var(--mid); font-weight: 600; font-size: 11.5px; }
262
+ tbody tr:hover { background: var(--soft); }
263
+ tbody tr + tr td { box-shadow: inset 0 1px 0 var(--soft); }
264
+ .mono, td.path { font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
265
+ td.ops { white-space: nowrap; }
266
+ td.ops .btn { margin: 0 3px 3px 0; }
267
+ .muted { color: var(--mid); }
268
+ .bad { font-weight: 700; }
269
+
270
+ /* ---------- 点击即复制的路径/标识 ---------- */
271
+ .copyable {
272
+ cursor: copy;
273
+ border-bottom: 1px dashed transparent;
274
+ transition: border-color 0.12s, color 0.12s;
275
+ }
276
+ .copyable:hover { border-bottom-color: var(--mid); }
277
+ .copyable:active { color: var(--ink); }
278
+ /* 适配器总表里多个可复制项并排 */
279
+ dd .adapter-dir { display: inline-block; margin-right: 14px; }
280
+
281
+ /* ---------- 设置页 ---------- */
282
+ .settings { display: grid; grid-template-columns: 120px 1fr; gap: 10px 16px; align-items: center; }
283
+ .settings dt { color: var(--mid); font-size: 12px; }
284
+ .settings dd { margin: 0; }
285
+ .row-inline { display: flex; gap: 8px; margin-top: 8px; }
286
+ .row-inline input { flex: 1; }
287
+
288
+ /* ---------- 提示条 ---------- */
289
+ .cli-hint {
290
+ color: var(--mid);
291
+ font-size: 11.5px;
292
+ padding: 6px 4px 2px;
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: 4px 8px;
296
+ align-items: baseline;
297
+ }
298
+ .cli-hint-label { flex-basis: 100%; }
299
+ /* CLI 命令串:一条命令一个可点击复制的 chip,密集但可读 */
300
+ .cli-cmd {
301
+ background: var(--soft);
302
+ border-radius: 5px;
303
+ padding: 1px 6px;
304
+ white-space: nowrap;
305
+ }
306
+
307
+ /* ---------- 弹窗 ---------- */
308
+ .modal {
309
+ position: fixed;
310
+ inset: 0;
311
+ background: rgba(251, 251, 252, 0.86);
312
+ backdrop-filter: blur(2px);
313
+ display: flex;
314
+ align-items: flex-start;
315
+ justify-content: center;
316
+ padding: 48px 16px;
317
+ z-index: 20;
318
+ }
319
+ .modal.hidden { display: none; }
320
+ .modal-box {
321
+ background: var(--paper);
322
+ border-radius: var(--radius);
323
+ box-shadow: 0 12px 40px rgba(20, 22, 26, 0.16);
324
+ width: min(880px, 96vw);
325
+ max-height: 84vh;
326
+ display: flex;
327
+ flex-direction: column;
328
+ overflow: hidden;
329
+ }
330
+ .modal-title {
331
+ padding: 12px 14px;
332
+ display: flex;
333
+ justify-content: space-between;
334
+ align-items: center;
335
+ font-weight: 700;
336
+ box-shadow: 0 1px 0 var(--soft);
337
+ }
338
+ .modal-body { padding: 14px; overflow: auto; }
339
+
340
+ pre {
341
+ margin: 0;
342
+ font-family: ui-monospace, Consolas, monospace;
343
+ font-size: 12px;
344
+ line-height: 1.6;
345
+ white-space: pre-wrap;
346
+ word-break: break-all;
347
+ }
348
+ .d-add { background: var(--soft-2); font-weight: 600; }
349
+ .d-del { color: var(--mid); text-decoration: line-through; }
350
+
351
+ .conflict-file { background: var(--soft); border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
352
+ .conflict-file .cf-head {
353
+ padding: 8px 12px;
354
+ display: flex;
355
+ justify-content: space-between;
356
+ align-items: center;
357
+ font-family: ui-monospace, Consolas, monospace;
358
+ font-size: 12px;
359
+ }
360
+ .conflict-file pre { padding: 10px 12px; background: var(--paper); }
361
+
362
+ footer { color: var(--mid); padding: 14px 20px 24px; font-size: 11.5px; text-align: center; }
363
+ footer code {
364
+ font-family: ui-monospace, Consolas, monospace;
365
+ background: var(--soft);
366
+ border-radius: 6px;
367
+ padding: 1px 6px;
368
+ }
@@ -0,0 +1 @@
1
+ import{o as e,s as t,t as n}from"./index-jhTtYCb9.js";var r=t();function i({module:t}){let{boot:i}=e(),a=(i?.commands||[]).filter(e=>e.module===t);return a.length?(0,r.jsxs)(`div`,{className:`cli-hint`,children:[(0,r.jsx)(`span`,{className:`cli-hint-label`,children:`CLI 等价(同构命令,加 --json 得机器可读输出):`}),a.map(e=>(0,r.jsx)(n,{className:`cli-cmd`,text:e.command,title:`点击复制:${e.usage}`,children:e.command},e.id))]}):null}export{i as t};
@@ -0,0 +1 @@
1
+ :root{--ink:#14161a;--paper:#fff;--soft:#f5f6f7;--soft-2:#ebedf0;--mid:#8b9096;--radius:12px;--radius-sm:8px;--shadow:0 1px 2px #14161a0f, 0 6px 16px #14161a0a}*{box-sizing:border-box}body{color:var(--ink);-webkit-font-smoothing:antialiased;background:#fbfbfc;margin:0;font-family:system-ui,Segoe UI,Microsoft YaHei,sans-serif;font-size:13.5px;line-height:1.55}header{background:var(--paper);box-shadow:0 1px 0 var(--soft-2);z-index:5;align-items:center;gap:20px;padding:12px 20px;display:flex;position:sticky;top:0}.brand{letter-spacing:.4px;font-weight:800}.brand .sub{color:var(--mid);margin-left:8px;font-size:12px;font-weight:400}nav{background:var(--soft);border-radius:999px;gap:2px;padding:3px;display:inline-flex}.tab{font:inherit;color:var(--mid);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:5px 16px;transition:background .15s,color .15s}.tab:hover{color:var(--ink)}.tab.active{background:var(--paper);color:var(--ink);box-shadow:var(--shadow);font-weight:600}.meta{color:var(--mid);margin-left:auto;font-family:ui-monospace,Consolas,monospace;font-size:11.5px}main{max-width:1240px;margin:0 auto;padding:18px 20px 28px}.panel{display:none}.panel.active{display:block}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px;display:flex}.toolbar label{color:var(--mid);font-size:12px}.sep{background:var(--soft-2);width:1px;height:20px;margin:0 4px}.grow{max-width:340px}input,select{font:inherit;color:var(--ink);background:var(--paper);border:1px solid var(--soft-2);border-radius:var(--radius-sm);padding:6px 10px;transition:border-color .15s,box-shadow .15s}input::placeholder{color:#b3b8bd}input:focus,select:focus{border-color:var(--ink);outline:none;box-shadow:0 0 0 3px #14161a12}.btn{font:inherit;cursor:pointer;border-radius:var(--radius-sm);background:var(--ink);color:var(--paper);border:0;padding:6px 12px;transition:opacity .15s,background .15s,color .15s}.btn:hover{opacity:.86}.btn.ghost{background:var(--soft);color:var(--ink)}.btn.ghost:hover{background:var(--soft-2);opacity:1}.btn.small{border-radius:7px;padding:3px 9px;font-size:12px}.btn:disabled{opacity:.4;cursor:default}.btn.danger{color:#fff;background:#b3261e}.toast-stack{z-index:60;pointer-events:none;flex-direction:column;align-items:center;gap:8px;display:flex;position:fixed;bottom:28px;left:50%;transform:translate(-50%)}.toast{background:var(--ink);color:var(--paper);white-space:pre-wrap;word-break:break-all;border-radius:10px;max-width:72vw;padding:9px 16px;font-size:12.5px;box-shadow:0 8px 24px #14161a40}.dlg{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:#fbfbfcdb;justify-content:center;align-items:flex-start;padding:96px 16px;display:flex;position:fixed;inset:0}.dlg-box{background:var(--paper);border-radius:var(--radius);width:min(460px,94vw);padding:16px 18px;box-shadow:0 12px 40px #14161a29}.dlg-title{margin-bottom:6px;font-weight:700}.dlg-msg{color:var(--ink);white-space:pre-wrap;word-break:break-all;margin-bottom:6px}.dlg-input{width:100%;margin:8px 0 4px;font-family:ui-monospace,Consolas,monospace;font-size:12.5px}.dlg-acts{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.card{background:var(--paper);border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:12px;padding:10px 12px}.card.hidden{display:none}.colhead{align-items:baseline;gap:8px;margin:2px 2px 8px;display:flex}h3{margin:0;font-size:13px;font-weight:700}.cols{align-items:flex-start;gap:14px;display:flex}.col{flex:1;min-width:0}.list{padding:4px}.row{border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:8px 10px;transition:background .12s;display:flex}.row:hover{background:var(--soft)}.row+.row{margin-top:2px}.row .tid{color:var(--mid);text-align:right;flex-shrink:0;min-width:30px;font-family:ui-monospace,Consolas,monospace;font-size:11.5px}.row .topic{background:var(--soft-2);white-space:nowrap;border-radius:5px;flex-shrink:0;padding:1px 6px;font-size:12px;font-weight:600}.row .text{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.row .time{color:var(--mid);white-space:nowrap;flex-shrink:0;font-size:11.5px}.row .note{color:var(--mid);word-break:break-word;flex-basis:100%;padding-left:40px;font-size:11.5px}.row-inline{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.row .name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-weight:600;overflow:hidden}.row .desc{color:var(--mid);text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:12px;overflow:hidden}.row .acts{flex-shrink:0;align-items:center;gap:6px;margin-left:auto;display:flex}.plats{flex-wrap:wrap;gap:4px;display:flex}.pill{font:inherit;cursor:pointer;background:var(--soft);color:var(--mid);border:0;border-radius:999px;padding:5px 8px;font-size:11px;line-height:1;transition:background .12s,color .12s}.pill:hover{background:var(--soft-2);color:var(--ink)}.pill.on{background:var(--ink);color:var(--paper);font-weight:600}.pill.on.lnk{background:var(--paper);color:var(--ink);border:1.5px dashed var(--ink);padding:4px 6.5px}.tag{background:var(--soft);color:var(--mid);white-space:nowrap;border-radius:999px;padding:2px 8px;font-size:11px}.tag.strong{background:var(--ink);color:var(--paper)}table{border-collapse:collapse;width:100%}th,td{text-align:left;vertical-align:top;padding:9px 10px}th{color:var(--mid);font-size:11.5px;font-weight:600}tbody tr:hover{background:var(--soft)}tbody tr+tr td{box-shadow:inset 0 1px 0 var(--soft)}.mono,td.path{word-break:break-all;font-family:ui-monospace,Consolas,monospace;font-size:12px}td.ops{white-space:nowrap}td.ops .btn{margin:0 3px 3px 0}.muted{color:var(--mid)}.bad{font-weight:700}.copyable{cursor:copy;border-bottom:1px dashed #0000;transition:border-color .12s,color .12s}.copyable:hover{border-bottom-color:var(--mid)}.copyable:active{color:var(--ink)}dd .adapter-dir{margin-right:14px;display:inline-block}.settings{grid-template-columns:120px 1fr;align-items:center;gap:10px 16px;display:grid}.settings dt{color:var(--mid);font-size:12px}.settings dd{margin:0}.row-inline{gap:8px;margin-top:8px;display:flex}.row-inline input{flex:1}.cli-hint{color:var(--mid);flex-wrap:wrap;align-items:baseline;gap:4px 8px;padding:6px 4px 2px;font-size:11.5px;display:flex}.cli-hint-label{flex-basis:100%}.cli-cmd{background:var(--soft);white-space:nowrap;border-radius:5px;padding:1px 6px}.modal{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:20;background:#fbfbfcdb;justify-content:center;align-items:flex-start;padding:48px 16px;display:flex;position:fixed;inset:0}.modal.hidden{display:none}.modal-box{background:var(--paper);border-radius:var(--radius);flex-direction:column;width:min(880px,96vw);max-height:84vh;display:flex;overflow:hidden;box-shadow:0 12px 40px #14161a29}.modal-title{box-shadow:0 1px 0 var(--soft);justify-content:space-between;align-items:center;padding:12px 14px;font-weight:700;display:flex}.modal-body{padding:14px;overflow:auto}pre{white-space:pre-wrap;word-break:break-all;margin:0;font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.6}.d-add{background:var(--soft-2);font-weight:600}.d-del{color:var(--mid);text-decoration:line-through}.conflict-file{background:var(--soft);border-radius:10px;margin-bottom:10px;overflow:hidden}.conflict-file .cf-head{justify-content:space-between;align-items:center;padding:8px 12px;font-family:ui-monospace,Consolas,monospace;font-size:12px;display:flex}.conflict-file pre{background:var(--paper);padding:10px 12px}footer{color:var(--mid);text-align:center;padding:14px 20px 24px;font-size:11.5px}footer code{background:var(--soft);border-radius:6px;padding:1px 6px;font-family:ui-monospace,Consolas,monospace}