dsh-structured 0.0.0-stage → 0.7.4

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,133 @@
1
+ # 视觉排版方案(dsh-structured v0.3)
2
+
3
+ ## 目标平台
4
+
5
+ **desktop 是主目标,本会话就运行在 dsh desktop 中。** 视觉层的设计、目检和验收都以 desktop 为准;Web 客户端只做兼容 smoke,不作为设计目标。`dsh.client.platform: 'web'` 是 DSH 客户端模块的格式,desktop 的 Web UI 也加载它,不代表 Web 优先。
6
+
7
+ ## 结论
8
+
9
+ 结构化只解决“信息顺序”,视觉层解决“扫读成本”。方案是三层,不动 markdown 渲染器、不接管 GenUI:
10
+
11
+ 1. **结构契约**(已有):首句结论 + 表格/清单/步骤 + 500 字上限,host 每请求注入。
12
+ 2. **视觉层**(本次落地):用客户端 CSS + 轻量 DOM 属性,给标题、表格、代码块、引用、列表、链接做统一排版;主题色全部走 `--dsw-alias-*`,自动跟随明暗主题。
13
+ 3. **组件层**(交接 GenUI):需要交互、图表、可排序表格时,用 ` ```dsh-ui ` fence 交给 `dsh-genui`;视觉层只美化它周围的普通 markdown。
14
+
15
+ 视觉方向定为 **「工程蓝图」一套**:蓝色强调、等宽小标签、清晰网格、明暗自动跟随。**不提供主题选择/主题菜单**;需要个性化时只改 `style.md` 的文字规则,视觉方案保持统一。
16
+
17
+ ## 现状与差距
18
+
19
+ 当前回复只有 markdown 原生样式:`h1–h6` 字号差小、表格无表头层次/斑马纹、代码块只有一条语言 banner、引用块是默认灰边。截图里“已完成 / 表格 / 引用”三类内容视觉权重几乎一样,读者要靠读文字找层级。
20
+
21
+ 目标不是“花”,是**一眼分层**:
22
+
23
+ - 标题能扫出章节;
24
+ - 表格能扫出表头、行、重点列;
25
+ - 代码块能一眼看出语言、边界、可复制;
26
+ - 风险/验收/结论这类语义块有颜色,但不超过 3 种语义色。
27
+
28
+ ## 参考项目(市场实测)
29
+
30
+ | 项目 | 做法 | 借什么 / 不借什么 |
31
+ | --- | --- | --- |
32
+ | `@changfenhuang/dsh-genui` 0.13.0(已装) | ` ```dsh-ui ` fence → 浏览器 React 组件;有 display typography、stat card、表格、图表、callout 等组件 | 借“组件优先”的分工和设计 token 思路;**不**把普通 markdown 全改写成组件,避免每条回复都生成 JSON |
33
+ | `dsh-markdown-xyy` 2.1.5(市场,theme 类) | 原生配色增强 + 4 套明暗主题 + 用户自定义 CSS | 借“主题预设 + 用户 CSS 覆盖”的产品形态;实现上仍用我们的 scoped CSS,不替换宿主 renderer |
34
+ | `dsh-md-render` 0.5.2(市场,docs 类) | 每个 fenced block 自己渲染:语言 banner + highlight.js | 借代码块“语言 banner + 可复制”的信息结构;宿主已有高亮 HTML 时只加颜色/边框,不重复打包 highlighter |
35
+ | `dsh-message-copy-enhance` 0.2.1 | 复制选区保留 Markdown / 代码语言 | 借代码块复制按钮的交互;P2 再做,先保证视觉 |
36
+ | `dsh-concise` 0.11.3(参考实现) | 客户端扫描 `[class*="_markdown"]` + MutationObserver,给 blockquote 打 class | 借“只加 class/attribute、不搬 React 节点”的稳定做法;流式期间延迟 DOM 改写,避免冻结 |
37
+
38
+ 宿主 markdown 的可用锚点(实测):`[class*="_markdown_"]`、`.md-code-block`、`[data-code-block-banner]`、`[data-code-wrap]`、`[data-line-numbers]`、`h1–h6`、`table`、`blockquote`、`markdown-hr`。主题 token:`--dsw-alias-bg-layer-1/2`、`--dsw-alias-border-l2`、`--dsw-alias-brand-primary`、`--dsw-alias-label-primary/secondary`、`--dsw-alias-state-warn/success/error-primary`。
39
+
40
+ ## 设计系统
41
+
42
+ ### 字号与节奏
43
+
44
+ | 元素 | 规格 |
45
+ | --- | --- |
46
+ | 正文 | 15px / 1.72,段间距 0.72em,中文 `letter-spacing:.01em` |
47
+ | H1 | 25px / 700,下边框分隔,上 30px 下 12px |
48
+ | H2 | 20px / 700,左侧 3px 强调条,上 26px 下 10px |
49
+ | H3 | 16.5px / 650,上 20px 下 8px |
50
+ | H4 | 15px / 650 |
51
+ | H5/H6 | 13px / 700,大写 + 字距,作 eyebrow 用 |
52
+ | 表格 | 14px,表头 650,单元格 9×12px |
53
+ | 代码 | 13px / 1.65,行内代码 0.9em |
54
+
55
+ ### 颜色角色
56
+
57
+ 只定义 5 个语义角色,全部从宿主 token 派生,不写死明暗两套:
58
+
59
+ | 角色 | 变量 | 用途 |
60
+ | --- | --- | --- |
61
+ | 强调 | `--ds-accent` ← `--dsw-alias-brand-primary` | 标题条、表头悬停、链接、图标底 |
62
+ | 正文 | `--ds-text` ← `--dsw-alias-label-primary` | 标题/正文 |
63
+ | 次要 | `--ds-muted` ← `--dsw-alias-label-secondary` | banner、eyebrow、辅助 |
64
+ | 边界 | `--ds-border` ← `--dsw-alias-border-l2` | 表格/代码块/卡片 |
65
+ | 语义 | `--dsw-alias-state-warn/success/error-primary` | 风险/验收/错误 callout |
66
+
67
+ ### 组件规格
68
+
69
+ - **标题图标**:H2/H3 按关键词映射(结论 ◆、方案 ▸、风险 ⚠、验收 ✓、对比 ⇄、下一步 →、背景 ◎、指标 ▦),放在 22px 圆角底块里;H1 不加图标,避免层级噪音。
70
+ - **表格**:1px 圆角外框、表头 `bg-layer-2`、斑马纹 55% 透明、悬停 7% 强调色、最后一行去底边;窄屏降字号/内边距。
71
+ - **代码块**:12px 圆角、1px 边框、banner 渐变底 + 语言色圆点;语言色:JS 黄、TS 蓝、Python 绿、Bash 青、JSON 紫、HTML/CSS 橙、SQL 青蓝,未知走品牌色。复用宿主高亮结果,不新增 highlighter。
72
+ - **引用块**:默认 3px 强调左边 + 6% 强调底;`风险/注意/警告` → warn 色,`验收/通过/成功` → success 色,`结论/判断/答案` → info 色;`> **结论:**` 升级为带边框和渐变的结论卡。
73
+ - **列表**:`::marker` 用强调色,项间距 0.28em;**链接**:强调色 + 2px 下划线偏移;**HR**:两侧透明的渐变细线。
74
+ - **无障碍**:所有颜色来自主题 token;`prefers-reduced-motion` 关闭过渡;不改变字号以外的布局;不隐藏任何文本。
75
+
76
+ ## 架构
77
+
78
+ ```text
79
+ host: systemPrompt.section (结构规则) → 已有,不动
80
+ client: 注入 scoped CSS → [class*="_markdown_"] 下的 h/table/code/blockquote
81
+ client: 轻量 DOM 属性 → 标题图标 / callout 色调 / 代码语言色
82
+ client: 结论卡 → blockquote 前缀匹配
83
+ GenUI: ```dsh-ui fence → 交互组件;视觉层不接管
84
+ ```
85
+
86
+ 原则:
87
+
88
+ - **CSS 优先**:能靠选择器解决的绝不做 DOM 改写;只有“按文本给语义”才加属性。
89
+ - **不搬 React 节点**:只加 class/attribute,避免 dsh-concise 踩过的流式冻结;MutationObserver + rAF 合批。
90
+ - **可回退**:所有选择器命中失败时只是“原生 markdown”,不会报错;`ctx.effect` 失败时 dispose 并降级。
91
+ - **主题跟随**:不写死浅色/深色,用 `color-mix()` 从宿主 token 派生透明度和强调底。
92
+ - **流式安全**:只改属性不改文本;重渲染后重新扫描,幂等。
93
+
94
+ ## 阶段计划
95
+
96
+ ### P0(已落地,v0.3.1)
97
+
98
+ CSS 视觉层 + 标题关键词图标 + callout 三色 + 代码语言色 + 结论卡;已装 desktop,可即时看效果。验收:标题/表格/代码/引用四类在明暗主题下都不溢出、不遮挡;DOM 只新增属性;`node --check` 通过;22/22 测试。
99
+
100
+ ### P1(1–2 天):单一视觉方案定稿与回归
101
+
102
+ - 锁定一套 token/字号/间距/卡片样式,不提供 `theme` 命令或主题菜单;`/structured on|off` 是唯一开关。
103
+ - 明暗由宿主 `--dsw-alias-*` 自动跟随;补对比度、密度、窄屏和 `prefers-reduced-motion` 回归。
104
+ - 验收:同一套视觉在明/暗、desktop 窗口宽度下都不溢出、不遮挡;没有主题选择入口。
105
+
106
+ ### P2(2–3 天):代码块与表格交互
107
+
108
+ - 代码块:复制按钮、语言切换(纯文本/高亮)、行号开关;复用宿主高亮,不打包 highlighter。
109
+ - 表格:横向滚动容器、复制为 Markdown/CSV、数值列右对齐(按列内容检测);仍不搬 React 节点。
110
+ - 标题:hover 锚点链接(复制 `#标题`),用于长文导航。
111
+ - 验收:复制内容与源码一致;流式 30 秒不冻结;长表格 50 行不抖动。
112
+
113
+ ### P3(2 天):GenUI 分工与质量门
114
+
115
+ - 更新 host 注入规则:>8 行表格、图表、可交互表单 → `dsh-ui`;普通说明 → markdown。
116
+ - 视觉回归:12 条固定 prompt × 明/暗截图;检查对比度 AA、无横向溢出、无控制台错误。
117
+ - 打包:主题预览图、README、市场描述;`dsh.client` 仍需 `platform: web`。
118
+
119
+ ## 风险与对策
120
+
121
+ | 风险 | 对策 |
122
+ | --- | --- |
123
+ | 宿主内部 class 名变化(`_markdown_`、`md-code-block`) | 选择器用 `[class*="_markdown_"]` + `.md-code-block` 双保险;每次升级跑视觉回归;命中失败自动退化为原生样式 |
124
+ | 流式期间 DOM 改写导致内容冻结 | 只加属性,不改文本/节点;MutationObserver + rAF 合批;不在流式中间做 wrapper |
125
+ | 主题 token 不覆盖代码高亮色 | 复用宿主 `--dsw-alias-markdown-code-block*`(如果存在),否则用 `color-mix` 派生;不写死 hex 作为唯一来源 |
126
+ | 视觉层让回复变长/变重 | CSS 不增加内容;组件只加边框/间距;P3 用截图和 DOM 节点数做回归 |
127
+ | 用户想要不同风格 | 不提供主题菜单;视觉方案保持一套;文字规则用 `style.md` 覆盖;未来如扩展另开 opt-in 包,不进入主插件 |
128
+
129
+ ## 已定
130
+
131
+ - 视觉方案:一套「工程蓝图」,不提供主题选择。
132
+ - 唯一开关:`/structured on|off`;不增加 `theme` 命令或 `visual` 配置。
133
+ - 个性化:`style.md` 只覆盖文字规则;视觉方案保持统一。
package/icon.svg ADDED
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Structured output">
2
+ <rect x="2" y="2" width="60" height="60" rx="14" fill="#1f2937"/>
3
+ <rect x="12" y="13" width="40" height="7" rx="3.5" fill="#f9fafb"/>
4
+ <rect x="12" y="26" width="18" height="10" rx="3" fill="#60a5fa"/>
5
+ <rect x="34" y="26" width="18" height="10" rx="3" fill="#93c5fd"/>
6
+ <rect x="12" y="40" width="10" height="10" rx="3" fill="#34d399"/>
7
+ <rect x="26" y="40" width="12" height="10" rx="3" fill="#a7f3d0"/>
8
+ <rect x="42" y="40" width="10" height="10" rx="3" fill="#34d399"/>
9
+ <path d="M15 44.5l2.2 2.4 4.2-5" fill="none" stroke="#064e3b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
10
+ </svg>
package/lib/client.js ADDED
@@ -0,0 +1,467 @@
1
+ // Generated from src/client.js by scripts/build.mjs — edit src/, then run `npm run build`.
2
+ // dsh-structured client module: renders "结论:" blockquotes as cards.
3
+ /**
4
+ * dsh-structured client
5
+ *
6
+ * 两层,都是纯 DOM 增强,不改消息内容、不替换 app root:
7
+ * 1) 对话 markdown 视觉层:标题字号/层级、表格、代码块、引用块、列表、链接的排版与配色。
8
+ * 2) 语义装饰:结论卡片、风险/验收等 callout 色调、标题关键词图标、代码块语言色。
9
+ *
10
+ * 只给宿主已有节点加 class/attribute,不搬 React 节点;markdown 重渲染后由 MutationObserver
11
+ * 重新扫描。主题色全部走 --dsw-alias-*,自动跟随明暗主题。
12
+ */
13
+ window.__ModuleLoader__.load({
14
+ id: 'dsh-structured',
15
+ factory() {
16
+ const CARD_ATTR = 'data-dsh-structured-card'
17
+ const CARD_TYPE_ATTR = 'data-dsh-structured-card-type'
18
+ const ICON_ATTR = 'data-dsh-structured-icon'
19
+ const TONE_ATTR = 'data-dsh-structured-tone'
20
+ const LANG_ATTR = 'data-dsh-structured-lang'
21
+ const STATUS_ATTR = 'data-dsh-structured-status'
22
+ const STATUS_TABLE_ATTR = 'data-dsh-structured-status-table'
23
+ const LEAD_ATTR = 'data-dsh-structured-lead'
24
+ const ROW_ATTR = 'data-dsh-structured-row'
25
+ const ROOT_ATTR = 'data-dsh-structured'
26
+ const STYLE_ID = 'dsh-structured-skin-style'
27
+ const LEGACY_STYLE_ID = 'dsh-structured-card-style'
28
+
29
+ const CARD_TYPES = [
30
+ [/^结论|^总结|^答案/, 'conclusion'],
31
+ [/^判断|^决策/, 'decision'],
32
+ [/^指标|^数据/, 'metric'],
33
+ [/^风险|^注意|^警告/, 'risk'],
34
+ [/^下一步|^行动|^待办/, 'next'],
35
+ [/^验收|^通过|^完成/, 'accept'],
36
+ [/^重点|^关键/, 'key'],
37
+ ]
38
+ const ICONS = [
39
+ [/结论|总结|答案|判断/, '◆'],
40
+ [/方案|计划|步骤|流程/, '▸'],
41
+ [/风险|注意|警告|坑/, '⚠'],
42
+ [/验收|检查|核对/, '✓'],
43
+ [/对比|选型|取舍/, '⇄'],
44
+ [/下一步|行动|待办/, '→'],
45
+ [/背景|原因|根因/, '◎'],
46
+ [/指标|数据|结果/, '▦'],
47
+ ]
48
+ const TONES = [
49
+ [/风险|注意|警告|坑/, 'warn'],
50
+ [/验收|通过|成功/, 'success'],
51
+ [/重点|关键|核心/, 'info'],
52
+ [/下一步|行动|待办/, 'info'],
53
+ [/结论|判断|答案/, 'info'],
54
+ ]
55
+
56
+ const CSS = `
57
+ [class*="_markdown_"]{
58
+ --ds-accent:#2563eb;
59
+ --ds-text:var(--dsw-alias-label-primary,#111827);
60
+ --ds-muted:var(--dsw-alias-label-secondary,#6b7280);
61
+ --ds-border:var(--dsw-alias-border-l2,rgba(127,127,127,.22));
62
+ --ds-surface:var(--dsw-alias-bg-layer-1,#fff);
63
+ --ds-surface-2:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06));
64
+ font-size:15px;line-height:1.72;letter-spacing:.01em;
65
+ }
66
+ [data-theme="dark"] [class*="_markdown_"]{--ds-accent:#60a5fa}
67
+ [class*="_markdown_"] p{margin:.72em 0}
68
+ [class*="_markdown_"] h1,[class*="_markdown_"] h2,[class*="_markdown_"] h3,
69
+ [class*="_markdown_"] h4,[class*="_markdown_"] h5,[class*="_markdown_"] h6{
70
+ color:var(--ds-text);line-height:1.32;letter-spacing:-.01em;
71
+ }
72
+ [class*="_markdown_"] h1{font-size:26px;font-weight:700;margin:32px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--ds-border)}
73
+ [class*="_markdown_"] h2{font-size:21px;font-weight:700;margin:28px 0 10px;padding-left:12px;border-left:3px solid var(--ds-accent)}
74
+ [class*="_markdown_"] h3{font-size:17px;font-weight:650;margin:22px 0 8px}
75
+ [class*="_markdown_"] h4{font-size:15px;font-weight:650;margin:16px 0 6px}
76
+ [class*="_markdown_"] h5,[class*="_markdown_"] h6{font-size:13px;font-weight:700;margin:14px 0 6px;color:var(--ds-muted);text-transform:uppercase;letter-spacing:.06em}
77
+ [class*="_markdown_"] h2[${ICON_ATTR}]::before,
78
+ [class*="_markdown_"] h3[${ICON_ATTR}]::before{
79
+ content:attr(${ICON_ATTR});
80
+ display:inline-flex;align-items:center;justify-content:center;
81
+ width:22px;height:22px;margin-right:8px;border-radius:6px;
82
+ background:color-mix(in srgb,var(--ds-accent) 12%,transparent);
83
+ color:var(--ds-accent);font-size:12px;font-weight:700;vertical-align:2px;
84
+ }
85
+ [class*="_markdown_"] strong{font-weight:700;color:var(--ds-text)}
86
+ [class*="_markdown_"] a{color:var(--ds-accent);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--ds-accent) 38%,transparent)}
87
+ [class*="_markdown_"] a:hover{border-bottom-color:var(--ds-accent)}
88
+ [class*="_markdown_"] ul,[class*="_markdown_"] ol{padding-left:1.5em;margin:.6em 0}
89
+ [class*="_markdown_"] li{margin:.28em 0}
90
+ [class*="_markdown_"] li::marker{color:var(--ds-accent)}
91
+ [class*="_markdown_"] hr{height:1px;border:0;margin:28px 0;background:linear-gradient(90deg,transparent,var(--ds-border),transparent)}
92
+ [class*="_markdown_"] table{
93
+ width:100%;margin:18px 0;border-collapse:separate;border-spacing:0;
94
+ border:1px solid var(--ds-border);border-radius:12px;overflow:hidden;font-size:13.5px;
95
+ box-shadow:0 1px 2px rgba(0,0,0,.03);
96
+ }
97
+ [class*="_markdown_"] thead th{
98
+ background:var(--ds-surface-2);
99
+ font-size:11px!important;letter-spacing:.07em!important;color:var(--ds-muted);
100
+ font-weight:700!important;text-align:left;padding:12px 18px!important;
101
+ border-bottom:1px solid var(--ds-border);
102
+ }
103
+ [class*="_markdown_"] tbody td{
104
+ padding:13px 18px!important;line-height:1.55;vertical-align:middle;border-left:0;border-right:0;
105
+ border-bottom:1px solid color-mix(in srgb,var(--ds-border) 55%,transparent);
106
+ word-break:break-word;
107
+ }
108
+ [class*="_markdown_"] tbody tr:nth-child(even){background:color-mix(in srgb,var(--ds-surface-2) 45%,transparent)}
109
+ [class*="_markdown_"] tbody tr:hover{background:color-mix(in srgb,var(--ds-accent) 5%,transparent)}
110
+ [class*="_markdown_"] tbody tr:last-child td{border-bottom:0}
111
+ [class*="_markdown_"] table:has(thead th:nth-child(2):last-child) th:first-child,
112
+ [class*="_markdown_"] table:has(thead th:nth-child(2):last-child) td:first-child{width:30%}
113
+ [class*="_markdown_"] table:not([${STATUS_TABLE_ATTR}]):has(thead th:nth-child(3):last-child) th:first-child,
114
+ [class*="_markdown_"] table:not([${STATUS_TABLE_ATTR}]):has(thead th:nth-child(3):last-child) td:first-child{width:24%}
115
+ [class*="_markdown_"] table:not([${STATUS_TABLE_ATTR}]):has(thead th:nth-child(3):last-child) th:nth-child(2),
116
+ [class*="_markdown_"] table:not([${STATUS_TABLE_ATTR}]):has(thead th:nth-child(3):last-child) td:nth-child(2){width:22%}
117
+ [class*="_markdown_"] table[${STATUS_TABLE_ATTR}] th:first-child,
118
+ [class*="_markdown_"] table[${STATUS_TABLE_ATTR}] td:first-child{
119
+ width:68px;text-align:center;padding:7px 8px!important;font-size:11px;font-weight:700;
120
+ white-space:nowrap;vertical-align:middle;border-radius:7px;
121
+ }
122
+ [class*="_markdown_"] table[${STATUS_TABLE_ATTR}] tbody tr[${ROW_ATTR}="success"] td:first-child{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 12%,transparent);color:var(--dsw-alias-state-success-primary,#16a34a)}
123
+ [class*="_markdown_"] table[${STATUS_TABLE_ATTR}] tbody tr[${ROW_ATTR}="warn"] td:first-child{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d97706) 12%,transparent);color:var(--dsw-alias-state-warn-primary,#d97706)}
124
+ [class*="_markdown_"] table[${STATUS_TABLE_ATTR}] tbody tr[${ROW_ATTR}="error"] td:first-child{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 12%,transparent);color:var(--dsw-alias-state-error-primary,#dc2626)}
125
+ [class*="_markdown_"] p[${LEAD_ATTR}="table"]{
126
+ border-left:2px solid var(--ds-accent);padding-left:10px;margin:10px 0 8px;
127
+ font-size:14px;color:var(--ds-text);
128
+ }
129
+ [class*="_markdown_"] p[${LEAD_ATTR}="table"] strong{color:var(--ds-accent)}
130
+ [class*="_markdown_"] h2+table,[class*="_markdown_"] h2+p,[class*="_markdown_"] h3+table,[class*="_markdown_"] h3+p{margin-top:8px}
131
+ [class*="_markdown_"] :not(pre)>code{
132
+ background:var(--ds-surface-2);border:1px solid var(--ds-border);border-radius:6px;
133
+ padding:.12em .38em;font-size:.9em;color:var(--ds-accent);overflow-wrap:anywhere;
134
+ }
135
+ [class*="_markdown_"] .md-code-block{
136
+ margin:16px 0;border:1px solid var(--ds-border);border-radius:12px;overflow:hidden;
137
+ background:var(--ds-surface-2);box-shadow:0 1px 2px rgba(0,0,0,.04);
138
+ }
139
+ [class*="_markdown_"] .md-code-block > [class*="_bannerWrap"]{
140
+ margin:0;padding:0;border:0;background:none;
141
+ }
142
+ [class*="_markdown_"] .md-code-block [data-code-block-banner]{
143
+ display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0;border-radius:0;
144
+ padding:6px 12px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
145
+ color:var(--ds-muted);border-bottom:1px solid var(--ds-border);
146
+ background:linear-gradient(90deg,color-mix(in srgb,var(--ds-code-accent,var(--ds-accent)) 14%,transparent),transparent 72%);
147
+ }
148
+ [class*="_markdown_"] .md-code-block [data-code-block-banner] > [class*="_heading"]{
149
+ flex:1;min-width:0;justify-content:flex-start;text-align:left;
150
+ }
151
+ [class*="_markdown_"] .md-code-block [data-code-block-banner] > [class*="_actions"]{
152
+ margin-left:auto;flex:none;
153
+ }
154
+ [class*="_markdown_"] .md-code-block > [data-code-block-content]{padding:0}
155
+ [class*="_markdown_"] .md-code-block [data-code-block-banner]::before{
156
+ content:"";width:8px;height:8px;border-radius:50%;flex:none;
157
+ background:var(--ds-code-accent,var(--ds-accent));
158
+ box-shadow:0 0 0 3px color-mix(in srgb,var(--ds-code-accent,var(--ds-accent)) 18%,transparent);
159
+ }
160
+ [class*="_markdown_"] .md-code-block pre{margin:0;padding:14px 16px;font-size:13px;line-height:1.65;overflow:auto}
161
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="js"]{--ds-code-accent:#eab308}
162
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="ts"],
163
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="tsx"]{--ds-code-accent:#3b82f6}
164
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="py"],
165
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="python"]{--ds-code-accent:#22c55e}
166
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="bash"],
167
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="sh"],
168
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="powershell"]{--ds-code-accent:#14b8a6}
169
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="json"]{--ds-code-accent:#a855f7}
170
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="html"],
171
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="css"]{--ds-code-accent:#f97316}
172
+ [class*="_markdown_"] .md-code-block[data-dsh-structured-lang="sql"]{--ds-code-accent:#06b6d4}
173
+ [class*="_markdown_"] blockquote{
174
+ margin:14px 0;padding:10px 14px;border-left:3px solid var(--ds-accent);
175
+ background:color-mix(in srgb,var(--ds-accent) 6%,transparent);border-radius:0 10px 10px 0;
176
+ }
177
+ [class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]{
178
+ position:relative;border-left:4px solid var(--ds-accent);border-radius:12px;
179
+ padding:14px 16px 14px 38px;
180
+ background:linear-gradient(135deg,color-mix(in srgb,var(--ds-accent) 7%,var(--ds-surface)),var(--ds-surface) 62%);
181
+ box-shadow:0 1px 2px rgba(0,0,0,.04),0 12px 32px -24px color-mix(in srgb,var(--ds-accent) 32%,transparent);
182
+ }
183
+ [class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]::before{
184
+ content:"◆";position:absolute;left:14px;top:15px;
185
+ color:var(--ds-accent);font-size:13px;line-height:1;
186
+ }
187
+ [class*="_markdown_"] blockquote[${LEAD_ATTR}="lead"]>p:first-child{
188
+ margin-top:0;font-size:16.5px;line-height:1.6;font-weight:600;color:var(--ds-text);
189
+ }
190
+ [class*="_markdown_"] blockquote[${TONE_ATTR}="warn"]{--ds-accent:var(--dsw-alias-state-warn-primary,#d97706)}
191
+ [class*="_markdown_"] blockquote[${TONE_ATTR}="success"]{--ds-accent:var(--dsw-alias-state-success-primary,#16a34a)}
192
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]{
193
+ border:1px solid color-mix(in srgb,var(--ds-accent) 16%,var(--ds-border));
194
+ border-left:3px solid var(--ds-accent);
195
+ border-radius:12px;padding:14px 16px 15px;
196
+ background:linear-gradient(135deg,color-mix(in srgb,var(--ds-accent) 6%,var(--ds-surface)),var(--ds-surface) 60%);
197
+ box-shadow:0 1px 2px rgba(0,0,0,.04),0 12px 32px -24px color-mix(in srgb,var(--ds-accent) 32%,transparent);
198
+ }
199
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:first-child>strong:first-child{
200
+ display:flex;align-items:center;gap:9px;position:relative;
201
+ font-size:13px;line-height:1.1;letter-spacing:.06em;text-transform:uppercase;
202
+ color:var(--ds-accent);margin-bottom:8px;
203
+ }
204
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:first-child>strong:first-child::before{
205
+ content:"";display:inline-flex;flex:none;
206
+ width:26px;height:26px;border-radius:7px;
207
+ background:color-mix(in srgb,var(--ds-accent) 12%,transparent);
208
+ }
209
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:first-child>strong:first-child::after{
210
+ content:"";position:absolute;left:5.5px;top:5.5px;width:15px;height:15px;
211
+ background-color:var(--ds-accent);
212
+ -webkit-mask:var(--ds-card-icon) center/contain no-repeat;
213
+ mask:var(--ds-card-icon) center/contain no-repeat;
214
+ }
215
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:nth-child(2)>strong:first-child{
216
+ display:block;font-size:16px;font-weight:700;line-height:1.45;
217
+ letter-spacing:-.01em;color:var(--ds-text);margin-bottom:4px;
218
+ }
219
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p{font-size:15.5px;line-height:1.7;font-weight:450;color:var(--ds-text)}
220
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p strong{font-weight:700;color:var(--ds-text)}
221
+ [class*="_markdown_"] blockquote[${TONE_ATTR}]>p:first-child>strong:first-child{color:var(--ds-accent)}
222
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="conclusion"]{
223
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l9 9-9 9-9-9 9-9z' fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round'/%3E%3C/svg%3E");
224
+ }
225
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="decision"]{
226
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 7h6l4 10h6M4 17h6l4-10h6' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
227
+ }
228
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="metric"]{
229
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 20V10M12 20V4M19 20v-7' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
230
+ }
231
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="risk"]{
232
+ --ds-accent:var(--dsw-alias-state-warn-primary,#d97706);
233
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 4l9 16H3L12 4z' fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round'/%3E%3Cpath d='M12 10v4M12 17v.5' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
234
+ }
235
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="next"]{
236
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
237
+ }
238
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="accept"]{
239
+ --ds-accent:var(--dsw-alias-state-success-primary,#16a34a);
240
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 12.5l5 5L20 6.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
241
+ }
242
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="key"]{
243
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l2.6 6.2 6.7.5-5.1 4.4 1.6 6.5L12 17.3 6.2 20.6l1.6-6.5L2.7 9.7l6.7-.5L12 3z' fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E");
244
+ }
245
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="statement"]{
246
+ --ds-card-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l9 9-9 9-9-9 9-9z' fill='none' stroke='black' stroke-width='1.8' stroke-linejoin='round'/%3E%3C/svg%3E");
247
+ }
248
+ [class*="_markdown_"] blockquote[${CARD_TYPE_ATTR}="statement"]>p:first-child>strong:first-child{
249
+ font-size:16px;line-height:1.45;letter-spacing:-.01em;text-transform:none;
250
+ color:var(--ds-text);margin-bottom:4px;
251
+ }
252
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:first-child{margin-top:0}
253
+ [class*="_markdown_"] blockquote[${CARD_ATTR}="1"]>p:last-child{margin-bottom:0}
254
+ [class*="_markdown_"] p[${STATUS_ATTR}],[class*="_markdown_"] li[${STATUS_ATTR}]{
255
+ border-left:3px solid var(--ds-accent);background:color-mix(in srgb,var(--ds-accent) 7%,transparent);
256
+ border-radius:0 6px 6px 0;padding:4px 10px;margin:.4em 0;
257
+ }
258
+ [class*="_markdown_"] p[${STATUS_ATTR}="success"],[class*="_markdown_"] li[${STATUS_ATTR}="success"]{--ds-accent:var(--dsw-alias-state-success-primary,#16a34a)}
259
+ [class*="_markdown_"] p[${STATUS_ATTR}="warn"],[class*="_markdown_"] li[${STATUS_ATTR}="warn"]{--ds-accent:var(--dsw-alias-state-warn-primary,#d97706)}
260
+ [class*="_markdown_"] p[${STATUS_ATTR}="error"],[class*="_markdown_"] li[${STATUS_ATTR}="error"]{--ds-accent:var(--dsw-alias-state-error-primary,#dc2626)}
261
+ @media (max-width:720px){
262
+ [class*="_markdown_"]{font-size:14.5px}
263
+ [class*="_markdown_"] h1{font-size:22px}
264
+ [class*="_markdown_"] h2{font-size:18px}
265
+ [class*="_markdown_"] table{font-size:13px}
266
+ [class*="_markdown_"] thead th,[class*="_markdown_"] tbody td{padding:7px 9px}
267
+ }
268
+ @media (prefers-reduced-motion:reduce){
269
+ [class*="_markdown_"] *{transition:none!important;animation:none!important}
270
+ }
271
+ `
272
+
273
+ function iconFor(text) {
274
+ for (const [pattern, icon] of ICONS) if (pattern.test(text)) return icon
275
+ return null
276
+ }
277
+
278
+ function cardTypeFor(text) {
279
+ for (const [pattern, type] of CARD_TYPES) if (pattern.test(text)) return type
280
+ return null
281
+ }
282
+
283
+ function inGenui(element) {
284
+ try {
285
+ return Boolean(element.closest?.('[data-genui], [class*="genui-root"]'))
286
+ } catch {
287
+ return false
288
+ }
289
+ }
290
+
291
+ function toneFor(text) {
292
+ for (const [pattern, tone] of TONES) if (pattern.test(text)) return tone
293
+ return null
294
+ }
295
+
296
+ function statusFor(text) {
297
+ if (/^✅/.test(text)) return 'success'
298
+ if (/^⚠/.test(text)) return 'warn'
299
+ if (/^❌/.test(text)) return 'error'
300
+ return null
301
+ }
302
+
303
+ function isFirstBlockquote(block) {
304
+ const markdown = block.closest?.('[class*="_markdown_"]')
305
+ return Boolean(markdown && markdown.querySelector('blockquote') === block)
306
+ }
307
+
308
+ function isStatementCard(block) {
309
+ const markdown = block.closest?.('[class*="_markdown_"]')
310
+ if (!markdown || markdown.querySelector('blockquote') !== block) return false
311
+ const paragraphs = [...block.children].filter((element) => element.tagName === 'P')
312
+ if (paragraphs.length < 2) {
313
+ const first = paragraphs[0]
314
+ const strong = first?.firstElementChild
315
+ if (!strong || strong.tagName !== 'STRONG') return false
316
+ const rest = first.textContent.slice(strong.textContent.length).trim()
317
+ return rest.length > 0 || Boolean(first.querySelector('br'))
318
+ }
319
+ const first = paragraphs[0]
320
+ return Boolean(first?.firstElementChild && first.firstElementChild.tagName === 'STRONG')
321
+ }
322
+
323
+ function decorate(root) {
324
+ for (const heading of root.querySelectorAll('[class*="_markdown_"] h2, [class*="_markdown_"] h3')) {
325
+ if (inGenui(heading)) continue
326
+ const icon = iconFor((heading.textContent ?? '').trim())
327
+ if (icon === null) heading.removeAttribute(ICON_ATTR)
328
+ else heading.setAttribute(ICON_ATTR, icon)
329
+ }
330
+
331
+ for (const block of root.querySelectorAll('blockquote')) {
332
+ if (inGenui(block)) continue
333
+ const text = (block.textContent ?? '').trimStart()
334
+ let type = cardTypeFor(text)
335
+ if (type === null && isStatementCard(block)) type = 'statement'
336
+ if (type === null) {
337
+ block.removeAttribute(CARD_ATTR)
338
+ block.removeAttribute(CARD_TYPE_ATTR)
339
+ if (isFirstBlockquote(block)) block.setAttribute(LEAD_ATTR, 'lead')
340
+ else block.removeAttribute(LEAD_ATTR)
341
+ const tone = toneFor(text)
342
+ if (tone === null) block.removeAttribute(TONE_ATTR)
343
+ else block.setAttribute(TONE_ATTR, tone)
344
+ continue
345
+ }
346
+ block.setAttribute(CARD_ATTR, '1')
347
+ block.setAttribute(CARD_TYPE_ATTR, type)
348
+ block.removeAttribute(LEAD_ATTR)
349
+ block.setAttribute(TONE_ATTR, type === 'risk' ? 'warn' : type === 'accept' ? 'success' : 'info')
350
+ }
351
+
352
+ for (const element of root.querySelectorAll('[class*="_markdown_"] p, [class*="_markdown_"] li')) {
353
+ if (inGenui(element) || element.closest('blockquote, table')) {
354
+ element.removeAttribute(STATUS_ATTR)
355
+ continue
356
+ }
357
+ const status = statusFor((element.textContent ?? '').trimStart())
358
+ if (status === null) element.removeAttribute(STATUS_ATTR)
359
+ else element.setAttribute(STATUS_ATTR, status)
360
+ }
361
+
362
+ for (const row of root.querySelectorAll('[class*="_markdown_"] table tr')) {
363
+ if (inGenui(row) || row.closest('thead')) {
364
+ row.removeAttribute(ROW_ATTR)
365
+ continue
366
+ }
367
+ const status = statusFor((row.textContent ?? '').trimStart())
368
+ if (status === null) row.removeAttribute(ROW_ATTR)
369
+ else row.setAttribute(ROW_ATTR, status)
370
+ }
371
+
372
+ for (const table of root.querySelectorAll('[class*="_markdown_"] table')) {
373
+ if (inGenui(table)) continue
374
+ const rows = [...table.querySelectorAll('tbody tr')]
375
+ const statusTable =
376
+ rows.length > 0 &&
377
+ rows.every((row) => statusFor((row.cells?.[0]?.textContent ?? '').trimStart()) !== null)
378
+ if (statusTable) table.setAttribute(STATUS_TABLE_ATTR, '1')
379
+ else table.removeAttribute(STATUS_TABLE_ATTR)
380
+ }
381
+
382
+ for (const paragraph of root.querySelectorAll('[class*="_markdown_"] p')) {
383
+ if (inGenui(paragraph) || paragraph.closest('blockquote, table')) {
384
+ paragraph.removeAttribute(LEAD_ATTR)
385
+ continue
386
+ }
387
+ if (/^表格结论/.test((paragraph.textContent ?? '').trimStart())) paragraph.setAttribute(LEAD_ATTR, 'table')
388
+ else paragraph.removeAttribute(LEAD_ATTR)
389
+ }
390
+
391
+ for (const code of root.querySelectorAll('[class*="_markdown_"] .md-code-block')) {
392
+ if (inGenui(code)) continue
393
+ const banner = code.querySelector('[data-code-block-banner]')
394
+ const raw = (banner?.textContent ?? '').trim().split(/\s+/)[0] ?? ''
395
+ const lang = raw.toLowerCase().replace(/[^a-z0-9+#]/g, '')
396
+ if (lang.length === 0) code.removeAttribute(LANG_ATTR)
397
+ else code.setAttribute(LANG_ATTR, lang)
398
+ }
399
+ }
400
+
401
+ return {
402
+ inject: [],
403
+ apply(ctx) {
404
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
405
+ if (document.body === null) return
406
+
407
+ document.getElementById(LEGACY_STYLE_ID)?.remove()
408
+ document.getElementById(STYLE_ID)?.remove()
409
+ const style = document.createElement('style')
410
+ style.id = STYLE_ID
411
+ style.textContent = CSS
412
+ document.head.appendChild(style)
413
+ document.documentElement.setAttribute(ROOT_ATTR, 'active')
414
+ try {
415
+ console.info('[dsh-structured] visual layer active')
416
+ } catch {
417
+ // console 不可用时忽略。
418
+ }
419
+
420
+ let disposed = false
421
+ let scheduled = false
422
+ let pending = null
423
+ const run = () => {
424
+ if (disposed) return
425
+ scheduled = false
426
+ pending = null
427
+ decorate(document)
428
+ }
429
+ const schedule = () => {
430
+ if (disposed || scheduled) return
431
+ scheduled = true
432
+ if (typeof requestAnimationFrame === 'function') pending = requestAnimationFrame(run)
433
+ else pending = setTimeout(run, 16)
434
+ }
435
+ const observer = new MutationObserver(schedule)
436
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true })
437
+ run()
438
+
439
+ const dispose = () => {
440
+ if (disposed) return
441
+ disposed = true
442
+ observer.disconnect()
443
+ if (pending !== null) {
444
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(pending)
445
+ else clearTimeout(pending)
446
+ }
447
+ style.remove()
448
+ document.documentElement.removeAttribute(ROOT_ATTR)
449
+ for (const attr of [CARD_ATTR, CARD_TYPE_ATTR, ICON_ATTR, TONE_ATTR, LANG_ATTR, STATUS_ATTR, STATUS_TABLE_ATTR, LEAD_ATTR, ROW_ATTR]) {
450
+ for (const element of document.querySelectorAll('[' + attr + ']')) element.removeAttribute(attr)
451
+ }
452
+ try {
453
+ console.info('[dsh-structured] visual layer disposed')
454
+ } catch {
455
+ // console 不可用时忽略。
456
+ }
457
+ }
458
+ try {
459
+ ctx?.effect?.(() => dispose, 'dsh-structured: visual layer')
460
+ } catch {
461
+ dispose()
462
+ }
463
+ return dispose
464
+ },
465
+ }
466
+ },
467
+ })