mini-figma-code-connect 0.1.0 → 0.1.3

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 (61) hide show
  1. package/LICENSE +13 -9
  2. package/README.md +52 -130
  3. package/dist/build.mjs +222 -0
  4. package/dist/figma-mapping.mjs +611 -0
  5. package/dist/generate-registry.mjs +79 -0
  6. package/dist/install-skill.mjs +85 -0
  7. package/dist/main/code.d.ts +1 -0
  8. package/dist/main/code.js +388 -0
  9. package/dist/main/handle.d.ts +55 -0
  10. package/dist/main/handle.js +167 -0
  11. package/dist/main/mapping-table.generated.json +1 -0
  12. package/dist/main/messages.d.ts +71 -0
  13. package/dist/main/messages.js +1 -0
  14. package/dist/main/schema.d.ts +9 -0
  15. package/dist/main/schema.js +39 -0
  16. package/dist/main/validate.d.ts +16 -0
  17. package/dist/main/validate.js +40 -0
  18. package/dist/runtime/define.d.ts +3 -0
  19. package/dist/runtime/define.js +4 -0
  20. package/{src/runtime/index.ts → dist/runtime/index.d.ts} +3 -14
  21. package/dist/runtime/index.js +9 -0
  22. package/dist/runtime/registry.d.ts +5 -0
  23. package/dist/runtime/registry.generated.d.ts +3 -0
  24. package/dist/runtime/registry.generated.js +1 -0
  25. package/{src/runtime/registry.ts → dist/runtime/registry.js} +7 -8
  26. package/dist/runtime/render.d.ts +6 -0
  27. package/dist/runtime/render.js +27 -0
  28. package/dist/runtime/tagged.d.ts +6 -0
  29. package/dist/runtime/tagged.js +45 -0
  30. package/dist/runtime/types.d.ts +107 -0
  31. package/dist/runtime/types.js +7 -0
  32. package/dist/scaffold-manifest.mjs +80 -0
  33. package/dist/scaffold-plugin.mjs +462 -0
  34. package/dist/ui/ui.d.ts +84 -0
  35. package/dist/ui/ui.js +190 -0
  36. package/package.json +23 -18
  37. package/scripts/build-plugin.mjs +0 -132
  38. package/scripts/copy-mapping-table.mjs +0 -19
  39. package/scripts/figma-mapping/ai-generate.mjs +0 -102
  40. package/scripts/figma-mapping/index.mjs +0 -250
  41. package/scripts/figma-mapping/lib.mjs +0 -112
  42. package/scripts/figma-mapping/registry.mjs +0 -12
  43. package/scripts/figma-mapping/scaffold.mjs +0 -153
  44. package/scripts/generate-registry.mjs +0 -94
  45. package/scripts/install-skill.mjs +0 -79
  46. package/scripts/is-cli-entrypoint.mjs +0 -21
  47. package/scripts/scaffold-manifest.mjs +0 -84
  48. package/scripts/scaffold-plugin.mjs +0 -172
  49. package/src/dev/demo.ts +0 -207
  50. package/src/dev/export.ts +0 -28
  51. package/src/main/code.ts +0 -422
  52. package/src/main/handle.ts +0 -207
  53. package/src/main/messages.ts +0 -49
  54. package/src/main/schema.ts +0 -45
  55. package/src/main/validate.ts +0 -49
  56. package/src/runtime/define.ts +0 -6
  57. package/src/runtime/render.ts +0 -26
  58. package/src/runtime/tagged.ts +0 -46
  59. package/src/runtime/types.ts +0 -86
  60. package/src/ui/ui.ts +0 -271
  61. /package/{src → dist}/ui/ui.html +0 -0
