docs: align etaf-ui DataGrid public contract

This commit is contained in:
Kinneyzhang 2026-08-31 13:21:37 +08:00
parent 7f465fc6d7
commit c1c3015fa7
3 changed files with 178 additions and 19 deletions

View File

@ -44,8 +44,9 @@ integer while the caller remains the owner of the ref.
The catalog uses one semantic Theme vocabulary for color roles. The main
tokens are `:ui-fg`, `:ui-bg`, `:ui-border`, `:ui-muted-fg`,
`:ui-disabled-*`, `:ui-button-primary-*`, `:ui-button-secondary-*`,
`:ui-button-ghost-*`, `:ui-checkbox-*`, `:ui-grid-*`, and
`:ui-panel-*`. Applications provide these through ETAF's
`:ui-button-ghost-*`, `:ui-checkbox-*`, `:ui-table-*`,
`:ui-data-grid-error-fg`, `:ui-pagination-muted-fg`, and `:ui-panel-*`.
Applications provide these through ETAF's
`etaf-theme-provide`; legacy `:ui-button-color`-style aliases remain accepted
for compatibility. `etaf-ui` does not depend on TP or on a renderer-specific
palette registry. If an application wants TP's light/dark palette data, the
@ -76,16 +77,41 @@ mouse-face and hit range semantically separate.
surface presentation props, and projects the named `header` slot plus the
default slot.
<!-- M0b1: business-props -->
The public Component business props are derived from the definitions and kept
separate from forwarded Host attributes:
| Component | Business props |
| --- | --- |
| `etaf-label` | `:text`, `:variant` |
| `etaf-button` | `:label`, `:on-press`, `:disabled`, `:ref`, `:class`, `:color`, `:bgcolor`, `:border`, `:padding`, `:font-weight`, `:tab-index`, `:aria-label`, `:use`, `:variant` |
| `etaf-checkbox` | `:checked`, `:label`, `:on-change`, `:disabled` |
| `etaf-panel` | `:title`, `:variant` |
| `etaf-number-input` | `:value`, `:label`, `:on-change`, `:disabled`, `:min`, `:max` |
| `etaf-table` | `:columns`, `:rows`, `:row-key`, `:row-ref`, `:on-row-press`, `:row-selected-p` |
| `etaf-data-grid` | `:controller`, `:columns`, `:row-key`, `:on-row-press`, `:row-ref`, `:row-selected-p`, `:loading-label`, `:error-label`, `:empty-label` |
| `etaf-pagination` | `:controller`, `:previous-ref`, `:next-ref`, `:class`, `:color`, `:bgcolor`, `:border`, `:padding`, `:aria-label` |
<!-- M0b1: single-host-root -->
Every public catalog Component mounts exactly one Host root. All valid Host
attributes that are not consumed as business props—such as `:ref`, `:class`,
and `:aria-label`—are forwarded to that root. This forwarding rule does not
make those attributes business props of every Component.
DataGrid accepts column descriptors, a function-valued `:row-key`, and the
optional `:loading-label`, `:error-label`, `:empty-label`, `:selected-key`, or
function-valued `:row-selected-p`. When `:on-row-press` is supplied, every
row is an interactive button: `:row-ref` is required, must return a non-nil
stable Host reference for each row, and the row receives role `button` and
tab index `0`. `:on-row-press` receives the row. Without `:on-row-press`, rows
have no callback or tab stop. `:row-key` remains a required non-nil stable
scalar identity for retained rows. DataGrid retains one action closure per
row key, so selection or data Range updates do not recreate handlers for
unchanged rows.
optional `:loading-label`, `:error-label`, `:empty-label`, or function-valued
`:row-selected-p`. When `:on-row-press` is supplied, every row is an
interactive button with role `button` and tab index `0`; the callback receives
the row. Without `:on-row-press`, rows have no callback or tab stop.
<!-- M0b1: row-ref-optional -->
`:row-ref` is optional for DataGrid. When it is omitted on an interactive
grid, DataGrid derives a stable internal Host reference from the row identity
owned by the controller. When it is supplied, the callback owns that identity
and must return a non-nil stable Host reference for every row. `:row-key`
remains a required non-nil stable scalar identity for retained rows. DataGrid
retains one action closure per row key, so selection or Data Range updates do
not recreate handlers for unchanged rows.
An integer column `:width` is readable character capacity, not a raw pixel
value. DataGrid passes Ebox's native character unit through and reserves one
native character between non-final tracks.

