mosaic-headless 1.29.2 → 1.30.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.
@@ -0,0 +1,341 @@
1
+ """Content-driven components: the slider, the tabs and the list come from the posts.
2
+
3
+ This is the part a page builder cannot do without a plugin per component. In Mosaic
4
+ a loop is a node, and three of the component families take a loop IN PLACE OF their
5
+ hand-authored children:
6
+
7
+ slider-slides > slider-loop-slides > slider-slide one slide per post
8
+ tabs-menu > tabs-loop-tabs > tabs-tab one tab per post
9
+ tabs-content > tabs-loop-tab-panes > tabs-tab-pane
10
+ list > list-loop-items > list-item
11
+ accordion > accordion-loop-items> accordion-item
12
+
13
+ Each loop node carries the same three keys an ordinary `loop` does - `loopType`,
14
+ `loopNamespace` and a `<loopType>Options` group - and the children inside it read
15
+ their row with `@VAR('<namespace>/…')`. Nothing on this page is typed out twice:
16
+ publish another post and every one of these grows by one.
17
+
18
+ The other half is `loop-pagination`, which is what turns a loop into an archive:
19
+ previous / numbered pages / next, each a node of its own under the loop.
20
+ """
21
+ import uuid
22
+
23
+ from _moksa import (CJK, DISPLAY, FOCUS_RING, MONO, RULE, T, bp, box, grid, ml,
24
+ mono, section, wrap)
25
+
26
+ U = lambda: str(uuid.uuid4())
27
+
28
+ POSTS = "wpPostTypePost"
29
+ INK = "rgb(22,24,28)"
30
+ PAPER = "rgb(250,250,247)"
31
+
32
+
33
+ def strip(label, types):
34
+ key = label.lower().replace(" ", "-").replace("/", "")
35
+ return box("ct-strip-" + key, {
36
+ "display": "flex", "justifyContent": "space-between",
37
+ "alignItems": "baseline", "columnGap": "16px", "flexWrap": "wrap",
38
+ "rowGap": "6px", "paddingBottom": "14px", "marginBottom": "26px",
39
+ "customDeclarations": "border-bottom:1px solid rgb(22,24,28);"},
40
+ [mono(label, size="11px", color="--mk-ink", track="0.26em"),
41
+ mono(types, size="10px", color="--mk-faint", track="0.08em")])
42
+
43
+
44
+ def var(attr, expr, tag="p", _t=None, _m=None, **st):
45
+ n = {"type": "text", "data": {"tagName": tag, "attrID": attr},
46
+ "children": [{"type": "wysiwyg-variable", "data": {"dynamicCode": expr}}]}
47
+ if st or _t or _m:
48
+ n["style"] = bp(st, _t, _m)
49
+ return n
50
+
51
+
52
+ def loop_data(attr, ns, maxitems, **extra):
53
+ d = {"attrID": attr, "loopType": POSTS, "loopNamespace": ns,
54
+ POSTS + "Options": dict({"maxItems": str(maxitems), "offset": "0"}, **extra)}
55
+ return d
56
+
57
+
58
+ # ---------------------------------------------------------------- 1. archive
59
+
60
+ def archive():
61
+ """A loop with real pagination - previous, numbers, next.
62
+
63
+ `paginationKey` is what the page's query string uses, so two paginated loops
64
+ on one page do not move together. Do NOT call it `p`: that is one of
65
+ WordPress's own reserved query vars (it means "post ID"), and `?p=2` takes the
66
+ visitor to post 2 instead of page 2 of this loop.
67
+ """
68
+ btn = {"&": {"_": {
69
+ "fontFamily": MONO, "fontSize": "10px", "letterSpacing": "0.16em",
70
+ "cursor": "pointer", "color": {"token": "--mk-ink"},
71
+ "paddingTop": "9px", "paddingBottom": "9px",
72
+ "paddingLeft": "14px", "paddingRight": "14px",
73
+ "customDeclarations": "border:1px solid rgba(22,24,28,.3);"},
74
+ "_t": {"fontSize": "12px", "paddingTop": "11px",
75
+ "paddingBottom": "11px"}},
76
+ "hover": {"_": {"backgroundColor": {"token": "--mk-ink"},
77
+ "color": {"token": "--mk-paper"}}},
78
+ "focus-visible": {"_": dict(FOCUS_RING)}}
79
+ return {"type": "loop",
80
+ "data": loop_data("ct-arch", "post", 3, paginationKey="notes"),
81
+ "children": [
82
+ {"type": "loop-items", "data": {"attrID": "ct-arch-items"},
83
+ "style": {"&": {"_": {"display": "flex", "flexDirection": "column"}}},
84
+ "children": [
85
+ {"type": "loop-item", "data": {"attrID": "ct-arch-item"},
86
+ "style": {"&": {"_": {
87
+ "display": "flex", "columnGap": "20px",
88
+ "alignItems": "baseline",
89
+ "paddingTop": "18px", "paddingBottom": "18px",
90
+ "customDeclarations":
91
+ "border-bottom:1px solid " + RULE + ";"}},
92
+ "hover": {"_": {"backgroundColor": {"token": "--mk-panel"}}}},
93
+ "children": [
94
+ var("ct-arch-date",
95
+ "@date('Y.m.d', @VAR('post/publish_timestamp'))",
96
+ "p", fontFamily=MONO, fontSize="10px",
97
+ letterSpacing="0.14em",
98
+ color={"token": "--mk-faint"},
99
+ width="88px", flexSizing={"type": "none"},
100
+ _t={"fontSize": "12px"}),
101
+ var("ct-arch-title", "@VAR('post/title')", "p",
102
+ fontFamily=CJK, fontSize="15px", lineHeight="1.6",
103
+ color={"token": "--mk-ink"},
104
+ flexSizing={"type": "custom",
105
+ "customOptions": {"flexGrow": "1",
106
+ "flexShrink": "1"}},
107
+ minWidth="0px"),
108
+ ]},
109
+ ]},
110
+ {"type": "loop-no-result", "data": {"attrID": "ct-arch-none"},
111
+ "children": [T("p", "沒有文章。", fontFamily=CJK,
112
+ fontSize="14px", color={"token": "--mk-muted"})]},
113
+ {"type": "loop-pagination", "data": {"attrID": "ct-arch-pag"},
114
+ "style": {"&": {"_": {
115
+ "display": "flex", "columnGap": "8px", "alignItems": "center",
116
+ "marginTop": "22px", "flexWrap": "wrap", "rowGap": "8px"}}},
117
+ "children": [
118
+ {"type": "loop-pagination-button-previous",
119
+ "data": {"attrID": "ct-arch-prev"}, "style": btn,
120
+ "children": [{"type": "wysiwyg-text",
121
+ "data": {"text": "← PREV"}}]},
122
+ {"type": "loop-pagination-numbers",
123
+ "data": {"attrID": "ct-arch-nums",
124
+ "pagesBefore": "2", "pagesAfter": "2"},
125
+ "style": {"&": {"_": {"display": "flex", "columnGap": "6px"}}},
126
+ "children": [
127
+ # ONE number. Mosaic repeats it per page, the same way a
128
+ # slider repeats one bullet.
129
+ {"type": "loop-pagination-number",
130
+ "data": {"attrID": "ct-arch-num"},
131
+ "style": {"&": {"_": {
132
+ "fontFamily": MONO, "fontSize": "11px",
133
+ "cursor": "pointer", "minWidth": "34px",
134
+ "color": {"token": "--mk-ink"},
135
+ "paddingTop": "9px", "paddingBottom": "9px",
136
+ "paddingLeft": "10px", "paddingRight": "10px",
137
+ "customDeclarations":
138
+ "border:1px solid " + RULE + ";text-align:center;"}},
139
+ "hover": {"_": {
140
+ "customDeclarations":
141
+ "border:1px solid rgb(22,24,28);"
142
+ "text-align:center;"}},
143
+ "focus-visible": {"_": dict(FOCUS_RING)}}},
144
+ ]},
145
+ {"type": "loop-pagination-button-next",
146
+ "data": {"attrID": "ct-arch-next"}, "style": btn,
147
+ "children": [{"type": "wysiwyg-text",
148
+ "data": {"text": "NEXT →"}}]},
149
+ ]},
150
+ ]}
151
+
152
+
153
+ # ---------------------------------------------------------------- 2. slider
154
+
155
+ def post_slider():
156
+ """One authored slide, repeated per post by `slider-loop-slides`."""
157
+ return {"type": "slider",
158
+ "data": {"attrID": "ct-slider", "animation": "slide",
159
+ "duration": "650", "easing": "ease", "isAutoplay": "1",
160
+ "autoplayDelay": "3200", "autoplayLimit": "0",
161
+ "isCarousel": "1", "defaultSlideIndex": "0",
162
+ "ariaLabel": "文章輪播"},
163
+ "style": bp({"position": "relative", "height": "300px",
164
+ "overflow": "hidden", "backgroundColor": INK},
165
+ None, {"height": "280px"}),
166
+ "children": [
167
+ {"type": "slider-slides", "data": {"attrID": "ct-slider-track"},
168
+ "children": [
169
+ {"type": "slider-loop-slides",
170
+ "data": loop_data("ct-slider-loop", "s", 6),
171
+ "children": [
172
+ {"type": "slider-slide",
173
+ "data": {"attrID": "ct-slide",
174
+ "title": {"v": "@VAR('s/title')"}},
175
+ "style": bp({"backgroundColor": INK, "display": "flex",
176
+ "flexDirection": "column",
177
+ "justifyContent": "flex-start",
178
+ "alignItems": "flex-start",
179
+ "height": "100%",
180
+ "paddingTop": "34px", "paddingBottom": "40px",
181
+ "paddingLeft": "36px", "paddingRight": "36px"},
182
+ None,
183
+ {"paddingTop": "22px", "paddingBottom": "28px",
184
+ "paddingLeft": "20px", "paddingRight": "20px"}),
185
+ "children": [
186
+ var("ct-slide-meta",
187
+ "@concat('#', @VAR('s/id'), ' · ', "
188
+ "@date('Y.m.d', @VAR('s/publish_timestamp')))",
189
+ "p", fontFamily=MONO, fontSize="10px",
190
+ letterSpacing="0.2em", color="rgba(250,250,247,.6)",
191
+ _t={"fontSize": "12px"}),
192
+ var("ct-slide-title", "@VAR('s/title')", "p",
193
+ fontFamily=DISPLAY, fontWeight="600",
194
+ fontSize="34px", lineHeight="1.18",
195
+ color=PAPER, marginTop="18px", maxWidth="700px",
196
+ _m={"fontSize": "24px"}),
197
+ var("ct-slide-ex",
198
+ "@excerpt(@remove_html(@VAR('s/content')), 54)",
199
+ "p", fontFamily=CJK, fontSize="13px",
200
+ lineHeight="1.85", color="rgba(250,250,247,.78)",
201
+ marginTop="14px", maxWidth="520px"),
202
+ ]},
203
+ ]},
204
+ ]},
205
+ ]}
206
+
207
+
208
+ # ---------------------------------------------------------------- 3. tabs
209
+
210
+ def post_tabs():
211
+ tab_style = {"&": {"_": {
212
+ "cursor": "pointer", "paddingTop": "13px", "paddingBottom": "13px",
213
+ "paddingLeft": "16px", "paddingRight": "16px",
214
+ "fontFamily": CJK, "fontSize": "13px",
215
+ "color": {"token": "--mk-ink"},
216
+ "customDeclarations": "border:1px solid " + RULE + ";border-bottom:0;"}},
217
+ "hover": {"_": {"backgroundColor": {"token": "--mk-panel"}}},
218
+ "___tab--active": {"_": {
219
+ "backgroundColor": {"token": "--mk-ink"},
220
+ "color": "rgb(250,250,247)",
221
+ "customDeclarations": "border:1px solid rgb(22,24,28);border-bottom:0;"}},
222
+ "focus-visible": {"_": dict(FOCUS_RING)}}
223
+ return {"type": "tabs", "data": {"attrID": "ct-tabs"},
224
+ "children": [
225
+ {"type": "tabs-menu", "data": {"attrID": "ct-tabs-menu"},
226
+ "style": bp({"display": "flex", "flexWrap": "wrap"},
227
+ None, {"flexDirection": "column"}),
228
+ "children": [
229
+ {"type": "tabs-loop-tabs",
230
+ "data": loop_data("ct-tabs-loop", "t", 4),
231
+ "children": [
232
+ {"type": "tabs-tab", "data": {"attrID": "ct-tab"},
233
+ "style": tab_style,
234
+ "children": [
235
+ # @substr counts BYTES, not characters: 14 cut a
236
+ # Han character in half and the tab read
237
+ # "離站攔不" + a replacement glyph. 18 is six
238
+ # three-byte characters exactly.
239
+ var("ct-tab-t",
240
+ "@substr(@VAR('t/title'), 0, 18)", "span"),
241
+ ]},
242
+ ]},
243
+ ]},
244
+ {"type": "tabs-content", "data": {"attrID": "ct-tabs-content"},
245
+ "style": bp({"paddingTop": "26px", "paddingBottom": "28px",
246
+ "paddingLeft": "26px", "paddingRight": "26px",
247
+ "backgroundColor": {"token": "--mk-panel"},
248
+ "customDeclarations": "border:1px solid " + RULE + ";"},
249
+ None, {"paddingLeft": "20px", "paddingRight": "20px"}),
250
+ "children": [
251
+ {"type": "tabs-loop-tab-panes",
252
+ "data": loop_data("ct-panes-loop", "t", 4),
253
+ "children": [
254
+ {"type": "tabs-tab-pane", "data": {"attrID": "ct-pane"},
255
+ "children": [
256
+ var("ct-pane-title", "@VAR('t/title')", "p",
257
+ fontFamily=DISPLAY, fontWeight="600",
258
+ fontSize="20px", lineHeight="1.4",
259
+ color={"token": "--mk-ink"}),
260
+ var("ct-pane-body",
261
+ "@remove_html(@VAR('t/content'))", "p",
262
+ fontFamily=CJK, fontSize="14px",
263
+ lineHeight="1.9", color={"token": "--mk-muted"},
264
+ marginTop="12px", maxWidth="640px"),
265
+ ]},
266
+ ]},
267
+ ]},
268
+ ]}
269
+
270
+
271
+ # ---------------------------------------------------------------- 4. list
272
+
273
+ def post_list():
274
+ return {"type": "list", "data": {"attrID": "ct-list", "tagName": "ol"},
275
+ "style": {"&": {"_": {
276
+ "display": "flex", "flexDirection": "column", "rowGap": "0px"}}},
277
+ "children": [
278
+ {"type": "list-loop-items",
279
+ "data": loop_data("ct-list-loop", "l", 7),
280
+ "children": [
281
+ {"type": "list-item", "data": {"attrID": "ct-li"},
282
+ "style": {"&": {"_": {
283
+ "paddingTop": "11px", "paddingBottom": "11px",
284
+ "customDeclarations":
285
+ "border-bottom:1px dotted " + RULE + ";"}}},
286
+ "children": [
287
+ var("ct-li-t", "@VAR('l/title')", "span",
288
+ fontFamily=CJK, fontSize="14px",
289
+ color={"token": "--mk-ink"}),
290
+ ]},
291
+ ]},
292
+ ]}
293
+
294
+
295
+ # ---------------------------------------------------------------- the page
296
+
297
+ def band(attr, label, types, body, bg="--mk-paper"):
298
+ return section(attr, [wrap(attr + "-w", [
299
+ box(attr + "-pad", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
300
+ strip(label, types),
301
+ body,
302
+ box(attr + "-foot", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
303
+ ])], bg=bg, pad_y="0px")
304
+
305
+
306
+ HEAD = section("ct-head", [wrap("ct-head-w", [
307
+ box("ct-head-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
308
+ mono("CONTENT-DRIVEN / 內容驅動", size="11px",
309
+ color="--mk-faint", track="0.26em"),
310
+ ml("h1", "寫一次,\n資料負責複製。",
311
+ fontFamily=DISPLAY, fontWeight="600", fontSize="58px", lineHeight="1.06",
312
+ letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
313
+ _t={"fontSize": "46px"}, _m={"fontSize": "32px"}),
314
+ T("p",
315
+ "下面的輪播、分頁、清單跟存檔,"
316
+ "都只寫了一個範本——投影片一張、"
317
+ "分頁一個、列一行——其餘是文章資料"
318
+ "複製出來的。發一篇新文章,"
319
+ "這四塊都會跡著多一個。",
320
+ fontFamily=CJK, fontSize="16px", lineHeight="1.95",
321
+ color={"token": "--mk-ink"}, marginTop="20px", maxWidth="650px"),
322
+ box("ct-head-gap", {"paddingTop": "20px"}, []),
323
+ ])], pad_y="0px")
324
+
325
+ TREE = {"type": "div", "data": {"attrID": "ct-page"},
326
+ "children": [
327
+ HEAD,
328
+ band("ct-slider-s", "SLIDER / 文章輪播",
329
+ "slider-slides › slider-loop-slides › slider-slide",
330
+ post_slider(), bg="--mk-paper"),
331
+ band("ct-tabs-s", "TABS / 文章分頁",
332
+ "tabs-menu › tabs-loop-tabs / tabs-content › tabs-loop-tab-panes",
333
+ post_tabs(), bg="--mk-panel"),
334
+ band("ct-arch-s", "ARCHIVE / 分頁存檔",
335
+ "loop › loop-items + loop-pagination › "
336
+ "button-previous / numbers › number / button-next",
337
+ archive(), bg="--mk-paper"),
338
+ band("ct-list-s", "LIST / 清單",
339
+ "list › list-loop-items › list-item",
340
+ post_list(), bg="--mk-panel"),
341
+ ]}
@@ -0,0 +1,225 @@
1
+ """The conversion lab: what survives a move from Elementor, counted.
2
+
3
+ A migration pitch is usually made with adjectives. This page is made with the
4
+ batch's own numbers, read at build time out of `data/conversion-batch.csv` and
5
+ `data/conversion-verification.csv`, so it cannot overstate the result: if the next
6
+ run converts less, this page says so without anyone editing it.
7
+
8
+ The honest framing matters more than the figures. A converter carries CONTENT and
9
+ LAYOUT INTENT. It does not carry a pixel-identical rendering, and anything claiming
10
+ otherwise is selling you a screenshot. What is checked is therefore not "does it
11
+ look the same" but "is everything still here": every source string, every image,
12
+ every link target, every heading level, found on the delivered Mosaic page.
13
+
14
+ Nothing here names a client or links to one of their pages. The batch table holds
15
+ post IDs only, which is the level a demonstration needs.
16
+ """
17
+ import csv
18
+ import io
19
+ import os
20
+ import re
21
+
22
+ from _moksa import (CJK, DISPLAY, MONO, RULE, T, bp, box, grid, ml, mono,
23
+ section, wrap)
24
+
25
+ _HERE = os.path.dirname(os.path.abspath(__file__))
26
+
27
+
28
+ def _table(name):
29
+ with io.open(os.path.join(_HERE, "..", "data", name), encoding="utf-8") as fh:
30
+ return list(csv.DictReader(fh))
31
+
32
+
33
+ BATCH = _table("conversion-batch.csv")
34
+
35
+
36
+ def _sum(col):
37
+ return sum(int(r[col]) for r in BATCH)
38
+
39
+
40
+ def _ratio(col):
41
+ got = want = 0
42
+ for r in BATCH:
43
+ m = re.match(r"(\d+) of (\d+)", r[col])
44
+ if m:
45
+ got += int(m.group(1))
46
+ want += int(m.group(2))
47
+ return got, want
48
+
49
+
50
+ PAGES = len(BATCH)
51
+ CONVERTED = _sum("converted")
52
+ SKIPPED = _sum("skipped")
53
+ TEXT = _ratio("text")
54
+ IMAGES = _ratio("images")
55
+ LINKS = _ratio("links")
56
+ ALL_PASS = all(r["result"] == "PASS" for r in BATCH)
57
+
58
+
59
+ def widgets():
60
+ """The handled widget types, read from the converter rather than retyped."""
61
+ src = io.open(os.path.join(_HERE, "..", "tools", "from_elementor.py"),
62
+ encoding="utf-8").read()
63
+ names = []
64
+ for group in re.findall(r"@handler\(([^)]*)\)", src):
65
+ for n in re.findall(r"[\"']([a-z0-9_-]+)[\"']", group):
66
+ if n not in names:
67
+ names.append(n)
68
+ return sorted(names)
69
+
70
+
71
+ WIDGETS = widgets()
72
+
73
+
74
+ def figure(key, value, caption):
75
+ return box("cv-fig-" + key, {
76
+ "display": "flex", "flexDirection": "column", "rowGap": "0px",
77
+ "paddingTop": "20px", "paddingBottom": "18px",
78
+ "customDeclarations": "border-top:1px solid rgb(22,24,28);"}, [
79
+ mono(key.upper(), size="10px", color="--mk-faint", track="0.24em"),
80
+ ml("p", value, fontFamily=DISPLAY, fontWeight="600", fontSize="44px",
81
+ lineHeight="1.02", letterSpacing="0.005em",
82
+ color={"token": "--mk-ink"}, marginTop="10px",
83
+ _m={"fontSize": "32px"}),
84
+ T("p", caption, fontFamily=CJK, fontSize="12px", lineHeight="1.75",
85
+ color={"token": "--mk-muted"}, marginTop="8px"),
86
+ ])
87
+
88
+
89
+ HEAD = section("cv-head", [wrap("cv-head-w", [
90
+ box("cv-head-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
91
+ mono("CONVERSION / Elementor 轉換", size="11px",
92
+ color="--mk-faint", track="0.26em"),
93
+ ml("h1", "搬過來的時候,\n什麼會跟著來?",
94
+ fontFamily=DISPLAY, fontWeight="600", fontSize="56px", lineHeight="1.07",
95
+ letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
96
+ _t={"fontSize": "44px"}, _m={"fontSize": "31px"}),
97
+ T("p",
98
+ "搜屋的說法通常是形容詞。"
99
+ "這一頁用的是批次本身的數字——"
100
+ "建置時從轉換紀錄表裡讀出來的,"
101
+ "所以下一次轉得比較差,這頁會自己說出來。",
102
+ fontFamily=CJK, fontSize="16px", lineHeight="1.95",
103
+ color={"token": "--mk-ink"}, marginTop="22px", maxWidth="640px"),
104
+ T("p",
105
+ "先把話說清楚:轉換器搬的是"
106
+ "「內容」和「版面意圖」,"
107
+ "不是像素級的一模一樣。"
108
+ "所以檢查的不是「看起來像不像」,"
109
+ "而是「東西是不是都還在」:"
110
+ "每一段文字、每一張圖、每一個連結目標、"
111
+ "每一級標題,都在交付的 Mosaic 頁面上找一遍。",
112
+ fontFamily=CJK, fontSize="14px", lineHeight="1.9",
113
+ color={"token": "--mk-muted"}, marginTop="14px", maxWidth="640px"),
114
+ box("cv-head-gap", {"paddingTop": "40px"}, []),
115
+ grid("cv-figs", 4, "20px", [
116
+ figure("pages", str(PAGES), "個頁面一批轉完,"
117
+ "每一頁都啟過檢查"),
118
+ figure("elements", "{:,}".format(CONVERTED),
119
+ "個 Elementor 元素轉成 Mosaic 節點"),
120
+ figure("text", "%d/%d" % TEXT,
121
+ "段原始文字在轉換後的頁面上找得到"),
122
+ figure("links", "%d/%d" % LINKS,
123
+ "個連結目標保住"),
124
+ ], tcols=2, mcols=1),
125
+ box("cv-head-foot", {"paddingTop": "56px"}, []),
126
+ ])], pad_y="0px")
127
+
128
+
129
+ SUPPORTED = section("cv-sup", [wrap("cv-sup-w", [
130
+ box("cv-sup-pad", {"paddingTop": "60px"}, []),
131
+ mono("WHAT CONVERTS", size="10px", color="--mk-faint", track="0.26em"),
132
+ ml("h2", "%d 種 widget,其他的會點名。" % len(WIDGETS),
133
+ fontFamily=DISPLAY, fontWeight="600", fontSize="32px", lineHeight="1.15",
134
+ letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
135
+ _m={"fontSize": "23px"}),
136
+ T("p",
137
+ "這份清單是建置時從轉換器本身讀出來的。"
138
+ "沒在清單上的元素不會被默默丟掉——"
139
+ "它會連同 id、類型和「為什麼沒轉」一起被報告出來,"
140
+ "--strict 還能讓整次轉換在有损失時直接拒絕輸出。"
141
+ "這批 %d 頁裡只有 %d 個元素沒轉。"
142
+ % (PAGES, SKIPPED),
143
+ fontFamily=CJK, fontSize="14px", lineHeight="1.9",
144
+ color={"token": "--mk-muted"}, marginTop="12px", maxWidth="660px"),
145
+ box("cv-sup-list", {
146
+ "marginTop": "22px", "display": "flex", "flexWrap": "wrap",
147
+ "columnGap": "8px", "rowGap": "8px"},
148
+ [box("cv-w-%d" % i, {
149
+ "paddingTop": "7px", "paddingBottom": "7px",
150
+ "paddingLeft": "11px", "paddingRight": "11px",
151
+ "customDeclarations": "border:1px solid " + RULE + ";"},
152
+ [mono(w, size="10px", color="--mk-ink", track="0.05em")])
153
+ for i, w in enumerate(WIDGETS)]),
154
+ box("cv-sup-foot", {"paddingTop": "62px"}, []),
155
+ ])], bg="--mk-panel", pad_y="0px")
156
+
157
+
158
+ LESSON = section("cv-lesson", [wrap("cv-lesson-w", [
159
+ box("cv-lesson-pad", {"paddingTop": "60px"}, []),
160
+ mono("THE ONE THAT BIT", size="10px", color="--mk-faint", track="0.26em"),
161
+ ml("h2", "一個 px 寬度,是「桌機意圖」,"
162
+ "不是寬度。",
163
+ fontFamily=DISPLAY, fontWeight="600", fontSize="30px", lineHeight="1.18",
164
+ letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="14px",
165
+ _m={"fontSize": "22px"}),
166
+ T("p",
167
+ "第一批轉完的頁面在手機上會橫向溢出,"
168
+ "而每一頁的內容檢查都是過的。"
169
+ "原因不在轉換器搜漏了什麼,"
170
+ "而在於 Elementor 自己的樣式表給每一個 widget "
171
+ "都加了 max-width:100%。"
172
+ "所以在 Elementor 裡寫死的 px 寬度,"
173
+ "實際上是「桌機上要這麼寬」的意圖,"
174
+ "並不是「所有尺寸都要這麼寬」。",
175
+ fontFamily=CJK, fontSize="15px", lineHeight="1.95",
176
+ color={"token": "--mk-ink"}, marginTop="14px", maxWidth="680px"),
177
+ T("p",
178
+ "轉換器現在會替每一個帶 px 寬度的元素"
179
+ "補上 max-width:100%。"
180
+ "這是一個值得記的通則:"
181
+ "搬家的時候,來源工具「沒寫出來的那些預設」"
182
+ "跟它寫出來的一樣重要。",
183
+ fontFamily=CJK, fontSize="14px", lineHeight="1.9",
184
+ color={"token": "--mk-muted"}, marginTop="14px", maxWidth="680px"),
185
+ box("cv-lesson-foot", {"paddingTop": "64px"}, []),
186
+ ])], pad_y="0px")
187
+
188
+
189
+ def point(num, title, body):
190
+ return box("cv-pt-%d" % num, {}, [
191
+ mono("%02d" % num, size="10px", color="--mk-accent-ink", track="0.22em"),
192
+ ml("h3", title, fontFamily=DISPLAY, fontWeight="600", fontSize="17px",
193
+ lineHeight="1.3", color={"token": "--mk-ink"}, marginTop="10px"),
194
+ T("p", body, fontFamily=CJK, fontSize="13px", lineHeight="1.85",
195
+ color={"token": "--mk-muted"}, marginTop="8px"),
196
+ ])
197
+
198
+
199
+ HOW = section("cv-how", [wrap("cv-how-w", [
200
+ box("cv-how-inner", {
201
+ "paddingTop": "54px", "paddingBottom": "84px",
202
+ "customDeclarations": "border-top:1px solid " + RULE + ";"}, [
203
+ mono("怎麼驗收", size="10px", color="--mk-faint", track="0.26em"),
204
+ box("cv-how-gap", {"paddingTop": "20px"}, []),
205
+ grid("cv-how-grid", 3, "22px", [
206
+ point(1, "比對的是來源,不是截圖",
207
+ "檢查工具把 Elementor 原始資料裡的"
208
+ "每一段字串、每一張圖、每一個連結目標"
209
+ "、每一級標題拿出來,到交付的頁面上找。"
210
+ "找不到就是 FAIL。"),
211
+ point(2, "沒轉的會有名字和理由",
212
+ "不在支援清單上的 widget 會轉成一筆記錄:"
213
+ "id、類型、以及為什麼沒轉。"
214
+ "想要宁可不轉也不要有損失,加 --strict。"),
215
+ point(3, "寬度檢查是分開的一關",
216
+ "內容都在,不代表版面是對的。"
217
+ "所以每一頁還要在 390 / 768 / 1280 三個寬度"
218
+ "各開一次,問同一個問題:"
219
+ "這頁會不會橫向捲動。"),
220
+ ], tcols=1, mcols=1),
221
+ ]),
222
+ ])], pad_y="0px")
223
+
224
+ TREE = {"type": "div", "data": {"attrID": "cv-page"},
225
+ "children": [HEAD, SUPPORTED, LESSON, HOW]}