@@ -1,45 +0,0 @@
1
- import type { ComponentSchema, FigmaPropertyType, PropertyDef } from '../runtime/types'
2
-
3
- /**
4
- * VARIANT 属性的定义住在 ComponentSet 上,其余属性也一并在那儿
5
- * (ComponentSet 的 componentPropertyDefinitions 同时包含 VARIANT 和 BOOLEAN/TEXT/INSTANCE_SWAP)。
6
- * 所以只取这一个 owner 不会漏属性,后面所有身份(key / name / id)都以它为准。
7
- */
8
- export function definitionOwner(main: ComponentNode): ComponentNode | ComponentSetNode {
9
- return main.parent && main.parent.type === 'COMPONENT_SET' ? main.parent : main
10
- }
11
-
12
- /** Step 3:组件 → 属性 schema */
13
- export async function extractSchema(main: ComponentNode): Promise<ComponentSchema> {
14
- const owner = definitionOwner(main)
15
-
16
- // 注意:这个 getter 在「是 variant 但拿不到所属 ComponentSet」时会 **抛错**,
17
- // 不是返回 undefined —— 所以必须 try/catch,`?? {}` 是无效防护。
18
- let defs: ComponentPropertyDefinitions = {}
19
- try {
20
- defs = owner.componentPropertyDefinitions
21
- } catch {
22
- defs = {}
23
- }
24
-
25
- const properties: PropertyDef[] = Object.keys(defs).map((key) => {
26
- const def = defs[key]
27
- return {
28
- // key 带 "#123:4" 后缀(VARIANT 除外)。只在展示层剥掉,查表时仍用完整 key
29
- name: key.split('#')[0],
30
- type: def.type as FigmaPropertyType,
31
- variantOptions: def.variantOptions ? def.variantOptions.slice() : undefined,
32
- defaultValue:
33
- typeof def.defaultValue === 'string' || typeof def.defaultValue === 'boolean'
34
- ? def.defaultValue
35
- : undefined,
36
- }
37
- })
38
-
39
- return {
40
- componentId: owner.id,
41
- componentKey: owner.key,
42
- componentName: owner.name,
43
- properties,
44
- }
45
- }
@@ -1,49 +0,0 @@
1
- import type { AccessorCall, ComponentSchema } from '../runtime/types'
2
-
3
- export type Finding = { level: 'error' | 'warn'; text: string }
4
-
5
- /**
6
- * Step 6 的自动化版本。
7
- *
8
- * 思路:不让模板重复声明它消费了什么,而是在 render 期间记录每一次 accessor 调用,
9
- * 事后拿这份调用记录和 schema 对账。「穷举缺失」和「属性未映射」就都能被机器发现。
10
- *
11
- * @param calls 真实一遍 render 的调用记录 —— 用来报错
12
- * @param coverage 探测一遍(所有 boolean 视为 true)的调用记录 —— 用来算覆盖率,
13
- * 避免条件分支里的属性被误报成未映射
14
- */
15
- export function validate(
16
- schema: ComponentSchema,
17
- calls: AccessorCall[],
18
- coverage: AccessorCall[] = calls,
19
- ): Finding[] {
20
- const real = calls.filter((c) => c.depth === 0)
21
- const touched = new Set(coverage.filter((c) => c.depth === 0).map((c) => c.prop))
22
- const findings: Finding[] = []
23
-
24
- for (const p of schema.properties) {
25
- if (!touched.has(p.name)) {
26
- findings.push({ level: 'warn', text: `属性 "${p.name}" (${p.type}) 未被模板消费` })
27
- continue
28
- }
29
- if (p.type === 'VARIANT' && p.variantOptions) {
30
- for (const c of real) {
31
- if (c.prop !== p.name || c.method !== 'getEnum' || !c.mappingKeys) continue
32
- const keys = c.mappingKeys
33
- const missing = p.variantOptions.filter((v) => keys.indexOf(v) === -1)
34
- if (missing.length > 0) {
35
- findings.push({
36
- level: 'error',
37
- text: `"${p.name}" 的 getEnum 字典漏了 ${missing.join('、')} —— 选到这些值时会静默产出 undefined`,
38
- })
39
- }
40
- }
41
- }
42
- }
43
-
44
- for (const c of real) {
45
- if (!c.ok) findings.push({ level: 'error', text: `${c.method}("${c.prop}"):${c.note ?? '失败'}` })
46
- }
47
-
48
- return findings
49
- }
@@ -1,6 +0,0 @@
1
- import type { Template } from './types'
2
-
3
- /** 只做类型收窄,让 .figma.ts 里能拿到 accessor 的补全 */
4
- export function defineTemplate(t: Template): Template {
5
- return t
6
- }
@@ -1,26 +0,0 @@
1
- import type { ResultSection } from './types'
2
-
3
- /** 去掉模板字面量带来的公共缩进 */
4
- export function dedent(text: string): string {
5
- const lines = text.replace(/^\n+/, '').replace(/\s+$/, '').split('\n')
6
- const indents = lines.filter((l) => l.trim() !== '').map((l) => /^[ \t]*/.exec(l)![0].length)
7
- const min = indents.length ? Math.min(...indents) : 0
8
- return lines.map((l) => l.slice(min)).join('\n')
9
- }
10
-
11
- /** ResultSection[] → 给人看的字符串。真正的消费端(Dev Mode / MCP)拿的是数组本身 */
12
- export function renderToString(sections: ResultSection[]): string {
13
- let out = ''
14
- for (const s of sections) {
15
- if (s.type === 'CODE') out += s.code
16
- else if (s.type === 'INSTANCE') out += `{/* <${s.name}> 未连接 Code Connect */}`
17
- else out += `/* ⚠ ${s.message} */`
18
- }
19
- return dedent(out)
20
- }
21
-
22
- export function collectErrors(sections: ResultSection[]): string[] {
23
- const out: string[] = []
24
- for (const s of sections) if (s.type === 'ERROR') out.push(s.message)
25
- return out
26
- }
@@ -1,46 +0,0 @@
1
- import type { ResultSection } from './types'
2
-
3
- export type Interpolable = ResultSection[] | string | number | boolean | null | undefined
4
-
5
- /**
6
- * figma.code`...` 的简版。
7
- *
8
- * 职责只有两件:把插值切成 ResultSection[],把嵌套的 section 数组摊平。
9
- * 它 **不解析代码语义** —— 这正是真实 Code Connect 的实现边界,
10
- * 也是为什么用 `+` 或 `.join()` 拼 section 会得到 [object Object]。
11
- */
12
- function tag(strings: TemplateStringsArray, values: Interpolable[]): ResultSection[] {
13
- const out: ResultSection[] = []
14
-
15
- const pushCode = (s: string) => {
16
- if (s === '') return
17
- const last = out[out.length - 1]
18
- if (last && last.type === 'CODE') last.code += s
19
- else out.push({ type: 'CODE', code: s })
20
- }
21
-
22
- strings.forEach((chunk, i) => {
23
- pushCode(chunk)
24
- if (i >= values.length) return
25
- const v = values[i]
26
- // null / undefined / false 一律不输出,方便写条件插值
27
- if (v === null || v === undefined || v === false) return
28
- if (Array.isArray(v)) {
29
- for (const sec of v) {
30
- if (sec.type === 'CODE') pushCode(sec.code)
31
- else out.push(sec)
32
- }
33
- return
34
- }
35
- pushCode(String(v))
36
- })
37
-
38
- return out
39
- }
40
-
41
- export function code(strings: TemplateStringsArray, ...values: Interpolable[]): ResultSection[] {
42
- return tag(strings, values)
43
- }
44
- /** 真实 CC 里这些只影响语法高亮,运行时行为完全一致 */
45
- export const tsx = code
46
- export const html = code
@@ -1,86 +0,0 @@
1
- /**
2
- * 迷你 Code Connect 运行时的类型定义。
3
- *
4
- * 关键一点,也是整套实现的地基:模板的输出不是字符串,而是 ResultSection[]。
5
- * 这样才能在拼装时保留实例身份、并让错误就地降级而不是拖垮整段 snippet。
6
- */
7
-
8
- /** 一段普通代码文本 */
9
- export type CodeSection = { type: 'CODE'; code: string }
10
- /** 「有实例但没有映射」的占位段:保留实例身份,真实 CC 在这里渲染成可展开的 pill */
11
- export type InstanceSection = { type: 'INSTANCE'; guid: string; symbolId: string; name: string }
12
- /** 就地降级的错误段 */
13
- export type ErrorSection = { type: 'ERROR'; message: string }
14
- export type ResultSection = CodeSection | InstanceSection | ErrorSection
15
-
16
- /** Figma 组件属性的封闭类型集(真实 CC 还有 SLOT,简版不做) */
17
- export type FigmaPropertyType = 'TEXT' | 'BOOLEAN' | 'VARIANT' | 'INSTANCE_SWAP'
18
-
19
- export type PropertyDef = {
20
- name: string
21
- type: FigmaPropertyType
22
- variantOptions?: string[]
23
- defaultValue?: string | boolean
24
- }
25
-
26
- /** Step 3 的产出:组件的属性 schema —— 映射的「左手边」 */
27
- export type ComponentSchema = {
28
- componentId: string
29
- componentKey: string
30
- componentName: string
31
- properties: PropertyDef[]
32
- }
33
-
34
- export type TemplateResult = {
35
- example: ResultSection[]
36
- id: string
37
- imports?: string[]
38
- metadata?: { nestable?: boolean; props?: Record<string, unknown> }
39
- }
40
-
41
- /** 头部三行绑定注释的结构化版本(真实 CC 是在构建时解析注释) */
42
- export type TemplateMeta = { url: string; source: string; component: string }
43
-
44
- export type Template = {
45
- meta: TemplateMeta
46
- id: string
47
- /** 怎么认出这个模板对应哪个 Figma 组件:优先 componentKey,退回组件名 */
48
- match: { componentName: string; componentKey?: string }
49
- /**
50
- * 映射的实体是这个函数,不是任何一段固定代码。
51
- * 写一份,覆盖该组件的全部 variant 组合。
52
- */
53
- render: (instance: InstanceLike) => TemplateResult
54
- }
55
-
56
- /** 模板能看到的实例接口(accessor 层) */
57
- export interface InstanceLike {
58
- readonly type: 'INSTANCE'
59
- readonly name: string
60
- getString(prop: string): string
61
- getBoolean(prop: string): boolean
62
- getBoolean<T>(prop: string, mapping: { true: T; false: T }): T
63
- getEnum<T>(prop: string, mapping: Record<string, T>): T | undefined
64
- getInstanceSwap(prop: string): InstanceLike | ErrorLike | null
65
- findInstance(layerName: string): InstanceLike | ErrorLike
66
- findText(layerName: string): TextLike | ErrorLike
67
- hasCodeConnect(): boolean
68
- codeConnectId(): string | null
69
- executeTemplate(): TemplateResult | null
70
- }
71
-
72
- /** 注意 type 是 'ERROR' 而不是 null —— 查找失败返回的是 truthy 的句柄。
73
- * 这是真实 CC 的实现权衡:为了把 message 带下去渲染成 ERROR 段而放弃 null 惯例。
74
- * 代价就是模板里每次都得写 `x.type === 'INSTANCE'` 检查。 */
75
- export interface ErrorLike { readonly type: 'ERROR'; readonly message: string }
76
- export interface TextLike { readonly type: 'TEXT'; readonly name: string; readonly textContent: string }
77
-
78
- /** 一次 accessor 调用的记录,供 Step 6 的自动校验使用 */
79
- export type AccessorCall = {
80
- depth: number
81
- prop: string
82
- method: 'getString' | 'getBoolean' | 'getEnum' | 'getInstanceSwap'
83
- mappingKeys?: string[]
84
- ok: boolean
85
- note?: string
86
- }
package/src/ui/ui.ts DELETED
@@ -1,271 +0,0 @@
1
- type Level = 'error' | 'warn'
2
- type Finding = { level: Level; text: string }
3
- type PropertyDef = { name: string; type: string; variantOptions?: string[] }
4
- type Schema = {
5
- componentId: string
6
- componentKey: string
7
- componentName: string
8
- properties: PropertyDef[]
9
- }
10
- type AccessorCall = { depth: number; prop: string; method: string; ok: boolean; note?: string }
11
- type Candidate = { id: string; name: string; mapped: boolean }
12
- type Msg =
13
- | {
14
- type: 'state'
15
- state: 'empty' | 'unmapped' | 'error'
16
- message: string
17
- schema?: Schema
18
- figmaUrl?: string | null
19
- candidates?: Candidate[]
20
- }
21
- | { type: 'state'; state: 'candidates'; message: string; candidates: Candidate[] }
22
- | {
23
- type: 'state'
24
- state: 'scan'
25
- message: string
26
- groups: {
27
- componentKey: string
28
- componentName: string
29
- count: number
30
- sampleNodeId: string
31
- mapped: boolean
32
- codeComponent: string | null
33
- }[]
34
- }
35
- | {
36
- type: 'state'
37
- state: 'ok'
38
- schema: Schema
39
- template: { id: string; meta: { url: string; source: string; component: string } }
40
- snippet: string
41
- imports: string[]
42
- findings: Finding[]
43
- calls: AccessorCall[]
44
- candidates?: Candidate[]
45
- }
46
- | { type: 'download'; filename: string; json: string }
47
-
48
- const body = document.getElementById('body') as HTMLElement
49
- const send = (type: string, extra?: Record<string, unknown>) =>
50
- parent.postMessage({ pluginMessage: { type, ...extra } }, '*')
51
-
52
- const esc = (s: string) =>
53
- s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
54
-
55
- function schemaTable(schema: Schema, calls: AccessorCall[]): string {
56
- const consumed = new Set(calls.filter((c) => c.depth === 0).map((c) => c.prop))
57
- const rows = schema.properties
58
- .map((p) => {
59
- const tag = consumed.has(p.name)
60
- ? '<span class="tag ok">已映射</span>'
61
- : '<span class="tag miss">未映射</span>'
62
- const opts = p.variantOptions ? esc(p.variantOptions.join(' / ')) : '—'
63
- return `<tr><td>${esc(p.name)}</td><td><code>${p.type}</code></td><td>${opts}</td><td>${tag}</td></tr>`
64
- })
65
- .join('')
66
- return `<table><thead><tr><th>属性</th><th>类型</th><th>可选值</th><th></th></tr></thead><tbody>${rows}</tbody></table>`
67
- }
68
-
69
- function backBar(candidates?: Candidate[]): string {
70
- if (!candidates || candidates.length === 0) return ''
71
- return `<div class="row"><button class="ghost back" type="button">← 返回列表(${candidates.length} 个实例)</button></div>`
72
- }
73
-
74
- function wireBack(): void {
75
- const btn = document.querySelector<HTMLButtonElement>('.back')
76
- if (btn) btn.addEventListener('click', () => send('backToList'))
77
- }
78
-
79
- // 一个按钮身兼两职:候选列表状态(选中的是容器,没有单个实例)导出全部;其它状态导出当前选中的单个实例
80
- const exportSchemaBtn = document.getElementById('export-schema') as HTMLButtonElement
81
- let inCandidatesMode = false
82
- function syncExportButton(state: string, count: number): void {
83
- inCandidatesMode = state === 'candidates'
84
- if (state === 'candidates') {
85
- exportSchemaBtn.disabled = count === 0
86
- exportSchemaBtn.textContent = count > 0 ? `导出全部 schema.json(${count} 个)` : '导出全部 schema.json'
87
- } else {
88
- exportSchemaBtn.disabled = false
89
- exportSchemaBtn.textContent = '导出 schema.json'
90
- }
91
- }
92
-
93
- function render(msg: Msg): void {
94
- if (msg.type !== 'download') {
95
- syncExportButton(msg.state, msg.state === 'candidates' ? msg.candidates.length : 0)
96
- }
97
-
98
- if (msg.type === 'download') {
99
- const url = URL.createObjectURL(new Blob([msg.json], { type: 'application/json' }))
100
- const a = document.createElement('a')
101
- a.href = url
102
- a.download = msg.filename
103
- document.body.appendChild(a)
104
- a.click()
105
- a.remove()
106
- URL.revokeObjectURL(url)
107
- // 兜底:iframe 的 sandbox 若拦掉下载,至少把内容留在剪贴板里
108
- void navigator.clipboard.writeText(msg.json).catch(() => undefined)
109
- return
110
- }
111
-
112
- if (msg.state === 'scan') {
113
- // 未映射排前面(跟 scanPage 排序一致),仿 Figma 官方 Code Connect UI 那张组件目录列表的样式:
114
- // 一行一个组件,左边 Figma 组件名,右边状态 + 代码组件名,顶部一个搜索框过滤
115
- const rows = msg.groups
116
- .map(
117
- (g) => `
118
- <li class="component-row" data-search="${esc(g.componentName.toLowerCase())}">
119
- <button class="row-name" type="button" data-id="${esc(g.sampleNodeId)}">
120
- <span class="diamond">◆</span>
121
- <span>${esc(g.componentName)}</span>
122
- <span class="dim">${g.count} 处</span>
123
- </button>
124
- <span class="row-code">
125
- ${
126
- g.mapped
127
- ? `<span class="status-dot ok">✓</span><code>&lt;${esc(g.codeComponent ?? '?')}&gt;</code>`
128
- : `<span class="status-dot miss">×</span><span class="dim">未映射</span>`
129
- }
130
- </span>
131
- </li>`,
132
- )
133
- .join('')
134
-
135
- body.innerHTML = `
136
- <section>
137
- <p class="hint">${esc(msg.message)}</p>
138
- <input class="scan-search" type="text" placeholder="搜索组件…" />
139
- <div class="component-list">
140
- <div class="list-header"><span>Figma 组件</span><span>代码组件</span></div>
141
- <ul class="component-rows">${rows}</ul>
142
- </div>
143
- </section>
144
- `
145
- body.querySelectorAll<HTMLButtonElement>('.row-name').forEach((el) => {
146
- el.addEventListener('click', () => send('pick', { id: el.dataset.id }))
147
- })
148
- const search = document.querySelector<HTMLInputElement>('.scan-search')
149
- if (search) {
150
- search.addEventListener('input', () => {
151
- const q = search.value.trim().toLowerCase()
152
- body.querySelectorAll<HTMLLIElement>('.component-row').forEach((row) => {
153
- row.style.display = !q || (row.dataset.search ?? '').includes(q) ? '' : 'none'
154
- })
155
- })
156
- }
157
- return
158
- }
159
-
160
- if (msg.state === 'candidates') {
161
- const items = msg.candidates
162
- .map(
163
- (c, i) =>
164
- `<li><button class="ghost pick" type="button" data-id="${esc(c.id)}"><span>${i + 1}. ${esc(
165
- c.name,
166
- )}</span><span class="tag ${c.mapped ? 'ok' : 'miss'}">${c.mapped ? '已映射' : '未映射'}</span></button></li>`,
167
- )
168
- .join('')
169
- body.innerHTML = `<section><p class="hint">${esc(msg.message)}</p><ul class="candidates">${items}</ul></section>`
170
- body.querySelectorAll<HTMLButtonElement>('.pick').forEach((btn) => {
171
- btn.addEventListener('click', () => send('pick', { id: btn.dataset.id }))
172
- })
173
- return
174
- }
175
-
176
- if (msg.state !== 'ok') {
177
- const extra =
178
- msg.schema && msg.schema.properties.length > 0
179
- ? `<section><h2>读到的属性 schema</h2>${schemaTable(msg.schema, [])}</section>`
180
- : ''
181
- // 未映射可能是因为压根没有对应的代码组件——给一条能直接丢进 AI 工具的提示词,
182
- // 先把组件生成出来(组件名跟 Figma 实例名保持一致,方便后面接 mini-code-connect 映射)
183
- const genPrompt =
184
- msg.state === 'unmapped' && msg.schema && msg.figmaUrl
185
- ? `<section>
186
- <div class="row"><h2>生成通用组件</h2><button class="ghost copy" id="copy-prompt" type="button">复制</button></div>
187
- <pre id="gen-prompt">/figma-design-to-code ${esc(msg.figmaUrl)},生成通用组件"${esc(
188
- msg.schema.componentName,
189
- )}",组件名和实例名保持一致</pre>
190
- </section>`
191
- : ''
192
- body.innerHTML = `${backBar(msg.candidates)}<section><p class="hint">${esc(
193
- msg.message,
194
- )}</p></section>${genPrompt}${extra}`
195
- wireBack()
196
- const copyPrompt = document.getElementById('copy-prompt')
197
- if (copyPrompt) {
198
- copyPrompt.addEventListener('click', () => {
199
- const pre = document.getElementById('gen-prompt')
200
- if (!pre || !pre.textContent) return
201
- void navigator.clipboard.writeText(pre.textContent).then(() => {
202
- copyPrompt.textContent = '已复制'
203
- setTimeout(() => (copyPrompt.textContent = '复制'), 1200)
204
- })
205
- })
206
- }
207
- return
208
- }
209
-
210
- const { schema, template, snippet, imports, findings, calls } = msg
211
-
212
- const bindingCard = `
213
- <div class="card">
214
- <div class="kv"><span>Figma 组件</span><span>${esc(schema.componentName)}</span></div>
215
- <div class="kv"><span>source</span><span><code>${esc(template.meta.source)}</code></span></div>
216
- <div class="kv"><span>component</span><span><code>${esc(template.meta.component)}</code></span></div>
217
- <div class="kv"><span>template id</span><span><code>${esc(template.id)}</code></span></div>
218
- </div>`
219
-
220
- const findingList =
221
- findings.length === 0
222
- ? '<p class="hint">没有发现问题。</p>'
223
- : `<ul class="findings">${findings
224
- .map((f) => `<li class="${f.level}">${esc(f.text)}</li>`)
225
- .join('')}</ul>`
226
-
227
- const importLines = imports.length ? esc(imports.join('\n')) + '\n\n' : ''
228
-
229
- body.innerHTML = `
230
- ${backBar(msg.candidates)}
231
- <section><h2>绑定</h2>${bindingCard}</section>
232
- <section>
233
- <div class="row"><h2>生成的代码片段</h2><button class="ghost copy" id="copy" type="button">复制</button></div>
234
- <pre id="snippet">${importLines}${esc(snippet)}</pre>
235
- </section>
236
- <section><h2>属性 schema 与覆盖率</h2>${schemaTable(schema, calls)}</section>
237
- <section><h2>自检(Step 6)</h2>${findingList}</section>
238
- <section><h2>本次 accessor 调用</h2><pre>${esc(
239
- calls
240
- .map((c) => `${' '.repeat(c.depth)}${c.method}("${c.prop}")${c.ok ? '' : ` ⚠ ${c.note ?? ''}`}`)
241
- .join('\n') || '(无)',
242
- )}</pre></section>`
243
-
244
- const copy = document.getElementById('copy')
245
- if (copy) {
246
- copy.addEventListener('click', () => {
247
- const pre = document.getElementById('snippet')
248
- if (!pre || !pre.textContent) return
249
- void navigator.clipboard.writeText(pre.textContent).then(() => {
250
- copy.textContent = '已复制'
251
- setTimeout(() => (copy.textContent = '复制'), 1200)
252
- })
253
- })
254
- }
255
- wireBack()
256
- }
257
-
258
- ;(document.getElementById('refresh') as HTMLButtonElement).addEventListener('click', () => send('refresh'))
259
- ;(document.getElementById('scan') as HTMLButtonElement).addEventListener('click', () => send('scan'))
260
- exportSchemaBtn.addEventListener('click', () => send(inCandidatesMode ? 'exportAllSchemas' : 'exportSchema'))
261
- ;(document.getElementById('export-mapping-table') as HTMLButtonElement).addEventListener('click', () =>
262
- send('exportMappingTable'),
263
- )
264
-
265
- onmessage = (e: MessageEvent) => {
266
- const msg = e.data && e.data.pluginMessage
267
- if (msg) render(msg as Msg)
268
- }
269
-
270
- // UI 就绪后主动拉一次。主线程不做首次 post —— 那会早于这行代码执行,首帧会丢
271
- send('refresh')
File without changes