View File

@ -40,7 +40,8 @@ Emacs 原生 minibuffer。它支持 `:value`、`:label`、`:on-change`、`:min`
目录统一使用语义化的 Theme 色彩 vocabulary主要 token 包括
`:ui-fg`、`:ui-bg`、`:ui-border`、`:ui-muted-fg`、`:ui-disabled-*`、
`:ui-button-primary-*`、`:ui-button-secondary-*`、`:ui-button-ghost-*`、
`:ui-checkbox-*`、`:ui-grid-*` 和 `:ui-panel-*`。应用通过 ETAF 的
`:ui-checkbox-*`、`:ui-table-*`、`:ui-data-grid-error-fg`、
`:ui-pagination-muted-fg``:ui-panel-*`。应用通过 ETAF 的
`etaf-theme-provide` 提供这些 token旧的 `:ui-button-color` 一类 alias
仍兼容。`etaf-ui` 不依赖 TP也不直接依赖 renderer-specific palette
registry如果应用想使用 TP 的亮/暗 palette 数据,应先通过可选的
@ -66,14 +67,38 @@ Component 的 `:styles` 声明负责默认外观。调用者提供的非 nil pre
`etaf-panel` 支持 `:title`、相同的
surface presentation 属性,并投影命名的 `header` slot 和默认 slot。
<!-- M0b1: business-props -->
公共 Component 的 business props 由实际定义导出,并与透传的 Host 属性分开:
| Component | Business props |
| --- | --- |
| `etaf-label` | `:text`、`:variant` |
| `etaf-button` | `:label`、`:on-press`、`:disabled`、`:ref`、`:class`、`:color`、`:bgcolor`、`:border`、`:padding`、`:font-weight`、`:tab-index`、`:aria-label`、`:use`、`:variant` |
| `etaf-checkbox` | `:checked`、`:label`、`:on-change`、`:disabled` |
| `etaf-panel` | `:title`、`:variant` |
| `etaf-number-input` | `:value`、`:label`、`:on-change`、`:disabled`、`:min`、`:max` |
| `etaf-table` | `:columns`、`:rows`、`:row-key`、`:row-ref`、`:on-row-press`、`:row-selected-p` |
| `etaf-data-grid` | `:controller`、`:columns`、`:row-key`、`:on-row-press`、`:row-ref`、`:row-selected-p`、`:loading-label`、`:error-label`、`:empty-label` |
| `etaf-pagination` | `:controller`、`:previous-ref`、`:next-ref`、`:class`、`:color`、`:bgcolor`、`:border`、`:padding`、`:aria-label` |
<!-- M0b1: single-host-root -->
目录中的每个公共 Component 都只挂载一个 Host root。未被作为 business prop
消费的所有合法 Host 属性(例如 `:ref`、`:class` 和 `:aria-label`)都会透传到
该 root透传规则并不意味着这些属性都是每个 Component 的 business prop。
DataGrid 支持列描述、函数型 `:row-key`,以及可选的
`:loading-label`、`:error-label`、`:empty-label`、`:selected-key` 或函数型
`:row-selected-p`。提供 `:on-row-press` 后,每一行都是可交互 button必须
提供 `:row-ref`,它对每一行返回非 nil 的稳定 Host ref行会获得
`button` role 和 `tab-index 0`,且回调接收该行。没有 `:on-row-press` 时,
行没有回调或 tab stop。`:row-key` 仍然是 retained row 必须具备的非 nil
稳定标量 identity。DataGrid 按 row key 保留唯一 action closure因此 selection
或 Data Range 更新不会为未变化的行重建 handler。
`:loading-label`、`:error-label`、`:empty-label` 或函数型
`:row-selected-p`。提供 `:on-row-press` 后,每一行都是带 `button` role 和
`tab-index 0` 的可交互 button且回调接收该行没有 `:on-row-press` 时,行
没有回调或 tab stop。
<!-- M0b1: row-ref-optional -->
DataGrid 的 `:row-ref` 是可选项。交互式 grid 省略它时DataGrid 会根据
controller 持有的 row identity 派生稳定的内部 Host ref显式提供它时
该回调拥有 identity并且必须为每一行返回非 nil 的稳定 Host ref。
`:row-key` 仍然是 retained row 必须具备的非 nil 稳定标量 identity。
DataGrid 按 row key 保留唯一 action closure因此 selection 或 Data Range
更新不会为未变化的行重建 handler。
整数列 `:width` 表示可读字符容量不是裸像素DataGrid 原样使用 Ebox 原生字符单位,
并在非末列之间保留一个原生字符间距。

