mosaic-headless 1.2.0 → 1.3.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.
Files changed (49) hide show
  1. package/README.ja.md +19 -4
  2. package/README.ko.md +111 -0
  3. package/README.md +30 -5
  4. package/README.zh-TW.md +17 -4
  5. package/SKILL.md +79 -7
  6. package/assets/templates/platforms/claude-ai.json +1 -1
  7. package/assets/templates/platforms/claude-code.json +1 -1
  8. package/assets/templates/platforms/codex-cli.json +1 -1
  9. package/assets/templates/platforms/copilot.json +1 -1
  10. package/assets/templates/platforms/gemini-cli.json +1 -1
  11. package/bin/check-release.mjs +29 -2
  12. package/data/animatable-properties.csv +23 -23
  13. package/data/browser-verification.csv +2238 -0
  14. package/data/condition-comparators.csv +13 -13
  15. package/data/condition-subjects.csv +60 -60
  16. package/data/data-class-hierarchy.csv +122 -0
  17. package/data/db-columns.csv +207 -207
  18. package/data/default-children.csv +11 -11
  19. package/data/design-audit.csv +1 -0
  20. package/data/dynamic-variables.csv +75 -75
  21. package/data/element-classes.csv +152 -152
  22. package/data/evaluator-functions.csv +20 -20
  23. package/data/interaction-types.csv +13 -13
  24. package/data/node-properties.csv +182 -182
  25. package/data/node-property-verification.csv +182 -182
  26. package/data/node-types.csv +123 -123
  27. package/data/node-verification.csv +123 -123
  28. package/data/placement-rules.csv +123 -123
  29. package/data/pluggables.csv +208 -208
  30. package/data/property-verification.csv +171 -171
  31. package/data/rest-routes.csv +115 -115
  32. package/data/rwd-verification.csv +577 -570
  33. package/data/style-properties.csv +99 -99
  34. package/data/style-states.csv +54 -54
  35. package/data/style-value-shapes.csv +23 -23
  36. package/data/style-verification.csv +99 -99
  37. package/package.json +59 -59
  38. package/references/failure-modes.md +74 -0
  39. package/sites/_moksa.py +106 -44
  40. package/sites/moksa.json +9075 -8684
  41. package/tools/build_page.py +34 -1
  42. package/tools/extract_node_types.py +26 -1
  43. package/tools/mo.py +686 -0
  44. package/tools/sweep_node_properties.py +41 -2
  45. package/tools/verify_browser.py +589 -0
  46. package/tools/verify_rwd.py +14 -1
  47. package/README.zh-CN.md +0 -87
  48. package/tools/build_all.py +0 -55
  49. package/tools/build_report.py +0 -221
@@ -53,6 +53,7 @@ import json
53
53
  import os
54
54
  import re
55
55
  import sys
56
+ import time
56
57
  import urllib.request
57
58
 
58
59
  BREAKPOINTS = {"_t": ("t", "max-width: 1079px"), "_m": ("m", "max-width: 767px")}
@@ -123,8 +124,20 @@ def base_customstyles(node, out):
123
124
 
124
125
 
125
126
  def fetch(url):
127
+ """Fetch the page the way a browser would - but never the cached copy.
128
+
129
+ A full-page cache in front of WordPress (Varnish on Cloudways, any CDN) will
130
+ happily serve the pre-commit HTML seconds after a successful commit, and every
131
+ declaration on a node you have just added then reports `no-element`. That is a
132
+ false negative from the verifier's own transport, which is the one kind of result
133
+ this tool must not produce: a unique query string makes the request a cache miss
134
+ and the assertion is made against what was actually written.
135
+ """
136
+ sep = "&" if "?" in url else "?"
137
+ url = "%s%s_v=%d" % (url, sep, int(time.time() * 1000))
126
138
  req = urllib.request.Request(url, headers={
127
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0 Safari/537.36"})
139
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0 Safari/537.36",
140
+ "Cache-Control": "no-cache", "Pragma": "no-cache"})
128
141
  with urllib.request.urlopen(req, timeout=60) as r:
129
142
  return r.read().decode("utf-8", "replace")
130
143
 
