cardbuddy 0.7.0 → 0.8.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.
@@ -27,6 +27,7 @@ import { createAssetProxyMiddleware } from './assetProxy.js'
27
27
  import { createSliceStaticMiddleware } from './sliceStatic.js'
28
28
  import { createCardImageMiddleware } from './cardImage.js'
29
29
  import { createOpenFolderMiddleware } from './cardOpenFolder.js'
30
+ import { createCodeBridgeMiddleware } from './codeBridge.js'
30
31
  import { buildDataFromSchema } from './buildDataFromSchema.js'
31
32
 
32
33
  export {
@@ -47,16 +48,19 @@ export {
47
48
  createSliceStaticMiddleware,
48
49
  createCardImageMiddleware,
49
50
  createOpenFolderMiddleware,
51
+ createCodeBridgeMiddleware,
50
52
  buildDataFromSchema,
51
53
  }
52
54
 
53
55
  /**
54
56
  * 把全部卡片中间件挂到 Connect 风格容器(需实现 .use(mw))。
55
57
  * @param {{ use: (mw: Function) => void }} app
56
- * @param {{ root: string, viteServer?: import('vite').ViteDevServer | null, upstream?: string }} opts
58
+ * @param {{ root: string, viteServer?: import('vite').ViteDevServer | null, upstream?: string, notify?: (data: object, event?: string) => void, watchFile?: (file: string, cb: () => void) => { close: () => void } }} opts
57
59
  * - upstream:FinMall 远端站点基址(来自 FINMALL_BASE_URL),资源代理回源用;空串表示不回源。
60
+ * - notify:SSE 推送入口(cardbuddy 的 /__events hub);代码桥经它推 bridge 事件,缺省时静默降级。
61
+ * - watchFile:代码桥单文件监听能力注入(chokidar 版);缺省代码桥用 fs.watch 兜底。
58
62
  */
59
- export function mountCoreMiddlewares(app, { root, viteServer = null, upstream = '' }) {
63
+ export function mountCoreMiddlewares(app, { root, viteServer = null, upstream = '', notify = null, watchFile = null }) {
60
64
  app.use(createSyncMiddleware({ root }))
61
65
  app.use(createPreviewMiddleware({ root }))
62
66
  app.use(createLoginCredentialsMiddleware({ root }))
@@ -76,6 +80,8 @@ export function mountCoreMiddlewares(app, { root, viteServer = null, upstream =
76
80
  app.use(createBboxMiddleware({ root }))
77
81
  // 在系统文件管理器中打开当前卡片本地目录(编辑器顶栏「打开目录」按钮触发,跨平台由后端完成)
78
82
  app.use(createOpenFolderMiddleware({ root }))
83
+ // 外部编辑器代码桥(VSCode / DSL_EDITOR_CMD):临时 js + 保存回写 + 会话 TTL 清理
84
+ app.use(createCodeBridgeMiddleware({ root, notify, watchFile }))
79
85
  // 卡片预览图静态服务:/__card-image/<id> → src/cards/<id>/<id>.<ext>(编辑器网格预览缩略图用,
80
86
  // 复用本地静态服务,避免实时无头截图)。
81
87
  app.use(createCardImageMiddleware({ root }))
@@ -290,8 +290,8 @@ function createEventsEndpoint() {
290
290
  clients.delete(res)
291
291
  })
292
292
  }
293
- const notify = (data) => {
294
- const payload = `event: cards-changed\ndata: ${JSON.stringify(data)}\n\n`
293
+ const notify = (data, event = 'cards-changed') => {
294
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
295
295
  for (const res of clients) res.write(payload)
296
296
  }
297
297
  return { handler, notify }
@@ -397,7 +397,22 @@ export async function startServer(userConfig = {}) {
397
397
 
398
398
  const vite = await createEmbeddedVite(config)
399
399
  // upstream(FINMALL_BASE_URL)透传给资源代理中间件,使其能回源 FinMall 取站内资源
400
- mountCoreMiddlewares(app, { root: config.root, viteServer: vite, upstream: config.upstream })
400
+ mountCoreMiddlewares(app, {
401
+ root: config.root,
402
+ viteServer: vite,
403
+ upstream: config.upstream,
404
+ notify: events.notify,
405
+ // 代码桥单文件监听注入 chokidar 版:原子保存(改名覆盖)由 add 兜住,awaitWriteFinish 防半截读
406
+ watchFile: (file, onChange) => {
407
+ const watcher = chokidar.watch(file, {
408
+ ignoreInitial: true,
409
+ awaitWriteFinish: { stabilityThreshold: 120, pollInterval: 40 },
410
+ })
411
+ watcher.on('change', onChange)
412
+ watcher.on('add', onChange)
413
+ return watcher
414
+ },
415
+ })
401
416
 
402
417
  watchCards(config.root, events.notify)
403
418
 
@@ -328,7 +328,9 @@
328
328
  </div>
329
329
  </div>
330
330
 
331
- <!-- 拖拽分隔条:位于预览列与操作面板之间,拖动调整预览宽度(手机自动缩放填充) -->
331
+
332
+ <!-- 左分隔条:预览列 | 工作区 接缝。底面板 / 状态栏的左缘即此缝(随预览宽度联动),
333
+ 故定位在 .app__stage 通到底部,全程可拖;右分隔条才收进两栏容器内。 -->
332
334
  <div
333
335
  v-show="!chromeOff"
334
336
  class="splitter"
@@ -337,22 +339,24 @@
337
339
  title="拖拽调整预览区域宽度"
338
340
  ></div>
339
341
 
340
- <!-- 拖拽分隔条:位于调试面板与右侧「接口调用」面板之间,拖动调整右栏宽度。
341
- 约束:中栏(调试面板)最小宽度 ≥ 屏宽 1/3,右栏最小宽度 ≥ 320px(见 startApiDrag) -->
342
- <div
343
- v-show="!chromeOff && apiPanelOpen"
344
- class="splitter splitter--api"
345
- :class="{ 'is-dragging': apiDragging }"
346
- @mousedown.prevent="startApiDrag"
347
- title="拖拽调整接口调用面板宽度"
348
- ></div>
349
-
350
342
  <!-- 中右工作区:上为「调试面板 + 接口调用面板」两栏,下为贯穿两栏的共同状态栏。
351
- 状态栏由 DebugPanel 内 Teleport 注入 #app-workside-bar;Teleport 目标须在 DOM 中
352
- 先于面板挂载,故 .workside__bar 写在前面,以 flex order 视觉置底。 -->
343
+ 状态栏与底部弹出面板(运行态 / 事件 / 绑定错误)均由 DebugPanel 内 Teleport 注入
344
+ #app-workside-bar / #app-workside-panels,宽度贯穿中右两栏;Teleport 目标须在 DOM 中
345
+ 先于面板挂载,故两个空容器写在前面,以 flex order 视觉置底。 -->
353
346
  <div class="workside" v-show="!chromeOff">
354
347
  <div id="app-workside-bar" class="workside__bar"></div>
348
+ <div id="app-workside-panels" class="workside__panels"></div>
355
349
  <div class="workside__cols">
350
+ <!-- 拖拽分隔条:置于两栏容器内(定位上下文),只贯穿列区高度,不穿过底部弹出面板与状态栏 -->
351
+ <!-- 中右分隔条:调试面板 | 接口调用面板 接缝,拖动调整右栏宽度。
352
+ 约束:中栏最小宽度 ≥ 屏宽 1/3,右栏最小宽度 ≥ 320px(见 startApiDrag) -->
353
+ <div
354
+ v-show="!chromeOff && apiPanelOpen"
355
+ class="splitter splitter--api"
356
+ :class="{ 'is-dragging': apiDragging }"
357
+ @mousedown.prevent="startApiDrag"
358
+ title="拖拽调整接口调用面板宽度"
359
+ ></div>
356
360
  <DebugPanel
357
361
  :chrome-off="chromeOff"
358
362
  :rt="rt"
@@ -4,6 +4,8 @@
4
4
  * 设计目标(前后端分离方案 Phase 2):
5
5
  * - 开发期(npm run dev,由 tools/dev.mjs 同起 cardbuddy):apiBase 默认
6
6
  * http://127.0.0.1:<FINMALL_CARD_PORT>(.env.finmall,默认 8787),与部署态一致。
7
+ * 若页面不是经回环地址打开(局域网 IP / 其它本机 IP),则退化为同源 '',
8
+ * 由 Vite dev server 代理到 8787,避开浏览器私有网络访问(PNA)拦截。
7
9
  * - 部署态(vite build 静态产物):默认读 localStorage 的「本地开发服务」配置;
8
10
  * 未配置时 apiBase = null,调用方据此提示「请先配置本地开发服务」。
9
11
  * - 本地开发服务(cardbuddy,默认 http://127.0.0.1:8787)经反射 CORS 放行任意
@@ -37,6 +39,23 @@ const DEFAULT_CONFIG = {
37
39
  token: '',
38
40
  }
39
41
 
42
+ // 页面是否经「非回环主机名」打开(Vite host:true 会额外打印局域网 IP / 其它本机地址,
43
+ // 例如 http://192.168.x.x:5180、http://2.0.0.1:5180)。此时浏览器把页面归为
44
+ // public/private 地址空间,而 cardbuddy 只绑 127.0.0.1(loopback),Chrome 的私有网络
45
+ // 访问(PNA)规则会以「The request client is not a secure context and the resource is in
46
+ // more-private address space `loopback`」直接拦掉 fetch / EventSource(net::ERR_FAILED),
47
+ // 表现为「本地开发服务已断开」。开发期改走同源(''):请求打到页面自己的 host,
48
+ // 由 vite.config.mjs 的 server.proxy 在 Node 侧转发到 127.0.0.1:<FINMALL_CARD_PORT>
49
+ // —— 服务端转发不受 CORS / PNA 约束。回环访问(localhost / 127.0.0.1)不受影响,
50
+ // 仍直连 8787 跨源链路,与部署态一致。
51
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
52
+ function isNonLoopbackDevHost() {
53
+ if (typeof window === 'undefined' || !window.location) return false
54
+ const { protocol, hostname } = window.location
55
+ if (protocol !== 'http:' && protocol !== 'https:') return false
56
+ return !LOOPBACK_HOSTS.has(hostname)
57
+ }
58
+
40
59
  export function loadDevServiceConfig() {
41
60
  try {
42
61
  const raw = localStorage.getItem(LS_KEY)
@@ -68,6 +87,8 @@ export function saveDevServiceConfig(cfg) {
68
87
  * - Vite dev server(import.meta.env.DEV 且无覆盖)→ 默认本地开发服务
69
88
  * http://127.0.0.1:<FINMALL_CARD_PORT>(.env.finmall,默认 8787)
70
89
  * (前后端分离:Vite 不再托管卡片后端,改由 tools/dev.mjs 同起的 cardbuddy 提供)
90
+ * 例外:页面经非回环主机名打开时返回 ''(同源),改由 Vite dev server 代理到 8787,
91
+ * 绕开浏览器私有网络访问(PNA)拦截,详见 isNonLoopbackDevHost 注释
71
92
  * - 部署态静态产物且未配置 → null(调用方需提示配置)
72
93
  */
73
94
  export function resolveApiBase() {
@@ -76,7 +97,9 @@ export function resolveApiBase() {
76
97
  if (override) return String(override).replace(/\/+$/, '')
77
98
  const cfg = loadDevServiceConfig()
78
99
  if (cfg.enabled && cfg.baseUrl) return cfg.baseUrl.replace(/\/+$/, '')
79
- if (import.meta.env && import.meta.env.DEV) return DEFAULT_CONFIG.baseUrl
100
+ if (import.meta.env && import.meta.env.DEV) {
101
+ return isNonLoopbackDevHost() ? '' : DEFAULT_CONFIG.baseUrl
102
+ }
80
103
  return null
81
104
  }
82
105
 
@@ -104,39 +127,86 @@ export async function apiFetch(path, opts = {}) {
104
127
  * @param {(state:'connecting'|'open'|'closed')=>void} [onState] 连接态变化回调,
105
128
  * 用于驱动顶栏「连接」图标实时反映服务端运行态(SSE readyState / Vite HMR ws 生命周期)。
106
129
  */
107
- export function subscribeCardsChanged(cb, onState) {
108
- const base = resolveApiBase()
109
- const emit = (s) => { if (typeof onState === 'function') onState(s) }
110
- if (base) {
111
- // 本地服务:SSE(浏览器原生自动重连)
112
- const es = new EventSource(base + '/__events')
113
- const handler = (e) => {
114
- try {
115
- cb(e.data ? JSON.parse(e.data) : {})
116
- } catch (_) {
117
- cb({})
130
+ /**
131
+ * 通用 SSE 订阅工厂:监听 /__events 上的一组事件名,返回取消订阅函数。
132
+ * base === ''(开发期同源代理)同样走 SSE:请求相对当前页面 host,由 Vite 转发到 8787,
133
+ * 连接态因此能真实反映 cardbuddy 运行态。
134
+ * @param {string[]} events 事件名列表
135
+ * @param {{ hmrEvents?: string[] }} [opts] hmrEvents:base 为 null(部署态未配置)时
136
+ * 回退到 Vite HMR 通道的事件名(仅 cards-changed 仍有该历史通道)。
137
+ */
138
+ function makeSseSubscriber(events, { hmrEvents = [] } = {}) {
139
+ return function subscribe(cb, onState) {
140
+ const base = resolveApiBase()
141
+ const emit = (s) => { if (typeof onState === 'function') onState(s) }
142
+ if (base !== null) {
143
+ // 本地服务 / 同源代理:SSE(浏览器原生自动重连)
144
+ const es = new EventSource(base + '/__events')
145
+ const handler = (e) => {
146
+ try {
147
+ cb(e.data ? JSON.parse(e.data) : {})
148
+ } catch (_) {
149
+ cb({})
150
+ }
118
151
  }
152
+ for (const name of events) es.addEventListener(name, handler)
153
+ es.onopen = () => emit('open')
154
+ es.onerror = () => {
155
+ // 断链时 onerror 触发,浏览器会自动重连:
156
+ // readyState===0(CONNECTING) 表示正在重连;===2(CLOSED) 表示已彻底关闭(如显式 close)。
157
+ emit(es.readyState === 2 ? 'closed' : 'connecting')
158
+ }
159
+ emit('connecting') // 刚创建即处于 CONNECTING,先置为「连接中」
160
+ return () => es.close()
119
161
  }
120
- es.addEventListener('cards-changed', handler)
121
- es.onopen = () => emit('open')
122
- es.onerror = () => {
123
- // 断链时 onerror 触发,浏览器会自动重连:
124
- // readyState===0(CONNECTING) 表示正在重连;===2(CLOSED) 表示已彻底关闭(如显式 close)。
125
- emit(es.readyState === 2 ? 'closed' : 'connecting')
126
- }
127
- emit('connecting') // 刚创建即处于 CONNECTING,先置为「连接中」
128
- return () => es.close()
129
- }
130
- // 同源 dev server:沿用 Vite HMR(底层 WebSocket,同样具备连接态)
131
- if (import.meta.hot) {
132
- import.meta.hot.on('cards-changed', cb)
133
- // 页面由同源 dev server 提供,初始即视为已连接;ws 断开 / 重连时更新状态
134
- import.meta.hot.on('vite:ws:connect', () => emit('open'))
135
- import.meta.hot.on('vite:ws:disconnect', () => emit('connecting'))
136
- emit('open')
137
- return () => {
138
- /* HMR 无显式 off,noop */
162
+ // 同源 dev server:沿用 Vite HMR(底层 WebSocket,同样具备连接态)
163
+ if (import.meta.hot && hmrEvents.length) {
164
+ for (const name of hmrEvents) import.meta.hot.on(name, cb)
165
+ // 页面由同源 dev server 提供,初始即视为已连接;ws 断开 / 重连时更新状态
166
+ import.meta.hot.on('vite:ws:connect', () => emit('open'))
167
+ import.meta.hot.on('vite:ws:disconnect', () => emit('connecting'))
168
+ emit('open')
169
+ return () => {
170
+ /* HMR 无显式 off,noop */
171
+ }
139
172
  }
173
+ return () => {}
140
174
  }
141
- return () => {}
175
+ }
176
+
177
+ /** 订阅磁盘变更推送(cards-changed)。 */
178
+ export const subscribeCardsChanged = makeSseSubscriber(['cards-changed'], { hmrEvents: ['cards-changed'] })
179
+
180
+ /** 订阅外部编辑器代码桥事件(code-bridge-changed / code-bridge-closed)。 */
181
+ export const subscribeCodeBridge = makeSseSubscriber(['code-bridge-changed', 'code-bridge-closed'])
182
+
183
+ /**
184
+ * 代码桥 REST 封装(端点见 server/core/codeBridge.js):
185
+ * open 建立会话并拉起编辑器;fetch 拉取外部保存后的最新代码;
186
+ * heartbeat 维持会话(TTL 兜底清理);close 结束会话并删除临时文件。
187
+ */
188
+ export async function codeBridgeOpen(code, label = '') {
189
+ const res = await apiFetch('/__code-bridge', {
190
+ method: 'POST',
191
+ headers: { 'Content-Type': 'application/json' },
192
+ body: JSON.stringify({ code, label }),
193
+ })
194
+ if (!res.ok) throw new Error(`code-bridge open failed: ${res.status}`)
195
+ return res.json()
196
+ }
197
+
198
+ export async function codeBridgeFetch(id) {
199
+ const res = await apiFetch(`/__code-bridge/${id}`)
200
+ if (!res.ok) throw new Error(`code-bridge fetch failed: ${res.status}`)
201
+ return res.json()
202
+ }
203
+
204
+ export async function codeBridgeHeartbeat(id) {
205
+ const res = await apiFetch(`/__code-bridge/${id}/heartbeat`, { method: 'POST' })
206
+ return res.ok
207
+ }
208
+
209
+ export async function codeBridgeClose(id) {
210
+ const res = await apiFetch(`/__code-bridge/${id}`, { method: 'DELETE' })
211
+ return res.ok
142
212
  }
@@ -32,7 +32,19 @@
32
32
  </button>
33
33
  </div>
34
34
  <div class="apipanel__body">
35
- <p v-if="!apiCalls.length" class="debug__empty">暂无接口调用</p>
35
+ <!-- 空状态占位:虚线圆形徽章 + 插头图标(与全局 24 网格线性图标同语言)+ 文案 + 弱提示 -->
36
+ <div v-if="!apiCalls.length" class="empty-state">
37
+ <span class="empty-state__badge" aria-hidden="true">
38
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
39
+ <path d="M9 7V3" />
40
+ <path d="M15 7V3" />
41
+ <path d="M6 7h12v4a6 6 0 0 1-12 0V7z" />
42
+ <path d="M12 17v4" />
43
+ </svg>
44
+ </span>
45
+ <p class="debug__empty">暂无API调用</p>
46
+ <p class="empty-state__hint">卡片事件触发 ynet.callJSAPI / postMessage 后,调用流水会实时显示在这里</p>
47
+ </div>
36
48
  <ul class="debug__list">
37
49
  <li v-for="(c, i) in apiCalls" :key="i" class="debug__item debug__item--api">
38
50
  <div class="debug__api-row">
@@ -15,7 +15,26 @@
15
15
  </div>
16
16
 
17
17
  <template v-else>
18
- <div class="apt__list">
18
+ <!-- 日志场景(rootCollapsed):根对象/数组先收成一行摘要、点击展开(Chrome 控制台风格),
19
+ 避免每个实参默认逐键占行稀释日志密度;接口调用面板不传该 prop,行为不变 -->
20
+ <div
21
+ v-if="rootCollapsed && isRoot"
22
+ class="apt__branch apt__branch--root"
23
+ @click="rootOpen = !rootOpen"
24
+ >
25
+ <span class="apt__caret" :class="{ 'apt__caret--open': rootOpen }">▶</span>
26
+ <span class="jt__bracket">{{ isArrayRoot ? '[' : '{' }}</span>
27
+ <span v-if="!rootOpen" class="jt__preview">{{ preview(value) }}</span>
28
+ <template v-else>
29
+ <span class="jt__count">{{ rootCount }} {{ isArrayRoot ? '项' : '键' }}</span>
30
+ </template>
31
+ <span v-if="!rootOpen" class="jt__bracket">{{ isArrayRoot ? ']' : '}' }}</span>
32
+ </div>
33
+ <div
34
+ v-show="!(rootCollapsed && isRoot) || rootOpen"
35
+ class="apt__list"
36
+ :class="{ 'apt__list--nested': rootCollapsed && isRoot && rootOpen }"
37
+ >
19
38
  <div v-for="(entry, i) in entries" :key="i" class="apt__node">
20
39
  <!-- 复合类型且未超最大深度:可折叠分支 -->
21
40
  <div v-if="canExpand(entry)" class="apt__branch" @click="toggle(entry)">
@@ -58,6 +77,10 @@
58
77
  </div>
59
78
  </div>
60
79
  </div>
80
+ <div v-if="rootCollapsed && isRoot && rootOpen" class="apt__close">
81
+ <span class="jt__bracket">{{ isArrayRoot ? ']' : '}' }}</span>
82
+ <span class="jt__count">{{ rootCount }} {{ isArrayRoot ? '项' : '键' }}</span>
83
+ </div>
61
84
  </template>
62
85
  </div>
63
86
  </template>
@@ -81,17 +104,27 @@ export default {
81
104
  // 最大可展开深度(保护):达到该深度的复合节点不再可展开,
82
105
  // 避免日志中循环引用 / 超深结构在展开时无限递归。默认 100(接口调用场景基本无感)。
83
106
  maxDepth: { type: Number, default: 100 },
107
+ // 根节点默认折叠成一行摘要(事件日志使用);接口调用面板保持逐行铺开
108
+ rootCollapsed: { type: Boolean, default: false },
84
109
  },
85
110
  data() {
86
111
  return {
87
112
  // 若未传入共享状态(理论只发生在根),自行创建,保证递归子组件拿到同一引用
88
113
  localOpenState: this.openState || reactive({}),
114
+ // 根摘要行的展开态(仅 rootCollapsed 场景使用)
115
+ rootOpen: false,
89
116
  }
90
117
  },
91
118
  computed: {
92
119
  isRoot() {
93
120
  return this.depth === 0
94
121
  },
122
+ isArrayRoot() {
123
+ return Array.isArray(this.value)
124
+ },
125
+ rootCount() {
126
+ return this.isArrayRoot ? this.value.length : Object.keys(this.value || {}).length
127
+ },
95
128
  state() {
96
129
  return this.openState || this.localOpenState
97
130
  },
@@ -129,11 +129,13 @@ export default {
129
129
  .cp__mask {
130
130
  position: fixed;
131
131
  inset: 0;
132
- background: var(--scrim);
132
+ /* 与确认框 .confirm-mask 同款模糊遮罩:暗色半透明 + 2px 背景模糊 */
133
+ background: var(--scrim-modal);
134
+ backdrop-filter: blur(var(--scrim-modal-blur));
133
135
  display: flex;
134
136
  align-items: center;
135
137
  justify-content: center;
136
- z-index: 1000;
138
+ z-index: var(--z-modal);
137
139
  }
138
140
 
139
141
  .cp {
@@ -142,8 +144,8 @@ export default {
142
144
  max-height: calc(100vh - 64px);
143
145
  background: var(--bg-panel);
144
146
  border: 1px solid var(--border-strong);
145
- border-radius: 10px;
146
- box-shadow: 0 18px 48px var(--shadow-pop);
147
+ border-radius: var(--radius-lg);
148
+ box-shadow: var(--shadow-4);
147
149
  display: flex;
148
150
  flex-direction: column;
149
151
  overflow: hidden;
@@ -152,29 +154,35 @@ export default {
152
154
 
153
155
  .cp__head {
154
156
  display: flex;
155
- align-items: baseline;
157
+ align-items: center;
156
158
  gap: 10px;
157
- padding: 14px 16px;
158
- border-bottom: 1px solid var(--border-strong);
159
+ padding: 10px 16px;
160
+ border-bottom: 1px solid var(--border);
159
161
  }
160
162
  .cp__title {
161
- font-size: 15px;
163
+ font-size: 13px;
162
164
  font-weight: 600;
163
165
  }
164
166
  .cp__hint {
165
- font-size: 12px;
166
- color: var(--text-dim);
167
+ font-size: 11px;
168
+ color: var(--text-faint);
167
169
  }
168
170
  .cp__close {
169
171
  margin-left: auto;
172
+ display: inline-flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ width: 22px;
176
+ height: 22px;
177
+ padding: 0;
170
178
  background: transparent;
171
179
  border: none;
172
- color: var(--text-dim);
173
- font-size: 16px;
180
+ border-radius: 6px;
181
+ color: var(--text-faint);
182
+ font-size: 15px;
174
183
  cursor: pointer;
175
184
  line-height: 1;
176
- padding: 2px 6px;
177
- border-radius: 4px;
185
+ transition: background 0.15s, color 0.15s;
178
186
  }
179
187
  .cp__close:hover {
180
188
  background: var(--bg-hover);
@@ -182,16 +190,16 @@ export default {
182
190
  }
183
191
 
184
192
  .cp__body {
185
- padding: 8px 16px 16px;
193
+ padding: 10px 16px 16px;
186
194
  overflow-y: auto;
187
195
  }
188
196
 
189
197
  .cp__group {
190
- margin-top: 12px;
198
+ margin-top: 10px;
191
199
  }
192
200
  .cp__group-title {
193
- margin: 0 0 8px;
194
- font-size: 12px;
201
+ margin: 0 0 6px;
202
+ font-size: 11px;
195
203
  font-weight: 600;
196
204
  color: var(--text-faint);
197
205
  letter-spacing: 0.04em;
@@ -200,7 +208,7 @@ export default {
200
208
  .cp__grid {
201
209
  display: grid;
202
210
  grid-template-columns: repeat(4, 1fr);
203
- gap: 8px;
211
+ gap: 6px;
204
212
  }
205
213
 
206
214
  .cp__item {
@@ -208,21 +216,17 @@ export default {
208
216
  flex-direction: column;
209
217
  align-items: center;
210
218
  gap: 4px;
211
- padding: 10px 6px;
212
- background: var(--bg-input);
219
+ padding: 8px 6px;
220
+ background: transparent;
213
221
  border: 1px solid var(--border);
214
- border-radius: 8px;
222
+ border-radius: 6px;
215
223
  cursor: pointer;
216
224
  color: var(--text);
217
- transition: border-color 0.12s, background 0.12s, transform 0.08s;
225
+ transition: border-color 0.15s, background 0.15s;
218
226
  }
219
227
  .cp__item:hover {
220
- border-color: var(--accent);
221
- background: var(--bg-hover);
222
- transform: translateY(-1px);
223
- }
224
- .cp__item:active {
225
- transform: translateY(0);
228
+ border-color: var(--accent-border);
229
+ background: var(--accent-soft);
226
230
  }
227
231
 
228
232
  .cp__avatar {
@@ -234,14 +238,17 @@ export default {
234
238
  justify-content: center;
235
239
  font-size: 15px;
236
240
  font-weight: 600;
237
- color: var(--on-accent);
238
- background: var(--accent);
241
+ color: var(--text-dim);
242
+ background: var(--bg-hover);
239
243
  }
244
+ /* 分组语义色:布局 = accent、基础 = ok,均用 soft 底 + 同色线性图标,与全局轻风格一致 */
240
245
  .cp__avatar[data-g='layout'] {
241
- background: var(--accent-2);
246
+ color: var(--accent);
247
+ background: var(--accent-soft);
242
248
  }
243
249
  .cp__avatar[data-g='basic'] {
244
- background: var(--ok);
250
+ color: var(--ok);
251
+ background: var(--ok-soft);
245
252
  }
246
253
  .cp__avatar svg {
247
254
  display: block;
@@ -272,14 +279,14 @@ export default {
272
279
  .cp__search {
273
280
  box-sizing: border-box;
274
281
  width: 100%;
275
- height: 32px;
282
+ height: 28px;
276
283
  margin-bottom: 10px;
277
- padding: 0 10px;
278
- font-size: 13px;
284
+ padding: 0 8px;
285
+ font-size: 12px;
279
286
  color: var(--text);
280
287
  background: var(--bg-input);
281
288
  border: 1px solid var(--border);
282
- border-radius: 8px;
289
+ border-radius: 6px;
283
290
  outline: none;
284
291
  }
285
292
  .cp__search:focus {
@@ -287,7 +294,7 @@ export default {
287
294
  }
288
295
  .cp__empty {
289
296
  margin: 8px 0;
290
- color: var(--text-dim);
297
+ color: var(--text-faint);
291
298
  font-size: 12px;
292
299
  text-align: center;
293
300
  }