View File

@ -0,0 +1,108 @@
;;; etaf-ui-m0b1-doc-contract-tests.el --- M0b1 UI documentation contracts -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'seq)
(require 'etaf-ui)
(declare-function etaf-ui-m0a-component-inventory
"../scripts/etaf-ui-m0a-inventory")
(load-file (expand-file-name "scripts/etaf-ui-m0a-inventory.el"
default-directory))
(defconst etaf-ui-m0b1-doc--readmes
'("README.md" "README.zh-CN.md")
"README files governed by the M0b1 etaf-ui documentation contract.")
(defun etaf-ui-m0b1-doc--contents (file)
"Return FILE contents from the package root."
(with-temp-buffer
(insert-file-contents (expand-file-name file default-directory))
(buffer-string)))
(defun etaf-ui-m0b1-doc--component-table-row (component chinese-p)
"Return COMPONENT's exact README table row.
Use Chinese punctuation when CHINESE-P is non-nil."
(format "| `%s` | %s |"
(plist-get component :name)
(mapconcat
(lambda (prop) (format "`:%s`" prop))
(plist-get component :observed-business-props)
(if chinese-p "" ", "))))
(ert-deftest etaf-ui-m0b1-readmes-match-runtime-component-manifest ()
"Keep the README contract aligned with the runtime-derived inventory."
(let* ((components (etaf-ui-m0a-component-inventory))
(grid (seq-find (lambda (entry)
(eq (plist-get entry :name) 'etaf-data-grid))
components)))
(should (= 8 (length components)))
(should
(equal '(controller columns row-key on-row-press row-ref row-selected-p
loading-label error-label empty-label)
(plist-get grid :observed-business-props)))
(dolist (file etaf-ui-m0b1-doc--readmes)
(let ((contents (etaf-ui-m0b1-doc--contents file))
(chinese-p (string-match-p "zh-CN" file)))
(should-not (string-match-p ":selected-key" contents))
(dolist (component components)
(should
(string-match-p
(regexp-quote
(etaf-ui-m0b1-doc--component-table-row component chinese-p))
contents))
(should (eq 'all-valid-host-attrs
(plist-get component :forwarded-host-attrs)))
(should (eq 'single-host-root
(plist-get component :root-guarantee))))))))
(ert-deftest etaf-ui-m0b1-readmes-document-forwarding-and-row-ref-fallback ()
"Document the single-root forwarding and optional row-ref contracts."
(dolist (file etaf-ui-m0b1-doc--readmes)
(let ((contents (etaf-ui-m0b1-doc--contents file)))
(should (string-match-p "M0b1: single-host-root" contents))
(should (string-match-p "M0b1: row-ref-optional" contents))
(should (string-match-p "M0b1: business-props" contents)))))
(ert-deftest etaf-ui-m0b1-data-grid-row-ref-current-contract ()
"Use a controller fallback when row-ref is nil and honor an explicit ref."
(dolist (explicit '(nil explicit))
(let* ((source (etaf-data-memory-source '((:id 7 :name "Ada"))
:id-key :id))
(controller (etaf-data-controller source :auto-load t))
(buffer-name (generate-new-buffer-name " *etaf-ui-m0b1-row-ref*"))
pressed)
(unwind-protect
(progn
(etaf-mount
buffer-name
(etaf-view
(etaf-data-grid
:controller controller
:columns '((:key :id :label "ID"))
:row-key (lambda (row) (plist-get row :id))
:row-ref (when explicit
(lambda (row)
(intern (format "documented-row-%s"
(plist-get row :id)))))
:on-row-press (lambda (row) (setq pressed row)))))
(let ((runtime (etaf-runtime-for-buffer buffer-name))
host-ref)
(maphash
(lambda (ref props)
(when (equal 7 (plist-get props :key))
(setq host-ref ref)))
(etaf-runtime-host-props runtime))
(should host-ref)
(when explicit
(should (eq 'documented-row-7 host-ref)))
(etaf-dispatch-event runtime host-ref 'press)
(should (equal 7 (plist-get pressed :id)))))
(when-let* ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(etaf-data-stop controller)
(when-let* ((buffer (get-buffer buffer-name)))
(kill-buffer buffer))))))
(provide 'etaf-ui-m0b1-doc-contract-tests)
;;; etaf-ui-m0b1-doc-contract-tests.el ends here