@v1hz/md2docx 1.0.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/LICENSE +21 -0
- package/README.md +115 -0
- package/config/config.json +34 -0
- package/config/config.schema.json +173 -0
- package/package.json +60 -0
- package/src/cli.ts +200 -0
- package/src/config.ts +52 -0
- package/src/index.ts +63 -0
- package/src/paths.ts +32 -0
- package/src/preprocess/caption.ts +107 -0
- package/src/preprocess/index.ts +49 -0
- package/src/preprocess/mermaid.ts +146 -0
- package/src/preprocess/title.ts +144 -0
- package/src/style/extract.ts +381 -0
- package/src/web/app.css +298 -0
- package/src/web/app.js +181 -0
- package/src/web/index.html +27 -0
- package/src/web.ts +148 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
3
|
+
import { useNamespaces } from "xpath";
|
|
4
|
+
import PizZip from "pizzip";
|
|
5
|
+
|
|
6
|
+
const W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
7
|
+
|
|
8
|
+
// 注册命名空间,创建带 w: 前缀的 xpath 选择器
|
|
9
|
+
const x = useNamespaces({ w: W });
|
|
10
|
+
|
|
11
|
+
type XmlNode = Node;
|
|
12
|
+
|
|
13
|
+
/** 选择单个元素 */
|
|
14
|
+
function one(node: XmlNode, expr: string): Element | null {
|
|
15
|
+
const result = x(expr, node) as Node | Node[] | null;
|
|
16
|
+
if (!result) return null;
|
|
17
|
+
if (Array.isArray(result)) return (result[0] as Element) ?? null;
|
|
18
|
+
return result as Element;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 选择元素列表 */
|
|
22
|
+
function all(node: XmlNode, expr: string): Element[] {
|
|
23
|
+
const result = x(expr, node);
|
|
24
|
+
if (!result) return [];
|
|
25
|
+
if (Array.isArray(result)) return result as Element[];
|
|
26
|
+
return [result as Element];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 取 w: 命名空间下的属性值 */
|
|
30
|
+
function wAttr(el: Element | null, local: string): string | null {
|
|
31
|
+
if (!el) return null;
|
|
32
|
+
return el.getAttributeNS(W, local) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wNumber(el: Element | null, local = "val"): number | null {
|
|
36
|
+
const value = wAttr(el, local);
|
|
37
|
+
if (value === null || value.trim() === "") return null;
|
|
38
|
+
const number = Number(value);
|
|
39
|
+
return Number.isFinite(number) ? number : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 读取 OOXML ON/OFF 属性;省略 val 表示 true。 */
|
|
43
|
+
function wBool(el: Element | null, local: string): boolean | null {
|
|
44
|
+
if (!el) return null;
|
|
45
|
+
const found = one(el, `w:${local}`);
|
|
46
|
+
if (!found) return null;
|
|
47
|
+
const v = wAttr(found, "val");
|
|
48
|
+
return v === null || !["0", "false", "off", "no"].includes(v.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 映射函数 ─────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function mapRunProps(rPr: Element | null): Record<string, unknown> {
|
|
54
|
+
if (!rPr) return {};
|
|
55
|
+
const out: Record<string, unknown> = {};
|
|
56
|
+
|
|
57
|
+
// 字体
|
|
58
|
+
const rFonts = one(rPr, "w:rFonts");
|
|
59
|
+
if (rFonts) {
|
|
60
|
+
const font: Record<string, string> = {};
|
|
61
|
+
for (const a of ["ascii", "hAnsi", "cs", "eastAsia"]) {
|
|
62
|
+
const v = wAttr(rFonts, a);
|
|
63
|
+
if (v) font[a] = v;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(font).length) out.font = font;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 字号
|
|
69
|
+
const sz = one(rPr, "w:sz");
|
|
70
|
+
if (sz) {
|
|
71
|
+
const v = wNumber(sz);
|
|
72
|
+
if (v !== null) out.size = v;
|
|
73
|
+
}
|
|
74
|
+
const szCs = one(rPr, "w:szCs");
|
|
75
|
+
if (szCs) {
|
|
76
|
+
const v = wNumber(szCs);
|
|
77
|
+
if (v !== null) out.sizeComplexScript = v;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 颜色
|
|
81
|
+
const color = one(rPr, "w:color");
|
|
82
|
+
if (color) {
|
|
83
|
+
const v = wAttr(color, "val");
|
|
84
|
+
if (v) out.color = v.toUpperCase();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 开关属性
|
|
88
|
+
const boolMap: [string, string][] = [
|
|
89
|
+
["b", "bold"],
|
|
90
|
+
["bCs", "boldComplexScript"],
|
|
91
|
+
["i", "italics"],
|
|
92
|
+
["iCs", "italicsComplexScript"],
|
|
93
|
+
["strike", "strike"],
|
|
94
|
+
["dstrike", "doubleStrike"],
|
|
95
|
+
["smallCaps", "smallCaps"],
|
|
96
|
+
["caps", "allCaps"],
|
|
97
|
+
["vanish", "vanish"],
|
|
98
|
+
["emboss", "emboss"],
|
|
99
|
+
["imprint", "imprint"],
|
|
100
|
+
];
|
|
101
|
+
for (const [tag, key] of boolMap) {
|
|
102
|
+
const v = wBool(rPr, tag);
|
|
103
|
+
if (v !== null) out[key] = v;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 上标/下标
|
|
107
|
+
const va = one(rPr, "w:vertAlign");
|
|
108
|
+
if (va) {
|
|
109
|
+
const v = wAttr(va, "val");
|
|
110
|
+
if (v === "superscript") out.superScript = true;
|
|
111
|
+
else if (v === "subscript") out.subScript = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 下划线
|
|
115
|
+
const u = one(rPr, "w:u");
|
|
116
|
+
if (u) {
|
|
117
|
+
const ul: Record<string, string> = {};
|
|
118
|
+
const ut = wAttr(u, "val");
|
|
119
|
+
if (ut) ul.type = ut;
|
|
120
|
+
const uc = wAttr(u, "color");
|
|
121
|
+
if (uc) ul.color = uc;
|
|
122
|
+
out.underline = ul;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 字符间距
|
|
126
|
+
const sp = one(rPr, "w:spacing");
|
|
127
|
+
if (sp) {
|
|
128
|
+
const v = wNumber(sp);
|
|
129
|
+
if (v !== null) out.characterSpacing = v;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 语言
|
|
133
|
+
const lang = one(rPr, "w:lang");
|
|
134
|
+
if (lang) {
|
|
135
|
+
const lo: Record<string, string> = {};
|
|
136
|
+
for (const a of ["val", "eastAsia", "bidi"]) {
|
|
137
|
+
const v = wAttr(lang, a);
|
|
138
|
+
if (v) lo[a] = v;
|
|
139
|
+
}
|
|
140
|
+
if (Object.keys(lo).length) out.language = lo;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function mapParagraphProps(pPr: Element | null): Record<string, unknown> {
|
|
147
|
+
if (!pPr) return {};
|
|
148
|
+
const out: Record<string, unknown> = {};
|
|
149
|
+
|
|
150
|
+
const ALIGN_MAP: Record<string, string> = {
|
|
151
|
+
left: "left",
|
|
152
|
+
center: "center",
|
|
153
|
+
right: "right",
|
|
154
|
+
both: "both",
|
|
155
|
+
start: "start",
|
|
156
|
+
end: "end",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// 对齐
|
|
160
|
+
const jc = one(pPr, "w:jc");
|
|
161
|
+
if (jc) {
|
|
162
|
+
const v = wAttr(jc, "val");
|
|
163
|
+
if (v && v in ALIGN_MAP) out.alignment = ALIGN_MAP[v];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 间距
|
|
167
|
+
const spacing = one(pPr, "w:spacing");
|
|
168
|
+
if (spacing) {
|
|
169
|
+
const sp: Record<string, number | string> = {};
|
|
170
|
+
for (const [tag, key] of [
|
|
171
|
+
["before", "before"],
|
|
172
|
+
["after", "after"],
|
|
173
|
+
["line", "line"],
|
|
174
|
+
] as [string, string][]) {
|
|
175
|
+
const v = wNumber(spacing, tag);
|
|
176
|
+
if (v !== null) sp[key] = v;
|
|
177
|
+
}
|
|
178
|
+
const lr = wAttr(spacing, "lineRule");
|
|
179
|
+
if (lr !== null) sp.lineRule = lr;
|
|
180
|
+
if (Object.keys(sp).length) out.spacing = sp;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 缩进
|
|
184
|
+
const ind = one(pPr, "w:ind");
|
|
185
|
+
if (ind) {
|
|
186
|
+
const id: Record<string, number> = {};
|
|
187
|
+
for (const a of ["left", "right", "firstLine", "hanging", "start", "end"]) {
|
|
188
|
+
const v = wNumber(ind, a);
|
|
189
|
+
if (v !== null) id[a] = v;
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(id).length) out.indent = id;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 开关
|
|
195
|
+
for (const tag of [
|
|
196
|
+
"keepNext",
|
|
197
|
+
"keepLines",
|
|
198
|
+
"pageBreakBefore",
|
|
199
|
+
"widowControl",
|
|
200
|
+
"contextualSpacing",
|
|
201
|
+
]) {
|
|
202
|
+
const v = wBool(pPr, tag);
|
|
203
|
+
if (v !== null) out[tag] = v;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 大纲级别
|
|
207
|
+
const ol = one(pPr, "w:outlineLvl");
|
|
208
|
+
if (ol) {
|
|
209
|
+
const v = wNumber(ol);
|
|
210
|
+
if (v !== null) out.outlineLevel = v;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── 导出类型 ─────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
export interface StyleEntry {
|
|
219
|
+
id: string;
|
|
220
|
+
name: string;
|
|
221
|
+
basedOn?: string;
|
|
222
|
+
next?: string;
|
|
223
|
+
link?: string;
|
|
224
|
+
quickFormat?: boolean;
|
|
225
|
+
semiHidden?: boolean;
|
|
226
|
+
unhideWhenUsed?: boolean;
|
|
227
|
+
uiPriority?: number;
|
|
228
|
+
run?: Record<string, unknown>;
|
|
229
|
+
paragraph?: Record<string, unknown>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface ExtractedStyles {
|
|
233
|
+
default?: Record<string, unknown>;
|
|
234
|
+
paragraphStyles?: StyleEntry[];
|
|
235
|
+
characterStyles?: StyleEntry[];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const BUILTIN_PARA: Record<string, string> = {
|
|
239
|
+
Title: "title",
|
|
240
|
+
Heading1: "heading1",
|
|
241
|
+
Heading2: "heading2",
|
|
242
|
+
Heading3: "heading3",
|
|
243
|
+
Heading4: "heading4",
|
|
244
|
+
Heading5: "heading5",
|
|
245
|
+
Heading6: "heading6",
|
|
246
|
+
Strong: "strong",
|
|
247
|
+
ListParagraph: "listParagraph",
|
|
248
|
+
FootnoteText: "footnoteText",
|
|
249
|
+
EndnoteText: "endnoteText",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const BUILTIN_CHAR: Record<string, string> = {
|
|
253
|
+
Hyperlink: "hyperlink",
|
|
254
|
+
FootnoteReference: "footnoteReference",
|
|
255
|
+
FootnoteTextChar: "footnoteTextChar",
|
|
256
|
+
EndnoteReference: "endnoteReference",
|
|
257
|
+
EndnoteTextChar: "endnoteTextChar",
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** 从 docx 提取 styles 配置,返回可直接给 `new Document({ styles })` 的对象 */
|
|
261
|
+
export function extractStylesFromDocx(docxPath: string): ExtractedStyles {
|
|
262
|
+
const zip = new PizZip(readFileSync(docxPath));
|
|
263
|
+
const stylesFile = zip.file("word/styles.xml");
|
|
264
|
+
if (!stylesFile) throw new Error("word/styles.xml not found in docx");
|
|
265
|
+
|
|
266
|
+
const xml = stylesFile.asText();
|
|
267
|
+
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
|
268
|
+
const parseError = doc.getElementsByTagName("parsererror").item(0);
|
|
269
|
+
if (parseError) throw new Error(`Invalid word/styles.xml: ${parseError.textContent}`);
|
|
270
|
+
|
|
271
|
+
const defaultDoc: Record<string, unknown> = {};
|
|
272
|
+
const defaultOverrides: Record<string, unknown> = {};
|
|
273
|
+
const paragraphStyles: StyleEntry[] = [];
|
|
274
|
+
const characterStyles: StyleEntry[] = [];
|
|
275
|
+
|
|
276
|
+
// 1. docDefaults
|
|
277
|
+
const xmlDocument = doc as unknown as XmlNode;
|
|
278
|
+
const dd = one(xmlDocument, "//w:docDefaults");
|
|
279
|
+
if (dd) {
|
|
280
|
+
const rPrDef = one(dd, "w:rPrDefault/w:rPr");
|
|
281
|
+
if (rPrDef) {
|
|
282
|
+
const run = mapRunProps(rPrDef);
|
|
283
|
+
if (Object.keys(run).length) defaultDoc.run = run;
|
|
284
|
+
}
|
|
285
|
+
const pPrDef = one(dd, "w:pPrDefault/w:pPr");
|
|
286
|
+
if (pPrDef) {
|
|
287
|
+
const para = mapParagraphProps(pPrDef);
|
|
288
|
+
if (Object.keys(para).length) defaultDoc.paragraph = para;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 2. 样式
|
|
293
|
+
for (const style of all(xmlDocument, "//w:style")) {
|
|
294
|
+
const styleId = wAttr(style, "styleId") ?? "";
|
|
295
|
+
const styleType = wAttr(style, "type") ?? "";
|
|
296
|
+
if (!styleId || !["paragraph", "character"].includes(styleType)) continue;
|
|
297
|
+
const name = wAttr(one(style, "w:name"), "val") ?? styleId;
|
|
298
|
+
|
|
299
|
+
// 跳过隐式样式和 token
|
|
300
|
+
if (["Normal", "DefaultParagraphFont", "TableNormal"].includes(styleId)) continue;
|
|
301
|
+
if (styleId.endsWith("Tok") && styleType === "character") continue;
|
|
302
|
+
|
|
303
|
+
const entry: StyleEntry = { id: styleId, name };
|
|
304
|
+
|
|
305
|
+
// basedOn
|
|
306
|
+
const bo = one(style, "w:basedOn");
|
|
307
|
+
if (bo) {
|
|
308
|
+
const v = wAttr(bo, "val");
|
|
309
|
+
if (v && !["Normal", "DefaultParagraphFont"].includes(v)) entry.basedOn = v;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// next
|
|
313
|
+
const next = one(style, "w:next");
|
|
314
|
+
if (next) {
|
|
315
|
+
const v = wAttr(next, "val");
|
|
316
|
+
if (v && v !== styleId) entry.next = v;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// link
|
|
320
|
+
const link = one(style, "w:link");
|
|
321
|
+
if (link) {
|
|
322
|
+
const v = wAttr(link, "val");
|
|
323
|
+
if (v) entry.link = v;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 标志位
|
|
327
|
+
if (one(style, "w:qFormat")) entry.quickFormat = true;
|
|
328
|
+
if (one(style, "w:semiHidden")) entry.semiHidden = true;
|
|
329
|
+
if (one(style, "w:unhideWhenUsed")) entry.unhideWhenUsed = true;
|
|
330
|
+
|
|
331
|
+
const ui = one(style, "w:uiPriority");
|
|
332
|
+
if (ui) {
|
|
333
|
+
const v = wNumber(ui);
|
|
334
|
+
if (v !== null) entry.uiPriority = v;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// run / paragraph
|
|
338
|
+
const run = mapRunProps(one(style, "w:rPr"));
|
|
339
|
+
if (Object.keys(run).length) entry.run = run;
|
|
340
|
+
const para = mapParagraphProps(one(style, "w:pPr"));
|
|
341
|
+
if (Object.keys(para).length) entry.paragraph = para;
|
|
342
|
+
|
|
343
|
+
// 分发
|
|
344
|
+
if (styleType === "paragraph") {
|
|
345
|
+
const key = BUILTIN_PARA[styleId];
|
|
346
|
+
if (key) {
|
|
347
|
+
const { id: _, ...rest } = entry;
|
|
348
|
+
defaultOverrides[key] = rest;
|
|
349
|
+
} else {
|
|
350
|
+
paragraphStyles.push(entry);
|
|
351
|
+
}
|
|
352
|
+
} else if (styleType === "character") {
|
|
353
|
+
const key = BUILTIN_CHAR[styleId];
|
|
354
|
+
if (key) {
|
|
355
|
+
const { id: _, ...rest } = entry;
|
|
356
|
+
defaultOverrides[key] = rest;
|
|
357
|
+
} else {
|
|
358
|
+
characterStyles.push(entry);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const result: ExtractedStyles = {};
|
|
364
|
+
if (Object.keys(defaultDoc).length > 0 || Object.keys(defaultOverrides).length > 0) {
|
|
365
|
+
result.default = {
|
|
366
|
+
...(Object.keys(defaultDoc).length > 0 ? { document: defaultDoc } : {}),
|
|
367
|
+
...defaultOverrides,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (paragraphStyles.length) result.paragraphStyles = paragraphStyles;
|
|
371
|
+
if (characterStyles.length) result.characterStyles = characterStyles;
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (import.meta.main) {
|
|
377
|
+
const { STYLE_REF_DOCX, STYLE_JSON } = await import("../paths");
|
|
378
|
+
const styles = extractStylesFromDocx(STYLE_REF_DOCX);
|
|
379
|
+
writeFileSync(STYLE_JSON, JSON.stringify(styles, null, 2), "utf-8");
|
|
380
|
+
console.log(`Styles extracted from ${STYLE_REF_DOCX} to ${STYLE_JSON}`);
|
|
381
|
+
}
|
package/src/web/app.css
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--paper: #f7f8fa;
|
|
3
|
+
--sheet: #ffffff;
|
|
4
|
+
--ink: #15233b;
|
|
5
|
+
--muted: #637087;
|
|
6
|
+
--rule: #dce2eb;
|
|
7
|
+
--blue: #2457d6;
|
|
8
|
+
--blue-soft: #eaf0ff;
|
|
9
|
+
--red: #b73535;
|
|
10
|
+
--shadow: 0 24px 60px rgb(21 35 59 / 9%);
|
|
11
|
+
font-family: "Segoe UI Variable", "Microsoft YaHei UI", sans-serif;
|
|
12
|
+
color: var(--ink);
|
|
13
|
+
background: var(--paper);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
html {
|
|
20
|
+
scroll-behavior: smooth;
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
margin: 0;
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
}
|
|
26
|
+
button,
|
|
27
|
+
input,
|
|
28
|
+
select {
|
|
29
|
+
font: inherit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.workspace {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: minmax(0, 820px);
|
|
35
|
+
justify-content: center;
|
|
36
|
+
gap: 28px;
|
|
37
|
+
max-width: 1320px;
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
padding: 36px;
|
|
40
|
+
align-items: start;
|
|
41
|
+
}
|
|
42
|
+
.config-index {
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 50%;
|
|
45
|
+
left: max(36px, calc((100vw - 1292px) / 2));
|
|
46
|
+
transform: translateY(-50%);
|
|
47
|
+
display: grid;
|
|
48
|
+
gap: 4px;
|
|
49
|
+
width: 180px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.config-index a {
|
|
53
|
+
padding: 9px 12px;
|
|
54
|
+
color: var(--muted);
|
|
55
|
+
border-left: 2px solid transparent;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
transition: 0.15s ease;
|
|
59
|
+
}
|
|
60
|
+
.config-index a:hover,
|
|
61
|
+
.config-index a:focus-visible {
|
|
62
|
+
color: var(--blue);
|
|
63
|
+
background: var(--blue-soft);
|
|
64
|
+
border-left-color: var(--blue);
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
.config-sheet {
|
|
68
|
+
min-height: 400px;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
background: var(--sheet);
|
|
71
|
+
border: 1px solid var(--rule);
|
|
72
|
+
box-shadow: var(--shadow);
|
|
73
|
+
}
|
|
74
|
+
.loading {
|
|
75
|
+
padding: 60px;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
}
|
|
78
|
+
.group {
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: 210px 1fr;
|
|
81
|
+
border-bottom: 1px solid var(--rule);
|
|
82
|
+
scroll-margin-top: 24px;
|
|
83
|
+
}
|
|
84
|
+
.group:last-child {
|
|
85
|
+
border-bottom: 0;
|
|
86
|
+
}
|
|
87
|
+
.group-title {
|
|
88
|
+
padding: 30px;
|
|
89
|
+
background: #fbfcfe;
|
|
90
|
+
border-right: 1px solid var(--rule);
|
|
91
|
+
}
|
|
92
|
+
.group-title h2 {
|
|
93
|
+
margin: 0;
|
|
94
|
+
font-size: 17px;
|
|
95
|
+
line-height: 1.45;
|
|
96
|
+
}
|
|
97
|
+
.fields {
|
|
98
|
+
padding: 14px 30px;
|
|
99
|
+
}
|
|
100
|
+
.field {
|
|
101
|
+
display: grid;
|
|
102
|
+
grid-template-columns: minmax(0, 1fr) minmax(180px, 260px);
|
|
103
|
+
gap: 24px;
|
|
104
|
+
padding: 18px 0;
|
|
105
|
+
align-items: center;
|
|
106
|
+
border-bottom: 1px solid #edf0f5;
|
|
107
|
+
}
|
|
108
|
+
.field:last-child {
|
|
109
|
+
border-bottom: 0;
|
|
110
|
+
}
|
|
111
|
+
.field > label {
|
|
112
|
+
display: block;
|
|
113
|
+
font-weight: 650;
|
|
114
|
+
line-height: 1.5;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
input[type="text"],
|
|
118
|
+
input[type="number"],
|
|
119
|
+
select {
|
|
120
|
+
width: 100%;
|
|
121
|
+
min-height: 42px;
|
|
122
|
+
padding: 9px 11px;
|
|
123
|
+
color: var(--ink);
|
|
124
|
+
background: white;
|
|
125
|
+
border: 1px solid #bfc8d6;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
outline: none;
|
|
128
|
+
}
|
|
129
|
+
input:focus,
|
|
130
|
+
select:focus,
|
|
131
|
+
button:focus-visible {
|
|
132
|
+
border-color: var(--blue);
|
|
133
|
+
box-shadow: 0 0 0 3px rgb(36 87 214 / 16%);
|
|
134
|
+
}
|
|
135
|
+
.switch {
|
|
136
|
+
position: relative;
|
|
137
|
+
width: 52px;
|
|
138
|
+
height: 30px;
|
|
139
|
+
justify-self: end;
|
|
140
|
+
}
|
|
141
|
+
.switch input {
|
|
142
|
+
position: absolute;
|
|
143
|
+
opacity: 0;
|
|
144
|
+
}
|
|
145
|
+
.switch-track {
|
|
146
|
+
display: block;
|
|
147
|
+
width: 100%;
|
|
148
|
+
height: 100%;
|
|
149
|
+
border-radius: 30px;
|
|
150
|
+
background: #b8c0cc;
|
|
151
|
+
transition: 0.18s ease;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
.switch-track::after {
|
|
155
|
+
content: "";
|
|
156
|
+
position: absolute;
|
|
157
|
+
top: 4px;
|
|
158
|
+
left: 4px;
|
|
159
|
+
width: 22px;
|
|
160
|
+
height: 22px;
|
|
161
|
+
border-radius: 50%;
|
|
162
|
+
background: white;
|
|
163
|
+
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
|
|
164
|
+
transition: 0.18s ease;
|
|
165
|
+
}
|
|
166
|
+
.switch input:checked + .switch-track {
|
|
167
|
+
background: var(--blue);
|
|
168
|
+
}
|
|
169
|
+
.switch input:checked + .switch-track::after {
|
|
170
|
+
transform: translateX(22px);
|
|
171
|
+
}
|
|
172
|
+
.switch input:disabled + .switch-track {
|
|
173
|
+
cursor: not-allowed;
|
|
174
|
+
opacity: 0.55;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.action-bar {
|
|
178
|
+
position: fixed;
|
|
179
|
+
z-index: 5;
|
|
180
|
+
top: 50%;
|
|
181
|
+
right: max(36px, calc((100vw - 1292px) / 2));
|
|
182
|
+
display: grid;
|
|
183
|
+
gap: 16px;
|
|
184
|
+
width: 180px;
|
|
185
|
+
padding: 18px;
|
|
186
|
+
background: var(--sheet);
|
|
187
|
+
border: 1px solid var(--rule);
|
|
188
|
+
border-radius: 6px;
|
|
189
|
+
box-shadow: var(--shadow);
|
|
190
|
+
transform: translateY(-50%);
|
|
191
|
+
}
|
|
192
|
+
.action-bar p {
|
|
193
|
+
margin: 0;
|
|
194
|
+
color: var(--muted);
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
line-height: 1.5;
|
|
197
|
+
}
|
|
198
|
+
.action-bar p.error {
|
|
199
|
+
color: var(--red);
|
|
200
|
+
}
|
|
201
|
+
.actions {
|
|
202
|
+
display: grid;
|
|
203
|
+
gap: 8px;
|
|
204
|
+
}
|
|
205
|
+
button {
|
|
206
|
+
min-height: 42px;
|
|
207
|
+
padding: 0 18px;
|
|
208
|
+
color: white;
|
|
209
|
+
background: var(--blue);
|
|
210
|
+
border: 1px solid var(--blue);
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
button.secondary {
|
|
216
|
+
color: var(--ink);
|
|
217
|
+
background: white;
|
|
218
|
+
border-color: #bfc8d6;
|
|
219
|
+
}
|
|
220
|
+
button:disabled {
|
|
221
|
+
opacity: 0.45;
|
|
222
|
+
cursor: not-allowed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@media (max-width: 1120px) {
|
|
226
|
+
.workspace {
|
|
227
|
+
grid-template-columns: 1fr;
|
|
228
|
+
}
|
|
229
|
+
.config-index {
|
|
230
|
+
position: sticky;
|
|
231
|
+
z-index: 4;
|
|
232
|
+
top: 0;
|
|
233
|
+
left: 0;
|
|
234
|
+
transform: none;
|
|
235
|
+
display: flex;
|
|
236
|
+
gap: 2px;
|
|
237
|
+
overflow-x: auto;
|
|
238
|
+
padding: 8px;
|
|
239
|
+
background: rgb(247 248 250 / 96%);
|
|
240
|
+
border: 1px solid var(--rule);
|
|
241
|
+
backdrop-filter: blur(12px);
|
|
242
|
+
width: auto;
|
|
243
|
+
}
|
|
244
|
+
.config-index a {
|
|
245
|
+
flex: 0 0 auto;
|
|
246
|
+
border-bottom: 2px solid transparent;
|
|
247
|
+
border-left: 0;
|
|
248
|
+
}
|
|
249
|
+
.config-index a:hover,
|
|
250
|
+
.config-index a:focus-visible {
|
|
251
|
+
border-bottom-color: var(--blue);
|
|
252
|
+
}
|
|
253
|
+
.group {
|
|
254
|
+
grid-template-columns: 1fr;
|
|
255
|
+
}
|
|
256
|
+
.group-title {
|
|
257
|
+
border-right: 0;
|
|
258
|
+
border-bottom: 1px solid var(--rule);
|
|
259
|
+
}
|
|
260
|
+
.action-bar {
|
|
261
|
+
position: static;
|
|
262
|
+
width: auto;
|
|
263
|
+
max-width: 820px;
|
|
264
|
+
margin: -8px auto 36px;
|
|
265
|
+
transform: none;
|
|
266
|
+
}
|
|
267
|
+
.actions {
|
|
268
|
+
grid-template-columns: 1fr 1fr;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
@media (max-width: 560px) {
|
|
272
|
+
.workspace {
|
|
273
|
+
padding-right: 18px;
|
|
274
|
+
padding-left: 18px;
|
|
275
|
+
}
|
|
276
|
+
.field {
|
|
277
|
+
grid-template-columns: 1fr;
|
|
278
|
+
}
|
|
279
|
+
.switch {
|
|
280
|
+
justify-self: start;
|
|
281
|
+
}
|
|
282
|
+
.action-bar {
|
|
283
|
+
margin-right: 18px;
|
|
284
|
+
margin-left: 18px;
|
|
285
|
+
padding: 16px;
|
|
286
|
+
}
|
|
287
|
+
button {
|
|
288
|
+
padding: 0 12px;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
@media (prefers-reduced-motion: reduce) {
|
|
292
|
+
html {
|
|
293
|
+
scroll-behavior: auto;
|
|
294
|
+
}
|
|
295
|
+
* {
|
|
296
|
+
transition: none !important;
|
|
297
|
+
}
|
|
298
|
+
}
|