cardbuddy 0.7.0 → 0.9.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 (36) hide show
  1. package/README.md +21 -0
  2. package/dist/cli.js +5 -0
  3. package/dist/preview-editor/assets/index-CrcrVs4F.css +1 -0
  4. package/dist/preview-editor/assets/index-DnpHHKWl.js +80 -0
  5. package/dist/preview-editor/index.html +2 -2
  6. package/dist/server-core/codeBridge.js +359 -0
  7. package/dist/server-core/index.js +27 -2
  8. package/dist/server-core/logStream.js +275 -0
  9. package/dist/standalone.js +33 -6
  10. package/dist/vendored/App.vue +28 -19
  11. package/dist/vendored/ansi.js +185 -0
  12. package/dist/vendored/api.js +164 -28
  13. package/dist/vendored/components/ApiCallPanel.vue +13 -1
  14. package/dist/vendored/components/ApiPayloadTree.vue +34 -1
  15. package/dist/vendored/components/CardGallery.vue +2 -2
  16. package/dist/vendored/components/ComponentPalette.vue +46 -39
  17. package/dist/vendored/components/ConfirmDialog.vue +2 -2
  18. package/dist/vendored/components/DebugPanel.vue +420 -69
  19. package/dist/vendored/components/EventEditor.vue +29 -3
  20. package/dist/vendored/components/LogValue.vue +2 -1
  21. package/dist/vendored/components/NodeInspector.vue +1 -1
  22. package/dist/vendored/components/PropControl.vue +6 -2
  23. package/dist/vendored/components/RemotePreview.vue +3 -1
  24. package/dist/vendored/components/StructureTree.vue +2 -0
  25. package/dist/vendored/components/setters/SetCode.vue +111 -3
  26. package/dist/vendored/styles/app.css +529 -96
  27. package/dist/vendored/styles/card-gallery.css +4 -11
  28. package/dist/vendored/styles/card-select.css +0 -15
  29. package/dist/vendored/styles/dropdown.css +3 -15
  30. package/dist/vendored/styles/json-tree.css +1 -1
  31. package/dist/vendored/styles/node-inspector.css +204 -131
  32. package/dist/vendored/styles/setters.css +91 -2
  33. package/dist/vendored/styles/structure-tree.css +27 -12
  34. package/package.json +1 -1
  35. package/dist/preview-editor/assets/index-BlscexDE.js +0 -79
  36. package/dist/preview-editor/assets/index-dlbSPb5W.css +0 -1
@@ -11,7 +11,7 @@
11
11
  - 编辑即时(防抖)重建预览(emit apply),「应用」把改动暂存进内存工作副本(currentSchema),落盘由顶栏「保存到本地」负责;不再 emit write-back。
12
12
  -->
13
13
  <template>
14
- <div class="ee-overlay" @click.self="onClose">
14
+ <div class="ee-overlay">
15
15
  <div class="ee-panel">
16
16
  <div class="ee-head">
17
17
  <span class="ee-title">
@@ -29,7 +29,7 @@
29
29
  v-for="evt in eventModel.eventOptions"
30
30
  :key="evt.eventKey"
31
31
  class="ee-evt"
32
- :class="{ 'is-on': isOn(evt) }"
32
+ :class="{ 'is-on': isOn(evt), 'is-sel': activeKey === evt.eventKey }"
33
33
  @click="onSelectEvent(evt)"
34
34
  >
35
35
  <span class="ee-evt-name" :title="evt.eventDesc || evt.eventName || evt.eventKey">{{ evt.eventName || evt.eventKey }}</span>
@@ -43,6 +43,26 @@
43
43
  ><span class="ee-switch__dot"></span></button>
44
44
  </div>
45
45
  </div>
46
+ <!-- 帮助图标:与事件项左右靠边、垂直居中;点击弹出当前动作的使用说明 -->
47
+ <button
48
+ class="ee-help"
49
+ type="button"
50
+ :title="helpOpen ? '关闭使用说明' : '使用说明'"
51
+ @click.stop="helpOpen = !helpOpen"
52
+ >
53
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
54
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
55
+ <circle cx="12" cy="12" r="10"></circle>
56
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
57
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
58
+ </svg>
59
+ </button>
60
+ <div v-if="helpOpen" class="ee-help__mask" @click="helpOpen = false"></div>
61
+ <Transition name="pop">
62
+ <div v-if="helpOpen" class="ee-help__pop" @click.stop>
63
+ <p class="ee-help__text">{{ helpText }}</p>
64
+ </div>
65
+ </Transition>
46
66
  </aside>
