@zzclub/pipeline 0.12.0 → 0.13.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/README.md +3 -1
- package/package.json +2 -2
- package/src/adapter-types.ts +3 -0
- package/src/adapters/builtin-image-renderer.test.ts +32 -0
- package/src/adapters/builtin-image-renderer.ts +41 -73
- package/src/commands/config.ts +14 -1
- package/src/commands/imgx.ts +3 -3
- package/src/commands/ingest-handoff.ts +15 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/render-theme.test.ts +115 -0
- package/src/commands/render.ts +27 -9
- package/src/config.ts +5 -0
- package/src/cover-theme-presets.json +59 -0
- package/src/imgx/assets/browser/cover.css +25 -0
- package/src/imgx/assets/browser/cover.js +40 -0
- package/src/imgx/assets/templates/longform-3-4.html +14 -36
- package/src/imgx/assets/templates/poster-3-4.html +15 -394
- package/src/imgx/assets/templates/wechat-cover-split.html +15 -334
- package/src/imgx/chrome-render.test.ts +38 -0
- package/src/imgx/chrome-render.ts +110 -0
- package/src/imgx/cover.test.ts +80 -0
- package/src/imgx/cover.ts +130 -0
- package/src/imgx/inline-text.ts +56 -0
- package/src/imgx/longform-theme.ts +74 -114
- package/src/imgx/obstacle-flow.test.ts +52 -0
- package/src/imgx/obstacle-flow.ts +177 -0
- package/src/imgx/pretext-adapter.ts +3 -0
- package/src/imgx/pretext-runtime.ts +0 -1
- package/src/imgx/render-article.test.ts +26 -1
- package/src/imgx/render-article.ts +165 -382
- package/src/imgx/render-card.ts +64 -204
- package/src/imgx/runtime.ts +0 -90
- package/src/render-branding.test.ts +26 -0
- package/src/render-branding.ts +24 -0
- package/src/runtime-paths.ts +12 -51
- package/src/schema/config.ts +15 -0
- package/src/schema/cover-theme.test.ts +54 -0
- package/src/schema/cover-theme.ts +164 -0
- package/src/schema/state.ts +2 -0
- package/src/text.test.ts +7 -7
- package/src/text.ts +2 -27
- package/src/workflow.test.ts +2 -1
- package/src/imgx/assets/browser/obstacle-flow.d.ts +0 -27
- package/src/imgx/assets/browser/obstacle-flow.js +0 -326
package/src/commands/render.ts
CHANGED
|
@@ -20,12 +20,15 @@ import { join } from "path";
|
|
|
20
20
|
import { parseArgs, requireArg, flagArg } from "../args";
|
|
21
21
|
import { printResult, renderRender } from "../output";
|
|
22
22
|
import { loadConfig } from "../config";
|
|
23
|
+
import { resolveRenderBranding } from "../render-branding";
|
|
24
|
+
import { resolveCoverTheme } from "../schema/cover-theme";
|
|
23
25
|
import { resolveImageRenderer } from "../adapter-loader";
|
|
24
26
|
import {
|
|
25
27
|
acquireStateOperationLock,
|
|
26
28
|
defaultBodyInputs,
|
|
27
29
|
normalizeNewspicRenderSpec,
|
|
28
30
|
readResolvedState,
|
|
31
|
+
reenterRender,
|
|
29
32
|
validateForPhase,
|
|
30
33
|
writeState,
|
|
31
34
|
type NewspicRenderSpec,
|
|
@@ -59,6 +62,7 @@ Usage: zzhub-pipeline render [options]
|
|
|
59
62
|
Options:
|
|
60
63
|
--state Path to state JSON (required)
|
|
61
64
|
--skip-render Plan only, don't invoke renderer (optional)
|
|
65
|
+
--cover-theme Cover theme ID; saves the task choice (optional)
|
|
62
66
|
`.trim());
|
|
63
67
|
return;
|
|
64
68
|
}
|
|
@@ -88,6 +92,20 @@ Options:
|
|
|
88
92
|
// ── Image Plan ──────────────────────────────────────────────────
|
|
89
93
|
|
|
90
94
|
const routePrimary = state.route.primary;
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
const requestedTheme = parsed["cover-theme"] === undefined ? undefined : requireArg(parsed, "cover-theme", "cover theme ID").trim();
|
|
97
|
+
const coverTheme = routePrimary === "blog" ? undefined : resolveCoverTheme(
|
|
98
|
+
config.render.cover,
|
|
99
|
+
routePrimary === "wechat-article" ? "wechat-cover-split" : "poster-3-4",
|
|
100
|
+
state.route.account,
|
|
101
|
+
requestedTheme ?? state.intent.cover_theme,
|
|
102
|
+
);
|
|
103
|
+
if (requestedTheme !== undefined && requestedTheme !== state.intent.cover_theme) {
|
|
104
|
+
if (!coverTheme) throw new Error("blog 路由不需要封面主题");
|
|
105
|
+
state.intent.cover_theme = requestedTheme;
|
|
106
|
+
reenterRender(state);
|
|
107
|
+
await writeState(statePath, state);
|
|
108
|
+
}
|
|
91
109
|
|
|
92
110
|
if (routePrimary === "blog") {
|
|
93
111
|
// Blog doesn't need images by default
|
|
@@ -143,6 +161,7 @@ Options:
|
|
|
143
161
|
template,
|
|
144
162
|
cover_template: coverTemplate,
|
|
145
163
|
cover_title: coverTitle,
|
|
164
|
+
cover_theme: coverTheme?.id ?? null,
|
|
146
165
|
output_dir: outputDir,
|
|
147
166
|
preview_required: false,
|
|
148
167
|
status: "planned",
|
|
@@ -203,7 +222,6 @@ Options:
|
|
|
203
222
|
|
|
204
223
|
// ── Invoke image renderer adapter ───────────────────────────────
|
|
205
224
|
|
|
206
|
-
const config = loadConfig();
|
|
207
225
|
const imageRenderer = await resolveImageRenderer(config);
|
|
208
226
|
reportProgress({ stage: "render.adapter", message: "开始生成图片" });
|
|
209
227
|
|
|
@@ -214,7 +232,9 @@ Options:
|
|
|
214
232
|
? state.images.body_inputs.received
|
|
215
233
|
: [];
|
|
216
234
|
|
|
235
|
+
const branding = resolveRenderBranding(config, state.route.account);
|
|
217
236
|
const renderResult = await imageRenderer.render({
|
|
237
|
+
coverTheme,
|
|
218
238
|
onProgress: reportProgress,
|
|
219
239
|
state,
|
|
220
240
|
bodyText: cleanBody,
|
|
@@ -231,14 +251,12 @@ Options:
|
|
|
231
251
|
bodyImages,
|
|
232
252
|
minPages: newspicRenderSpec.min_pages,
|
|
233
253
|
maxPages: newspicRenderSpec.max_pages,
|
|
234
|
-
accountVisualParams:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
: undefined,
|
|
254
|
+
accountVisualParams: {
|
|
255
|
+
footer: branding.footerText,
|
|
256
|
+
bg: state.route.account_visual_params?.bg,
|
|
257
|
+
highlight: state.route.account_visual_params?.highlight,
|
|
258
|
+
fallbackIcon: branding.logo,
|
|
259
|
+
},
|
|
242
260
|
theme: routePrimary === "wechat-newspic" ? getLongformTheme(state.route.account) : undefined,
|
|
243
261
|
template,
|
|
244
262
|
});
|
package/src/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { homedir, platform } from "os";
|
|
11
11
|
import { dirname, isAbsolute, join, resolve } from "path";
|
|
12
12
|
import { PipelineConfigSchema } from "./schema/config";
|
|
13
|
+
import { mergeCoverSettings } from "./schema/cover-theme";
|
|
13
14
|
|
|
14
15
|
import type {
|
|
15
16
|
PipelineConfig,
|
|
@@ -196,6 +197,7 @@ export function normalizeConfig(value: unknown, legacyValue?: unknown): Pipeline
|
|
|
196
197
|
cos: mergedCos,
|
|
197
198
|
plugins: mergedPlugins,
|
|
198
199
|
imgx: mergedImgx,
|
|
200
|
+
render: mergeCoverSettings(legacy.render ?? {}, source.render ?? {}),
|
|
199
201
|
});
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -403,6 +405,9 @@ export function setConfigValue(
|
|
|
403
405
|
): PipelineConfig {
|
|
404
406
|
const next = JSON.parse(JSON.stringify(config)) as PipelineConfig;
|
|
405
407
|
const segments = key.split(".").filter(Boolean);
|
|
408
|
+
if (segments.some((segment) => ["__proto__", "constructor", "prototype"].includes(segment))) {
|
|
409
|
+
throw new Error("invalid config key");
|
|
410
|
+
}
|
|
406
411
|
if (segments.length === 0) {
|
|
407
412
|
throw new Error("config key cannot be empty");
|
|
408
413
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"business": {
|
|
3
|
+
"name": "商务", "description": "浅灰与深蓝,左对齐标题与细线",
|
|
4
|
+
"colors": { "text": "#13243A", "accent": "#2457A7", "muted": "#586A80" },
|
|
5
|
+
"background": { "fill": { "type": "solid", "color": "#F1F5F9" } },
|
|
6
|
+
"decoration": { "pattern": "lines", "color": "#2457A7", "opacity": 0.18 }
|
|
7
|
+
},
|
|
8
|
+
"technology": {
|
|
9
|
+
"name": "科技", "description": "深蓝渐变与青色网格",
|
|
10
|
+
"colors": { "text": "#EAF4FF", "accent": "#67E8F9", "muted": "#A6BDD8" },
|
|
11
|
+
"background": { "fill": { "type": "linear", "angle": 135, "stops": [{ "color": "#0B1027", "offset": 0 }, { "color": "#182B5B", "offset": 100 }] } },
|
|
12
|
+
"decoration": { "pattern": "grid", "color": "#67E8F9", "opacity": 0.1, "size": 48 }
|
|
13
|
+
},
|
|
14
|
+
"minimal": {
|
|
15
|
+
"name": "简约", "description": "白底黑字,宽留白",
|
|
16
|
+
"colors": { "text": "#18181B", "accent": "#52525B", "muted": "#71717A" },
|
|
17
|
+
"background": { "fill": { "type": "solid", "color": "#FFFFFF" } },
|
|
18
|
+
"layout": { "safeArea": { "left": 108, "right": 108, "top": 108, "bottom": 72 } }
|
|
19
|
+
},
|
|
20
|
+
"solid": {
|
|
21
|
+
"name": "纯色", "description": "钴蓝与白色大字,黄色强调",
|
|
22
|
+
"colors": { "text": "#FFFFFF", "accent": "#FFE08A", "muted": "#D9E1FF" },
|
|
23
|
+
"background": { "fill": { "type": "solid", "color": "#3155D9" } },
|
|
24
|
+
"layout": { "textAlign": "center" }
|
|
25
|
+
},
|
|
26
|
+
"cute": {
|
|
27
|
+
"name": "可爱", "description": "粉黄渐变、手写字与轻盈圆点",
|
|
28
|
+
"typography": { "title": { "fontFamily": "LXGWWenKai", "fontWeight": 700 }, "subtitle": { "fontFamily": "LXGWWenKai", "fontSize": 56, "minFontSize": 40, "fontWeight": 400, "lineHeight": 1.4 } },
|
|
29
|
+
"colors": { "text": "#623C47", "accent": "#D64B78", "muted": "#856675" },
|
|
30
|
+
"background": { "fill": { "type": "linear", "angle": 150, "stops": [{ "color": "#FFF1F4", "offset": 0 }, { "color": "#FFF6DF", "offset": 100 }] } },
|
|
31
|
+
"decoration": { "pattern": "dots", "color": "#E4658C", "opacity": 0.12, "size": 58 },
|
|
32
|
+
"layout": { "textAlign": "center" }
|
|
33
|
+
},
|
|
34
|
+
"serious": {
|
|
35
|
+
"name": "严肃", "description": "米白纸面、墨色宋体与暗红强调",
|
|
36
|
+
"typography": { "title": { "fontFamily": "LXGWNeoZhiSongPlus", "fontWeight": 700 } },
|
|
37
|
+
"colors": { "text": "#202020", "accent": "#8A302B", "muted": "#655E54" },
|
|
38
|
+
"background": { "fill": { "type": "solid", "color": "#F3F0E8" } },
|
|
39
|
+
"decoration": { "pattern": "lines", "color": "#8A302B", "opacity": 0.18 }
|
|
40
|
+
},
|
|
41
|
+
"abstract": {
|
|
42
|
+
"name": "抽象", "description": "淡紫与冰蓝,边缘错位圆环",
|
|
43
|
+
"colors": { "text": "#302456", "accent": "#7653D5", "muted": "#695A87" },
|
|
44
|
+
"background": { "fill": { "type": "radial", "position": { "x": 20, "y": 10 }, "stops": [{ "color": "#EEE6FF", "offset": 0 }, { "color": "#E1F2FF", "offset": 100 }] } },
|
|
45
|
+
"decoration": { "pattern": "rings", "color": "#7653D5", "opacity": 0.16, "size": 180 }
|
|
46
|
+
},
|
|
47
|
+
"entertainment": {
|
|
48
|
+
"name": "娱乐", "description": "明黄底色、深紫大字与粉色几何装饰",
|
|
49
|
+
"colors": { "text": "#241341", "accent": "#B52D74", "muted": "#62532B" },
|
|
50
|
+
"background": { "fill": { "type": "solid", "color": "#FFF06A" } },
|
|
51
|
+
"decoration": { "pattern": "burst", "color": "#ED4F9A", "opacity": 0.32, "size": 144 }
|
|
52
|
+
},
|
|
53
|
+
"fresh-sage": { "name": "清新绿", "description": "早早集市原有绿色配色" },
|
|
54
|
+
"soft-rose": {
|
|
55
|
+
"name": "柔和粉", "description": "古一账号原有粉色配色",
|
|
56
|
+
"colors": { "text": "#1A1A1A", "accent": "#CA6093", "muted": "#795C6D" },
|
|
57
|
+
"background": { "fill": { "type": "solid", "color": "#FAF5F8" } }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
html, body { margin: 0; }
|
|
3
|
+
.cover { width: var(--width); height: var(--height); position: relative; overflow: hidden; background: var(--background); color: var(--text); }
|
|
4
|
+
.layer { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
5
|
+
.pattern { opacity: var(--decoration-opacity); }
|
|
6
|
+
.pattern-lines { border-top: 2px solid var(--decoration); border-bottom: 2px solid var(--decoration); inset: 24px; width: auto; height: auto; }
|
|
7
|
+
.pattern-grid { background-image: linear-gradient(var(--decoration) 1px, transparent 1px), linear-gradient(90deg, var(--decoration) 1px, transparent 1px); background-size: var(--decoration-size) var(--decoration-size); }
|
|
8
|
+
.pattern-dots { background-image: radial-gradient(circle, var(--decoration) 2px, transparent 3px); background-size: var(--decoration-size) var(--decoration-size); }
|
|
9
|
+
.pattern-rings { background: repeating-radial-gradient(circle at 105% 8%, transparent 0, transparent calc(var(--decoration-size) - 3px), var(--decoration) calc(var(--decoration-size) - 2px), var(--decoration) var(--decoration-size)); }
|
|
10
|
+
.pattern-burst { background: repeating-conic-gradient(from 15deg at 105% -5%, var(--decoration) 0deg 8deg, transparent 8deg 28deg); mask-image: radial-gradient(circle at 100% 0, black 0, transparent 65%); }
|
|
11
|
+
.copy { position: absolute; inset: 0 auto 0 0; width: var(--copy-width); }
|
|
12
|
+
.text-area { position: absolute; top: var(--top); right: var(--right); bottom: calc(var(--bottom) + 80px); left: var(--left); display: flex; flex-direction: column; justify-content: var(--vertical); text-align: var(--align); }
|
|
13
|
+
.text-stack { flex: 0 0 auto; width: 100%; }
|
|
14
|
+
.title, .subtitle { margin: 0; white-space: pre-wrap; text-wrap: balance; overflow-wrap: anywhere; }
|
|
15
|
+
.title { font-family: var(--title-font), sans-serif; font-size: var(--title-size); font-weight: var(--title-weight); line-height: var(--title-leading); letter-spacing: var(--title-tracking); }
|
|
16
|
+
.subtitle { margin-top: var(--block-gap); font-family: var(--subtitle-font), serif; font-size: var(--subtitle-size); font-weight: var(--subtitle-weight); line-height: var(--subtitle-leading); letter-spacing: var(--subtitle-tracking); }
|
|
17
|
+
.subtitle:empty { display: none; }
|
|
18
|
+
.highlight { color: var(--accent); }
|
|
19
|
+
.signature { position: absolute; left: var(--left); right: var(--right); bottom: var(--bottom); display: flex; align-items: center; gap: 12px; color: var(--muted); }
|
|
20
|
+
.signature span { min-width: 0; overflow-wrap: anywhere; font-family: var(--footer-font), sans-serif; font-size: var(--footer-size); font-weight: var(--footer-weight); line-height: var(--footer-leading); letter-spacing: var(--footer-tracking); }
|
|
21
|
+
.signature img { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%; object-fit: contain; }
|
|
22
|
+
.logo-panel { display: none; }
|
|
23
|
+
.cover-banner .signature img { display: none; }
|
|
24
|
+
.cover-banner .logo-panel { display: flex; position: absolute; right: 0; top: 0; width: 400px; height: 400px; align-items: center; justify-content: center; }
|
|
25
|
+
.logo-panel img { width: 216px; height: 216px; border-radius: 40px; object-fit: contain; }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 字号测量和最终成图都使用同一个 DOM,避免二次换行偏差。
|
|
2
|
+
window.__zzhubRenderReady = (async function fitCover() {
|
|
3
|
+
try {
|
|
4
|
+
const config = JSON.parse(document.getElementById("cover-config").textContent);
|
|
5
|
+
const cover = document.querySelector(".cover");
|
|
6
|
+
const area = document.querySelector(".text-area");
|
|
7
|
+
const stack = document.querySelector(".text-stack");
|
|
8
|
+
const signature = document.querySelector(".signature");
|
|
9
|
+
const title = document.querySelector(".title");
|
|
10
|
+
const subtitle = document.querySelector(".subtitle");
|
|
11
|
+
// 大型 CJK 字体必须加载完成,不能用回退字体计算字号。
|
|
12
|
+
await Promise.all(Object.values(config.typography).map(font => document.fonts.load(`${font.fontWeight} ${font.fontSize}px "${font.fontFamily}"`)));
|
|
13
|
+
await document.fonts.ready;
|
|
14
|
+
await Promise.all([...document.images].map(image => image.decode()));
|
|
15
|
+
area.style.bottom = `${config.layout.safeArea.bottom + signature.getBoundingClientRect().height + 32}px`;
|
|
16
|
+
if (area.clientWidth <= 0 || area.clientHeight <= 0) throw new Error("封面安全区无法容纳文字和署名");
|
|
17
|
+
function fit(ratio) {
|
|
18
|
+
for (const role of ["title", "subtitle"]) {
|
|
19
|
+
const font = config.typography[role];
|
|
20
|
+
cover.style.setProperty(`--${role}-size`, `${font.minFontSize + (font.fontSize - font.minFontSize) * ratio}px`);
|
|
21
|
+
}
|
|
22
|
+
return stack.getBoundingClientRect().height <= area.clientHeight + 0.5
|
|
23
|
+
&& title.scrollWidth <= title.clientWidth + 1
|
|
24
|
+
&& subtitle.scrollWidth <= subtitle.clientWidth + 1;
|
|
25
|
+
}
|
|
26
|
+
if (!fit(0)) throw new Error("完整标题在最小字号下仍超出安全区,请缩短标题或调整主题字号、安全区");
|
|
27
|
+
let low = 0;
|
|
28
|
+
let high = 1;
|
|
29
|
+
for (let iteration = 0; iteration < 12; iteration++) {
|
|
30
|
+
const middle = (low + high) / 2;
|
|
31
|
+
if (fit(middle)) low = middle;
|
|
32
|
+
else high = middle;
|
|
33
|
+
}
|
|
34
|
+
fit(low);
|
|
35
|
+
document.documentElement.dataset.coverStatus = "ready";
|
|
36
|
+
} catch (error) {
|
|
37
|
+
document.documentElement.dataset.coverStatus = "error";
|
|
38
|
+
document.documentElement.dataset.coverError = encodeURIComponent(error instanceof Error ? error.message : String(error));
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
Parameters:
|
|
10
10
|
{{THEME_CSS_VARS}} — 主题变量
|
|
11
11
|
{{FOOTER_TEXT}} — 底部文字
|
|
12
|
-
{{ICON_PATH}} —
|
|
13
|
-
{{AVATAR_PATH}} — 底部头像
|
|
12
|
+
{{ICON_PATH}} — 署名图标
|
|
14
13
|
{{FONT_PATH}} — 顶部/底部字体
|
|
15
14
|
{{BODY_FONT_PATH}} — 正文字体
|
|
16
15
|
{{LONGFORM_STAGE_HTML}} — 已排版页 HTML
|
|
@@ -46,18 +45,6 @@
|
|
|
46
45
|
padding: var(--body-padding-y) var(--body-padding-x) 0;
|
|
47
46
|
flex-shrink: 0;
|
|
48
47
|
}
|
|
49
|
-
.deco-top {
|
|
50
|
-
width: 100%;
|
|
51
|
-
height: var(--logo-size);
|
|
52
|
-
display: flex;
|
|
53
|
-
align-items: flex-start;
|
|
54
|
-
}
|
|
55
|
-
.deco-top img {
|
|
56
|
-
width: var(--logo-size);
|
|
57
|
-
height: var(--logo-size);
|
|
58
|
-
border-radius: 50%;
|
|
59
|
-
object-fit: cover;
|
|
60
|
-
}
|
|
61
48
|
.content {
|
|
62
49
|
min-height: 0;
|
|
63
50
|
width: 100%;
|
|
@@ -78,31 +65,23 @@
|
|
|
78
65
|
position: absolute;
|
|
79
66
|
left: 0;
|
|
80
67
|
font-family: "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif;
|
|
81
|
-
|
|
82
|
-
line-height: 1.75;
|
|
68
|
+
|
|
83
69
|
color: var(--body-color);
|
|
84
70
|
white-space: pre;
|
|
85
71
|
}
|
|
86
|
-
.body-heading {
|
|
72
|
+
.body-heading, .body-subheading, .body-minor-heading {
|
|
87
73
|
position: absolute;
|
|
88
|
-
left: 0;
|
|
89
|
-
font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
|
|
90
|
-
font-size: 38px;
|
|
91
|
-
line-height: 1.47;
|
|
92
74
|
color: var(--accent-color);
|
|
93
75
|
white-space: pre;
|
|
94
76
|
}
|
|
95
77
|
.body-quote {
|
|
96
78
|
position: absolute;
|
|
97
79
|
left: 0;
|
|
98
|
-
padding-left: 20px;
|
|
99
|
-
border-left: 4px solid color-mix(in srgb, var(--accent-color) 38%, white);
|
|
100
80
|
font-family: "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif;
|
|
101
|
-
font-size: 30px;
|
|
102
|
-
line-height: 1.73;
|
|
103
81
|
color: var(--quote-color);
|
|
104
82
|
white-space: pre;
|
|
105
83
|
}
|
|
84
|
+
.body-quote::before { content: ""; position: absolute; left: -24px; top: 8px; bottom: 8px; border-left: 3px solid var(--accent-color); }
|
|
106
85
|
.body-list-bullet {
|
|
107
86
|
position: absolute;
|
|
108
87
|
min-width: 24px;
|
|
@@ -118,9 +97,9 @@
|
|
|
118
97
|
.body-caption {
|
|
119
98
|
position: absolute;
|
|
120
99
|
left: 0;
|
|
121
|
-
font-family: "
|
|
122
|
-
font-size:
|
|
123
|
-
line-height:
|
|
100
|
+
font-family: "LXGWNeoZhiSongPlus", "PingFang SC", serif;
|
|
101
|
+
font-size: 28px;
|
|
102
|
+
line-height: 40px;
|
|
124
103
|
color: var(--caption-color);
|
|
125
104
|
white-space: normal;
|
|
126
105
|
text-align: center;
|
|
@@ -150,20 +129,18 @@
|
|
|
150
129
|
border-radius: 50%;
|
|
151
130
|
object-fit: cover;
|
|
152
131
|
}
|
|
132
|
+
.watermark .page-number { margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
153
133
|
.watermark span {
|
|
154
|
-
font-size:
|
|
134
|
+
font-size: 26px;
|
|
135
|
+
line-height: 36px;
|
|
155
136
|
color: var(--watermark-color);
|
|
156
137
|
font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
|
|
157
|
-
letter-spacing:
|
|
138
|
+
letter-spacing: 0;
|
|
158
139
|
}
|
|
159
140
|
</style>
|
|
160
141
|
</head>
|
|
161
142
|
<body>
|
|
162
|
-
<div class="header-shell">
|
|
163
|
-
<div class="deco-top">
|
|
164
|
-
<img src="{{ICON_PATH}}" alt="icon">
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
143
|
+
<div class="header-shell"></div>
|
|
167
144
|
|
|
168
145
|
<main class="content">
|
|
169
146
|
<div class="content-stage" id="content-stage">{{LONGFORM_STAGE_HTML}}</div>
|
|
@@ -171,8 +148,9 @@
|
|
|
171
148
|
|
|
172
149
|
<div class="footer-shell">
|
|
173
150
|
<div class="watermark">
|
|
174
|
-
|
|
151
|
+
{{BRAND_IMAGE}}
|
|
175
152
|
<span>{{FOOTER_TEXT}}</span>
|
|
153
|
+
<span class="page-number">{{PAGE_LABEL}}</span>
|
|
176
154
|
</div>
|
|
177
155
|
</div>
|
|
178
156
|
</body>
|