feat(etaf): complete unified core package boundary

Keep View, Component, Runtime, reactive, Context, Resource, Behavior, Action, and Data in one core package; add the Grid Host lowering path; remove bundled UI and Playground copies; and document the independent package graph.
This commit is contained in:
Kinneyzhang 2026-08-05 06:59:22 +08:00
parent 43b17192d9
commit 904ac451a4
16 changed files with 194 additions and 531 deletions

View File

@ -1,18 +1,18 @@
EMACS ?= emacs EMACS ?= emacs
LOAD_PATH = -L . -L ../emacs-box LOAD_PATH = -L . -L ../emacs-box
SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el etaf-ui.el etaf-playground.el SOURCES = etaf-view.el etaf-component.el etaf-reactive.el etaf-context.el etaf-resource.el etaf-data.el etaf-renderer.el etaf-runtime.el etaf-behavior.el etaf-actions.el etaf-events.el etaf.el
TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-ui-tests.el tests/etaf-playground-tests.el TESTS = tests/etaf-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el
.PHONY: test compile load checkdoc docs-check check clean .PHONY: test compile load checkdoc docs-check check clean
test: compile test: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" -l tests/etaf-tests.el -l tests/etaf-resource-tests.el -l tests/etaf-data-tests.el -l tests/etaf-ui-tests.el -l tests/etaf-playground-tests.el \ $(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" -l tests/etaf-tests.el -l tests/etaf-resource-tests.el -l tests/etaf-data-tests.el \
-f ert-run-tests-batch-and-exit -f ert-run-tests-batch-and-exit
compile: clean compile: clean
$(EMACS) -Q --batch $(LOAD_PATH) \ $(EMACS) -Q --batch $(LOAD_PATH) \
--eval "(load-file \"etaf.el\")" \ --eval "(load-file \"etaf.el\")" \
--eval "(dolist (file '(\"etaf-view.el\" \"etaf-component.el\" \"etaf-reactive.el\" \"etaf-context.el\" \"etaf-resource.el\" \"etaf-data.el\" \"etaf-renderer.el\" \"etaf-runtime.el\" \"etaf-behavior.el\" \"etaf-actions.el\" \"etaf-events.el\" \"etaf.el\" \"etaf-ui.el\" \"etaf-playground.el\")) (byte-compile-file file))" --eval "(dolist (file '(\"etaf-view.el\" \"etaf-component.el\" \"etaf-reactive.el\" \"etaf-context.el\" \"etaf-resource.el\" \"etaf-data.el\" \"etaf-renderer.el\" \"etaf-runtime.el\" \"etaf-behavior.el\" \"etaf-actions.el\" \"etaf-events.el\" \"etaf.el\")) (byte-compile-file file))"
load: compile load: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" --eval "(require 'etaf)" --eval "(princ \"ETAF load OK\\n\")" $(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" --eval "(require 'etaf)" --eval "(princ \"ETAF load OK\\n\")"

View File

@ -46,6 +46,17 @@ Define a Component:
- [User guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md) - [User guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
- [Implementation plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md) - [Implementation plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
## Independent packages
| Package | Role |
| --- | --- |
| [`etaf-ui`](../etaf-ui/README.md) | Official Component catalog: Button, Checkbox, Label, Panel, and DataGrid. |
| [`etaf-sqlite`](../etaf-sqlite/README.md) | Concrete SQLite Data Source; the Data Controller remains in ETAF core. |
| [`etaf-playground`](../etaf-playground/README.md) | ETAF examples, with the UI catalog loaded only when requested. |
| [`ebox-playground`](../ebox-playground/README.md) | Ebox-only layout examples, independent from ETAF. |
There is no separate `etaf-data` install: Data is a core ETAF capability. There is no generic `etaf-adapters` package: other databases, services, files, or ORMs should provide concrete Data Source packages with explicit names.
## Load and verify ## Load and verify
During development, load the sibling Ebox checkout before ETAF: During development, load the sibling Ebox checkout before ETAF:
@ -62,4 +73,4 @@ Run the complete local gate:
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
``` ```
The gate byte-compiles the implementation, runs the core/Data/Resource/UI/Playground tests, and checks documentation/API boundaries. `etaf-ui` and `etaf-playground` are optional modules and are not loaded by the core facade. The core gate byte-compiles the implementation, runs the core/Data/Resource tests, and checks documentation/API boundaries. Run `make check` in the sibling `etaf-ui`, `etaf-sqlite`, `etaf-playground`, and `ebox-playground` repositories for their independent gates; none is loaded by the core facade.

View File

@ -46,6 +46,17 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
- [English User Guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md) - [English User Guide](docs/user-guide.en.md) · [中文用户指南](docs/user-guide.zh.md)
- [English Implementation Plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md) - [English Implementation Plan](docs/implementation-plan.en.md) · [中文实施计划](docs/implementation-plan.zh.md)
## 独立包
| 包 | 职责 |
| --- | --- |
| [`etaf-ui`](../etaf-ui/README.md) | 官方 Component 目录Button、Checkbox、Label、Panel 和 DataGrid。 |
| [`etaf-sqlite`](../etaf-sqlite/README.md) | 具体 SQLite Data SourceData Controller 仍属于 ETAF core。 |
| [`etaf-playground`](../etaf-playground/README.md) | ETAF 示例;只有请求官方目录示例时才加载 UI 包。 |
| [`ebox-playground`](../ebox-playground/README.md) | 只使用 Ebox 的布局示例,与 ETAF 独立。 |
没有单独需要安装的 `etaf-data`Data 是 ETAF 核心能力。也没有笼统的 `etaf-adapters` 包:其他数据库、服务、文件或 ORM 应使用明确名称的具体 Data Source 包实现同一契约。
## 加载与验证 ## 加载与验证
开发时先把同级 Ebox 检出目录加入 `load-path` 开发时先把同级 Ebox 检出目录加入 `load-path`
@ -62,4 +73,4 @@ Component(props, Scope) → View → Renderer → Ebox Node → Emacs buffer
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
``` ```
门禁会编译实现、运行 core/Data/Resource/UI/Playground 测试,并检查文档和 API 边界。`etaf-ui` 与 `etaf-playground` 是可选模块,不会被 core facade 自动加载。 核心门禁会编译实现、运行 core/Data/Resource 测试,并检查文档和 API 边界。还应在同级 `etaf-ui`、`etaf-sqlite`、`etaf-playground` 和 `ebox-playground` 仓库分别运行 `make check`;它们都不会被 core facade 自动加载。

View File

@ -215,7 +215,7 @@ Inside a Component, `slot` projects content. In a Component call's child region,
ETAF core intentionally provides only minimal, unstyled Hosts: ETAF core intentionally provides only minimal, unstyled Hosts:
```text ```text
text · fragment · container · row · column · stack · flex · spacer text · fragment · container · row · column · stack · flex · grid · spacer
``` ```
| Host | Meaning | Lowering direction | | Host | Meaning | Lowering direction |
@ -227,10 +227,13 @@ text · fragment · container · row · column · stack · flex · spacer
| `column` | Vertical children | Ebox column layout | | `column` | Vertical children | Ebox column layout |
| `stack` | A structural composition container | Ebox container path | | `stack` | A structural composition container | Ebox container path |
| `flex` | Flex-distributed children | Ebox flex layout | | `flex` | Flex-distributed children | Ebox flex layout |
| `grid` | Two-dimensional tracks, placement, and spans | Ebox Grid formatting context |
| `spacer` | Intentional empty geometry | Ebox spacer | | `spacer` | Intentional empty geometry | Ebox spacer |
Strings are the smallest text View and lower to Ebox content. Nested `text` Views in a text-compatible position become propertized inline runs; a non-text child falls back to normal layout lowering. Text, View Hosts, Components, and Ebox Nodes are therefore successive representations, not competing element classes. Strings are the smallest text View and lower to Ebox content. Nested `text` Views in a text-compatible position become propertized inline runs; a non-text child falls back to normal layout lowering. Text, View Hosts, Components, and Ebox Nodes are therefore successive representations, not competing element classes.
`grid` reuses Ebox's public two-dimensional layout contract. Its properties include track templates, `auto`/fractional/`minmax` tracks, gaps, row/column placement, spans, auto-flow, and item alignment. Ebox owns track measurement and placement; ETAF only maps the `grid` Host into that node. Ebox's optional native reflow backend is not required for correctness; a Grid tree uses the ordinary Ebox renderer when that backend does not support the node.
ETAF's Renderer is the only framework module that calls Ebox. It uses Ebox public constructors, property readers, host-reference queries, and publication APIs. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data. ETAF's Renderer is the only framework module that calls Ebox. It uses Ebox public constructors, property readers, host-reference queries, and publication APIs. Ebox does not know about Components, slots, Actions, Context, Behaviors, or Data.
`raw-ebox` is the one deliberate escape: `raw-ebox` is the one deliberate escape:
@ -399,13 +402,18 @@ etaf → ebox
etaf-ui → etaf etaf-ui → etaf
└── official Button, Checkbox, Label, Panel, DataGrid, ... Components └── official Button, Checkbox, Label, Panel, DataGrid, ... Components
etaf-sqlite → etaf
└── typed SQLite Data Source
etaf-playground → etaf etaf-playground → etaf
└── optionally etaf-ui for catalog examples └── optionally etaf-ui for catalog examples
``` ```
`etaf-ui` is the official ready-made Component catalog. Its public user concept is Component; files are maintainer boundaries. Controls, Widgets, and DataGrid are not parallel runtime types, and DataGrid is simply a compound Component built from the same View, props, slots, events, and Data contracts. `etaf-ui` is the official ready-made Component catalog. Its public user concept is Component; files are maintainer boundaries. Controls, Widgets, and DataGrid are not parallel runtime types, and DataGrid is simply a compound Component built from the same View, props, slots, events, and Data contracts.
The new `etaf-playground` uses ETAF public APIs and does not call Ebox private APIs or depend on `ebox-playground`. The existing `ebox-playground` uses Ebox public APIs only and does not load ETAF. Core packages do not load either Playground automatically. `etaf-sqlite` is a concrete source package. It owns schema declarations, identifier validation, connections, pagination, and mutations; `etaf-data` remains part of ETAF core. PostgreSQL, REST, file, and ORM integrations may implement the same source capability in separate packages without adding an `etaf-adapters` concept.
`ebox-playground`, `etaf-playground`, `etaf-ui`, and `etaf-sqlite` are independently loadable sibling packages. `ebox-playground` uses only Ebox public APIs and never loads ETAF. `etaf-playground` uses ETAF public APIs and optionally loads `etaf-ui`; it never depends on `ebox-playground`. Core packages do not load any optional package automatically.
Optional storage integrations should use explicit package names such as a concrete SQLite or PostgreSQL source. A generic adapter package would add a name without owning a stable behavior, so it is not part of the public model. Optional storage integrations should use explicit package names such as a concrete SQLite or PostgreSQL source. A generic adapter package would add a name without owning a stable behavior, so it is not part of the public model.

View File

@ -214,7 +214,7 @@ children 只是匿名/默认 slot 的便捷写法,不是第二套内容模型
ETAF core 只提供最小且无样式的 Host ETAF core 只提供最小且无样式的 Host
```text ```text
text · fragment · container · row · column · stack · flex · spacer text · fragment · container · row · column · stack · flex · grid · spacer
``` ```
| Host | 作者看到的含义 | lowering 方向 | | Host | 作者看到的含义 | lowering 方向 |
@ -226,10 +226,13 @@ text · fragment · container · row · column · stack · flex · spacer
| `column` | 垂直排列子节点 | Ebox column layout | | `column` | 垂直排列子节点 | Ebox column layout |
| `stack` | 结构性的组合容器 | Ebox container 路径 | | `stack` | 结构性的组合容器 | Ebox container 路径 |
| `flex` | 通过 flex 分配空间 | Ebox flex layout | | `flex` | 通过 flex 分配空间 | Ebox flex layout |
| `grid` | 二维轨道、放置和跨度 | Ebox Grid formatting context |
| `spacer` | 有意表达的空几何 | Ebox spacer | | `spacer` | 有意表达的空几何 | Ebox spacer |
字符串是最小的文本 View会降低为 Ebox content。text 中兼容的嵌套 text 会成为带 text properties 的 inline run非文本子节点则回到普通布局 lowering。因此 Text、View Host、Component 和 Ebox Node 是连续的表示层,而不是三棵相互竞争的树。 字符串是最小的文本 View会降低为 Ebox content。text 中兼容的嵌套 text 会成为带 text properties 的 inline run非文本子节点则回到普通布局 lowering。因此 Text、View Host、Component 和 Ebox Node 是连续的表示层,而不是三棵相互竞争的树。
`grid` 复用 Ebox 的公共二维布局契约,支持轨道模板、`auto`/分数/`minmax` 轨道、间距、行列放置、跨度、自动流向和 item 对齐。轨道测量和放置由 Ebox 负责ETAF 只把 `grid` Host 映射到这个节点。Ebox 的可选 native reflow 后端不参与正确性保证;如果该后端不支持 GridGrid 树会使用普通 Ebox 渲染器。
ETAF 的 Renderer 是唯一调用 Ebox 的框架模块,并且只使用 Ebox 公共构造器、属性读取器、Host 引用查询和发布 API。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。 ETAF 的 Renderer 是唯一调用 Ebox 的框架模块,并且只使用 Ebox 公共构造器、属性读取器、Host 引用查询和发布 API。Ebox 不理解 Component、slot、Action、Context、Behavior 或 Data。
`raw-ebox` 是唯一明确的底层出口: `raw-ebox` 是唯一明确的底层出口:
@ -398,13 +401,18 @@ etaf → ebox
etaf-ui → etaf etaf-ui → etaf
└── 官方 Button、Checkbox、Label、Panel、DataGrid 等 Component └── 官方 Button、Checkbox、Label、Panel、DataGrid 等 Component
etaf-sqlite → etaf
└── 类型化 SQLite Data Source
etaf-playground → etaf etaf-playground → etaf
└── 展示官方 Component 时可选依赖 etaf-ui └── 展示官方 Component 时可选依赖 etaf-ui
``` ```
`etaf-ui` 是官方现成 Component 目录。用户只需要理解 Component文件只是维护者边界。Controls、Widgets 和 DataGrid 不是平行的 Runtime 类型DataGrid 只是由同一套 View、props、slot、事件和 Data 契约构成的复合 Component。 `etaf-ui` 是官方现成 Component 目录。用户只需要理解 Component文件只是维护者边界。Controls、Widgets 和 DataGrid 不是平行的 Runtime 类型DataGrid 只是由同一套 View、props、slot、事件和 Data 契约构成的复合 Component。
新的 `etaf-playground` 只使用 ETAF 公共 API不调用 Ebox 私有 API也不依赖 `ebox-playground`。现有的 `ebox-playground` 只使用 Ebox 公共 API不加载 ETAF。Core 包不会自动加载任一 Playground。 `etaf-sqlite` 是具体的数据源包,负责 schema 声明、标识符校验、连接、分页和 mutation`etaf-data` 仍然属于 ETAF core。PostgreSQL、REST、文件和 ORM 集成可以在独立包中实现同一个 source capability不增加 `etaf-adapters` 概念。
`ebox-playground`、`etaf-playground`、`etaf-ui` 和 `etaf-sqlite` 都是可以独立加载的同级包。`ebox-playground` 只使用 Ebox 公共 API不加载 ETAF`etaf-playground` 使用 ETAF 公共 API并且只在展示官方组件时可选加载 `etaf-ui`,不依赖 `ebox-playground`。核心包不会自动加载任何可选包。
可选存储集成应使用具体名称,例如 SQLite 或 PostgreSQL source。通用 adapter 包无法拥有稳定的行为,只会增加用户需要记忆的名称,因此不属于公共模型。 可选存储集成应使用具体名称,例如 SQLite 或 PostgreSQL source。通用 adapter 包无法拥有稳定的行为,只会增加用户需要记忆的名称,因此不属于公共模型。

View File

@ -12,7 +12,7 @@ A milestone is complete only when:
4. The implementation has a paired English/Chinese documentation update when the public contract changes. 4. The implementation has a paired English/Chinese documentation update when the public contract changes.
5. A failed candidate does not corrupt the last committed Runtime or buffer. 5. A failed candidate does not corrupt the last committed Runtime or buffer.
The repository is complete for the agreed unified architecture when the mandatory milestones below are green. More official Components, concrete storage sources, richer Ebox layouts, and asynchronous integrations are additive work; they must reuse the frozen contracts instead of widening the core vocabulary. The repository is complete for the agreed unified architecture when the mandatory milestones below are green. The first official UI catalog, Grid layout, SQLite source, and two independent Playgrounds are delivered as sibling packages; future Components, storage sources, and asynchronous integrations are additive work and must reuse the frozen contracts instead of widening the core vocabulary.
## 2. Milestone status ## 2. Milestone status
@ -22,8 +22,8 @@ The repository is complete for the agreed unified architecture when the mandator
| P1 Components | `:view`, `:setup`, props, default/named slots, retained instances, lifecycle, `:key`, raw Ebox escape | Component, slot, mount, prop-update, raw-node, and rollback tests | | P1 Components | `:view`, `:setup`, props, default/named slots, retained instances, lifecycle, `:key`, raw Ebox escape | Component, slot, mount, prop-update, raw-node, and rollback tests |
| P2 Runtime | refs, computed, effects, watches, Scope cleanup, Context, Theme, Behaviors, events, focus, Actions | Mounted event/focus tests, reactive failure rollback, cleanup tests | | P2 Runtime | refs, computed, effects, watches, Scope cleanup, Context, Theme, Behaviors, events, focus, Actions | Mounted event/focus tests, reactive failure rollback, cleanup tests |
| P3 presentation | scoped styles, selector matching, Theme precedence, inline text runs, Resource and error boundary | Style, Theme, text-surface, Resource, and error tests | | P3 presentation | scoped styles, selector matching, Theme precedence, inline text runs, Resource and error boundary | Style, Theme, text-surface, Resource, and error tests |
| P4 application layer | core Data Controller, memory source, selection, mutation, official UI catalog, DataGrid, ETAF Playground | Data, UI, DataGrid, playground, load-boundary, and static-scan tests | | P4 application layer | core Data Controller, memory source, selection, mutation, Grid Host, official UI catalog, DataGrid, SQLite source, and two independent Playgrounds | Core Data/Grid tests plus the four sibling-package `make check` gates |
| Quality gates | independent package entry, warning-free byte compilation, complete test command, documentation contract | `make check`, `make load`, and `docs-check` | | Quality gates | independent package entries, warning-free byte compilation, checkdoc, public API tests, and documentation contracts | Core `make check` plus `etaf-ui`, `etaf-sqlite`, `etaf-playground`, and `ebox-playground` checks |
## 3. Ownership map ## 3. Ownership map
@ -40,8 +40,10 @@ The repository is complete for the agreed unified architecture when the mandator
| `etaf-actions.el` | named Action registry and Runtime dispatch | View construction or presentation | | `etaf-actions.el` | named Action registry and Runtime dispatch | View construction or presentation |
| `etaf-resource.el` | Scope-owned synchronous loader state and error boundary | a second async/task object model | | `etaf-resource.el` | Scope-owned synchronous loader state and error boundary | a second async/task object model |
| `etaf-data.el` | source capability contract, controller state, pagination, mutation, selection | database-specific code or storage assumptions | | `etaf-data.el` | source capability contract, controller state, pagination, mutation, selection | database-specific code or storage assumptions |
| `etaf-ui.el` | one official Component catalog, including compound DataGrid | a second Control/Widget/Component taxonomy | | sibling `etaf-ui/etaf-ui.el` | one official Component catalog, including compound DataGrid | a second Control/Widget/Component taxonomy |
| `etaf-playground.el` | public-API-only ETAF examples and optional preview entry points | Ebox private APIs or `ebox-playground` dependency | | sibling `etaf-playground/etaf-playground.el` | public-API-only ETAF examples and optional catalog entry points | Ebox private APIs or `ebox-playground` dependency |
| sibling `etaf-sqlite/etaf-sqlite.el` | typed SQLite Data Source and transaction boundary | Data Controller state or a generic ORM layer |
| sibling `ebox-playground/ebox-playground.el` | public Ebox layout examples | ETAF APIs or Ebox private APIs |
| `etaf.el` | one core facade and local package load-path preference | loading optional UI or Playground modules | | `etaf.el` | one core facade and local package load-path preference | loading optional UI or Playground modules |
Do not split a file merely to create a shorter name. Split only when a stable responsibility, lifecycle, external boundary, or release boundary moves with the code. Do not split a file merely to create a shorter name. Split only when a stable responsibility, lifecycle, external boundary, or release boundary moves with the code.
@ -55,7 +57,7 @@ Do not split a file merely to create a shorter name. Split only when a stable re
- Attribute values are ordinary Elisp expressions. - Attribute values are ordinary Elisp expressions.
- The only child computation bridge is `(expr :value ELISP-EXPRESSION)`. - The only child computation bridge is `(expr :value ELISP-EXPRESSION)`.
- A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed. - A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed.
- Core Hosts are `text`, `fragment`, `container`, `row`, `column`, `stack`, `flex`, and `spacer`. - Core Hosts are `text`, `fragment`, `container`, `row`, `column`, `stack`, `flex`, `grid`, and `spacer`.
- `raw-ebox` accepts only `:value` and optional `:key` and remains opaque to ETAF semantics. - `raw-ebox` accepts only `:value` and optional `:key` and remains opaque to ETAF semantics.
### 4.2 Component and slots ### 4.2 Component and slots

View File

@ -12,7 +12,7 @@
4. 公共契约发生变化时,同步更新中英文文档。 4. 公共契约发生变化时,同步更新中英文文档。
5. 候选渲染失败时,不破坏上一次已经提交的 Runtime 或 buffer。 5. 候选渲染失败时,不破坏上一次已经提交的 Runtime 或 buffer。
当下面的强制里程碑全部通过时,统一架构的实施完成。更多官方 Component、具体存储 source、更丰富的 Ebox 布局和异步集成都是增量工作,必须复用已经冻结的契约,不能扩大 core 词汇。 当下面的强制里程碑全部通过时,统一架构的实施完成。第一版官方 UI 目录、Grid 布局、SQLite source 和两个独立 Playground 已作为同级包交付;未来的 Component、数据源和异步集成都是增量工作,必须复用已经冻结的契约,不能扩大 core 词汇。
## 2. 里程碑状态 ## 2. 里程碑状态
@ -22,8 +22,8 @@
| P1 Component | `:view`、`:setup`、props、默认/命名 slot、retained instance、生命周期、`:key`、raw Ebox 出口 | Component、slot、mount、prop 更新、raw node、rollback 测试 | | P1 Component | `:view`、`:setup`、props、默认/命名 slot、retained instance、生命周期、`:key`、raw Ebox 出口 | Component、slot、mount、prop 更新、raw node、rollback 测试 |
| P2 Runtime | ref、computed、effect、watch、Scope cleanup、Context、Theme、Behavior、事件、focus、Action | 挂载事件/focus、响应式失败回滚和 cleanup 测试 | | P2 Runtime | ref、computed、effect、watch、Scope cleanup、Context、Theme、Behavior、事件、focus、Action | 挂载事件/focus、响应式失败回滚和 cleanup 测试 |
| P3 presentation | 作用域样式、selector、Theme 优先级、inline text runs、Resource 和 error boundary | 样式、Theme、文本 surface、Resource、error 测试 | | P3 presentation | 作用域样式、selector、Theme 优先级、inline text runs、Resource 和 error boundary | 样式、Theme、文本 surface、Resource、error 测试 |
| P4 application | core Data Controller、memory source、selection、mutation、官方 UI 目录、DataGrid、ETAF Playground | Data、UI、DataGrid、Playground、加载边界和静态扫描测试 | | P4 application | core Data Controller、memory source、selection、mutation、Grid Host、官方 UI 目录、DataGrid、SQLite source 和两个独立 Playground | core Data/Grid 测试以及四个同级包的 `make check` |
| Quality gates | 独立包入口、无 warning 编译、完整测试命令、文档契约 | `make check`、`make load`、`docs-check` | | Quality gates | 独立包入口、无 warning 编译、checkdoc、公共 API 测试和文档契约 | core `make check``etaf-ui`、`etaf-sqlite`、`etaf-playground`、`ebox-playground` 的检查 |
## 3. 职责地图 ## 3. 职责地图
@ -40,8 +40,10 @@
| `etaf-actions.el` | 命名 Action 注册表和 Runtime dispatch | View 构造或 presentation | | `etaf-actions.el` | 命名 Action 注册表和 Runtime dispatch | View 构造或 presentation |
| `etaf-resource.el` | Scope 所有的同步 loader 状态和 error boundary | 第二套异步/task 对象模型 | | `etaf-resource.el` | Scope 所有的同步 loader 状态和 error boundary | 第二套异步/task 对象模型 |
| `etaf-data.el` | source capability、controller 状态、分页、mutation、selection | 数据库专用代码或存储假设 | | `etaf-data.el` | source capability、controller 状态、分页、mutation、selection | 数据库专用代码或存储假设 |
| `etaf-ui.el` | 一个官方 Component 目录,包括复合 DataGrid | 第二套 Control/Widget/Component 分类 | | 同级 `etaf-ui/etaf-ui.el` | 一个官方 Component 目录,包括复合 DataGrid | 第二套 Control/Widget/Component 分类 |
| `etaf-playground.el` | 只使用公共 API 的 ETAF 示例和可选预览入口 | Ebox 私有 API 或 `ebox-playground` 依赖 | | 同级 `etaf-playground/etaf-playground.el` | 只使用公共 API 的 ETAF 示例和可选目录入口 | Ebox 私有 API 或 `ebox-playground` 依赖 |
| 同级 `etaf-sqlite/etaf-sqlite.el` | 类型化 SQLite Data Source 和事务边界 | Data Controller 状态或通用 ORM 层 |
| 同级 `ebox-playground/ebox-playground.el` | 只使用公共 Ebox API 的布局示例 | ETAF API 或 Ebox 私有 API |
| `etaf.el` | 一个 core facade 和本地包路径优先级 | 自动加载可选 UI 或 Playground | | `etaf.el` | 一个 core facade 和本地包路径优先级 | 自动加载可选 UI 或 Playground |
不要为了获得更短的文件名而拆文件。只有稳定职责、生命周期、外部边界或发行边界发生转移时才拆分。 不要为了获得更短的文件名而拆文件。只有稳定职责、生命周期、外部边界或发行边界发生转移时才拆分。
@ -55,7 +57,7 @@
- 属性值是普通 Elisp 表达式。 - 属性值是普通 Elisp 表达式。
- 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)` - 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)`
- Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。 - Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。
- Core Host 是 `text`、`fragment`、`container`、`row`、`column`、`stack`、`flex` 和 `spacer` - Core Host 是 `text`、`fragment`、`container`、`row`、`column`、`stack`、`flex`、`grid``spacer`
- `raw-ebox` 只接受 `:value` 和可选 `:key`,并且对 ETAF 语义保持 opaque。 - `raw-ebox` 只接受 `:value` 和可选 `:key`,并且对 ETAF 语义保持 opaque。
### 4.2 Component 与 slot ### 4.2 Component 与 slot
@ -131,8 +133,8 @@
| --- | --- | --- | | --- | --- | --- |
| 外部异步数据 | 具体 source 定义 callback、generation、取消和 error owner | 放在具体 source 包,或增加窄化的 Data capability不要增加通用 task 层级 | | 外部异步数据 | 具体 source 定义 callback、generation、取消和 error owner | 放在具体 source 包,或增加窄化的 Data capability不要增加通用 task 层级 |
| 更多 UI Component | 既有 props/children/slot/event/Behavior 契约 | 在 `etaf-ui` 增加普通 Component并通过 Runtime 测试 | | 更多 UI Component | 既有 props/children/slot/event/Behavior 契约 | 在 `etaf-ui` 增加普通 Component并通过 Runtime 测试 |
| 数据库 source | 已冻结的 `etaf-data-source` capability plist | 使用具体 SQLite/PostgreSQL source 包,保持存储在 core 外 | | 数据库 source | 已冻结的 `etaf-data-source` capability plist | `etaf-sqlite` 已交付;只有真实用例才增加具体 PostgreSQL/REST/文件/ORM source 包 |
| 更丰富布局 | 完整的 Ebox measurement/layout/publication 契约 | 先扩展 Ebox再通过 Host 或 Component 暴露 | | 更丰富布局 | 完整的 Ebox measurement/layout/publication 契约 | Grid 已交付;未来先扩展 Ebox再通过 Host 或 Component 暴露 |
| Playground 可视化工具 | 公共 inspection 和 reporting API | 扩展 `etaf-playground`,不与 Ebox Playground 耦合 | | Playground 可视化工具 | 公共 inspection 和 reporting API | 扩展 `etaf-playground`,不与 Ebox Playground 耦合 |
每个扩展都必须配套测试和文档。如果确实需要新的公共名词,先更新 `architecture.en.md``architecture.zh.md`,再更新本文和用户指南。 每个扩展都必须配套测试和文档。如果确实需要新的公共名词,先更新 `architecture.en.md``architecture.zh.md`,再更新本文和用户指南。

View File

@ -4,7 +4,7 @@ ETAF builds text applications from one small vocabulary: `View`, `Component`, pr
## 1. Install and load ## 1. Install and load
ETAF depends on the independent Ebox package. During development, put both checkouts on `load-path` and load the one public ETAF entry: ETAF depends on the independent Ebox package. During development, put the core checkouts on `load-path` and load the one public ETAF entry:
```elisp ```elisp
(add-to-list 'load-path "/path/to/github/emacs-box") (add-to-list 'load-path "/path/to/github/emacs-box")
@ -12,7 +12,7 @@ ETAF depends on the independent Ebox package. During development, put both check
(require 'etaf) (require 'etaf)
``` ```
The package entry loads the core View, Component, Runtime, reactive, Context, Data, Resource, event, Behavior, and Action APIs. `etaf-ui` and the Playgrounds are optional modules; loading `etaf` does not load them. The package entry loads the core View, Component, Runtime, reactive, Context, Data, Resource, event, Behavior, and Action APIs. `etaf-ui`, `etaf-sqlite`, and the two Playgrounds are independent optional packages; loading `etaf` does not load them.
## 2. The first View ## 2. The first View
@ -94,6 +94,25 @@ Quote has one ordinary Elisp meaning:
For example, `'bold` is the face symbol, while `'(text "data")` is only data and will not render. A dynamic View must be written as `(etaf-view (text "data"))`. For example, `'bold` is the face symbol, while `'(text "data")` is only data and will not render. A dynamic View must be written as `(etaf-view (text "data"))`.
The core `grid` Host is the two-dimensional layout choice:
```elisp
(etaf-mount
"*etaf-grid*"
(etaf-view
(grid
:width '(640)
:grid-template-columns '((200) 1fr)
:grid-template-rows '(1 1)
:gap '(1 (12))
(text :face 'bold "Name")
(text "Value")
(text "Ada")
(text "Lovelace"))))
```
Use `auto`, `(fr FACTOR)`, symbols such as `1fr`, `(minmax MIN MAX)`, and `(repeat COUNT TRACK)` in track templates. `:grid-auto-columns` and `:grid-auto-rows` size implicit tracks; `:grid-auto-flow` accepts `row` or `column`. Children may use `:grid-column`, `:grid-row`, `:grid-column-span`, and `:grid-row-span`; Ebox performs measurement, placement, and item/content alignment. The optional native backend falls back to the Elisp Ebox renderer for Grid trees.
## 4. Define a Component ## 4. Define a Component
The beginner form is a stateless `:view` Component: The beginner form is a stateless `:view` Component:
@ -433,6 +452,29 @@ DataGrid requires `:row-key` to return a non-nil stable scalar for every row. It
Storage is not tied to SQLite. A PostgreSQL, REST, file, or ORM integration should expose a concrete Data Source with the same contract. Such an integration is optional and does not change the ETAF user model. Storage is not tied to SQLite. A PostgreSQL, REST, file, or ORM integration should expose a concrete Data Source with the same contract. Such an integration is optional and does not change the ETAF user model.
The independent `etaf-sqlite` package is the first concrete storage source:
```elisp
(require 'etaf-sqlite)
(let* ((table (etaf-sqlite-table
'tasks
(list (etaf-sqlite-column :id "id"
:type 'integer :primary t)
(etaf-sqlite-column :title "title" :type 'text))
:id))
(database (etaf-sqlite-database "tasks.sqlite" table))
(source (etaf-sqlite-source database)))
(etaf-sqlite-initialize database)
(let ((controller (etaf-data-controller source :auto-load t)))
(etaf-data-mutate controller 'insert
'(:id 1 :title "Write the guide."))
(message "%S" (etaf-value (etaf-data-items controller)))
(etaf-data-stop controller)))
```
This package uses Emacs' built-in SQLite support and deliberately does not add an ORM layer. Other storage packages should implement `etaf-data-source` directly.
## 11. Resource and raw Ebox ## 11. Resource and raw Ebox
Use a Resource for a Scope-owned synchronous loader: Use a Resource for a Scope-owned synchronous loader:
@ -475,9 +517,9 @@ The only low-level escape is `raw-ebox`:
Use it only when the normal Host and Component lowering path cannot express a real Ebox requirement. The returned Node is opaque to ETAF semantics. Use it only when the normal Host and Component lowering path cannot express a real Ebox requirement. The returned Node is opaque to ETAF semantics.
## 12. Playground ## 12. Playgrounds
The optional core playground is a complete ETAF application example: The independent ETAF playground is a complete ETAF application example:
```elisp ```elisp
(require 'etaf-playground) (require 'etaf-playground)
@ -492,6 +534,15 @@ The core playground depends only on ETAF. To include the official catalog:
`etaf-playground-close` unmounts and kills the default playground buffer. It is independent of `ebox-playground`; neither package is loaded by core ETAF. `etaf-playground-close` unmounts and kills the default playground buffer. It is independent of `ebox-playground`; neither package is loaded by core ETAF.
The independent Ebox playground demonstrates the lower-level layout contract:
```elisp
(require 'ebox-playground)
(ebox-playground-open)
```
It depends only on Ebox. Use it when inspecting Ebox boxes and Grid layout; use `etaf-playground` when inspecting Components, Runtime, Data, and the official catalog.
## 13. Public API map ## 13. Public API map
| API family | Main entry points | Use it when | | API family | Main entry points | Use it when |
@ -508,7 +559,7 @@ The core playground depends only on ETAF. To include the official catalog:
| Data | `etaf-data-source`, `etaf-data-controller`, `etaf-data-memory-source`, `etaf-data-*` | Query, paginate, mutate, select, and stop data | | Data | `etaf-data-source`, `etaf-data-controller`, `etaf-data-memory-source`, `etaf-data-*` | Query, paginate, mutate, select, and stop data |
| Resource | `etaf-resource`, `etaf-resource-result`, `etaf-error-boundary-run` | Own loader state and cleanup | | Resource | `etaf-resource`, `etaf-resource-result`, `etaf-error-boundary-run` | Own loader state and cleanup |
| Official UI | `require 'etaf-ui`, `etaf-button`, `etaf-checkbox`, `etaf-label`, `etaf-panel`, `etaf-data-grid` | Use ready-made Components | | Official UI | `require 'etaf-ui`, `etaf-button`, `etaf-checkbox`, `etaf-label`, `etaf-panel`, `etaf-data-grid` | Use ready-made Components |
| Playground | `etaf-playground-open`, `etaf-playground-open-ui`, `etaf-playground-close` | Explore the framework interactively | | Playground | `etaf-playground-open`, `etaf-playground-open-ui`, `etaf-playground-close`, `ebox-playground-open`, `ebox-playground-close` | Explore the corresponding layer interactively |
Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`, `etaf-ref`, and event callbacks at first. The remaining APIs are additive capabilities, not prerequisites for understanding the core grammar. Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`, `etaf-ref`, and event callbacks at first. The remaining APIs are additive capabilities, not prerequisites for understanding the core grammar.

View File

@ -4,7 +4,7 @@ ETAF 使用一套很小的词汇构建文本应用:`View`、`Component`、prop
## 1. 安装与加载 ## 1. 安装与加载
ETAF 依赖独立的 Ebox 包。开发时把两个检出目录放入 `load-path`,然后只加载 ETAF 的公共入口: ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`,然后只加载 ETAF 的公共入口:
```elisp ```elisp
(add-to-list 'load-path "/path/to/github/emacs-box") (add-to-list 'load-path "/path/to/github/emacs-box")
@ -12,7 +12,7 @@ ETAF 依赖独立的 Ebox 包。开发时把两个检出目录放入 `load-path`
(require 'etaf) (require 'etaf)
``` ```
这个入口会加载核心 View、Component、Runtime、reactive、Context、Data、Resource、事件、Behavior 和 Action API。`etaf-ui` 与两个 Playground 都是可选模块;加载 `etaf` 不会自动加载它们。 这个入口会加载核心 View、Component、Runtime、reactive、Context、Data、Resource、事件、Behavior 和 Action API。`etaf-ui`、`etaf-sqlite` 和两个 Playground 都是独立的可选包;加载 `etaf` 不会自动加载它们。
## 2. 第一个 View ## 2. 第一个 View
@ -94,6 +94,25 @@ quote 只有普通 Elisp 的含义:
例如 `'bold` 是 face symbol`'(text "data")` 只是数据,不会渲染。动态 View 必须写成 `(etaf-view (text "data"))` 例如 `'bold` 是 face symbol`'(text "data")` 只是数据,不会渲染。动态 View 必须写成 `(etaf-view (text "data"))`
核心 `grid` Host 用于二维布局:
```elisp
(etaf-mount
"*etaf-grid*"
(etaf-view
(grid
:width '(640)
:grid-template-columns '((200) 1fr)
:grid-template-rows '(1 1)
:gap '(1 (12))
(text :face 'bold "Name")
(text "Value")
(text "Ada")
(text "Lovelace"))))
```
轨道模板支持 `auto`、`(fr FACTOR)`、`1fr` 等符号、`(minmax MIN MAX)` 和 `(repeat COUNT TRACK)`。`:grid-auto-columns` 与 `:grid-auto-rows` 负责隐式轨道尺寸;`:grid-auto-flow` 支持 `row``column`。子节点可以使用 `:grid-column`、`:grid-row`、`:grid-column-span` 和 `:grid-row-span`;测量、放置以及 item/content 对齐由 Ebox 完成。可选 native backend 不支持 Grid 时会回退到 Elisp Ebox renderer。
## 4. 定义 Component ## 4. 定义 Component
入门形式是无状态的 `:view` Component 入门形式是无状态的 `:view` Component
@ -433,6 +452,29 @@ DataGrid 要求 `:row-key` 对每一行返回非 nil 的稳定标量。它通过
存储不绑定 SQLite。PostgreSQL、REST、文件或 ORM 集成都应该提供同样契约的具体 Data Source它们是可选集成不改变 ETAF 的用户模型。 存储不绑定 SQLite。PostgreSQL、REST、文件或 ORM 集成都应该提供同样契约的具体 Data Source它们是可选集成不改变 ETAF 的用户模型。
独立的 `etaf-sqlite` 包是第一个具体存储 source
```elisp
(require 'etaf-sqlite)
(let* ((table (etaf-sqlite-table
'tasks
(list (etaf-sqlite-column :id "id"
:type 'integer :primary t)
(etaf-sqlite-column :title "title" :type 'text))
:id))
(database (etaf-sqlite-database "tasks.sqlite" table))
(source (etaf-sqlite-source database)))
(etaf-sqlite-initialize database)
(let ((controller (etaf-data-controller source :auto-load t)))
(etaf-data-mutate controller 'insert
'(:id 1 :title "Write the guide."))
(message "%S" (etaf-value (etaf-data-items controller)))
(etaf-data-stop controller)))
```
这个包使用 Emacs 内置 SQLite 能力,有意不增加 ORM 层。其他存储包应直接实现 `etaf-data-source`
## 11. Resource 与 raw Ebox ## 11. Resource 与 raw Ebox
Resource 是 Scope 所有的同步 loader Resource 是 Scope 所有的同步 loader
@ -475,9 +517,9 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
只有普通 Host 和 Component lowering 无法表达真实 Ebox 需求时才使用它;返回的 Node 对 ETAF 语义保持 opaque。 只有普通 Host 和 Component lowering 无法表达真实 Ebox 需求时才使用它;返回的 Node 对 ETAF 语义保持 opaque。
## 12. Playground ## 12. Playgrounds
可选的 core Playground 是一个完整的 ETAF 应用示例: 独立的 ETAF Playground 是一个完整的 ETAF 应用示例:
```elisp ```elisp
(require 'etaf-playground) (require 'etaf-playground)
@ -492,6 +534,15 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
`etaf-playground-close` 会卸载并删除默认 Playground buffer。它与 `ebox-playground` 独立,核心 ETAF 不会自动加载任一 Playground。 `etaf-playground-close` 会卸载并删除默认 Playground buffer。它与 `ebox-playground` 独立,核心 ETAF 不会自动加载任一 Playground。
独立的 Ebox Playground 用来展示底层布局契约:
```elisp
(require 'ebox-playground)
(ebox-playground-open)
```
它只依赖 Ebox。检查 Ebox Box 和 Grid 布局时使用它;检查 Component、Runtime、Data 和官方目录时使用 `etaf-playground`
## 13. 公共 API 速查 ## 13. 公共 API 速查
| API 家族 | 主要入口 | 何时使用 | | API 家族 | 主要入口 | 何时使用 |
@ -508,7 +559,7 @@ Loader 错误会保存在 `etaf-resource-error`cleanup/type 错误保持可
| Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择和停止数据 | | Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择和停止数据 |
| Resource | `etaf-resource`、`etaf-resource-result`、`etaf-error-boundary-run` | 管理 loader 状态和 cleanup | | Resource | `etaf-resource`、`etaf-resource-result`、`etaf-error-boundary-run` | 管理 loader 状态和 cleanup |
| 官方 UI | `require 'etaf-ui`、`etaf-button`、`etaf-checkbox`、`etaf-label`、`etaf-panel`、`etaf-data-grid` | 使用现成 Component | | 官方 UI | `require 'etaf-ui`、`etaf-button`、`etaf-checkbox`、`etaf-label`、`etaf-panel`、`etaf-data-grid` | 使用现成 Component |
| Playground | `etaf-playground-open`、`etaf-playground-open-ui`、`etaf-playground-close` | 探索框架 | | Playground | `etaf-playground-open`、`etaf-playground-open-ui`、`etaf-playground-close`、`ebox-playground-open`、`ebox-playground-close` | 探索对应层 |
大多数应用一开始只需要 `etaf-view`、`etaf-mount`、`etaf-define-component`、`etaf-ref` 和事件 callback。其余 API 都是可选能力,不是理解核心语法的前置条件。 大多数应用一开始只需要 `etaf-view`、`etaf-mount`、`etaf-define-component`、`etaf-ref` 和事件 callback。其余 API 都是可选能力,不是理解核心语法的前置条件。

View File

@ -1,114 +0,0 @@
;;; etaf-playground.el --- Small ETAF application examples -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; The ETAF playground is an optional application example. It exercises only
;; public ETAF APIs; the Ebox playground remains a separate sibling package.
;;; Code:
(require 'cl-lib)
(require 'etaf)
(defconst etaf-playground-buffer-name "*ETAF Playground*"
"Default buffer name used by `etaf-playground-open'.")
;;;###autoload
(etaf-define-component etaf-playground-counter (&key title)
"Render a small stateful counter titled TITLE.
The counter demonstrates a retained `:setup' Scope, reactive state, semantic
Host references, and ordinary `:on-press' callbacks without introducing a
special counter node or a second event model."
:setup
(let ((count (etaf-ref 0 :name 'playground-count)))
(lambda ()
(etaf-view
(column
:class "etaf-playground-counter"
(text :face 'bold (expr :value title))
(text
(expr :value (format "Count: %d" (etaf-value count))))
(row
(text
:ref 'decrement
:role 'button
:tab-index 0
:on-press (lambda () (cl-decf (etaf-value count)))
" - ")
(text
:ref 'increment
:role 'button
:tab-index 0
:on-press (lambda () (cl-incf (etaf-value count)))
" + ")))))))
;;;###autoload
(defun etaf-playground-view ()
"Return the core ETAF playground View.
The returned View has no buffer or global state side effect until it is passed
to `etaf-mount'."
(etaf-view
(column
:class "etaf-playground-root"
(text :face 'bold "ETAF Playground")
(text :color "#687386"
"One grammar: Hosts, Components, props, children, and expr.")
(etaf-playground-counter :title "Reactive counter")
(text :color "#687386"
"Press + or - with `etaf-dispatch-event' or the normal event bridge."))))
;;;###autoload
(defun etaf-playground-ui-view ()
"Return a playground View that also demonstrates the official UI catalog.
Loading this function requires `etaf-ui'; the core playground itself does not
depend on that optional Component catalog."
(require 'etaf-ui)
(etaf-view
(column
(etaf-panel :title "Official Components"
(etaf-button
:label "A public Component"
:on-press (lambda () (message "ETAF UI button pressed")))
(etaf-checkbox
:checked nil
:label "Controlled checkbox"
:on-change (lambda (value)
(message "Checkbox value: %s" value))))
(etaf-playground-counter :title "Core counter"))))
;;;###autoload
(defun etaf-playground-open (&optional buffer-name)
"Mount the core ETAF playground in BUFFER-NAME and return its buffer."
(interactive)
(etaf-mount (or buffer-name etaf-playground-buffer-name)
(etaf-playground-view)))
;;;###autoload
(defun etaf-playground-open-ui (&optional buffer-name)
"Mount the ETAF playground with official UI Components in BUFFER-NAME."
(interactive)
(etaf-mount (or buffer-name etaf-playground-buffer-name)
(etaf-playground-ui-view)))
;;;###autoload
(defun etaf-playground-close (&optional buffer-name)
"Unmount and kill the playground BUFFER-NAME, returning its buffer.
When BUFFER-NAME is nil, use `etaf-playground-buffer-name'."
(interactive)
(let* ((name (or buffer-name etaf-playground-buffer-name))
(buffer (get-buffer name)))
(when-let ((runtime (and buffer (etaf-runtime-for-buffer buffer))))
(etaf-unmount runtime))
(when buffer
(kill-buffer buffer))
buffer))
(provide 'etaf-playground)
;;; etaf-playground.el ends here

View File

@ -346,7 +346,7 @@ the ordinary layout lowering path instead."
(list (apply #'ebox-spacer props))) (list (apply #'ebox-spacer props)))
('fragment ('fragment
(etaf--render-value-list children (append path (list :fragment)))) (etaf--render-value-list children (append path (list :fragment))))
((or 'row 'column 'container 'stack 'flex) ((or 'row 'column 'container 'stack 'flex 'grid)
(let ((nodes nil) (let ((nodes nil)
(index 0)) (index 0))
(dolist (child children) (dolist (child children)
@ -373,6 +373,8 @@ the ordinary layout lowering path instead."
(ebox-build (append (list 'column) props nodes)))) (ebox-build (append (list 'column) props nodes))))
((eq name 'flex) ((eq name 'flex)
(apply #'ebox-flex (append props nodes))) (apply #'ebox-flex (append props nodes)))
((eq name 'grid)
(apply #'ebox-grid (append props nodes)))
(t (t
(signal 'etaf-renderer-error (signal 'etaf-renderer-error
(list (format "Not a layout Host: %S" name)))))) (list (format "Not a layout Host: %S" name))))))

View File

@ -1,190 +0,0 @@
;;; etaf-ui.el --- Official ETAF Components -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; This file is the official Component catalog. It deliberately exposes
;; Components, not Control/Widget/DataGrid runtime categories: a DataGrid is
;; a normal compound Component built from the same View, props, events, and
;; reactive Data contracts as every other catalog entry.
;;; Code:
(require 'cl-lib)
(require 'etaf)
(declare-function etaf-data-status "etaf-data" (controller))
(declare-function etaf-data-items "etaf-data" (controller))
(declare-function text "etaf-view" (&rest arguments))
(declare-function row "etaf-view" (&rest arguments))
(declare-function column "etaf-view" (&rest arguments))
(declare-function expr "etaf-view" (&rest arguments))
(declare-function slot "etaf-view" (&rest arguments))
(defun etaf-ui--column-value (column key)
"Return KEY from COLUMN, accepting a plist or alist descriptor."
(if (and (listp column) (keywordp (car column)))
(plist-get column key)
(alist-get key column)))
(defun etaf-ui--grid-cell-value (row key)
"Return KEY from data ROW, accepting a plist, alist, or hash table."
(cond
((hash-table-p row) (gethash key row))
((and (proper-list-p row)
(zerop (% (length row) 2))
(keywordp (car row)))
(plist-get row key))
((listp row) (alist-get key row))
(t nil)))
(defun etaf-ui--grid-header-cell (column)
"Return one header View for COLUMN."
(etaf-view
(text
:face 'bold
:width (etaf-ui--column-value column :width)
(expr :value (format "%s"
(or (etaf-ui--column-value column :label)
(etaf-ui--column-value column :key)))))))
(defun etaf-ui--grid-header (columns)
"Return a View header row for COLUMNS."
(etaf-view
(row
:class "etaf-data-grid-header"
(expr :value (mapcar #'etaf-ui--grid-header-cell columns)))))
(defun etaf-ui--grid-cell (row column)
"Return one data cell View for ROW and COLUMN."
(let ((key (etaf-ui--column-value column :key)))
(etaf-view
(text
:width (etaf-ui--column-value column :width)
(expr :value
(format "%s" (or (etaf-ui--grid-cell-value row key) "")))))))
(defun etaf-ui--grid-cells (row columns)
"Return data cell Views for ROW and COLUMNS."
(mapcar (lambda (column) (etaf-ui--grid-cell row column)) columns))
(defun etaf-ui--grid-row (row columns row-key on-row-press)
"Return a View row for ROW, COLUMNS, ROW-KEY, and ON-ROW-PRESS."
(let ((key (funcall row-key row)))
(unless key
(error "ETAF DataGrid row-key must return a non-nil stable scalar"))
(etaf-view
(row
:key key
:class "etaf-data-grid-row"
:on-press (when on-row-press
(lambda () (funcall on-row-press row)))
(expr :value (etaf-ui--grid-cells row columns))))))
;;;###autoload
(etaf-define-component etaf-button (&key label on-press disabled ref)
"Render a standard pressable button with LABEL and ON-PRESS.
DISABLED removes the callback and the default focus tab index. The Component
has no `variant' or Control type; callers use ordinary props and `:styles'
when they need a product-specific appearance."
:styles
(styles
("&" :padding (0 1) :border ((1) solid "#687386"))
("&.disabled" :color "#8A93A6")
("&.enabled" :face bold))
:view
(text
:class (if disabled "etaf-button disabled" "etaf-button enabled")
:role 'button
:ref ref
:disabled disabled
:tab-index (unless disabled 0)
:on-press (unless disabled on-press)
(expr :value label)))
;;;###autoload
(etaf-define-component etaf-checkbox (&key checked label on-change ref)
"Render a controlled checkbox with CHECKED, LABEL, and ON-CHANGE.
ON-CHANGE receives the next boolean value. State ownership remains with the
caller, so the Component is reusable in both local and Data-backed forms."
:view
(row
:class "etaf-checkbox"
:role 'checkbox
:ref ref
:aria-label label
:on-press (lambda ()
(when on-change
(funcall on-change (not checked))))
(text :face 'bold (expr :value (if checked "" "")))
(text (expr :value (if label (concat " " label) "")))))
;;;###autoload
(etaf-define-component etaf-label (&key text face)
"Render TEXT as a semantic text label with optional FACE."
:view
(text :face face (expr :value text)))
;;;###autoload
(etaf-define-component etaf-panel (&key title)
"Render a titled panel with header and default slot projections."
:styles
(styles
("&" :padding (1 2) :border ((1) solid "#687386"))
(".etaf-panel-title" :face bold))
:view
(column
:class "etaf-panel"
(expr
:value
(when title
(etaf-view (text :class "etaf-panel-title"
(expr :value title)))))
(slot :name 'header)
(slot)))
;;;###autoload
(etaf-define-component etaf-data-grid
(&key controller columns row-key on-row-press
loading-label error-label empty-label)
"Render rows from reactive DATA CONTROLLER and COLUMNS.
COLUMNS is a list of descriptors such as `(:key :name :label NAME)'.
ROW-KEY is a function receiving each row and must return a non-nil stable
scalar key.
The Data Controller owns loading, errors, pagination, mutation, and selection;
this Component only projects that state into ordinary Hosts."
:view
(column
:class "etaf-data-grid"
(expr :value (etaf-ui--grid-header columns))
(expr
:value
(progn
(unless (functionp row-key)
(error "ETAF DataGrid requires a function-valued :row-key"))
(let ((status (etaf-value (etaf-data-status controller)))
(items (etaf-value (etaf-data-items controller))))
(cond
((eq status 'loading)
(etaf-view (text (expr :value (or loading-label "Loading...")))))
((eq status 'error)
(etaf-view (text :color "#FF6B6B"
(expr :value
(or error-label "Unable to load data.")))))
((null items)
(etaf-view (text (expr :value (or empty-label "No data.")))))
(t
(mapcar
(lambda (item)
(etaf-ui--grid-row item columns row-key
on-row-press))
items))))))
(slot :name 'footer)))
(provide 'etaf-ui)
;;; etaf-ui.el ends here

View File

@ -66,7 +66,7 @@
key-thunk) key-thunk)
(defconst etaf--host-names (defconst etaf--host-names
'(text fragment container row column stack flex spacer) '(text fragment container row column stack flex grid spacer)
"Minimal unstyled Hosts implemented by ETAF core. "Minimal unstyled Hosts implemented by ETAF core.
Product Components such as Button belong to `etaf-ui'; they are not added to Product Components such as Button belong to `etaf-ui'; they are not added to

View File

@ -1,45 +0,0 @@
;;; etaf-playground-tests.el --- ETAF playground tests -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Code:
(require 'ert)
(require 'etaf-playground)
(ert-deftest etaf-playground-mounts-and-dispatches-core-example ()
"Mount the public playground and update its retained counter."
(let ((buffer-name " *etaf-playground-test*"))
(unwind-protect
(progn
(etaf-mount buffer-name (etaf-playground-view))
(with-current-buffer buffer-name
(should (string-match-p "ETAF Playground" (buffer-string)))
(should (string-match-p "Count: 0" (buffer-string))))
(let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-dispatch-event runtime 'increment 'press)
(with-current-buffer buffer-name
(should (string-match-p "Count: 1" (buffer-string))))))
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
(ert-deftest etaf-playground-ui-keeps-the-catalog-optional ()
"Mount the optional UI catalog through the playground public entry point."
(require 'etaf-ui)
(let ((buffer-name " *etaf-playground-ui-test*"))
(unwind-protect
(progn
(etaf-mount buffer-name (etaf-playground-ui-view))
(with-current-buffer buffer-name
(let ((contents (buffer-string)))
(should (string-match-p "Official Components" contents))
(should (string-match-p "A public Component" contents))
(should (string-match-p "Controlled checkbox" contents)))))
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
;;; etaf-playground-tests.el ends here

View File

@ -243,6 +243,18 @@
(etaf-test--render-text (etaf-test--render-text
(etaf-view (column (text "A") (text "B"))))))) (etaf-view (column (text "A") (text "B")))))))
(ert-deftest etaf-view-grid-lowers-to-ebox-grid ()
"Lower the Grid Host through Ebox's two-dimensional layout node."
(let ((node (etaf-render
(etaf-view
(grid
:grid-template-columns '((20) (20))
(text "A")
(text "B"))))))
(should (eq (plist-get node :ebox-type) 'grid))
(should (string-match-p "A" (substring-no-properties (ebox-render node))))
(should (string-match-p "B" (substring-no-properties (ebox-render node))))))
(ert-deftest etaf-view-prefixed-host-alias-lowers-to-canonical-host () (ert-deftest etaf-view-prefixed-host-alias-lowers-to-canonical-host ()
"Resolve an explicit `etaf-' Host spelling to its core Host name." "Resolve an explicit `etaf-' Host spelling to its core Host name."
(should (should

View File

@ -1,146 +0,0 @@
;;; etaf-ui-tests.el --- Official ETAF Component tests -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Code:
(require 'ert)
(require 'etaf-ui)
(defun etaf-ui-test--buffer-text (buffer-name)
"Return plain text currently published in BUFFER-NAME."
(with-current-buffer buffer-name
(string-trim-right (substring-no-properties (buffer-string)))))
(ert-deftest etaf-ui-button-dispatches-controlled-press ()
"Render a button and dispatch its public Host callback."
(let ((buffer-name " *etaf-ui-button-test*")
(presses 0))
(unwind-protect
(progn
(etaf-mount
buffer-name
(etaf-view
(etaf-button
:label "Save"
:ref 'save
:on-press (lambda () (cl-incf presses)))))
(should (string-match-p "Save" (etaf-ui-test--buffer-text buffer-name)))
(etaf-dispatch-event
(etaf-runtime-for-buffer buffer-name) 'save 'press)
(should (= 1 presses)))
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
(ert-deftest etaf-ui-checkbox-emits-next-value ()
"Render a controlled checkbox and emit its next checked value."
(let ((buffer-name " *etaf-ui-checkbox-test*")
next)
(unwind-protect
(progn
(etaf-mount
buffer-name
(etaf-view
(etaf-checkbox
:checked nil
:label "Done"
:ref 'done
:on-change (lambda (value) (setq next value)))))
(should (string-match-p "☐ Done"
(etaf-ui-test--buffer-text buffer-name)))
(etaf-dispatch-event
(etaf-runtime-for-buffer buffer-name)
'done
'press)
(should (eq t next)))
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime))
(when-let ((buffer (get-buffer buffer-name)))
(kill-buffer buffer)))))
(ert-deftest etaf-ui-panel-projects-named-and-default-slots ()
"Project panel header and body slots through the common slot model."
(let ((rendered
(string-trim-right
(substring-no-properties
(ebox-render
(etaf-render
(etaf-view
(etaf-panel :title "Account"
(slot :name 'header (text "Settings"))
(text "Body")))))))))
(dolist (label '("Account" "Settings" "Body"))
(should (string-match-p (regexp-quote label) rendered)))))
(ert-deftest etaf-ui-data-grid-projects-reactive-controller ()
"Render DataGrid rows and update them through the Data Controller."
(let* ((source (etaf-data-memory-source
'((:id 1 :name "Ada") (:id 2 :name "Grace"))
:id-key :id))
(controller (etaf-data-controller source :page-size 10 :auto-load t))
(buffer-name " *etaf-ui-grid-test*"))
(unwind-protect
(progn
(etaf-mount
buffer-name
(etaf-view
(etaf-data-grid
:controller controller
:columns '((:key :id :label "ID")
(:key :name :label "Name"))
:row-key (lambda (row) (plist-get row :id)))))
(should (string-match-p "Ada" (etaf-ui-test--buffer-text buffer-name)))
(etaf-data-mutate controller 'insert '(:id 3 :name "Alan"))
(should (string-match-p "Alan" (etaf-ui-test--buffer-text buffer-name))))
(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)))))
(ert-deftest etaf-ui-data-grid-requires-stable-row-key ()
"Reject a DataGrid that cannot identify retained rows."
(let* ((source (etaf-data-memory-source
'((:id 1 :name "Ada"))
:id-key :id))
(controller (etaf-data-controller source :page-size 10 :auto-load t))
(buffer-name " *etaf-ui-grid-row-key-test*"))
(unwind-protect
(should-error
(etaf-mount
buffer-name
(etaf-view
(etaf-data-grid
:controller controller
:columns '((:key :id :label "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)))))
(ert-deftest etaf-ui-data-grid-rejects-nil-row-key ()
"Reject a DataGrid row-key function that returns no identity."
(let* ((source (etaf-data-memory-source
'((:id 1 :name "Ada"))
:id-key :id))
(controller (etaf-data-controller source :auto-load t))
(buffer-name " *etaf-ui-grid-nil-row-key-test*"))
(unwind-protect
(should-error
(etaf-mount
buffer-name
(etaf-view
(etaf-data-grid
:controller controller
:columns '((:key :id :label "ID"))
:row-key (lambda (_row) nil)))))
(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)))))
;;; etaf-ui-tests.el ends here