base-paint 0.2.0 → 0.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.
package/dist/blocks.js ADDED
@@ -0,0 +1,862 @@
1
+ /** base-paint/blocks:#104 B1 区块组件整合(12 区块,接口 owner)。
2
+ *
3
+ * 地位(契约 `docs/base-paint-contract.md` §4.1/§4.4 #104 行):
4
+ * - 区块级 = 页面级组合:**组合**冻结控件(`renderEmptyState`/`renderActionBar`/
5
+ * `renderToast`/`renderErrorReceipt`/`renderStatusBadge`/`charts.*`),**不得**
6
+ * 重定义控件签名、不得复制控件实现、不得改 `CONTROL_*`/`STATUS_*`/`COPY_*` 常量;
7
+ * - **防火墙**:本模块**不在** `src/spec/`、**不进** `SPEC_FROZEN_SURFACE`、
8
+ * **不从** `src/index.ts` 导出(签名测试把 `dist/index.js` 出口与冻结清单绑死,
9
+ * 区块接口是可演进的组合层,不是冻结签名)。消费方走子路径 `base-paint/blocks`;
10
+ * - 清单裁定(DB-1):12 区块取 `docs/visual-spec-blocks.md` §1 拟定清单为终版,
11
+ * 只做命名收敛(英文键),锚点语义不变;
12
+ * - 样式闭集裁定(DB-2):新增 `BLOCK_STYLE_SECTIONS` 12 区闭集(**不改**
13
+ * `CONTROL_STYLE_SECTIONS`);区块 CSS 由本模块唯一产出者 `blocksCss()` 产出,
14
+ * 只读冻结 token(`CSS_VAR_TOKENS`)+ 局部 CSS 常量(圆角 `{8,14,20,999}`、
15
+ * `--line` 派生软线),**不新增 token 名**(D-5/DB-5)。
16
+ *
17
+ * 三条硬边界(违反即红):
18
+ * 1. **静态呈现 + 复制**(§4.1):12 个函数全是纯函数产 HTML 字符串;B-08 只产原生
19
+ * `<details>/<summary>`,B-09 只产静态 `label+input`(实时预览/空值拦截的
20
+ * **行为**归宿主=插件 client,B7;本模块只给宿主留 `data-*` 约定);
21
+ * 2. **禁交互控件名**(B7):`formPrompt`/`selectList`/`smartSelect`/`confirm`/
22
+ * `foldBox` 不得出现为导出(含大小写变体);
23
+ * 3. **模块代码零 DOM**(AC-7):本文件编译进 `dist/blocks.js` 的**代码**里不出现
24
+ * `document.`/`window.`/`navigator.`(与 #76 同口径,`test/blocks.test.mjs` 钉死)。
25
+ *
26
+ * 受信透传口径(与 `renderEmptyState` 的 `actionHtml` 同口径):`contentHtml`/
27
+ * `previewText` 等「已由区块函数自产的 HTML」**不转义**(调用方不得塞入未转义的用户串;
28
+ * 用户串一律走各函数的文本字段,本模块用冻结五字符表转义);
29
+ * `actionHtml`(B-10)沿用冻结语义(调用方负责内容安全)。
30
+ */
31
+ import { charts } from './charts.js';
32
+ import { renderActionBar, renderEmptyState, renderErrorReceipt, renderStatusBadge, renderToast } from './controls.js';
33
+ import { ACTION_BAR_DEFAULTS, ACTION_ID_ATTR, CHART_KINDS, COPY_ACTION_IDS, DEFAULT_DATA_ATTR, ESCAPE_HTML_CHARS, ESCAPE_HTML_ENTITIES, SCENE_STATUS, STATUS_KINDS, TOAST_DEFAULTS, } from './spec/index.js';
34
+ import { STYLE_PREFIX } from './style.js';
35
+ /* ── 错误形态(与 TemplateError/ControlsError 同口径:抛、不返空;调用方按 name/code 判定) ── */
36
+ /** 区块层错误。code 恒为 `bad-input`(输入缺失/非法一律抛,缺失阻断不返空)。 */
37
+ export class BlocksError extends Error {
38
+ code;
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = 'BlocksError';
42
+ this.code = 'bad-input';
43
+ }
44
+ }
45
+ function badInput(message) {
46
+ throw new BlocksError(message);
47
+ }
48
+ /* ── 转义(AC-14:唯一口径 = 冻结的 `ESCAPE_HTML_CHARS`/`ESCAPE_HTML_ENTITIES`,与 controls.ts 同构) ── */
49
+ const ESCAPE_RE = new RegExp('[' + ESCAPE_HTML_CHARS.join('') + ']', 'g');
50
+ function esc(value) {
51
+ return value.replace(ESCAPE_RE, (ch) => ESCAPE_HTML_ENTITIES[ch] ?? ch);
52
+ }
53
+ /* ── 入参校验小件 ── */
54
+ function assertPlainObject(value, field) {
55
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
56
+ badInput(field + ' 必须是对象');
57
+ }
58
+ function assertNoInlineHandler(value, field) {
59
+ for (const key of Object.keys(value)) {
60
+ if (/^on/i.test(key))
61
+ badInput(field + ' 不得含内联事件处理器字段:' + key);
62
+ }
63
+ }
64
+ function reqText(value, field) {
65
+ if (typeof value !== 'string' || value === '')
66
+ badInput(field + ' 必须是非空字符串');
67
+ return value;
68
+ }
69
+ function optText(value) {
70
+ return typeof value === 'string' && value !== '' ? value : undefined;
71
+ }
72
+ /* ── 区块样式区闭集(DB-2:新增闭集,不改 CONTROL_STYLE_SECTIONS) ── */
73
+ /** 12 区块样式区(DB-1 清单的代码落点;顺序即 B-01…B-12)。 */
74
+ export const BLOCK_STYLE_SECTIONS = [
75
+ 'pageShell',
76
+ 'kpiCard',
77
+ 'dataTable',
78
+ 'chartBlock',
79
+ 'listRows',
80
+ 'preBlock',
81
+ 'detailSection',
82
+ 'disclosure',
83
+ 'paramForm',
84
+ 'emptyBlock',
85
+ 'copyBlock',
86
+ 'feedbackBlock',
87
+ ];
88
+ /** 区名 → kebab(`kpiCard` → `kpi-card`),只用于类名与 CSS 注释头。 */
89
+ function sectionSlug(section) {
90
+ return section.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
91
+ }
92
+ /** 区块根类:`ilife-block ilife-block-<slug>`(与控件类的 `ilife-<区>` 拼写不撞)。 */
93
+ function blockRoot(section) {
94
+ return STYLE_PREFIX + 'block ' + STYLE_PREFIX + 'block-' + sectionSlug(section);
95
+ }
96
+ /** 区块子件类:`ilife-block-<slug>-<part>`。 */
97
+ function blockPart(section, part) {
98
+ return STYLE_PREFIX + 'block-' + sectionSlug(section) + '-' + part;
99
+ }
100
+ /** B-01:单页容器+标题三件套(eyebrow/title/subtitle)+正文。 */
101
+ export function renderPageShell(input) {
102
+ assertPlainObject(input, 'renderPageShell: input');
103
+ assertNoInlineHandler(input, 'renderPageShell: input');
104
+ const shell = input;
105
+ const title = reqText(shell.title, 'renderPageShell: input.title');
106
+ if (typeof shell.content !== 'string')
107
+ badInput('renderPageShell: input.content 必须是字符串');
108
+ const parts = ['<section class="' + blockRoot('pageShell') + '">'];
109
+ const eyebrow = optText(shell.eyebrow);
110
+ if (eyebrow !== undefined)
111
+ parts.push('<p class="' + blockPart('pageShell', 'eyebrow') + '">' + esc(eyebrow) + '</p>');
112
+ parts.push('<h1 class="' + blockPart('pageShell', 'title') + '">' + esc(title) + '</h1>');
113
+ const subtitle = optText(shell.subtitle);
114
+ if (subtitle !== undefined)
115
+ parts.push('<p class="' + blockPart('pageShell', 'subtitle') + '">' + esc(subtitle) + '</p>');
116
+ parts.push('<div class="' + blockPart('pageShell', 'body') + '">' + shell.content + '</div>');
117
+ parts.push('</section>');
118
+ return parts.join('');
119
+ }
120
+ /** B-02:单张 KPI 卡(value 带 `tnum`;非法 status 由冻结语义降级 `empty`)。 */
121
+ export function renderKpiCard(input) {
122
+ assertPlainObject(input, 'renderKpiCard: input');
123
+ assertNoInlineHandler(input, 'renderKpiCard: input');
124
+ const card = input;
125
+ const label = reqText(card.label, 'renderKpiCard: input.label');
126
+ const value = reqText(card.value, 'renderKpiCard: input.value');
127
+ const parts = ['<div class="' + blockRoot('kpiCard') + '">'];
128
+ parts.push('<div class="' + blockPart('kpiCard', 'label') + '">' + esc(label) + '</div>');
129
+ parts.push('<div class="' + blockPart('kpiCard', 'value-row') + '">'
130
+ + '<span class="' + blockPart('kpiCard', 'value') + '">' + esc(value) + '</span>');
131
+ const unit = optText(card.unit);
132
+ if (unit !== undefined)
133
+ parts.push('<span class="' + blockPart('kpiCard', 'unit') + '">' + esc(unit) + '</span>');
134
+ parts.push('</div>');
135
+ const detail = optText(card.detail);
136
+ if (detail !== undefined)
137
+ parts.push('<div class="' + blockPart('kpiCard', 'detail') + '">' + esc(detail) + '</div>');
138
+ if (card.status !== undefined) {
139
+ const status = STATUS_KINDS.includes(card.status) ? card.status : 'empty';
140
+ const badge = renderStatusBadge(card.statusText === undefined ? { status } : { status, text: card.statusText });
141
+ parts.push('<div class="' + blockPart('kpiCard', 'badge') + '">' + badge + '</div>');
142
+ }
143
+ parts.push('</div>');
144
+ return parts.join('');
145
+ }
146
+ /** B-02 伴生布局:KPI 卡网格(不是第 13 个区块,无独立样式区,用 `kpiCard` 区)。 */
147
+ export function renderKpiGrid(cards) {
148
+ if (!Array.isArray(cards) || cards.length === 0)
149
+ badInput('renderKpiGrid: cards 必须是非空数组');
150
+ return '<div class="' + blockPart('kpiCard', 'grid') + '">'
151
+ + cards.map((card) => renderKpiCard(card)).join('') + '</div>';
152
+ }
153
+ const TABLE_ALIGNS = ['left', 'center', 'right'];
154
+ function cellText(value, field) {
155
+ if (value === null || value === undefined)
156
+ return '';
157
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
158
+ return esc(String(value));
159
+ }
160
+ badInput(field + ' 只许 string/number/boolean/bigint/null/undefined');
161
+ }
162
+ /** B-03:语义表格(`table/thead/th/td`;零行 → `renderEmptyState`,不渲染空表)。 */
163
+ export function renderDataTable(input) {
164
+ assertPlainObject(input, 'renderDataTable: input');
165
+ assertNoInlineHandler(input, 'renderDataTable: input');
166
+ const table = input;
167
+ if (!Array.isArray(table.columns) || table.columns.length === 0) {
168
+ badInput('renderDataTable: input.columns 必须是非空数组');
169
+ }
170
+ if (!Array.isArray(table.rows))
171
+ badInput('renderDataTable: input.rows 必须是数组');
172
+ const columns = table.columns.map((column, i) => {
173
+ const field = 'renderDataTable: input.columns[' + i + ']';
174
+ assertPlainObject(column, field);
175
+ const key = reqText(column.key, field + '.key');
176
+ const label = reqText(column.label, field + '.label');
177
+ const align = column.align ?? 'left';
178
+ if (!TABLE_ALIGNS.includes(align))
179
+ badInput(field + '.align 必须是 left/center/right');
180
+ return { key, label, align: align };
181
+ });
182
+ if (table.rows.length === 0) {
183
+ const text = optText(table.emptyText) ?? '无数据';
184
+ return '<div class="' + blockRoot('dataTable') + '">' + renderEmptyState({ text }) + '</div>';
185
+ }
186
+ const head = '<thead><tr>' + columns.map((column) => '<th scope="col" class="' + blockPart('dataTable', 'cell-' + column.align) + '">' + esc(column.label) + '</th>').join('') + '</tr></thead>';
187
+ const body = '<tbody>' + table.rows.map((row, ri) => {
188
+ assertPlainObject(row, 'renderDataTable: input.rows[' + ri + ']');
189
+ return '<tr>' + columns.map((column) => '<td class="' + blockPart('dataTable', 'cell-' + column.align) + '">'
190
+ + cellText(row[column.key], 'renderDataTable: input.rows[' + ri + '].' + column.key)
191
+ + '</td>').join('') + '</tr>';
192
+ }).join('') + '</tbody>';
193
+ const caption = optText(table.caption);
194
+ return '<div class="' + blockRoot('dataTable') + '"><table class="' + blockPart('dataTable', 'table') + '">'
195
+ + (caption === undefined ? '' : '<caption class="' + blockPart('dataTable', 'caption') + '">' + esc(caption) + '</caption>')
196
+ + head + body + '</table></div>';
197
+ }
198
+ /** B-04:图表区块(分发给冻结 `charts` 8 接口;空数组走空态/结构违规抛错恒为冻结语义)。 */
199
+ export function renderChartBlock(input) {
200
+ assertPlainObject(input, 'renderChartBlock: input');
201
+ assertNoInlineHandler(input, 'renderChartBlock: input');
202
+ const block = input;
203
+ if (!CHART_KINDS.includes(block.kind)) {
204
+ badInput('renderChartBlock: input.kind 必须是 ' + CHART_KINDS.join('/'));
205
+ }
206
+ if (block.input === null || typeof block.input !== 'object')
207
+ badInput('renderChartBlock: input.input 必须是对象');
208
+ const chartHtml = (() => {
209
+ switch (block.kind) {
210
+ case 'bar': return charts.bar(block.input).html;
211
+ case 'line': return charts.line(block.input).html;
212
+ case 'donut': return charts.donut(block.input).html;
213
+ case 'progress': return charts.progress(block.input).html;
214
+ case 'combo': return charts.combo(block.input).html;
215
+ case 'sparkline': return charts.sparkline(block.input).html;
216
+ case 'gauge': return charts.gauge(block.input).html;
217
+ case 'scatter': return charts.scatter(block.input).html;
218
+ default: return badInput('renderChartBlock: input.kind 未覆盖(防御,正常不可达)');
219
+ }
220
+ })();
221
+ const title = optText(block.title);
222
+ return '<section class="' + blockRoot('chartBlock') + '">'
223
+ + (title === undefined ? '' : '<h2 class="' + blockPart('chartBlock', 'title') + '">' + esc(title) + '</h2>')
224
+ + '<div class="' + blockPart('chartBlock', 'canvas') + '">' + chartHtml + '</div>'
225
+ + '</section>';
226
+ }
227
+ /** B-05:行列表(首行无边框;`done` → 删除线+成功色;零行 → 空态)。 */
228
+ export function renderListRows(input) {
229
+ assertPlainObject(input, 'renderListRows: input');
230
+ assertNoInlineHandler(input, 'renderListRows: input');
231
+ const list = input;
232
+ if (!Array.isArray(list.items))
233
+ badInput('renderListRows: input.items 必须是数组');
234
+ if (list.items.length === 0) {
235
+ const text = optText(list.emptyText) ?? '无数据';
236
+ return '<div class="' + blockRoot('listRows') + '">' + renderEmptyState({ text }) + '</div>';
237
+ }
238
+ const rows = list.items.map((item, i) => {
239
+ const field = 'renderListRows: input.items[' + i + ']';
240
+ assertPlainObject(item, field);
241
+ const row = item;
242
+ const main = reqText(row.main, field + '.main');
243
+ const done = row.done === true;
244
+ const cls = blockPart('listRows', 'row') + (done ? ' ' + blockPart('listRows', 'row-done') : '');
245
+ const left = optText(row.left);
246
+ const right = optText(row.right);
247
+ return '<div class="' + cls + '">'
248
+ + (left === undefined ? '' : '<span class="' + blockPart('listRows', 'left') + '">' + esc(left) + '</span>')
249
+ + '<span class="' + blockPart('listRows', 'main') + '">' + esc(main) + '</span>'
250
+ + (right === undefined ? '' : '<span class="' + blockPart('listRows', 'right') + '">' + esc(right) + '</span>')
251
+ + '</div>';
252
+ }).join('');
253
+ return '<div class="' + blockRoot('listRows') + '" role="list">' + rows + '</div>';
254
+ }
255
+ /** B-06:指令块(`<pre>` 逐字命令;无命令 → `bad-input`,不渲染空板)。 */
256
+ export function renderPreBlock(input) {
257
+ assertPlainObject(input, 'renderPreBlock: input');
258
+ assertNoInlineHandler(input, 'renderPreBlock: input');
259
+ const block = input;
260
+ const command = reqText(block.command, 'renderPreBlock: input.command');
261
+ const parts = ['<div class="' + blockRoot('preBlock') + '">'];
262
+ const label = optText(block.label);
263
+ if (label !== undefined)
264
+ parts.push('<div class="' + blockPart('preBlock', 'label') + '">' + esc(label) + '</div>');
265
+ parts.push('<pre class="' + blockPart('preBlock', 'code') + '">' + esc(command) + '</pre>');
266
+ if (block.actionId !== undefined) {
267
+ const actionId = reqText(block.actionId, 'renderPreBlock: input.actionId');
268
+ const text = typeof block.copyText === 'string' ? block.copyText : command;
269
+ const copyLabel = optText(block.copyLabel) ?? '复制指令';
270
+ parts.push('<button type="button" class="' + STYLE_PREFIX + 'copy-btn ' + STYLE_PREFIX + 'copy-btn-ghost" '
271
+ + ACTION_ID_ATTR + '="' + esc(actionId) + '" ' + DEFAULT_DATA_ATTR + '="' + esc(text) + '">'
272
+ + esc(copyLabel) + '</button>');
273
+ }
274
+ parts.push('</div>');
275
+ return parts.join('');
276
+ }
277
+ /** 类型徽章(与 `src/help.ts` 同口径:字符串走 CSS 默认配色;`{text,bg?,fg?}` 配色写内联 style)。 */
278
+ function typeBadgeHtml(badge) {
279
+ if (typeof badge === 'string') {
280
+ return '<span class="' + blockPart('detailSection', 'type') + '">' + esc(badge) + '</span>';
281
+ }
282
+ assertPlainObject(badge, 'renderDetailSection: types[] 元素');
283
+ const element = badge;
284
+ const text = reqText(element.text, 'renderDetailSection: types[] 元素的 text');
285
+ const style = [];
286
+ if (typeof element.bg === 'string' && element.bg !== '')
287
+ style.push('background:' + element.bg);
288
+ if (typeof element.fg === 'string' && element.fg !== '')
289
+ style.push('color:' + element.fg);
290
+ return '<span class="' + blockPart('detailSection', 'type') + '"'
291
+ + (style.length === 0 ? '' : ' style="' + esc(style.join(';')) + '"') + '>' + esc(text) + '</span>';
292
+ }
293
+ /** B-07:场景详情(`prompt_template` 全文逐字展示;`【待开发】` 徽章下复制按钮仍可点=纯展示,无行为)。 */
294
+ export function renderDetailSection(input) {
295
+ assertPlainObject(input, 'renderDetailSection: input');
296
+ assertNoInlineHandler(input, 'renderDetailSection: input');
297
+ const section = input;
298
+ assertPlainObject(section.scene, 'renderDetailSection: input.scene');
299
+ const scene = section.scene;
300
+ const id = reqText(scene.id, 'renderDetailSection: input.scene.id');
301
+ const title = reqText(scene.title, 'renderDetailSection: input.scene.title');
302
+ const wakeWord = reqText(scene.wake_word, 'renderDetailSection: input.scene.wake_word');
303
+ if (!SCENE_STATUS.includes(scene.status)) {
304
+ badInput('renderDetailSection: input.scene.status 必须是 SCENE_STATUS 两值之一');
305
+ }
306
+ if (typeof scene.prompt_template !== 'string' || scene.prompt_template === '') {
307
+ badInput('renderDetailSection: input.scene.prompt_template 必须是非空字符串');
308
+ }
309
+ const parts = ['<section class="' + blockRoot('detailSection') + '" data-scene-id="' + esc(id) + '">'];
310
+ parts.push('<h2 class="' + blockPart('detailSection', 'title') + '">' + esc(title) + '</h2>');
311
+ parts.push('<div class="' + blockPart('detailSection', 'meta') + '">'
312
+ + '<span class="' + blockPart('detailSection', 'wake') + '">' + esc(wakeWord) + '</span>');
313
+ if (scene.status === '【待开发】') {
314
+ parts.push('<span class="' + blockPart('detailSection', 'dev') + '">待开发</span>');
315
+ }
316
+ if (scene.types !== undefined) {
317
+ if (!Array.isArray(scene.types))
318
+ badInput('renderDetailSection: input.scene.types 必须是数组');
319
+ for (const badge of scene.types) {
320
+ if (typeof badge !== 'string')
321
+ assertNoInlineHandler(badge, 'renderDetailSection: types[] 元素');
322
+ parts.push(typeBadgeHtml(badge));
323
+ }
324
+ }
325
+ parts.push('</div>');
326
+ const cli = optText(section.cli);
327
+ if (cli !== undefined) {
328
+ parts.push('<pre class="' + blockPart('preBlock', 'code') + ' ' + blockPart('detailSection', 'cli') + '">'
329
+ + esc(cli) + '</pre>');
330
+ }
331
+ parts.push('<pre class="' + blockPart('preBlock', 'code') + ' ' + blockPart('detailSection', 'prompt') + '">'
332
+ + esc(scene.prompt_template) + '</pre>');
333
+ parts.push('</section>');
334
+ return parts.join('');
335
+ }
336
+ /** B-08:折叠区(原生 `details/summary`;`[open]` 驱动展开态;复制按钮在内容里不触发 toggle=宿主行为)。 */
337
+ export function renderDisclosure(input) {
338
+ assertPlainObject(input, 'renderDisclosure: input');
339
+ assertNoInlineHandler(input, 'renderDisclosure: input');
340
+ const block = input;
341
+ const title = reqText(block.title, 'renderDisclosure: input.title');
342
+ if (typeof block.contentHtml !== 'string')
343
+ badInput('renderDisclosure: input.contentHtml 必须是字符串');
344
+ return '<details class="' + blockRoot('disclosure') + '"' + (block.open === true ? ' open' : '') + '>'
345
+ + '<summary class="' + blockPart('disclosure', 'summary') + '">' + esc(title) + '</summary>'
346
+ + '<div class="' + blockPart('disclosure', 'body') + '">' + block.contentHtml + '</div>'
347
+ + '</details>';
348
+ }
349
+ /** B-09:参数表单(`placeholder = hint`;`required` 落 `data-required` 供宿主拦截;零 JS)。 */
350
+ export function renderParamForm(input) {
351
+ assertPlainObject(input, 'renderParamForm: input');
352
+ assertNoInlineHandler(input, 'renderParamForm: input');
353
+ const form = input;
354
+ if (!Array.isArray(form.fields) || form.fields.length === 0) {
355
+ badInput('renderParamForm: input.fields 必须是非空数组');
356
+ }
357
+ const parts = ['<div class="' + blockRoot('paramForm') + '">'];
358
+ const description = optText(form.description);
359
+ if (description !== undefined) {
360
+ parts.push('<p class="' + blockPart('paramForm', 'description') + '">' + esc(description) + '</p>');
361
+ }
362
+ form.fields.forEach((field, i) => {
363
+ const path = 'renderParamForm: input.fields[' + i + ']';
364
+ assertPlainObject(field, path);
365
+ assertNoInlineHandler(field, path);
366
+ const item = field;
367
+ const name = reqText(item.name, path + '.name');
368
+ const label = reqText(item.label, path + '.label');
369
+ const hint = optText(item.hint);
370
+ const value = typeof item.value === 'string' ? item.value : '';
371
+ const required = item.required === true;
372
+ parts.push('<label class="' + blockPart('paramForm', 'field') + '">'
373
+ + '<span class="' + blockPart('paramForm', 'label') + '">' + esc(label)
374
+ + (required ? '<span class="' + blockPart('paramForm', 'required') + '" aria-hidden="true"> *</span>' : '')
375
+ + '</span>'
376
+ + '<input class="' + blockPart('paramForm', 'input') + '" name="' + esc(name) + '" value="' + esc(value) + '"'
377
+ + (hint === undefined ? '' : ' placeholder="' + esc(hint) + '"')
378
+ + (required ? ' data-required="1" required' : '')
379
+ + ' />'
380
+ + '</label>');
381
+ });
382
+ if (typeof form.previewText === 'string' && form.previewText !== '') {
383
+ parts.push('<pre class="' + blockPart('preBlock', 'code') + ' ' + blockPart('paramForm', 'preview') + '">'
384
+ + esc(form.previewText) + '</pre>');
385
+ }
386
+ parts.push('</div>');
387
+ return parts.join('');
388
+ }
389
+ /** B-10:空态区块(冻结 `renderEmptyState` + 区块 wrapper;异常走 B-12,两者不混用 DB-8)。 */
390
+ export function renderEmptyBlock(input) {
391
+ assertPlainObject(input, 'renderEmptyBlock: input');
392
+ assertNoInlineHandler(input, 'renderEmptyBlock: input');
393
+ const block = input;
394
+ const title = optText(block.title);
395
+ return '<section class="' + blockRoot('emptyBlock') + '">'
396
+ + (title === undefined ? '' : '<h2 class="' + blockPart('emptyBlock', 'title') + '">' + esc(title) + '</h2>')
397
+ + renderEmptyState(block)
398
+ + '</section>';
399
+ }
400
+ /** B-11:复制区块(冻结 `renderActionBar`;id 缺省取 `COPY_ACTION_IDS.actionBar.*`,调用方须保页内唯一)。 */
401
+ export function renderCopyBlock(input) {
402
+ assertPlainObject(input, 'renderCopyBlock: input');
403
+ assertNoInlineHandler(input, 'renderCopyBlock: input');
404
+ const block = input;
405
+ const bar = {};
406
+ if (block.buttons !== undefined)
407
+ bar.buttons = block.buttons;
408
+ if (block.dataText !== undefined || block.dataActionId !== undefined) {
409
+ bar.copyData = {
410
+ actionId: block.dataActionId ?? COPY_ACTION_IDS.actionBar.copyData,
411
+ label: ACTION_BAR_DEFAULTS.copyDataLabel,
412
+ ...(block.dataText === undefined ? {} : { text: block.dataText }),
413
+ };
414
+ }
415
+ if (block.logText !== undefined || block.logActionId !== undefined) {
416
+ bar.copyLog = {
417
+ actionId: block.logActionId ?? COPY_ACTION_IDS.actionBar.copyLog,
418
+ label: ACTION_BAR_DEFAULTS.copyLogLabel,
419
+ ...(block.logText === undefined ? {} : { text: block.logText }),
420
+ };
421
+ }
422
+ const title = optText(block.title);
423
+ return '<section class="' + blockRoot('copyBlock') + '">'
424
+ + (title === undefined ? '' : '<h2 class="' + blockPart('copyBlock', 'title') + '">' + esc(title) + '</h2>')
425
+ + renderActionBar(bar)
426
+ + '</section>';
427
+ }
428
+ /** B-12:反馈区块(冻结 `renderToast` 和/或 `renderErrorReceipt`;两者至少其一)。 */
429
+ export function renderFeedbackBlock(input) {
430
+ assertPlainObject(input, 'renderFeedbackBlock: input');
431
+ assertNoInlineHandler(input, 'renderFeedbackBlock: input');
432
+ const block = input;
433
+ if (block.toast === undefined && block.error === undefined) {
434
+ badInput('renderFeedbackBlock: input.toast 与 input.error 至少其一');
435
+ }
436
+ const title = optText(block.title);
437
+ return '<section class="' + blockRoot('feedbackBlock') + '">'
438
+ + (title === undefined ? '' : '<h2 class="' + blockPart('feedbackBlock', 'title') + '">' + esc(title) + '</h2>')
439
+ + (block.toast === undefined ? '' : renderToast(block.toast))
440
+ + (block.error === undefined ? '' : renderErrorReceipt(block.error))
441
+ + '</section>';
442
+ }
443
+ /* ══════════════════════════════════════════════════════════════
444
+ * 区块样式资产(DB-2 后半:12 区的 CSS 唯一产出者)
445
+ * ══════════════════════════════════════════════════════════════ */
446
+ /** 换行(仓库口径:`String.fromCharCode(10)`,不写字面换行转义)。 */
447
+ const LF = String.fromCharCode(10);
448
+ /** 软线派生字面量:`--line`(`#d2d2d7`)的 RGB(与 `src/style.ts` 的 `BLUE_RGB` 同口径:
449
+ * 派生字面量、不是第二份 token 表,不新增 token 名)。 */
450
+ const LINE_RGB = '210, 210, 215';
451
+ /** 主色 RGB(`--blue` `#007aff`;与 `src/style.ts` 同值,复用罗列处注明出处)。 */
452
+ const BLUE_RGB = '0, 122, 255';
453
+ /** 待开发徽章配色(沿用 HELP 壳 `help-shell-badge-dev` 逐值,不另发明)。 */
454
+ const DEV_BG = 'rgba(255, 149, 0, .12)';
455
+ const DEV_FG = '#b25000';
456
+ /** 必填星号色(沿用 HELP 壳 `help-shell-field-required` 逐值)。 */
457
+ const REQUIRED_FG = '#c0392b';
458
+ /** 局部圆角常量(D-5:`{8,14,20,999}`,不新增 token 名)。 */
459
+ const RADIUS_SM = 8;
460
+ const RADIUS_MD = 14;
461
+ const RADIUS_LG = 20;
462
+ const RADIUS_PILL = 999;
463
+ /** 区块 CSS:12 区各自实现(键与 `BLOCK_STYLE_SECTIONS` 一一对应,缺区导入即抛,防静默缺样式)。 */
464
+ const BLOCK_SECTION_BUILDERS = {
465
+ pageShell: (p) => [
466
+ '.' + p + 'block-page-shell {',
467
+ ' display: block;',
468
+ ' max-width: 960px;',
469
+ ' margin: 0 auto;',
470
+ ' padding: 32px 20px 80px;',
471
+ ' color: var(--fg);',
472
+ ' font-feature-settings: "tnum";',
473
+ '}',
474
+ '.' + p + 'block-page-shell-eyebrow {',
475
+ ' margin: 0;',
476
+ ' color: var(--blue);',
477
+ ' font-size: 12px;',
478
+ ' font-weight: 600;',
479
+ ' letter-spacing: .08em;',
480
+ ' text-transform: uppercase;',
481
+ '}',
482
+ '.' + p + 'block-page-shell-title {',
483
+ ' margin: 6px 0 0;',
484
+ ' font-size: 32px;',
485
+ ' font-weight: 700;',
486
+ ' letter-spacing: -.4px;',
487
+ ' line-height: 1.2;',
488
+ '}',
489
+ '.' + p + 'block-page-shell-subtitle {',
490
+ ' margin: 6px 0 0;',
491
+ ' color: var(--fg2);',
492
+ ' font-size: 15px;',
493
+ '}',
494
+ '.' + p + 'block-page-shell-body {',
495
+ ' display: block;',
496
+ ' margin-top: 16px;',
497
+ '}',
498
+ '@media (max-width: 640px) {',
499
+ ' .' + p + 'block-page-shell {',
500
+ ' padding: 20px 16px 60px;',
501
+ ' }',
502
+ ' .' + p + 'block-page-shell-title {',
503
+ ' font-size: 26px;',
504
+ ' }',
505
+ '}',
506
+ ].join(LF),
507
+ kpiCard: (p) => [
508
+ '.' + p + 'block-kpi-card-grid {',
509
+ ' display: grid;',
510
+ ' grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));',
511
+ ' gap: 12px;',
512
+ '}',
513
+ '.' + p + 'block-kpi-card {',
514
+ ' padding: 14px;',
515
+ ' border: 1px solid var(--line);',
516
+ ' border-radius: ' + RADIUS_MD + 'px;',
517
+ ' background: var(--card);',
518
+ ' box-shadow: var(--shadow);',
519
+ '}',
520
+ '.' + p + 'block-kpi-label {',
521
+ ' color: var(--fg2);',
522
+ ' font-size: 12px;',
523
+ ' font-weight: 600;',
524
+ '}',
525
+ '.' + p + 'block-kpi-value-row {',
526
+ ' display: flex;',
527
+ ' align-items: baseline;',
528
+ ' gap: 6px;',
529
+ ' margin-top: 4px;',
530
+ '}',
531
+ '.' + p + 'block-kpi-value {',
532
+ ' color: var(--fg);',
533
+ ' font-size: 28px;',
534
+ ' font-weight: 700;',
535
+ ' line-height: 1.2;',
536
+ ' font-variant-numeric: tabular-nums;',
537
+ '}',
538
+ '.' + p + 'block-kpi-unit {',
539
+ ' color: var(--fg2);',
540
+ ' font-size: 13px;',
541
+ '}',
542
+ '.' + p + 'block-kpi-detail {',
543
+ ' margin-top: 4px;',
544
+ ' color: var(--fg3);',
545
+ ' font-size: 12px;',
546
+ '}',
547
+ '.' + p + 'block-kpi-badge {',
548
+ ' margin-top: 8px;',
549
+ '}',
550
+ ].join(LF),
551
+ dataTable: (p) => [
552
+ '.' + p + 'block-data-table {',
553
+ ' overflow-x: auto;',
554
+ ' border: 1px solid var(--line);',
555
+ ' border-radius: ' + RADIUS_MD + 'px;',
556
+ ' background: var(--card);',
557
+ '}',
558
+ '.' + p + 'block-data-table-table {',
559
+ ' width: 100%;',
560
+ ' border-collapse: collapse;',
561
+ ' font-variant-numeric: tabular-nums;',
562
+ '}',
563
+ '.' + p + 'block-data-table-caption {',
564
+ ' padding: 10px 14px;',
565
+ ' color: var(--fg2);',
566
+ ' font-size: 13px;',
567
+ ' font-weight: 600;',
568
+ ' text-align: left;',
569
+ '}',
570
+ '.' + p + 'block-data-table th {',
571
+ ' padding: 10px 14px;',
572
+ ' border-bottom: 1px solid var(--line);',
573
+ ' background-color: transparent;',
574
+ ' color: var(--fg3);',
575
+ ' font-size: 12px;',
576
+ ' font-weight: 600;',
577
+ ' text-transform: uppercase;',
578
+ ' letter-spacing: .04em;',
579
+ ' white-space: nowrap;',
580
+ '}',
581
+ '.' + p + 'block-data-table td {',
582
+ ' padding: 12px 14px;',
583
+ ' border-bottom: 1px solid rgba(' + LINE_RGB + ', .6);',
584
+ ' color: var(--fg2);',
585
+ ' font-size: 13px;',
586
+ '}',
587
+ '.' + p + 'block-data-table tr:last-child td {',
588
+ ' border-bottom: none;',
589
+ '}',
590
+ '.' + p + 'block-data-table-cell-left {',
591
+ ' text-align: left;',
592
+ '}',
593
+ '.' + p + 'block-data-table-cell-center {',
594
+ ' text-align: center;',
595
+ '}',
596
+ '.' + p + 'block-data-table-cell-right {',
597
+ ' text-align: right;',
598
+ '}',
599
+ ].join(LF),
600
+ chartBlock: (p) => [
601
+ '.' + p + 'block-chart-block {',
602
+ ' margin: 16px 0;',
603
+ ' padding: 14px;',
604
+ ' border: 1px solid var(--line);',
605
+ ' border-radius: ' + RADIUS_MD + 'px;',
606
+ ' background: var(--card);',
607
+ '}',
608
+ '.' + p + 'block-chart-block-title {',
609
+ ' margin: 0 0 10px;',
610
+ ' font-size: 15px;',
611
+ ' font-weight: 700;',
612
+ '}',
613
+ '.' + p + 'block-chart-block-canvas {',
614
+ ' padding: 0;',
615
+ '}',
616
+ ].join(LF),
617
+ listRows: (p) => [
618
+ '.' + p + 'block-list-rows {',
619
+ ' border: 1px solid var(--line);',
620
+ ' border-radius: ' + RADIUS_MD + 'px;',
621
+ ' background: var(--card);',
622
+ '}',
623
+ '.' + p + 'block-list-rows-row {',
624
+ ' display: grid;',
625
+ ' grid-template-columns: 44px minmax(0, 1fr) auto;',
626
+ ' gap: 8px;',
627
+ ' align-items: center;',
628
+ ' padding: 10px 14px;',
629
+ ' border-top: 1px solid rgba(' + LINE_RGB + ', .6);',
630
+ ' font-size: 13px;',
631
+ '}',
632
+ '.' + p + 'block-list-rows-row:first-child {',
633
+ ' border-top: 0;',
634
+ '}',
635
+ '.' + p + 'block-list-rows-left {',
636
+ ' color: var(--fg3);',
637
+ ' font-variant-numeric: tabular-nums;',
638
+ '}',
639
+ '.' + p + 'block-list-rows-main {',
640
+ ' min-width: 0;',
641
+ ' overflow: hidden;',
642
+ ' color: var(--fg);',
643
+ ' text-overflow: ellipsis;',
644
+ ' white-space: nowrap;',
645
+ '}',
646
+ '.' + p + 'block-list-rows-right {',
647
+ ' color: var(--fg2);',
648
+ ' font-variant-numeric: tabular-nums;',
649
+ '}',
650
+ '.' + p + 'block-list-rows-row-done .' + p + 'block-list-rows-main {',
651
+ ' color: var(--ok);',
652
+ ' text-decoration: line-through;',
653
+ '}',
654
+ ].join(LF),
655
+ preBlock: (p) => [
656
+ '.' + p + 'block-pre-block {',
657
+ ' margin: 12px 0;',
658
+ '}',
659
+ '.' + p + 'block-pre-block-label {',
660
+ ' margin-bottom: 6px;',
661
+ ' color: var(--fg2);',
662
+ ' font-size: 12px;',
663
+ ' font-weight: 600;',
664
+ '}',
665
+ '.' + p + 'block-pre-block-code {',
666
+ ' margin: 0;',
667
+ ' padding: 12px;',
668
+ ' border-radius: ' + RADIUS_SM + 'px;',
669
+ ' background: var(--soft);',
670
+ ' color: var(--fg2);',
671
+ ' font-family: "SF Mono", monospace;',
672
+ ' font-size: 12px;',
673
+ ' line-height: 1.55;',
674
+ ' white-space: pre-wrap;',
675
+ ' overflow-x: auto;',
676
+ '}',
677
+ ].join(LF),
678
+ detailSection: (p) => [
679
+ '.' + p + 'block-detail-section {',
680
+ ' margin: 16px 0;',
681
+ ' padding: 14px;',
682
+ ' border: 1px solid var(--line);',
683
+ ' border-radius: ' + RADIUS_MD + 'px;',
684
+ ' background: var(--card);',
685
+ ' box-shadow: var(--shadow);',
686
+ '}',
687
+ '.' + p + 'block-detail-section-title {',
688
+ ' margin: 0;',
689
+ ' font-size: 17px;',
690
+ ' font-weight: 600;',
691
+ '}',
692
+ '.' + p + 'block-detail-section-meta {',
693
+ ' display: flex;',
694
+ ' flex-wrap: wrap;',
695
+ ' align-items: center;',
696
+ ' gap: 8px;',
697
+ ' margin-top: 8px;',
698
+ '}',
699
+ '.' + p + 'block-detail-section-wake {',
700
+ ' color: var(--fg3);',
701
+ ' font-family: "SF Mono", monospace;',
702
+ ' font-size: 11.5px;',
703
+ '}',
704
+ '.' + p + 'block-detail-section-dev {',
705
+ ' display: inline-flex;',
706
+ ' align-items: center;',
707
+ ' padding: 1px 8px;',
708
+ ' border-radius: ' + RADIUS_PILL + 'px;',
709
+ ' background: ' + DEV_BG + ';',
710
+ ' color: ' + DEV_FG + ';',
711
+ ' font-size: 11px;',
712
+ ' font-weight: 600;',
713
+ '}',
714
+ '.' + p + 'block-detail-section-type {',
715
+ ' display: inline-flex;',
716
+ ' align-items: center;',
717
+ ' padding: 1px 8px;',
718
+ ' border-radius: ' + RADIUS_PILL + 'px;',
719
+ ' background: var(--soft);',
720
+ ' color: var(--blue2);',
721
+ ' font-size: 11px;',
722
+ ' font-weight: 600;',
723
+ '}',
724
+ '.' + p + 'block-detail-section-cli,',
725
+ '.' + p + 'block-detail-section-prompt {',
726
+ ' margin-top: 10px;',
727
+ '}',
728
+ ].join(LF),
729
+ disclosure: (p) => [
730
+ '.' + p + 'block-disclosure {',
731
+ ' margin: 12px 0;',
732
+ ' border: 1px solid var(--line);',
733
+ ' border-radius: ' + RADIUS_MD + 'px;',
734
+ ' background: var(--card);',
735
+ '}',
736
+ '.' + p + 'block-disclosure-summary {',
737
+ ' padding: 10px 14px;',
738
+ ' font-size: 14px;',
739
+ ' font-weight: 600;',
740
+ ' cursor: pointer;',
741
+ ' list-style: none;',
742
+ '}',
743
+ '.' + p + 'block-disclosure-summary::-webkit-details-marker {',
744
+ ' display: none;',
745
+ '}',
746
+ '.' + p + 'block-disclosure-summary::before {',
747
+ ' content: "▸";',
748
+ ' display: inline-block;',
749
+ ' margin-right: 8px;',
750
+ ' color: var(--fg3);',
751
+ ' transition: transform .15s ease;',
752
+ '}',
753
+ '.' + p + 'block-disclosure[open] > .' + p + 'block-disclosure-summary::before {',
754
+ ' transform: rotate(90deg);',
755
+ '}',
756
+ '.' + p + 'block-disclosure-body {',
757
+ ' padding: 0 14px 14px;',
758
+ '}',
759
+ ].join(LF),
760
+ paramForm: (p) => [
761
+ '.' + p + 'block-param-form {',
762
+ ' margin: 12px 0;',
763
+ '}',
764
+ '.' + p + 'block-param-form-description {',
765
+ ' margin: 0 0 8px;',
766
+ ' color: var(--fg2);',
767
+ ' font-size: 13px;',
768
+ '}',
769
+ '.' + p + 'block-param-form-field {',
770
+ ' display: block;',
771
+ ' margin: 8px 0;',
772
+ '}',
773
+ '.' + p + 'block-param-form-label {',
774
+ ' display: block;',
775
+ ' margin-bottom: 4px;',
776
+ ' color: var(--fg2);',
777
+ ' font-size: 13px;',
778
+ '}',
779
+ '.' + p + 'block-param-form-required {',
780
+ ' color: ' + REQUIRED_FG + ';',
781
+ ' font-weight: 600;',
782
+ '}',
783
+ '.' + p + 'block-param-form-input {',
784
+ ' box-sizing: border-box;',
785
+ ' width: 100%;',
786
+ ' min-height: 44px;',
787
+ ' padding: 0 12px;',
788
+ ' border: 0;',
789
+ ' border-radius: ' + RADIUS_SM + 'px;',
790
+ ' background: var(--bg);',
791
+ ' color: var(--fg);',
792
+ ' font-family: inherit;',
793
+ ' font-size: 13px;',
794
+ '}',
795
+ '.' + p + 'block-param-form-input:focus {',
796
+ ' background: var(--card);',
797
+ ' border: 1px solid var(--blue);',
798
+ ' box-shadow: 0 0 0 3px rgba(' + BLUE_RGB + ', .25);',
799
+ ' outline: none;',
800
+ '}',
801
+ '.' + p + 'block-param-form-input:focus-visible {',
802
+ ' outline: 2px solid var(--blue);',
803
+ ' outline-offset: 2px;',
804
+ '}',
805
+ '.' + p + 'block-param-form-preview {',
806
+ ' margin-top: 10px;',
807
+ '}',
808
+ ].join(LF),
809
+ emptyBlock: (p) => [
810
+ '.' + p + 'block-empty-block {',
811
+ ' margin: 16px 0;',
812
+ '}',
813
+ '.' + p + 'block-empty-block-title {',
814
+ ' margin: 0 0 8px;',
815
+ ' font-size: 15px;',
816
+ ' font-weight: 700;',
817
+ '}',
818
+ ].join(LF),
819
+ copyBlock: (p) => [
820
+ '.' + p + 'block-copy-block {',
821
+ ' margin: 16px 0;',
822
+ '}',
823
+ '.' + p + 'block-copy-block-title {',
824
+ ' margin: 0 0 8px;',
825
+ ' font-size: 15px;',
826
+ ' font-weight: 700;',
827
+ '}',
828
+ ].join(LF),
829
+ feedbackBlock: (p) => [
830
+ '.' + p + 'block-feedback-block {',
831
+ ' margin: 16px 0;',
832
+ '}',
833
+ '.' + p + 'block-feedback-block-title {',
834
+ ' margin: 0 0 8px;',
835
+ ' font-size: 15px;',
836
+ ' font-weight: 700;',
837
+ '}',
838
+ ].join(LF),
839
+ };
840
+ /** 闭集漂移 fail-fast(与 `src/style.ts` 同口径):缺区实现导入即抛。 */
841
+ for (const section of BLOCK_STYLE_SECTIONS) {
842
+ if (BLOCK_SECTION_BUILDERS[section] === undefined) {
843
+ throw new Error('base-paint/blocks:BLOCK_STYLE_SECTIONS 闭集缺样式区实现(' + section + ')');
844
+ }
845
+ }
846
+ /** 区块样式资产唯一产出者(用法:调用方把返回值拼进 `TemplateAssets.sharedCssText`
847
+ * 再交 `fillTemplate` 包裹注入;不得走 `StyleSheetInput.extraCss`——其语义被契约
848
+ * 限死为技能作用域 token 覆盖块,不得塞壳层样式)。恒返回非空 CSS 文本。 */
849
+ export function blocksCss(input) {
850
+ const prefix = input !== undefined && input !== null && typeof input.prefix === 'string' && input.prefix !== ''
851
+ ? input.prefix
852
+ : STYLE_PREFIX;
853
+ const parts = ['/* base-paint 区块样式资产 · #104 · 12 区 */'];
854
+ for (const section of BLOCK_STYLE_SECTIONS) {
855
+ parts.push('/* block-' + sectionSlug(section) + ' */');
856
+ parts.push(BLOCK_SECTION_BUILDERS[section](prefix));
857
+ }
858
+ return parts.join(LF);
859
+ }
860
+ /* ── 再导出冻结引用(只读透传,方便 108–113 从单一入口取双属性名;零新语义) ── */
861
+ export { ACTION_ID_ATTR, COPY_ACTION_IDS, DEFAULT_DATA_ATTR, TOAST_DEFAULTS };
862
+ //# sourceMappingURL=blocks.js.map