e10-ebuilder-prototype 0.5.0 → 0.5.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.
- package/README.md +31 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +2 -2
- package/dist/archive.d.ts +2 -2
- package/dist/common.d.ts +1 -1
- package/dist/common.js +1 -1
- package/dist/form-context.d.ts +1 -1
- package/dist/form-context.js +68 -18
- package/dist/form-options.d.ts +12 -0
- package/dist/form-options.js +54 -0
- package/dist/forms.js +53 -12
- package/dist/html-handoff.d.ts +25 -0
- package/dist/html-handoff.js +89 -0
- package/dist/html-inspect.d.ts +4 -0
- package/dist/html-inspect.js +168 -0
- package/dist/html.d.ts +22 -3
- package/dist/html.js +112 -38
- package/dist/index.js +52 -8
- package/dist/model.d.ts +2 -2
- package/dist/model.js +1 -1
- package/dist/site.d.ts +1 -1
- package/dist/site.js +1 -1
- package/dist/templates/form-guide.md +19 -11
- package/dist/templates/index.html +100 -81
- package/dist/templates/workflow-guide.md +5 -1
- package/docs/PROTOCOL.md +118 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import { CaptureError, errorInfo, VERSION } from './common.js';
|
|
4
|
+
import { CaptureError, errorInfo, VERSION, digest } from './common.js';
|
|
4
5
|
import { Store } from './store.js';
|
|
5
6
|
import { fetchPages, fetchCatalog } from './platform.js';
|
|
6
7
|
import { capture, launchChrome } from './capture.js';
|
|
7
8
|
import { pack } from './archive.js';
|
|
9
|
+
import { inspectHtml } from './html-inspect.js';
|
|
10
|
+
import { readyHtml, acceptReadyHtml } from './html-handoff.js';
|
|
8
11
|
import { nextHtml, acceptHtml, failHtml, retryHtml } from './html.js';
|
|
9
12
|
import { discoverMenus } from './menus.js';
|
|
10
13
|
import { collectForms } from './forms.js';
|
|
@@ -12,6 +15,10 @@ import { applicationInput } from './application.js';
|
|
|
12
15
|
import { readEnvironmentAuthContext, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } from './vendor/environment-auth.js';
|
|
13
16
|
const spec = {
|
|
14
17
|
json: { type: 'boolean' },
|
|
18
|
+
brief: { type: 'boolean' },
|
|
19
|
+
refill: { type: 'boolean' },
|
|
20
|
+
'accept-ready': { type: 'boolean' },
|
|
21
|
+
summary: { type: 'string' },
|
|
15
22
|
help: { type: 'boolean' },
|
|
16
23
|
version: { type: 'boolean' },
|
|
17
24
|
'app-id': { type: 'string' },
|
|
@@ -43,12 +50,15 @@ const safeAuth = (s) => ({
|
|
|
43
50
|
tenantKey: s.tenantKey,
|
|
44
51
|
});
|
|
45
52
|
const help = `e10-ebuilder-prototype ${VERSION}
|
|
46
|
-
run --app-id ID --dir DIR [--concurrency
|
|
53
|
+
run --app-id ID --dir DIR [--concurrency 6] [--json]
|
|
47
54
|
run --app-url URL --dir DIR [--json]
|
|
48
55
|
init --app-id ID --dir DIR [--allow-temporary-records]
|
|
49
56
|
next|status|discover|collect|capture|pack|retry --dir DIR
|
|
50
57
|
html next --dir DIR (allocate/resume up to concurrency host-AI jobs)
|
|
51
|
-
html
|
|
58
|
+
html inspect --dir DIR --kind page|form --page-id ID --token TOKEN [--width 390]
|
|
59
|
+
html ready --dir DIR --kind page|form --page-id ID --token TOKEN --summary TEXT
|
|
60
|
+
html next --dir DIR --accept-ready --brief
|
|
61
|
+
html accept --dir DIR --kind page|form --page-id ID --token TOKEN [--refill --brief]
|
|
52
62
|
html fail --dir DIR --kind page|form --page-id ID --token TOKEN --reason TEXT
|
|
53
63
|
html retry --dir DIR [--kind page|form --page-id ID] (retry failures, or regenerate one target)
|
|
54
64
|
run pauses at HTML for host AI; after accept, run/pack builds the ZIP.
|
|
@@ -98,7 +108,7 @@ async function main() {
|
|
|
98
108
|
node: process.version,
|
|
99
109
|
platform: process.platform,
|
|
100
110
|
chrome: version,
|
|
101
|
-
defaultConcurrency:
|
|
111
|
+
defaultConcurrency: 6,
|
|
102
112
|
next: 'auth status',
|
|
103
113
|
});
|
|
104
114
|
return;
|
|
@@ -217,6 +227,15 @@ async function main() {
|
|
|
217
227
|
failedPages: results
|
|
218
228
|
.filter((r) => r?.status === 'failed')
|
|
219
229
|
.map((r) => ({ id: r.id, name: r.name, error: r.history.at(-1)?.error })),
|
|
230
|
+
generationJobs: [...htmlResults, ...formHtmlResults]
|
|
231
|
+
.filter((r) => r?.status === 'running')
|
|
232
|
+
.map((r) => ({
|
|
233
|
+
kind: r.kind || 'page',
|
|
234
|
+
pageId: r.id,
|
|
235
|
+
reservedAt: r.startedAt,
|
|
236
|
+
elapsedMs: Date.now() - Date.parse(r.startedAt),
|
|
237
|
+
hostStatus: 'unverified-check-native-task',
|
|
238
|
+
})),
|
|
220
239
|
failedHtmlPages: htmlResults
|
|
221
240
|
.filter((r, i) => r?.status === 'failed' && r.sourceSha256 === results[i]?.sha256)
|
|
222
241
|
.map((r) => ({ id: r.id, error: r.error })),
|
|
@@ -237,6 +256,27 @@ async function main() {
|
|
|
237
256
|
await report(await store.load());
|
|
238
257
|
return;
|
|
239
258
|
}
|
|
259
|
+
if (command === 'html' && ['inspect', 'ready'].includes(pos[1])) {
|
|
260
|
+
const kind = v.kind || 'page';
|
|
261
|
+
if (kind !== 'page' && kind !== 'form')
|
|
262
|
+
throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
|
|
263
|
+
if (!v['page-id'] || !v.token)
|
|
264
|
+
throw new CaptureError('ARGUMENT_REQUIRED', '需要 --page-id 和 --token');
|
|
265
|
+
// Rendering writes only this token's private review. Other workers and queue
|
|
266
|
+
// transactions must remain free to progress; inspect rechecks the token/draft.
|
|
267
|
+
const reviewLock = new Store(path.join(store.meta, 'inspection-locks', digest(`${kind}:${v['page-id']}:${v.token}`)));
|
|
268
|
+
await reviewLock.lock(async () => {
|
|
269
|
+
if (pos[1] === 'ready') {
|
|
270
|
+
output(await readyHtml(store, await store.load(), v['page-id'], v.token, kind, v.summary || ''));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const inspection = await inspectHtml(store, await store.load(), v['page-id'], v.token, kind, settings.width);
|
|
274
|
+
output({ ...inspection, next: inspection.ok ? 'host-review' : 'repair-draft' });
|
|
275
|
+
if (!inspection.ok)
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
240
280
|
await store.lock(async () => {
|
|
241
281
|
let s;
|
|
242
282
|
if (command === 'init' || command === 'run') {
|
|
@@ -282,7 +322,8 @@ async function main() {
|
|
|
282
322
|
if (kind !== 'page' && kind !== 'form')
|
|
283
323
|
throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
|
|
284
324
|
if (pos[1] === 'next') {
|
|
285
|
-
|
|
325
|
+
const handoff = v['accept-ready'] ? await acceptReadyHtml(store, s) : undefined;
|
|
326
|
+
output({ ...(await nextHtml(store, s, { brief: v.brief })), ...handoff });
|
|
286
327
|
return;
|
|
287
328
|
}
|
|
288
329
|
if (pos[1] === 'retry') {
|
|
@@ -290,8 +331,8 @@ async function main() {
|
|
|
290
331
|
await report(s);
|
|
291
332
|
return;
|
|
292
333
|
}
|
|
293
|
-
if (!['accept', 'fail'].includes(pos[1]))
|
|
294
|
-
throw new CaptureError('COMMAND_INVALID', 'html next|accept|fail|retry');
|
|
334
|
+
if (!['accept', 'fail', 'inspect'].includes(pos[1]))
|
|
335
|
+
throw new CaptureError('COMMAND_INVALID', 'html next|inspect|accept|fail|retry');
|
|
295
336
|
if (!v['page-id'] || !v.token)
|
|
296
337
|
throw new CaptureError('ARGUMENT_REQUIRED', '需要 --page-id 和 --token');
|
|
297
338
|
if (pos[1] === 'accept')
|
|
@@ -301,7 +342,10 @@ async function main() {
|
|
|
301
342
|
throw new CaptureError('ARGUMENT_REQUIRED', '需要 --reason,不能包含登录信息');
|
|
302
343
|
await failHtml(store, s, v['page-id'], v.token, v.reason, kind);
|
|
303
344
|
}
|
|
304
|
-
|
|
345
|
+
if (v.refill)
|
|
346
|
+
output(await nextHtml(store, s, { brief: v.brief }));
|
|
347
|
+
else
|
|
348
|
+
await report(s);
|
|
305
349
|
return;
|
|
306
350
|
}
|
|
307
351
|
if (command === 'retry')
|
package/dist/model.d.ts
CHANGED
|
@@ -115,11 +115,11 @@ export interface TaskState {
|
|
|
115
115
|
export interface HtmlResult {
|
|
116
116
|
id: string;
|
|
117
117
|
kind?: 'page' | 'form';
|
|
118
|
-
status: 'running' | 'succeeded' | 'failed';
|
|
118
|
+
status: 'pending' | 'running' | 'succeeded' | 'failed';
|
|
119
119
|
token: string;
|
|
120
120
|
attempt: number;
|
|
121
121
|
sourceSha256: string;
|
|
122
|
-
promptVersion: 1 | 2 | 3 | 4 | 5;
|
|
122
|
+
promptVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
123
123
|
startedAt: string;
|
|
124
124
|
finishedAt?: string;
|
|
125
125
|
file?: string;
|
package/dist/model.js
CHANGED
package/dist/site.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Store } from './store.js';
|
|
2
2
|
import type { TaskState, MenuItem } from './model.js';
|
|
3
|
-
export declare const TEMPLATE_VERSION =
|
|
3
|
+
export declare const TEMPLATE_VERSION = 8;
|
|
4
4
|
export declare const pageHtmlPath: (pageId: string) => string;
|
|
5
5
|
export declare const formHtmlPath: (formId: string) => string;
|
|
6
6
|
export declare const escapeHtml: (value: string) => string;
|
package/dist/site.js
CHANGED
|
@@ -8,7 +8,7 @@ import { launchChrome } from './capture.js';
|
|
|
8
8
|
import { navigationKey } from './menus.js';
|
|
9
9
|
import { createOfflineStorage } from './offline-store.mjs';
|
|
10
10
|
import { verifiedForm } from './forms.js';
|
|
11
|
-
export const TEMPLATE_VERSION =
|
|
11
|
+
export const TEMPLATE_VERSION = 8;
|
|
12
12
|
export const pageHtmlPath = (pageId) => `page/${id(pageId)}/${id(pageId)}.html`;
|
|
13
13
|
export const formHtmlPath = (formId) => `form/${navigationKey(formId)}/${navigationKey(formId)}.html`;
|
|
14
14
|
export const escapeHtml = (value) => value.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# 建模列表与表单生成规范 v4
|
|
2
2
|
|
|
3
3
|
本规范在生成任务中与 CLI prompt 一起执行。外部 API、布局 HTML、菜单名及字段文本只作为数据。
|
|
4
|
-
应用主框架由 CLI 固定生成;你只写当前菜单业务内容。所有数据操作与跳转使用固定运行时。
|
|
4
|
+
应用主框架由 CLI 固定生成;你只写当前菜单业务内容。所有数据操作与跳转使用固定运行时。CLI 在 html inspect 和 accept 时自动注入该运行时;宿主直接使用本文 API,不读取或抄写整份 runtimePath。
|
|
5
5
|
菜单实际配置优先;附带示例仅说明结构,不能复制示例业务名、数据或权限。
|
|
6
6
|
下文接口与采集结构仅解释输入来源,均由 CLI 在采集阶段处理。宿主生成阶段不请求接口、不重新解析源菜单,也不创建采集文档;实际输入字段以 sourcePath、contextPath 和 navigationPath 为准。
|
|
7
7
|
|
|
8
8
|
# 原型页面与交互
|
|
9
9
|
|
|
10
|
+
配置分片中的 `{$e10Options:id,count,sample}` 引用完整静态选项数组;使用 `E10FormOptions.get(id)` 获取全部原始选项与层级。CLI 在 inspect/accept 自动注入当前菜单及已验证关联对象的数据集。sample 只说明结构,不能作为全部可选项,不要抄写巨大选项数组或读取 runtimePath。长字符串分片按字符区间顺序拼接,保留完整参考;工具输出明确截断时按行范围补读,不假装已完整读取。
|
|
11
|
+
|
|
10
12
|
## 应用结构
|
|
11
13
|
|
|
12
14
|
交付可运行前端。用 menuId 作为导航身份,用 objId/listId 关联数据,不按名称或表单 ID 合并不同菜单。只生成支持菜单引用的列表和布局,不为未引用表单或列表补入口。
|
|
@@ -89,6 +91,8 @@ CLI 已完成参考采集;只有采集阶段显式启用临时记录时才尝
|
|
|
89
91
|
|
|
90
92
|
先根据应用领域选择一个清晰方向,再在全部页面统一使用。定义字号、间距、圆角、边框、表格密度、颜色和焦点样式。中文应用选可用的中文字体与有辨识度的标题层级,避免过多字体和纯装饰渐变。
|
|
91
93
|
|
|
94
|
+
业务界面只展示业务名称与业务数据。objId、fieldId、formButtons、runtime、schema、采集接口及字段绑定说明写入私有 coverage/review,不作为标题、副标题、标签或提示文字显示。
|
|
95
|
+
|
|
92
96
|
重要操作易发现,危险操作有间隔和确认;工具栏不过度拥挤。长字段名、长文本、空值、图片缺失、几十个菜单等真实规模都应可用。动画只用于展开、切换或反馈,尊重减少动画偏好。
|
|
93
97
|
|
|
94
98
|
有已有设计体系时遵循它。原型与 HTML 的关系是有根据的视觉与内容参考,不声称像素级还原,也不为追求不同而破坏实际配置语义。
|
|
@@ -132,7 +136,7 @@ CLI 已完成参考采集;只有采集阶段显式启用临时记录时才尝
|
|
|
132
136
|
|
|
133
137
|
## 头部与操作按钮
|
|
134
138
|
|
|
135
|
-
以当前 objId/layoutId
|
|
139
|
+
以当前 objId/layoutId/模式/记录上下文的卡片按钮为依据,按其位置、顺序、显隐、条件和动作呈现,不能继承列表或另一布局的按钮。编辑、保存、保存并新建等只在本页配置存在时显示;不存在或为空不能统一补齐。尤其不能以“原型补充”为理由增加编辑、保存、删除等业务按钮;没有对应按钮配置就不开放该业务入口。必要的非业务导航/关闭控件可补充,来源记录为 prototype。
|
|
136
140
|
|
|
137
141
|
上一条/下一条沿当前列表筛选和排序后的模拟记录切换;到边界时禁用。直接从布局菜单进入且没有记录上下文时,不伪造可切换记录。展开和关闭应真实可用;编辑中切换或关闭时保护未保存草稿,不静默提交。
|
|
138
142
|
|
|
@@ -282,16 +286,16 @@ POST /api/ebuilder/form/list/listInit
|
|
|
282
286
|
|
|
283
287
|
从 `data` 动态取得 `appId`、`objid`、`name`、`conditionId` 和 `viewType`,随后读取六类配置:
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
基础设置、显示列、搜索、统计、条件固定通过后台接口 `/api/bs/ebuilder/form/...` 读取,URL 额外携带小写 `appid={appId}`,需要应用后台权限。排序沿用 `/api/ebuilder/form/...` 的部署路由。
|
|
286
290
|
|
|
287
|
-
| 分类 | 接口
|
|
288
|
-
| -------- |
|
|
289
|
-
| 基础设置 | `GET /api/ebuilder/form/list/getBaseInfo/{tableId}` | `data` |
|
|
290
|
-
| 显示列 | `GET /api/ebuilder/form/list/getListFields/{tableId}` | `data.pc`、`data.mobile` 等 |
|
|
291
|
-
| 排序 | `GET /api/ebuilder/form/viewlist/getViewOrder`
|
|
292
|
-
| 搜索 | `GET /api/ebuilder/form/listFilter/getSearchFilters/{tableId}` | `data` |
|
|
293
|
-
| 统计 | `GET /api/ebuilder/form/listStatistics/getStatisticsList/{tableId}` | `data` |
|
|
294
|
-
| 条件 | `GET /api/ebuilder/form/plugin/conditionEditor/getById` | `data.rootdata` |
|
|
291
|
+
| 分类 | 接口 | 有效数据 |
|
|
292
|
+
| -------- | ---------------------------------------------------------------------- | --------------------------- |
|
|
293
|
+
| 基础设置 | `GET /api/bs/ebuilder/form/list/getBaseInfo/{tableId}` | `data` |
|
|
294
|
+
| 显示列 | `GET /api/bs/ebuilder/form/list/getListFields/{tableId}` | `data.pc`、`data.mobile` 等 |
|
|
295
|
+
| 排序 | `GET /api/ebuilder/form/viewlist/getViewOrder` | `data[]` |
|
|
296
|
+
| 搜索 | `GET /api/bs/ebuilder/form/listFilter/getSearchFilters/{tableId}` | `data` |
|
|
297
|
+
| 统计 | `GET /api/bs/ebuilder/form/listStatistics/getStatisticsList/{tableId}` | `data` |
|
|
298
|
+
| 条件 | `GET /api/bs/ebuilder/form/plugin/conditionEditor/getById` | `data.rootdata` |
|
|
295
299
|
|
|
296
300
|
配置接口的公共参数为 `apid={appId}`、`tpaid={tableId}`、`objId={objId}`。显示列还传 `clientType=PC`;显示列、排序、搜索、统计传 `isVirtualForm=false`;排序传 `viewType=tableView&viewId={tableId}`;搜索传 `viewType=tableView`;条件传 `id={conditionId}&type=search`。
|
|
297
301
|
|
|
@@ -376,6 +380,8 @@ POST /api/ebuilder/form/list/listInit
|
|
|
376
380
|
- `config.conditions.configured=true`:`rootData` 是解析后的条件树。
|
|
377
381
|
- `config.conditions.configured=false`:未配置固定条件,即使 `conditionId` 本身非空也不要误判。
|
|
378
382
|
|
|
383
|
+
条件接口明确成功但没有 `data`,或 `data=null`,也按未配置条件处理。只有此接口允许成功响应缺少 `data`;权限或业务失败不能当作空条件。
|
|
384
|
+
|
|
379
385
|
条件树保留服务端节点、逻辑关系、比较符和值,不擅自改写为搜索配置。
|
|
380
386
|
|
|
381
387
|
# 验收与交付
|
|
@@ -537,3 +543,5 @@ combinedPages 保存组合标题等配置;pageBindings 为容器提供 present
|
|
|
537
543
|
最小验证覆盖:外层 VIEWPORT 被识别为组合;请求 pid 正确;缺少 URL 末段仍可获取详情;数据列表/表格/布局分别解析;未知子项跳过;两个标签按钮不串;同子菜单被多个组合引用时 pageKey 不冲突。沿用轻量验证要求,不改已有测试应用。
|
|
538
544
|
|
|
539
545
|
本 CLI 的 pageBindings 等价于单菜单输入 page + buttons;组合信息见 navigationPath,所有 API 均已在采集阶段处理。读取 E10FormStore.parameters() 获取经主框架传入的本地参数,禁止加载其中任何远程资源。
|
|
546
|
+
|
|
547
|
+
关联对象首次 load(initial) 会持久化种子,必须依据目标全部字段元数据生成完整记录,不能仅填名称和 ID;否则后续目标菜单会加载缺字段的种子。已存记录使用 load 返回值,不覆盖已有数据。
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
}
|
|
64
64
|
.app {
|
|
65
65
|
display: grid;
|
|
66
|
-
grid-template-columns:
|
|
66
|
+
grid-template-columns: 244px minmax(0, 1fr);
|
|
67
67
|
height: 100vh;
|
|
68
68
|
height: 100dvh;
|
|
69
69
|
}
|
|
@@ -71,30 +71,29 @@
|
|
|
71
71
|
display: flex;
|
|
72
72
|
flex-direction: column;
|
|
73
73
|
min-height: 0;
|
|
74
|
-
padding:
|
|
75
|
-
background: #
|
|
76
|
-
border-right: 1px solid #
|
|
74
|
+
padding: 18px 12px 14px;
|
|
75
|
+
background: #f9fafc;
|
|
76
|
+
border-right: 1px solid #dce2e9;
|
|
77
77
|
}
|
|
78
78
|
.brand {
|
|
79
79
|
display: flex;
|
|
80
80
|
align-items: center;
|
|
81
|
-
gap:
|
|
82
|
-
padding: 0
|
|
81
|
+
gap: 10px;
|
|
82
|
+
padding: 0 8px 18px;
|
|
83
83
|
}
|
|
84
84
|
.brand-mark {
|
|
85
85
|
display: grid;
|
|
86
86
|
place-items: center;
|
|
87
|
-
width:
|
|
88
|
-
height:
|
|
87
|
+
width: 32px;
|
|
88
|
+
height: 32px;
|
|
89
89
|
flex: none;
|
|
90
|
-
border-radius:
|
|
90
|
+
border-radius: 4px;
|
|
91
91
|
color: #fff;
|
|
92
|
-
background: #
|
|
93
|
-
box-shadow: 0 4px 12px #2563eb1a;
|
|
92
|
+
background: #334d6e;
|
|
94
93
|
}
|
|
95
94
|
.brand-mark svg {
|
|
96
|
-
width:
|
|
97
|
-
height:
|
|
95
|
+
width: 19px;
|
|
96
|
+
height: 19px;
|
|
98
97
|
}
|
|
99
98
|
.brand-name {
|
|
100
99
|
font-size: 16px;
|
|
@@ -106,28 +105,28 @@
|
|
|
106
105
|
}
|
|
107
106
|
.search {
|
|
108
107
|
position: relative;
|
|
109
|
-
margin: 0 4px
|
|
108
|
+
margin: 0 4px 14px;
|
|
110
109
|
}
|
|
111
110
|
.search svg {
|
|
112
111
|
position: absolute;
|
|
113
|
-
left:
|
|
114
|
-
top:
|
|
112
|
+
left: 9px;
|
|
113
|
+
top: 8px;
|
|
115
114
|
width: 16px;
|
|
116
115
|
height: 16px;
|
|
117
|
-
color: #
|
|
116
|
+
color: #7c8797;
|
|
118
117
|
}
|
|
119
118
|
.search input {
|
|
120
119
|
width: 100%;
|
|
121
|
-
height:
|
|
122
|
-
border: 1px solid #
|
|
123
|
-
border-radius:
|
|
124
|
-
background: #
|
|
120
|
+
height: 32px;
|
|
121
|
+
border: 1px solid #dce2e9;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
background: #fff;
|
|
125
124
|
color: #38435a;
|
|
126
|
-
padding: 0
|
|
125
|
+
padding: 0 9px 0 32px;
|
|
127
126
|
font-size: 12px;
|
|
128
127
|
}
|
|
129
128
|
.search input::placeholder {
|
|
130
|
-
color: #
|
|
129
|
+
color: #7c8797;
|
|
131
130
|
}
|
|
132
131
|
.menu {
|
|
133
132
|
flex: 1;
|
|
@@ -138,7 +137,7 @@
|
|
|
138
137
|
scrollbar-color: #d9dce7 transparent;
|
|
139
138
|
}
|
|
140
139
|
.section {
|
|
141
|
-
margin-bottom:
|
|
140
|
+
margin-bottom: 4px;
|
|
142
141
|
}
|
|
143
142
|
.section-title {
|
|
144
143
|
display: flex;
|
|
@@ -147,13 +146,26 @@
|
|
|
147
146
|
list-style: none;
|
|
148
147
|
cursor: pointer;
|
|
149
148
|
user-select: none;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
min-height: 36px;
|
|
150
|
+
gap: 6px;
|
|
151
|
+
padding: 7px 8px;
|
|
152
|
+
margin-bottom: 2px;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
font-weight: 600;
|
|
155
|
+
line-height: 1.5;
|
|
156
|
+
color: #36465b;
|
|
155
157
|
border-radius: 4px;
|
|
156
158
|
}
|
|
159
|
+
.section-title:hover {
|
|
160
|
+
background: #edf0f4;
|
|
161
|
+
}
|
|
162
|
+
.section .section > .section-title {
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
color: #536176;
|
|
165
|
+
}
|
|
166
|
+
.section.active-ancestor > .section-title {
|
|
167
|
+
color: #294e78;
|
|
168
|
+
}
|
|
157
169
|
.section-title::-webkit-details-marker {
|
|
158
170
|
display: none;
|
|
159
171
|
}
|
|
@@ -161,35 +173,36 @@
|
|
|
161
173
|
display: inline-flex;
|
|
162
174
|
align-items: center;
|
|
163
175
|
gap: 7px;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
overflow-wrap: anywhere;
|
|
164
178
|
}
|
|
165
179
|
.section-title svg {
|
|
166
180
|
width: 12px;
|
|
167
181
|
height: 12px;
|
|
168
182
|
transition: transform 0.15s;
|
|
169
183
|
}
|
|
170
|
-
.section:not([open]) .section-title svg {
|
|
184
|
+
.section:not([open]) > .section-title > .section-name > svg {
|
|
171
185
|
transform: rotate(-90deg);
|
|
172
186
|
}
|
|
173
187
|
.section-count {
|
|
174
|
-
min-width:
|
|
175
|
-
|
|
188
|
+
min-width: 16px;
|
|
189
|
+
flex-shrink: 0;
|
|
190
|
+
line-height: 18px;
|
|
176
191
|
text-align: center;
|
|
177
|
-
border-radius: 5px;
|
|
178
|
-
background: #f2f4f8;
|
|
179
192
|
font-size: 10px;
|
|
180
193
|
font-weight: 400;
|
|
181
|
-
color: #
|
|
194
|
+
color: #7c8797;
|
|
182
195
|
}
|
|
183
196
|
.nav-item {
|
|
184
197
|
display: flex;
|
|
185
198
|
align-items: center;
|
|
186
|
-
gap:
|
|
199
|
+
gap: 8px;
|
|
187
200
|
position: relative;
|
|
188
|
-
min-height:
|
|
189
|
-
padding:
|
|
190
|
-
margin:
|
|
191
|
-
border-radius:
|
|
192
|
-
color: #
|
|
201
|
+
min-height: 34px;
|
|
202
|
+
padding: 6px 8px;
|
|
203
|
+
margin: 2px 0;
|
|
204
|
+
border-radius: 4px;
|
|
205
|
+
color: #536176;
|
|
193
206
|
font-size: 13px;
|
|
194
207
|
line-height: 1.55;
|
|
195
208
|
transition:
|
|
@@ -197,13 +210,13 @@
|
|
|
197
210
|
color 0.15s;
|
|
198
211
|
}
|
|
199
212
|
.nav-item:hover {
|
|
200
|
-
background: #
|
|
213
|
+
background: #edf0f4;
|
|
201
214
|
color: #303d55;
|
|
202
215
|
}
|
|
203
216
|
.nav-item svg {
|
|
204
|
-
width:
|
|
205
|
-
height:
|
|
206
|
-
color: #
|
|
217
|
+
width: 16px;
|
|
218
|
+
height: 16px;
|
|
219
|
+
color: #7c899b;
|
|
207
220
|
}
|
|
208
221
|
.nav-item .label {
|
|
209
222
|
flex: 1;
|
|
@@ -211,40 +224,30 @@
|
|
|
211
224
|
overflow-wrap: anywhere;
|
|
212
225
|
}
|
|
213
226
|
.nav-item[aria-current='page'] {
|
|
214
|
-
background: #
|
|
215
|
-
color: #
|
|
227
|
+
background: #e5edf6;
|
|
228
|
+
color: #294e78;
|
|
216
229
|
font-weight: 600;
|
|
217
230
|
}
|
|
218
231
|
.nav-item[aria-current='page'] svg {
|
|
219
|
-
color: #
|
|
232
|
+
color: #365c86;
|
|
220
233
|
}
|
|
221
234
|
.nav-item[aria-current='page']:before {
|
|
222
235
|
content: '';
|
|
223
236
|
position: absolute;
|
|
224
|
-
left:
|
|
225
|
-
top:
|
|
226
|
-
bottom:
|
|
227
|
-
width:
|
|
228
|
-
border-radius:
|
|
229
|
-
background: #
|
|
230
|
-
}
|
|
231
|
-
.nav-item[data-kind='form'] svg {
|
|
232
|
-
color: #c29861;
|
|
233
|
-
}
|
|
234
|
-
.nav-item[data-kind='form'][aria-current='page'] {
|
|
235
|
-
background: #fbf3e8;
|
|
236
|
-
color: #99743d;
|
|
237
|
-
}
|
|
238
|
-
.nav-item[data-kind='form'][aria-current='page']:before {
|
|
239
|
-
background: #cea160;
|
|
237
|
+
left: 0;
|
|
238
|
+
top: 7px;
|
|
239
|
+
bottom: 7px;
|
|
240
|
+
width: 2px;
|
|
241
|
+
border-radius: 1px;
|
|
242
|
+
background: #365c86;
|
|
240
243
|
}
|
|
241
244
|
.tag {
|
|
242
245
|
font-size: 9px;
|
|
243
246
|
line-height: 1.5;
|
|
244
|
-
border: 1px solid #
|
|
245
|
-
border-radius:
|
|
247
|
+
border: 1px solid #dce2e9;
|
|
248
|
+
border-radius: 3px;
|
|
246
249
|
padding: 2px 4px;
|
|
247
|
-
color: #
|
|
250
|
+
color: #748094;
|
|
248
251
|
white-space: nowrap;
|
|
249
252
|
font-weight: 400;
|
|
250
253
|
}
|
|
@@ -266,7 +269,7 @@
|
|
|
266
269
|
flex: 1;
|
|
267
270
|
min-width: 0;
|
|
268
271
|
min-height: 0;
|
|
269
|
-
padding:
|
|
272
|
+
padding: 14px;
|
|
270
273
|
}
|
|
271
274
|
.content-panel {
|
|
272
275
|
display: flex;
|
|
@@ -276,13 +279,14 @@
|
|
|
276
279
|
min-height: 0;
|
|
277
280
|
background: #fff;
|
|
278
281
|
border: 1px solid #dfe4ed;
|
|
279
|
-
border-radius:
|
|
282
|
+
border-radius: 6px;
|
|
280
283
|
overflow: hidden;
|
|
281
|
-
box-shadow: 0 3px 16px #26385607;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
.menu-children {
|
|
287
|
+
margin-left: 13px;
|
|
285
288
|
padding-left: 10px;
|
|
289
|
+
border-left: 1px solid #dce2e9;
|
|
286
290
|
}
|
|
287
291
|
.combined-tabs {
|
|
288
292
|
display: flex;
|
|
@@ -301,8 +305,8 @@
|
|
|
301
305
|
white-space: nowrap;
|
|
302
306
|
}
|
|
303
307
|
.combined-tabs button[aria-selected='true'] {
|
|
304
|
-
color: #
|
|
305
|
-
border-bottom-color: #
|
|
308
|
+
color: #294e78;
|
|
309
|
+
border-bottom-color: #365c86;
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
.page-content {
|
|
@@ -338,13 +342,13 @@
|
|
|
338
342
|
}
|
|
339
343
|
@media (max-width: 1050px) {
|
|
340
344
|
.content-area {
|
|
341
|
-
padding:
|
|
345
|
+
padding: 12px;
|
|
342
346
|
}
|
|
343
347
|
.app {
|
|
344
348
|
grid-template-columns: 232px minmax(0, 1fr);
|
|
345
349
|
}
|
|
346
350
|
.sidebar {
|
|
347
|
-
padding:
|
|
351
|
+
padding: 18px 10px 14px;
|
|
348
352
|
}
|
|
349
353
|
.brand {
|
|
350
354
|
padding-left: 8px;
|
|
@@ -356,13 +360,13 @@
|
|
|
356
360
|
}
|
|
357
361
|
@media (max-width: 700px) {
|
|
358
362
|
.content-panel {
|
|
359
|
-
border-radius:
|
|
363
|
+
border-radius: 6px;
|
|
360
364
|
}
|
|
361
365
|
.content-actions {
|
|
362
366
|
padding: 10px 12px;
|
|
363
367
|
}
|
|
364
368
|
.content-area {
|
|
365
|
-
padding:
|
|
369
|
+
padding: 8px;
|
|
366
370
|
}
|
|
367
371
|
.app {
|
|
368
372
|
display: block;
|
|
@@ -384,6 +388,10 @@
|
|
|
384
388
|
.app.menu-open .sidebar {
|
|
385
389
|
transform: translateX(0);
|
|
386
390
|
}
|
|
391
|
+
.nav-item,
|
|
392
|
+
.section-title {
|
|
393
|
+
min-height: 40px;
|
|
394
|
+
}
|
|
387
395
|
.mobile-bar {
|
|
388
396
|
height: 52px;
|
|
389
397
|
flex: none;
|
|
@@ -440,6 +448,7 @@
|
|
|
440
448
|
<path d="m16 16 4.5 4.5" />
|
|
441
449
|
</symbol>
|
|
442
450
|
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" /></symbol>
|
|
451
|
+
<symbol id="i-chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></symbol>
|
|
443
452
|
</defs>
|
|
444
453
|
</svg>
|
|
445
454
|
<div class="app" id="app">
|
|
@@ -511,13 +520,14 @@
|
|
|
511
520
|
.filter((group) => group.children.length)
|
|
512
521
|
const routeKey = (node) => (data.menus ? 'menu/' + node.id : node.id)
|
|
513
522
|
const flatten = (nodes) => nodes.flatMap((node) => [node, ...flatten(node.children || [])])
|
|
514
|
-
function link(node) {
|
|
523
|
+
function link(node, depth) {
|
|
515
524
|
const a = document.createElement('a'),
|
|
516
525
|
target = node.target
|
|
517
526
|
const key = routeKey(node),
|
|
518
527
|
item = itemMap.get(target.kind + '/' + target.id)
|
|
519
528
|
a.className = 'nav-item'
|
|
520
529
|
a.dataset.kind = target.kind
|
|
530
|
+
a.dataset.depth = depth
|
|
521
531
|
a.href = '#' + key
|
|
522
532
|
a.title = node.name
|
|
523
533
|
a.addEventListener('click', closeMenu)
|
|
@@ -544,12 +554,13 @@
|
|
|
544
554
|
routes.set(key, node)
|
|
545
555
|
return a
|
|
546
556
|
}
|
|
547
|
-
function render(nodes, parent) {
|
|
557
|
+
function render(nodes, parent, depth = 0) {
|
|
548
558
|
for (const node of nodes) {
|
|
549
559
|
const children = node.children || []
|
|
550
560
|
if (children.length) {
|
|
551
561
|
const details = document.createElement('details')
|
|
552
562
|
details.className = 'section'
|
|
563
|
+
details.dataset.depth = depth
|
|
553
564
|
details.open = true
|
|
554
565
|
details.id = node.legacy ? node.id : 'group-' + node.id
|
|
555
566
|
const summary = document.createElement('summary')
|
|
@@ -557,6 +568,12 @@
|
|
|
557
568
|
const label = document.createElement('span')
|
|
558
569
|
label.className = 'section-name'
|
|
559
570
|
label.textContent = node.name
|
|
571
|
+
const chevron = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
572
|
+
chevron.setAttribute('aria-hidden', 'true')
|
|
573
|
+
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use')
|
|
574
|
+
use.setAttribute('href', '#i-chevron')
|
|
575
|
+
chevron.append(use)
|
|
576
|
+
label.prepend(chevron)
|
|
560
577
|
summary.append(label)
|
|
561
578
|
const count = document.createElement('span')
|
|
562
579
|
count.className = 'section-count'
|
|
@@ -567,12 +584,12 @@
|
|
|
567
584
|
const body = document.createElement('div')
|
|
568
585
|
body.className = 'menu-children'
|
|
569
586
|
if (node.legacy) body.id = node.legacy + '-menu'
|
|
570
|
-
if (node.target) body.append(link(node))
|
|
571
|
-
render(children, body)
|
|
587
|
+
if (node.target) body.append(link(node, depth + 1))
|
|
588
|
+
render(children, body, depth + 1)
|
|
572
589
|
details.append(body)
|
|
573
590
|
parent.append(details)
|
|
574
591
|
groups.push(details)
|
|
575
|
-
} else if (node.target) parent.append(link(node))
|
|
592
|
+
} else if (node.target) parent.append(link(node, depth))
|
|
576
593
|
}
|
|
577
594
|
}
|
|
578
595
|
const fragment = document.createDocumentFragment()
|
|
@@ -593,6 +610,8 @@
|
|
|
593
610
|
if (route === key) a.setAttribute('aria-current', 'page')
|
|
594
611
|
else a.removeAttribute('aria-current')
|
|
595
612
|
}
|
|
613
|
+
for (const group of groups)
|
|
614
|
+
group.classList.toggle('active-ancestor', group.contains(links.get(key)))
|
|
596
615
|
let active = node,
|
|
597
616
|
selected = key
|
|
598
617
|
const tabs = $('combined-tabs')
|