base-paint 0.3.0 → 0.3.2
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.
- package/README.md +8 -0
- package/dist/blocks.d.ts +143 -6
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +1060 -35
- package/dist/blocks.js.map +1 -1
- package/dist/charts.d.ts +6 -4
- package/dist/charts.d.ts.map +1 -1
- package/dist/charts.js +378 -64
- package/dist/charts.js.map +1 -1
- package/dist/controls.d.ts +20 -5
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +246 -34
- package/dist/controls.js.map +1 -1
- package/dist/font.d.ts +12 -0
- package/dist/font.d.ts.map +1 -0
- package/dist/font.js +12 -0
- package/dist/font.js.map +1 -0
- package/dist/help.d.ts +3 -3
- package/dist/help.js +12 -12
- package/dist/help.js.map +1 -1
- package/dist/helpShell.d.ts +57 -0
- package/dist/helpShell.d.ts.map +1 -0
- package/dist/helpShell.js +65 -0
- package/dist/helpShell.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/injector.d.ts +1 -1
- package/dist/output/saveHtml.d.ts +64 -0
- package/dist/output/saveHtml.d.ts.map +1 -0
- package/dist/output/saveHtml.js +339 -0
- package/dist/output/saveHtml.js.map +1 -0
- package/dist/pageShapes.d.ts +117 -0
- package/dist/pageShapes.d.ts.map +1 -0
- package/dist/pageShapes.js +366 -0
- package/dist/pageShapes.js.map +1 -0
- package/dist/pageUi.d.ts +37 -0
- package/dist/pageUi.d.ts.map +1 -0
- package/dist/pageUi.js +237 -0
- package/dist/pageUi.js.map +1 -0
- package/dist/spec/charts.d.ts +12 -3
- package/dist/spec/charts.d.ts.map +1 -1
- package/dist/spec/charts.js +1 -1
- package/dist/spec/charts.js.map +1 -1
- package/dist/spec/controls.d.ts +30 -5
- package/dist/spec/controls.d.ts.map +1 -1
- package/dist/spec/controls.js +14 -3
- package/dist/spec/controls.js.map +1 -1
- package/dist/spec/help.d.ts +4 -4
- package/dist/spec/help.d.ts.map +1 -1
- package/dist/spec/help.js +2 -2
- package/dist/spec/index.d.ts +6 -3
- package/dist/spec/index.d.ts.map +1 -1
- package/dist/spec/index.js +27 -2
- package/dist/spec/index.js.map +1 -1
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +283 -55
- package/dist/style.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** #136 · help模板新家(base-paint 子路径 `base-paint/help-shell`,不进主入口)。
|
|
2
|
+
*
|
|
3
|
+
* 来源:`packages/base-render/assets/help-template.html`(help模板唯一真相源;人类/AI 可读可编辑,3 槽注释+前缀 title 占位即契约)。
|
|
4
|
+
* 生成方式:本文件由 `node packages/base-render/scripts/gen-help-shell.cjs` 机器生成,禁止手工改动
|
|
5
|
+
* (含换行与转义);改模板只改源→跑 `pnpm --filter base-paint gen:help-shell`→过门(字节锁/像素门/单测)。
|
|
6
|
+
* 生成器断言:源按 help-data 开标签尾/配对闭标签切分得 PREFIX/SUFFIX,中段须含 3 槽注释、PREFIX 须含 title 占位;
|
|
7
|
+
* 产出全文过 base-paint 纯度口径(`--check` 在 CI/本地校验 drift,重跑不一致即非 0)。
|
|
8
|
+
* 纯度注记:旧壳 SUFFIX 是冻结的页面侧遗留运行时,文本内含 `window` 赋值;字面量在命中点内
|
|
9
|
+
* 以 `"a" + "b"` 切块(运行时拼接值不变,源码面无连续命中串),故本文件常量为多段拼接而非单字面量。
|
|
10
|
+
* 子路径理由(沿 `src/blocks.ts` 防火墙惯例):主入口 `src/index.ts` 受冻结面出口锁
|
|
11
|
+
* (`test/contract-signatures.test.mjs`:新增运行时出口须等于清单 implemented 项),且既有
|
|
12
|
+
* `src/help.ts:renderHelpShell(input: HelpShellInput)` 同名不同参——本 help模板走子路径,不污染冻结面。
|
|
13
|
+
*
|
|
14
|
+
* 接入示例(其他技能照抄即接,只传自家 HELP JSON):
|
|
15
|
+
* import { renderHelpShell } from 'base-paint/help-shell';
|
|
16
|
+
* const html = renderHelpShell({ skill_name, title, subtitle, contact, groups });
|
|
17
|
+
* // groups 为空即抛 HelpShellError(code 'missing-data',不返空页);
|
|
18
|
+
* // JSON 小于号转义防 script 破壳,parse 后逐字一致。
|
|
19
|
+
*/
|
|
20
|
+
/** help模板渲染失败(空分组缺数据;调用方按 `code` 判定,不做 instanceof 跨包断言)。 */
|
|
21
|
+
export class HelpShellError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'HelpShellError';
|
|
26
|
+
this.code = 'missing-data';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** 壳前缀(源首行至 help-data 开标签尾,verbatim;改源跑 gen,禁手工改)。 */
|
|
30
|
+
export const HELP_SHELL_PREFIX = "<!DOCTYPE html>\r\n<html lang=\"zh-CN\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n<title>__HELP_TITLE__</title>\r\n<style>\r\n *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}\r\n :root{--fg:#1d1d1f;--fg2:#6e6e73;--fg3:#86868b;--bg:#f2f2f7;--card:#fff;--line:#d1d1d6;--blue:#007aff;--blue2:#0a63ce;--soft:#f0f6ff;--ok:#34c759;--orange:#ff9500;--red:#ff3b30;--shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(0,0,0,.05)}\r\n body{font-family:-apple-system,BlinkMacSystemFont,\"PingFang SC\",\"Microsoft YaHei\",sans-serif;background:var(--bg);color:var(--fg);line-height:1.5;-webkit-font-smoothing:antialiased;font-size:14px}\r\n button{font-family:inherit}\r\n .stage{min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:24px 16px 60px;background:var(--bg)}\r\n .phone{width:680px;max-width:100%;height:auto;background:transparent;border-radius:0;border:0;overflow:visible;position:relative;box-shadow:none}\r\n .screen{height:auto;overflow:visible;-webkit-overflow-scrolling:touch;background:transparent;scrollbar-width:none;overscroll-behavior-y:auto}\r\n .screen::-webkit-scrollbar{display:none}\r\n .notch{display:none}\r\n .app{padding:12px 0 74px}\r\n .app-inner{padding:0 6px}\r\n .hero{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:11px 12px;margin-bottom:8px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}\r\n .h-left{flex:1;min-width:0}\r\n .eyebrow{font-size:9px;color:var(--blue);font-weight:800;letter-spacing:.07em;text-transform:uppercase}\r\n h1{font-size:19px;line-height:1.15;letter-spacing:-.02em}\r\n .lead{color:var(--fg2);font-size:11.5px}\r\n .hero-steps{display:flex;align-items:center;gap:4px;flex-basis:100%;margin-top:6px}\r\n .hero-steps .step{background:var(--soft);border:1px solid #d7e6ff;color:var(--blue2);font-weight:700;font-size:10.5px;padding:3px 10px;border-radius:999px;white-space:nowrap}\r\n .hero-steps .arrow{color:var(--blue);font-weight:800;font-size:11px}\r\n .h-badge{background:var(--soft);border:1px solid #d7e6ff;color:var(--blue2);font-size:11px;font-weight:700;padding:4px 10px;border-radius:999px;white-space:nowrap;flex-shrink:0}\r\n /* ===== D4 首次使用横幅(可配 · 未初始化时显示 · 可手动关闭)===== */\r\n .init-banner{background:linear-gradient(135deg,#0a63ce,#007aff 55%,#34c759);color:#fff;border-radius:12px;padding:12px 14px;margin-bottom:8px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;position:relative}\r\n .init-banner .ib-text{flex:1;min-width:200px;padding-right:26px}\r\n .init-banner .ib-title{font-size:14px;font-weight:800;margin-bottom:2px}\r\n .init-banner .ib-sub{font-size:11.5px;opacity:.9;line-height:1.5}\r\n .init-banner .copy-btn{background:#fff;color:var(--blue);min-height:30px;font-weight:700;border-radius:999px;padding:0 11px;font-size:11.5px;flex-shrink:0;margin-right:26px}\r\n .init-banner .ib-close{position:absolute;top:8px;right:8px;width:22px;height:22px;border-radius:50%;border:0;background:rgba(255,255,255,.22);color:#fff;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;flex-shrink:0}\r\n /* 初始化步骤卡(可选 · 技能传 steps 显示 · >4 步横向滑动)*/\r\n .init-steps{display:flex;gap:6px;flex-basis:100%;margin-top:4px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px}\r\n .init-steps::-webkit-scrollbar{display:none}\r\n .init-step{flex:0 0 calc(25% - 4.5px);background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.28);border-radius:10px;padding:7px 5px;text-align:center;min-width:0}\r\n .init-step .s-n{width:20px;height:20px;border-radius:50%;background:#fff;color:var(--blue2);font-weight:800;font-size:10px;display:flex;align-items:center;justify-content:center;margin:0 auto 4px}\r\n .init-step .s-t{font-size:9.5px;font-weight:700;color:#fff;line-height:1.3;word-break:break-all}\r\n .init-step .s-d{font-size:8px;color:rgba(255,255,255,.75);margin-top:1px;line-height:1.3;word-break:break-all}\r\n /* ===== 关于 Tab(最后 · 域概览 + 联系作者 + 版本)===== */\r\n .about-sec{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-bottom:8px;overflow:hidden}\r\n .about-sec .as-head{padding:9px 12px;font-size:13px;font-weight:800;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:7px}\r\n .about-sec .as-body{padding:4px 12px 8px}\r\n .about-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12.5px}\r\n .about-row:last-child{border-bottom:0}\r\n .about-row .a-ico{width:30px;height:30px;border-radius:9px;background:var(--soft);color:var(--blue2);display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}\r\n .about-row .a-ico svg{width:17px;height:17px;display:block}\r\n .about-row .a-t{flex:1;min-width:0}\r\n .about-row .a-t b{font-size:12.5px;display:block}\r\n .about-row .a-t span{font-size:10.5px;color:var(--fg3)}\r\n .about-row .a-badge{font-size:9.5px;color:var(--fg3);background:var(--bg);border-radius:999px;padding:1px 8px;flex-shrink:0}\r\n .about-row .a-link{color:var(--blue);font-size:10.5px;word-break:break-all}\r\n .about-row .a-copy{min-height:28px;padding:0 10px;font-size:10.5px;border-radius:8px;flex-shrink:0}\r\n /* ===== D4 联系作者(可配)===== */\r\n .contact-box{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-top:10px;overflow:hidden}\r\n .contact-box>summary{min-height:44px;padding:0 13px;cursor:pointer;font-size:13.5px;font-weight:700;list-style:none;display:flex;align-items:center;gap:8px;user-select:none}\r\n .contact-box>summary::-webkit-details-marker{display:none}\r\n .contact-box>summary::before{content:\"▸\";font-size:11px;color:var(--fg3);transition:transform .15s;flex-shrink:0}\r\n .contact-box[open]>summary::before{transform:rotate(90deg);color:var(--blue)}\r\n .contact-body{padding:4px 13px 12px}\r\n .contact-item{display:flex;gap:10px;align-items:center;font-size:12.5px;color:var(--fg2);padding:7px 0;border-bottom:1px dashed var(--line)}\r\n .contact-item:last-child{border-bottom:0}\r\n .contact-item .k{font-weight:700;color:var(--fg);min-width:64px;flex-shrink:0}\r\n .contact-item a{color:var(--blue);text-decoration:none;word-break:break-all}\r\n .contact-copy{margin-top:10px}\r\n .search-wrap{position:sticky;top:0;z-index:400;background:rgba(242,242,247,.92);-webkit-backdrop-filter:saturate(180%) blur(14px);backdrop-filter:saturate(180%) blur(14px);padding:5px 0;margin-bottom:2px}\r\n .search-box{position:relative}\r\n .search-box input{width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 34px 9px 34px;font-size:13.5px;background:#fff;outline:none}\r\n .search-box input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(0,122,255,.13)}\r\n .s-icon{position:absolute;left:11px;top:50%;transform:translateY(-50%);font-size:13px;color:var(--fg3);pointer-events:none}\r\n .s-clear{position:absolute;right:9px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;border:0;background:var(--bg);color:var(--fg3);font-size:12px;cursor:pointer;display:none;align-items:center;justify-content:center;line-height:1}\r\n .hitcount{font-size:11px;color:var(--fg2);margin-bottom:5px;text-align:center}\r\n .search-empty{background:var(--card);border:1.5px dashed var(--line);border-radius:12px;padding:20px 12px;text-align:center;color:var(--fg3);font-size:12.5px}\r\n .chip{display:inline-block;background:var(--soft);color:var(--blue2);padding:1px 7px 1px 0;border-radius:999px;font-size:9.5px;font-weight:700;flex-shrink:0;line-height:1.55}\r\n .type-badge{display:inline-block;padding:1px 7px;border-radius:999px;font-size:9px;font-weight:700;flex-shrink:0;background:#e9e9ee;color:#666;line-height:1.55}\r\n .t-view{background:#e8f2ff;color:#0a63ce}\r\n .t-wizard{background:#e2f7f5;color:#00897b}\r\n .t-collect{background:#e7f8ee;color:#1a7a3a}\r\n .t-result{background:#e8f2ff;color:#0a63ce}\r\n .t-batch{background:#f3e9fb;color:#8e3fc9}\r\n .t-dev{background:#fff1e2;color:#b25b00}\r\n .copy-btn{display:inline-flex;align-items:center;justify-content:center;border:0;background:var(--blue);color:#fff;border-radius:8px;padding:0 9px;font-size:11.5px;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0;min-height:36px;transition:background .12s}\r\n .copy-btn:active{background:var(--blue2)}\r\n .copy-btn.ghost{background:#fff;color:var(--blue);border:1px solid var(--blue)}\r\n mark{background:#ffe08a;border-radius:3px;padding:0 1px;color:inherit}\r\n\r\n /* ===== 横向滑动容器(ViewPager)===== */\r\n .pages{display:flex;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;scrollbar-width:none;scroll-behavior:smooth;align-items:flex-start}\r\n .pages::-webkit-scrollbar{display:none}\r\n .page{flex:0 0 100%;scroll-snap-align:start}\r\n\r\n /* ===== 二级:子功能折叠组 ===== */\r\n .page{display:flex;flex-direction:column}\r\n .subgroup{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-bottom:8px;overflow:hidden;display:flex;flex-direction:column}\r\n .subgroup>summary{min-height:44px;padding:0 12px;display:flex;align-items:center;gap:8px;list-style:none;cursor:pointer;font-size:13.5px;font-weight:700;user-select:none;flex-shrink:0}\r\n .subgroup>summary::-webkit-details-marker{display:none}\r\n .subgroup>summary::before{content:\"▸\";font-size:11px;color:var(--fg3);transition:transform .15s;flex-shrink:0}\r\n .subgroup[open]>summary::before{transform:rotate(90deg);color:var(--blue)}\r\n .subgroup .sg-count{font-size:10px;color:var(--fg3);font-weight:600;margin-left:auto;background:var(--bg);border-radius:999px;padding:1px 8px}\r\n .subgroup .sg-body{padding:7px 8px 9px;display:flex;flex-direction:column;flex:1;min-height:0}\r\n .subgroup:not([open]) .sg-body{display:none}\r\n\r\n /* ===== 三级:2 列场景卡片 ===== */\r\n .grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}\r\n .mini{background:var(--bg);border:1px solid var(--line);border-radius:12px;padding:9px;display:flex;flex-direction:column;gap:6px;cursor:pointer;box-shadow:none;transition:transform .08s;min-width:0;overflow:hidden}\r\n .mini:active{transform:scale(.985);background:#eaeaef}\r\n .mini .m-top{display:flex;align-items:center;justify-content:flex-end;gap:2px;min-height:18px;flex-wrap:wrap}\r\n .mini .m-top .chip{max-width:65%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;margin-right:auto}\r\n .mini .m-top .type-badge{flex-shrink:0;white-space:nowrap}\r\n .mini .m-bottom{display:flex;align-items:center;gap:8px;min-width:0}\r\n .mini .m-name{flex:1;min-width:0;font-size:12.5px;font-weight:700;line-height:1.35;word-break:break-all;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\r\n .mini .copy-btn{min-height:26px;padding:0 9px;font-size:11px;border-radius:9px;flex-shrink:0}\r\n\r\n /* ===== 底部 Tab(可横向滑动)===== */\r\n .tab-bar{position:fixed;left:0;right:0;bottom:0;background:rgba(255,255,255,.94);-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px);border-top:1px solid var(--line);display:flex;z-index:600;padding:6px 8px calc(6px + env(safe-area-inset-bottom,0));gap:6px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;touch-action:pan-x}\r\n /* 宽屏: Tab 栏宽度对齐主体内容区(居中限宽, 不横跨全屏); 手机端保持全宽(见 media query)*/\r\n @media(min-width:501px){\r\n .tab-bar{width:680px;max-width:100%;left:50%;transform:translateX(-50%);border-left:1px solid var(--line);border-right:1px solid var(--line);border-radius:0}\r\n }\r\n .tab-bar::-webkit-scrollbar{display:none}\r\n .tab{flex:0 0 auto;min-width:66px;display:flex;flex-direction:column;align-items:center;gap:3px;padding:5px 10px 4px;cursor:pointer;font-size:10px;color:var(--fg3);font-weight:600;min-height:54px;justify-content:center;border:0;background:none;border-radius:14px;transition:background .08s,color .08s,box-shadow .08s;white-space:nowrap}\r\n .tab .t-ico{width:22px;height:22px;display:flex;align-items:center;justify-content:center;line-height:1}\r\n .tab .t-ico svg{width:20px;height:20px;display:block}\r\n .tab .t-ico .t-emoji{font-size:16px;line-height:1}\r\n .tab.active{background:var(--blue);color:#fff;box-shadow:0 4px 12px rgba(0,122,255,.3)}\r\n .tab.active .t-ico{transform:scale(1.05)}\r\n\r\n /* ===== Sheet 弹层(高度自适应内容 · 不满屏不滚动)===== */\r\n .sheet-mask{position:fixed;left:8px;right:8px;top:8px;bottom:8px;background:rgba(0,0,0,.45);opacity:0;pointer-events:none;transition:opacity .25s;z-index:900;border-radius:36px}\r\n .sheet-mask.show{opacity:1;pointer-events:auto}\r\n .sheet{position:fixed;left:8px;right:8px;bottom:8px;background:var(--card);border-radius:22px 22px 36px 36px;padding:10px 16px calc(8px + env(safe-area-inset-bottom,0));transform:translateY(105%);transition:transform .32s cubic-bezier(.32,.72,.24,1);z-index:901;max-height:82%;display:flex;flex-direction:column;box-shadow:0 -8px 40px rgba(0,0,0,.25)}\r\n .sheet.show{transform:translateY(0)}\r\n .sheet .grip{width:38px;height:4.5px;border-radius:999px;background:var(--line);margin:0 auto 12px;flex-shrink:0}\r\n .sheet .s-scroll{overflow-y:auto;flex:1;min-height:0;scrollbar-width:none;padding-bottom:66px}\r\n .sheet .s-scroll::-webkit-scrollbar{display:none}\r\n .sheet .s-head{display:flex;align-items:center;gap:3.5px;flex-wrap:wrap;margin-bottom:4px}\r\n .sheet .s-title{font-size:17px;font-weight:700;flex-basis:100%;line-height:1.3}\r\n .sheet .s-close{position:absolute;top:16px;right:16px;width:30px;height:30px;border-radius:50%;border:0;background:var(--bg);color:var(--fg3);font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center}\r\n .sheet .prompt-box{background:#1d1d1d;color:#f5f5f7;border-radius:12px;padding:11px 13px;white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.65;margin-top:9px;word-break:break-all}\r\n .sheet .pform{margin-top:9px;background:var(--bg);border:1px solid var(--line);border-radius:12px;padding:9px 10px}\r\n .sheet .pfield{display:flex;align-items:center;gap:7px;margin-bottom:7px}\r\n .sheet .pfield:last-of-type{margin-bottom:0}\r\n .sheet .pfield label{font-size:12px;color:var(--fg2);font-weight:600;flex-shrink:0;min-width:72px}\r\n .sheet .pfield label em{color:var(--fg3);font-weight:400;font-size:9px}\r\n .sheet .pfield input{flex:1;min-width:0;border:1px solid var(--line);border-radius:9px;padding:8px 10px;font-size:13px;background:#fff;outline:none;font-family:inherit;color:var(--fg)}\r\n .sheet .pfield input:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(0,122,255,.12)}\r\n /* 未填时:灰淡背景 + 提示文字(placeholder 由技能方提供)*/\r\n .sheet .pfield input:placeholder-shown{background:#f2f2f4;border-color:#e0e0e4;color:#8e8e93}\r\n .sheet .pfield input:placeholder-shown:focus{background:#fff;border-color:var(--blue)}\r\n .sheet .pprev{margin-top:9px;background:#1d1d1d;color:#f5f5f7;border-radius:12px;padding:11px 13px;font-size:12px;line-height:1.65;word-break:break-all;white-space:pre-wrap;font-family:ui-monospace,Menlo,monospace}\r\n .sheet .pprev .pv-label{font-size:9px;color:#8e8e93;display:block;letter-spacing:.05em;margin-bottom:4px}\r\n .sheet .s-actions{position:absolute;left:12px;right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0));display:flex;gap:8px;flex-shrink:0}\r\n .sheet .s-actions .copy-btn{flex:1;min-height:48px;font-size:13px;font-weight:600;border-radius:12px;background:var(--blue);color:#fff;border:0;box-shadow:none}\r\n @media(max-width:500px){\r\n body{background:var(--bg)}\r\n .stage{padding:0;background:var(--bg)}\r\n .phone{width:100%;height:100vh;border:0;border-radius:0;max-height:none;background:var(--bg);overflow:hidden}\r\n .screen{height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch;background:var(--bg);overscroll-behavior-y:contain}\r\n .notch{display:none}\r\n .sheet-mask{left:0;right:0;top:0;bottom:0;border-radius:0}\r\n .sheet{left:0;right:0;bottom:0;border-radius:22px 22px 0 0}\r\n }\r\n</style>\r\n</head>\r\n<body>\r\n\r\n<div class=\"stage\">\r\n\r\n <div class=\"phone\"><div class=\"notch\"></div><div class=\"screen\" id=\"screen\"></div></div>\r\n\r\n <div class=\"sheet-mask\" id=\"sheetMask\"></div>\r\n <div class=\"sheet\" id=\"sheet\">\r\n <div class=\"grip\"></div>\r\n <button class=\"s-close\" id=\"sheetClose\">✕</button>\r\n <div class=\"s-scroll\">\r\n <div class=\"s-head\" id=\"shHead\"></div>\r\n <div id=\"shBody\"></div>\r\n </div>\r\n <div class=\"s-actions\">\r\n <button class=\"copy-btn\" id=\"shCopy\">复制</button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"toast\" id=\"toast\" style=\"display:none\"></div>\r\n</div>\r\n\r\n<!-- 公共组件注入管线 -->\r\n<script id=\"help-data\" type=\"application/json\">";
|
|
31
|
+
/** 壳后缀(help-data 配对 script 闭标签至源文末,含 helpers+共享 CSS+运行时,verbatim;改源跑 gen,禁手工改)。 */
|
|
32
|
+
export const HELP_SHELL_SUFFIX = "</script>\r\n<script>\r\n/* Base Skill 控件库 v1.2(唯一真相源 · 跨技能 · 领域无关)\r\n * 契约: docs/component-contract.md v1.2\r\n * v1.2 核心(#269 试点用户拍板):\r\n * - snapshot 结构化接口: buildDataText/buildLogText 领域无关(title/summary/sections)\r\n * - toast 通用提示控件: 4 形态(徽章/操作/计数/留空)+ 队列 + 图标库 + 多操作 + 富详情 + 无障碍\r\n * - 复制按钮控件化: 复制数据/日志 = Base 控件;预览/格式选择/脱敏/导出\r\n * - 新控件 P0+P1: formPrompt/selectList/confirm/foldBox/statusBadge/emptyState/errorReceipt\r\n * - 结构校验违规直接报错(硬拦截)\r\n * 注入点: SHARED-HELPERS 占位符(由 injector.py 替换本文件)\r\n * 修改必须走公共层 ISSUE(总纲 09)+ CHANGELOG(见 docs/component-contract.md §8)\r\n */\r\n\r\n/* ── P0 守卫组 ── */\r\nfunction esc(s){return String(s??'').replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));}\r\nfunction arr(v){return Array.isArray(v)?v:[];}\r\nfunction val(v){return v===null||v===undefined||v===''?'<span style=\"color:var(--fg3)\">未填写</span>':esc(v);}\r\nfunction yes(v){return v?'<span class=\"ok\">通过</span>':'<span class=\"bad\">未通过</span>';}\r\nfunction validate(p){if(!p||typeof p!=='object')return{ok:false,msg:'数据未注入'};if(p.status!=='ok')return{ok:false,msg:p.message||'数据状态非 ok'};if(!p.data||typeof p.data!=='object')return{ok:false,msg:'data 缺失'};return{ok:true};}\r\nfunction _fbCopy(s){var ok=false;try{var t=document.createElement('textarea');t.value=s;t.style.cssText='position:fixed;left:-9999px;top:0;opacity:0';document.body.appendChild(t);t.focus();t.select();ok=document.execCommand('copy');document.body.removeChild(t);}catch(e){ok=false;}return ok;}\r\n\r\n/* ── 复制动作 v2(不改按钮文字 + toast 反馈 · v1.10 #328 文案钩子)──\r\n * copyText(s, opts?)\r\n * opts: { silent, toast:{ok:{msg,detail,icon?}, fail:{msg,detail,icon?}}, onOk, onFail }\r\n * - toast 文案配置: 只覆盖提供的字段, 缺省回落默认文案(08 规范: 文案由技能按场景自设计)\r\n * - onOk/onFail 回调: 成功/最终失败必触发; silent 时不弹 toast 仍触发回调\r\n * - 未传新选项: 行为与 v1.9 逐字一致(向后兼容)\r\n */\r\nfunction copyText(s, opts){\r\n opts = opts || {};\r\n if(!s) return;\r\n var t = opts.toast || {}, okCfg = t.ok || {}, failCfg = t.fail || {};\r\n var okMsg = okCfg.msg !== undefined ? okCfg.msg : '已复制';\r\n var okDetail = okCfg.detail !== undefined ? okCfg.detail : '粘贴给 AI';\r\n var failMsg = failCfg.msg !== undefined ? failCfg.msg : '复制失败';\r\n var failDetail = failCfg.detail !== undefined ? failCfg.detail : '长按选择文本手动复制';\r\n function ok(){\r\n if(opts.onOk) opts.onOk();\r\n if(!opts.silent){\r\n var o = {};\r\n if(okCfg.icon) o.icon = okCfg.icon;\r\n toast(okMsg, okDetail, Object.keys(o).length ? o : undefined);\r\n }\r\n }\r\n function fail(){\r\n if(opts.onFail) opts.onFail();\r\n if(!opts.silent){\r\n var o = {badge:{text:'失败',type:'danger'}};\r\n if(failCfg.icon) o.icon = failCfg.icon;\r\n toast(failMsg, failDetail, o);\r\n }\r\n }\r\n if(navigator.clipboard&&navigator.clipboard.writeText){\r\n try{\r\n var pr=navigator.clipboard.writeText(s);\r\n if(pr&&pr.then){pr.then(function(){ok();}).catch(function(){_fbCopy(s)?ok():fail();});}\r\n else{ok();}\r\n }catch(e){_fbCopy(s)?ok():fail();}\r\n }else{_fbCopy(s)?ok():fail();}\r\n}\r\n\r\n/* ── toast 通用提示控件 v1.8(堆叠模式 · #304)──\r\n * toast(msg, detail?, options?)\r\n * options: { icon, badge:{text,type}, actions:[{label,onClick}], count, lines:[], code, timeout, maxStack }\r\n * 向后兼容: toast(msg, detail) = 等价 v1.1\r\n * 堆叠模式(v1.8 · #304 用户拍板): 同屏最多 N 条同时可见(N 默认 5, opts.maxStack 可配,\r\n * 栈容量取栈内各 toast maxStack 最大值; ≤820px 视口自动收窄为 3); 老上旧下(新 toast\r\n * 贴屏幕底部出现, 旧的向上顶, 间距 8px); 超 N 挤掉最旧(FIFO); 单条独立计时消失\r\n */\r\n(function(){\r\n var stack = null;\r\n var styleInjected = false;\r\n var DEFAULT_MAX = 5;\r\n var MOBILE_MAX = 3;\r\n var ICONS = { copy:'📋', ok:'✅', warn:'⚠️', danger:'❌', info:'💡' };\r\n var CSS = '.hm-toast-stack{position:fixed;left:50%;bottom:calc(24px + env(safe-area-inset-bottom,0));transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;z-index:9999;pointer-events:none}.hm-toast{position:relative;transform:scale(.9);opacity:0;background:rgba(28,28,30,.94);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);color:#f0f0f0;border-radius:14px;padding:13px 14px 13px 16px;display:flex;align-items:flex-start;gap:12px;max-width:480px;min-width:300px;box-shadow:0 10px 32px rgba(0,0,0,.32),0 0 0 .5px rgba(255,255,255,.08) inset;pointer-events:none;transition:opacity .22s ease-out,transform .22s cubic-bezier(.34,1.56,.64,1)}.hm-toast.show{opacity:1;transform:scale(1);pointer-events:auto}.hm-toast-icon{font-size:20px;line-height:1;padding-top:1px;flex-shrink:0}.hm-toast-body{flex:1;min-width:0}.hm-toast-title-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.hm-toast-title{font-weight:600;font-size:12.5px;line-height:1.4;color:#fff;margin-bottom:2px}.hm-toast-detail{font-size:11px;line-height:1.5;color:#c8c8cc}.hm-toast-lines{font-size:11px;line-height:1.55;color:#c8c8cc}.hm-toast-code{background:rgba(0,0,0,.32);border-radius:8px;padding:8px 10px;font-size:10.5px;line-height:1.5;color:#aeb0b8;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;margin-top:6px;max-height:140px;overflow:auto}.hm-toast-chip{font-size:10px;font-weight:700;padding:2px 8px;border-radius:999px;letter-spacing:.02em;flex-shrink:0}.hm-toast-chip.ok{background:rgba(52,199,89,.18);color:#4dd96b}.hm-toast-chip.warn{background:rgba(255,149,0,.18);color:#ffb340}.hm-toast-chip.danger{background:rgba(255,59,48,.18);color:#ff6961}.hm-toast-chip.gray{background:rgba(255,255,255,.12);color:#c8c8cc}.hm-toast-count{font-size:10px;font-weight:700;color:#c8c8cc;background:rgba(255,255,255,.08);padding:2px 8px;border-radius:6px;font-variant-numeric:tabular-nums;flex-shrink:0}.hm-toast-act{font-size:11px;font-weight:600;color:#4dd96b;border:none;background:none;cursor:pointer;padding:2px 4px;flex-shrink:0;font-family:inherit}.hm-toast-act:active{opacity:.7}.hm-toast-close{background:rgba(255,255,255,.10);color:#34c759;border:0;border-radius:8px;padding:5px 9px;font-size:10.5px;font-weight:500;font-family:inherit;cursor:pointer;white-space:nowrap;margin-left:6px;flex-shrink:0}.hm-toast-close:active{background:rgba(255,255,255,.2)}@media(max-width:820px){.hm-toast-stack{left:12px;right:12px;bottom:calc(12px + env(safe-area-inset-bottom,0));transform:none;align-items:stretch}.hm-toast{transform:scale(.95);max-width:none;min-width:0}.hm-toast.show{transform:scale(1)}}';\r\n\r\n function ensureStyle(){\r\n if(styleInjected) return;\r\n styleInjected = true;\r\n var st = document.createElement('style');\r\n st.id = 'hm-toast-style';\r\n st.textContent = CSS;\r\n document.head.appendChild(st);\r\n }\r\n\r\n function getStack(){\r\n if(!stack){\r\n stack = document.createElement('div');\r\n stack.id = 'hm-toast-stack';\r\n document.body.appendChild(stack);\r\n }\r\n return stack;\r\n }\r\n\r\n function isMobile(){\r\n return window.matchMedia('(max-width: 820px)').matches;\r\n }\r\n\r\n /* 栈容量 = 栈内各 toast maxStack 的最大值(空栈默认 5); 移动端 ≤820px 收窄为 3 */\r\n function stackCap(){\r\n var cap = 0;\r\n if(stack) stack.querySelectorAll('.hm-toast').forEach(function(t){\r\n var m = parseInt(t.getAttribute('data-max')||DEFAULT_MAX, 10);\r\n if(m > cap) cap = m;\r\n });\r\n if(cap === 0) cap = DEFAULT_MAX;\r\n if(isMobile() && cap > MOBILE_MAX) cap = MOBILE_MAX;\r\n return cap;\r\n }\r\n\r\n /* 超 N 挤掉最旧(FIFO) */\r\n function evictOldest(){\r\n var cap = stackCap();\r\n while(stack.querySelectorAll('.hm-toast').length > cap){\r\n var oldest = stack.querySelector('.hm-toast');\r\n if(!oldest) break;\r\n clearTimeout(oldest._timer);\r\n oldest.remove();\r\n }\r\n }\r\n\r\n function dismiss(t){\r\n if(!t || !t.isConnected) return;\r\n clearTimeout(t._timer);\r\n t.classList.remove('show');\r\n setTimeout(function(){\r\n if(t.isConnected) t.remove();\r\n }, 250);\r\n }\r\n\r\n function show(item){\r\n var opts = item.opts;\r\n var container = getStack();\r\n ensureStyle();\r\n var msg = item.msg, detail = item.detail;\r\n var t = document.createElement('div');\r\n t.className = 'hm-toast';\r\n t.setAttribute('role','status');\r\n t.setAttribute('aria-live','polite');\r\n t.setAttribute('data-max', String(opts.maxStack || DEFAULT_MAX));\r\n var icon = ICONS[opts.icon] || opts.icon || '📋';\r\n var titleRow = '<span class=\"hm-toast-title\">'+esc(msg)+'</span>';\r\n if(opts.badge){\r\n var b = opts.badge;\r\n var btype = ['ok','warn','danger'].indexOf(b.type)>=0 ? b.type : 'ok';\r\n titleRow += '<span class=\"hm-toast-chip '+btype+'\">'+esc(b.text)+'</span>';\r\n }\r\n if(opts.count){\r\n titleRow += '<span class=\"hm-toast-count\">'+esc(opts.count)+'</span>';\r\n }\r\n if(opts.actions && opts.actions.length){\r\n opts.actions.slice(0,2).forEach(function(a){\r\n titleRow += '<button class=\"hm-toast-act\" data-act=\"'+esc(a.label)+'\">'+esc(a.label)+'</button>';\r\n });\r\n }\r\n var bodyHtml = '<div class=\"hm-toast-title-row\">'+titleRow+'</div>';\r\n if(detail){\r\n bodyHtml += '<div class=\"hm-toast-detail\">'+esc(detail)+'</div>';\r\n }\r\n if(opts.lines && opts.lines.length){\r\n bodyHtml += '<div class=\"hm-toast-lines\">'+opts.lines.map(function(l){return esc(l);}).join('<br>')+'</div>';\r\n }\r\n if(opts.code){\r\n bodyHtml += '<div class=\"hm-toast-code\">'+esc(opts.code)+'</div>';\r\n }\r\n t.innerHTML = '<div class=\"hm-toast-icon\">'+icon+'</div><div class=\"hm-toast-body\">'+bodyHtml+'</div><button class=\"hm-toast-close\">✓ 知道了</button>';\r\n container.appendChild(t);\r\n evictOldest();\r\n if(opts.actions && opts.actions.length){\r\n var actBtns = t.querySelectorAll('.hm-toast-act');\r\n opts.actions.slice(0,2).forEach(function(a, i){\r\n actBtns[i].addEventListener('click', function(){\r\n var f = a.onClick;\r\n dismiss(t);\r\n if(f) setTimeout(f, 0);\r\n });\r\n });\r\n }\r\n t.querySelector('.hm-toast-close').addEventListener('click', function(){ dismiss(t); });\r\n var timer = setTimeout(function(){ dismiss(t); }, opts.timeout || 4500);\r\n t._timer = timer;\r\n requestAnimationFrame(function(){ t.classList.add('show'); });\r\n }\r\n\r\n window" + ".toast = function(msg, detail, options){\r\n options = options || {};\r\n show({ msg: msg, detail: detail || '', opts: options });\r\n };\r\n /* 清空栈(技能页面销毁/测试用):移除当前全部显示 */\r\n window" + ".__hmToastFlush = function(){\r\n if(!stack) return;\r\n stack.querySelectorAll('.hm-toast').forEach(function(t){ clearTimeout(t._timer); t.remove(); });\r\n };\r\n})();\r\n\r\n/* ── snapshot 结构校验(违规直接报错)── */\r\nfunction _validateSnapshot(s){\r\n if(!s || typeof s !== 'object'){\r\n throw new Error('snapshot 违规: scene.snapshot 必须是对象');\r\n }\r\n if(typeof s.title !== 'string' || !s.title.trim()){\r\n throw new Error('snapshot 违规: title 必须是非空字符串');\r\n }\r\n if(!Array.isArray(s.summary)){\r\n throw new Error('snapshot 违规: summary 必须是数组');\r\n }\r\n if(!Array.isArray(s.sections)){\r\n throw new Error('snapshot 违规: sections 必须是数组');\r\n }\r\n s.sections.forEach(function(sec, i){\r\n if(!sec || typeof sec !== 'object' || typeof sec.heading !== 'string' || !Array.isArray(sec.rows)){\r\n throw new Error('snapshot 违规: sections['+i+'] 必须含 heading(字符串) + rows(数组)');\r\n }\r\n });\r\n return true;\r\n}\r\n\r\n/* 行渲染: 支持字符串 或 {text, sensitive}(脱敏)*/\r\nfunction _rowText(r){\r\n if(r && typeof r === 'object' && !Array.isArray(r)){\r\n if(r.sensitive) return '****(敏感字段已脱敏)';\r\n return String(r.text ?? '');\r\n }\r\n return String(r ?? '');\r\n}\r\n\r\n/* ── buildDataText v1.2(snapshot 结构化 · 领域无关 · format 支持)──\r\n * buildDataText(p, format?) format: 'text'(默认) | 'json' | 'csv'\r\n */\r\nfunction buildDataText(p, format){\r\n var d = p && p.data || {};\r\n var me = d.meta || {};\r\n var s = d.scene || {};\r\n var snap = s.snapshot;\r\n _validateSnapshot(snap);\r\n format = format || 'text';\r\n\r\n if(format === 'json'){\r\n return JSON.stringify(snap, null, 2);\r\n }\r\n if(format === 'csv'){\r\n var out = [];\r\n out.push(snap.title);\r\n out.push(snap.summary.map(_rowText).join('; '));\r\n snap.sections.forEach(function(sec){\r\n out.push('[' + sec.heading + ']');\r\n sec.rows.forEach(function(r){ out.push(_rowText(r)); });\r\n });\r\n return out.join('\\n');\r\n }\r\n\r\n var L = ['【'+(me.skill_name||me.command_cn||'')+' · '+(me.command_cn||'')+'】'];\r\n L.push('场景: '+(me.command_cn||'')+(s.scene_id?('('+s.scene_id+')'):'')+(me.wake_word?(' · 唤醒词「'+me.wake_word+'」'):''));\r\n L.push('时间: '+(me.occurred_at||''));\r\n if(snap.summary.length){\r\n snap.summary.forEach(function(x){ L.push(_rowText(x)); });\r\n }\r\n snap.sections.forEach(function(sec){\r\n if(!sec.rows.length) return;\r\n L.push('▍'+sec.heading);\r\n sec.rows.forEach(function(r){ L.push(' · '+_rowText(r)); });\r\n });\r\n return L.join('\\n');\r\n}\r\n\r\n/* ── buildLogText v1.2(6 段日志 · format 支持)── */\r\nfunction buildLogText(p, format){\r\n var d = p && p.data || {};\r\n var me = d.meta || {};\r\n var s = d.scene || {};\r\n var cl = d.copy_log || s.copy_log || {};\r\n format = format || 'text';\r\n\r\n var L = ['① 场景标识'];\r\n L.push(' 命 令: '+(me.command_cn||''));\r\n L.push(' 唤醒词: '+(me.wake_word||'(未知)'));\r\n L.push(' 场景名: '+(me.command_cn||'')+(s.scene_id?('('+s.scene_id+')'):''));\r\n L.push('');\r\n L.push('② AI 思考链');\r\n L.push(' '+(cl.thinking||'(本地渲染 · 无 AI 链)'));\r\n L.push('');\r\n L.push('③ 底层数据结构');\r\n L.push(' '+(cl.data_structure||'(只读查询)'));\r\n L.push('');\r\n L.push('④ 调用链');\r\n L.push(' '+(cl.call_chain||'(未知)'));\r\n L.push('');\r\n L.push('⑤ 时间戳 + 版本');\r\n L.push(' 本地时间: '+(cl.timestamp||me.occurred_at||'(未知)'));\r\n L.push(' 版 本: '+(me.skill_version||'(未知)'));\r\n L.push('');\r\n L.push('⑥ 异常信息');\r\n L.push(' '+(cl.exception||'无'));\r\n return L.join('\\n');\r\n}\r\n\r\n/* ── metaHeader / remindersBlock(v1.1 保留)── */\r\nfunction metaHeader(p, m){var me=p.data.meta||{};var stage=m&&m.stage?('<span class=\"stage\">'+esc(m.stage)+'</span>'):'';return '<header class=\"hero\"><div class=\"eyebrow\" style=\"color:#8e8e93;font-size:11px;font-weight:600\">'+esc(me.command_cn||'')+' · '+esc(me.occurred_at||'')+'</div><h1>'+esc(m&&m.title||me.command_cn||'')+'</h1>'+(m&&m.lead?'<p class=\"lead\">'+esc(m.lead)+'</p>':'')+stage+'</header>';}\r\nfunction remindersBlock(p){var rs=arr(p.data.reminders);if(!rs.length)return '';return '<section class=\"remind\"><h2>顺路提醒</h2>'+rs.map(r=>'<div class=\"remind-item '+(r.type==='warn'?'warn':r.type==='danger'?'bad':'')+'\">'+esc(r.text)+'</div>').join('')+'</section>';}\r\n\r\n/* ── actionBar v1.2(场景按钮 + 复制数据/日志 ghost)──\r\n * actionBar(p, extra?, opts?) opts: { preview, formatMenu, download }\r\n */\r\nfunction actionBar(p, extra, opts){\r\n window" + ".__hmPayload = p;\r\n opts = opts || {};\r\n var btns = arr(extra || []);\r\n var sb = opts.noSb ? [] : arr(p && p.data && p.data.scene && p.data.scene.buttons || []);\r\n var html = '<div class=\"hm-actions\">';\r\n btns.concat(sb).forEach(function(b){\r\n html += '<button class=\"copy '+(b.kind||'')+'\" onclick=\"copyText(this.dataset.t)\" data-t=\"'+esc(b.text).replace(/\"/g,'"')+'\">'+esc(b.label)+'</button>';\r\n });\r\n html += '<button class=\"copy ghost\" onclick=\"'+(opts.preview?'window.__hmCopyData(true)':'copyText(buildDataText(window.__hmPayload))')+'\">复制数据</button>';\r\n html += '<button class=\"copy ghost\" onclick=\"'+(opts.preview?'window.__hmCopyLog(true)':'copyText(buildLogText(window.__hmPayload))')+'\">复制日志</button>';\r\n html += '</div>';\r\n if(opts.preview){\r\n html += '<div class=\"hm-preview-overlay\" id=\"hm-preview\" style=\"display:none\">'\r\n + '<div class=\"hm-preview-panel\"><div class=\"hm-preview-head\"><span id=\"hm-preview-title\">复制预览</span>'\r\n + '<button class=\"hm-preview-close\" onclick=\"document.getElementById(\\'hm-preview\\').style.display=\\'none\\'\">✕</button></div>'\r\n + '<pre class=\"hm-preview-body\" id=\"hm-preview-body\"></pre>'\r\n + '<div class=\"hm-preview-actions\"><button class=\"copy primary\" id=\"hm-preview-confirm\">复制</button>'\r\n + '<button class=\"copy\" onclick=\"document.getElementById(\\'hm-preview\\').style.display=\\'none\\'\">取消</button></div>'\r\n + '</div></div>';\r\n }\r\n return html;\r\n}\r\n\r\n/* 复制预览面板(actionBar preview 模式) */\r\nfunction _showCopyPreview(kind){\r\n var p = window.__hmPayload;\r\n var body = document.getElementById('hm-preview-body');\r\n var title = document.getElementById('hm-preview-title');\r\n if(!body) return;\r\n var text = kind === 'log' ? buildLogText(p) : buildDataText(p);\r\n title.textContent = kind === 'log' ? '复制日志 · 预览' : '复制数据 · 预览';\r\n body.textContent = text;\r\n document.getElementById('hm-preview').style.display = 'flex';\r\n document.getElementById('hm-preview-confirm').onclick = function(){\r\n copyText(text);\r\n document.getElementById('hm-preview').style.display = 'none';\r\n };\r\n}\r\nwindow" + ".__hmCopyData = function(){ _showCopyPreview('data'); };\r\nwindow" + ".__hmCopyLog = function(){ _showCopyPreview('log'); };\r\n\r\n/* ── formPrompt(P0 · 用户填参数表单 + 实时预览 + 空值拦截)──\r\n * formPrompt(fields, template) → HTML 字符串\r\n * fields: [{ key, label, type:'text'|'number'|'select', options?, default?, placeholder? }]\r\n * template: 含 {key} 占位符的 prompt 模板\r\n */\r\nfunction formPrompt(fields, template){\r\n fields = arr(fields);\r\n var uid = 'fp' + Math.random().toString(36).slice(2,7);\r\n var html = '<div class=\"fp\" id=\"'+uid+'\">';\r\n fields.forEach(function(f, i){\r\n var fid = uid + '-f' + i;\r\n html += '<div class=\"fp-field\"><label class=\"fp-label\">'+esc(f.label)+'</label>';\r\n if(f.type === 'select'){\r\n html += '<select class=\"fp-input\" id=\"'+fid+'\" data-key=\"'+esc(f.key)+'\">';\r\n arr(f.options).forEach(function(o){\r\n var sel = (String(o.value ?? o) === String(f.default ?? '')) ? ' selected' : '';\r\n html += '<option value=\"'+esc(o.value ?? o)+'\"'+sel+'>'+esc(o.label ?? o)+'</option>';\r\n });\r\n html += '</select>';\r\n } else {\r\n html += '<input class=\"fp-input\" id=\"'+fid+'\" type=\"'+(f.type==='number'?'number':'text')+'\" data-key=\"'+esc(f.key)+'\" value=\"'+esc(f.default ?? '')+'\" placeholder=\"'+esc(f.placeholder ?? '')+'\">';\r\n }\r\n html += '</div>';\r\n });\r\n html += '<div class=\"fp-preview\"><div class=\"fp-preview-label\">Prompt 预览</div><pre class=\"fp-preview-body\" id=\"'+uid+'-preview\"></pre></div>';\r\n html += '<div class=\"fp-actions\"><button class=\"copy primary\" id=\"'+uid+'-btn\">复制指令</button></div>';\r\n html += '</div>';\r\n\r\n function renderPreview(){\r\n var vals = {};\r\n fields.forEach(function(f, i){\r\n var el = document.getElementById(uid + '-f' + i);\r\n vals[f.key] = el ? el.value : '';\r\n });\r\n var text = String(template || '').replace(/\\{(\\w+)\\}/g, function(_, k){ return vals[k] ?? ''; });\r\n document.getElementById(uid + '-preview').textContent = text;\r\n var btn = document.getElementById(uid + '-btn');\r\n var emptyRequired = fields.some(function(f){ return f.required && !vals[f.key]; });\r\n if(emptyRequired){\r\n btn.disabled = true;\r\n btn.textContent = '请填写必填项';\r\n } else {\r\n btn.disabled = false;\r\n btn.textContent = '复制指令';\r\n }\r\n }\r\n\r\n setTimeout(function(){\r\n fields.forEach(function(f, i){\r\n var el = document.getElementById(uid + '-f' + i);\r\n if(el){\r\n el.addEventListener('input', renderPreview);\r\n el.addEventListener('change', renderPreview);\r\n }\r\n });\r\n document.getElementById(uid + '-btn').addEventListener('click', function(){\r\n if(this.disabled) { toast('请先填写','必填字段未填写',{badge:{text:'提示',type:'warn'}}); return; }\r\n var text = document.getElementById(uid + '-preview').textContent;\r\n copyText(text);\r\n });\r\n renderPreview();\r\n }, 0);\r\n\r\n return html;\r\n}\r\n\r\n/* ── selectList(P0 · 勾选列表 + 批量操作 + 计数联动 + 行内控件 v1.9)──\r\n * selectList(items, batchActions?, opts?)\r\n * items: [{ id, title, sub?, group?, widget? }]\r\n * widget(v1.9 · #327): { type:'date'|'text'|'select', key, label?, placeholder?, options? }\r\n * - options: [ {value,label} | '原始值' ](select 用; 缺省渲染占位「请选择」)\r\n * - 非法 type 降级 text(宽容渲染, 不报错); key 缺省 'w'+行号\r\n * batchActions: [{ label, kind:'ok'|'danger', onClick(ids, values?) }]\r\n * - v1.9: onClick 第二参 values = 勾选条目对应的行内值 { [id]: { [key]: value } }\r\n * (只读勾选条目; 未填 → null, 不报错; 未勾选条目不参与)\r\n * opts: { onSubmit(selectedIds, values) } —— v1.9 读取接口(选定形态, 等价 getValues)\r\n * - 任意批量操作按钮点击后触发(与 onClick 并列; 无勾选时不触发, 与既有拦截一致)\r\n * - values = 全部行内值 { [id]: { [key]: value } }(含未勾选条目; 未填 → null; 无 widget 条目不出现)\r\n * 安全: 行内 label/placeholder/option value+label 渲染一律 esc, 零注入面\r\n * 兼容: 未声明 widget 的既有调用渲染输出与行为完全不变(守卫测试回归 outerHTML)\r\n */\r\nfunction selectList(items, batchActions, opts){\r\n items = arr(items);\r\n batchActions = arr(batchActions);\r\n opts = opts || {};\r\n var uid = 'sl' + Math.random().toString(36).slice(2,7);\r\n var groups = {};\r\n items.forEach(function(it){\r\n var g = it.group || '';\r\n (groups[g] = groups[g] || []).push(it);\r\n });\r\n var groupNames = Object.keys(groups);\r\n\r\n /* 行内控件 HTML(v1.9 · #327): 未声明 widget → 空串, 渲染零变化 */\r\n function widgetHtml(it, idx){\r\n var w = it.widget;\r\n if(!w || typeof w !== 'object') return '';\r\n var t = w.type === 'date' ? 'date' : (w.type === 'select' ? 'select' : 'text'); // 非法 type 降级 text\r\n var key = w.key != null ? String(w.key) : ('w' + idx);\r\n var lbl = w.label ? '<span class=\"sl-widget-label\">'+esc(w.label)+'</span>' : '';\r\n var inner;\r\n if(t === 'select'){\r\n inner = '<select class=\"sl-widget-input\" data-wkey=\"'+esc(key)+'\">';\r\n var os = arr(w.options);\r\n if(!os.length) inner += '<option value=\"\">请选择</option>';\r\n os.forEach(function(o){\r\n var ov = (o && typeof o === 'object') ? (o.value != null ? o.value : o.label) : o;\r\n var ol = (o && typeof o === 'object') ? (o.label != null ? o.label : o.value) : o;\r\n inner += '<option value=\"'+esc(String(ov == null ? '' : ov))+'\">'+esc(String(ol == null ? '' : ol))+'</option>';\r\n });\r\n inner += '</select>';\r\n } else {\r\n inner = '<input class=\"sl-widget-input\" data-wkey=\"'+esc(key)+'\" type=\"'+t+'\"'\r\n + (w.placeholder ? ' placeholder=\"'+esc(w.placeholder)+'\"' : '')\r\n + '>';\r\n }\r\n return '<span class=\"sl-widget\">'+lbl+inner+'</span>';\r\n }\r\n\r\n var html = '<div class=\"sl\" id=\"'+uid+'\">';\r\n if(batchActions.length){\r\n html += '<div class=\"sl-batchbar\" id=\"'+uid+'-batch\">';\r\n batchActions.forEach(function(a){\r\n html += '<button class=\"copy '+(a.kind==='danger'?'red':'primary')+'\" data-batch=\"'+esc(a.label)+'\">'+esc(a.label)+'</button>';\r\n });\r\n html += '<span class=\"sl-count\" id=\"'+uid+'-count\">已选 0/'+items.length+'</span>';\r\n html += '</div>';\r\n }\r\n groupNames.forEach(function(g){\r\n var gItems = groups[g];\r\n html += '<div class=\"sl-group\"><div class=\"sl-group-head\">'+esc(g||'全部');\r\n html += ' <span class=\"sl-group-count\">本组已选 0/'+gItems.length+'</span></div>';\r\n html += '<div class=\"sl-group-items\">';\r\n gItems.forEach(function(it, idx){\r\n html += '<label class=\"sl-item\"><input type=\"checkbox\" data-id=\"'+esc(it.id)+'\" data-g=\"'+esc(g)+'\">'\r\n + '<span class=\"sl-item-body\"><span class=\"sl-item-title\">'+esc(it.title)+'</span>'\r\n + (it.sub?'<span class=\"sl-item-sub\">'+esc(it.sub)+'</span>':'')\r\n + widgetHtml(it, idx)\r\n + '</span></label>';\r\n });\r\n html += '</div></div>';\r\n });\r\n html += '</div>';\r\n\r\n setTimeout(function(){\r\n var root = document.getElementById(uid);\r\n var checkboxes = root.querySelectorAll('input[type=\"checkbox\"]');\r\n var globalCount = document.getElementById(uid + '-count');\r\n function update(){\r\n var total = checkboxes.length;\r\n var checked = root.querySelectorAll('input[type=\"checkbox\"]:checked').length;\r\n if(globalCount) globalCount.textContent = '已选 '+checked+'/'+total;\r\n root.querySelectorAll('.sl-group').forEach(function(gEl){\r\n var boxes = gEl.querySelectorAll('input[type=\"checkbox\"]');\r\n var c = gEl.querySelectorAll('input[type=\"checkbox\"]:checked').length;\r\n var lbl = gEl.querySelector('.sl-group-count');\r\n if(lbl) lbl.textContent = '本组已选 '+c+'/'+boxes.length;\r\n });\r\n return checked;\r\n }\r\n checkboxes.forEach(function(cb){\r\n cb.addEventListener('change', update);\r\n });\r\n /* 行内值读取(v1.9 · #327): 计数只随勾选态, 控件值变化不干扰 */\r\n function widgetVal(el){\r\n var v = el.value;\r\n return (v === '' || v === null || v === undefined) ? null : v; // 未填统一归一 null\r\n }\r\n function readAll(){\r\n var out = {};\r\n root.querySelectorAll('.sl-item').forEach(function(itemEl){\r\n var cb = itemEl.querySelector('input[type=\"checkbox\"]');\r\n var id = cb ? cb.getAttribute('data-id') : null;\r\n if(id == null) return;\r\n itemEl.querySelectorAll('.sl-widget-input').forEach(function(el){\r\n var key = el.getAttribute('data-wkey');\r\n if(key == null) return;\r\n (out[id] = out[id] || {})[key] = widgetVal(el);\r\n });\r\n });\r\n return out;\r\n }\r\n function readChecked(){\r\n var out = {};\r\n root.querySelectorAll('input[type=\"checkbox\"]:checked').forEach(function(cb){\r\n var id = cb.getAttribute('data-id');\r\n var itemEl = cb.closest('.sl-item');\r\n itemEl.querySelectorAll('.sl-widget-input').forEach(function(el){\r\n var key = el.getAttribute('data-wkey');\r\n if(key == null) return;\r\n (out[id] = out[id] || {})[key] = widgetVal(el);\r\n });\r\n });\r\n return out;\r\n }\r\n root.querySelectorAll('[data-batch]').forEach(function(btn){\r\n btn.addEventListener('click', function(){\r\n var ids = root.querySelectorAll('input[type=\"checkbox\"]:checked');\r\n if(!ids.length){ toast('请先勾选','勾选要处理的条目',{badge:{text:'提示',type:'warn'}}); return; }\r\n var idList = Array.prototype.map.call(ids, function(x){ return x.getAttribute('data-id'); });\r\n var a = batchActions.filter(function(x){ return x.label === btn.getAttribute('data-batch'); })[0];\r\n if(a && a.onClick) a.onClick(idList, readChecked()); // v1.9: 第二参 = 勾选条目行内值\r\n if(opts.onSubmit) opts.onSubmit(idList, readAll()); // v1.9: 读取接口 = 全部行内值\r\n });\r\n });\r\n update();\r\n }, 0);\r\n\r\n return html;\r\n}\r\n\r\n/* ── confirm(P0 · 危险操作二次确认)──\r\n * confirm({ title, detail?, danger?, onOk })\r\n */\r\nfunction confirm(cfg){\r\n cfg = cfg || {};\r\n var overlay = document.createElement('div');\r\n overlay.className = 'hm-confirm-overlay';\r\n overlay.innerHTML = '<div class=\"hm-confirm-panel\">'\r\n + '<div class=\"hm-confirm-title\">'+(cfg.danger?'⚠️ ':'')+esc(cfg.title||'确认操作')+'</div>'\r\n + (cfg.detail?'<div class=\"hm-confirm-detail\">'+esc(cfg.detail)+'</div>':'')\r\n + '<div class=\"hm-confirm-actions\">'\r\n + '<button class=\"copy '+(cfg.danger?'red':'primary')+'\" data-c=\"1\">'+(cfg.danger?'确认删除':'确认')+'</button>'\r\n + '<button class=\"copy\" data-c=\"0\">取消</button>'\r\n + '</div></div>';\r\n document.body.appendChild(overlay);\r\n overlay.querySelector('[data-c=\"1\"]').addEventListener('click', function(){\r\n overlay.remove();\r\n if(cfg.onOk) cfg.onOk();\r\n });\r\n overlay.querySelector('[data-c=\"0\"]').addEventListener('click', function(){ overlay.remove(); });\r\n overlay.addEventListener('click', function(e){\r\n if(e.target === overlay) overlay.remove();\r\n });\r\n}\r\n\r\n/* ── foldBox(P1 · 折叠区)── */\r\nfunction foldBox(title, contentHtml){\r\n return '<details class=\"hm-fold\"><summary>'+esc(title)+'</summary><div class=\"hm-fold-body\">'+(contentHtml||'')+'</div></details>';\r\n}\r\n\r\n/* ── statusBadge(P1 · 状态徽章)──\r\n * statusBadge(status, text?)\r\n * status: 'ok'|'warn'|'danger'|'empty'(非法值降级 'empty', 防无样式徽章)\r\n * text: 自定义文案(缺省用语义默认: 成功/警告/失败/无数据)\r\n * 安全: status 白名单后直拼 class(无注入面); text 经 esc\r\n */\r\nvar STATUS_BADGE_MAP = { ok:'成功', warn:'警告', danger:'失败', empty:'无数据' };\r\nfunction statusBadge(status, text){\r\n status = STATUS_BADGE_MAP[status] ? status : 'empty'; // 非法/未知值降级 empty\r\n var txt = (text === null || text === undefined || text === '') ? STATUS_BADGE_MAP[status] : String(text);\r\n return '<span class=\"hm-status '+status+'\">'+esc(txt)+'</span>';\r\n}\r\n\r\n/* ── emptyState(P1 · 空状态)──\r\n * emptyState({icon?, text, hint?, action?})\r\n * icon: emoji/文本图标(esc)\r\n * text: 主文案(缺省 '暂无数据', esc)\r\n * hint: 次要提示(esc)\r\n * action: 操作区 HTML —— 受信 HTML 透传(调用方负责其内容安全, 通常为按钮;\r\n * 如含用户数据必须先 esc)。其余字段一律 esc, 防 XSS。\r\n */\r\nfunction emptyState(cfg){\r\n cfg = cfg || {};\r\n return '<div class=\"hm-empty\">'\r\n + (cfg.icon?'<div class=\"hm-empty-icon\">'+esc(cfg.icon)+'</div>':'')\r\n + '<div class=\"hm-empty-text\">'+esc(cfg.text||'暂无数据')+'</div>'\r\n + (cfg.hint?'<div class=\"hm-empty-hint\">'+esc(cfg.hint)+'</div>':'')\r\n + (cfg.action?('<div class=\"hm-empty-action\">'+cfg.action+'</div>'):'')\r\n + '</div>';\r\n}\r\n\r\n/* ── errorReceipt(P1 · 错误回执 · 08 规范 §6.1 三层反馈)──\r\n * errorReceipt({message, retryPrompt?, data?, log?, payload?})\r\n * message: 错误描述(esc, 缺省 '操作失败')\r\n * retryPrompt: 修正重试按钮文案(有则渲染「修正重试」按钮, 复制该 prompt)\r\n * data: 复制数据按钮内容(字符串直传; 或省略时从 payload 生成)\r\n * log: 复制日志按钮内容(字符串直传; 或省略时从 payload 生成)\r\n * payload: 数据信封 {status, data:{meta, scene}} —— 显式传入, 优先于全局\r\n * __hmPayload: 兼容全局兜底(actionBar 已设置; 显式 payload 优先)\r\n * 布局: 修正重试 primary 独立一行 + 复制数据/日志 ghost 一行 2 个(08 规范奇数按钮)\r\n * 安全: message/data/log 经 esc + data-t 中转(onclick 仅 copyText(this.dataset.t), 零注入面)\r\n */\r\nfunction errorReceipt(cfg){\r\n cfg = cfg || {};\r\n var payload = cfg.payload || window.__hmPayload || null;\r\n var dataText = cfg.data != null ? String(cfg.data) : '';\r\n var logText = cfg.log != null ? String(cfg.log) : '';\r\n // 有效数据源 = payload 含 scene.snapshot(残缺 payload 视为无数据, 不生成垃圾复制内容)\r\n var pValid = !!(payload && payload.data && payload.data.scene && payload.data.scene.snapshot);\r\n if(!dataText && pValid){\r\n try { dataText = buildDataText(payload); } catch(e){ dataText = ''; } // 数据不完整 → 不渲染复制数据按钮(错误回执容错)\r\n }\r\n if(!logText && pValid){\r\n try { logText = buildLogText(payload); } catch(e){ logText = ''; } // 同上\r\n }\r\n var html = '<div class=\"hm-error\">'\r\n + '<div class=\"hm-error-title\">❌ '+esc(cfg.message||'操作失败')+'</div>';\r\n if(cfg.retryPrompt){\r\n html += '<div class=\"hm-actions\"><button class=\"copy primary wide\" onclick=\"copyText(this.dataset.t)\" data-t=\"'+esc(cfg.retryPrompt).replace(/\"/g,'"')+'\">修正重试</button></div>';\r\n }\r\n var btns = '';\r\n if(dataText) btns += '<button class=\"copy ghost\" onclick=\"copyText(this.dataset.t)\" data-t=\"'+esc(dataText).replace(/\"/g,'"')+'\">复制数据</button>';\r\n if(logText) btns += '<button class=\"copy ghost\" onclick=\"copyText(this.dataset.t)\" data-t=\"'+esc(logText).replace(/\"/g,'"')+'\">复制日志</button>';\r\n if(btns) html += '<div class=\"hm-actions\">'+btns+'</div>';\r\n return html + '</div>';\r\n}\r\n\r\n/* ── smartSelect 选择器组件(v1.12 · #312 · 复用优先·新建其次)──\r\n * smartSelect(inputEl, config) → { getState, getValue }\r\n * 字段级组件: 一页 N 实例, 每字段一容器一 config 一隐藏 input, 互不干扰。\r\n * 定位: 与 copyText/actionBar 同级 Base 组件; 账户/分类/账本/运动类型等\r\n * 「复用优先·新建其次」字段通用; 零领域词, label/文案/选项/prompt 全由外部注入。\r\n * config(snake_case, 与数据契约 form.selector.<fieldKey> 对齐):\r\n * options: [{name, disabled}] 已有项(含停用态); 缺省 [] 且无推断/推荐/initial → 降级普通输入\r\n * inferred: string|null AI 推断最可能的已有项\r\n * recommended_new: string|null AI 推荐新建项(不在 options 中)\r\n * initial: {name, source}|null 显式初始选中; source 白名单 = _SS_SOURCES\r\n * maxChips: number|null 候选区折叠阈值(v1.12 · 缺省 8; 非正整数=不折叠;\r\n * 折叠态只显前 maxChips 个 + 「展开全部(N)」, 选中项保可见; 搜索时全量过滤)\r\n * texts: {candTitle, search, newPlaceholder, newButton, emptyButton,\r\n * badgeInferred, badgeRecommendedNew, badgeExisting, badgeHistory, badgeCustom,\r\n * cardSrc:{inferred, recommended_new, existing, history, custom, empty}}\r\n * theme: {brand, brandSoft, onBrand, deep} 缺省账本藏蓝 #123A63\r\n * 初始选中推导(initial 缺省时): AI 推断 > 历史预填(input.value) > AI 推荐新建 > 空\r\n * 行为: 已选卡片(chips 顶部, SVG ✓+来源徽章) + 候选 chips(超限折叠) + 搜索过滤(全量) + 自定义新建\r\n * (重名自动选中已有) + 相似提示 + 留空按钮; 停用划线置灰不可点; 绝不静默填错\r\n * 回填协议: input.value + dataset.source + dataset.new('1'=新建) + change 事件(bubbles)\r\n * 安全: 所有动态文本经 esc; 类名全 ss- 命名空间(封装纪律, 防宿主同名类冲突)\r\n * 校验: 结构违规直接报错(对齐 Base v1.2「违规直接报错」)\r\n */\r\nvar _SS_SOURCES = ['inferred','recommended_new','existing','history','custom','empty'];\r\nvar _SS_THEME = { brand:'#123a63', brandSoft:'#e9f0f7', onBrand:'#ffffff', deep:'#0b1f3b' };\r\nvar _SS_TEXTS = {\r\n candTitle:'候选(点击更换 · 输入过滤)',\r\n search:'输入过滤候选',\r\n newPlaceholder:'输入新名称',\r\n newButton:'+ 新建',\r\n emptyButton:'留空(不填)',\r\n badgeInferred:'AI 推断',\r\n badgeRecommendedNew:'AI 推荐·新建',\r\n badgeExisting:'已有',\r\n badgeHistory:'历史',\r\n badgeCustom:'自定义',\r\n cardSrc:{\r\n inferred:'AI 推断 · 识别到相关信息',\r\n recommended_new:'AI 推荐新建 · 提交时创建',\r\n existing:'从已有项中选',\r\n history:'沿用历史记录',\r\n custom:'自定义新建 · 提交时创建',\r\n empty:'不填该字段'\r\n }\r\n};\r\nfunction smartSelect(inputEl, config){\r\n /* ── 结构校验(违规直接报错)── */\r\n if(!inputEl || inputEl.tagName !== 'INPUT'){\r\n throw new Error('smartSelect 违规: inputEl 必须是 <input> 元素');\r\n }\r\n if(!config || typeof config !== 'object' || Array.isArray(config)){\r\n throw new Error('smartSelect 违规: config 必须是对象');\r\n }\r\n var options = config.options === undefined ? [] : config.options;\r\n if(!Array.isArray(options)){\r\n throw new Error('smartSelect 违规: options 必须是数组');\r\n }\r\n options.forEach(function(o, i){\r\n if(!o || typeof o !== 'object' || typeof o.name !== 'string' || !o.name.trim()){\r\n throw new Error('smartSelect 违规: options['+i+'] 必须含非空 name(字符串)');\r\n }\r\n if(o.disabled !== undefined && typeof o.disabled !== 'boolean'){\r\n throw new Error('smartSelect 违规: options['+i+'].disabled 必须是布尔');\r\n }\r\n });\r\n ['inferred','recommended_new'].forEach(function(k){\r\n var v = config[k];\r\n if(v !== undefined && v !== null && typeof v !== 'string'){\r\n throw new Error('smartSelect 违规: '+k+' 必须是字符串或 null');\r\n }\r\n });\r\n if(config.initial !== undefined && config.initial !== null){\r\n var ini = config.initial;\r\n if(!ini || typeof ini !== 'object' || typeof ini.name !== 'string'\r\n || _SS_SOURCES.indexOf(ini.source) < 0){\r\n throw new Error('smartSelect 违规: initial 必须含 name(字符串) + source(白名单 '+_SS_SOURCES.join('|')+')');\r\n }\r\n }\r\n if(config.texts !== undefined && (typeof config.texts !== 'object' || Array.isArray(config.texts))){\r\n throw new Error('smartSelect 违规: texts 必须是对象');\r\n }\r\n if(config.theme !== undefined && (typeof config.theme !== 'object' || Array.isArray(config.theme))){\r\n throw new Error('smartSelect 违规: theme 必须是对象');\r\n }\r\n\r\n var texts = Object.assign({}, _SS_TEXTS, config.texts || {});\r\n texts.cardSrc = Object.assign({}, _SS_TEXTS.cardSrc, (config.texts && config.texts.cardSrc) || {});\r\n var theme = Object.assign({}, _SS_THEME, config.theme || {});\r\n var custom = [];\r\n /* 候选区折叠(v1.12): maxChips 可配(缺省 8, 0/负数/非数字=不折叠) · 搜索时全量过滤 */\r\n var maxChips = (config.maxChips === undefined || config.maxChips === null)\r\n ? 8\r\n : ((typeof config.maxChips === 'number' && config.maxChips > 0)\r\n ? Math.floor(config.maxChips) : null);\r\n var searchRaw = '';\r\n var searchQ = '';\r\n var expanded = false;\r\n\r\n /* ── 降级模式: 无选项/无推断/无推荐/无 initial → 普通输入 ── */\r\n var degraded = !options.length && !config.inferred && !config.recommended_new && !config.initial;\r\n if(degraded){\r\n inputEl.classList.add('ss-plain');\r\n inputEl.style.display = '';\r\n inputEl.removeAttribute('hidden');\r\n var syncDegraded = function(){\r\n var v = inputEl.value;\r\n inputEl.dataset.source = v ? 'custom' : 'empty';\r\n inputEl.dataset.new = v ? '1' : '0';\r\n };\r\n syncDegraded();\r\n inputEl.addEventListener('input', syncDegraded);\r\n return {\r\n getState: function(){ var v = inputEl.value; return { name: v || null, mode: v ? 'custom' : 'empty' }; },\r\n getValue: function(){ return inputEl.value; }\r\n };\r\n }\r\n\r\n /* ── 初始选中推导: initial > AI 推断 > 历史预填(input.value) > AI 推荐新建 > 空 ── */\r\n var state;\r\n if(config.initial){\r\n state = { name: config.initial.name, mode: config.initial.source };\r\n } else if(config.inferred){\r\n state = { name: config.inferred, mode: 'inferred' };\r\n } else if(inputEl.value){\r\n state = { name: inputEl.value, mode: 'history' };\r\n } else if(config.recommended_new){\r\n state = { name: config.recommended_new, mode: 'recommended_new' };\r\n } else {\r\n state = { name: null, mode: 'empty' };\r\n }\r\n\r\n inputEl.style.display = 'none';\r\n var root = document.createElement('div');\r\n root.className = 'ss-root';\r\n root.style.setProperty('--ss-brand', theme.brand);\r\n root.style.setProperty('--ss-brand-soft', theme.brandSoft);\r\n root.style.setProperty('--ss-on-brand', theme.onBrand);\r\n root.style.setProperty('--ss-deep', theme.deep);\r\n inputEl.parentNode.insertBefore(root, inputEl.nextSibling);\r\n\r\n var CHECK_SVG = '<svg viewBox=\"0 0 20 20\" fill=\"none\"><path d=\"M4 10.5l4 4 8-9\" stroke=\"#fff\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>';\r\n var CIRCLE_SVG = '<svg viewBox=\"0 0 20 20\" fill=\"none\"><circle cx=\"10\" cy=\"10\" r=\"7\" stroke=\"#8b949e\" stroke-width=\"2\"/><path d=\"M7 10h6\" stroke=\"#8b949e\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg>';\r\n\r\n function badgeHtml(mode){\r\n if(mode === 'inferred') return '<span class=\"ss-badge ss-badge-ai\">'+esc(texts.badgeInferred)+'</span>';\r\n if(mode === 'recommended_new') return '<span class=\"ss-badge ss-badge-new\">'+esc(texts.badgeRecommendedNew)+'</span>';\r\n if(mode === 'custom') return '<span class=\"ss-badge ss-badge-plain\">'+esc(texts.badgeCustom)+'</span>';\r\n if(mode === 'history') return '<span class=\"ss-badge ss-badge-plain\">'+esc(texts.badgeHistory)+'</span>';\r\n if(mode === 'existing') return '<span class=\"ss-badge ss-badge-plain\">'+esc(texts.badgeExisting)+'</span>';\r\n return '';\r\n }\r\n\r\n function writeBack(){\r\n inputEl.value = state.name || '';\r\n inputEl.dataset.source = state.mode || 'empty';\r\n inputEl.dataset.new = (state.mode === 'recommended_new' || state.mode === 'custom') ? '1' : '0';\r\n inputEl.dispatchEvent(new Event('change', { bubbles: true }));\r\n }\r\n\r\n function setSel(name, mode){\r\n state.name = name;\r\n state.mode = mode;\r\n render();\r\n writeBack();\r\n }\r\n\r\n function allNames(){\r\n var names = options.map(function(o){ return o.name; });\r\n return names.concat(custom);\r\n }\r\n\r\n function similarHint(v){\r\n if(!v) return '';\r\n var s = allNames().filter(function(n){\r\n return n !== v && (n.indexOf(v) >= 0 || v.indexOf(n) >= 0);\r\n });\r\n return s.length ? '已有相似「'+s[0]+'」, 是否使用已有的?(点候选即选)' : '';\r\n }\r\n\r\n function render(){\r\n function isSel(n, m){ return state.name === n && state.mode === m; }\r\n var card = state.name\r\n ? '<div class=\"ss-card\"><div class=\"ss-ic\">'+CHECK_SVG+'</div>'\r\n + '<div class=\"ss-body\"><div class=\"ss-nm\">'+esc(state.name)+'</div>'\r\n + '<div class=\"ss-src\">'+esc(texts.cardSrc[state.mode] || '')+'</div></div>'\r\n + badgeHtml(state.mode) + '</div>'\r\n : '<div class=\"ss-card ss-card-empty\"><div class=\"ss-ic\">'+CIRCLE_SVG+'</div>'\r\n + '<div class=\"ss-body\"><div class=\"ss-nm\">'+esc(texts.emptyButton)+'</div>'\r\n + '<div class=\"ss-src\">'+esc(texts.cardSrc.empty || '')+'</div></div></div>';\r\n function chip(n, m, dis, badge){\r\n var sel = !dis && isSel(n, m);\r\n return '<button type=\"button\" class=\"ss-chip'+(dis?' ss-chip-dis':'')+(sel?' ss-chip-sel':'')+'\"'\r\n + (dis ? ' disabled' : ' data-n=\"'+esc(n)+'\" data-m=\"'+m+'\"')\r\n + '>'+(sel?'✓ ':'')+esc(n)+badge+(dis?' · 停用':'')+'</button>';\r\n }\r\n var chips = []; // chip HTML\r\n var chipsMeta = []; // 对齐元信息 {name, sel}(折叠保可见/搜索过滤用)\r\n function pushChip(n, m, dis, badge){\r\n var sel = !dis && isSel(n, m);\r\n chipsMeta.push({ name: n, sel: sel });\r\n chips.push(chip(n, m, dis, badge));\r\n }\r\n if(config.inferred){\r\n pushChip(config.inferred, 'inferred', false, '<span class=\"ss-badge ss-badge-ai\">'+esc(texts.badgeInferred)+'</span>');\r\n }\r\n if(config.recommended_new){\r\n pushChip(config.recommended_new, 'recommended_new', false, '<span class=\"ss-badge ss-badge-new\">'+esc(texts.badgeRecommendedNew)+'</span>');\r\n }\r\n options.forEach(function(o){\r\n if(o.name === config.inferred || o.name === config.recommended_new) return;\r\n pushChip(o.name, 'existing', !!o.disabled, '');\r\n });\r\n custom.forEach(function(n){\r\n pushChip(n, 'custom', false, '<span class=\"ss-badge ss-badge-plain\">'+esc(texts.badgeCustom)+'</span>');\r\n });\r\n\r\n /* 候选区折叠(v1.12): 折叠态只显前 maxChips 个, 选中项保可见; 搜索时全量过滤 */\r\n var showAll = !maxChips || expanded || !!searchQ || chips.length <= maxChips;\r\n var list = chips;\r\n var listMeta = chipsMeta;\r\n if(!showAll){\r\n var selIdx = -1;\r\n for(var i = 0; i < chipsMeta.length; i++){\r\n if(chipsMeta[i].sel){ selIdx = i; break; }\r\n }\r\n list = chips.slice(0, maxChips);\r\n listMeta = chipsMeta.slice(0, maxChips);\r\n if(selIdx >= maxChips){\r\n list[maxChips - 1] = chips[selIdx];\r\n listMeta[maxChips - 1] = chipsMeta[selIdx];\r\n }\r\n }\r\n var out = [];\r\n for(var j = 0; j < list.length; j++){\r\n if(searchQ && listMeta[j].name.toLowerCase().indexOf(searchQ) < 0) continue;\r\n out.push(list[j]);\r\n }\r\n var moreBtn = '';\r\n if(!showAll){\r\n moreBtn = '<button type=\"button\" class=\"ss-more\" data-more=\"1\">展开全部('+chips.length+')</button>';\r\n } else if(expanded && maxChips && chips.length > maxChips && !searchQ){\r\n moreBtn = '<button type=\"button\" class=\"ss-more\" data-more=\"0\">收起</button>';\r\n }\r\n\r\n root.innerHTML =\r\n card\r\n + '<div class=\"ss-title\">'+esc(texts.candTitle)+'</div>'\r\n + '<input class=\"ss-search\" placeholder=\"'+esc(texts.search)+'\">'\r\n + '<div class=\"ss-chips\">'+out.join('')+moreBtn+'</div>'\r\n + '<div class=\"ss-new\"><input placeholder=\"'+esc(texts.newPlaceholder)+'\"><button type=\"button\">'+esc(texts.newButton)+'</button></div>'\r\n + '<div class=\"ss-hint\"></div>'\r\n + '<button type=\"button\" class=\"ss-empty\">'+esc(texts.emptyButton)+'</button>';\r\n\r\n /* 搜索框内容跨 render 保持(过滤状态不丢, 输入框不空) */\r\n if(searchRaw) root.querySelector('.ss-search').value = searchRaw;\r\n root.querySelector('.ss-search').addEventListener('input', function(e){\r\n searchRaw = e.target.value;\r\n searchQ = searchRaw.trim().toLowerCase();\r\n render();\r\n });\r\n root.querySelectorAll('.ss-more').forEach(function(el){\r\n el.addEventListener('click', function(){\r\n expanded = el.getAttribute('data-more') === '1';\r\n render();\r\n });\r\n });\r\n root.querySelectorAll('.ss-chip[data-n]').forEach(function(el){\r\n el.addEventListener('click', function(){ setSel(el.getAttribute('data-n'), el.getAttribute('data-m')); });\r\n });\r\n var newIn = root.querySelector('.ss-new input');\r\n var doNew = function(){\r\n var v = newIn.value.trim();\r\n if(!v) return;\r\n if(allNames().indexOf(v) >= 0){ setSel(v, 'existing'); return; }\r\n custom.unshift(v);\r\n setSel(v, 'custom');\r\n };\r\n root.querySelector('.ss-new button').addEventListener('click', doNew);\r\n newIn.addEventListener('keydown', function(e){ if(e.key === 'Enter') doNew(); });\r\n newIn.addEventListener('input', function(){\r\n var h = root.querySelector('.ss-hint');\r\n var s = similarHint(newIn.value.trim());\r\n h.textContent = s;\r\n h.style.display = s ? '' : 'none';\r\n });\r\n root.querySelector('.ss-empty').addEventListener('click', function(){ setSel(null, 'empty'); });\r\n }\r\n\r\n render();\r\n writeBack();\r\n\r\n return {\r\n getState: function(){ return { name: state.name, mode: state.mode }; },\r\n getValue: function(){ return inputEl.value; }\r\n };\r\n}\r\n</script>\r\n<style>\r\n/* Base Skill token A 组 + 控件样式 v1.11(唯一真相源 · 跨技能 · 领域无关)\r\n * 来源: 居家管家模板 :root token(对齐 #248/08-HTML交互规范.md 按钮规范)\r\n * v1.2: 新增新控件样式(formPrompt/selectList/confirm/foldBox/statusBadge/emptyState/errorReceipt/复制预览面板)\r\n * v1.7: .hm-actions 自约束 max-width 520px 居中(#322 · 修复复制按钮被容器拉伸成半屏宽, 胶囊语义恢复)\r\n * v1.9: selectList 行内控件样式(.sl-widget* · #327 · 样式走 token A 组)\r\n * v1.11: smartSelect 选择器组件样式(.ss-* · #312 · 主题色经 .ss-root 内联 CSS 变量覆盖, 缺省账本藏蓝)\r\n * v1.12: smartSelect 候选区折叠按钮(.ss-more · #312 实测反馈: 大量候选时折叠+展开)\r\n * 注入点: 由 injector.py 的 SHARED-CSS 占位符注入(模板声明点, 勿含闭合标签字样)\r\n */\r\n\r\n/* ── token A 组(12 变量, 与组件契约 §6.4 一致)── */\r\n:root {\r\n --fg: #1d1d1f; /* 主文字 */\r\n --fg2: #6e6e73; /* 次级文字 */\r\n --fg3: #86868b; /* 弱化文字/占位 */\r\n --bg: #f5f5f7; /* 页面底色 */\r\n --card: #ffffff; /* 卡片底 */\r\n --line: #d2d2d7; /* 分隔线 */\r\n --blue: #007aff; /* 主色(场景按钮) */\r\n --blue2: #0a63ce; /* 主色深(按压态/链接) */\r\n --soft: #f5f8ff; /* 浅蓝底(stage 徽章) */\r\n --ok: #34c759; /* 成功/通过 */\r\n --shadow: 0 1px 2px rgba(0,0,0,.04), 0 12px 36px rgba(0,0,0,.06);\r\n}\r\n\r\n/* ── 复制按钮基础(对齐 08 规范: ≤3 色按功能区分)── */\r\nbutton.copy {\r\n -webkit-tap-highlight-color: transparent;\r\n touch-action: manipulation;\r\n -webkit-touch-callout: none;\r\n border: none;\r\n background: var(--line);\r\n color: var(--fg);\r\n border-radius: 999px;\r\n padding: 10px 12px;\r\n font-weight: 700;\r\n cursor: pointer;\r\n margin: 0;\r\n font-size: 13.5px;\r\n min-height: 44px;\r\n min-width: 0;\r\n flex: 1 1 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n white-space: nowrap;\r\n font-family: inherit;\r\n}\r\nbutton.copy:focus { outline: none; }\r\nbutton.copy:active { filter: brightness(.93); }\r\nbutton.copy:disabled { opacity: .45; cursor: not-allowed; }\r\n\r\n/* 主场景按钮(蓝实心, 单色系) */\r\nbutton.copy.primary {\r\n background: var(--blue);\r\n color: #fff;\r\n}\r\nbutton.copy.primary:active { background: var(--blue2); }\r\n\r\n/* 危险/破坏性操作(红实心) */\r\nbutton.copy.red {\r\n background: #ff3b30;\r\n color: #fff;\r\n}\r\nbutton.copy.red:active { background: #c0392b; }\r\n\r\n/* ghost: 复制数据/复制日志 = 白底 + 轻主色描边 + 主色文字胶囊(独立一行)\r\n * v1.26 · #329 降权重: 描边 1px/38% 透明度 + 12px/600 + min-height 40px —— 视觉重心归主操作(VLM 两轮反馈)\r\n * v1.28 · 审查修复: 透明度 35%→38%(与拍板页 329-ghost对比拍板页.html B 方案所见一致) */\r\nbutton.copy.ghost {\r\n background: var(--card);\r\n color: var(--blue);\r\n border: 1px solid var(--blue);\r\n border-color: color-mix(in srgb, var(--blue) 38%, transparent);\r\n min-height: 40px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n}\r\n\r\n/* ── actionBar 网格(偶数一行 2 个; 奇数时重要按钮单独一行由模板控制 wide)── */\r\n.hm-actions {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n gap: 8px;\r\n margin-top: 14px;\r\n /* v1.7 #322: 胶囊按钮自约束宽度——避免被全宽容器拉伸成半屏宽块(跨技能一致) */\r\n max-width: 520px;\r\n margin-left: auto;\r\n margin-right: auto;\r\n box-sizing: border-box;\r\n}\r\n.hm-actions .copy.wide {\r\n grid-column: 1 / -1;\r\n background: var(--blue);\r\n color: #fff;\r\n border: none;\r\n}\r\n\r\n/* 成功/失败徽章(yes 函数用) */\r\n.ok { color: var(--ok); font-weight: 700; }\r\n.bad { color: #ff3b30; font-weight: 700; }\r\n\r\n/* ── formPrompt(参数表单)── */\r\n.fp { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px 20px; margin: 12px 0; }\r\n.fp-field { margin-bottom: 12px; }\r\n.fp-label { display: block; font-size: 12px; font-weight: 700; color: var(--fg2); margin-bottom: 6px; letter-spacing: .02em; }\r\n.fp-input {\r\n width: 100%;\r\n border: 1.5px solid var(--line);\r\n border-radius: 10px;\r\n padding: 10px 12px;\r\n font-size: 14px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n background: #fff;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n min-height: 44px;\r\n}\r\n.fp-input:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(0,122,255,.12); }\r\n.fp-preview { background: #fafbfc; border: 1px dashed var(--line); border-radius: 10px; padding: 10px 14px; margin: 10px 0; }\r\n.fp-preview-label { font-size: 10.5px; font-weight: 700; color: var(--fg3); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 6px; }\r\n.fp-preview-body {\r\n font-size: 12px;\r\n color: var(--fg2);\r\n font-family: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;\r\n line-height: 1.6;\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n overflow-wrap: anywhere;\r\n margin: 0;\r\n}\r\n.fp-actions { margin-top: 12px; }\r\n\r\n/* ── selectList(勾选列表 + 批量操作)── */\r\n.sl { margin: 12px 0; }\r\n.sl-batchbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }\r\n.sl-count { font-size: 12px; font-weight: 600; color: var(--fg2); margin-left: auto; white-space: nowrap; }\r\n.sl-group { margin-bottom: 14px; }\r\n.sl-group-head { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 700; margin-bottom: 8px; }\r\n.sl-group-count { font-size: 11px; font-weight: 600; color: var(--fg3); }\r\n.sl-group-items { display: flex; flex-direction: column; gap: 6px; }\r\n.sl-item {\r\n display: flex;\r\n align-items: flex-start;\r\n gap: 10px;\r\n background: #fafbfc;\r\n border: 1px solid var(--line);\r\n border-radius: 12px;\r\n padding: 12px 14px;\r\n cursor: pointer;\r\n font-family: inherit;\r\n transition: border-color .15s, background .15s;\r\n}\r\n.sl-item:hover { border-color: var(--blue); background: #fff; }\r\n.sl-item input[type=\"checkbox\"] { width: 20px; height: 20px; margin-top: 2px; accent-color: var(--blue); flex-shrink: 0; }\r\n.sl-item-body { flex: 1; min-width: 0; }\r\n.sl-item-title { display: block; font-size: 13.5px; font-weight: 600; color: var(--fg); line-height: 1.5; word-break: break-word; }\r\n.sl-item-sub { display: block; font-size: 12px; color: var(--fg3); margin-top: 2px; line-height: 1.5; word-break: break-word; }\r\n\r\n/* ── selectList 行内控件(v1.9 · #327 · 样式走 token A 组)── */\r\n.sl-widget { display: block; margin-top: 8px; }\r\n.sl-widget-label { display: block; font-size: 11px; font-weight: 600; color: var(--fg3); margin-bottom: 4px; }\r\n.sl-widget-input {\r\n width: 100%;\r\n box-sizing: border-box;\r\n border: 1.5px solid var(--line);\r\n border-radius: 8px;\r\n padding: 8px 10px;\r\n font-size: 13px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n background: #fff;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n min-height: 40px;\r\n}\r\n.sl-widget-input:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(0,122,255,.12); }\r\n\r\n/* ── confirm 对话框 ── */\r\n.hm-confirm-overlay {\r\n position: fixed; inset: 0;\r\n background: rgba(0,0,0,.4);\r\n display: flex; align-items: center; justify-content: center;\r\n z-index: 10000;\r\n padding: 20px;\r\n animation: hmFade .18s ease-out;\r\n}\r\n@keyframes hmFade { from { opacity: 0; } to { opacity: 1; } }\r\n.hm-confirm-panel {\r\n background: var(--card);\r\n border-radius: 20px;\r\n max-width: 420px;\r\n width: 100%;\r\n padding: 24px 26px;\r\n box-shadow: var(--shadow);\r\n animation: hmPop .22s cubic-bezier(.34,1.56,.64,1);\r\n}\r\n@keyframes hmPop { from { transform: scale(.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }\r\n.hm-confirm-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }\r\n.hm-confirm-detail { font-size: 13.5px; color: var(--fg2); line-height: 1.65; margin-bottom: 16px; }\r\n.hm-confirm-actions { display: flex; gap: 10px; }\r\n.hm-confirm-actions .copy { flex: 1; }\r\n\r\n/* ── foldBox 折叠区 ── */\r\n.hm-fold {\r\n background: #fafbfc;\r\n border: 1px solid var(--line);\r\n border-radius: 12px;\r\n padding: 12px 16px;\r\n margin: 10px 0;\r\n font-size: 13px;\r\n}\r\n.hm-fold summary { cursor: pointer; font-weight: 600; color: var(--blue2); outline: none; font-size: 13px; user-select: none; }\r\n.hm-fold summary::marker { color: var(--blue); }\r\n.hm-fold[open] summary { margin-bottom: 10px; }\r\n.hm-fold-body { font-size: 12.5px; line-height: 1.6; color: var(--fg2); }\r\n\r\n/* ── statusBadge 状态徽章 ── */\r\n.hm-status {\r\n display: inline-flex; align-items: center; gap: 6px;\r\n padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;\r\n width: fit-content;\r\n}\r\n.hm-status.ok { background: #e6f7ec; color: #1f8c3d; }\r\n.hm-status.warn { background: #fff5e0; color: #a25b00; }\r\n.hm-status.danger { background: #fff0ee; color: #a83228; }\r\n.hm-status.empty { background: #f0f0f3; color: #6e6e73; }\r\n\r\n/* ── emptyState 空状态 ── */\r\n.hm-empty {\r\n background: #fafbfc;\r\n border: 1px dashed var(--line);\r\n border-radius: 12px;\r\n padding: 26px 20px;\r\n text-align: center;\r\n color: var(--fg2);\r\n font-size: 13.5px;\r\n line-height: 1.7;\r\n}\r\n.hm-empty-icon { font-size: 32px; margin-bottom: 8px; }\r\n.hm-empty-text { font-weight: 600; color: var(--fg); }\r\n.hm-empty-hint { font-size: 12.5px; color: var(--fg3); margin-top: 4px; }\r\n.hm-empty-action { margin-top: 14px; }\r\n\r\n/* ── errorReceipt 错误回执 ── */\r\n.hm-error {\r\n background: #fff0ee;\r\n border: 1.5px solid #ffc4c0;\r\n border-radius: 16px;\r\n padding: 20px 22px;\r\n margin: 12px 0;\r\n}\r\n.hm-error-title { font-size: 15px; font-weight: 700; color: #a83228; margin-bottom: 14px; }\r\n/* errorReceipt 内按钮布局: 第一个 .hm-actions(修正重试)贴标题; 后续 .hm-actions(复制数据/日志)与上方保持间距, 防贴死(#290 修复) */\r\n.hm-error .hm-actions { margin-top: 0; }\r\n.hm-error .hm-actions + .hm-actions { margin-top: 14px; }\r\n@media (max-width: 820px) {\r\n .hm-error { padding: 16px 14px; }\r\n .hm-error-title { margin-bottom: 12px; }\r\n}\r\n\r\n/* ── 复制预览面板(actionBar preview)── */\r\n.hm-preview-overlay {\r\n position: fixed; inset: 0;\r\n background: rgba(0,0,0,.4);\r\n display: flex; align-items: center; justify-content: center;\r\n z-index: 10001;\r\n padding: 16px;\r\n}\r\n.hm-preview-panel {\r\n background: var(--card);\r\n border-radius: 16px;\r\n max-width: 560px;\r\n width: 100%;\r\n max-height: 80vh;\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n box-shadow: var(--shadow);\r\n}\r\n.hm-preview-head {\r\n display: flex; align-items: center; justify-content: space-between;\r\n padding: 14px 18px;\r\n border-bottom: 1px solid var(--line);\r\n font-size: 14px; font-weight: 700;\r\n}\r\n.hm-preview-close { border: none; background: none; font-size: 15px; color: var(--fg3); cursor: pointer; padding: 4px; }\r\n.hm-preview-body {\r\n flex: 1;\r\n overflow: auto;\r\n padding: 14px 18px;\r\n font-size: 12px;\r\n line-height: 1.65;\r\n font-family: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n overflow-wrap: anywhere;\r\n color: var(--fg2);\r\n margin: 0;\r\n}\r\n.hm-preview-actions { display: flex; gap: 10px; padding: 12px 18px; border-top: 1px solid var(--line); }\r\n.hm-preview-actions .copy { flex: 1; }\r\n\r\n/* 手机适配: 单列网格保持按钮不换行 */\r\n@media (max-width: 820px) {\r\n .hm-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }\r\n .hm-actions .copy.wide { grid-column: 1 / -1; }\r\n .hm-actions .copy { min-height: 46px; }\r\n .sl-batchbar { flex-direction: column; align-items: stretch; }\r\n .sl-count { margin-left: 0; }\r\n .hm-confirm-panel { padding: 20px 18px; }\r\n}\r\n\r\n/* ── smartSelect 选择器组件(v1.11 · #312 · 复用优先·新建其次)──\r\n * 类名全 ss- 命名空间(封装纪律, 防宿主同名类冲突); 主题色 = .ss-root 内联 CSS 变量,\r\n * 缺省账本藏蓝 #123A63(deep #0B1F3B / soft #E9F0F7), 每实例可独立覆盖\r\n */\r\n.ss-root {\r\n --ss-brand: #123a63;\r\n --ss-brand-soft: #e9f0f7;\r\n --ss-on-brand: #ffffff;\r\n --ss-deep: #0b1f3b;\r\n margin-top: 6px;\r\n}\r\n.ss-card {\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n padding: 12px 14px;\r\n border-radius: 10px;\r\n border: 2px solid var(--ss-brand);\r\n background: var(--ss-brand-soft);\r\n}\r\n.ss-card .ss-ic {\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 50%;\r\n background: var(--ss-brand);\r\n flex: none;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n.ss-card .ss-ic svg { width: 18px; height: 18px; }\r\n.ss-body { flex: 1; min-width: 0; }\r\n.ss-nm { font-size: 16px; font-weight: 700; color: var(--fg); word-break: break-word; }\r\n.ss-src { font-size: 11px; color: var(--fg3); margin-top: 2px; }\r\n.ss-card.ss-card-empty { border: 2px dashed var(--line); background: transparent; }\r\n.ss-card.ss-card-empty .ss-ic { background: transparent; border: 2px solid var(--fg3); }\r\n.ss-card.ss-card-empty .ss-nm { color: var(--fg3); }\r\n.ss-badge {\r\n display: inline-block;\r\n font-size: 10px;\r\n line-height: 1;\r\n padding: 2px 6px;\r\n border-radius: 10px;\r\n flex-shrink: 0;\r\n}\r\n.ss-badge-ai { background: var(--ss-brand-soft); color: var(--ss-brand); }\r\n.ss-badge-new { background: #fff8c5; color: #9a6700; }\r\n.ss-badge-plain { background: #eef; color: var(--fg3); }\r\n.ss-title { font-size: 11px; color: var(--fg3); margin: 10px 0 6px; }\r\n.ss-search {\r\n width: 100%;\r\n box-sizing: border-box;\r\n padding: 8px 12px;\r\n border: 1.5px solid var(--line);\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n background: #fff;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n min-height: 40px;\r\n}\r\n.ss-search:focus { outline: none; border-color: var(--ss-brand); box-shadow: 0 0 0 3px rgba(18, 58, 99, .12); }\r\n.ss-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }\r\n.ss-chip {\r\n padding: 6px 12px;\r\n border: 1.5px solid var(--line);\r\n border-radius: 16px;\r\n font-size: 13px;\r\n background: #fff;\r\n cursor: pointer;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n -webkit-tap-highlight-color: transparent;\r\n touch-action: manipulation;\r\n}\r\n.ss-chip:hover { border-color: var(--ss-brand); }\r\n.ss-chip.ss-chip-sel {\r\n background: var(--ss-brand);\r\n border-color: var(--ss-brand);\r\n color: var(--ss-on-brand);\r\n font-weight: 700;\r\n box-shadow: 0 2px 6px rgba(0, 0, 0, .18);\r\n}\r\n.ss-chip.ss-chip-sel .ss-badge-ai { background: rgba(255, 255, 255, .2); color: #fff; }\r\n.ss-chip.ss-chip-sel .ss-badge-new { background: rgba(255, 255, 255, .25); color: #ffe9a8; }\r\n.ss-chip.ss-chip-sel .ss-badge-plain { background: rgba(255, 255, 255, .18); color: #fff; }\r\n.ss-chip.ss-chip-dis {\r\n color: var(--fg3);\r\n text-decoration: line-through;\r\n cursor: not-allowed;\r\n background: var(--bg);\r\n border-color: var(--line);\r\n}\r\n/* 候选区折叠按钮(v1.12 · #312 实测反馈: 分类几十上百时 chips 全量平铺过长) */\r\n.ss-more {\r\n padding: 6px 12px;\r\n border: 1.5px dashed var(--line);\r\n border-radius: 16px;\r\n font-size: 12.5px;\r\n background: var(--bg);\r\n color: var(--fg2);\r\n cursor: pointer;\r\n font-family: inherit;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n min-height: 36px;\r\n -webkit-tap-highlight-color: transparent;\r\n touch-action: manipulation;\r\n}\r\n.ss-more:hover { border-color: var(--ss-brand); color: var(--ss-brand); background: #fff; }\r\n.ss-new { display: flex; gap: 8px; margin-top: 10px; }\r\n.ss-new input {\r\n flex: 1;\r\n min-width: 0;\r\n padding: 8px 12px;\r\n border: 1.5px solid var(--line);\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n background: #fff;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n min-height: 40px;\r\n}\r\n.ss-new input:focus { outline: none; border-color: var(--ss-brand); }\r\n.ss-new button {\r\n padding: 0 14px;\r\n border: 1.5px solid var(--line);\r\n border-radius: 8px;\r\n background: #fff;\r\n color: var(--fg);\r\n font-size: 13px;\r\n cursor: pointer;\r\n white-space: nowrap;\r\n font-family: inherit;\r\n min-height: 40px;\r\n}\r\n.ss-new button:hover { border-color: var(--ss-brand); color: var(--ss-brand); }\r\n.ss-hint { margin-top: 6px; font-size: 12px; color: #b25b00; display: none; }\r\n.ss-empty {\r\n margin-top: 8px;\r\n width: 100%;\r\n padding: 8px;\r\n border: 1px dashed var(--line);\r\n border-radius: 8px;\r\n background: transparent;\r\n color: var(--fg3);\r\n cursor: pointer;\r\n font-family: inherit;\r\n font-size: 13px;\r\n min-height: 36px;\r\n}\r\n.ss-empty:hover { border-color: var(--ss-brand); color: var(--ss-brand); }\r\n/* 降级模式(options 空数组等 → 普通输入) */\r\n.ss-plain {\r\n width: 100%;\r\n box-sizing: border-box;\r\n padding: 8px 12px;\r\n border: 1.5px solid var(--line);\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-family: inherit;\r\n color: var(--fg);\r\n background: #fff;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n min-height: 44px;\r\n}\r\n.ss-plain:focus { outline: none; border-color: var(--ss-brand); box-shadow: 0 0 0 3px rgba(18, 58, 99, .12); }\r\n@media (max-width: 820px) {\r\n .ss-new { flex-direction: column; align-items: stretch; }\r\n .ss-new button { min-height: 44px; }\r\n .ss-chip { min-height: 38px; }\r\n}\r\n</style>\r\n\r\n<script>\r\n/* ===== 技能身份与数据(scene-data 契约 v1 · 全外部注入)===== */\r\nvar HELP = JSON.parse(document.getElementById('help-data').textContent);\r\nvar SKILL_NAME = HELP.skill_name || '';\r\nvar TITLE = HELP.title || '能力速查台';\r\nvar SUBTITLE = HELP.subtitle || '';\r\nvar META_BLOCKS = HELP.meta_blocks || [];\r\nvar INIT_BANNER = HELP.init_banner || null;\r\nvar CONTACT = HELP.contact || null;\r\nvar SKILL_VERSION = HELP.version || '';\r\nvar ABOUT_EXTRA = {};\r\nvar RECOMMENDATIONS = HELP.recommendations || [];\r\n\r\n/* 契约 v1 (groups[{id,icon,label,subgroups[{id,label,scenes[]}]}])\r\n -> 原型内部结构 (key/icon/name + subgroups[{name,scenes}]) */\r\nfunction normalizeScenes(scenes){\r\n return (scenes || []).map(function(s){\r\n return {\r\n id: s.id,\r\n name: s.title,\r\n chip: s.wake_word,\r\n types: (s.types && s.types.length ? s.types : []),\r\n dev: (s.status === '【待开发】'),\r\n prompt: s.prompt_template,\r\n params: (s.editable_fields || []).map(function(f){\r\n return { key: f.name, label: f.label, value: f.value || '', req: !!f.required, hint: f.hint || '' };\r\n })\r\n };\r\n });\r\n}\r\nvar GROUPS = (HELP.groups || []).map(function(g){\r\n return {\r\n key: g.id, icon: g.icon || 'grid', name: g.label,\r\n subgroups: (g.subgroups || []).map(function(sg){\r\n return { name: sg.label, scenes: normalizeScenes(sg.scenes) };\r\n })\r\n };\r\n});\r\n\r\n/* ===== 扁平化索引(原型逻辑 · 渲染/卡片/搜索依赖)===== */\r\nvar ALL = [];\r\nvar SCENE = {};\r\nGROUPS.forEach(function(g){\r\n g.subgroups.forEach(function(sg){\r\n sg.scenes.forEach(function(s){\r\n s.g = g.name; s.gkey = g.key; s.gico = g.icon; s.sg = sg.name;\r\n ALL.push(s); SCENE[s.id] = s;\r\n });\r\n });\r\n});\r\n\r\nfunction esc(s){ return String(s==null?'':s).replace(/[&<>\"']/g, function(c){ return {'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]; }); }\r\n/* 类型徽章: types 数组元素可为字符串(默认配色)或 {text,bg,fg}(调用者自定义颜色, 缺省走默认) */\r\nvar TYPE_DEFAULT = {\r\n '采集': {bg:'#e7f8ee', fg:'#1a7a3a'},\r\n '查看': {bg:'#e8f2ff', fg:'#0a63ce'},\r\n '结果': {bg:'#e8f2ff', fg:'#0a63ce'},\r\n '向导': {bg:'#e2f7f5', fg:'#00897b'},\r\n '批量': {bg:'#f3e9fb', fg:'#8e3fc9'},\r\n '校验': {bg:'#e8f2ff', fg:'#0a63ce'},\r\n '选择': {bg:'#e8f2ff', fg:'#0a63ce'},\r\n '过程': {bg:'#e2f7f5', fg:'#00897b'},\r\n '回执': {bg:'#e8f2ff', fg:'#0a63ce'},\r\n '录入': {bg:'#e7f8ee', fg:'#1a7a3a'}\r\n};\r\nfunction typeBadgeHTML(t){\r\n var text, bg, fg;\r\n if (t && typeof t === 'object') {\r\n text = t.text;\r\n var d = TYPE_DEFAULT[text] || TYPE_DEFAULT['查看'];\r\n bg = t.bg || d.bg;\r\n fg = t.fg || d.fg;\r\n } else {\r\n text = t;\r\n var d2 = TYPE_DEFAULT[text] || TYPE_DEFAULT['查看'];\r\n bg = d2.bg; fg = d2.fg;\r\n }\r\n return '<span class=\"type-badge\" style=\"background:' + esc(bg) + ';color:' + esc(fg) + '\">' + esc(text) + '</span>';\r\n}\r\nfunction chipHTML(s){\r\n var h = '<span class=\"chip\">' + esc(s.chip) + '</span>';\r\n if (s.dev) h += '<span class=\"type-badge t-dev\">待开发</span>';\r\n (s.types || []).forEach(function(t){ h += typeBadgeHTML(t); });\r\n return h;\r\n}\r\n\r\n/* ===== SVG 图标库 ===== */\r\nvar SVG_ICONS = {\r\n write:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z\"/></svg>',\r\n calendar:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"/><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"/><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"/><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"/></svg>',\r\n chart:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" y1=\"20\" x2=\"18\" y2=\"10\"/><line x1=\"12\" y1=\"20\" x2=\"12\" y2=\"4\"/><line x1=\"6\" y1=\"20\" x2=\"6\" y2=\"14\"/></svg>',\r\n grid:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/></svg>',\r\n target:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/></svg>',\r\n rocket:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z\"/><path d=\"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2Z\"/><path d=\"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0\"/><path d=\"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5\"/></svg>'\r\n};\r\nfunction tabIconHTML(g){\r\n var svg = SVG_ICONS[g.icon];\r\n if (svg) return '<span class=\"t-ico\">' + svg + '</span>';\r\n return '<span class=\"t-ico\"><span class=\"t-emoji\">' + esc(g.icon) + '</span></span>';\r\n}\r\n\r\n/* ===== 参数机制:prompt + 末尾每行「key: value」===== */\r\nfunction readParams(id){\r\n var fm = document.querySelector('.pform[data-pid=\"' + id + '\"]');\r\n var v = {};\r\n if (fm) fm.querySelectorAll('input[data-p]').forEach(function(inp){ v[inp.dataset.p] = inp.value; });\r\n return v;\r\n}\r\nfunction buildPrompt(s, v){\r\n v = v || {};\r\n var lines = [s.prompt];\r\n var paramLines = [];\r\n (s.params || []).forEach(function(p){\r\n var val = (v[p.key] != null ? v[p.key] : '').trim();\r\n if (val) paramLines.push(p.label + ': ' + val);\r\n });\r\n if (paramLines.length) lines.push(''); /* 空行分隔 */\r\n lines = lines.concat(paramLines);\r\n return lines.join('\\n');\r\n}\r\nfunction getMissing(s, v){\r\n return (s.params || []).filter(function(p){ return p.req && !(v[p.key] || '').trim(); }).map(function(p){ return p.label; });\r\n}\r\n\r\n/* ===== 渲染主页面 ===== */\r\nvar screen = document.getElementById('screen');\r\nvar h = '<div class=\"app\"><div class=\"app-inner\">' +\r\n '<header class=\"hero\"><div class=\"h-left\"><div class=\"eyebrow\">' + esc(SKILL_NAME) + '</div><h1>' + esc(TITLE) + '</h1><div class=\"lead\">' + ALL.length + ' 场景 · 点击卡片查看详情并复制指令</div></div><span class=\"h-badge\">' + ALL.length + ' 场景</span>' +\r\n '<div class=\"hero-steps\"><span class=\"step\">🔍 找场景</span><span class=\"arrow\">→</span><span class=\"step\">📋 复制指令</span><span class=\"arrow\">→</span><span class=\"step\">💬 发给 AI</span></div>' +\r\n '</header>' +\r\n (INIT_BANNER && !INIT_BANNER.hidden ? '<div class=\"init-banner\" id=\"initBanner\"><div class=\"ib-text\"><div class=\"ib-title\">' + esc(INIT_BANNER.title) + '</div><div class=\"ib-sub\">' + esc(INIT_BANNER.subtitle) + '</div></div><button class=\"copy-btn\" data-c=\"' + esc(INIT_BANNER.prompt) + '\">' + esc(INIT_BANNER.button_text) + '</button>' + (INIT_BANNER.closable === false ? '' : '<button class=\"ib-close\" id=\"initClose\" aria-label=\"关闭\">✕</button>') +\r\n (INIT_BANNER.steps && INIT_BANNER.steps.length ? '<div class=\"init-steps\">' + INIT_BANNER.steps.map(function(st, i){ return '<div class=\"init-step\"><div class=\"s-n\">' + (i+1) + '</div><div class=\"s-t\">' + esc(st.title) + '</div>' + (st.desc ? '<div class=\"s-d\">' + esc(st.desc) + '</div>' : '') + '</div>'; }).join('') + '</div>' : '') +\r\n '</div>' : '') +\r\n '<div class=\"search-wrap\"><div class=\"search-box\"><span class=\"s-icon\">🔍</span><input id=\"sB\" type=\"search\" placeholder=\"搜索全部场景\" autocomplete=\"off\"><button class=\"s-clear\" id=\"sClear\">✕</button></div></div>' +\r\n '<div class=\"hitcount\" id=\"hitC\" hidden></div><div class=\"search-empty\" id=\"emptyC\" hidden>没有找到相关场景,换个词试试~</div>' +\r\n '<div class=\"pages\" id=\"pages\">';\r\nGROUPS.forEach(function(g){\r\n h += '<div class=\"page\" data-page=\"' + g.key + '\">';\r\n g.subgroups.forEach(function(sg){\r\n h += '<details class=\"subgroup\" open><summary>' + sg.name + '<span class=\"sg-count\">' + sg.scenes.length + '</span></summary><div class=\"sg-body\"><div class=\"grid\">';\r\n sg.scenes.forEach(function(s){\r\n h += '<div class=\"mini\" data-key=\"' + s.id + '\">' +\r\n '<div class=\"m-top\">' + chipHTML(s) + '</div>' +\r\n '<div class=\"m-bottom\">' +\r\n '<div class=\"m-name\">' + esc(s.name) + '</div>' +\r\n '<button class=\"copy-btn\" data-c=\"' + esc(buildPrompt(s, {})) + '\">复制</button>' +\r\n '</div>' +\r\n '</div>';\r\n });\r\n h += '</div></div></details>';\r\n });\r\n h += '</div>';\r\n});\r\n/* ===== 关于 Tab(最后 · 联系作者 + 版本 + 其他技能 · 拼在 pages 容器内)===== */\r\nh += '<div class=\"page\" data-page=\"about\">';\r\n/* ① 联系作者(第一位)*/\r\nif (CONTACT && CONTACT.items && CONTACT.items.length) {\r\n h += '<div class=\"about-sec\"><div class=\"as-head\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;flex-shrink:0\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg> 联系作者</div><div class=\"as-body\">';\r\n CONTACT.items.forEach(function(it){\r\n if (it.url && String(it.value).indexOf('http') === 0) {\r\n h += '<div class=\"about-row\"><span class=\"a-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg></span><div class=\"a-t\"><b>' + esc(it.label) + '</b><a class=\"a-link\" href=\"' + esc(it.value) + '\" target=\"_blank\" rel=\"noopener\">' + esc(it.value) + '</a></div></div>';\r\n } else {\r\n h += '<div class=\"about-row\"><span class=\"a-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg></span><div class=\"a-t\"><b>' + esc(it.label) + '</b><span>' + esc(it.value) + '</span></div></div>';\r\n }\r\n });\r\n if (CONTACT.copy_all) {\r\n var contactTxt = CONTACT.items.map(function(it){ return it.label + ': ' + it.value; }).join('\\n');\r\n h += '<div class=\"about-row\"><span class=\"a-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg></span><div class=\"a-t\"><b>一键复制</b><span>全部联系信息</span></div><button class=\"copy-btn a-copy\" data-c=\"' + esc(contactTxt) + '\">复制</button></div>';\r\n }\r\n h += '</div></div>';\r\n}\r\n/* ② 版本(第二位)*/\r\nh += '<div class=\"about-sec\"><div class=\"as-head\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;flex-shrink:0\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><line x1=\"12\" y1=\"11\" x2=\"12\" y2=\"16\"/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"/></svg> 版本</div><div class=\"as-body\"><div class=\"about-row\"><span class=\"a-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"/><line x1=\"7\" y1=\"7\" x2=\"7.01\" y2=\"7\"/></svg></span><div class=\"a-t\"><b>' + esc(SKILL_NAME) + '</b><span>v' + esc(SKILL_VERSION) + ' · HELP 模板 v4</span></div></div></div></div>';\r\n/* ③ 其他技能(第三位 · SVG 图标)*/\r\nif (RECOMMENDATIONS && RECOMMENDATIONS.length) {\r\n h += '<div class=\"about-sec\"><div class=\"as-head\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;flex-shrink:0\"><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/><path d=\"M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0 0 12 2z\"/></svg> 其他技能</div><div class=\"as-body\">';\r\n RECOMMENDATIONS.forEach(function(r){\r\n h += '<div class=\"about-row\"><span class=\"a-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\"/></svg></span><div class=\"a-t\"><b>' + esc(r.name) + '</b><span>' + esc(r.desc) + '</span></div>' + (r.wake ? '<span class=\"a-badge\">' + esc(r.wake) + '</span>' : '') + '</div>';\r\n });\r\n h += '</div></div>';\r\n}\r\nh += '</div>'; /* 关闭 about page */\r\nh += '</div></div>'; /* 关闭 pages + app-inner */\r\nh += '<div class=\"tab-bar\" id=\"tabBar\">';\r\nGROUPS.forEach(function(g, i){\r\n h += '<button class=\"tab' + (i===0?' active':'') + '\" data-nav=\"' + g.key + '\">' + tabIconHTML(g) + g.name + '</button>';\r\n});\r\nh += '<button class=\"tab\" data-nav=\"about\"><span class=\"t-ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><line x1=\"12\" y1=\"11\" x2=\"12\" y2=\"16\"/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"/></svg></span>关于</button>';\r\nh += '</div></div>';\r\nscreen.innerHTML = h;\r\n\r\n/* 初始化横幅手动关闭(✕)*/\r\nvar initClose = document.getElementById('initClose');\r\nif (initClose) initClose.addEventListener('click', function(){\r\n var b = document.getElementById('initBanner');\r\n if (b) b.style.display = 'none';\r\n});\r\n/* ===== 自然滚动: 内容跟手, 搜索栏 sticky 吸顶(CSS) =====\r\n 旧版两段式 transform 抵消导致: 吸顶前内容不跟手、短页(关于)滚不到吸顶 → 全部移除 */\r\n/* 页面高度 = 内容自然高度(不撑高): 内容短则页面短, 滑到底即停, 不出灰色空白\r\n (原实现 pg.style.minHeight = screen.clientHeight 把短页撑到一屏高,\r\n 内容下方露出透明 page 背景 → 灰色底被顶上来;#289 用户验收拍板)*/\r\n\r\n/* ===== 滑动切页(ViewPager)===== */\r\nvar pages = document.getElementById('pages');\r\nvar tabs = Array.prototype.slice.call(document.querySelectorAll('.tab'));\r\nvar screen = document.querySelector('.screen');\r\n/* ③ 容器高度锁定: 用内容实际全高(而非首屏高), 防空白可滑也防内容被截断 */\r\nfunction lockPagesHeight(idx){\r\n if (idx === undefined) idx = Math.min(curIndex(), pages.children.length - 1);\r\n var pg = pages.children[idx];\r\n if (!pg) return;\r\n pages.style.height = pg.scrollHeight + 'px';\r\n}\r\nfunction curIndex(){ return Math.round(pages.scrollLeft / pages.clientWidth); }\r\nfunction syncTab(){\r\n var i = Math.min(curIndex(), tabs.length - 1);\r\n tabs.forEach(function(t, j){ t.classList.toggle('active', j === i); });\r\n}\r\npages.addEventListener('scroll', function(){\r\n clearTimeout(syncTab._t);\r\n syncTab._t = setTimeout(function(){\r\n if (Date.now() < syncTab._lockUntil) return; /* 点击后的滚动动画期不覆盖 active */\r\n syncTab();\r\n lockPagesHeight(); /* 横向滑动结束也校准高度 */\r\n }, 15);\r\n}, {passive:true});\r\ntabs.forEach(function(t){\r\n t.addEventListener('click', function(){\r\n var i = tabs.indexOf(t);\r\n tabs.forEach(function(x, j){ x.classList.toggle('active', j === i); });\r\n syncTab._lockUntil = Date.now() + 900; /* 锁 ~0.9s: 覆盖点击动画期即可, 缩短滑动跟手延迟 */\r\n /* 切换立即: ① 归零纵向滚动(防旧位置灰色遮挡) ② 立即锁定新页高度(防内容截断/滑不动) */\r\n /* 手机端: .screen 是滚动容器, scrollTop=0 = 回到内容顶部(页面不跳);\r\n 宽屏: .screen overflow:visible, 真正滚动的是 window —— 定位到 pages 容器顶部(搜索栏处), 不跳整页最上层 */\r\n if (screen.scrollTop !== undefined && screen.clientHeight > 0 && screen.scrollHeight > screen.clientHeight) {\r\n screen.scrollTop = 0;\r\n } else {\r\n var pagesTop = pages.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop || 0);\r\n /* 落点必须再让开吸顶搜索栏: 只减 8px 时 .pages 顶正好落在吸顶栏之下, 当页第一个元素(首个子功能组摘要)被压成一条缝,\r\n 点不到也看不见 —— 吸顶栏高动态取(不写死像素: 视口宽/字体缩放都会改它), 取不到时按 0 退化为原行为 */\r\n var stickyBar = document.querySelector('.search-wrap');\r\n window.scrollTo(0, Math.max(0, pagesTop - (stickyBar ? stickyBar.offsetHeight : 0) - 8));\r\n }\r\n pages.style.scrollSnapType = 'none';\r\n pages.scrollTo({left: i * pages.clientWidth, behavior:'smooth'});\r\n lockPagesHeight(i); /* 直接传目标索引, 不等动画 */\r\n /* 点击的 Tab 尽可能居中显示(首尾贴边) */\r\n centerTab(t);\r\n setTimeout(function(){\r\n pages.style.scrollSnapType = '';\r\n syncTab._lockUntil = 0; /* 动画结束立即解锁, 之后滑动变色实时跟手 */\r\n lockPagesHeight(i); /* 动画结束后再校准一次 */\r\n }, 900);\r\n });\r\n});\r\n/* ===== Tab 条: 点击的 Tab 居中显示 ===== */\r\nfunction centerTab(tab){\r\n var bar = document.getElementById('tabBar');\r\n var max = bar.scrollWidth - bar.clientWidth;\r\n var target = tab.offsetLeft + tab.offsetWidth / 2 - bar.clientWidth / 2;\r\n target = Math.max(0, Math.min(target, max));\r\n bar.scrollTo({left: target, behavior: 'smooth'});\r\n}\r\n\r\n/* ===== Sheet 弹层(B3: 复制不自动关 · B4: 无参数显 prompt)===== */\r\nvar mask = document.getElementById('sheetMask');\r\nvar sheet = document.getElementById('sheet');\r\nvar shHead = document.getElementById('shHead');\r\nvar shBody = document.getElementById('shBody');\r\nvar shCopy = document.getElementById('shCopy');\r\nvar curScene = null;\r\n\r\nfunction openSheet(s){\r\n curScene = s;\r\n shHead.innerHTML = '<span class=\"s-title\">' + esc(s.name) + '</span>' + chipHTML(s);\r\n var form = '';\r\n var hasParams = s.params && s.params.length;\r\n if (hasParams) {\r\n form = '<div class=\"pform\" data-pid=\"' + s.id + '\">';\r\n s.params.forEach(function(p){\r\n form += '<div class=\"pfield\"><label>' + esc(p.label) + (p.req ? '' : '<em>选填</em>') + '</label>' +\r\n '<input data-p=\"' + esc(p.key) + '\" value=\"' + esc(p.value || '') + '\" placeholder=\"' + esc(p.hint || '') + '\"></div>';\r\n });\r\n form += '</div>';\r\n }\r\n /* 预览:prompt + 末尾 key:value(有参数时实时重组)*/\r\n var prev = '<div class=\"pprev\"><span class=\"pv-label\">Prompt 预览</span><span data-prev>' + esc(buildPrompt(s, {})) + '</span></div>';\r\n shBody.innerHTML = form + prev;\r\n shCopy.dataset.pid = s.id;\r\n shCopy.dataset.c = buildPrompt(s, {});\r\n /* 参数化实时预览 */\r\n var fm = sheet.querySelector('.pform');\r\n if (fm) fm.addEventListener('input', function(){\r\n var pv = sheet.querySelector('[data-prev]');\r\n if (pv) pv.textContent = buildPrompt(s, readParams(s.id));\r\n });\r\n mask.classList.add('show');\r\n sheet.classList.add('show');\r\n sheet.querySelector('.s-scroll').scrollTop = 0;\r\n}\r\nfunction closeSheet(){\r\n mask.classList.remove('show');\r\n sheet.classList.remove('show');\r\n}\r\nmask.addEventListener('click', closeSheet);\r\ndocument.getElementById('sheetClose').addEventListener('click', closeSheet);\r\n\r\n/* ===== 卡片交互 ===== */\r\nscreen.addEventListener('click', function(e){\r\n var mini = e.target.closest('.mini');\r\n if (mini) {\r\n if (e.target.closest('[data-c]')) return;\r\n openSheet(SCENE[mini.dataset.key]);\r\n }\r\n});\r\n\r\n/* ===== Toast(公共组件)===== */\r\nfunction showToast(){\r\n toast('已复制', '粘贴给 AI,技能会自动执行,完成后你会拿到结果 HTML。', {icon:'copy'});\r\n}\r\nfunction toastMsg(msg){\r\n toast('请先填写: ' + msg, '填写完整后再复制指令', {icon:'warn', badge:{text:'未完成', type:'warn'}});\r\n}\r\nfunction doCopy(text){\r\n var ta = document.createElement('textarea');\r\n ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';\r\n document.body.appendChild(ta); ta.select();\r\n try { document.execCommand('copy'); } catch(e){}\r\n document.body.removeChild(ta);\r\n}\r\n\r\n/* ===== 全局复制(含参数校验)===== */\r\ndocument.addEventListener('click', function(e){\r\n var b = e.target.closest('[data-c]');\r\n if (!b) return;\r\n e.preventDefault(); e.stopPropagation();\r\n var s = SCENE[b.dataset.pid || (b.closest('.mini') ? b.closest('.mini').dataset.key : '')];\r\n var text = b.dataset.c;\r\n if (s && s.params && s.params.length) {\r\n var v = readParams(s.id);\r\n var miss = getMissing(s, v);\r\n if (miss.length) { toastMsg(miss.join('、')); return; }\r\n text = buildPrompt(s, v);\r\n }\r\n doCopy(text);\r\n showToast();\r\n /* B3: 复制后不自动关弹窗 */\r\n});\r\n\r\n/* ===== 搜索(全局跨 Tab + 自动跳转 + 高亮)===== */\r\nvar sB = document.getElementById('sB');\r\nvar sClear = document.getElementById('sClear');\r\nvar lastQuery = '';\r\nfunction doSearch(){\r\n var q = sB.value.trim().toLowerCase();\r\n sClear.style.display = q ? 'flex' : 'none';\r\n if (!q) {\r\n var cur = curIndex();\r\n Array.prototype.forEach.call(pages.children, function(pg, i){\r\n pg.querySelectorAll('.mini').forEach(function(m){\r\n m.style.display = '';\r\n m.querySelector('.m-name').innerHTML = esc(SCENE[m.dataset.key].name);\r\n });\r\n pg.querySelectorAll('.subgroup').forEach(function(sg){ sg.style.display = ''; });\r\n });\r\n /* 恢复全量后必须同步重算高度锁: 筛选期 .pages 被锁在「筛选后」的当页高, 不重算就当页被锁矮,\r\n 后半页连同末张场景卡一起滚不到 —— 这里的 scrollTo 目标就是当前位置(不触发 scroll 事件), 兜不住这次重算 */\r\n lockPagesHeight();\r\n if (lastQuery) pages.scrollTo({left: cur * pages.clientWidth, behavior:'smooth'});\r\n lastQuery = '';\r\n document.getElementById('hitC').hidden = true;\r\n document.getElementById('emptyC').hidden = true;\r\n return;\r\n }\r\n lastQuery = q;\r\n var total = 0, firstHitPage = -1;\r\n Array.prototype.forEach.call(pages.children, function(pg, i){\r\n var pageHit = 0;\r\n pg.querySelectorAll('.mini').forEach(function(m){\r\n var show = m.textContent.toLowerCase().indexOf(q) > -1;\r\n m.style.display = show ? '' : 'none';\r\n if (show) pageHit++;\r\n var orig = SCENE[m.dataset.key].name;\r\n var i2 = orig.toLowerCase().indexOf(q);\r\n if (q && i2 >= 0) m.querySelector('.m-name').innerHTML = esc(orig.slice(0,i2)) + '<mark>' + esc(orig.slice(i2,i2+q.length)) + '</mark>' + esc(orig.slice(i2+q.length));\r\n else m.querySelector('.m-name').innerHTML = esc(orig);\r\n });\r\n /* 子功能组:无命中则隐藏 */\r\n pg.querySelectorAll('.subgroup').forEach(function(sg){\r\n var vis = sg.querySelectorAll('.mini:not([style*=\"none\"])').length > 0;\r\n sg.style.display = vis ? '' : 'none';\r\n if (q && vis) sg.open = true;\r\n });\r\n total += pageHit;\r\n if (pageHit > 0 && firstHitPage === -1) firstHitPage = i;\r\n });\r\n if (firstHitPage >= 0 && firstHitPage !== curIndex()) {\r\n pages.scrollTo({left: firstHitPage * pages.clientWidth, behavior:'smooth'});\r\n }\r\n document.getElementById('hitC').hidden = false;\r\n document.getElementById('hitC').textContent = '匹配 ' + total + ' 个场景';\r\n document.getElementById('emptyC').hidden = total > 0;\r\n}\r\nsB.addEventListener('input', doSearch);\r\nsClear.addEventListener('click', function(){ sB.value = ''; doSearch(); sB.focus(); });\r\n/* 初始锁定 + 子功能组折叠/展开时高度校准 */\r\nlockPagesHeight();\r\ndocument.addEventListener('click', function(e){\r\n var sg = e.target.closest('.subgroup > summary');\r\n if (sg) setTimeout(lockPagesHeight, 60); /* details 切换后重算 */\r\n});\r\n</script>\r\n</body>\r\n</html>\r\n";
|
|
33
|
+
/** help-data 容器开标签(PREFIX 与 SUFFIX 的切分锚点)。 */
|
|
34
|
+
export const HELP_SHELL_DATA_OPEN = "<script id=\"help-data\" type=\"application/json\">";
|
|
35
|
+
/** 文档标题占位(在 PREFIX 内;渲染时替换,源里写死即回原型水印)。 */
|
|
36
|
+
export const HELP_SHELL_TITLE_SLOT = "__HELP_TITLE__";
|
|
37
|
+
/** 文本节点转义(标题只进 <title>,五字符即够)。 */
|
|
38
|
+
function escapeTitleText(s) {
|
|
39
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
40
|
+
}
|
|
41
|
+
/** 文档标题=`skill_name · title`;`title` 已自带技能名时**不再重复前缀**(#145)。
|
|
42
|
+
*
|
|
43
|
+
* 背景:#141 把 `<title>` 从原型水印改成「由 5 键派生」,规则写作 `skill_name + ' · ' + title`。
|
|
44
|
+
* 但**老世代口径要求大标题含技能名**(老 `render_help.py` 自述的 #303 验收反馈:技能名应在大字行,
|
|
45
|
+
* eyebrow 小字不算),于是饼干记账这类技能的 `title` 本身就是 `饼干记账 · 使用手册(HELP)`——
|
|
46
|
+
* 照拼即得 `饼干记账 · 饼干记账 · 使用手册(HELP)`(标签页重复)。
|
|
47
|
+
* 故此处只加一条**结构性**判断:`title` 含 `skill_name` ⇒ 直接用 `title`;卡路里(`title` 不含技能名)
|
|
48
|
+
* 走原分支,输出逐字节不变。 */
|
|
49
|
+
export function composeDocTitle(data) {
|
|
50
|
+
const skill = String(data.skill_name);
|
|
51
|
+
const title = String(data.title);
|
|
52
|
+
return title.includes(skill) ? title : skill + ' · ' + title;
|
|
53
|
+
}
|
|
54
|
+
/** 5 键 JSON → 全壳 HTML(与老实物同壳;小于号转义防 script 破壳,parse 后逐字一致)。 */
|
|
55
|
+
export function renderHelpShellHtml(data) {
|
|
56
|
+
if (!data || !Array.isArray(data.groups) || data.groups.length === 0) {
|
|
57
|
+
throw new HelpShellError('HELP 渲染缺分组(不返空页)。');
|
|
58
|
+
}
|
|
59
|
+
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
60
|
+
const title = escapeTitleText(composeDocTitle(data));
|
|
61
|
+
return HELP_SHELL_PREFIX.split(HELP_SHELL_TITLE_SLOT).join(title) + json + HELP_SHELL_SUFFIX;
|
|
62
|
+
}
|
|
63
|
+
/** #136 标准出口(`renderHelpShellHtml` 同实现;skill 侧只传自家 HELP JSON 即接)。 */
|
|
64
|
+
export const renderHelpShell = renderHelpShellHtml;
|
|
65
|
+
//# sourceMappingURL=helpShell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpShell.js","sourceRoot":"","sources":["../src/helpShell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAWH,4DAA4D;AAC5D,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAiB;IAE9B,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED,sDAAsD;AACtD,MAAM,CAAC,MAAM,iBAAiB,GAAW,+lhBAA+lhB,CAAC;AAEzohB,mFAAmF;AACnF,MAAM,CAAC,MAAM,iBAAiB,GAAW,8rUAA8rU,GAAG,kMAAkM,GAAG,84IAA84I,GAAG,mrEAAmrE,GAAG,oEAAoE,GAAG,qxnEAAqxnE,CAAC;AAEn1pF,8CAA8C;AAC9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,qDAA8D,CAAC;AAEnG,2CAA2C;AAC3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAyB,CAAC;AAE/D,kCAAkC;AAClC,SAAS,eAAe,CAAC,CAAS;IAChC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC9G,CAAC;AAED;;;;;;;mBAOmB;AACnB,MAAM,UAAU,eAAe,CAAC,IAA+D;IAC7F,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAC/D,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,mBAAmB,CAAC,IAAmB;IACrD,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,cAAc,CAAC,mBAAmB,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,eAAe,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,OAAO,iBAAiB,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,iBAAiB,CAAC;AAC/F,CAAC;AAED,qEAAqE;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,15 @@ export { buildDataText, buildLogText } from './text.js';
|
|
|
24
24
|
* `ChartError` **不在此导出**——与 `TemplateError`/`ControlsError` 同口径(冻结面无该运行时条目,
|
|
25
25
|
* 调用方按 `name`/`code` 判定)。 */
|
|
26
26
|
export { buildChartsHelpersJs, charts } from './charts.js';
|
|
27
|
-
/** #78 HELP
|
|
28
|
-
* `HelpSchemaError
|
|
27
|
+
/** #78 HELP模板(契约 §3.5.3/§6.5):数据页分型,走 `fillTemplate`,`#88` 直接复用 `HelpShellInput`。
|
|
28
|
+
* `HelpSchemaError`/内置help模板 **不在此导出**(同口径;模板可经 `HelpShellInput.template` 覆盖)。 */
|
|
29
29
|
export { renderHelpShell } from './help.js';
|
|
30
|
+
/** #525 页面级两层(只追加,不动上面任何一条 export):**移动端配方**(断点/触摸区/安全区/
|
|
31
|
+
* 窄屏表格行为/页内定位)与**页面级形状件**(事实条/图片与 GIF 容器/时间轴条/媒体占位件)。
|
|
32
|
+
* 与 `blocks.js` 的 12 区块样式区分住:这两层是「整页怎么摆」,不参与区块组合;
|
|
33
|
+
* 是否启用由整页装配的 `pageUi` 位决定,不启用则产出物逐字节不变。 */
|
|
34
|
+
export { PAGE_UI_CLASS, PAGE_UI_VIEWPORT, pageUiCss } from './pageUi.js';
|
|
35
|
+
export type { PageUiCssInput } from './pageUi.js';
|
|
36
|
+
export { MEDIA_RATIOS, pageShapeCss, renderFactStrip, renderMediaFigure, renderMediaPlaceholder, renderTimelineRows, } from './pageShapes.js';
|
|
37
|
+
export type { FactItemInput, FactStripInput, FactTone, MediaFigureInput, MediaPlaceholderInput, MediaRatio, TimelineRowInput, TimelineRowsInput, } from './pageShapes.js';
|
|
30
38
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnG,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAClI,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,4BAA4B,EAAE,yBAAyB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACjH,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACvG,sEAAsE;AACtE,cAAc,iBAAiB,CAAC;AAChC;+DAC+D;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C;qEACqE;AACrE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB;;8BAE8B;AAC9B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACxD;;;8BAG8B;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnG,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAClI,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,EAAE,4BAA4B,EAAE,yBAAyB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACjH,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACvG,sEAAsE;AACtE,cAAc,iBAAiB,CAAC;AAChC;+DAC+D;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C;qEACqE;AACrE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB;;8BAE8B;AAC9B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACxD;;;8BAG8B;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D;mFACmF;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C;;;4CAG4C;AAC5C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,qBAAqB,EACrB,UAAU,EACV,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,13 @@ export { buildDataText, buildLogText } from './text.js';
|
|
|
20
20
|
* `ChartError` **不在此导出**——与 `TemplateError`/`ControlsError` 同口径(冻结面无该运行时条目,
|
|
21
21
|
* 调用方按 `name`/`code` 判定)。 */
|
|
22
22
|
export { buildChartsHelpersJs, charts } from './charts.js';
|
|
23
|
-
/** #78 HELP
|
|
24
|
-
* `HelpSchemaError
|
|
23
|
+
/** #78 HELP模板(契约 §3.5.3/§6.5):数据页分型,走 `fillTemplate`,`#88` 直接复用 `HelpShellInput`。
|
|
24
|
+
* `HelpSchemaError`/内置help模板 **不在此导出**(同口径;模板可经 `HelpShellInput.template` 覆盖)。 */
|
|
25
25
|
export { renderHelpShell } from './help.js';
|
|
26
|
+
/** #525 页面级两层(只追加,不动上面任何一条 export):**移动端配方**(断点/触摸区/安全区/
|
|
27
|
+
* 窄屏表格行为/页内定位)与**页面级形状件**(事实条/图片与 GIF 容器/时间轴条/媒体占位件)。
|
|
28
|
+
* 与 `blocks.js` 的 12 区块样式区分住:这两层是「整页怎么摆」,不参与区块组合;
|
|
29
|
+
* 是否启用由整页装配的 `pageUi` 位决定,不启用则产出物逐字节不变。 */
|
|
30
|
+
export { PAGE_UI_CLASS, PAGE_UI_VIEWPORT, pageUiCss } from './pageUi.js';
|
|
31
|
+
export { MEDIA_RATIOS, pageShapeCss, renderFactStrip, renderMediaFigure, renderMediaPlaceholder, renderTimelineRows, } from './pageShapes.js';
|
|
26
32
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnG,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAElI,OAAO,EAAE,4BAA4B,EAAE,yBAAyB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEjH,sEAAsE;AACtE,cAAc,iBAAiB,CAAC;AAChC;+DAC+D;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C;qEACqE;AACrE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB;;8BAE8B;AAC9B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACxD;;;8BAG8B;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnG,OAAO,EAAE,WAAW,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAElI,OAAO,EAAE,4BAA4B,EAAE,yBAAyB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEjH,sEAAsE;AACtE,cAAc,iBAAiB,CAAC;AAChC;+DAC+D;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C;qEACqE;AACrE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB;;8BAE8B;AAC9B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACxD;;;8BAG8B;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3D;mFACmF;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C;;;4CAG4C;AAC5C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC"}
|
package/dist/injector.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
*
|
|
3
3
|
* better-sidebar 槽位注册只许住这里:link-core/combos 禁止自装配
|
|
4
4
|
*(tooling/check-boundaries.mjs 可执行断言)。host 无关设计——
|
|
5
|
-
* 以 SlotsPort 注入真实服务,B
|
|
5
|
+
* 以 SlotsPort 注入真实服务,B 实施实测时接线,单测用假端口。
|
|
6
6
|
* 语义抄 matt 实证:幂等(disposer 非空即跳过)+ 有界重试(1s×10)+
|
|
7
7
|
* 卸载清理(ctx.effect 同构),成功后无轮询;openTab 走 path seed 内容型打开。
|
|
8
8
|
*/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** 撞名策略(四态;只表态「撞名怎么办」,**不改** `stem`/`file` 怎么解释。三支 `reuse` 见件头)。
|
|
2
|
+
*
|
|
3
|
+
* `reuse` 的三支都「有就不新建」:`byDay`(当天已有)/`byContent`(内容相同)/`{byAge: 毫秒}`(不超龄)。 */
|
|
4
|
+
export type HtmlOnExists = 'succession' | 'overwrite' | 'fail' | {
|
|
5
|
+
readonly reuse: 'byDay' | 'byContent' | {
|
|
6
|
+
readonly byAge: number;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/** **HELP 产物的缺省复用窗口**:一天(小时数)——「24 小时内只产出 1 个」。
|
|
10
|
+
*
|
|
11
|
+
* 维护者 2026-09-12 原话:「该方法内需要提供过期时间,比如 1 天内直接返回一份,不会再创建新的。又或者 3 天
|
|
12
|
+
* 这种参数。」⇒ **缺省就是一天**;要别的窗口由调用方显式给(见 `reuseWindowOfHours`)。
|
|
13
|
+
* 与 `byAge` 同源,单位换算只有一处(本件)。 */
|
|
14
|
+
export declare const HELP_REUSE_DEFAULT_HOURS = 24;
|
|
15
|
+
/** 落点**意图**(给「按通式落一份」的调用者):`dir` 落哪个目录、`stem` 文件名**主体**(不含扩展名)。
|
|
16
|
+
*
|
|
17
|
+
* 扩展名、时间戳、同秒递补都由本件钉死——调用者只声明「落哪、叫什么」。要**确切文件名**的调用者
|
|
18
|
+
* 改走 `file`(见件头「两个正交的口子」)。 */
|
|
19
|
+
export interface HtmlLanding {
|
|
20
|
+
readonly dir: string;
|
|
21
|
+
readonly stem: string;
|
|
22
|
+
}
|
|
23
|
+
/** 落盘回执:`path` 恒绝对路径;`bytes` 恒**实际落盘**字节数(写后回读,不是期望值)。 */
|
|
24
|
+
export interface HtmlReceipt {
|
|
25
|
+
readonly mode: 'file';
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly bytes: number;
|
|
28
|
+
}
|
|
29
|
+
/** 复用窗口的**参数面**(`reuseHours`)→ 毫秒(`onExists:{reuse:{byAge}}`)。
|
|
30
|
+
*
|
|
31
|
+
* 唯一出口,五家技能命令行(卡路里/记账/备忘录/作息/大厨)都走这里——「几小时=多少毫秒」与
|
|
32
|
+
* 「坏参怎么判」不许各抄一份(铁律二)。
|
|
33
|
+
* 口径(#245):
|
|
34
|
+
* - `undefined`/空串 ⇒ **缺省**:有 `defaultHours` 时用它(各技能缺省= `HELP_REUSE_DEFAULT_HOURS`
|
|
35
|
+
* =一天),没有就返回 `0`(=「每次都落新的」);
|
|
36
|
+
* - `0` ⇒ 同上「每次都落新的」——**显式要一份最新的**;
|
|
37
|
+
* - 正的有限数 ⇒ 该窗口的毫秒数(3 天=`72`)/数字串按数算(JSON 与 argv 两路同口径);
|
|
38
|
+
* - 其余(负数/`NaN`/非数/非空的非数字串)⇒ 抛 `RangeError`「reuseHours 非法…」
|
|
39
|
+
* (**坏参阻断,不静默当 0**;各技能出口把它归到「参数错」那一档退出码)。
|
|
40
|
+
*/
|
|
41
|
+
export declare function reuseWindowOfHours(value: unknown, defaultHours?: number): number;
|
|
42
|
+
/** 造一个「按本家口径收拾坏参」的换算器:五家技能出口(卡路里/记账/备忘录/作息/大厨)都这么接——
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* const windowOf = helpReuseWindowOf((m) => fail(2, m)); // 出口的「参数错」那一档
|
|
46
|
+
* const windowMs = windowOf(params);
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* 为什么要这个工厂:五家原先的写法是「`try { reuseWindowOfHours(…) } catch { fail(2, …) }`」五份逐字
|
|
50
|
+
* 相同(只有「翻成哪家错误」不同)——那正是仓规铁律二禁止的「同一件事五份实现」。这里只做一件事:
|
|
51
|
+
* 把 `RangeError` 交给你给的处理器(各家自己决定翻成 `fail(2)` 还是别家错误类型),其余错误原样穿过
|
|
52
|
+
* (不吞非预期异常)。 */
|
|
53
|
+
export declare function helpReuseWindowOf(onBadParam: (message: string) => never): (params: Record<string, unknown>) => number;
|
|
54
|
+
/** 落一份 HTML 产物(**唯一落盘点**,对外唯一函数)。见件头:两个名字口子/四态/回执口径。
|
|
55
|
+
*
|
|
56
|
+
* `stem`(主体)与 `file`(确切文件名)**只给一个**;`onExists` 只表态撞名怎么办。 */
|
|
57
|
+
export declare function saveHtmlFile(input: {
|
|
58
|
+
dir: string;
|
|
59
|
+
stem?: string;
|
|
60
|
+
file?: string;
|
|
61
|
+
html: string;
|
|
62
|
+
onExists?: HtmlOnExists;
|
|
63
|
+
}): HtmlReceipt;
|
|
64
|
+
//# sourceMappingURL=saveHtml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saveHtml.d.ts","sourceRoot":"","sources":["../../src/output/saveHtml.ts"],"names":[],"mappings":"AAsDA;;8EAE8E;AAC9E,MAAM,MAAM,YAAY,GACpB,YAAY,GACZ,WAAW,GACX,MAAM,GACN;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE3E;;;;iCAIiC;AACjC,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;;+BAG+B;AAC/B,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAeD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAWhF;AAED;;;;;;;;;;iBAUiB;AACjB,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAS7C;AA6LD;;6DAE6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB,GAAG,WAAW,CAqCd"}
|