hd-garden 0.14.2 → 0.14.5
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/.agents/skills/garden-compose/SKILL.md +14 -17
- package/.agents/skills/garden-compose/references/engine-gaps.md +8 -0
- package/.agents/skills/garden-compose/references/frames.md +6 -0
- package/.agents/skills/garden-compose/references/layout-recipes.md +53 -7
- package/.agents/skills/garden-compose/references/measured-connectors.js +94 -0
- package/.agents/skills/garden-compose/references/mobile-contract.md +23 -0
- package/.agents/skills/garden-compose/references/rendering-model.md +16 -2
- package/.agents/skills/garden-compose/references/visual-selection.md +23 -4
- package/.agents/skills/garden-compose/references/workflow.md +26 -0
- package/.agents/skills/garden-compose/scripts/lint-layout.mjs +87 -0
- package/app/components/PostReadingView.tsx +1 -1
- package/app/styles/a2.css +13 -1
- package/app/styles/shell.css +4 -4
- package/bin/garden.mjs +8 -2
- package/docs/handbook/components/compose.md +2 -0
- package/docs/handbook/en/components/compose.md +2 -0
- package/docs/handbook/en/reference/compose.md +7 -0
- package/docs/handbook/reference/compose.md +7 -0
- package/editors/dist/garden-typora-0.14.5.zip +0 -0
- package/editors/dist/typora/main.js +76 -77
- package/editors/dist/typora/manifest.json +7 -3
- package/editors/dist/typora/style.css +31 -18
- package/editors/dist/vscode/{garden-ui-0.14.2.vsix → garden-ui-0.14.5.vsix} +0 -0
- package/package.json +3 -2
- package/scripts/handbook-facts.mjs +5 -4
- package/scripts/pack-editors.mjs +8 -1
- package/scripts/rendered-site.mjs +130 -0
- package/scripts/sync-editor-version.mjs +5 -5
- package/templates/skills-en/garden-compose/SKILL.md +15 -17
- package/ui/compose/compose.css +6 -8
- package/ui/host.mjs +0 -1
- package/ui/preview/dist/garden-ui.css +31 -18
- package/ui/preview/dist/garden-ui.esm.js +77 -78
- package/ui/preview/dist/garden-ui.iife.js +77 -78
- package/ui/react/components/chart.tsx +2 -1
- package/ui/react/components/sandbox.tsx +3 -0
- package/ui/react/ui.css +12 -9
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: garden-compose
|
|
3
|
-
description: "
|
|
3
|
+
description: "将原文与一句效果或参考图,编译为 Garden Compose 内保真、可验证的说明布局;用于创作、微调及修复。"
|
|
4
4
|
---
|
|
5
5
|
# Garden Compose
|
|
6
|
+
只处理授权的文章/块;建议请求不写文件。不自动提交、发布、安装依赖或在文章任务中修改技能。
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## 短决策链:问题 → 约束 → 推荐路线 → 原文 → 验证
|
|
9
|
+
1. 作者只需原文+一句效果/参考图+可选不要什么。分开事实、效果与修改范围;保留措辞、顺序、完整段落和资源,不补摘要、数据、因果。普通 Markdown 足够就不加组件。
|
|
10
|
+
2. 选型必读 `references/visual-selection.md`。AI 内部先判断移动阅读任务(扫读/顺序/对照/空间关系),必读 `references/mobile-contract.md`,写桌面+窄屏约束卡,再取**唯一推荐路线**;不是限定合法组件的白名单。只在事实关系、隐藏内容、阅读顺序等重大取舍不明时询问;其余按已记录默认收敛,不让作者填技术表单。
|
|
11
|
+
3. 原文关键数值+标签/单位用`metric`;独立解释类别用`equal-cards`,不造数值。标题+徽标用`row align="center" justify="between" wrap="nowrap"`:justify管主轴、align管交叉轴,标题保留内容宽;nowrap仅约束局部,长标题仍须窄屏验收。按路线只读 `references/layout-recipes.md` 对应小节;陌生组合/参考图再读 `references/rendering-model.md`。需要新组合时读 `references/composition-patterns.md`,仍先建立共享容器。frame/tone/size是正交选择,不因微调换组件家族。
|
|
12
|
+
4. 作者组件只用 `garden compose`;内部静态/语义/交互能力按需读 `references/static-play.md`。语义字段、图标、允许子项以 `npx --no-install garden ui schema` 为准,按需存到 `.garden`,不预读整本目录。Markdown/安全HTML岛保存完整块内容;受限表达式不等于任意JS。
|
|
8
13
|
|
|
9
|
-
##
|
|
10
|
-
1.
|
|
11
|
-
2.
|
|
12
|
-
3.
|
|
13
|
-
4.
|
|
14
|
+
## 写入与验证
|
|
15
|
+
1. 必读 `references/workflow.md`:scan,保存id/行号、完整围栏base hash与原文。首次插入意图块和已有块apply不同;未编译意图不是完成品。
|
|
16
|
+
2. 先按 `references/rendering-model.md` 量宿主可用宽度与各层padding,再生成候选;不把主题/运行时挤压误诊成原文过长。正文默认继承文章动态字体/字号/行高;不缩字凑版。显式size只响应明确要求或真实层级。框读frames、颜色读colors;不要把场景框、内部边框和底色混为一谈。
|
|
17
|
+
3. 属性级微调优先,但当前apply仍替换完整围栏:候选文件+稳定id(没有才用scan行号)+原完整base hash;冲突即重读,不绕过保护。不误伤邻块或改写intent原话。
|
|
18
|
+
4. `scripts/check.mjs` 与 `garden check`保持原契约;有内部约束卡可另跑 `scripts/lint-layout.mjs`(workflow说明)。风险提示不等于语法错误;静态属性可能被丢弃,解析通过不等于生效。
|
|
19
|
+
5. 所有新布局都默认承担移动端责任,不等作者指出手机问题。按mobile-contract先验最窄屏和放大正文,再在完整主题与真实CSS顺序下检查桌面、computed样式、几何及阅读密度;交互另测正常首页Link进入、返回再进、后退/前进与直接链接;sandbox不能以强刷或_top代替修复。静态构建自动验导出资源,也可用 `garden verify --rendered` 重跑,但它不证明浏览器导航。分别报告:结构/解析、渲染、浏览器几何/交互、独立AI生成试验。没有对应证据就写未验证。
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
1. 在站点根目录读 `references/workflow.md`。运行 `node .agents/skills/garden-compose/scripts/scan.mjs content/posts/路径.md`,保存目标 id/行号、原始完整围栏 hash、原文和 diagnostics。对已有正确块不重画。
|
|
17
|
-
2. 属性、默认值、允许子项与图标以 **`npx --no-install garden ui schema`** 为准;需要配方再运行 `npx --no-install garden ui prompt`。把 JSON 存到 `.garden` 后只读取当前需要的组件/图标,不预加载整本手册。静态布局部分属性可能被忽略,须检查归一化结果与真实样式,不能把「解析接受」当作「生效」。
|
|
18
|
-
3. 原文没有围栏时,仅在授权位置首次插入一个 `garden compose` 意图块,保留原文与效果要求,再 scan。该插入不是 apply API;未编译意图不是完成品。候选始终是完整 `garden compose`;编译后原话保留在 intent,不重复展示。
|
|
19
|
-
4. `text/title/caption` 用行内 Markdown 与安全行内 HTML;完整段落、列表、表格、代码块等放 `markdown/html` 内容岛。岛内允许实际白名单 HTML/CSS,不允许脚本、事件与危险 URL。需要执行的独立演示用同一 Compose 的 `sandbox`;DIL 表达式和岛内字面内容的边界见能力参考。不要把受限语言误称任意 HTML/JS,也不要把它误称完全不支持 Markdown/HTML。
|
|
20
|
-
5. 正文默认随**实际文章动态配置**继承字号/字体,不设通用像素基线、不缩字凑版。需要强调才用支持的 size/tone/weight 机制;标题、数值、图标签、caption 和控件保留语义层级。当前其他语义尺寸与上下文看 rendering-model,不把某张截图的 computed 值当全局常量。按需读 colors/frames/examples。
|
|
21
|
-
6. 候选存临时 Markdown,再用 `scripts/apply.mjs` 调权威 CLI 写回:有 id 用 `--id`,否则 scan 的 `--at`;总是加原完整围栏 `--base` hash 和候选 `--file`。原文变了就重新 scan,不移除保护强行覆盖;不使用空 intentHash 定位。
|
|
22
|
-
7. 运行 `scripts/check.mjs`;本地文件另做 `npx --no-install garden check 文章.md`。陌生布局/指定视觉效果还要在完整主题及真实 CSS 顺序下验证桌面、窄屏、几何与实际交互。分别报告「解析通过」「渲染成功」「几何/交互通过」与「独立模型生成可靠性」;手写场景的通过不能证明短提示词可靠。没有浏览器或独立试验就如实写未验证。
|
|
23
|
-
|
|
24
|
-
脚本只委托本地 CLI,不复制验证器、不下载包、不默认调用模型或改剪贴板。本地 garden 缺失就报告依赖问题,不另装版本。可复用的新经验只提出技能维护候选,文章处理时不自行改技能。
|
|
21
|
+
脚本仅使用已安装本地引擎,不下载包、不默认调用模型或导出全目录。缺依赖就报告;可复用维护经验只提出候选,不在文章任务里自行改技能。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Engine follow-ups · 本轮只记录
|
|
2
|
+
按预期收益而非虚构测量排序;skill规则不能替代这些运行时能力,修改需另行授权。
|
|
3
|
+
1. 高:静态未知/无效属性显式诊断。现归一化会丢弃,技能无法完整恢复作者意图。
|
|
4
|
+
2. 高:独立主轴/交叉轴/文字对齐,以及可靠的内容宽图标标题组合;centered-chip候选先跑完整主题几何再决定最小引擎修复。
|
|
5
|
+
3. 中:保留base冲突保护的属性patch/稳定块定位,减少微调必须整围栏替换的负担。
|
|
6
|
+
4. 中:完整主题的几何断言、真实列表间距和动态正文对照;先取证,不假定所有CSS默认错误。默认wrap的row与right箭头已有主题级横→纵同步回退,不能再列为缺失API;nowrap在手机仍横排而箭头转向的上下文需单独核查。
|
|
7
|
+
5. 后续:glyph光学齐平;行盒相等不保证视觉齐平。
|
|
8
|
+
未运行浏览器或独立模型实验时不得把以上能力标成已解决。
|
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
- `border="none"`, `background="transparent"`, and `backgroundColor="transparent"` affect different aspects of a supported node. Transparent background is not a border/frame removal API.
|
|
6
6
|
|
|
7
7
|
Don't add raw CSS to emulate unsupported framing. Don't promise frame:none for every rendering path; confirm the actual generated wrapper.
|
|
8
|
+
|
|
9
|
+
## 先选择层级,再动旋钮
|
|
10
|
+
1. 静态整个场景:唯一最外层`<figure frame="none">…</figure>`去场景边界;默认card。它不删除内部卡片,也不是Play头语法。
|
|
11
|
+
2. 内部容器:支持的card/box使用border或frame;card frame="text"是无表面的内容壳,不等于删除外场景。
|
|
12
|
+
3. 底色:background token或支持的backgroundColor;transparent不删除border。
|
|
13
|
+
“去外框”默认只改第1层;“无底色留内框”分别改第1、3层并保留第2层。编辑性说明偏向融入文章,独立实验保留必要边界;不因去框更换内容家族。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Layout recipes · 布局组合要点
|
|
2
2
|
|
|
3
|
-
Read only the section matching the requested layout. /
|
|
3
|
+
Read only the section matching the requested layout. / 按需读取对应布局小节;所有配方先读mobile-contract.md决定手机阅读任务,再选择局部结构。
|
|
4
4
|
|
|
5
5
|
## Horizontal equal-width cards · 横向等宽卡片
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Use `<grid cols={3} gap={3}>` with three direct `<card>` children. `cols` accept
|
|
|
14
14
|
|
|
15
15
|
三个直接 card 子元素配 `grid cols={3} gap={3}`;cols 为 1–6。等宽来自等分网格;当前主题在视口 ≤800px 时收为单列,不强制窄屏横排。
|
|
16
16
|
|
|
17
|
-
- `gap` and `padding`: 0–4 → 0/4/8/12/16px. A restrained consistent default is `padding={3} radius="md" border="subtle" background="
|
|
17
|
+
- `gap` and `padding`: 0–4 → 0/4/8/12/16px. A restrained consistent default is `padding={3} radius="md" border="subtle" background="transparent"` on each card. Preserve grouping borders; do not add fill merely because items are peers. Choose explicit `background="surface"` only for intentional reading groups or requested hierarchy. / 默认透明并保留合理分组边框;内卡surface按阅读分组或明确层级需要选择,不是所有并列项都加底。
|
|
18
18
|
- Supported radii: none/sm/md/lg/xl/full. Borders: true/false/subtle/none/strong. Backgrounds include surface, surface-secondary, surface-accent, surface-tip, surface-muted, paper, well, transparent. Consult tokens only when needed.
|
|
19
19
|
- Equal width does not mean equal word count, forced line breaks or fixed heights. Grid items stretch within their row by normal CSS grid behavior; content remains natural, without height clamps or filler text. / 不为等宽删改原文、补字或强插换行,也不把多行内容裁成固定高度。
|
|
20
20
|
- Icons and semantic colors are optional, not mandatory decoration. Do not invent icons or inject arbitrary CSS. / 图标与颜色可选,不强行装饰。
|
|
@@ -28,20 +28,22 @@ Use `<grid cols={3} gap={3}>` with three direct `<card>` children. `cols` accept
|
|
|
28
28
|
- **产品**:一款真正的好产品本身长什么样,有哪些绝不能碰的死穴。
|
|
29
29
|
使用 garden compose 来呈现横向等宽卡片。
|
|
30
30
|
</intent>
|
|
31
|
+
<figure frame="none">
|
|
31
32
|
<grid cols={3} gap={3}>
|
|
32
|
-
<card padding={3} radius="md" border="subtle" background="
|
|
33
|
+
<card padding={3} radius="md" border="subtle" background="transparent">
|
|
33
34
|
<title>人员</title>
|
|
34
35
|
<text>解决的是团队到底需要哪几类人,各自管什么、不管什么,他们如何进行合作。</text>
|
|
35
36
|
</card>
|
|
36
|
-
<card padding={3} radius="md" border="subtle" background="
|
|
37
|
+
<card padding={3} radius="md" border="subtle" background="transparent">
|
|
37
38
|
<title>流程</title>
|
|
38
39
|
<text>从冒出一个点子到软件上线,先迈左脚还是先迈右脚,怎么用最低成本试错。</text>
|
|
39
40
|
</card>
|
|
40
|
-
<card padding={3} radius="md" border="subtle" background="
|
|
41
|
+
<card padding={3} radius="md" border="subtle" background="transparent">
|
|
41
42
|
<title>产品</title>
|
|
42
43
|
<text>一款真正的好产品本身长什么样,有哪些绝不能碰的死穴。</text>
|
|
43
44
|
</card>
|
|
44
45
|
</grid>
|
|
46
|
+
</figure>
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
Follow workflow.md for id/line/hash safeguards. No default prompt export, clipboard or model call. Structural validation does not prove browser appearance. / 扫描、带 hash 单块写回和复检由 AI 内部完成;不默认导出提示词或调用模型,未看浏览器不声称视觉验收。
|
|
@@ -56,6 +58,7 @@ Follow workflow.md for id/line/hash safeguards. No default prompt export, clipbo
|
|
|
56
58
|
最小语法示意(实施时使用原文;占位文字不是正文素材):
|
|
57
59
|
```garden compose
|
|
58
60
|
<intent>语法示意:固定图标列,共用右侧文字列。</intent>
|
|
61
|
+
<figure frame="none">
|
|
59
62
|
<stack gap={3}>
|
|
60
63
|
<row wrap="nowrap" gap={3}>
|
|
61
64
|
<icon name="book-open" size="xl"/>
|
|
@@ -74,11 +77,54 @@ Follow workflow.md for id/line/hash safeguards. No default prompt export, clipbo
|
|
|
74
77
|
</group>
|
|
75
78
|
</row>
|
|
76
79
|
</stack>
|
|
80
|
+
</figure>
|
|
77
81
|
```
|
|
78
82
|
|
|
79
|
-
标题用 text + 加粗保证与正文同字号;需要不同标题层级才改用 title 并核对实际字号。图标选真实目录中的合适名字,不要求作者指定。divider 是外层 stack 的子项,不是 list
|
|
83
|
+
标题用 text + 加粗保证与正文同字号;需要不同标题层级才改用 title 并核对实际字号。图标选真实目录中的合适名字,不要求作者指定。divider 是外层 stack 的子项,不是 list 的直接子项。编辑性段落默认采用外层 figure frame="none" 融入文章;作者明确要独立场景边界时保留。不要误用内部 box 的 border。
|
|
80
84
|
|
|
81
85
|
上下文、默认值、prose 与 group 的差别及窄屏规则读取 rendering-model.md。检查后的视觉验收:图标列对齐;标题及所有正文段落共用右侧左边界;标题不居中;段落不回到图标列;每两个单元仅一条细线;没有卡片背景、箭头或窄屏横向溢出。
|
|
82
86
|
|
|
83
87
|
### Icon-led editorial list: operational summary
|
|
84
|
-
Use this for independent explanations, not a workflow. Derive the alignment hierarchy before selecting leaves: an outer `stack` repeats `row wrap="nowrap"` with a fixed-size direct `icon` and one flexible `group`. Put the bold heading and **every** original paragraph into separate `text` children of that same group. Place `divider` between rows in the outer stack. Do not place the body outside the row, center the ancestor, use a direct list divider, or substitute a nested stack without checking its contextual padding. The tested syntax above is illustrative; substitute real source text and catalog icons.
|
|
88
|
+
Use this for independent explanations, not a workflow. Derive the alignment hierarchy before selecting leaves: an outer `stack` repeats `row wrap="nowrap"` with a fixed-size direct `icon` and one flexible `group`. Put the bold heading and **every** original paragraph into separate `text` children of that same group. Place `divider` between rows in the outer stack. Do not place the body outside the row, center the ancestor, use a direct list divider, or substitute a nested stack without checking its contextual padding. The tested syntax above is illustrative; substitute real source text and catalog icons. For editorial prose prefer an outer frameless figure unless a scene boundary was requested; an explicit outer scene `figure frame="none"` removes only the scene frame. Verify shared heading/body left edges, fixed icon alignment, retained paragraphs, separators and no narrow-screen overflow under the complete article theme.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## flow-row / flow-stack · 短阶段与长阶段
|
|
92
|
+
只有原文明确顺序才用箭头。短阶段默认用普通row(保持默认wrap)中的text与right箭头;普通正文基线,不加粗、不缩字凑横排。当前完整主题在≤800px把这种row改成column,并取消right箭头的旋转;底层SVG本来向下,因此实际箭头随之朝下。不能只读data-dir="right"就误报移动端方向错误,要核对computed transform或SVG屏幕向量。
|
|
93
|
+
本轮完整主题1200/500浏览器已验证默认row的横→纵回退。长阶段或明确纵向请求仍用stack与down箭头。不要加nowrap:手机保持横排时主题仍可能把right箭头画向下;只有显式要求、实际验证方向与溢出均正确时才偏离默认。没有必要为默认响应式回退询问作者,也不发明响应式属性。
|
|
94
|
+
作者明确“略小”时仅在文字叶子设sm,勿父子重复缩放;编辑性流程默认外figure frame="none"。不把配方验证扩大到所有主题/字体或未知嵌套。
|
|
95
|
+
|
|
96
|
+
## centered-chip · 整体居中、图标贴标题(候选,待浏览器验证)
|
|
97
|
+
编辑性说明默认用外figure frame="none"包裹;这与内部frame="text"标题壳不是同一层。关系是“图标+标题”为一个内容宽整体,正文另占一行;它是局部标题效果,不能默认把整份长清单变成重复居中的大展示单元;不是图标与一个flex:1标题平分整格。
|
|
98
|
+
候选沿用真实文章的结构:`stack align="center" → card frame="text" padding={0} → row align="center" gap={0} wrap="nowrap" → icon inline + text`;正文作为外stack的下一text。frame=text标题壳不是信息卡片。不得把此候选称作跨主题已验证;检查标题壳实际宽度、图文距离、图标与标题行盒、整体中心、长标题、窄屏溢出。icon inline当前另带6px右距,gap=0不等于零距离;row>text仍有flex规则。若失败,记录引擎缺口,不用换字号掩盖。
|
|
99
|
+
|
|
100
|
+
## prose-list · 正文列表手感
|
|
101
|
+
不需要场景时保留普通Markdown。需要嵌入icon-rail时,把真实Markdown无序/有序列表放右侧group内的markdown岛,保留全部子项和层级。title/text是行内内容,不能用“•”拼出列表。结构上核对ul/li;完整主题另验悬挂缩进、段距和换行,不由AST宣称观感相同。
|
|
102
|
+
|
|
103
|
+
## frameless-scene · 正交的框选项
|
|
104
|
+
编辑性说明默认外`figure frame="none"`,内card是否有border、background分别决定;独立实验或明确要求场景边界时保留外框。详见frames.md。默认不加size/tone来补救布局,不删事实、不裁正文。
|
|
105
|
+
|
|
106
|
+
## 内部验收卡(不是作者表单)
|
|
107
|
+
选定路线后在工作笔记记:读者问题、源条目/段落数、共享容器与悬挂、居中对象、相对正文的字号、三层框选择、桌面/窄屏回退、禁止项。唯一推荐不等于凭经验宣称已验证:centered-chip与响应式flow尚须实际浏览器证据;已有icon-rail/equal-cards也须在本次上下文核对。
|
|
108
|
+
|
|
109
|
+
## 各路线共用的移动责任
|
|
110
|
+
- flow-row:响应式阅读任务声明为reflow时,不用nowrap硬撑阶段标签。row方向和实际绘制箭头必须一起改变;换行/纵排/横滚各自不同,不能只看无外层溢出就判通过。
|
|
111
|
+
- equal-cards:单列后再次核对每项扫读节奏;若原本只是短标签/短问句,优先选择紧凑列表/分组而不是机械重复卡片。显式指定卡片时保留要求,自然段长则自然增长,不删字、不设固定高。
|
|
112
|
+
- icon-rail:nowrap只固定图标+可收缩右栏的局部关系,不是让整组条目横排;长段落/真列表在右group内自然换行。
|
|
113
|
+
- centered-chip:只承诺局部贴标。完整问句、长标题、双语标签和放大字体必须实测;不自动把所有正文居中。长扫描清单优先开始对齐的连续阅读,必要时用局部重点居中,不通过sm来压高度。
|
|
114
|
+
- prose-list / frameless:保持真实标记、顺序、正文基线和自然段落;去掉无意义的多层场景表面不是删掉所有必要分组。
|
|
115
|
+
同一树能响应式重排时使用已有机制;若需要尚未支持的断点条件对齐/拓扑,不复制两套文字隐藏一套、不发明媒体属性。采用兼容两端且保真的最小方案并注明视觉取舍;无法满足作者明确硬约束时标未完成,而非让作者做设备调参。
|
|
116
|
+
|
|
117
|
+
## Source-backed metrics · 原文指标
|
|
118
|
+
Use `metric` for a source-backed number plus label/unit/caption; use equal-cards for independent explanatory categories. Do not invent values or convert prose-only peers into metrics. / 原文数值+标签/单位/说明用metric,独立解释类别用equal-cards;不造数据。
|
|
119
|
+
|
|
120
|
+
## Title and badge · 标题与徽标
|
|
121
|
+
Use `row align="center" justify="between" wrap="nowrap"` with a title and badge. Explicit justify controls distribution; align controls the cross axis. Between/around title leaves remain content-sized; nowrap keeps this local row, not the whole article, together. Verify long titles at the actual narrow width. / between让标题与徽标两端分布,align管交叉轴;标题保留内容宽,nowrap只约束这一局部,长标题仍须窄屏实测。
|
|
122
|
+
|
|
123
|
+
## Measured DOM connectors · 可复用的实测连线
|
|
124
|
+
|
|
125
|
+
- First choose native `relations/entity/edge` when its compact ring and detail interaction match the reader's task. It is not an arbitrary-position graph router. / 环形关系+详情合适时优先原生relations,不承诺自由拓扑排版。
|
|
126
|
+
- Custom topology, branch ports or exact placement: keep the same Compose fence and use an offline `sandbox`. Keep labels in ordinary HTML, inheriting its body font; SVG draws **lines only**, never a whole text diagram scaled to fit. The iframe is an isolated document: article web fonts are not automatically available. / 自定义拓扑用同一compose入口内的sandbox;文字留在DOM,SVG只画线,不缩放整张文字图。iframe不自动继承文章web字体,须如实验证。
|
|
127
|
+
- Reuse `measured-connectors.js` inside the sandbox's literal JS, then call `mountMeasuredConnectors(root, [{from:'a',to:'b'}, {from:'a',to:'c',route:'elbow'}, {from:'b',to:'c',route:'elbow',dashed:true}])`. The root needs `position:relative`; node elements need unique `data-node` ids. CSS owns layout, the recipe owns measurement. No new engine component/API or network import is involved. / 根容器定位,节点唯一id;CSS布局与测量连线分工,不发明组件或远程依赖。
|
|
128
|
+
- It uses ResizeObserver, font readiness and SVG screen-coordinate conversion. It returns a disposer; remount on topology changes. Supported for a stable node set: straight edges by default; `route:"elbow"` draws an axis-aligned, midpoint-bend path between measured boundary anchors; `dashed:true` adds a 6/4 dash pattern independently. All update on node/root resize, font readiness and position-only node style/class changes; dragging controls and animation are not supplied. **Not supported:** automatic obstacle avoidance, edge labels, arrowheads, dragging or automatic graph layout. Nodes must not overlap. Choose a readable non-crossing arrangement; if hard requirements need these missing behaviors, implement and test them explicitly rather than claiming this recipe supplies them. / 支持实测直线、`route:'elbow'`轴对齐中点转折与独立`dashed:true`虚线;基于真实节点框重测两端,随字体/容器重排及节点style/class移动更新;不提供自动避障、边标签、箭头、拖拽或自动排图,不能把端点测试当成完整复杂图验收。
|
|
129
|
+
- Protect readable node widths before choosing a narrow layout. For spatial relationships that must retain their arrangement, wrap the graph in a labelled, keyboard-focusable local viewport (`max-width:100%; overflow-x:auto`); a two-column graph can use `min-width:34em` and natural word wrapping (`overflow-wrap:break-word; word-break:normal`). Font enlargement increases that floor, not shrinks type. Resize the viewport, not below the node floor. If the task permits reflow, explicitly reroute edges so they do not run through intervening nodes. This is recipe CSS, not a new engine API. / 空间关系需保留时用有标签、可键盘访问的局部横滚;节点保留可读最小宽,正文自然换词,不以anywhere挤成碎词、不缩字。可重排的任务另验改序与连线,不把直排后穿过中间节点当成功。
|
|
130
|
+
- Verify actual endpoints, elbow segment axes and dash style after reflow and enlarged text at 300/344/375/430 and desktop widths; inspect crossings, reading order, scrollbar ownership and fullscreen separately. `tests/measured-connectors-browser.test.mjs` verifies the foundation, not every topology. / 按真实节点框验端点,另审交叉、阅读顺序、滚动归属与全屏;测试基础能力不等于所有拓扑都已验收。
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Offline sandbox recipe, not a Compose component or an automatic graph router.
|
|
2
|
+
// root: positioned DOM container; nodes: unique data-node ids.
|
|
3
|
+
// edges: {from,to,route?:"straight"|"elbow",dashed?:boolean}; no obstacle router.
|
|
4
|
+
function mountMeasuredConnectors(root, edges) {
|
|
5
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
6
|
+
const nodes = new Map([...root.querySelectorAll('[data-node]')].map(node => [node.dataset.node, node]));
|
|
7
|
+
if (nodes.size !== root.querySelectorAll('[data-node]').length) throw new Error('Duplicate connector node id');
|
|
8
|
+
for (const edge of edges) {
|
|
9
|
+
if (!nodes.has(edge.from) || !nodes.has(edge.to)) throw new Error('Unknown connector node id');
|
|
10
|
+
if (edge.route !== undefined && !['straight', 'elbow'].includes(edge.route)) throw new Error('Unknown connector route');
|
|
11
|
+
if (edge.dashed !== undefined && typeof edge.dashed !== 'boolean') throw new Error('Connector dashed must be boolean');
|
|
12
|
+
}
|
|
13
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
14
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
15
|
+
svg.classList.add('connector-lines');
|
|
16
|
+
Object.assign(svg.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' });
|
|
17
|
+
const paths = edges.map(edge => {
|
|
18
|
+
const line = document.createElementNS(namespace, 'path');
|
|
19
|
+
line.dataset.from = edge.from;
|
|
20
|
+
line.dataset.to = edge.to;
|
|
21
|
+
line.dataset.route = edge.route ?? 'straight';
|
|
22
|
+
if (edge.dashed) line.setAttribute('stroke-dasharray', '6 4');
|
|
23
|
+
line.setAttribute('fill', 'none');
|
|
24
|
+
line.setAttribute('stroke', 'currentColor');
|
|
25
|
+
line.setAttribute('stroke-width', '1.5');
|
|
26
|
+
svg.append(line);
|
|
27
|
+
return line;
|
|
28
|
+
});
|
|
29
|
+
root.prepend(svg);
|
|
30
|
+
let active = true;
|
|
31
|
+
let scheduled = 0;
|
|
32
|
+
function draw() {
|
|
33
|
+
scheduled = 0;
|
|
34
|
+
if (!active || !svg.isConnected) return;
|
|
35
|
+
const matrix = svg.getScreenCTM();
|
|
36
|
+
if (!matrix) return;
|
|
37
|
+
const inverse = matrix.inverse();
|
|
38
|
+
for (const [index, edge] of edges.entries()) {
|
|
39
|
+
const from = nodes.get(edge.from).getBoundingClientRect();
|
|
40
|
+
const to = nodes.get(edge.to).getBoundingClientRect();
|
|
41
|
+
const a = { x: from.x + from.width / 2, y: from.y + from.height / 2 };
|
|
42
|
+
const b = { x: to.x + to.width / 2, y: to.y + to.height / 2 };
|
|
43
|
+
// Prefer a genuinely separated axis: tall peer cards can overlap vertically.
|
|
44
|
+
// A center-only choice could send the first elbow segment back into its node.
|
|
45
|
+
const separatedX = from.right <= to.left || to.right <= from.left;
|
|
46
|
+
const separatedY = from.bottom <= to.top || to.bottom <= from.top;
|
|
47
|
+
const horizontal = separatedX !== separatedY ? separatedX : Math.abs(b.x - a.x) > Math.abs(b.y - a.y);
|
|
48
|
+
if (horizontal) {
|
|
49
|
+
const forward = b.x > a.x;
|
|
50
|
+
a.x = forward ? from.right : from.left;
|
|
51
|
+
b.x = forward ? to.left : to.right;
|
|
52
|
+
} else {
|
|
53
|
+
const forward = b.y > a.y;
|
|
54
|
+
a.y = forward ? from.bottom : from.top;
|
|
55
|
+
b.y = forward ? to.top : to.bottom;
|
|
56
|
+
}
|
|
57
|
+
const start = new DOMPoint(a.x, a.y).matrixTransform(inverse);
|
|
58
|
+
const end = new DOMPoint(b.x, b.y).matrixTransform(inverse);
|
|
59
|
+
const points = [start];
|
|
60
|
+
if (edge.route === 'elbow') {
|
|
61
|
+
// Mid-gap orthogonal route, recalculated from actual node bounds.
|
|
62
|
+
// Intentionally no obstacle search or automatic node placement.
|
|
63
|
+
if (horizontal) {
|
|
64
|
+
const x = (start.x + end.x) / 2;
|
|
65
|
+
points.push({ x, y: start.y }, { x, y: end.y });
|
|
66
|
+
} else {
|
|
67
|
+
const y = (start.y + end.y) / 2;
|
|
68
|
+
points.push({ x: start.x, y }, { x: end.x, y });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
points.push(end);
|
|
72
|
+
paths[index].setAttribute('d', points.map((point, i) => `${i ? 'L' : 'M'}${point.x},${point.y}`).join(' '));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function schedule() { if (active && !scheduled) scheduled = requestAnimationFrame(draw); }
|
|
76
|
+
const observer = new ResizeObserver(schedule);
|
|
77
|
+
observer.observe(root);
|
|
78
|
+
for (const node of nodes.values()) observer.observe(node);
|
|
79
|
+
const changes = new MutationObserver(schedule);
|
|
80
|
+
// Position-only style/class changes do not trigger ResizeObserver.
|
|
81
|
+
// Do not observe path `d` writes: measurement must not reschedule itself.
|
|
82
|
+
changes.observe(root, { attributes: true, attributeFilter: ['style', 'class'], subtree: true });
|
|
83
|
+
window.addEventListener('resize', schedule);
|
|
84
|
+
document.fonts?.ready.then(schedule);
|
|
85
|
+
schedule();
|
|
86
|
+
return () => {
|
|
87
|
+
active = false;
|
|
88
|
+
cancelAnimationFrame(scheduled);
|
|
89
|
+
observer.disconnect();
|
|
90
|
+
changes.disconnect();
|
|
91
|
+
window.removeEventListener('resize', schedule);
|
|
92
|
+
svg.remove();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Mobile contract · 阅读优先的内部编译契约
|
|
2
|
+
作者只提供原文与效果;手机不是额外选项,也不是作者要填写的规格。先识别读者任务,再做视觉配方,最后在真实小屏验证。
|
|
3
|
+
|
|
4
|
+
## 生成前:两个尺度、一份原文
|
|
5
|
+
AI内部记录:读者扫读/顺序/对照/空间关系;条目数及长短分布;标题与正文的共享边界;整体/局部居中;桌面与窄屏的流向;正文相对字号;框层级;应保留的顺序、完整段落和重点。不让作者选择断点、标签和font-size。
|
|
6
|
+
先从最窄阅读宽度推导可持续结构,再扩展桌面。优先同一内容树重排,不复制两份文字,不删事实、不缩字凑版、不把长内容藏进交互。作者明确的局部效果不自动等于整个场景的阅读模式;居中图标标题不能推导全部长正文居中。
|
|
7
|
+
|
|
8
|
+
## 三道门,不能互相替代
|
|
9
|
+
1. **结构/源保真**:权威parser/check;原文各叶子及顺序、真实列表、共享容器、未知属性限制。内部约束卡可选mobile,见workflow;不是围栏属性或作者新API。
|
|
10
|
+
2. **浏览器几何**:真实CSS viewport与available content width;当前主题/完整CSS顺序;正文默认及放大字号;每个滚动容器scrollWidth/clientWidth,不只页面外框;最窄叶子的换行与文字宽度;实际SVG屏幕向量和父布局轴;不裁切、遮挡、重叠或隐藏事实。data-dir、DOM存在和无页面横滚都不是充分证据。
|
|
11
|
+
3. **阅读节奏人工审查**:把整组而非一个漂亮单元看完。能否迅速找下一项?有没有多余重复标题、层层表面、每条都成为独立海报、过量空白或居中长段让眼睛反复找起点?很长的真实材料可以跨多屏;禁止用“必须一屏”迫使删字、缩字或隐藏。
|
|
12
|
+
|
|
13
|
+
## 通用测试矩阵,不是设计硬阈值
|
|
14
|
+
起步样本可用CSS宽375/430/768/1200,以及正常正文和1.25倍正文;这些只是回归采样,不是作者批准的断点或科学最优值。必须记录浏览器实际innerWidth,不把截图物理像素或Chrome窗口参数当CSS viewport。主题已有≤800px机制要按实际CSS理解。
|
|
15
|
+
记录sceneHeight/availableHeight、每项行数、字号相对正文、内部横滚、每项起点及序号可见性;这些是证据,不是把某个比值硬判为不美观。可疑高空白/重复表面/多行短标签要求人工复核;任何“最多几项/最多几屏/字号下限”的新阈值都先是待校准建议,不可伪称用户规则或测量结论。
|
|
16
|
+
|
|
17
|
+
## 默认修复顺序由AI承担
|
|
18
|
+
错误阅读拓扑/容器 → 不必要nowrap或宽度约束 → 重复框与间距 → 局部对齐。字号最后且只用于真实层级/明确需求,不为修布局来回sm/lg。局部图标栏的nowrap可以合理,不能全局禁用。
|
|
19
|
+
默认wrap的短阶段row已有手机纵排+箭头转向;不要要求作者决定技术回退。硬nowrap阶段链会保留横向却可能出现向下箭头,并把文字挤成多行;即使没有scroll也失败。
|
|
20
|
+
当只靠现有语法不能同时满足显式硬约束时,交付中记录准确能力缺口与保真取舍,不能假称视觉完成。几何通过后仍需阅读审查,缺浏览器就标移动端待验,不把QA任务退给作者。
|
|
21
|
+
|
|
22
|
+
## 防止只记住这次两张图
|
|
23
|
+
冻结首稿,先评估再修。回归覆盖短/长顺序、短并列与长扫描清单、多段解释、嵌套真列表、长中英混排/链接和放大字体。独立AI只读原文+一句效果/参考图,不能看到标准输出。区分同一上下文多例与多次隔离试验,不用一次通过宣称可靠率。
|
|
@@ -6,10 +6,22 @@
|
|
|
6
6
|
- 关系决定共同容器:一个图标对应的标题与所有正文要属于一个重复单元;标题和正文必须同在右侧容器。分隔整条目时 divider 放两条 row 之间,而非右栏内。
|
|
7
7
|
- row 默认 wrap/stretch、8px gap;直接 card/stack/title/text 共分可用宽度,icon 固定不缩。窄屏默认会改行流;需要持续图标栏用 wrap="nowrap" 并测试不溢出。
|
|
8
8
|
- row > stack 有 **12px 顶部 padding**;不能假定任意嵌套同样表现。group 为 width:100%、min-width:0 的纵向容器,能在固定 icon 旁收缩,支持 gap,避开该上下文顶距。prose 不能凭空赋予未实现的 gap 行为。
|
|
9
|
-
- align="center"
|
|
9
|
+
- row 的 align 管交叉轴,justify 管主轴;显式 justify 始终优先,不被 align="center" 或徽标的默认居中覆盖。文字仍可能继承 text-align。当前 title/text 的显式 align="start" 会保留并输出 data-align。用一个共享 group 和显式开始对齐,不要把 title 独立成横向等分栏、正文又放回全宽。
|
|
10
10
|
- icon 名查 schema/icons 实际目录;不存在报错。SVG width 属性不等于最终像素:当前 CSS sm13、md15、lg18、xl22px,小卡上下文另有覆盖,必须测 computed/rect。
|
|
11
11
|
- 多段正文分别放 text,或用 markdown/html 岛保存真实 p。text 内空行是 inline break,不是独立段落。图标签有截断/行钳制时不能塞完整正文去丢字。
|
|
12
12
|
|
|
13
|
+
## Diagnose the host before rewriting content · 先诊断宿主
|
|
14
|
+
Measure viewport → `.article-reading-column` → `.garden-ui-body` → iframe; record each width and computed padding. Do not infer content width from a desktop breakpoint. The reading track takes priority over side rails; a frameless runtime has no body inset; fullscreen controls occupy a top band outside the frame. Post mobile TOC entries are in normal flow before the article. / 依次量视口、正文栏、运行时正文与iframe,不先删字或缩字。侧栏让位正文,frame="none"不再叠运行时内距,全屏按钮占外部顶栏,手机目录入口在正文前正常排版。
|
|
15
|
+
|
|
16
|
+
For a title and trailing badge, use `<row align="center" justify="between" wrap="nowrap"><title align="start">…</title><badge>…</badge></row>`. Its text/title children are content-sized and may shrink/wrap naturally, not width:100% columns. Explicit fills, scene framing and fullscreen are independent choices. / 标题+尾部徽标直接组合已有属性;不靠换组件、固定标题宽度或缩字绕开。
|
|
17
|
+
|
|
18
|
+
## 集中禁令与风险分级
|
|
19
|
+
- **必选结构(已选icon-rail)**:icon和一个group同属row,标题与全部正文/列表同属group;分隔线在row之间。不要正文逃出右栏,不用row>stack替代group来碰运气。
|
|
20
|
+
- **无效属性**:stack不因direction="row"变横向;用真实row。未知静态属性可能归一化丢失,当前lint不能从归一化树找回它们;对照源属性与schema,缺少未知属性诊断是引擎需求,不谎称已机器覆盖。
|
|
21
|
+
- **风险而非全局错误**:row>stack的12px顶距、居中row直接title/text的flex:1、center仍可能向后代继承文字居中(但不覆盖显式justify)、nowrap长链、显式size和叠框。这些在其他语境可能有意,必须结合内部约束卡判断,不能一律禁用。
|
|
22
|
+
- **箭头看实际绘制方向**:right是源意图,不必等于当前屏幕方向。真实SVG默认向下;桌面旋转为right,≤800px取消旋转配合默认row转column。先看computed transform/屏幕向量,不把data-dir当几何证据。nowrap让row保持横向却不自动保留箭头旋转,须专项检查。
|
|
23
|
+
- **必须浏览器**:icon glyph光学齐平不是节点矩形相等;内容宽居中、完整主题字号/列表间距、溢出与手机回退不能从AST推断。用computed与rect及截图分别取证。
|
|
24
|
+
|
|
13
25
|
## 默认与显式样式
|
|
14
26
|
普通正文默认继承实际文章动态字体/字号;不再使用固定正文像素基线。text/title 的 sm=.875em、lg=1.125em、xl=1.25em,md 保持基线;title 默认 600/1.4。外层 figure 标题18px、caption14px及控件/图标签等是语义角色,不应全局拉成正文。其他语义尺寸及实际上下文见下表与完整主题。显式字体变化必须有信息层级依据,不缩字替代排版。
|
|
15
27
|
|
|
@@ -43,7 +55,7 @@ The authoritative, demand-loaded catalog is `npx garden ui schema`: actual seman
|
|
|
43
55
|
|
|
44
56
|
First identify what belongs together. A heading and all its paragraphs must share a container. Next choose columns and proportions, then alignments and typography. Only then add surfaces and decorative details.
|
|
45
57
|
|
|
46
|
-
A static `row` wraps by default. Its direct text/title/stack/card children share available width (`flex: 1 1 0`), whereas an icon is content-sized. A heading placed directly in that row is a sibling column, not the heading of a paragraph below the row. `align="center"`
|
|
58
|
+
A static `row` wraps by default. Its direct text/title/stack/card children share available width (`flex: 1 1 0`), whereas an icon is content-sized. A heading placed directly in that row is a sibling column, not the heading of a paragraph below the row. `align="center"` controls the cross axis and can inherit centered text; an explicit `justify` independently controls the main axis and takes precedence over default badge centering. A centered stack propagates text alignment. For an editorial list use explicit start alignment on children of a shared right-hand group; do not center each title independently.
|
|
47
59
|
|
|
48
60
|
Direct `row > stack` has 12px top padding in the current theme. A `group` is a width:100%, min-width:0 vertical container that shrinks beside the fixed icon and reads gap without that contextual padding; use it when icon and heading must begin on the same top line. Explicit `align="start"` survives normalization and emits data-align.
|
|
49
61
|
|
|
@@ -111,6 +123,8 @@ Use `markdown` or `html` islands for full paragraphs, lists, headings, blockquot
|
|
|
111
123
|
|
|
112
124
|
Safe HTML supports allowlisted local `style` properties, including useful layout and typography; inspect the generated schema for the actual set. Unsupported tags/properties, events, unsafe URLs, executable scripts and network CSS functions report diagnostics. Do not expand the article allowlist into arbitrary execution. An intentional executable demo uses `sandbox` inside the **same Compose entry**, with literal html/css/js and no parent origin access. Sandbox layout is its own document, not inherited article prose.
|
|
113
125
|
|
|
126
|
+
For measured node connections, load layout-recipes.md and measured-connectors.js: straight, midpoint orthogonal elbow and optional dashed edges follow real DOM bounds. This is a sandbox recipe, not automatic graph layout or obstacle avoidance.
|
|
127
|
+
|
|
114
128
|
## Verification levels
|
|
115
129
|
|
|
116
130
|
1. Run `npx garden ui check <file>` (and `npx garden check <file>` for real file paths). Preserve original text/resources and inspect diagnostics.
|
|
@@ -2,12 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Load when choosing how to explain source content, not for every repair. Use the current lowercase Compose surface. / 需要选图时读取,不是每次修复都读。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
5
|
+
## Effect compiler · 效果先收敛
|
|
6
|
+
作者给原文+一句效果/参考图即可;以下是AI内部动作,不要求作者懂标签。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
1. 确定读者问题、移动阅读任务和原文真实关系;尊重明确效果。没有顺序不画流程,没有因果不加因果箭头,没有数据不造图表。
|
|
9
|
+
2. 写一张内部几何约束卡(工作笔记/临时JSON即可,非新作者语法):读者问题;共享容器;悬挂与正文范围;居中的是文字还是图标标题整体;字号相对正文;场景框/内部边框/底色;窄屏回退;不要什么。参考图也先提取这些约束,不能只识别“图标+文字”。
|
|
10
|
+
3. 全局阅读结构先于局部效果:长清单的任务是连续扫读,不是把每项放大为一个居中展示单元。按mobile-contract检查内容量、长短差异和小屏阅读节奏,再按下表取一条推荐路线,填原文叶子。默认正文基线、开始对齐、自然换行,不新增强调色;只在有意强调时加样式。未知但不影响事实的细节用默认,不把问题推给作者;可能改变事实关系、隐藏内容或阅读顺序时才问。
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
| 效果/读者问题 | 唯一推荐路线 | whenNot / 不这样用 |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| 图标窄栏、标题正文共左缘 | icon-rail:stack重复row(icon+group),divider在row间 | 不把正文放row外;不当流程 |
|
|
15
|
+
| 短阶段从左到右 | flow-row:row+真实顺序箭头;窄屏/长内容回退flow-stack | 不靠缩字/nowrap强撑手机 |
|
|
16
|
+
| 长阶段或纵向顺序 | flow-stack:stack+下箭头 | 无顺序的条目不加箭头 |
|
|
17
|
+
| 原文关键数值+标签/单位 | metric:保留原始值、单位与说明 | 不造数据;无数值的解释类别仍用equal-cards |
|
|
18
|
+
| 并列等宽卡片 | equal-cards:grid+直接card | 非真实共同维度不走compare |
|
|
19
|
+
| 图标贴标题,两者一起居中 | centered-chip:内容宽标题壳+内部图标标题行,下方正文独立 | 不只给全宽row设center;候选须浏览器验收 |
|
|
20
|
+
| 跟正文一样的列表 | prose-list:普通Markdown;场景内用markdown岛 | 不写假圆点;不为列表手感换到compare |
|
|
21
|
+
| 去外框/留内框 | 原路线+frameless-scene选项:外figure frame=none | 不把透明底色当去边框,不改错层级 |
|
|
22
|
+
|
|
23
|
+
这些是**唯一推荐默认**,不是组件白名单;先使用最接近的已验局部结构,确有关系需求再按composition-patterns组合新形式,并保留同样约束和验证。frame/tone是正交选项,不衍生新的配方家族。
|
|
24
|
+
|
|
25
|
+
边界:普通Markdown优先;compare仅真实共同维度对照。当前只有garden compose围栏,旧garden list不再推荐。Compose内部icon-list仅在需要其原生语义/宿主行为且接受其样式契约时用;普通编辑性图标段落默认icon-rail,不能仅为调字号来回迁移。需要语义/交互时再读static-play,不靠标签同名猜兼容。
|
|
11
26
|
|
|
12
27
|
## Capability map · 典型形式与边界
|
|
13
28
|
|
|
@@ -36,3 +51,7 @@ This is a selection map, not a new syntax catalog. For chart attributes use the
|
|
|
36
51
|
No arbitrary SVG/canvas, custom CSS/JS, general node-id edge routing, automatic graph/tree placement, simulated execution, arbitrary nested rich `compare` items, or full parity between static and Play. A small drawn branch does not imply support for a full flowchart language. `steps` detail toggles and `tabs` are not numerical Play state.
|
|
37
52
|
|
|
38
53
|
If the requested relation needs unsupported routing/interaction, state the gap and suggest a simpler faithful Compose scene or an existing appropriate article format; do not silently switch engines or invent tags. Validation proves syntax/semantics only; inspect the browser before claiming visual/responsive behavior was verified.
|
|
54
|
+
|
|
55
|
+
## Mobile is part of compilation · 不是交给作者的QA
|
|
56
|
+
断点自动单列只证明重排,不证明好读。centered-chip是局部贴标结构,不是长清单的全局默认;equal-cards也是按信息关系选择,不是见到N项就生成N张大卡。普通短清单优先prose-list;有独立标题/多段解释才用共享右栏。明确要求的卡片/居中仍须保留其意图,但不能以缩字、硬横排、折叠事实来达成。
|
|
57
|
+
长内容自然跨屏不算失败;要识别的是重复装饰、冗余表面、过量容器间距、窄列拆词和反复居中造成的阅读负担。移动约束由AI内部收敛与验证,不询问作者要断点、字号或方向细节。当前能力确实无法同时满足硬约束时如实标未完成/引擎缺口,不静默改事实或发明属性。
|
|
@@ -25,3 +25,29 @@ node .agents/skills/garden-compose/scripts/check.mjs content/posts/post.md
|
|
|
25
25
|
## Prose outside a fence · 围栏外的原文
|
|
26
26
|
|
|
27
27
|
Separate the source paragraphs/list items from the requested effect. At the authorized location, create one `garden compose` intent block preserving both verbatim, then scan it. Initial insertion is distinct from replacing a block: `apply` requires a scanned existing target. For existing blocks, use scan → candidate → apply with the original base hash → check. Preserve unrelated prose and render the source once. / 分离原文与效果要求,在授权位置首次创建 `garden compose` 意图块后扫描;已有块始终带原 base hash 写回并复检,不把 apply 当作插入 API,不重复展示原文。
|
|
28
|
+
|
|
29
|
+
## Local micro-edit · 微调不重画
|
|
30
|
+
scan → 保存稳定id(没有则目标行)与完整围栏base → 仅改候选中的必要属性/叶子 → 审查diff → 带原base apply → rescan/check。去粗先区分源Markdown加粗与title默认600;略小仅在明确要求时改文字叶子size;紧一点改对应容器gap;去外框只改最外figure。不要为了旋钮变化换标签家族或清空intent。稳定id在获授权编辑该块时建立,不全篇加id。并发修改造成base冲突必须重新读取。
|
|
31
|
+
当前没有属性patch或dry-run;apply始终写回完整围栏。最小候选diff不等于局部写入API;保留围栏外字节及完整原文核对。
|
|
32
|
+
|
|
33
|
+
## Opt-in layout check · 独立的内部约束检查
|
|
34
|
+
原check.mjs的参数/退出状态不变。AI内部约束卡可保存为临时JSON(不是围栏属性/作者新语法):
|
|
35
|
+
```json
|
|
36
|
+
{"at":90,"base":"扫描所得完整围栏hash","layout":"icon-rail","units":3,"outerFrame":"none"}
|
|
37
|
+
```
|
|
38
|
+
有稳定id用id替代at;base必填。运行:
|
|
39
|
+
```sh
|
|
40
|
+
node .agents/skills/garden-compose/scripts/lint-layout.mjs content/posts/post.md --constraints .garden/layout.json
|
|
41
|
+
```
|
|
42
|
+
仅支持当前静态Compose;复用实际本地hd-garden的scanner和parser,不下载包、不复制解析器。错误退出1,工具/约束输入错误退出2,风险/浏览器待验不使退出失败。只对显式声明的icon-rail/equal-cards和outerFrame做有限结构断言;其他路线仍需人工结构清单。不能从归一化树还原未知属性、推断自然语言效果或验证几何;保真核对另做,不把机器支持范围夸大。
|
|
43
|
+
|
|
44
|
+
## Mobile acceptance · AI内部,不要求作者补规格
|
|
45
|
+
几何卡补可选`"mobile":{"readingMode":"sequence","reflow":true}`;readingMode可选sequence/scan-list/comparison/spatial,reflow为布尔。lint只在显式reflow的flow-row中拒绝场景顶层流向row的nowrap(不误禁内部贴标row);icon-rail局部nowrap不被误禁。scan-list的成组居中只给风险提示,不凭AST断言过高或难读。
|
|
46
|
+
浏览器验证遵守mobile-contract:记录实际CSS viewport、内容宽、正常/放大正文、每层滚动与实际箭头向量,并人工审查整组阅读节奏。保留首稿和源hash,失败后的修订单独编号,不能把修后结果算首轮成功。默认check参数/退出状态不变,mobile字段不是runtime DIL属性。
|
|
47
|
+
|
|
48
|
+
## Built artifact gate · 最终产物门
|
|
49
|
+
`STATIC_EXPORT=1 garden build` now checks the final export automatically. `garden verify --rendered` reruns it without rebuilding: actual HTML stylesheet/script links, literal JS imports (including lazy chunks), Compose/runtime CSS and per-page client navigation payloads. Missing assets or corrupt payloads fail. / 静态构建后自动检查真实HTML/CSS/JS依赖及导航payload,缺失即失败;不是只看源代码导入。
|
|
50
|
+
|
|
51
|
+
This is asset integrity, **not** visual or lifecycle acceptance. Complete-theme browser tests and the real site's normal Link → article → home → re-entry → history/direct sequence remain separate gates. Record iframe height messages from its own contentWindow, srcdoc length and computed geometry; a populated srcdoc alone does not prove execution. Do not claim navigation fixed from a direct-link pass. / 资源完整≠样式生效≠生命周期通过;iframe需记录实际消息、srcdoc与几何,不能凭直接链接成功宣称客户端导航已修好。
|
|
52
|
+
|
|
53
|
+
Measured connector recipes: verify boundary endpoints after enlarged labels and container resize; for elbow edges also verify every segment is axis-aligned, and dashed edges retain their dash style. Do not infer obstacle avoidance from endpoint tests. / 连线需验实测端点、肘线各段轴对齐与虚线样式;另审交叉,不能据此宣称自动避障。
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createRequire} from 'node:module';
|
|
4
|
+
import {pathToFileURL} from 'node:url';
|
|
5
|
+
const layouts = new Set(['icon-rail','equal-cards','flow-row','flow-stack','centered-chip','prose-list']);
|
|
6
|
+
export function validateCard(card) {
|
|
7
|
+
const keys = new Set(['id','at','base','layout','units','outerFrame','mobile']);
|
|
8
|
+
if (!card || typeof card !== 'object' || Array.isArray(card) || Object.keys(card).some(k=>!keys.has(k))) throw new Error('Unknown constraint field/object');
|
|
9
|
+
if ((typeof card.id==='string' && card.id.length>0) === (Number.isInteger(card.at) && card.at>0)) throw new Error('Choose exactly one nonempty id or positive at line');
|
|
10
|
+
if (card.id!==undefined && (typeof card.id!=='string'||!card.id)) throw new Error('Invalid id');
|
|
11
|
+
if (card.at!==undefined && (!Number.isInteger(card.at)||card.at<1)) throw new Error('Invalid at');
|
|
12
|
+
if (!/^[a-f0-9]{8}$/.test(card.base??'')) throw new Error('base must be the scanned complete fence hash');
|
|
13
|
+
if (!layouts.has(card.layout)) throw new Error('Unsupported layout recommendation');
|
|
14
|
+
if (card.units!==undefined && (!Number.isInteger(card.units)||card.units<1)) throw new Error('units must be positive');
|
|
15
|
+
if (card.outerFrame!==undefined && !['none','card'].includes(card.outerFrame)) throw new Error('Invalid outerFrame');
|
|
16
|
+
if (card.mobile!==undefined) {
|
|
17
|
+
const m=card.mobile;
|
|
18
|
+
if (!m || typeof m!=='object' || Array.isArray(m) || Object.keys(m).some(k=>!['readingMode','reflow'].includes(k))) throw new Error('Invalid mobile constraint');
|
|
19
|
+
if (m.readingMode!==undefined && !['sequence','scan-list','comparison','spatial'].includes(m.readingMode)) throw new Error('Invalid mobile readingMode');
|
|
20
|
+
if (m.reflow!==undefined && typeof m.reflow!=='boolean') throw new Error('Invalid mobile reflow');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function inspectLayout(doc, card) {
|
|
24
|
+
validateCard(card);
|
|
25
|
+
const diagnostics=[];
|
|
26
|
+
const add=(severity,code,message)=>diagnostics.push({severity,code,message});
|
|
27
|
+
if (!Array.isArray(doc?.nodes)) throw new Error('Only normalized static Compose is supported');
|
|
28
|
+
const nodes=doc.nodes.length===1 && doc.nodes[0].type==='figure' ? doc.nodes[0].children : doc.nodes;
|
|
29
|
+
if (card.outerFrame && (doc.frame??'card')!==card.outerFrame) add('error','outer-frame','Declared scene frame does not match normalized scene');
|
|
30
|
+
if (card.layout==='icon-rail') {
|
|
31
|
+
const stack=nodes.length===1 && nodes[0].type==='stack' ? nodes[0] : null;
|
|
32
|
+
if (!stack) add('error','rail-container','Recommended rail needs one outer stack');
|
|
33
|
+
else {
|
|
34
|
+
const rows=stack.children.filter(n=>n.type==='row');
|
|
35
|
+
if (!rows.length || stack.children.some(n=>!['row','divider'].includes(n.type))) add('error','rail-outside-body','Only rows and between-row dividers belong in the outer rail');
|
|
36
|
+
if (card.units!==undefined && rows.length!==card.units) add('error','unit-count','Row count differs from declared source units');
|
|
37
|
+
for (const [i,n] of stack.children.entries()) if (n.type==='divider' && (stack.children[i-1]?.type!=='row'||stack.children[i+1]?.type!=='row')) add('error','rail-divider','Divider must separate two rows');
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const icons=row.children.filter(n=>n.type==='icon'), groups=row.children.filter(n=>n.type==='group');
|
|
40
|
+
if (row.children.length!==2 || icons.length!==1 || groups.length!==1 || row.children[0].type!=='icon') add('error','rail-shared-container','Row needs icon then exactly one shared right-hand group');
|
|
41
|
+
const group=groups[0];
|
|
42
|
+
if (group && (group.children.length<2 || !['text','title'].includes(group.children[0].type))) add('error','rail-heading-body','Group needs heading and body/content leaves');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (card.layout==='equal-cards') {
|
|
47
|
+
const grid=nodes.length===1 && nodes[0].type==='grid' ? nodes[0] : null;
|
|
48
|
+
if (!grid || !grid.children.length || grid.children.some(n=>n.type!=='card')) add('error','cards-grid','Recommended equal cards need one grid with direct cards');
|
|
49
|
+
else if (card.units!==undefined && (grid.children.length!==card.units || grid.cols!==Math.min(card.units,6))) add('error','unit-count','Grid cards/columns differ from declared source units');
|
|
50
|
+
}
|
|
51
|
+
const visit=(n,parent)=>{
|
|
52
|
+
if (card.mobile?.reflow===true && card.layout==='flow-row' && nodes.includes(n) && n.type==='row' && n.wrap==='nowrap') add('error','mobile-flow-nowrap','Declared responsive sequence cannot hard-lock its row; inspect painted arrows and narrow label wrapping');
|
|
53
|
+
if (card.mobile?.readingMode==='scan-list' && ['stack','grid'].includes(n.type) && n.align==='center' && (n.children??[]).filter(c=>['card','group','stack'].includes(c.type)).length>1) add('warning','mobile-scan-centering','Repeated centered scan units require whole-scene density and reading review; AST does not prove a failure');
|
|
54
|
+
if (parent?.type==='row' && n.type==='stack') add('warning','row-stack-padding','Current theme adds 12px top padding');
|
|
55
|
+
if (n.type==='row' && n.align==='center' && n.children?.some(c=>['title','text'].includes(c.type))) add('warning','center-flex','Center couples axes/text; direct title/text keep flex:1');
|
|
56
|
+
if (n.type==='row' && n.wrap==='nowrap') add('warning','nowrap','Browser must verify wrapping and overflow');
|
|
57
|
+
if (n.size && n.size!=='md') add('warning','normalized-size','Non-baseline normalized size: compare with source intent; normalization does not prove an authored attribute');
|
|
58
|
+
if (n.type==='card' && parent?.type==='card') add('warning','nested-surface','Check whether nested surfaces are intentional');
|
|
59
|
+
for (const child of n.children??[]) visit(child,n);
|
|
60
|
+
};
|
|
61
|
+
doc.nodes.forEach(n=>visit(n,null));
|
|
62
|
+
return {ok:!diagnostics.some(d=>d.severity==='error'),diagnostics,coverage:{structural:[...(card.outerFrame?['declared outerFrame']:[]),...(['icon-rail','equal-cards'].includes(card.layout)?[card.layout+' shape']:[]) ,...(card.mobile?.reflow===true&&card.layout==='flow-row'?['declared mobile top-level flow nowrap guard']:[])],notChecked:['source fidelity','unknown source attributes','other route structures','DOM geometry','responsive layout','optical alignment','AI reliability']},browserRequired:true};
|
|
63
|
+
}
|
|
64
|
+
async function main() {
|
|
65
|
+
const args=process.argv.slice(2);
|
|
66
|
+
if (args.length!==3 || args[1]!=='--constraints') throw new Error('Usage: lint-layout.mjs post.md --constraints card.json');
|
|
67
|
+
const card=JSON.parse(fs.readFileSync(args[2],'utf8')); validateCard(card);
|
|
68
|
+
const cwd=process.cwd();
|
|
69
|
+
const localManifest=path.join(cwd,'package.json');
|
|
70
|
+
const local=JSON.parse(fs.readFileSync(localManifest,'utf8'));
|
|
71
|
+
const root=local.name==='hd-garden'?cwd:path.dirname(createRequire(localManifest).resolve('hd-garden/package.json'));
|
|
72
|
+
// These exported functions in the installed package are authoritative. No parser copy/fallback/download.
|
|
73
|
+
const {scanMarkdown,checkComposeFence}=await import(pathToFileURL(path.join(root,'ui/core/scan.mjs')).href);
|
|
74
|
+
const {scanFences}=await import(pathToFileURL(path.join(root,'ui/core/fence.mjs')).href);
|
|
75
|
+
const file=path.resolve(args[0]), source=fs.readFileSync(file,'utf8');
|
|
76
|
+
const blocks=scanMarkdown(source,{markdownPath:file});
|
|
77
|
+
const matches=blocks.filter(b=>card.id?b.id===card.id:b.startLine===card.at);
|
|
78
|
+
if (matches.length!==1) throw new Error('Constraint locator must match exactly one block');
|
|
79
|
+
const block=matches[0];
|
|
80
|
+
if (block.hash!==card.base) throw new Error('Stale constraint base; rescan before checking');
|
|
81
|
+
const fence=scanFences(source).find(f=>f.startLine===block.startLine);
|
|
82
|
+
const parsed=checkComposeFence(fence,{markdownPath:file});
|
|
83
|
+
if (!parsed.ok) { console.log(JSON.stringify({ok:false,diagnostics:parsed.diagnostics,browserRequired:true},null,2)); process.exitCode=1; return; }
|
|
84
|
+
const result=inspectLayout(parsed.doc,card);
|
|
85
|
+
console.log(JSON.stringify(result,null,2)); process.exitCode=result.ok?0:1;
|
|
86
|
+
}
|
|
87
|
+
if (process.argv[1] && import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href) main().catch(error=>{console.error(JSON.stringify({ok:false,code:'layout-input',message:error.message}));process.exitCode=2;});
|
|
@@ -110,6 +110,7 @@ export function PostReadingView({
|
|
|
110
110
|
) : null}
|
|
111
111
|
</header>
|
|
112
112
|
<div className="article-reading-layout">
|
|
113
|
+
<TableOfContents headings={headings} />
|
|
113
114
|
<div className="article-reading-column">
|
|
114
115
|
<ArticleMeasure />
|
|
115
116
|
<div className="e-content">
|
|
@@ -125,7 +126,6 @@ export function PostReadingView({
|
|
|
125
126
|
/>
|
|
126
127
|
<Comments slug={commentsSlug} />
|
|
127
128
|
</div>
|
|
128
|
-
<TableOfContents headings={headings} />
|
|
129
129
|
</div>
|
|
130
130
|
</article>
|
|
131
131
|
</main>
|
package/app/styles/a2.css
CHANGED
|
@@ -274,7 +274,7 @@ body {
|
|
|
274
274
|
--rail: 200px;
|
|
275
275
|
--gap: 72px;
|
|
276
276
|
display: grid;
|
|
277
|
-
grid-template-columns: minmax(0,
|
|
277
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, var(--col)) minmax(0, 1fr);
|
|
278
278
|
column-gap: var(--gap);
|
|
279
279
|
justify-content: center;
|
|
280
280
|
align-items: start;
|
|
@@ -300,6 +300,8 @@ body {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
.post-reading-page .article-reading-layout > .toc {
|
|
303
|
+
min-width: 0;
|
|
304
|
+
overflow-wrap: anywhere;
|
|
303
305
|
grid-column: 3;
|
|
304
306
|
grid-row: 1;
|
|
305
307
|
margin-top: 0;
|
|
@@ -1582,6 +1584,16 @@ html[data-theme="dark"] .g-quiz-out.ok { color: #8fd1a0; }
|
|
|
1582
1584
|
html[data-theme="dark"] .g-quiz-out.no { color: #f29aab; }
|
|
1583
1585
|
.g-quiz-ex { margin-top: 8px; font-size: 15px; color: var(--soft); }
|
|
1584
1586
|
.g-play { border: 1px solid var(--rule); border-radius: var(--r); padding: 14px 16px; margin: 24px 0; background: transparent; }
|
|
1587
|
+
/* Fullscreen controls own a top band, not a second frame or a narrow text rail. */
|
|
1588
|
+
.ux-fs > .ux-fsbar + .g-play { padding: 44px 0 0; border: 0; }
|
|
1589
|
+
.g-play > .garden-ui { margin: 0; }
|
|
1590
|
+
@media (max-width: 900px) {
|
|
1591
|
+
.post-reading-page .toc-mobile-trigger,
|
|
1592
|
+
.post-reading-page .toc-nojs-trigger {
|
|
1593
|
+
position: static;
|
|
1594
|
+
margin: 0 0 16px;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1585
1597
|
.g-play-f { margin: 0 0 10px; font-size: 14px; color: var(--soft); }
|
|
1586
1598
|
.g-range { display: grid; grid-template-columns: 110px minmax(0, 1fr) 64px; gap: 12px; align-items: center; font-size: 14px; color: var(--muted); margin: 6px 0; }
|
|
1587
1599
|
.g-range input { width: 100%; accent-color: var(--blue); min-height: 32px; }
|