agentcss 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/01-tokens/README.md +1 -1
- package/01-tokens/tokens.css +67 -1
- package/01-tokens/typography.css +1 -1
- package/02-primitives/button/demo.html +8 -0
- package/02-primitives/card/README.md +15 -0
- package/02-primitives/choice/README.md +30 -0
- package/02-primitives/choice/choice.css +1 -1
- package/02-primitives/input/README.md +1 -0
- package/02-primitives/select/README.md +42 -2
- package/02-primitives/select/demo.html +11 -6
- package/02-primitives/select/select.js +184 -94
- package/02-primitives/timeline/demo.html +2 -2
- package/02-primitives/timeline/timeline.css +1 -1
- package/03-patterns/accordion/README.md +15 -0
- package/03-patterns/content/README.md +11 -0
- package/03-patterns/drawer/README.md +1 -1
- package/03-patterns/dropdown/demo.html +1 -1
- package/03-patterns/form-validation/README.md +18 -0
- package/03-patterns/form-validation/form-validation.css +12 -1
- package/03-patterns/list/README.md +11 -0
- package/03-patterns/overlay/README.md +26 -6
- package/03-patterns/overlay/overlay.css +22 -3
- package/03-patterns/overlay/overlay.js +63 -17
- package/03-patterns/states/README.md +15 -0
- package/03-patterns/states/states.css +18 -1
- package/03-patterns/tooltip/README.md +12 -0
- package/04-recipes/longform/longform.html +3 -3
- package/04-recipes/report/README.md +9 -0
- package/04-recipes/report/report.css +5 -0
- package/04-recipes/table/README.md +16 -0
- package/04-recipes/tierA-tokens.css +3 -3
- package/09-assets/README.md +18 -0
- package/09-assets/bar/bar.css +25 -10
- package/09-assets/echarts-adapter/demo.html +3 -1
- package/09-assets/echarts-adapter/echarts-adapter.js +107 -34
- package/09-assets/scientific-plot/README.md +2 -1
- package/09-assets/scientific-plot/scientific-plot.js +0 -7
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +370 -18
- package/CONTRIBUTING.md +6 -5
- package/README.md +9 -2
- package/README.zh-CN.md +25 -10
- package/SECURITY.md +5 -2
- package/START-HERE.md +7 -7
- package/SUPPORT.md +86 -24
- package/ai/START-HERE.md +3 -2
- package/ai/cli.js +126 -6
- package/ai/components.json +258 -2
- package/ai/components.meta.json +257 -40
- package/ai/components.schema.json +96 -0
- package/ai/contract.schema.json +11 -0
- package/ai/pdf-pages.js +79 -0
- package/ai/pdf.js +354 -0
- package/ai/tokens.json +2 -2
- package/ai/validate.js +48 -1
- package/dist/01-tokens/tokens.css +1 -1
- package/dist/02-primitives/select/select.js +158 -71
- package/dist/03-patterns/form-validation/form-validation.css +1 -1
- package/dist/03-patterns/overlay/overlay.css +1 -1
- package/dist/03-patterns/overlay/overlay.js +40 -11
- package/dist/03-patterns/states/states.css +1 -1
- package/dist/04-recipes/report/report.css +1 -1
- package/dist/04-recipes/tierA-tokens.css +1 -1
- package/dist/09-assets/bar/bar.css +1 -1
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +75 -33
- package/dist/09-assets/scientific-plot/scientific-plot.js +0 -7
- package/dist/COSTS.md +37 -37
- package/docs/ACCESSIBILITY.md +3 -3
- package/docs/FACTS.md +144 -0
- package/docs/LAYER-BASELINE.md +28 -3
- package/docs/PRINT.md +450 -0
- package/docs/RELEASE-PROCESS.md +286 -0
- package/docs/VERIFY-RELEASE.md +167 -0
- package/index.html +1 -1
- package/llms-full.txt +8 -8
- package/llms.txt +5 -0
- package/package.json +6 -1
package/ai/pdf.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* pdf.js — HTML → PDF 导出。零运行时依赖(不需要 puppeteer,也不需要联网)。
|
|
3
|
+
*
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 🔴 为什么导出器在**这里**,而不是在组件库里
|
|
6
|
+
* ============================================================================
|
|
7
|
+
* `@page` 是**文档级决定**:纸多大、边距多少,取决于"这份东西要交给谁",
|
|
8
|
+
* 不取决于某个按钮长什么样。库一旦在 `tokens.css` 里声明 `@page`,
|
|
9
|
+
* 使用者的边距控制会**静默失效**(实测同一份页面:943 runs vs 978 runs,
|
|
10
|
+
* 只是多了一条 `@page` 规则)。所以:
|
|
11
|
+
*
|
|
12
|
+
* 组件库 → `@media print` 里的**行为**(隐藏什么、展开什么、不跨页切断)
|
|
13
|
+
* 导出器 → `@page` 里的**版式**(纸幅、边距)
|
|
14
|
+
*
|
|
15
|
+
* 这条分工不是偏好:谁声明 `@page`,谁就接管了使用者的边距控制。
|
|
16
|
+
* 所以它只能由"产出这份文件的人"给 —— 这正是本文件存在的理由。
|
|
17
|
+
*
|
|
18
|
+
* ============================================================================
|
|
19
|
+
* 🔴 为什么用 Chrome 的命令行,而不是 puppeteer
|
|
20
|
+
* ============================================================================
|
|
21
|
+
* 本库的承诺是**零运行时依赖**(package.json 里 dependencies 是空的)。
|
|
22
|
+
* 为了一个"5% 的人才会用"的导出功能塞一个 ~300 MB 的浏览器包,
|
|
23
|
+
* 等于让 95% 的人为 5% 的人付安装代价 —— 那是这条承诺唯一的实质内容。
|
|
24
|
+
*
|
|
25
|
+
* 所以:`--print-to-pdf` 走使用者**已经装好的** Chrome/Chromium/Edge。
|
|
26
|
+
* 找不到就**明确报错**(`no-chrome`),绝不静默降级成一个空文件。
|
|
27
|
+
*
|
|
28
|
+
* ============================================================================
|
|
29
|
+
* ⚠️ 一条必须写下来的观测差异
|
|
30
|
+
* ============================================================================
|
|
31
|
+
* 本工具产出的 PDF **包含底色**(headless 打印的默认行为,实测:
|
|
32
|
+
* `.bg{background:#1b4d8f}` 在内容流里是 `.1059 .302 .5608 rg … re f`),
|
|
33
|
+
* 而浏览器打印对话框默认**不勾**"背景图形"。
|
|
34
|
+
*
|
|
35
|
+
* ⇒ 同一份页面,「导出」与「Ctrl+P」在**彩色块**上观感不同:
|
|
36
|
+
* 导出里白字压深蓝可读;对话框里底色被丢弃、白字会被压成 #ABABAB。
|
|
37
|
+
* 两者不是谁错了,是两个产品。`docs/PRINT.md` §八 写了这件事。
|
|
38
|
+
*
|
|
39
|
+
* 用法:
|
|
40
|
+
* const { exportPdf } = require('./pdf');
|
|
41
|
+
* const r = exportPdf({ src: 'page.html', out: 'page.pdf', paper: 'a4',
|
|
42
|
+
* margin: '18mm 15mm', budget: 8 });
|
|
43
|
+
* if (!r.ok) … // r.code ∈ usage|no-chrome|render|timeout|over-budget|unreadable
|
|
44
|
+
*/
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const os = require('os');
|
|
49
|
+
const path = require('path');
|
|
50
|
+
const { spawnSync } = require('child_process');
|
|
51
|
+
const { pathToFileURL } = require('url');
|
|
52
|
+
const pages = require('./pdf-pages');
|
|
53
|
+
|
|
54
|
+
/* 纸幅。CSS 的 `size` 关键字就够,不需要自己换算 pt ——
|
|
55
|
+
换算过的地方迟早和引擎的取整方式差 0.28pt,然后有人拿它当"回归"。 */
|
|
56
|
+
const PAPERS = {
|
|
57
|
+
a3: { css: 'A3', w: 841.89, h: 1190.55 },
|
|
58
|
+
a4: { css: 'A4', w: 595.28, h: 841.89 },
|
|
59
|
+
a5: { css: 'A5', w: 419.53, h: 595.28 },
|
|
60
|
+
letter: { css: 'Letter', w: 612, h: 792 },
|
|
61
|
+
legal: { css: 'Legal', w: 612, h: 1008 },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DEFAULT_PAPER = 'a4';
|
|
65
|
+
/* 18mm/15mm 是"文档"而不是"网页"的常见留白;12mm/10mm 是浏览器默认量级。
|
|
66
|
+
两者都不神圣 —— 关键是**由导出方显式给出**,不是由库偷偷定。 */
|
|
67
|
+
const DEFAULT_MARGIN = '18mm 15mm';
|
|
68
|
+
|
|
69
|
+
/* ============================================================================
|
|
70
|
+
* Chrome 定位
|
|
71
|
+
* ============================================================================
|
|
72
|
+
* ⚠️ 这张表与 `05-audit/browser.js` 的候选表**有意重复**:那一份要给
|
|
73
|
+
* puppeteer 用,还要认 Playwright 的缓存目录与引擎参数;这一份要能
|
|
74
|
+
* 跟着包发到使用者机器上、不许 require 任何东西。两边的共同判据只有一条:
|
|
75
|
+
* **找不到就明确报错,不许静默降级**。
|
|
76
|
+
* 漂移由 `05-audit/pdf-export-gate.js` 的 E8 交叉核对(两边都找到时必须是同一个文件)。
|
|
77
|
+
*/
|
|
78
|
+
function chromeCandidates() {
|
|
79
|
+
const out = [];
|
|
80
|
+
if (process.env.AGENTCSS_CHROME) out.push(process.env.AGENTCSS_CHROME);
|
|
81
|
+
if (process.env.CHROME_PATH) out.push(process.env.CHROME_PATH);
|
|
82
|
+
if (process.env.CHROME_BIN) out.push(process.env.CHROME_BIN);
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
const local = process.env.LOCALAPPDATA;
|
|
85
|
+
out.push(
|
|
86
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
87
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
88
|
+
local && path.join(local, 'Google/Chrome/Application/chrome.exe'),
|
|
89
|
+
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
|
90
|
+
'C:/Program Files/Microsoft/Edge/Application/msedge.exe');
|
|
91
|
+
} else if (process.platform === 'darwin') {
|
|
92
|
+
out.push(
|
|
93
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
94
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
95
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
|
|
96
|
+
} else {
|
|
97
|
+
out.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
|
|
98
|
+
'/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
|
|
99
|
+
}
|
|
100
|
+
/* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"只在开发机上成立 */
|
|
101
|
+
for (const root of [path.join(os.homedir(), '.cache/ms-playwright'),
|
|
102
|
+
path.join(os.homedir(), 'Library/Caches/ms-playwright')]) {
|
|
103
|
+
let names;
|
|
104
|
+
try { names = fs.readdirSync(root); } catch (e) { continue; }
|
|
105
|
+
for (const n of names) {
|
|
106
|
+
if (!/^chromium/.test(n)) continue;
|
|
107
|
+
for (const rel of ['chrome-linux/chrome',
|
|
108
|
+
'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
|
109
|
+
'chrome-win/chrome.exe', 'chrome-linux/headless_shell']) {
|
|
110
|
+
out.push(path.join(root, n, ...rel.split('/')));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} [explicit] 显式路径(`--chrome=`)
|
|
119
|
+
* @param {object} [opts] `{candidates}` —— **只为可测性**留的口子:
|
|
120
|
+
* 没有它,"一个浏览器都没有"这条报错就只能靠"这台机器恰好没装 Chrome"才验证得了,
|
|
121
|
+
* 而有 Chrome 的开发机上它**永远测不到**(与 validate.js 的 `opt.components` 同一个理由)。
|
|
122
|
+
* @returns {{path:string, source:string}|{error:string}}
|
|
123
|
+
*/
|
|
124
|
+
function resolveChrome(explicit, opts) {
|
|
125
|
+
if (explicit) {
|
|
126
|
+
/* 显式给了却不存在 ⇒ 立刻报错。悄悄回落到自动探测会让"我明明指定了它"
|
|
127
|
+
变成一句谎话,而且失败现场看起来像是"这个 Chrome 不能打印"。 */
|
|
128
|
+
if (!fs.existsSync(explicit)) {
|
|
129
|
+
return { error: '--chrome 指定的文件不存在:' + explicit
|
|
130
|
+
+ '\n 这里要的是 Chrome / Chromium / Edge 的**可执行文件**路径。' };
|
|
131
|
+
}
|
|
132
|
+
return { path: explicit, source: 'explicit' };
|
|
133
|
+
}
|
|
134
|
+
const list = (opts && opts.candidates) || chromeCandidates();
|
|
135
|
+
for (const p of list) {
|
|
136
|
+
try { if (fs.existsSync(p)) return { path: p, source: 'auto' }; } catch (e) { /* 下一个 */ }
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
error: '找不到 Chrome / Chromium / Edge。\n'
|
|
140
|
+
+ ' 用法:--chrome="C:/Program Files/Google/Chrome/Application/chrome.exe"\n'
|
|
141
|
+
+ ' 或设环境变量 AGENTCSS_CHROME / CHROME_PATH。\n'
|
|
142
|
+
+ ' 本工具不打包浏览器:为了 5% 的导出需求让 95% 的人多装 300 MB,不值。',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ============================================================================
|
|
147
|
+
* 版式
|
|
148
|
+
* ============================================================================ */
|
|
149
|
+
|
|
150
|
+
/** `18mm` / `18mm,15mm` / `18mm 15mm` / 四段 → CSS margin 简写。非法则返回 null。 */
|
|
151
|
+
function normalizeMargin(v) {
|
|
152
|
+
if (v === undefined || v === null || v === '') return DEFAULT_MARGIN;
|
|
153
|
+
const parts = String(v).trim().split(/[\s,]+/).filter(Boolean);
|
|
154
|
+
if (parts.length < 1 || parts.length > 4) return null;
|
|
155
|
+
for (const p of parts) {
|
|
156
|
+
if (!/^\d+(\.\d+)?(mm|cm|in|pt|px)$/.test(p)) return null;
|
|
157
|
+
}
|
|
158
|
+
return parts.join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function paperSpec(name) {
|
|
162
|
+
const k = String(name || DEFAULT_PAPER).toLowerCase();
|
|
163
|
+
return PAPERS[k] ? { key: k, ...PAPERS[k] } : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 注入到 `<head>` 末尾(**必须在使用者样式之后**,否则同特异性被覆盖 —— E254)。 */
|
|
167
|
+
function injectSheet(html, css) {
|
|
168
|
+
const style = '<style id="agentcss-paper" data-agentcss="paper">' + css + '</style>';
|
|
169
|
+
if (/<\/head\s*>/i.test(html)) return html.replace(/<\/head\s*>/i, style + '</head>');
|
|
170
|
+
if (/<html[^>]*>/i.test(html)) return html.replace(/<html[^>]*>/i, '$&' + style);
|
|
171
|
+
return style + html;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cssFor(spec, margin) {
|
|
175
|
+
return '@page{size:' + spec.css + ';margin:' + margin + '}';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ============================================================================
|
|
179
|
+
* 导出
|
|
180
|
+
* ============================================================================ */
|
|
181
|
+
|
|
182
|
+
function isRemote(src) {
|
|
183
|
+
return /^https?:\/\//i.test(src);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {object} o
|
|
188
|
+
* @param {string} o.src 本地 HTML 路径,或 http(s) URL
|
|
189
|
+
* @param {string} o.out 输出 PDF 路径(父目录不存在会创建)
|
|
190
|
+
* @param {string} [o.paper] a3|a4|a5|letter|legal(默认 a4)
|
|
191
|
+
* @param {string} [o.margin] CSS margin 简写(默认 18mm 15mm)
|
|
192
|
+
* @param {number} [o.budget] 页数上限;超了返回 over-budget
|
|
193
|
+
* @param {string} [o.chrome] Chrome 可执行文件
|
|
194
|
+
* @param {number} [o.timeoutMs]
|
|
195
|
+
* @returns {object} `{ok:true, ...}` 或 `{ok:false, code, message, ...}`
|
|
196
|
+
*/
|
|
197
|
+
function exportPdf(o) {
|
|
198
|
+
o = o || {};
|
|
199
|
+
const t0 = Date.now();
|
|
200
|
+
if (!o.src) return { ok: false, code: 'usage', message: '缺少 src' };
|
|
201
|
+
if (!o.out) return { ok: false, code: 'usage', message: '缺少 out' };
|
|
202
|
+
|
|
203
|
+
const spec = paperSpec(o.paper);
|
|
204
|
+
if (!spec) {
|
|
205
|
+
return { ok: false, code: 'usage',
|
|
206
|
+
message: '未知纸幅 ' + JSON.stringify(o.paper) + '(可用:'
|
|
207
|
+
+ Object.keys(PAPERS).join(' / ') + ')' };
|
|
208
|
+
}
|
|
209
|
+
const margin = normalizeMargin(o.margin);
|
|
210
|
+
if (!margin) {
|
|
211
|
+
return { ok: false, code: 'usage',
|
|
212
|
+
message: '边距写法不认识:' + JSON.stringify(o.margin)
|
|
213
|
+
+ '(要 CSS 简写,例:18mm 或 "18mm 15mm")' };
|
|
214
|
+
}
|
|
215
|
+
if (o.budget !== undefined && o.budget !== null) {
|
|
216
|
+
const b = Number(o.budget);
|
|
217
|
+
if (!Number.isInteger(b) || b < 1) {
|
|
218
|
+
/* ⚠️ 用 String() 而不是 JSON.stringify:`JSON.stringify(NaN)` 是 `"null"`,
|
|
219
|
+
于是 `--max-pages=abc` 会报「收到 null」—— 使用者据这句话会去查一个
|
|
220
|
+
根本不存在的 null(实测踩过,见 E264)。 */
|
|
221
|
+
return { ok: false, code: 'usage',
|
|
222
|
+
message: '--max-pages 必须是 ≥1 的整数,收到 ' + String(o.budget) };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const chrome = resolveChrome(o.chrome);
|
|
227
|
+
if (chrome.error) return { ok: false, code: 'no-chrome', message: chrome.error };
|
|
228
|
+
|
|
229
|
+
const outAbs = path.resolve(o.out);
|
|
230
|
+
fs.mkdirSync(path.dirname(outAbs), { recursive: true });
|
|
231
|
+
|
|
232
|
+
/* 本地文件才注入 @page:远程页面我们拿不到它的字节,硬注入等于假装改了它。 */
|
|
233
|
+
let target = o.src;
|
|
234
|
+
let tmpHtml = null;
|
|
235
|
+
if (!isRemote(o.src)) {
|
|
236
|
+
const srcAbs = path.resolve(o.src);
|
|
237
|
+
if (!fs.existsSync(srcAbs)) {
|
|
238
|
+
return { ok: false, code: 'usage', message: '找不到输入文件:' + srcAbs };
|
|
239
|
+
}
|
|
240
|
+
const html = fs.readFileSync(srcAbs, 'utf8');
|
|
241
|
+
/* ⚠️ 临时文件必须落在**同一个目录**:换成系统临时目录,页面里的
|
|
242
|
+
`./tokens.css` / `/02-primitives/...` 会全部 404,而产物看起来"只是没样式"。 */
|
|
243
|
+
tmpHtml = path.join(path.dirname(srcAbs),
|
|
244
|
+
'.agentcss-print-' + process.pid + '-' + Date.now().toString(36) + '.html');
|
|
245
|
+
fs.writeFileSync(tmpHtml, injectSheet(html, cssFor(spec, margin)), 'utf8');
|
|
246
|
+
target = pathToFileURL(tmpHtml).href;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'agentcss-chrome-'));
|
|
250
|
+
const args = [
|
|
251
|
+
'--headless=new',
|
|
252
|
+
'--disable-gpu',
|
|
253
|
+
'--disable-extensions',
|
|
254
|
+
'--no-first-run',
|
|
255
|
+
'--no-default-browser-check',
|
|
256
|
+
/* 每次一个新 profile:使用者可能正开着 Chrome,共用 profile 会抢锁 */
|
|
257
|
+
'--user-data-dir=' + profile,
|
|
258
|
+
/* 页眉页脚(日期 / 标题 / 1/2)默认是开的,必须显式关掉 */
|
|
259
|
+
'--no-pdf-header-footer',
|
|
260
|
+
'--print-to-pdf-no-header',
|
|
261
|
+
'--print-to-pdf=' + outAbs,
|
|
262
|
+
target,
|
|
263
|
+
];
|
|
264
|
+
/* 容器里以 root 跑才需要关沙箱 —— 默认不开,也不替使用者决定 */
|
|
265
|
+
if (process.env.AGENTCSS_NO_SANDBOX === '1') args.unshift('--no-sandbox');
|
|
266
|
+
|
|
267
|
+
let res;
|
|
268
|
+
try {
|
|
269
|
+
res = spawnSync(chrome.path, args, {
|
|
270
|
+
timeout: o.timeoutMs || 60000,
|
|
271
|
+
encoding: 'utf8',
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
274
|
+
});
|
|
275
|
+
} finally {
|
|
276
|
+
/* 清理必须**同步**做完:CLI 随后就 process.exit,挂一个 setTimeout
|
|
277
|
+
等于每次导出都在系统临时目录里留一个 Chrome profile(实测漏了 6 个才发现)。
|
|
278
|
+
删不掉不算错(Windows 上偶发 EBUSY),但不能因为删不掉就不删。 */
|
|
279
|
+
if (tmpHtml) { try { fs.unlinkSync(tmpHtml); } catch (e) { /* 已删 */ } }
|
|
280
|
+
try { fs.rmSync(profile, { recursive: true, force: true, maxRetries: 3 }); }
|
|
281
|
+
catch (e) { /* 交给系统清理 */ }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const ms = Date.now() - t0;
|
|
285
|
+
const stderr = String((res && res.stderr) || '').slice(-800);
|
|
286
|
+
if (res && res.error && res.error.code === 'ETIMEDOUT') {
|
|
287
|
+
return { ok: false, code: 'timeout', message: 'Chrome 在 '
|
|
288
|
+
+ Math.round((o.timeoutMs || 60000) / 1000) + 's 内没有结束(页面可能有死循环或等待网络)', ms };
|
|
289
|
+
}
|
|
290
|
+
if (!fs.existsSync(outAbs)) {
|
|
291
|
+
return { ok: false, code: 'render', ms,
|
|
292
|
+
message: 'Chrome 没有产出文件(退出码 ' + (res ? res.status : '?') + ')'
|
|
293
|
+
+ (stderr ? '\n ' + stderr.trim().split('\n').slice(-4).join('\n ') : '') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const buf = fs.readFileSync(outAbs);
|
|
297
|
+
if (buf.length < 5 || buf.slice(0, 5).toString('latin1') !== '%PDF-') {
|
|
298
|
+
return { ok: false, code: 'render',
|
|
299
|
+
message: '产出的文件不是 PDF(' + buf.length + ' 字节)—— 通常是页面加载失败', ms };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const info = pages.inspect(buf);
|
|
303
|
+
const injected = !isRemote(o.src);
|
|
304
|
+
const base = {
|
|
305
|
+
out: outAbs, bytes: buf.length, pdf: buf, ms,
|
|
306
|
+
/* 远程页面没注入 @page ⇒ `paper` 只能是 null:报 "a4" 是在陈述一个
|
|
307
|
+
没有发生的事实(实测远程导出是 612×792)。真实纸幅看 `mediaBox`。 */
|
|
308
|
+
paper: injected ? spec.key : null,
|
|
309
|
+
margin: injected ? margin : null, chrome: chrome.path,
|
|
310
|
+
pages: info.pages, mediaBox: info.mediaBox,
|
|
311
|
+
injected: injected,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/* 页数两套说法不一致 ⇒ 不判预算,也不假装通过 */
|
|
315
|
+
if (!info.agree && o.budget !== undefined && o.budget !== null) {
|
|
316
|
+
return { ...base, ok: false, code: 'unreadable',
|
|
317
|
+
message: 'PDF 里 `/Type /Page` 数出 ' + info.pages + ' 页、页树 `/Count` 说 '
|
|
318
|
+
+ info.countFromTree + ' 页 —— 两套说法不一致,无法判定预算' };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (o.budget !== undefined && o.budget !== null && info.pages > o.budget) {
|
|
322
|
+
return { ...base, ok: false, code: 'over-budget', budget: Number(o.budget),
|
|
323
|
+
message: overBudgetMessage(info.pages, Number(o.budget), spec, margin, outAbs,
|
|
324
|
+
info.mediaBox, !isRemote(o.src)) };
|
|
325
|
+
}
|
|
326
|
+
return { ...base, ok: true,
|
|
327
|
+
budget: (o.budget === undefined || o.budget === null) ? null : Number(o.budget) };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* 报错要**可执行**:三条出路 + 一条"我不代做"的说明。
|
|
331
|
+
为什么不实现 truncate:截断 = 静默丢内容,正是这套门禁在防的事。
|
|
332
|
+
一个会替你悄悄删掉后半份报告的工具,比一个报错停下的工具危险得多。 */
|
|
333
|
+
function overBudgetMessage(got, budget, spec, margin, out, box, injected) {
|
|
334
|
+
const size = box ? (Math.round(box.width) + '×' + Math.round(box.height) + 'pt') : '?';
|
|
335
|
+
/* 远程页面没注入 @page ⇒ 纸幅与边距都不是我们给的。
|
|
336
|
+
这时报"A4 / 18mm 15mm"是在描述一个不存在的事实(实测:远程导出是 612×792,
|
|
337
|
+
而参数写着 a4)—— 报错里写错事实,比不写更误导。 */
|
|
338
|
+
const layout = injected
|
|
339
|
+
? '(' + spec.css + ',' + size + ',边距 ' + margin + ')'
|
|
340
|
+
: '(页面自定义纸幅,实测 ' + size + ';未注入 @page,--paper/--margin 不生效)';
|
|
341
|
+
return '超出页数预算:' + got + ' 页 > ' + budget + ' 页' + layout + '\n'
|
|
342
|
+
+ ' 产出文件保留在:' + out + '\n'
|
|
343
|
+
+ ' 三条出路(按代价从低到高):\n'
|
|
344
|
+
+ ' ① 拆文档 把一章拆成单独文件分别导出:agentcss pdf chapter-2.html --out ch2.pdf\n'
|
|
345
|
+
+ ' ② 进附录 明细移到附录,正文只留汇总 —— 需要作者改内容,不是换个参数\n'
|
|
346
|
+
+ ' ③ 截断 **本工具不代做**。静默丢内容正是这套判据在防的事;\n'
|
|
347
|
+
+ ' 真要截断,请删掉要丢的段落再导出(这样它会出现在 diff 里)\n'
|
|
348
|
+
+ ' 确认预算定低了,就调高它:--max-pages=' + Math.max(got, budget);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
exportPdf, resolveChrome, normalizeMargin, paperSpec, PAPERS,
|
|
353
|
+
DEFAULT_PAPER, DEFAULT_MARGIN,
|
|
354
|
+
};
|
package/ai/tokens.json
CHANGED
|
@@ -593,11 +593,11 @@
|
|
|
593
593
|
"$description": "实算 原来的 #959ba3 对 paper 只有 4.14:1 不达 AA,提到这个值"
|
|
594
594
|
},
|
|
595
595
|
"--text-on-accent": {
|
|
596
|
-
"$value": "#
|
|
596
|
+
"$value": "#15181c",
|
|
597
597
|
"$type": "color"
|
|
598
598
|
},
|
|
599
599
|
"--text-on-solid": {
|
|
600
|
-
"$value": "#
|
|
600
|
+
"$value": "#15181c",
|
|
601
601
|
"$type": "color"
|
|
602
602
|
},
|
|
603
603
|
"--skeleton-bg": {
|
package/ai/validate.js
CHANGED
|
@@ -368,8 +368,55 @@ function validateDoc(doc, opt) {
|
|
|
368
368
|
')不允许携带扩展字段' });
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// ---- 介质:paper 才追加的打印义务
|
|
372
|
+
//
|
|
373
|
+
// 🔴 为什么这一节不能省("打印不是已经在 CSS 里做了吗")
|
|
374
|
+
// CSS 里做的是**行为**(隐藏什么、展开什么),它回答不了三个问题:
|
|
375
|
+
// ① 这份文档准备印几页?没有 pageBudget,"几百张纸"是惊喜而不是错误;
|
|
376
|
+
// ② 文档里 `pagination:"server"` 的表格,纸上会不会只剩一页?
|
|
377
|
+
// ③ 用了 `printPolicy:"omit"` 的组件,作者知道它不会印出来吗?
|
|
378
|
+
// 前两个是错,第三个是提示。三条都只在 paper 介质下成立 ——
|
|
379
|
+
// 所以默认 screen 时一条都不跑(95% 的人不该为打印付任何注意力)。
|
|
380
|
+
var medium = opt.medium || 'screen';
|
|
381
|
+
if (['screen', 'paper'].indexOf(medium) < 0) {
|
|
382
|
+
errors.push({ path: '/medium',
|
|
383
|
+
msg: '未知介质 ' + medium + '(可用:screen / paper)' });
|
|
384
|
+
medium = 'screen';
|
|
385
|
+
}
|
|
386
|
+
if (medium === 'paper') {
|
|
387
|
+
if (doc.pageBudget === undefined || doc.pageBudget === null) {
|
|
388
|
+
errors.push({ path: '/pageBudget',
|
|
389
|
+
msg: 'paper 介质下必须声明 pageBudget(页数上限,正整数)。'
|
|
390
|
+
+ '页数取决于内容,库不给默认值 —— 猜一个数字比不给更坏' });
|
|
391
|
+
}
|
|
392
|
+
var omitSeen = {};
|
|
393
|
+
ids.forEach(function (id) {
|
|
394
|
+
var n = nodes[id];
|
|
395
|
+
if (n.kind === 'text' || n.kind === 'slot') return;
|
|
396
|
+
var c = comps[n.kind];
|
|
397
|
+
if (!c) return; // 未知组件上面已经报过,不重复计错
|
|
398
|
+
if (!Array.isArray(c.printPolicy)) {
|
|
399
|
+
errors.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind
|
|
400
|
+
+ ' 在契约里没有 printPolicy 字段 —— paper 介质下无法判断它在纸上会发生什么' });
|
|
401
|
+
}
|
|
402
|
+
if (c.pagination === 'server') {
|
|
403
|
+
errors.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind
|
|
404
|
+
+ ' 是服务端分页(pagination:"server"):纸上只有当前一页,'
|
|
405
|
+
+ '而导出的文件看起来是完整的 —— 要全量数据必须先换 client 分页' });
|
|
406
|
+
}
|
|
407
|
+
if (omitSeen[n.kind]) return; // 同一个组件只提示一次
|
|
408
|
+
(Array.isArray(c.printPolicy) ? c.printPolicy : []).forEach(function (r) {
|
|
409
|
+
if (r.policy !== 'omit' || omitSeen[n.kind]) return;
|
|
410
|
+
omitSeen[n.kind] = true;
|
|
411
|
+
warnings.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind + ' 的 '
|
|
412
|
+
+ r.selector + ' 声明 omit:这部分**不会出现在纸上**'
|
|
413
|
+
+ (r.why ? '(' + r.why + ')' : '') });
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
371
418
|
return { ok: errors.length === 0, errors: errors, warnings: warnings,
|
|
372
|
-
profile: profileName };
|
|
419
|
+
profile: profileName, medium: medium };
|
|
373
420
|
}
|
|
374
421
|
|
|
375
422
|
module.exports = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root {--raw-paper:#f6f7f8;--raw-surface:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-surface-raised:#ffffff;--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-hover:#8f1e17;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-success:#2f6b3a;--raw-warning:#8a5a00;--raw-info:#1d5f8a;--raw-danger-bg:#fdeeec;--raw-success-bg:#eaf2e6;--raw-warning-bg:#fbf1dd;--raw-info-bg:#e8f1f8;--raw-dir-up:#c0392b;--raw-dir-down:#2f6b3a;--raw-accent:#1b4d8f;--raw-switch-on:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-tooltip-bg:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-tooltip-border:#15181c;--paper:var(--raw-paper);--surface:var(--raw-surface);--surface-sunken:var(--raw-surface-sunken);--surface-raised:var(--raw-surface-raised);--scrollbar-size:10px;--scrollbar-size-thin:6px;--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--scrollbar-radius:5px;--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--skeleton-h:1em;--skeleton-size:40px;--skeleton-duration:1.4s;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--progress-h:6px;--progress-dur:240ms;--r-full:999px;--ring-size:64px;--ring-thickness:8px;--progress:0%;--progress-num:0;--z-popover:60;--popover-min-w:200px;--popover-max-w:320px;--popover-p:var(--sp-3);--popover-gap:8px;--popover-dur:140ms;--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-hover:var(--raw-danger-hover);--scrim:var(--raw-scrim);--success:var(--raw-success);--warning:var(--raw-warning);--info:var(--raw-info);--danger-bg:var(--raw-danger-bg);--success-bg:var(--raw-success-bg);--warning-bg:var(--raw-warning-bg);--info-bg:var(--raw-info-bg);--dir-up:var(--raw-dir-up);--dir-down:var(--raw-dir-down);--accent:var(--raw-accent);--switch-on:var(--raw-switch-on);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-fg:var(--raw-tooltip-fg);--tooltip-border:var(--raw-tooltip-border);--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:clamp(1rem, 0.95rem + 0.3vw, 1.25rem);--sp-5:clamp(1.25rem, 1.15rem + 0.7vw, 1.75rem);--sp-6:clamp(1.5rem, 1.3rem + 1.2vw, 2.25rem);--sp-7:clamp(2rem, 1.7rem + 1.8vw, 3.5rem);--sp-8:clamp(2.5rem, 2.1rem + 2.4vw, 4.5rem);--bp-sm:30rem;--bp-md:48rem;--bp-lg:64rem;--bp-xl:80rem;--fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:clamp(1rem, 0.95rem + 0.3vw, 1.125rem);--fs-md:clamp(1.125rem, 1.05rem + 0.5vw, 1.3125rem);--fs-lg:clamp(1.3125rem, 1.2rem + 0.7vw, 1.5rem);--fs-xl:clamp(1.5rem, 1.3rem + 1.2vw, 1.875rem);--fs-2xl:clamp(1.75rem, 1.45rem + 1.8vw, 2.5rem);--lh-tight:1.25;--lh-base:1.7;--lh-loose:1.85;--measure-page:52rem;--tracking-title:-0.02em;--r-sm:6px;--r-xs:3px;--r-md:10px;--r-lg:14px;--r-pill:999px;--shadow-1:0 1px 2px rgba(31, 29, 25, 0.05), 0 1px 3px rgba(31, 29, 25, 0.06);--shadow-2:0 2px 4px rgba(31, 29, 25, 0.05), 0 6px 16px rgba(31, 29, 25, 0.08);--dur-fast:120ms;--dur-base:180ms;--dur-slow:280ms;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out:cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer:cubic-bezier(0.32, 0.72, 0, 1);--lift-sm:1px;--lift-md:2px;--lift-lg:8px;--z-base:0;--z-sticky:10;--z-header:20;--z-raised:5;--z-overlay:30;--z-modal:40;--z-toast:50;--sz-touch:44px;--sz-touch-sm:32px;}[data-cq] {container-type:inline-size;}@media (prefers-reduced-motion:reduce) {*, *::before, *::after {animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}:focus-visible {outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm);}:root {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}:root:lang(en), :root:lang(en-US), :root:lang(en-GB), :root:lang(fr), :root:lang(de), :root:lang(es), :root:lang(pt), :root:lang(ru), :root:lang(el), :root:lang(tr), :root:lang(vi) {--measure-prose:68ch;--measure-title:34ch;--measure-lede:62ch;--measure-pull:58ch;--measure-aside:56ch;}:root:lang(ar) {--measure-prose:60ch;--measure-title:30ch;--measure-lede:56ch;--measure-pull:52ch;--measure-aside:50ch;}:root:lang(hi), :root:lang(th), :root:lang(km), :root:lang(my) {--measure-prose:56ch;--measure-title:28ch;--measure-lede:52ch;--measure-pull:48ch;--measure-aside:46ch;}:root:lang(zh), :root:lang(zh-CN), :root:lang(zh-TW), :root:lang(ja), :root:lang(ko) {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}@media (prefers-color-scheme:dark) {:root:not([data-theme="light"]) {--raw-paper:#14171A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-border-control:#8A9099;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-danger:#E07A6E;--raw-danger-hover:#F09A90;--raw-danger-bg:#341D1A;--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-dir-up:#E07A6E;--raw-dir-down:#6FAA7A;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-accent:#7AA9DE;--raw-switch-on:#3E6EA8;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-tooltip-bg:#232830;--raw-tooltip-fg:#E4E7EA;--raw-tooltip-border:#2A3038;--raw-scrim:rgba(0, 0, 0, 0.62);}}[data-theme="light"] {--raw-accent:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-bg:#fdeeec;--raw-danger-hover:#8f1e17;--raw-dir-down:#2f6b3a;--raw-dir-up:#c0392b;--raw-info:#1d5f8a;--raw-info-bg:#e8f1f8;--raw-paper:#f6f7f8;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-success:#2f6b3a;--raw-success-bg:#eaf2e6;--raw-surface:#ffffff;--raw-surface-raised:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-switch-on:#1b4d8f;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-tooltip-bg:#15181c;--raw-tooltip-border:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-warning:#8a5a00;--raw-warning-bg:#fbf1dd;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}:root[data-theme="dark"], [data-theme="dark"] {--raw-accent:#7AA9DE;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-border-control:#8A9099;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-danger:#E07A6E;--raw-danger-bg:#341D1A;--raw-danger-hover:#F09A90;--raw-dir-down:#6FAA7A;--raw-dir-up:#E07A6E;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-paper:#14171A;--raw-scrim:rgba(0, 0, 0, 0.62);--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-switch-on:#3E6EA8;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-tooltip-bg:#232830;--raw-tooltip-border:#2A3038;--raw-tooltip-fg:#E4E7EA;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}:root {--sans:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Geeza Pro', 'Segoe UI Historic', sans-serif;--mono:ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Consolas, 'Sarasa Mono SC', 'Microsoft YaHei Mono', monospace;--serif:'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Songti SC', 'SimSun', 'Noto Serif CJK SC', 'Source Han Serif SC', serif;--tracking-normal:0;}:root:lang(ar), :root:lang(fa), :root:lang(he), :root:lang(ur) {--tracking-normal:0.01em;}a, button, label, summary, input[type="checkbox"], input[type="radio"], input[type="button"], input[type="submit"], [role="button"], [role="switch"], [role="tab"], [role="option"], .choice, .switch, .btn, .accordion__trigger, .tabs__tab, .card, .list__row {-webkit-tap-highlight-color:transparent;}.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:0.82;}@media (hover:hover) and (pointer:fine) {.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:1;}}
|
|
1
|
+
:root {--raw-paper:#f6f7f8;--raw-surface:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-surface-raised:#ffffff;--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-hover:#8f1e17;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-success:#2f6b3a;--raw-warning:#8a5a00;--raw-info:#1d5f8a;--raw-danger-bg:#fdeeec;--raw-success-bg:#eaf2e6;--raw-warning-bg:#fbf1dd;--raw-info-bg:#e8f1f8;--raw-dir-up:#c0392b;--raw-dir-down:#2f6b3a;--raw-accent:#1b4d8f;--raw-switch-on:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-tooltip-bg:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-tooltip-border:#15181c;--paper:var(--raw-paper);--surface:var(--raw-surface);--surface-sunken:var(--raw-surface-sunken);--surface-raised:var(--raw-surface-raised);--scrollbar-size:10px;--scrollbar-size-thin:6px;--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--scrollbar-radius:5px;--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--skeleton-h:1em;--skeleton-size:40px;--skeleton-duration:1.4s;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--progress-h:6px;--progress-dur:240ms;--r-full:999px;--ring-size:64px;--ring-thickness:8px;--progress:0%;--progress-num:0;--z-popover:60;--popover-min-w:200px;--popover-max-w:320px;--popover-p:var(--sp-3);--popover-gap:8px;--popover-dur:140ms;--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-hover:var(--raw-danger-hover);--scrim:var(--raw-scrim);--success:var(--raw-success);--warning:var(--raw-warning);--info:var(--raw-info);--danger-bg:var(--raw-danger-bg);--success-bg:var(--raw-success-bg);--warning-bg:var(--raw-warning-bg);--info-bg:var(--raw-info-bg);--dir-up:var(--raw-dir-up);--dir-down:var(--raw-dir-down);--accent:var(--raw-accent);--switch-on:var(--raw-switch-on);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-fg:var(--raw-tooltip-fg);--tooltip-border:var(--raw-tooltip-border);--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:clamp(1rem, 0.95rem + 0.3vw, 1.25rem);--sp-5:clamp(1.25rem, 1.15rem + 0.7vw, 1.75rem);--sp-6:clamp(1.5rem, 1.3rem + 1.2vw, 2.25rem);--sp-7:clamp(2rem, 1.7rem + 1.8vw, 3.5rem);--sp-8:clamp(2.5rem, 2.1rem + 2.4vw, 4.5rem);--bp-sm:30rem;--bp-md:48rem;--bp-lg:64rem;--bp-xl:80rem;--fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:clamp(1rem, 0.95rem + 0.3vw, 1.125rem);--fs-md:clamp(1.125rem, 1.05rem + 0.5vw, 1.3125rem);--fs-lg:clamp(1.3125rem, 1.2rem + 0.7vw, 1.5rem);--fs-xl:clamp(1.5rem, 1.3rem + 1.2vw, 1.875rem);--fs-2xl:clamp(1.75rem, 1.45rem + 1.8vw, 2.5rem);--lh-tight:1.25;--lh-base:1.7;--lh-loose:1.85;--measure-page:52rem;--tracking-title:-0.02em;--r-sm:6px;--r-xs:3px;--r-md:10px;--r-lg:14px;--r-pill:999px;--shadow-1:0 1px 2px rgba(31, 29, 25, 0.05), 0 1px 3px rgba(31, 29, 25, 0.06);--shadow-2:0 2px 4px rgba(31, 29, 25, 0.05), 0 6px 16px rgba(31, 29, 25, 0.08);--dur-fast:120ms;--dur-base:180ms;--dur-slow:280ms;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out:cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer:cubic-bezier(0.32, 0.72, 0, 1);--lift-sm:1px;--lift-md:2px;--lift-lg:8px;--z-base:0;--z-sticky:10;--z-header:20;--z-raised:5;--z-overlay:30;--z-modal:40;--z-toast:50;--sz-touch:44px;--sz-touch-sm:32px;}[data-cq] {container-type:inline-size;}@media (prefers-reduced-motion:reduce) {*, *::before, *::after {animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}:focus-visible {outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm);}:root {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}:root:lang(en), :root:lang(en-US), :root:lang(en-GB), :root:lang(fr), :root:lang(de), :root:lang(es), :root:lang(pt), :root:lang(ru), :root:lang(el), :root:lang(tr), :root:lang(vi) {--measure-prose:68ch;--measure-title:34ch;--measure-lede:62ch;--measure-pull:58ch;--measure-aside:56ch;}:root:lang(ar) {--measure-prose:60ch;--measure-title:30ch;--measure-lede:56ch;--measure-pull:52ch;--measure-aside:50ch;}:root:lang(hi), :root:lang(th), :root:lang(km), :root:lang(my) {--measure-prose:56ch;--measure-title:28ch;--measure-lede:52ch;--measure-pull:48ch;--measure-aside:46ch;}:root:lang(zh), :root:lang(zh-CN), :root:lang(zh-TW), :root:lang(ja), :root:lang(ko) {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}@media screen and (prefers-color-scheme:dark) {:root:not([data-theme="light"]) {--raw-paper:#14171A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-border-control:#8A9099;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-danger:#E07A6E;--raw-danger-hover:#F09A90;--raw-danger-bg:#341D1A;--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-dir-up:#E07A6E;--raw-dir-down:#6FAA7A;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-accent:#7AA9DE;--raw-switch-on:#3E6EA8;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-tooltip-bg:#232830;--raw-tooltip-fg:#E4E7EA;--raw-tooltip-border:#2A3038;--raw-scrim:rgba(0, 0, 0, 0.62);}}[data-theme="light"] {--raw-accent:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-bg:#fdeeec;--raw-danger-hover:#8f1e17;--raw-dir-down:#2f6b3a;--raw-dir-up:#c0392b;--raw-info:#1d5f8a;--raw-info-bg:#e8f1f8;--raw-paper:#f6f7f8;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-success:#2f6b3a;--raw-success-bg:#eaf2e6;--raw-surface:#ffffff;--raw-surface-raised:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-switch-on:#1b4d8f;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-tooltip-bg:#15181c;--raw-tooltip-border:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-warning:#8a5a00;--raw-warning-bg:#fbf1dd;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}@media screen {:root[data-theme="dark"], [data-theme="dark"] {--raw-accent:#7AA9DE;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-border-control:#8A9099;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-danger:#E07A6E;--raw-danger-bg:#341D1A;--raw-danger-hover:#F09A90;--raw-dir-down:#6FAA7A;--raw-dir-up:#E07A6E;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-paper:#14171A;--raw-scrim:rgba(0, 0, 0, 0.62);--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-switch-on:#3E6EA8;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-tooltip-bg:#232830;--raw-tooltip-border:#2A3038;--raw-tooltip-fg:#E4E7EA;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}}:root {--sans:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Geeza Pro', 'Segoe UI Historic', sans-serif;--mono:ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Consolas, 'Sarasa Mono SC', 'Microsoft YaHei Mono', monospace;--serif:'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Songti SC', 'SimSun', 'Noto Serif CJK SC', 'Source Han Serif SC', serif;--tracking-normal:0;}:root:lang(ar), :root:lang(fa), :root:lang(he), :root:lang(ur) {--tracking-normal:0.01em;}a, button, label, summary, input[type="checkbox"], input[type="radio"], input[type="button"], input[type="submit"], [role="button"], [role="switch"], [role="tab"], [role="option"], .choice, .switch, .btn, .accordion__trigger, .tabs__tab, .card, .list__row {-webkit-tap-highlight-color:transparent;}.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:0.82;}@media (hover:hover) and (pointer:fine) {.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:1;}}.print-only {display:none;}@media print {:root {color-scheme:light;--text-on-accent:#15181c;--text-on-solid:#15181c;}.no-print {display:none !important;}.print-only {display:block;}.tabs .tabs__panel[hidden] {display:block;}.accordion .accordion__panel[hidden] {display:grid !important;grid-template-rows:1fr;visibility:visible;}.toast, .tooltip, .popover, .skeleton, .tabs__list, .nav__list, .nav__toggle, .pagination, .accordion__marker {display:none !important;}tr, img, figure, svg, .card, .metric, .sparkline, .splot {break-inside:avoid;}.print-allow-break {break-inside:auto;}}
|