annotatable-html 0.0.0-stage → 1.38.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +289 -2
- package/agent/annot_answer.js +7 -0
- package/agent/annot_edit.js +6 -0
- package/agent/annot_poll.js +12 -0
- package/agent/annot_read.js +5 -0
- package/agent/annot_reply.js +50 -0
- package/bin/cli.js +239 -0
- package/lib/core.js +2198 -0
- package/lib/lang-en.js +462 -0
- package/lib/serve.js +293 -0
- package/lib/vendor/marked.cjs +2811 -0
- package/lib/vendor/temml.cjs +14710 -0
- package/package.json +48 -4
- package/templates/AGENTS.md +107 -0
- package/templates/SKILL.md +99 -0
- package/templates/annotatable-doc.html +5708 -0
- package/templates/annotatable.theme.css +28 -0
- package/templates/commands/html4agent.md +37 -0
- package/templates/theme-studio.html +330 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* annotatable.theme.css —— 主题自定义文件
|
|
2
|
+
* 「annotatable new」生成文档时会自动应用本文件:
|
|
3
|
+
* 1. 下面的 :root 变量块会替换文档的默认配色;
|
|
4
|
+
* 2. 其余 CSS 会追加到文档样式末尾(可覆盖任何默认样式)。
|
|
5
|
+
* 不需要主题时删除本文件即可恢复默认。 */
|
|
6
|
+
|
|
7
|
+
:root{
|
|
8
|
+
--paper:#f6f4ee; /* 页面背景 */
|
|
9
|
+
--card:#ffffff; /* 文档卡片背景 */
|
|
10
|
+
--ink:#2b2721; /* 正文颜色 */
|
|
11
|
+
--muted:#8a8375; /* 次要文字 */
|
|
12
|
+
--line:#e7e2d6; /* 边框线 */
|
|
13
|
+
--accent:#b45309; /* 主题色(按钮/标题/强调) */
|
|
14
|
+
--accent-soft:#fdf3e3; /* 主题色浅底 */
|
|
15
|
+
--hl:#fde047; /* 划线高亮 */
|
|
16
|
+
--hl2:#fb923c; /* 有批语的高亮 */
|
|
17
|
+
--danger:#dc2626; /* 危险操作 */
|
|
18
|
+
|
|
19
|
+
/* 版式挂钩(v1.17.0,可选):只写想改的,其余保持模板默认 */
|
|
20
|
+
/* --wrap-max:980px; */ /* 正文版心最大宽度 */
|
|
21
|
+
/* --root-fs:16px; */ /* 根字号(rem 基准,全局缩放) */
|
|
22
|
+
/* --lh:1.75; */ /* 正文行高 */
|
|
23
|
+
/* --font-body:"Segoe UI",system-ui,sans-serif; */ /* 正文字体栈 */
|
|
24
|
+
/* --font-head:var(--font-body); */ /* 标题字体栈 */
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ↓↓↓ 额外自定义样式写在下面(会覆盖默认样式)↓↓↓ */
|
|
28
|
+
/* 想可视化调参:运行 annotatable studio 打开主题工坊(实时预览+一键保存)。 */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: HTML4agent:生成可批注 HTML(划线批注+AI流式回答+版本回滚),值守监听批注
|
|
3
|
+
---
|
|
4
|
+
参数 $ARGUMENTS:首次使用 = 文档需求(主题与内容要求);之后 = 可省略(值守中自动接批注),或写批注反馈。
|
|
5
|
+
|
|
6
|
+
本命令是**值守循环工作流**,按对话阶段执行对应一节(只执行当前阶段,不要越步):
|
|
7
|
+
|
|
8
|
+
## 阶段一:生成并开始值守(用户给出文档需求,或本会话尚未生成文档时)
|
|
9
|
+
|
|
10
|
+
1. 目录无 AGENTS.md → 运行 `npx annotatable-html init`(已存在自动跳过);
|
|
11
|
+
2. 运行 `npx annotatable-html new <名称>.html`(名称从需求推断,中文即可);
|
|
12
|
+
3. 把内容写进生成的 html 的各 section;**不要动**批注层脚本与 annotations-store 区块;
|
|
13
|
+
4. 起伴随服务器(后台任务,会话期间保持):`npx annotatable-html serve <文档所在目录> --port 8980`;
|
|
14
|
+
5. 有浏览器操作能力 → 自己打开 `http://localhost:8980/<文档名>.html`;没有 → 把 URL 给用户;
|
|
15
|
+
6. 挂监听(后台任务):`npx annotatable-html poll <文档路径>`;
|
|
16
|
+
7. 告知用户:「打开这个地址直接划线批注,我在值守——批完不用喊我,回答会在页面上实时长出来。」
|
|
17
|
+
|
|
18
|
+
记住文档路径(后续阶段要用)。
|
|
19
|
+
|
|
20
|
+
## 阶段二:值守应答(POLL_HIT 唤醒,或用户直接给批注反馈时)
|
|
21
|
+
|
|
22
|
+
1. 运行 `npx annotatable-html read <文档路径>` 列出待回应批注(含对话线);
|
|
23
|
+
(无待回应批注:温和告知"还没看到新批注";继续值守。)
|
|
24
|
+
2. 逐条处理 status:"new" 的批注:通读 thread 全部轮次与原文上下文。批语是祈使句(「这里改成X」)→ 走 edit(见规则);疑问句 → 流式回答:
|
|
25
|
+
- `npx annotatable-html reply <文档路径> --stream '<第一段>' --id <批注id>`(开始,必带 --id);
|
|
26
|
+
- `npx annotatable-html reply <文档路径> --stream '<后续段>'`(续段);
|
|
27
|
+
- `npx annotatable-html reply <文档路径> --stream-end '<完整全文>'`(收尾);
|
|
28
|
+
- **退出码 4 = 用户按了⏹,5 = 批注已被删除:两种都立即停手,不要重试、不要续写**;
|
|
29
|
+
- 长回答(>5 行)若适合分阶教学 → 改用 `npx annotatable-html answer` 写教学卡,reply 只留摘要;
|
|
30
|
+
3. 一轮答完**重新挂 poll**(后台任务)。除非用户喊停,值守持续整个会话。
|
|
31
|
+
|
|
32
|
+
## 规则
|
|
33
|
+
|
|
34
|
+
- 批语含「暂存/留着」→ 划线内容进 ANSWER.md 暂存区;含「沉淀(到:文件名)」→ 整理进 docs/Learn/;
|
|
35
|
+
- 每次写回 rev+1 并维护快照(reply/edit 工具已封装;手工改务必遵守);
|
|
36
|
+
- 起不了 serve 时才让用户双击文件打开(file:// 兜底):告知用 Edge/Chrome 打开,同文件夹绑过一次后其余文档自动互联;其余流程不变;
|
|
37
|
+
- 目录存在 AGENTS.md 时以其细节为准;用户明确要"普通 html"时才输出不带批注层的文件。
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!-- annotatable 主题工坊(v1.17.0)
|
|
3
|
+
* 由 `annotatable studio` 经 GET /__annot/studio 内存直出(core.studioHtml 装配)。
|
|
4
|
+
* 装配点:下方 style 块内的模板 CSS 标记、JS 里的模板全文与版本号占位。
|
|
5
|
+
* 实时预览 = 模板副本 + 工坊主题烤入(浏览器端按 lib/core.js applyTheme 同规则),
|
|
6
|
+
* 所见即所得;保存走 POST /__annot/theme(未从 CLI 启动时降级为下载文件)。 -->
|
|
7
|
+
<html lang="zh-CN">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>主题工坊 · annotatable 可批注模板</title>
|
|
12
|
+
<style id="studio-tpl-css">
|
|
13
|
+
/*__TPL_CSS__*/
|
|
14
|
+
</style>
|
|
15
|
+
<style>
|
|
16
|
+
/* 工坊自身骨架(st- 前缀,避免与模板类名相撞) */
|
|
17
|
+
html,body{height:100%}
|
|
18
|
+
body.st-body{margin:0;display:flex;flex-direction:column;background:#22242a;color:#e8e5df;
|
|
19
|
+
font:14px/1.6 "Segoe UI",system-ui,"PingFang SC","Microsoft YaHei",sans-serif;overflow:hidden}
|
|
20
|
+
.st-head{display:flex;align-items:baseline;gap:12px;padding:10px 16px;background:#1a1c21;border-bottom:1px solid #33363e}
|
|
21
|
+
.st-head h1{font-size:15px;margin:0;font-weight:700;color:#f0ede7}
|
|
22
|
+
.st-head .st-sub{font-size:12px;color:#9a968e}
|
|
23
|
+
.st-main{flex:1;display:flex;min-height:0}
|
|
24
|
+
.st-rail{width:320px;min-width:320px;overflow-y:auto;padding:14px 16px 24px;background:#26282f;border-right:1px solid #33363e}
|
|
25
|
+
.st-group{margin-bottom:18px}
|
|
26
|
+
.st-group>h2{font-size:12.5px;margin:0 0 8px;color:#c9a86b;letter-spacing:.08em;font-weight:700}
|
|
27
|
+
.st-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
28
|
+
.st-chip{cursor:pointer;border:1px solid #4a4d56;border-radius:8px;padding:4px 10px;font-size:12px;
|
|
29
|
+
color:#d8d5cf;background:#2e3138}
|
|
30
|
+
.st-chip:hover{border-color:#c9a86b}
|
|
31
|
+
.st-chip.on{border-color:#c9a86b;color:#f0e7d3;background:#3a3327}
|
|
32
|
+
.st-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 12px}
|
|
33
|
+
.st-cv{display:flex;align-items:center;gap:8px;font-size:12.5px;color:#c9c6bf}
|
|
34
|
+
.st-cv input[type=color]{width:34px;height:24px;border:1px solid #4a4d56;border-radius:5px;padding:0;background:none;cursor:pointer}
|
|
35
|
+
.st-row{display:flex;align-items:center;gap:8px;margin:7px 0;font-size:12.5px;color:#c9c6bf}
|
|
36
|
+
.st-row label{min-width:60px}
|
|
37
|
+
.st-row input[type=range]{flex:1;accent-color:#c9a86b}
|
|
38
|
+
.st-row .st-val{min-width:52px;text-align:right;color:#9a968e;font-variant-numeric:tabular-nums}
|
|
39
|
+
.st-row select,.st-row input[type=text]{flex:1;background:#2e3138;color:#e2dfd9;border:1px solid #4a4d56;
|
|
40
|
+
border-radius:6px;padding:4px 6px;font-size:12.5px}
|
|
41
|
+
.st-btns{display:flex;flex-direction:column;gap:8px}
|
|
42
|
+
.st-btn{cursor:pointer;border:0;border-radius:8px;padding:9px 12px;font-size:13.5px;font-weight:700;text-align:center}
|
|
43
|
+
.st-btn.primary{background:#c9a86b;color:#241d10}
|
|
44
|
+
.st-btn.primary:hover{background:#d8b87a}
|
|
45
|
+
.st-btn.ghost{background:#2e3138;color:#d8d5cf;border:1px solid #4a4d56}
|
|
46
|
+
.st-btn.ghost:hover{border-color:#c9a86b}
|
|
47
|
+
.st-note{font-size:12px;color:#8e8a82;line-height:1.7;margin-top:10px}
|
|
48
|
+
.st-css{width:100%;height:110px;background:#1d1f24;color:#a8c4a0;border:1px solid #33363e;border-radius:6px;
|
|
49
|
+
font:11.5px/1.5 ui-monospace,Consolas,monospace;padding:8px;resize:vertical;white-space:pre}
|
|
50
|
+
.st-prev{flex:1;display:flex;min-width:0;background:#fff}
|
|
51
|
+
.st-prev iframe{border:0;width:100%;height:100%;background:#fff}
|
|
52
|
+
.st-toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);background:#2e3138;color:#f0ede7;
|
|
53
|
+
border:1px solid #c9a86b;border-radius:10px;padding:10px 18px;font-size:13.5px;display:none;z-index:99;
|
|
54
|
+
box-shadow:0 6px 24px rgba(0,0,0,.4)}
|
|
55
|
+
@media print{ body.st-body .st-rail,body.st-body .st-head{display:none} }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body class="st-body">
|
|
59
|
+
<div class="st-head">
|
|
60
|
+
<h1>🎨 主题工坊</h1>
|
|
61
|
+
<span class="st-sub">可批注模板可视化定制 · 左边调、右边看 · 保存后 new / md / inject 自动应用(<span id="st-ver"></span>)</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="st-main">
|
|
64
|
+
<aside class="st-rail">
|
|
65
|
+
<div class="st-group">
|
|
66
|
+
<h2>配色预设</h2>
|
|
67
|
+
<div class="st-chips" id="st-presets"></div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="st-group">
|
|
70
|
+
<h2>颜色</h2>
|
|
71
|
+
<div class="st-grid" id="st-colors"></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="st-group">
|
|
74
|
+
<h2>字体</h2>
|
|
75
|
+
<div class="st-row"><label>正文字体</label><select id="st-fbody"></select></div>
|
|
76
|
+
<div class="st-row" id="st-fbody-custom" style="display:none"><label>自定义</label><input type="text" id="st-fbody-x" placeholder='"Georgia", serif'></div>
|
|
77
|
+
<div class="st-row"><label>标题字体</label><select id="st-fhead"></select></div>
|
|
78
|
+
<div class="st-row" id="st-fhead-custom" style="display:none"><label>自定义</label><input type="text" id="st-fhead-x" placeholder='"Georgia", serif'></div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="st-group">
|
|
81
|
+
<h2>版式</h2>
|
|
82
|
+
<div class="st-row"><label>根字号</label><input type="range" id="st-fs" min="13" max="20" step="0.5"><span class="st-val" id="st-fs-v"></span></div>
|
|
83
|
+
<div class="st-row"><label>行高</label><input type="range" id="st-lh" min="1.4" max="2.4" step="0.05"><span class="st-val" id="st-lh-v"></span></div>
|
|
84
|
+
<div class="st-row"><label>页面宽度</label><input type="range" id="st-w" min="640" max="1400" step="20"><span class="st-val" id="st-w-v"></span></div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="st-group st-btns">
|
|
87
|
+
<button class="st-btn primary" id="st-save">💾 保存到当前目录</button>
|
|
88
|
+
<button class="st-btn primary" id="st-save-g">🌍 设为全局默认</button>
|
|
89
|
+
<button class="st-btn ghost" id="st-dl">⬇ 下载 annotatable.theme.css</button>
|
|
90
|
+
<button class="st-btn ghost" id="st-reset">↺ 恢复默认</button>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="st-group">
|
|
93
|
+
<h2>theme.css 预览</h2>
|
|
94
|
+
<textarea class="st-css" id="st-cssout" readonly></textarea>
|
|
95
|
+
</div>
|
|
96
|
+
<p class="st-note">
|
|
97
|
+
· 右侧即真实模板渲染:批注、划线、🧭 导航窗格都能直接试;窗格默认泊位,拖出/缩放可验证页面宽度与导航让位互不干扰。<br>
|
|
98
|
+
· 「保存到当前目录」写入 annotatable.theme.css;「设为全局默认」写入 ~/.annotatable/theme.css(目录级优先于全局)。
|
|
99
|
+
</p>
|
|
100
|
+
</aside>
|
|
101
|
+
<div class="st-prev"><iframe id="st-frame" title="模板实时预览"></iframe></div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="st-toast" id="st-toast"></div>
|
|
104
|
+
<script>
|
|
105
|
+
'use strict';
|
|
106
|
+
/* 当前模板全文(由 core.studioHtml 装配;脚本闭串已在源码层转义,防本块提前闭合) */
|
|
107
|
+
var TPL_SRC = "__TPL_SRC__";
|
|
108
|
+
|
|
109
|
+
/* 预览示例正文(标题供 🧭 导航窗格抓取;mark.annot 观察划线配色) */
|
|
110
|
+
var SAMPLE_HTML = [
|
|
111
|
+
'<p class="kicker">示例预览</p>',
|
|
112
|
+
'<h1>示例文档标题</h1>',
|
|
113
|
+
'<p class="meta">主题工坊示例 · 用于观察配色、字体与版式效果</p>',
|
|
114
|
+
'<p>这是一段用于观察<mark class="annot">正文颜色与字体</mark>的示例文字。段落间距、行高在这里最直观,改动滑杆后稍候片刻即可看到效果。</p>',
|
|
115
|
+
'<h2>一、排版元素</h2>',
|
|
116
|
+
'<p>正文直铺在页面底色上(无白卡片),留白交给版心。<strong>强调文字</strong>与<code>行内代码</code>随主题色联动。</p>',
|
|
117
|
+
'<blockquote>引用块:观察左边线与底色。</blockquote>',
|
|
118
|
+
'<h3>表格与代码</h3>',
|
|
119
|
+
'<div class="table-wrap"><table><thead><tr><th>项目</th><th>观察点</th></tr></thead><tbody>',
|
|
120
|
+
'<tr><td>表格</td><td>边线颜色、表头底色</td></tr>',
|
|
121
|
+
'<tr><td>代码块</td><td>底色与等宽字体</td></tr>',
|
|
122
|
+
'</tbody></table></div>',
|
|
123
|
+
'<pre><code>const theme = "studio"; // 观察代码块底色</code></pre>',
|
|
124
|
+
'<h2>二、批注层外观</h2>',
|
|
125
|
+
'<p>划线高亮:<mark class="annot">黄色为普通划线</mark>;<mark class="annot">橙色代表已有批语</mark>。试着在预览里选中一段文字划线,看看高亮是否顺眼。</p>',
|
|
126
|
+
'<ul><li>列表项观察行距与标记颜色</li><li>第二项</li></ul>',
|
|
127
|
+
'<p>页面宽度滑杆与 🧭 导航窗格互不干扰:窗格泊位时正文自动让位左移,拖窄、拖宽、拖出窗格都可以试。</p>'
|
|
128
|
+
].join('\n');
|
|
129
|
+
|
|
130
|
+
/* ---------- 状态 ---------- */
|
|
131
|
+
function parseRoot(src) {
|
|
132
|
+
var m = src.match(/:root\s*\{([^}]*)\}/);
|
|
133
|
+
var vars = {};
|
|
134
|
+
if (m) m[1].replace(/\/\*[\s\S]*?\*\//g, '').split(';').forEach(function (kv) {
|
|
135
|
+
var i = kv.indexOf(':');
|
|
136
|
+
if (i > 0) vars[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
|
|
137
|
+
});
|
|
138
|
+
return vars;
|
|
139
|
+
}
|
|
140
|
+
var DEFAULTS = parseRoot(TPL_SRC);
|
|
141
|
+
var state = {};
|
|
142
|
+
Object.keys(DEFAULTS).forEach(function (k) { state[k] = DEFAULTS[k]; });
|
|
143
|
+
|
|
144
|
+
var COLOR_VARS = [
|
|
145
|
+
['--bg', '页面底色'], ['--bg2', '次表面'], ['--card', '卡片'], ['--accent', '强调色'],
|
|
146
|
+
['--text', '正文'], ['--border', '边线'], ['--hl', '划线高亮'], ['--hl2', '批语高亮'], ['--danger', '危险']
|
|
147
|
+
];
|
|
148
|
+
var FONT_OPTS = [
|
|
149
|
+
['', '默认(跟随模板)', ''],
|
|
150
|
+
['hei', '黑体(系统无衬线)', '"Microsoft YaHei","PingFang SC","HarmonyOS Sans SC",sans-serif'],
|
|
151
|
+
['song', '宋体(衬线)', '"Noto Serif SC","Source Han Serif SC","SimSun",serif'],
|
|
152
|
+
['kai', '楷体', '"Kaiti SC","STKaiti","KaiTi",serif'],
|
|
153
|
+
['mono', '等宽', 'ui-monospace,Consolas,"Courier New",monospace'],
|
|
154
|
+
['x', '自定义…', null]
|
|
155
|
+
];
|
|
156
|
+
var PRESETS = [
|
|
157
|
+
['暖白棕(默认)', {}],
|
|
158
|
+
['冷灰蓝', { '--bg': '#f3f5f8', '--bg2': '#e7ebf1', '--card': '#ffffff', '--accent': '#4a6fa5', '--text': '#2a3440', '--border': '#d3dae2', '--hl': '#ffd54f', '--hl2': '#ff9e40', '--danger': '#d32f2f' }],
|
|
159
|
+
['护眼米绿', { '--bg': '#e9efe2', '--bg2': '#dce5d1', '--card': '#f7faf3', '--accent': '#4a7c59', '--text': '#2f3b2f', '--border': '#c8d6c2', '--hl': '#ffe082', '--hl2': '#ffb74d', '--danger': '#c62828' }],
|
|
160
|
+
['暗色', { '--bg': '#1e2025', '--bg2': '#26292f', '--card': '#2b2e35', '--accent': '#d8a657', '--text': '#e6e2d8', '--border': '#3a3e47', '--hl': '#a98f2f', '--hl2': '#b05f34', '--danger': '#e05252' }]
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/* ---------- 控件 ---------- */
|
|
164
|
+
function el(id) { return document.getElementById(id); }
|
|
165
|
+
function buildColors() {
|
|
166
|
+
var g = el('st-colors');
|
|
167
|
+
COLOR_VARS.forEach(function (cv) {
|
|
168
|
+
var d = document.createElement('label');
|
|
169
|
+
d.className = 'st-cv';
|
|
170
|
+
var inp = document.createElement('input');
|
|
171
|
+
inp.type = 'color'; inp.id = 'c' + cv[0].slice(1);
|
|
172
|
+
var cap = document.createElement('span'); cap.textContent = cv[1];
|
|
173
|
+
d.appendChild(inp); d.appendChild(cap); g.appendChild(d);
|
|
174
|
+
inp.addEventListener('input', function () { state[cv[0]] = inp.value; markPreset(null); onChange(); });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function buildFonts(selId, customRowId, customId, varName) {
|
|
178
|
+
var sel = el(selId), row = el(customRowId), txt = el(customId);
|
|
179
|
+
FONT_OPTS.forEach(function (fo) {
|
|
180
|
+
var o = document.createElement('option');
|
|
181
|
+
o.value = fo[0]; o.textContent = fo[1];
|
|
182
|
+
sel.appendChild(o);
|
|
183
|
+
});
|
|
184
|
+
function syncSel() {
|
|
185
|
+
var cur = state[varName] || '';
|
|
186
|
+
if (cur === DEFAULTS[varName]) { sel.value = ''; row.style.display = 'none'; return; }
|
|
187
|
+
var hit = FONT_OPTS.filter(function (fo) { return fo[2] === cur; })[0];
|
|
188
|
+
if (hit) { sel.value = hit[0]; row.style.display = 'none'; }
|
|
189
|
+
else { sel.value = 'x'; row.style.display = 'flex'; txt.value = cur; }
|
|
190
|
+
}
|
|
191
|
+
sel.addEventListener('change', function () {
|
|
192
|
+
var fo = FONT_OPTS.filter(function (f) { return f[0] === sel.value; })[0];
|
|
193
|
+
if (fo && fo[2]) { state[varName] = fo[2]; row.style.display = 'none'; }
|
|
194
|
+
else if (fo) { state[varName] = ''; row.style.display = 'none'; }
|
|
195
|
+
else { row.style.display = 'flex'; txt.focus(); }
|
|
196
|
+
onChange();
|
|
197
|
+
});
|
|
198
|
+
txt.addEventListener('input', function () { state[varName] = txt.value.trim(); onChange(); });
|
|
199
|
+
syncSel();
|
|
200
|
+
return syncSel;
|
|
201
|
+
}
|
|
202
|
+
function buildSlider(id, vid, varName, unit) {
|
|
203
|
+
var s = el(id), v = el(vid);
|
|
204
|
+
var def = parseFloat(DEFAULTS[varName]) || 0;
|
|
205
|
+
if (def < parseFloat(s.min) || def > parseFloat(s.max)) { s.min = Math.floor(def); s.max = Math.ceil(def) + 2; }
|
|
206
|
+
s.value = def; v.textContent = def + unit;
|
|
207
|
+
s.addEventListener('input', function () {
|
|
208
|
+
state[varName] = s.value + unit;
|
|
209
|
+
v.textContent = s.value + unit;
|
|
210
|
+
onChange();
|
|
211
|
+
});
|
|
212
|
+
return function sync() { def = parseFloat(DEFAULTS[varName]) || 0; s.value = def; v.textContent = def + unit; };
|
|
213
|
+
}
|
|
214
|
+
var syncFonts = [], syncSliders = [];
|
|
215
|
+
function buildPresets() {
|
|
216
|
+
var box = el('st-presets');
|
|
217
|
+
PRESETS.forEach(function (pr, ix) {
|
|
218
|
+
var b = document.createElement('button');
|
|
219
|
+
b.type = 'button'; b.className = 'st-chip'; b.textContent = pr[0];
|
|
220
|
+
b.addEventListener('click', function () {
|
|
221
|
+
COLOR_VARS.forEach(function (cv) { state[cv[0]] = pr[1][cv[0]] || DEFAULTS[cv[0]]; });
|
|
222
|
+
markPreset(ix); syncColorInputs(); onChange();
|
|
223
|
+
});
|
|
224
|
+
box.appendChild(b);
|
|
225
|
+
});
|
|
226
|
+
markPreset(0);
|
|
227
|
+
}
|
|
228
|
+
function markPreset(ix) {
|
|
229
|
+
var chips = el('st-presets').children;
|
|
230
|
+
for (var i = 0; i < chips.length; i++) chips[i].classList.toggle('on', i === ix);
|
|
231
|
+
}
|
|
232
|
+
function syncColorInputs() {
|
|
233
|
+
COLOR_VARS.forEach(function (cv) {
|
|
234
|
+
var inp = el('c' + cv[0].slice(1));
|
|
235
|
+
var v = state[cv[0]] || '';
|
|
236
|
+
if (/^#[0-9a-fA-F]{6}$/.test(v)) inp.value = v;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------- 主题 CSS 与预览 ---------- */
|
|
241
|
+
function themeCss() {
|
|
242
|
+
var diffs = [];
|
|
243
|
+
Object.keys(DEFAULTS).forEach(function (k) {
|
|
244
|
+
if (state[k] !== DEFAULTS[k] && state[k] !== '') diffs.push(' ' + k + ':' + state[k] + ';');
|
|
245
|
+
});
|
|
246
|
+
if (!diffs.length) return '';
|
|
247
|
+
return ':root{\n' + diffs.join('\n') + '\n}\n';
|
|
248
|
+
}
|
|
249
|
+
function buildPreviewDoc() {
|
|
250
|
+
var doc = TPL_SRC;
|
|
251
|
+
var css = themeCss();
|
|
252
|
+
if (css) {
|
|
253
|
+
var rm = css.match(/:root\s*\{[^}]*\}/);
|
|
254
|
+
if (rm) doc = doc.replace(/:root\s*\{[^}]*\}/, function () { return rm[0]; });
|
|
255
|
+
var rest = rm ? css.replace(rm[0], '') : css;
|
|
256
|
+
if (rest.trim()) doc = doc.replace('</style>', function () { return rest + '\n</style>'; });
|
|
257
|
+
}
|
|
258
|
+
doc = doc.replace(/<article>[\s\S]*?<\/article>/, function () { return '<article>\n' + SAMPLE_HTML + '\n </article>'; });
|
|
259
|
+
doc = doc.replace(/<title>[\s\S]*?<\/title>/, '<title>主题工坊 · 预览</title>');
|
|
260
|
+
var iS = doc.lastIndexOf('<script>');
|
|
261
|
+
doc = doc.slice(0, iS + 8) + '\nwindow.__ANNOT_NO_BINDNUDGE__=1;' + doc.slice(iS + 8);
|
|
262
|
+
return doc;
|
|
263
|
+
}
|
|
264
|
+
var rebuildTimer = null;
|
|
265
|
+
function onChange() {
|
|
266
|
+
el('st-cssout').value = themeCss() || '(当前与默认一致,保存空主题即可恢复默认)';
|
|
267
|
+
clearTimeout(rebuildTimer);
|
|
268
|
+
rebuildTimer = setTimeout(function () { el('st-frame').srcdoc = buildPreviewDoc(); }, 400);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ---------- 保存 ---------- */
|
|
272
|
+
function toast(msg) {
|
|
273
|
+
var t = el('st-toast');
|
|
274
|
+
t.textContent = msg; t.style.display = 'block';
|
|
275
|
+
clearTimeout(t._h);
|
|
276
|
+
t._h = setTimeout(function () { t.style.display = 'none'; }, 2600);
|
|
277
|
+
}
|
|
278
|
+
function saveTheme(scope) {
|
|
279
|
+
var css = themeCss();
|
|
280
|
+
fetch('/__annot/theme', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ scope: scope, css: css })
|
|
284
|
+
}).then(function (r) {
|
|
285
|
+
if (!r.ok) return r.text().then(function (tx) { throw new Error(tx); });
|
|
286
|
+
return r.json();
|
|
287
|
+
}).then(function (o) {
|
|
288
|
+
toast(scope === 'global' ? '✅ 已设为全局默认(' + o.path + ')' : '✅ 已保存到当前目录(' + o.path + ')');
|
|
289
|
+
}).catch(function () {
|
|
290
|
+
if (!css) { toast('当前与默认一致,无需保存'); return; }
|
|
291
|
+
download(css); // 未从 CLI 启动(或服务未开)→ 下载兜底
|
|
292
|
+
toast('服务不可达,已转为下载 annotatable.theme.css,请放进文档目录');
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function download(css) {
|
|
296
|
+
var a = document.createElement('a');
|
|
297
|
+
a.href = URL.createObjectURL(new Blob([css], { type: 'text/css' }));
|
|
298
|
+
a.download = 'annotatable.theme.css';
|
|
299
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
300
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); }, 3000);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ---------- 启动 ---------- */
|
|
304
|
+
buildPresets();
|
|
305
|
+
buildColors();
|
|
306
|
+
syncColorInputs();
|
|
307
|
+
syncFonts.push(buildFonts('st-fbody', 'st-fbody-custom', 'st-fbody-x', '--font-body'));
|
|
308
|
+
syncFonts.push(buildFonts('st-fhead', 'st-fhead-custom', 'st-fhead-x', '--font-head'));
|
|
309
|
+
syncSliders.push(buildSlider('st-fs', 'st-fs-v', '--root-fs', 'px'));
|
|
310
|
+
syncSliders.push(buildSlider('st-lh', 'st-lh-v', '--lh', ''));
|
|
311
|
+
syncSliders.push(buildSlider('st-w', 'st-w-v', '--wrap-max', 'px'));
|
|
312
|
+
el('st-save').addEventListener('click', function () { saveTheme('cwd'); });
|
|
313
|
+
el('st-save-g').addEventListener('click', function () { saveTheme('global'); });
|
|
314
|
+
el('st-dl').addEventListener('click', function () {
|
|
315
|
+
var css = themeCss();
|
|
316
|
+
if (!css) { toast('当前与默认一致,无需下载'); return; }
|
|
317
|
+
download(css);
|
|
318
|
+
});
|
|
319
|
+
el('st-reset').addEventListener('click', function () {
|
|
320
|
+
Object.keys(DEFAULTS).forEach(function (k) { state[k] = DEFAULTS[k]; });
|
|
321
|
+
syncColorInputs(); syncFonts.forEach(function (f) { f(); }); syncSliders.forEach(function (f) { f(); });
|
|
322
|
+
markPreset(0); onChange();
|
|
323
|
+
toast('已恢复模板默认外观');
|
|
324
|
+
});
|
|
325
|
+
el('st-ver').textContent = '当前模板内置版 __PKG_VER__';
|
|
326
|
+
el('st-frame').srcdoc = buildPreviewDoc();
|
|
327
|
+
el('st-cssout').value = '(当前与默认一致,保存空主题即可恢复默认)';
|
|
328
|
+
</script>
|
|
329
|
+
</body>
|
|
330
|
+
</html>
|