@pactor-app/skills 1.35.0 → 1.41.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pactor-app/skills",
3
- "version": "1.35.0",
3
+ "version": "1.41.0",
4
4
  "description": "Agent skills for rapidly building pactor Server-Driven UI applications",
5
5
  "keywords": [
6
6
  "pactor",
@@ -181,7 +181,33 @@ Facts that are NOT guessable and must be followed:
181
181
 
182
182
  - Top level is `{ dsl, page, state?, dataSources?, actions?, components?, lifecycle?, body? }` — `page` is a wrapper object, `dsl: '1.0'` is mandatory.
183
183
  - `Table` reads data via an **expression** (`dataSource: ${data.customers}`), not a source name; column templates use the `cell` key (not `render`) with `${row.x}` scope; pagination is `pagination: { pageSize: 5 }`.
184
- - Form fields bind via `props.name`; the submit button uses `submit: true` (not `htmlType`); `Form onSubmit` receives all values as `${event.<field>}`.
184
+ - **Inside `Form`, every form control must be wrapped in a `Field`** (`Input`/`NumberInput`/`Select`/`Textarea`/`RadioGroup`/`Checkbox`/`Switch`/`Slider`/`DatePicker` …): label / required / description / error belong to `Field`, the control keeps only its own props (`name` / `placeholder` / `value` / `options` / `rules`). Search-bar buttons (查询/重置) also go inside a `Field` so they align with the inputs; **multiple buttons in one `Field` are placed side by side directly — adjacent buttons get spacing automatically (`[&>button+button]:ml-2`), no extra wrapper or margin needed** (use a `Flex` with `gap` inside the `Field` only when aligning mixed content, e.g. a right-aligned button row). Fields bind via `props.name`; the submit button uses `submit: true` (not `htmlType`); `Form onSubmit` receives all values as `${event.<field>}`.
185
+
186
+ ```yaml
187
+ - type: Form
188
+ props: { layout: inline }
189
+ children:
190
+ - type: Field
191
+ props: { label: 关键词 }
192
+ children:
193
+ - type: Input
194
+ props: { name: keyword, placeholder: 输入关键词 }
195
+ - type: Field
196
+ props: { label: 状态 }
197
+ children:
198
+ - type: Select
199
+ props:
200
+ name: status
201
+ options:
202
+ - { label: 启用, value: enabled }
203
+ - { label: 停用, value: disabled }
204
+ - type: Field # 查询/重置按钮同样放 Field 内,与输入框对齐
205
+ children:
206
+ - type: Button
207
+ props: { text: 查询, variant: primary, submit: true }
208
+ - type: Button
209
+ props: { text: 重置 }
210
+ ```
185
211
  - `navigate` takes `to`; `message` takes `text` + `messageType` (a `type` key in action `params` disables expression evaluation).
186
212
  - Query linkage = `onSubmit: [setState, reload]`; the data source's `params` referencing `${state.x}` pick up new values on reload.
187
213
 
@@ -357,6 +383,7 @@ pnpm pactor create my-app
357
383
  - **Do not** rely on frontend permission checks for security; enforce authorization on backend APIs.
358
384
  - **Do not** forget to import `@pactor-app/ui/styles.css` when using the default shadcn kit (`@pactor-app/chat/styles.css` additionally for chat surfaces).
359
385
  - **Do not** put a `type` key in action `params` (see the events workflow pitfall).
386
+ - **Do not** place bare form controls directly under `Form`; wrap every control (including search-bar buttons) in a `Field`.
360
387
  - **Do not** expect DslNode slots (sidebar / toolbar / inspector / …) to render outside a page runtime; in pure React hosts pass ReactNode instead.
361
388
 
362
389
  ## References (bundled — works from npm install)
@@ -178,7 +178,9 @@ Event payloads (`${event}`):
178
178
  | Button `onClick` | none |
179
179
  | Input / Select / Switch `onChange` | the new value (`${event}`) |
180
180
  | Tabs `onChange` | new active key |
181
- | Table `onChange` | `{ current, pageSize, total }` |
181
+ | Table `onChange` | `{ current, pageSize, total, field?, order? }` |
182
+ | Table `onSelectChange` | selected row keys array (`rowSelection: true`) |
183
+ | Table `onRowDrop` | `{ dragKey, dropKey, position: 'before' \| 'after' }` (`rowDraggable: true`) |
182
184
  | Form `onSubmit` | all values (`${event.name}`) |
183
185
  | Form `onValuesChange` | ONLY the changed field `{ <field>: newValue }` |
184
186
 
@@ -198,6 +200,7 @@ Action catalog:
198
200
  | `call` | `name` | run a top-level named action, inherits caller scope |
199
201
  | `delay` | `ms` | wait |
200
202
  | `form.setValues` | `values` (req), `form?` (default `default`) | write field values; does NOT trigger validation or onValuesChange |
203
+ | `form.submit` | `form?` (default `default`) | trigger the form's native submit — runs validation and `onSubmit`; for submit buttons outside the `<form>` DOM (e.g. Drawer `footer` slot) |
201
204
  | `overlay.open` / `overlay.close` | see below | open overlay and await close; close `payload` becomes open's result |
202
205
 
203
206
  `overlay.open` params: `{ overlay: { type: modal|drawer, title?, width?, content: <DslNode> | page: { pageId }, params? } }`. Overlay content evaluates in overlay scope: `${params.x}` = injected params, `${parent.x}` = parent page snapshot, `${state}`/`${data}` still live.
@@ -249,17 +252,24 @@ body:
249
252
  - action: reload
250
253
  params: { dataSource: customers }
251
254
  children:
252
- - type: Input
253
- props: { name: keyword, label: Keyword, placeholder: Name / owner }
254
- - type: Select
255
- props:
256
- name: status
257
- label: Status
258
- options:
259
- - { label: Enabled, value: active }
260
- - { label: Disabled, value: inactive }
261
- - type: Button
262
- props: { text: Search, variant: primary, submit: true } # submit: true, NOT htmlType
255
+ - type: Field # Form 内控件必须放 Field 内(label 归 Field)
256
+ props: { label: Keyword }
257
+ children:
258
+ - type: Input
259
+ props: { name: keyword, placeholder: Name / owner }
260
+ - type: Field
261
+ props: { label: Status }
262
+ children:
263
+ - type: Select
264
+ props:
265
+ name: status
266
+ options:
267
+ - { label: Enabled, value: active }
268
+ - { label: Disabled, value: inactive }
269
+ - type: Field # 查询按钮也放 Field 内,与输入框对齐
270
+ children:
271
+ - type: Button
272
+ props: { text: Search, variant: primary, submit: true } # submit: true, NOT htmlType
263
273
  - type: Card
264
274
  props: { title: Customer Data }
265
275
  children:
@@ -315,15 +325,19 @@ events:
315
325
  - action: message
316
326
  params: { messageType: error, text: Save failed, please retry }
317
327
  children:
318
- - type: Input
319
- props:
320
- name: name
321
- label: Name
322
- value: ${params.name} # field-level initial value (row data backfill)
323
- rules:
324
- - { required: true, message: Please enter a name }
325
- - type: Button
326
- props: { text: Save, variant: primary, submit: true }
328
+ - type: Field
329
+ props: { label: Name, required: true }
330
+ children:
331
+ - type: Input
332
+ props:
333
+ name: name
334
+ value: ${params.name} # field-level initial value (row data backfill)
335
+ rules:
336
+ - { required: true, message: Please enter a name }
337
+ - type: Field
338
+ children:
339
+ - type: Button
340
+ props: { text: Save, variant: primary, submit: true }
327
341
  - action: if # runs after the drawer closes; cancel (X / mask / Esc) skips
328
342
  params:
329
343
  condition: ${actions.editResult.success}
@@ -334,6 +348,60 @@ events:
334
348
  params: { messageType: success, text: Customer updated }
335
349
  ```
336
350
 
351
+ ### 8.2.1 Controlled overlay nodes (state + `open` expression)
352
+
353
+ Alternative to `overlay.open`: declare `Drawer` / `Dialog` / `AlertDialog` as ordinary DSL nodes with `open: ${state.x}` and drive them with `setState`. Use this when the overlay is a fixed part of the page tree (templates, statically known content). Key wiring:
354
+
355
+ ```yaml
356
+ state: { drawerOpen: false }
357
+ body:
358
+ type: Page
359
+ children:
360
+ - type: Button # open
361
+ props: { text: 新建, variant: primary }
362
+ events:
363
+ onClick:
364
+ - action: setState
365
+ params: { drawerOpen: true }
366
+ - type: Drawer
367
+ props:
368
+ title: 编辑
369
+ open: "${state.drawerOpen}"
370
+ style: { width: 480, maxWidth: 'none' }
371
+ okText: 保存 # 默认 footer [取消, 保存]
372
+ events:
373
+ onClose: # 关闭图标 / mask / Esc / 默认「取消」
374
+ - action: setState
375
+ params: { drawerOpen: false }
376
+ onOk: # 默认「保存」→ form.submit 桥接校验 + onSubmit
377
+ - action: form.submit
378
+ children:
379
+ - type: Form
380
+ events:
381
+ onSubmit:
382
+ - action: message
383
+ params: { messageType: success, text: 保存成功 }
384
+ - action: setState
385
+ params: { drawerOpen: false }
386
+ children:
387
+ - type: Field
388
+ props: { label: 名称, required: true }
389
+ children:
390
+ - type: Input
391
+ props:
392
+ name: name
393
+ rules:
394
+ - { required: true, message: 请输入名称 }
395
+ ```
396
+
397
+ Rules of thumb:
398
+
399
+ - `Drawer` 默认带右上角关闭图标(`closable: false` 关闭)与默认 footer([取消 → onClose, 确定 → onOk],右对齐、固定底部);关闭链路在未 wiring `onClose` 时也会默认自闭合,wiring `onClose` 回写 state 是推荐做法。
400
+ - 表单抽屉优先用 `okText` + `events.onOk: form.submit`(最简);需要非按钮组布局时才给 `footer` 传 DslNode 或 `false`。
401
+ - A `submit: true` button only submits the enclosing `<form>`; from the `footer` slot (outside the `<form>` DOM) use the `form.submit` action instead — it runs the same validation + `onSubmit` chain.
402
+ - `AlertDialog` in controlled mode never self-closes: wire BOTH `events.onConfirm` and `events.onCancel` to `setState x=false`.
403
+ - `${form.x}` is written to state only on successful submit; for per-step data in a wizard, submit at each step (or read Form values only after submit).
404
+
337
405
  ### 8.3 Server-side pagination
338
406
 
339
407
  Table's own `pagination` paginates the data already bound to it (client-side). For a server-paginated API, turn it off and pair the table with a standalone `Pagination` component driving state + reload:
@@ -372,6 +440,28 @@ dataSources:
372
440
 
373
441
  Table also supports `rowKey` (row key field, default `'id'`); Table `onChange` (`{ current, pageSize, total }`) exists for its built-in client-side pager.
374
442
 
443
+ ### 8.3.1 Table extras: tree data, row drag, sticky header
444
+
445
+ ```yaml
446
+ - type: Table
447
+ props:
448
+ dataSource: ${data.menus} # rows containing a `children` array render as a tree table automatically
449
+ childrenColumnName: children # default; set another field name, or false to disable tree mode
450
+ indentSize: 16 # px per tree level (default 16); expand toggles render in the first column
451
+ defaultExpandAllRows: true # start fully expanded (default: all collapsed, toggle via the row arrow)
452
+ rowDraggable: true # grip handle column; drag to reorder within the same parent level
453
+ scrollY: 400 # px; body scrolls vertically with the header stuck on top
454
+ events:
455
+ onRowDrop: # reorder already applied internally; use this to persist
456
+ - action: request
457
+ params:
458
+ url: /api/menus/reorder
459
+ method: POST
460
+ body: ${event} # { dragKey, dropKey, position }
461
+ ```
462
+
463
+ Tree tables sort recursively per level and paginate by top-level rows. Sortable columns (`sorter: true`), `rowSelection`, fixed columns (`fixed: right` on the action column) all compose with the above.
464
+
375
465
  ### 8.4 Confirm then act
376
466
 
377
467
  ```yaml
@@ -396,23 +486,67 @@ events:
396
486
  state:
397
487
  cityMap: { Zhejiang: [{ label: Hangzhou, value: Hangzhou }], Jiangsu: [{ label: Nanjing, value: Nanjing }] }
398
488
  cityOptions: []
399
- # fields:
400
- - type: Select
401
- props: { name: region, label: Province, options: [{ label: Zhejiang, value: Zhejiang }, { label: Jiangsu, value: Jiangsu }] }
402
- events:
403
- onChange:
404
- - action: form.setValues # clear the child field first
405
- params: { values: { city: '' } } # add `form: edit` when targeting a named form
406
- - action: setState # then update child options (computed member access)
407
- params: { cityOptions: ${state.cityMap[event]} }
408
- - type: Select
409
- props:
410
- name: city
411
- label: City
412
- options: ${state.cityOptions}
413
- disabled: ${state.cityOptions.length === 0}
489
+ # fields (each control wrapped in Field, as required inside Form):
490
+ - type: Field
491
+ props: { label: Province }
492
+ children:
493
+ - type: Select
494
+ props: { name: region, options: [{ label: Zhejiang, value: Zhejiang }, { label: Jiangsu, value: Jiangsu }] }
495
+ events:
496
+ onChange:
497
+ - action: form.setValues # clear the child field first
498
+ params: { values: { city: '' } } # add `form: edit` when targeting a named form
499
+ - action: setState # then update child options (computed member access)
500
+ params: { cityOptions: ${state.cityMap[event]} }
501
+ - type: Field
502
+ props: { label: City }
503
+ children:
504
+ - type: Select
505
+ props:
506
+ name: city
507
+ options: ${state.cityOptions}
508
+ disabled: ${state.cityOptions.length === 0}
414
509
  ```
415
510
 
511
+ ### 8.6 Card list page (CardList / EntityCard business components)
512
+
513
+ `CardList` renders a grid of `EntityCard` summary cards in one node — no `For` + nested `Flex`/`Text`/`Tag` scaffolding. Item fields mirror `EntityCard` props; extra business fields ride along and come back in event payloads.
514
+
515
+ ```yaml
516
+ state:
517
+ plugins:
518
+ - id: 1
519
+ icon: users # Icon whitelist name (or `logo` image URL)
520
+ title: 用户管理
521
+ subtitle: v0.1.0 # auxiliary line under the title
522
+ description: 管理插件安装状态与运行生命周期,支持启停与依赖检查。
523
+ tags: [{ text: 已安装, color: success }] # Tag colors: default/success/warning/error/info
524
+ actions: [{ text: 停用 }] # footer buttons, right-aligned, muted bar
525
+ body:
526
+ - type: CardList
527
+ props:
528
+ items: ${state.plugins}
529
+ columns: 3 # grid columns, default 3; 1 = single wide rows
530
+ gap: 16 # default 16
531
+ showCount: true # footer "共 N 个" count line
532
+ # createText: 新建插件 # dashed "create" placeholder card in the first cell
533
+ # hoverable: true # whole-card pointer + hover highlight
534
+ events:
535
+ onCreateClick: { action: message, params: { messageType: info, text: open create dialog } }
536
+ onItemClick: { action: message, params: { messageType: info, text: "进入详情:${event.item.title}" } }
537
+ onItemAction: # ${event} = { key, text, item, index }
538
+ - action: confirm
539
+ id: opConfirm
540
+ params: { title: "确认${event.text}「${event.item.title}」?" }
541
+ - action: if
542
+ params:
543
+ condition: ${actions.opConfirm}
544
+ then: [{ action: message, params: { messageType: success, text: done } }]
545
+ ```
546
+
547
+ - `EntityCard` standalone: `icon`/`logo`, `title`, `subtitle`, `tags[]`, `description` (two-line clamp), `meta` (right side of the middle row), `actions[]` (`{ text, key?, variant?, danger? }`), `footerBackground` (muted footer bar, default `true`), `hoverable`. Events `onClick` (whole card) and `onAction` (`{ key, text, index }`) — footer button clicks never trigger the card click.
548
+ - Empty `items` renders an `Empty` state (`emptyText` overrides the default).
549
+
416
550
  ## 9. Common mistakes (observed — do not do these)
417
551
 
418
552
  | Wrong | Right |
@@ -423,6 +557,10 @@ state:
423
557
  | Column template key `render` | `cell` |
424
558
  | `pagination: true` | `pagination: { pageSize: 10 }` |
425
559
  | `htmlType: submit` | `submit: true` |
560
+ | Bare `Input`/`Select`/`Button` directly under `Form` | Wrap every form control (incl. search-bar buttons) in `Field`; label / required / description / error belong to `Field` |
561
+ | Relying on `Field required: true` for validation | It is a visual `*` marker only — declare `rules: [{ required: true, message: ... }]` on the CONTROL (`Input`/`Select`/…); Form validates control rules on submit |
562
+ | `style` / `className` as node-level keys (sibling of `props:`) | They are component props — always nest them inside `props: { className: ..., style: ... }`; node-level keys are silently ignored |
563
+ | `children` nested inside `props:` | `children` is a NODE-level key (sibling of `props:`); nested under `props:` it is silently dropped |
426
564
  | `navigate` param `path` / `url` | `to` |
427
565
  | `message` param `type: success` | `messageType: success` — a `type` (or `action`) key inside action `params` makes the engine treat params as a nested DSL/action config and SKIP expression evaluation |
428
566
  | Reading full form values in `onValuesChange` | It only carries the changed field; use `onSubmit` or `${form.x}` |
@@ -2,7 +2,7 @@
2
2
  name: pactor-prd
3
3
  description: |
4
4
  Use when PRD or markdown wireframes drive Pactor YAML pages (skill id pactor-prd). Greenfield: copy template/ into a new repo. Incremental: in an existing Pactor project, add or update pages from new/changed wireframe files (DSL, sitemap, menus, data API, styles). Triggers: pactor-prd, prd-to-app, 线框图增量, 已有项目加页面, PRD 更新页面.
5
- version: 1.4.0
5
+ version: 1.5.0
6
6
  ---
7
7
 
8
8
  # PRD → YAML 页面应用生成
@@ -17,6 +17,7 @@ version: 1.4.0
17
17
  ## 输入要求
18
18
 
19
19
  - PRD/线框图:总览 README(模块 × 页面 × 优先级)+ 分页线框 markdown(ASCII + 区域 + 交互)
20
+ - **接口文档/说明(可选)**:API 路径、方法、参数、返回结构;存在时页面 dataSources 直接对接文档接口,不再走 `/api/d/...` 约定
20
21
  - **增量**:可只提供 **变更的线框图文件** 列表(或 PRD 目录 + 标明 diff 范围)
21
22
  - 原产品代码仓(可选):抽设计令牌、自定义 CSS、交互
22
23
  - 目标:空目录(新建工程)或已有工程根目录(增量)
@@ -25,7 +26,7 @@ version: 1.4.0
25
26
 
26
27
  ## 红线(必须遵守)
27
28
 
28
- 1. **dataSources 一律 `type: http`**,指向 `/api/d/...`,不写 `type: service`,便于后续替换真实接口
29
+ 1. **dataSources 一律 `type: http`,不写 `type: service`**;**PRD 含接口文档/说明时,url 取文档中的接口地址**(方法、路径、返回形状对齐,见「1. 输入解析」);无接口文档时指向 `/api/d/...`(转发层空数据回退,后续按 `references/data-api.md` 接真实接口)
29
30
  2. **布局与间距以样板页为准**:新建工程时以 `template/` 同类页为准;**增量时以目标工程内同模块/同类型页**为准。线框图只提取结构与功能,不逐像素还原 ASCII
30
31
  3. **样式与原产品一致**:主题令牌(含深浅色)、自定义 CSS、换肤入口全部移植对齐,见 `references/theme-and-style.md`
31
32
  4. **功能与原产品一致**:线框图描述的筛选/搜索/跳转/弹窗/提交等交互必须经 events/actions 闭环可用,禁止只画静态骨架;错误分支(校验失败提示等)也要实现
@@ -48,6 +49,7 @@ version: 1.4.0
48
49
 
49
50
  - 读 README / 变更线框图:模块树、页面 id(kebab-case)、path、菜单、P0/P1/P2
50
51
  - 逐页读线框:页面类型、区域、字段、交互
52
+ - **接口文档解析**:PRD 含接口文档/说明时,提取「资源 → 路径 / 方法 / 返回形状」接口清单,页面 dataSources 以该清单为准;无接口文档则按 `references/data-api.md` 命名约定写 `/api/d/...`
51
53
  - **新建工程**:输出全量 **页面清单表**(`references/page-inventory-and-files.md`)
52
54
  - **增量**:扫描工程现状清单 + 输出 **delta 表**(add / update / deprecate,`references/incremental-wireframe-delta.md`);P2 不建文件
53
55
 
@@ -72,7 +74,7 @@ version: 1.4.0
72
74
  - 校验见 `page-inventory-and-files.md`(全量)或 `incremental-wireframe-delta.md`(增量)
73
75
  - 按 `references/component-mapping.md` 映射组件;按 `references/page-templates.md` 选列表/详情/表单模板(**数据列表页直接复制 `yaml/models/data-list.yaml` 替换占位符**);表单一律 `Form` 包裹
74
76
  - 同步更新 `sitemap.yaml` 的 `pages` + `menus`(hidden 详情/编辑带 `activeMenu`)
75
- - 每页 dataSources 全部 `type: http`,url 按 `references/data-api.md` 命名约定直接写最终接口路径(无后端时转发层返回空数据,无需 mock);交互 events + actions 闭环
77
+ - 每页 dataSources 全部 `type: http`:**有接口文档 → url 取文档接口地址**;无 → 按 `references/data-api.md` 命名约定写 `/api/d/...`(转发层空数据回退,无需 mock);交互 events + actions 闭环
76
78
 
77
79
  ### 4. 样式移植
78
80
 
@@ -13,7 +13,7 @@
13
13
  | 下拉筛选(操作人等枚举) | `Select` | `options` 可绑 `${data.xxx}`(接口下发选项,单段路径) |
14
14
  | 日期范围筛选 | `DatePicker` | range 模式绑 state,onChange reload |
15
15
  | 数据表格 | `Table` | `columns` + `dataSource: ${data.x.items}`;操作列按钮统一 `Button variant: link`;「编辑」与「新建」共用同一表单,仅以预填数据区分(initialValues / form.setValues) |
16
- | 卡片网格(约 3-4 列) | `Grid` + `Card` | `columns: 3, gap: 16`;「添加卡」用 `Card` 虚线边框自定义样式 |
16
+ | 卡片网格(约 3-4 列) | `CardList`(业务组件) | 数据项直接携带 `icon`/`title`/`subtitle`/`tags`/`description`/`meta`/`actions` 字段;`columns`、`createText`(首格虚线新建卡)、`showCount`、事件 `onItemClick`/`onItemAction`/`onCreateClick`(`${event}` 含 item/index/key);单卡即 `EntityCard`,无需 `For`+`Card` 手写三层结构 |
17
17
  | 状态徽标(启用/停用/告警) | `Badge` / `StatusBadge` | 颜色语义化,状态文案取自线框图 |
18
18
  | 启停开关图标(⏻) | `StatusToggleIcon` | 卡片/行内启停,onChange 闭环动作 + reload |
19
19
  | 权限控制显隐(无权限隐藏新建卡等) | 表达式 `hidden` / `if` 动作 | 绑 `${data.session.permissions}` 等,按会话裁剪 |
@@ -1,4 +1,6 @@
1
- # 数据接口层(纯转发 + 空数据约定)
1
+ # 数据接口层(接口文档优先 + 纯转发空数据约定)
2
+
3
+ **接口文档驱动(优先)**:PRD/产品文档含接口文档或接口说明时,页面 dataSources 的 url 直接取文档中的接口地址——方法、参数、返回形状均以文档为准,不再使用下述 `/api/d/...` 命名约定。同源相对路径直接写;跨域绝对地址写完整 URL(`type: http` 原生支持)。转发层与命名约定仅在**无接口文档**时兜底。
2
4
 
3
5
  页面 DSL 的 http 数据源统一入口:`app/api/d/[...parts]/route.ts`(**纯转发层,不含任何路径映射**)。**路径即契约**:DSL 中的 url(如 `/api/d/providers`)就是服务端接口路径;转发层按相同路径、方法、查询与请求体原样转发到 `$NEXT_PUBLIC_API_BASE_URL/api/d/<parts>`(携带会话 cookie,匿名 401)。
4
6
 
@@ -25,7 +27,7 @@
25
27
 
26
28
  ## 对页面 yaml 的要求
27
29
 
28
- - dataSources 一律 `type: http`,url 按命名约定直接写最终接口路径
30
+ - dataSources 一律 `type: http`;有接口文档时 url 取文档接口地址(方法/返回形状对齐),否则按命名约定写 `/api/d/...` 最终路径
29
31
  - 列表接口返回形状约定 `{ items, total, filters? }`;详情接口对空 id 返回空对象而非 404(新建预填链路依赖)
30
32
  - 选项类接口(如下拉的操作人列表)用单段路径,页面 `options: ${data.xxx}` 绑定
31
33
  - 写操作走 `ops/` 前缀 POST,页面提交闭环(request → message → reload)不依赖返回体内容
@@ -3,9 +3,8 @@
3
3
  /**
4
4
  * 页面 DSL 引用的自定义组件注册表(页面级自定义组件
5
5
  * `components={{...}}`),经 SitemapApp 的 `components` prop 全局注册。
6
+ * ChipFilter / ChipNode 快筛组件已内置在 @pactor-app/ui,无需在此注册。
6
7
  */
7
- import { ChipNode } from "./chip-node";
8
- import { ChipFilter } from "./chip-filter";
9
8
  import { StatusBadge } from "./status-badge";
10
9
  import { LogStatus } from "./log-status";
11
10
  import { DebugChat } from "./debug-chat";
@@ -13,8 +12,6 @@ import { CardLink, StatusToggleIcon, TextLink } from "./card-links";
13
12
  import { ThemeSwitcher } from "./theme-switcher";
14
13
 
15
14
  export const pactorComponents: Record<string, unknown> = {
16
- ChipNode,
17
- ChipFilter,
18
15
  StatusBadge,
19
16
  LogStatus,
20
17
  DebugChat,
@@ -1,89 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * 快筛 Chip 组(列表页共享的自定义组件):Chip 列表整体作为单一组件,
5
- * 选项(value / 文案 / 计数 / 色点)由接口返回,页面 DSL 不再逐枚硬编码 ChipNode。
6
- * 选项文案含计数,由接口侧预计算(表达式引擎不支持数组方法与函数调用)。
7
- *
8
- * 兼容各列表页的两种交互(Provider / Model / Route / Client / ClientTemplate):
9
- * - mode="single":单选,value 为字符串。点击已选项回到 allValue(默认 'all'),
10
- * 与「状态 / 类型」单选快筛的 toggle 语义一致;
11
- * - mode="multi":多选,value 为已选值数组。选项中含 value === allValue 的
12
- * 「全部」项时,点击它清空选择、仅在无选择时高亮(标签 / 厂商快筛语义);
13
- * 不含「全部」项时纯切换、不选即全部(Provider 类型快筛语义)。
14
- *
15
- * DSL 用法(${event} = 新值,single 为字符串 / multi 为数组):
16
- * {
17
- * type: "ChipFilter",
18
- * props: {
19
- * mode: single,
20
- * value: "${state.statusChip}",
21
- * items: "${data.providers.filters.status}",
22
- * },
23
- * events: { onChange: [setState, reload] },
24
- * }
25
- */
26
- import { ChipNode } from "./chip-node";
27
-
28
- export interface ChipFilterItem {
29
- /** 提交值('all' 为约定的「全部」项,可用 allValue 改写) */
30
- value: string;
31
- /** 展示文案(含计数,接口预计算) */
32
- text: string;
33
- /** 可选状态色点 */
34
- dot?: string;
35
- }
36
-
37
- export function ChipFilter({
38
- items,
39
- mode = "single",
40
- value,
41
- allValue = "all",
42
- onChange,
43
- }: {
44
- items?: ChipFilterItem[];
45
- mode?: "single" | "multi";
46
- value?: string | string[];
47
- allValue?: string;
48
- onChange?: (value: string | string[]) => void;
49
- }) {
50
- if (!Array.isArray(items)) return null;
51
- const selectedValues = Array.isArray(value) ? value : [];
52
- return (
53
- <>
54
- {items.map((item) => {
55
- const selected =
56
- mode === "multi"
57
- ? item.value === allValue
58
- ? selectedValues.length === 0
59
- : selectedValues.includes(item.value)
60
- : (value ?? allValue) === item.value;
61
- return (
62
- <ChipNode
63
- key={item.value}
64
- text={item.text}
65
- dot={item.dot}
66
- selected={selected}
67
- onClick={() => {
68
- if (mode === "multi") {
69
- if (item.value === allValue) {
70
- onChange?.([]);
71
- } else {
72
- onChange?.(
73
- selected
74
- ? selectedValues.filter((v) => v !== item.value)
75
- : [...selectedValues, item.value],
76
- );
77
- }
78
- } else {
79
- onChange?.(
80
- selected && item.value !== allValue ? allValue : item.value,
81
- );
82
- }
83
- }}
84
- />
85
- );
86
- })}
87
- </>
88
- );
89
- }
@@ -1,46 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * 快筛 Chip(Provider / Model / Route 列表页共享的自定义组件)。
5
- * 未选中 = outline 小按钮(border-border,hover 走 accent);
6
- * 选中 = 主色实底 + 主色前景文字(一眼可辨的激活态)。
7
- * text / selected 由运行时对 props 模板求值后传入(selected 接收布尔表达式结果);
8
- * dot 为可选状态色点(Provider 状态快筛使用);
9
- * 点击动作由 DSL 节点 events.onClick 注入(运行时执行为回调,语义同 Button onClick)。
10
- *
11
- * DSL 用法:
12
- * {
13
- * type: "ChipNode",
14
- * props: {
15
- * text: "全部 ${counts.all.enabled}/${counts.all.total}",
16
- * selected: "${state.statusChip == 'all'}",
17
- * },
18
- * events: { onClick: [setState / reload ...] },
19
- * }
20
- */
21
- export function ChipNode({
22
- text,
23
- selected,
24
- dot,
25
- onClick,
26
- }: {
27
- text?: string;
28
- selected?: boolean;
29
- dot?: string;
30
- onClick?: () => void;
31
- }) {
32
- return (
33
- <button
34
- type="button"
35
- onClick={onClick}
36
- className={`inline-flex h-8 shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 ${
37
- selected
38
- ? "border-primary bg-primary text-primary-foreground shadow-sm"
39
- : "border-border bg-card hover:bg-accent hover:text-accent-foreground"
40
- }`}
41
- >
42
- {dot ? <span style={{ color: dot, fontSize: 10 }}>●</span> : null}
43
- {text}
44
- </button>
45
- );
46
- }