package/README.zh-CN.md DELETED
@@ -1,87 +0,0 @@
1
- # mosaic-headless
2
-
3
- 直接写入数据模型来构建和修改 [Mosaic Pro](https://mosaicbuilder.com)(Nextend)网站——
4
- 不开可视化编辑器,不碰 DOM。
5
-
6
- *其他语言:[English](README.md) · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md)*
7
-
8
- ---
9
-
10
- Mosaic 把一个页面放在 **23 张自定义数据表**里,不在 `post_content`,也不在 `postmeta`。
11
- 一个元素一行,树状结构靠 `parentID` 字段,同层顺序是一个 fractional-index 字符串。
12
- 编辑器只是这个模型的一个客户端。它不是格式本身,你也不需要它。
13
-
14
- 这个 skill 就是那个模型的地图——**对着真实安装量出来的,不是从源码读出来的**。
15
-
16
- ## 唯一一条凌驾一切的规则
17
-
18
- **绝不凭印象写节点类型、属性名、枚举值、样式键或 Free/Pro 的判断。去 `data/` 里查。**
19
-
20
- 然后去看页面。Mosaic 有四种失效模式,**其中只有一种会改变 HTTP 状态码**:
21
-
22
- ```
23
- 校验器干净地拒绝 HTTP 200 + body 里一个 exceptions 数组
24
- commit 时 PHP fatal HTTP 500 (122 种类型里有 15 种,光放在 div 里就会)
25
- 结构无效的节点 HTTP 200、已写入、数据库有那一行,然后整个公开页面
26
- 变成一段 54 字节的错误字符串
27
- 值的「形状」错误 HTTP 200、已保存,而那条 CSS 规则就是不存在
28
- 该网址没有对应模板 HTTP 406,且对未登录者是空白 body
29
- ```
30
-
31
- **commit 成功不能当作页面正常的证据。**
32
-
33
- ## 验证了什么,怎么验的
34
-
35
- 全部跑在真实安装上——WordPress 7.1、WooCommerce 11.1、Mosaic Pro 1.0.7、**未授权**:
36
- 授权管的是主题库和更新,不是节点工厂,所以 Pro 类型照样注册、照样渲染。
37
-
38
- | 项目 | 结果 |
39
- |---|---|
40
- | **节点类型** | 122 / 122,一份文档一种类型:写入 → 渲染 → 断言 → 删除。70 RENDERED、30 COMMITTED、15 COMMIT_5xx、7 BROKE_PAGE |
41
- | **样式属性** | 98 / 98 写进真实页面并对照编译后的 CSS:58 COMPILED、18 ABSENT、21 SKIPPED |
42
- | **节点属性** | 181 / 181,用每个属性自己的 validator chain 推导出的值重测:35 APPLIED、42 NO_EFFECT、55 NO_HOST、47 SKIPPED |
43
- | **响应式** | 两个站共 569 条 `_t`/`_m` 声明,对照网站实际送出的样式表逐条断言——全部通过 |
44
- | **主题导出/导入** | 完整往返验证:导出整个主题、以副本身份导入,副本送出的页面逐字节相同 |
45
- | **线上量测** | 114 条 REST 路由、151 个 element class、59 个条件主体、23 张表 / 206 个字段 |
46
-
47
- `SKIPPED`、`NO_HOST`、`INCONCLUSIVE` 永远不并入通过率。
48
- **一个把自己的盲点算成成功的扫描工具,正是这个 skill 要反对的东西。**
49
-
50
- ### 动手之前值得先知道的两个结果
51
-
52
- **带有 `group` 的属性,单独设置一律无效。** 两个方向都精确:78 个无 group 的属性得到
53
- 58 COMPILED、0 ABSENT;20 个有 group 的全部 0 COMPILED。所以 `borderLeftWidth`、
54
- `outlineColor`、`gridColumnStart` 是**同一条规则的三个实例**,不是三个各自的怪毛病。
55
- 要用就用分组形状——`border` 收 `{width, style, color}`——或者退回 `customStyles`。
56
-
57
- **断点覆盖只能「改变」属性,永远不能「移除」。** 窄屏的 `customStyles` 如果只是
58
- 没写某条边框,宽屏那条边框会活下来,在塌成单列的版面正中间画一条线。
59
- **要把 `border-left:0` 明确写出来。**
60
-
61
- ## 工具
62
-
63
- ```bash
64
- wp eval-file tools/bootstrap_probe_theme.php # 免授权的测试主题
65
- python tools/build_site.py --config c.json --site sites/moksa.json
66
- python tools/verify_rwd.py --config c.json --site sites/moksa.json --csv rwd.csv
67
- python tools/copy_styles.py --config c.json --from a --to-prefix b- --only "&._m"
68
- wp eval-file tools/theme_export.php active > theme.json
69
- wp eval-file tools/theme_import.php theme.json "名称" rebind activate
70
- ```
71
-
72
- `sites/_moksa.py` 是完整示例:一个真实工作室的首页——报头、规格区块、服务、九行作品表格、
73
- 流程、技术栈、产品、客户评价、联系——**590 个节点全部通过数据表写入**,
74
- 还有一个用具名 view timeline 做的、会跟随滚动的条款索引,完全没有 JavaScript。
75
-
76
- ## 从哪里开始读
77
-
78
- 1. `references/data-model.md` —— 页面到底住在哪里。
79
- 2. `references/write-protocol.md` —— checkout / check / commit。
80
- 3. `references/failure-modes.md` —— Mosaic 怎么坏的,都是量出来的。**动笔前先读。**
81
- 4. `references/responsive.md` —— state / breakpoint / property 这个轴。
82
- 5. `references/styling.md` —— 一个样式值怎么变成 CSS。
83
- 6. `references/design-system.md` —— element class 与设计 token。
84
-
85
- ## 许可
86
-
87
- MIT。Mosaic Pro 本身是有授权的第三方软件,**不包含**在这个 repo 里。
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Rebuild every design page in designs/ against a freshly reset theme.
3
-
4
- python build_all.py --config sweep.json
5
-
6
- Each run resets the theme first, so the result is reproducible rather than accumulating
7
- a new master and template per invocation. Prints a per-page verdict: a page that
8
- commits but serves under MIN_HEALTHY_BYTES is reported as broken, because a 200
9
- response is not evidence that a Mosaic write worked.
10
- """
11
- import argparse
12
- import glob
13
- import json
14
- import os
15
- import sys
16
- import urllib.request
17
-
18
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
- from build_page import build # noqa: E402
20
- from sweep_node_types import MIN_HEALTHY_BYTES, Client # noqa: E402
21
-
22
- HERE = os.path.dirname(os.path.abspath(__file__))
23
- DESIGNS = os.path.join(HERE, "..", "designs")
24
-
25
-
26
- def slug_of(spec):
27
- return "probe-" + os.path.basename(spec["_path"])[:-5]
28
-
29
-
30
- if __name__ == "__main__":
31
- ap = argparse.ArgumentParser()
32
- ap.add_argument("--config", required=True)
33
- ap.add_argument("--only", help="comma-separated design names")
34
- a = ap.parse_args()
35
- cfg = json.load(open(a.config, encoding="utf-8"))
36
- client = Client(cfg)
37
-
38
- wanted = a.only.split(",") if a.only else None
39
- specs = []
40
- for path in sorted(glob.glob(os.path.join(DESIGNS, "*.json"))):
41
- name = os.path.basename(path)[:-5]
42
- if name.startswith("_") or name == "smoke" or (wanted and name not in wanted):
43
- continue
44
- spec = json.load(open(path, encoding="utf-8"))
45
- spec["_path"] = path
46
- specs.append(spec)
47
-
48
- for spec in specs:
49
- master_id, count = build(client, cfg, spec, force=False)
50
- url = "%s/%s/" % (cfg["base"].rstrip("/"), slug_of(spec))
51
- with urllib.request.urlopen(url, timeout=90) as r:
52
- body = r.read()
53
- ok = len(body) >= MIN_HEALTHY_BYTES
54
- print(" %-14s %-7s %6d bytes %d nodes %s" %
55
- (os.path.basename(spec["_path"])[:-5], "OK" if ok else "BROKEN", len(body), count, url))
@@ -1,221 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Assemble the Mosaic verification report as a single self-contained HTML page."""
3
- import io
4
- import json
5
- import os
6
-
7
- ROOT = r"C:/Users/yun19/OneDrive/桌面/mosaic"
8
- SHOTS = json.load(io.open(os.path.join(ROOT, "shots/web/_embed.json"), encoding="utf-8"))
9
- BASE = "https://woocommerce-1469814-6656199.cloudwaysapps.com"
10
-
11
- PAGES = [
12
- dict(slug="brutalist", shot="01-brutalist", name="Brutalist Grid", nodes=21, bytes=31366,
13
- palette=["#f5f3ee", "#101010"],
14
- style="紙白底、墨黑字,唯一字族是 Courier New 等寬體。84px 標題 letter-spacing -4px 壓到幾乎相黏,line-height 0.88 讓兩行咬在一起。三張卡片用 3px 實心黑框直接相鄰、gap 為 0,邊框自己變成分隔線——沒有圓角、沒有陰影、沒有第三個顏色。",
15
- motion="hover 時整格背景由紙白翻成墨黑(<code>background-color</code>,200ms ease)。只有一個動畫,因為這個風格的主張就是「除了結構什麼都不要」。",
16
- components="<code>section</code> ×1、<code>div</code> ×4、<code>text</code> ×8(h1/p/h3)、<code>wysiwyg-text</code> ×8",
17
- props="20 個樣式屬性;<code>borderStyle</code> 物件(12 個 border 長寫)、<code>textTransform</code>、<code>letterSpacing</code>、<code>fontFamily</code>"),
18
- dict(slug="glass", shot="02-glass", name="Glassmorphism", nodes=21, bytes=31221,
19
- palette=["#2b1055", "#7597de", "#f2a1c4"],
20
- style="140° 三段線性漸層(紫→藍→粉)鋪滿整個 viewport,卡片是 14% 白 + <code>backdrop-filter: blur(16px)</code> 的霧面板,配 1px 半透明白邊界當邊緣光。深度來自模糊,不是陰影。標題用 300 字重的細體置中,跟厚重的漸層形成反差。",
21
- motion="hover:卡片上浮 10px(<code>translateY</code>),同時背景不透明度 0.14→0.24、模糊 16px→22px、邊界亮度提高,320ms ease 一起過渡。折射感是靠模糊半徑變化做出來的,不是靠位移。",
22
- components="<code>section</code> ×1、<code>div</code> ×4、<code>text</code> ×8、<code>wysiwyg-text</code> ×8",
23
- props="22 個;漸層與 backdrop-filter 走 <code>customStyles</code>,圓角走 <code>borderRadius</code> 結構化值"),
24
- dict(slug="editorial", shot="03-editorial", name="Editorial Serif", nodes=24, bytes=30758,
25
- palette=["#fcfbf7", "#1a1816", "#b03a2e"],
26
- style="雜誌內頁。Georgia 襯線體撐全場,72px 標題 400 字重(不是粗體)置中,副標用斜體。單一磚紅色只出現在 eyebrow 標籤和三個欄位小標,總共四次。三欄 46px 間距、17px 內文配 1.75 行高——是為了「讀」而不是「掃」而排的。",
27
- motion="沒有動畫。這是刻意的:編輯風格的權威感來自靜止與留白,加 hover 效果反而會削弱它。八頁裡有兩頁沒有任何動態,這也是設計決策的一部分。",
28
- components="<code>section</code> ×1、<code>div</code> ×5(含一條 1px 分隔線)、<code>text</code> ×9、<code>wysiwyg-text</code> ×9",
29
- props="23 個;<code>fontStyle: italic</code>、<code>letterSpacing</code> 3px 的全大寫標籤、<code>maxWidth</code> + auto margin 控制行長"),
30
- dict(slug="neobrutal", shot="04-neobrutal", name="Neo Brutalist", nodes=21, bytes=31865,
31
- palette=["#ffde59", "#ff6b6b", "#6ce0a8", "#96b0ff"],
32
- style="飽和到近乎刺眼的黃底,三張卡片分別是珊瑚紅、薄荷綠、藍紫。每張都有 3px 純黑外框、14px 圓角,以及 8px 位移、0 模糊的硬陰影——陰影是實心色塊而不是柔光,這是這個風格的簽名。78px 800 字重標題把版面壓住。",
33
- motion="hover:卡片上移 6px,同時硬陰影從 8px 推到 14px,180ms ease。位移和陰影同時變大,讓卡片看起來真的離開了頁面。這是 <code>boxShadow</code> 結構化陣列 + <code>transform</code> 陣列一起動的例子。",
34
- components="<code>section</code> ×1、<code>div</code> ×4、<code>text</code> ×8、<code>wysiwyg-text</code> ×8",
35
- props="22 個;<code>boxShadow</code>(type: outside)、<code>borderStyle</code>、<code>borderRadius</code>、<code>transform</code>、<code>transition</code> 五種結構化值全在這頁"),
36
- dict(slug="darkglow", shot="05-darkglow", name="Dark Glow", nodes=23, bytes=31114,
37
- palette=["#090b18", "#1b2a5e", "#6ee7ff"],
38
- style="1100×600 的徑向漸層從頂端中央往外散,深海軍藍收到近黑——一個漸層取代了整張 hero 圖。訊號色只有一個高彩度青色,用在 eyebrow 和三個卡片標題。卡片本身是 6%→2% 的白色線性漸層加 22% 藍紫邊界,在暗底上幾乎只看得到輪廓。",
39
- motion="hover:卡片上移 8px,同時長出 40px 模糊、-6px 內縮、55% 不透明度的藍色光暈,300ms ease。因為模糊半徑夠大而位移夠小,讀起來是「發光」不是「投影」。",
40
- components="<code>section</code> ×1、<code>div</code> ×4、<code>text</code> ×9、<code>wysiwyg-text</code> ×9",
41
- props="25 個;徑向漸層與卡片漸層走 <code>customStyles</code>,光暈走 <code>boxShadow</code> 結構化陣列"),
42
- dict(slug="swiss", shot="06-swiss", name="Swiss Grid", nodes=33, bytes=30403,
43
- palette=["#ffffff", "#0c0c0c", "#de1a1a"],
44
- style="純白、Helvetica、四欄 32px 溝槽。開頭一條 120×8px 的紅色實心橫槓,接著 90px 500 字重(中等,不是粗體)標題、letter-spacing -4px。四個欄位各自頂著一條 2px 黑色上緣線。紅色出現三次,全部是文字或那條槓,從不當背景。右側整欄留空是刻意的。",
45
- motion="沒有動畫。瑞士風格的秩序感建立在絕對靜止上——任何 hover 位移都會破壞網格的權威。",
46
- components="<code>section</code> ×1、<code>div</code> ×6(含紅槓與四個欄位)、<code>text</code> ×13、<code>wysiwyg-text</code> ×13",
47
- props="20 個;上緣線用 <code>customStyles: border-top</code>(因為只要單邊,用 borderStyle 物件要寫十二個鍵)"),
48
- dict(slug="motion", shot="07-motion", name="Motion Matrix", nodes=46, bytes=33615,
49
- palette=["#0e0e10", "#1c1c22", "#78b4ff"], shot2="07b-motion-hover-glow",
50
- style="近黑底上的 4×2 深灰磚陣列,這頁的目的就是把八種動畫並排放在同一個視覺條件下比較,所以靜態設計刻意壓到最低——同樣的圓角、同樣的內距、同樣的字級,只有動畫不同。",
51
- motion="八格各自動一種屬性:<b>Lift</b> translateY −14px、<b>Tilt</b> rotateZ 4deg、<b>Grow</b> scaleX 1.06、<b>Glow</b> 46px 模糊藍色光暈、<b>Skew</b> skewX 6deg、<b>Fade</b> opacity 1→0.45、<b>Round</b> 圓角 14px→40px、<b>Blur</b> filter blur 3px。全部走 <code>transition</code> 陣列,260–320ms ease。右側附圖是用真實指標 hover 在 Glow 格上截下來的。",
52
- components="<code>section</code> ×1、<code>div</code> ×9、<code>text</code> ×18、<code>wysiwyg-text</code> ×18",
53
- props="26 個;<code>transform</code> 四種型別(translateY / rotateZ / scaleX / skewX)、<code>boxShadow</code>、<code>opacity</code>、<code>borderRadius</code>、<code>filter</code>(走 customStyles)"),
54
- dict(slug="tokens", shot="09-tokens", name="Design Tokens", nodes=29, bytes=33740,
55
- palette=["#12141c", "#0d6c66", "#666e70", "#ffffff", "#f7f6f3"],
56
- style="這頁是唯一一個「照真實網站該有的方式」建的。整頁沒有任何寫死的顏色——五個 collection variable 編譯成 <code>:root</code> 上的 <code>--ink</code>/<code>--paper</code>/<code>--brand</code>/<code>--muted</code>/<code>--surface</code>,樹裡每一處顏色都是 <code>var()</code> 引用(全頁 16 次)。h1/h2/h3/段落的排版也不在節點上,而是提交給內建的 element class meta,所以是全站生效的規則。",
57
- motion="卡片 hover 上移 6px 並長出 28px 模糊陰影,220ms ease。動態部分不是動畫而是內容:標題與說明文字在渲染當下才讀取。",
58
- components="<code>section</code> ×1、<code>div</code> ×9(3 卡片 + 5 色票 + 網格)、<code>text</code> ×9、<code>wysiwyg-text</code> ×7、<code>wysiwyg-variable</code> ×2",
59
- props="theme 層:5 個 collectionVariable + 4 個 elementClass(Heading 1/2/3、Paragraph);節點層只剩排版與間距"),
60
- dict(slug="components", shot="08-components", name="Composite Components", nodes=58, bytes=35378,
61
- palette=["#f8f8fa", "#14141a"],
62
- style="安靜的淺灰底白卡,因為這頁的主體不是視覺而是結構——它測的是那些「巢狀寫錯就會把整頁弄死」的複合元件能不能正確組出來。三個區塊:手風琴、分頁、清單。",
63
- motion="手風琴 <code>animation: slide</code>、<code>openMode: single</code>、<code>firstOpened: 1</code>——展開收合由 Mosaic 自己的 Accordion.js 驅動,不是 CSS。分頁標籤 hover 有 180ms 背景過渡,active 分頁由 <code>___tab--active</code> 這個節點型別專屬 state 上色。",
64
- components="<code>accordion</code> ×1 →<code>accordion-item</code> ×3 →(<code>accordion-title</code> + <code>accordion-content</code>)、<code>tabs</code> ×1 →<code>tabs-menu</code>→<code>tabs-tab</code> ×2 / <code>tabs-content</code>→<code>tabs-tab-pane</code> ×2、<code>list</code>→<code>list-item</code> ×3",
65
- props="18 個;另用到 <code>openMode</code>、<code>animation</code>、<code>firstOpened</code>、<code>defaultTabIndex</code>、<code>listType</code> 五個 enum 節點屬性"),
66
- ]
67
-
68
- CARDS = "\n".join("""
69
- <article class="spec" id="{slug}">
70
- <div class="shot"><img src="{img}" alt="{name} 測試頁截圖" loading="lazy">{img2}</div>
71
- <div class="body">
72
- <header>
73
- <div class="swatches">{sw}</div>
74
- <h3>{name}</h3>
75
- <a class="live" href="{base}/probe-{slug}/" target="_blank" rel="noopener">probe-{slug} ↗</a>
76
- </header>
77
- <dl>
78
- <dt>設計風格</dt><dd>{style}</dd>
79
- <dt>動畫</dt><dd>{motion}</dd>
80
- <dt>元件結構</dt><dd class="mono-ish">{components}</dd>
81
- <dt>樣式屬性</dt><dd class="mono-ish">{props}</dd>
82
- </dl>
83
- <p class="verified"><span>{nodes}</span> 個節點提交 · 交付 <span>{bytes:,}</span> bytes · 瀏覽器實拍</p>
84
- </div>
85
- </article>""".format(
86
- img=SHOTS[p["shot"]], base=BASE,
87
- img2=('<img src="%s" alt="Glow 格 hover 狀態,藍色光暈已展開" loading="lazy">'
88
- '<figcaption>hover:Glow 格的 46px 模糊光暈</figcaption>' % SHOTS[p["shot2"]])
89
- if p.get("shot2") else "",
90
- sw="".join('<i style="background:%s"></i>' % c for c in p["palette"]),
91
- **p) for p in PAGES)
92
-
93
- HTML = """<title>Mosaic 無頭建站驗證</title>
94
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=JetBrains+Mono:wght@400;500&display=swap">
95
- <style>
96
- :root {
97
- --paper:#f5f6f4; --card:#ffffff; --ink:#14171a; --muted:#5c6668; --faint:#8a9294;
98
- --rule:#dfe3df; --accent:#0d6c66; --accent-soft:#e3efed; --warn:#a8442a;
99
- --display:'Archivo','Helvetica Neue',Arial,sans-serif;
100
- --body:'Newsreader',Georgia,'Times New Roman',serif;
101
- --mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace;
102
- --shadow:0 1px 2px rgba(20,23,26,.05), 0 8px 24px -12px rgba(20,23,26,.14);
103
- }
104
- @media (prefers-color-scheme: dark) {
105
- :root:not([data-theme="light"]) {
106
- --paper:#0f1213; --card:#171b1c; --ink:#e6ebe9; --muted:#9aa5a4; --faint:#6d7877;
107
- --rule:#262c2d; --accent:#4cc4b8; --accent-soft:#12302e; --warn:#e08a6c;
108
- --shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 28px -14px rgba(0,0,0,.7);
109
- }
110
- }
111
- :root[data-theme="dark"] {
112
- --paper:#0f1213; --card:#171b1c; --ink:#e6ebe9; --muted:#9aa5a4; --faint:#6d7877;
113
- --rule:#262c2d; --accent:#4cc4b8; --accent-soft:#12302e; --warn:#e08a6c;
114
- --shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 28px -14px rgba(0,0,0,.7);
115
- }
116
- * { box-sizing:border-box; }
117
- body { margin:0; background:var(--paper); color:var(--ink); font-family:var(--body);
118
- font-size:17px; line-height:1.7; -webkit-font-smoothing:antialiased; }
119
- .wrap { max-width:1080px; margin:0 auto; padding:56px 28px 96px; }
120
- h1,h2,h3 { font-family:var(--display); font-weight:600; letter-spacing:-.02em;
121
- text-wrap:balance; margin:0; }
122
- h1 { font-size:clamp(34px,5vw,52px); line-height:1.06; letter-spacing:-.035em; }
123
- .eyebrow { font-family:var(--mono); font-size:11.5px; letter-spacing:.18em;
124
- text-transform:uppercase; color:var(--accent); margin:0 0 14px; }
125
- .lede { font-size:19px; color:var(--muted); max-width:60ch; margin:18px 0 0; }
126
-
127
- .tally { display:grid; grid-template-columns:repeat(auto-fit,minmax(132px,1fr));
128
- gap:1px; background:var(--rule); border:1px solid var(--rule);
129
- border-radius:3px; margin:40px 0 8px; overflow:hidden; }
130
- .tally div { background:var(--card); padding:16px 18px; }
131
- .tally b { display:block; font-family:var(--display); font-size:27px; font-weight:700;
132
- letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
133
- .tally span { font-family:var(--mono); font-size:10.5px; letter-spacing:.1em;
134
- text-transform:uppercase; color:var(--faint); }
135
- .tally .bad b { color:var(--warn); }
136
- .tally-note { font-size:14.5px; color:var(--faint); margin:12px 0 0; }
137
-
138
- h2 { font-size:15px; letter-spacing:.14em; text-transform:uppercase;
139
- font-family:var(--mono); font-weight:500; color:var(--faint);
140
- margin:64px 0 22px; padding-bottom:10px; border-bottom:1px solid var(--rule); }
141
-
142
- .spec { display:grid; grid-template-columns:minmax(0,420px) minmax(0,1fr); gap:30px;
143
- background:var(--card); border:1px solid var(--rule); border-radius:4px;
144
- padding:22px; margin-bottom:20px; box-shadow:var(--shadow); }
145
- .shot { display:flex; flex-direction:column; gap:8px; }
146
- .shot img { display:block; width:100%; height:auto; border-radius:2px;
147
- border:1px solid var(--rule); }
148
- .shot figcaption { font-family:var(--mono); font-size:10.5px; color:var(--faint);
149
- letter-spacing:.06em; }
150
- .body header { display:flex; flex-wrap:wrap; align-items:baseline; gap:10px 14px;
151
- margin-bottom:14px; }
152
- .body h3 { font-size:23px; }
153
- .swatches { display:flex; gap:4px; width:100%; }
154
- .swatches i { width:26px; height:7px; border-radius:1px;
155
- box-shadow:inset 0 0 0 1px rgba(128,128,128,.28); }
156
- .live { font-family:var(--mono); font-size:12px; color:var(--accent);
157
- text-decoration:none; border-bottom:1px solid transparent; }
158
- .live:hover, .live:focus-visible { border-bottom-color:currentColor; }
159
- dl { margin:0; display:grid; grid-template-columns:auto minmax(0,1fr);
160
- gap:9px 18px; align-items:baseline; }
161
- dt { font-family:var(--mono); font-size:11px; letter-spacing:.1em;
162
- text-transform:uppercase; color:var(--faint); white-space:nowrap; }
163
- dd { margin:0; font-size:15.5px; line-height:1.62; }
164
- dd.mono-ish { font-size:14px; color:var(--muted); }
165
- code { font-family:var(--mono); font-size:.86em; background:var(--accent-soft);
166
- color:var(--accent); padding:1px 5px; border-radius:2px; }
167
- dd.mono-ish code { background:none; padding:0; color:var(--ink); }
168
- .verified { font-family:var(--mono); font-size:11.5px; color:var(--faint);
169
- margin:16px 0 0; padding-top:12px; border-top:1px solid var(--rule);
170
- font-variant-numeric:tabular-nums; }
171
- .verified span { color:var(--ink); font-weight:500; }
172
-
173
- .notes { margin-top:16px; }
174
- .notes li { margin-bottom:11px; color:var(--muted); font-size:16px; }
175
- .notes b { color:var(--ink); font-weight:500; }
176
- .foot { margin-top:60px; padding-top:20px; border-top:1px solid var(--rule);
177
- font-family:var(--mono); font-size:12px; color:var(--faint); line-height:1.9; }
178
- a { color:var(--accent); }
179
- @media (max-width:820px) { .spec { grid-template-columns:1fr; } .wrap { padding:36px 18px 64px; } }
180
- @media (prefers-reduced-motion:reduce) { * { transition:none !important; animation:none !important; } }
181
- </style>
182
-
183
- <div class="wrap">
184
- <p class="eyebrow">Mosaic Pro 1.0.7 · 無授權 · WP 7.1 / Woo 11.1</p>
185
- <h1>九個測試頁,九種設計語言</h1>
186
- <p class="lede">每一頁都只靠 <code>mosaic-headless</code> skill 的資料表建成——查表決定用哪個節點型別、什麼可以放進什麼、樣式值該寫成什麼形狀,然後走 REST 提交,最後在真實瀏覽器裡拍下來。建得出來的地方證明表是對的;建不出來的地方就是表的破洞,都列在最後。最後一頁是唯一按照「真實網站該有的方式」建的:設計 token、全域排版、渲染當下讀取的動態內容。</p>
187
-
188
- <div class="tally">
189
- <div><b>122</b><span>節點型別掃描</span></div>
190
- <div><b>70</b><span>渲染成功</span></div>
191
- <div class="bad"><b>22</b><span>放錯位置會炸</span></div>
192
- <div><b>98</b><span>樣式屬性</span></div>
193
- <div><b>53</b><span>樣式 state</span></div>
194
- <div><b>74</b><span>動態變數</span></div>
195
- <div><b>9</b><span>設計頁全數通過</span></div>
196
- </div>
197
- <p class="tally-note">節點掃描是逐型別單獨放上真站 → 提交 → 渲染 → 比對 attrID → 刪除,122 型跑滿。九頁共 276 個節點。</p>
198
-
199
- <h2>測試頁</h2>
200
- {cards}
201
-
202
- <h2>建構過程揭露的問題</h2>
203
- <ul class="notes">
204
- <li><b>網格靜默失效。</b><code>gridTemplateColumns</code> 接受 <code>"repeat(3, 1fr)"</code>、存進資料庫、然後編譯出<em>空</em>——三欄卡片全部塌成單欄。八頁都建完、第一張截圖出來才發現。這類「值的形狀錯了」不會有任何錯誤訊息。</li>
205
- <li><b>五種結構化值已破解。</b><code>borderRadius</code>、<code>transform</code>、<code>boxShadow</code>、<code>transition</code>、<code>borderStyle</code> 都不吃字串。<code>boxShadow</code> 的 <code>type</code> 必須是 <code>outside</code>/<code>inside</code>,寫 CSS 慣用的 <code>outset</code> 會靜默編譯成 <code>box-shadow:none</code>。</li>
206
- <li><b><code>canBeParentFor</code> 不是複合元件的權威。</b><code>accordion-item</code> 宣告只收 accordion-item,但它的 <code>getAccordionItemDefaultData()</code> 說要 title + content——而 title 正是 Accordion.js 綁點擊事件的元素。少了它,伺服器渲染乾淨無誤,瀏覽器 console 直接拋 <code>Cannot read properties of null</code>。</li>
207
- <li><b>Pro 型別不需要授權。</b><code>isLicenseActive: false</code> 但 48 個 Pro 型別照常註冊、照常渲染。授權擋的是主題庫和更新,不是 node factory。</li>
208
- <li><b>動態內容的語法猜不到。</b>裸識別字完全沒實作——<code>Evaluator</code> 對 Identifier 節點回傳的是字串 <code>"Identifiers are not used currently"</code>。所以 <code>@post.title</code> 存得下、渲染成文字 <code>@post.title</code>,看起來像你打錯字而不是功能壞了。正確寫法是函式呼叫 <code>@VAR('post/title')</code>,而且解析不到的名稱回空字串、不報錯。</li>
209
- <li><b>無效的巢狀結構會被靜默剪掉。</b>提交完整的 interaction 回 200、無 exception、正常 create 信封,存進去的卻只剩 <code>{type, name, uuid}</code>。在這個資料模型上,把 row 讀回來跟送出的做 diff,那個 diff 就是 Mosaic 從來不給你的錯誤訊息。</li>
210
- </ul>
211
-
212
- <p class="foot">
213
- 測試站 woocommerce-1469814-6656199.cloudwaysapps.com · 主題為腳本生成的免授權 fixture,可整體重置重建<br>
214
- 建構工具 tools/build_page.py + build_all.py · 設計規格 designs/*.json · 截圖 Playwright 1440×900 full-page
215
- </p>
216
- </div>
217
- """.replace("{cards}", CARDS)
218
-
219
- path = os.path.join(ROOT, "report.html")
220
- io.open(path, "w", encoding="utf-8").write(HTML)
221
- print("wrote %s (%d KB)" % (path, os.path.getsize(path)//1024))