47
67
 
48
68
  <section class="ee-detail">
@@ -58,7 +78,6 @@
58
78
  @change="onActionType(activeRow, $event)"
59
79
  />
60
80
  </div>
61
- <p v-if="actionDesc(activeRow.actionType)" class="ee-evt-desc">{{ actionDesc(activeRow.actionType) }}</p>
62
81
  </div>
63
82
 
64
83
  <div class="ee-detail-body">
@@ -108,6 +127,7 @@ export default {
108
127
  return {
109
128
  eventModel: null,
110
129
  activeKey: '',
130
+ helpOpen: false,
111
131
  _t: null,
112
132
  _snapshot: null,
113
133
  _dirty: false,
@@ -127,6 +147,11 @@ export default {
127
147
  if (!this.eventModel || !this.activeKey) return null
128
148
  return this.eventModel.rowForEvent(this.activeKey) || null
129
149
  },
150
+ /** 帮助弹层文案:当前动作类型的使用说明;无选中动作时给通用引导 */
151
+ helpText() {
152
+ const d = this.activeRow ? this.actionDesc(this.activeRow.actionType) : ''
153
+ return d || '先在上方开启事件,再选择动作类型并填写参数;编辑即时预览,「应用」写入历史。'
154
+ },
130
155
  },
131
156
  watch: {
132
157
  comp: {
@@ -141,6 +166,7 @@ export default {
141
166
  const sn = this.schemaNode
142
167
  this.eventModel = sn ? reactive(new EventEditModel(sn)) : null
143
168
  this.activeKey = ''
169
+ this.helpOpen = false
144
170
  this._snapshot = sn ? this.snapshotEventActions(sn) : null
145
171
  this._dirty = false
146
172
  this._written = false
@@ -7,7 +7,8 @@
7
7
  -->
8
8
  <template>
9
9
  <span v-if="!complex" class="log__val" :class="'jt__val--' + typeClass">{{ display }}</span>
10
- <div v-else class="log__json"><ApiPayloadTree :value="value" :max-depth="14" /></div>
10
+ <!-- 日志里的对象/数组参数:根节点默认一行摘要、点击展开,保证日志行密度 -->
11
+ <div v-else class="log__json"><ApiPayloadTree :value="value" :max-depth="14" root-collapsed /></div>
11
12
  </template>
12
13
 
13
14
  <script>
@@ -16,7 +16,7 @@
16
16
  -->
17
17
  <template>
18
18
  <div class="ni-overlay">
19
- <div class="ni-panel">
19
+ <div class="ni-panel modal-card">
20
20
  <div class="ni-head">
21
21
  <span class="ni-title">
22
22
  节点属性 · {{ compName }}
@@ -233,9 +233,13 @@ export default {
233
233
  .pc-body { line-height: 0; }
234
234
  .pc-srcs { display: flex; gap: 4px; line-height: 1.5; }
235
235
  .pc-src {
236
- flex: 1; font-size: 12px; padding: 3px 0; border: 1px solid var(--border);
236
+ flex: 1; height: 24px; font-size: 12px; padding: 0 10px; border: 1px solid var(--border);
237
237
  background: transparent; color: var(--text-dim); border-radius: 6px; cursor: pointer;
238
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
238
239
  }
239
- .pc-src.is-active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
240
+ .pc-src:hover { background: var(--bg-hover); color: var(--text); }
241
+ /* 选中态与分段控件 .st-seg 同语言:浅底 + 同色文字 + 发丝描边,不做实心填充 */
242
+ .pc-src.is-active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
243
+ .pc-src.is-active:hover { background: var(--accent-soft); color: var(--accent); }
240
244
  .pc--readonly .pc-src { opacity: .6; cursor: not-allowed; }
241
245
  </style>
@@ -1,6 +1,7 @@
1
1
  <template>
2
+ <transition name="modal">
2
3
  <div v-if="visible" class="rp-mask" @click.self="$emit('close')">
3
- <div class="rp-card" role="dialog" aria-label="远端预览二维码">
4
+ <div class="rp-card modal-card" role="dialog" aria-label="远端预览二维码">
4
5
  <div class="rp-head">
5
6
  <span class="rp-title">远端预览(真机扫码)</span>
6
7
  <button class="rp-x" type="button" @click="$emit('close')" aria-label="关闭">×</button>
@@ -26,6 +27,7 @@
26
27
  </div>
27
28
  </div>
28
29
  </div>
30
+ </transition>
29
31
  </template>
30
32
 
31
33
  <script>
@@ -172,6 +172,7 @@
172
172
  <line x1="12" y1="17" x2="12" y2="17.01" />
173
173
  </svg>
174
174
  </button>
175
+ <Transition name="pop">
175
176
  <div v-if="helpOpen" class="st__help-pop" @click.stop>
176
177
  <div class="st__help-pop-head">
177
178
  <span class="st__help-pop-title">快捷键</span>
@@ -189,6 +190,7 @@
189
190
  </li>
190
191
  </ul>
191
192
  </div>
193
+ </Transition>
192
194
  </div>
193
195
  </div>
194
196
  </template>
@@ -12,9 +12,40 @@
12
12
  -->
13
13
  <template>
14
14
  <div class="st-code">
15
- <div ref="editor" class="st-code__editor" :class="{ 'is-disabled': disabled }"></div>
15
+ <div
16
+ ref="editor"
17
+ class="st-code__editor"
18
+ :class="{ 'is-disabled': disabled, 'is-bridged': !!bridge }"
19
+ ></div>
20
+ <!-- 右上角语言标签:弱化的只读标识,不参与交互 -->
21
+ <span class="st-code__lang" aria-hidden="true">JS</span>
16
22
  <div class="st-code__foot">
17
23
  <span class="st-code__hint">{{ lines }} 行 · {{ chars }} 字符</span>
24
+ <!-- VSCode 代码桥:生成临时 js 用外部编辑器编辑,保存实时回写;断开即删临时文件 -->
25
+ <span class="st-code__foot-actions">
26
+ <span v-if="bridge" class="st-code__bridge-tag" :title="bridge.file">VSCode 同步中</span>
27
+ <button
28
+ type="button"
29
+ class="st-code__vscode"
30
+ :class="{ 'is-on': !!bridge }"
31
+ :title="
32
+ bridge
33
+ ? '断开 VSCode 桥接并删除临时文件'
34
+ : '在 VSCode 中编辑:保存实时同步回编辑区,断开后清除临时文件'
35
+ "
36
+ :disabled="disabled"
37
+ @click="toggleBridge"
38
+ >
39
+ <!-- 图标左侧小字:未桥接时是动作说明;桥接后由状态徽标接替,避免两段文字挤一行 -->
40
+ <span v-if="!bridge" class="st-code__vscode-label">在编辑器中打开</span>
41
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
42
+ <path
43
+ fill="currentColor"
44
+ d="M23.15 2.587 18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
45
+ />
46
+ </svg>
47
+ </button>
48
+ </span>
18
49
  </div>
19
50
  </div>
20
51
  </template>
@@ -24,13 +55,20 @@ import { CodeJar } from 'codejar'
24
55
  import Prism from 'prismjs'
25
56
  import 'prismjs/components/prism-javascript'
26
57
  import { setterProps, setterEmits } from './props.js'
58
+ import {
59
+ codeBridgeOpen,
60
+ codeBridgeFetch,
61
+ codeBridgeHeartbeat,
62
+ codeBridgeClose,
63
+ subscribeCodeBridge,
64
+ } from '../../api.js'
27
65
 
28
66
  export default {
29
67
  name: 'SetCode',
30
68
  props: setterProps,
31
69
  emits: setterEmits,
32
70
  data() {
33
- return { code: '' }
71
+ return { code: '', bridge: null }
34
72
  },
35
73
  computed: {
36
74
  codeKey() {
@@ -49,6 +87,8 @@ export default {
49
87
  },
50
88
  beforeUnmount() {
51
89
  if (this._t) clearTimeout(this._t)
90
+ // 组件卸载(抽屉关闭 / 切节点 / key 重建)即结束桥接会话,服务端删除临时文件
91
+ this.teardownBridge(true)
52
92
  if (this._jar && this._jar.destroy) this._jar.destroy()
53
93
  },
54
94
  methods: {
@@ -75,7 +115,7 @@ export default {
75
115
  this._jar.onUpdate((code) => this.onCode(code))
76
116
  },
77
117
  onCode(code) {
78
- if (this.disabled) return
118
+ if (this.disabled || this.bridge) return
79
119
  this.code = code
80
120
  // 保留 compiled 等未知键,只覆写 codeKey
81
121
  const base = this.value && typeof this.value === 'object' ? Object.assign({}, this.value) : {}
@@ -85,6 +125,74 @@ export default {
85
125
  if (this._t) clearTimeout(this._t)
86
126
  this._t = setTimeout(() => this.$emit('change'), 400)
87
127
  },
128
+ /* ---------------- VSCode 代码桥 ---------------- */
129
+ toggleBridge() {
130
+ if (this.bridge) this.teardownBridge(true)
131
+ else this.setupBridge()
132
+ },
133
+ async setupBridge() {
134
+ if (this.disabled || this.bridge) return
135
+ const label = this.itemInfo && this.itemInfo.itemKey ? String(this.itemInfo.itemKey) : ''
136
+ try {
137
+ const info = await codeBridgeOpen(this.code, label)
138
+ this.bridge = { id: info.id, file: info.file }
139
+ } catch (e) {
140
+ console.error('[VSCode 桥接建立失败]', e)
141
+ return
142
+ }
143
+ // 桥接期间 Web 编辑区只读:外部编辑器为唯一写入源,避免双向冲突
144
+ this.setEditorEditable(false)
145
+ this._hb = setInterval(() => {
146
+ if (!this.bridge) return
147
+ codeBridgeHeartbeat(this.bridge.id).catch(() => {})
148
+ }, 5000)
149
+ this._bridgeUnsub = subscribeCodeBridge((payload) => {
150
+ if (!this.bridge || !payload || payload.id !== this.bridge.id) return
151
+ // code-bridge-closed 带 reason(ttl / client):服务端已清理,本地同步收 UI
152
+ if (payload.reason) return this.teardownBridge(false)
153
+ this.pullBridgeCode()
154
+ })
155
+ },
156
+ async pullBridgeCode() {
157
+ if (!this.bridge) return
158
+ let r
159
+ try {
160
+ r = await codeBridgeFetch(this.bridge.id)
161
+ } catch (e) {
162
+ return // 会话已失效:等 closed 事件或 TTL 收尾
163
+ }
164
+ const code = String(r.code == null ? '' : r.code)
165
+ if (code === this.code) return
166
+ this.applyExternalCode(code)
167
+ },
168
+ applyExternalCode(code) {
169
+ this.code = code
170
+ // callOnUpdate=false:不触发 onUpdate,切断「回写 → input → 再写临时文件」回声环
171
+ if (this._jar) this._jar.updateCode(code, false)
172
+ const base = this.value && typeof this.value === 'object' ? Object.assign({}, this.value) : {}
173
+ base[this.codeKey] = code
174
+ this.$emit('input', base)
175
+ if (this._t) clearTimeout(this._t)
176
+ this._t = setTimeout(() => this.$emit('change'), 400)
177
+ },
178
+ teardownBridge(notifyServer) {
179
+ const b = this.bridge
180
+ this.bridge = null
181
+ if (this._hb) {
182
+ clearInterval(this._hb)
183
+ this._hb = null
184
+ }
185
+ if (this._bridgeUnsub) {
186
+ this._bridgeUnsub()
187
+ this._bridgeUnsub = null
188
+ }
189
+ this.setEditorEditable(true)
190
+ if (b && notifyServer) codeBridgeClose(b.id).catch(() => {})
191
+ },
192
+ setEditorEditable(on) {
193
+ const el = this.$refs.editor
194
+ if (el) el.setAttribute('contenteditable', on ? 'true' : 'false')
195
+ },
88
196
  },
89
197
  }
90
198
  </script>