Replace console demo with SQLite Research Shelf

This commit is contained in:
Kinneyzhang 2026-08-22 08:21:26 +08:00
parent f93bdf56d6
commit dcb0ad9d83
15 changed files with 1293 additions and 1558 deletions

176
DESIGN.md
View File

@ -2,104 +2,156 @@
## Source of truth ## Source of truth
- Status: Active draft for the sole reviewed pair - Status: Active
- Last refreshed: 2026-08-20 - Last refreshed: 2026-08-22
- Primary product surface: `examples/operations-console.etaf` + `examples/operations-console.el` - Primary product surface: `examples/research-shelf.etaf` + `examples/research-shelf.el`
- Visual reference: `design/operations-console.html` - Visual reference: `design/research-shelf.html`
- Evidence reviewed: current pair loader/manifest, `etaf-ui.el`, Ebox viewport contract, and the current GUI/performance evidence. - Evidence reviewed: ETAF public View/Component/Data/Resource APIs, `etaf-ui`
Button/Checkbox/Panel/DataGrid/Pagination, and `etaf-sqlite`'s typed source
contract.
## Brand ## Brand
- Personality: calm operations cockpit; precise, warm, and quietly technical. - Personality: editorial, focused, warm, quietly intelligent.
- Trust signals: explicit status chips, visible last-updated time, deterministic counts, clear success/error feedback. - Trust signals: durable SQLite data, visible save state, selected-item detail,
- Avoid: generic toy dashboard, rainbow gradients, dense ungrouped controls, decorative widgets with no semantic state. explicit progress/status, and recoverable storage errors.
- Avoid: generic admin dashboards, KPI walls, fake charts, rainbow gradients,
and a showcase that exists only to exercise APIs.
## Product goals ## Product goals
- Make one attractive app prove ETAF's Component, composition, reactive, Context/Theme, Behavior, event/Action, Data, Resource, error-boundary, focus, and lifecycle contracts. - Build a useful personal reading/research shelf backed by a real SQLite file.
- Do not add another Playground scenario or hide framework limitations behind fake animation. - Make triage, selection, progress updates, filtering, pagination, and
- Keep the HTML prototype and ETAF rendering aligned in hierarchy, state labels, interaction outcomes, and compact/fullscreen geometry. recovery feel like one coherent product rather than isolated demos.
- Exercise existing ETAF components through composition before adding any new
component type.
- Keep the static `.etaf` file responsible for shell/section composition while
the `.el` companion owns storage, state, callbacks, and lifecycle.
## Personas and jobs ## Personas and jobs
- Primary personas: ETAF framework reviewer and application author learning the public API. - Primary persona: an Emacs user collecting papers, books, essays, and notes.
- User jobs: understand the layout model, trigger each capability, inspect visible state changes, and verify recovery/cleanup behavior. - Jobs: find what to read next, filter by state/priority, inspect one item,
- Key contexts: 900940 px compact GUI, fullscreen GUI, keyboard-only review, and batch semantic tests. update progress, mark an item finished, and recover after a storage error.
- Contexts: compact desktop window, fullscreen review, keyboard-first use, and
a local SQLite database that survives unmount/remount.
## Information architecture ## Information architecture
- Shell: brand/header -> navigation tabs -> page surface -> persistent status footer. - Shell: compact brand bar -> filter rail -> reading list -> detail inspector.
- Overview: hero/status strip, KPI cards, activity/control workspace, capability cards, and event timeline. - Filter rail: All, In progress, Unread, Finished, and Starred; each is a real
- Data: query/selection summary, interactive DataGrid, and selection detail panel. action and the list summary exposes the matching count.
- Resource: resource status, reload/fail-next controls, boundary result, and cleanup counter. - Reading list: title/author/status/progress rows, single selection, paging,
- Content hierarchy: page title and status first; primary action second; diagnostics/details third. empty/loading/error states.
- Detail inspector: title, author, type, status, progress, priority, note,
last updated, and actions (`+10%`, `Finish`, `Star`, `Archive`).
- Footer: database path/status, visible result count, and current page.
## Design principles ## Design principles
1. One visual hierarchy: every control belongs to a named surface and every surface has a visible status. 1. One useful workflow: every visible control helps decide or update what to
2. State before decoration: theme, loading, error, selected, disabled, and success states are readable as text as well as color. read next.
3. Stable geometry: controls remain single-line at compact width; page changes replace only the page surface. 2. Editorial hierarchy over dashboard chrome: one strong title, restrained
4. Public API truth: the prototype may use ordinary HTML/JS, but the ETAF port uses only public ETAF/ETAF-UI APIs. metadata, and generous whitespace around the selected record.
3. State is durable and observable: SQLite is the source of truth; UI status
tells the user when a mutation is loading, saved, or failed.
4. Composition first: reuse public UI Components and keep storage/state in the
companion, not in `.etaf` or a parallel widget layer.
5. Fail closed: a SQLite failure keeps the last usable list and exposes a
retry action instead of replacing the whole surface with a stack trace.
## Visual language ## Visual language
- Color: ink navy `#142235`, paper `#F6F1E8`, slate `#526174`, teal `#2E8B83`, coral `#E26D5A`, amber `#D99A3D`, success `#3E9B72`. - Color: ink `#172033`, paper `#F7F3EA`, cobalt `#3657D6`, mint `#3E9B8F`,
- Typography: readable sans body, compact monospace labels/metrics, bold page titles. coral `#D86B5D`, amber `#C58A3A`, muted slate `#6D7482`.
- Spacing/layout rhythm: one-line outer rhythm, two-column workspace, fixed gaps, full-width shell, deliberate card padding. - Typography: readable proportional labels with compact monospace metadata;
- Shape/elevation: thin dark borders and restrained card elevation; no floating overlay is required by the ETAF port. title and selected record use bold weight, not oversized headings.
- Motion: no animation is required; state changes are immediate and reduced-motion safe. - Spacing/layout rhythm: 1-cell outer rhythm, 2-cell panel padding, 1-cell
- Imagery/iconography: text marks and Unicode status glyphs only; no external asset dependency. grid gaps, and stable intrinsic control widths.
- Shape/elevation: thin borders, small radius-like grouped rows where Ebox
permits, no floating overlays or decorative shadows.
- Motion: immediate state changes; no timer-driven animation; reduced-motion
safe by construction.
- Imagery/iconography: Unicode marks with semantic meaning: `⌕`, `★`, `✓`,
`◷`, `↗`, `⚠`, and `·`.
## Components ## Components
- Existing components to reuse: public `button`, `checkbox`, `label`, `panel`, and `data-grid` from `etaf-ui`. - Existing components to reuse: public `button`, `checkbox`, `label`, `panel`,
- New/changed components: `operations-console-shell`, `hero-status`, `metric-strip`, `activity-card`, `capability-card`, `timeline`, `data-page`, and `resource-page` in the companion. `data-grid`, and `pagination` from `etaf-ui`.
- Variants/states: active/inactive nav, light/dark theme, selected/unselected row, Behavior on/off, resource ready/loading/error, boundary handled, compact/fullscreen. - Companion composition: `research-shelf-shell`, `filter-rail`,
- Ownership: `.etaf` owns static composition and section names; `.el` owns tokens, props, refs, resources, handlers, and state. `reading-list`, and `detail-inspector`; these are ordinary ETAF Components,
not a second UI kit.
- Variants/states: light/dark Theme, selected/unselected row, starred, empty,
loading, error, saved, disabled-at-boundary, and archived.
- Token ownership: palette and spacing tokens live in the companion; `.etaf`
contains only validated structural facts and labels.
## Playground framework boundary
- `etaf-playground.el` is a reusable framework, not an application module.
- The framework owns pair registration semantics, safe inert `.etaf` parsing,
companion loading, mount/reset/close, and test/GUI entry points. Replaceable
deployment metadata lives in `etaf-playground-catalog.el`, outside the
loader implementation.
- A pair companion owns its Components, storage adapters, state, resources,
events, and root factory. The framework must not reference a business
component, database package, table schema, palette, or application-specific
ref.
- Reuse is expressed through the generic manifest/loader contract and the
existing public ETAF/etaf-ui components. New business behavior belongs in a
same-basename pair, never in the framework.
- Any future Playground example must add a catalog entry and its own `.etaf`/
`.el` files without changing framework semantics.
## Accessibility ## Accessibility
- Target: keyboard-complete, high-contrast text, semantic roles through public UI components. - Target: keyboard-complete and high-contrast desktop UI.
- Keyboard/focus: Tab and Shift-Tab traverse nav/actions/data rows; RET dispatches the focused ref; focus clears when a target is removed. - Keyboard/focus: Tab order follows filters -> rows -> detail actions -> pager;
- Every state has text feedback; color is never the only signal. No hover-only behavior. RET dispatches the focused public ref; selected row is announced in text.
- Ebox Theme carries inherited Ebox defaults; application palette lookups remain - Color is never the only state signal; status uses text and Unicode marks.
in the companion, while the Renderer filters unknown token names before they - Disabled actions remain visible with disabled semantics and explanatory help.
reach Ebox geometry.
## Responsive behavior ## Responsive behavior
- Supported surfaces: compact 900940 px GUI body and fullscreen desktop GUI. - Wide/fullscreen: three-column grid (rail / list / detail).
- Metric cards remain in one row when possible; workspace falls back from two columns to stacked sections; controls keep intrinsic single-line widths. - Compact: rail becomes a top filter row, detail follows the list, and all
- Use `flex` for rows that distribute remaining width; reserve `row` for fixed controls remain intrinsic single-line widths.
intrinsic control groups. The navigation control group is max-content so - Use `grid` for the product skeleton, `flex` for toolbars and action groups,
each hover/active range remains an independent control. and `row` only for compact intrinsic controls.
- Mouse-1 and keyboard activation use the same public refs.
## Interaction states ## Interaction states
- Loading: resource card reports `Loading…` without moving the shell. - Loading: list keeps its frame and shows `◷ Loading library…`.
- Empty: DataGrid reports an explicit empty state and keeps selection controls available. - Empty: selected filter explains why there are no matches and offers `All`.
- Error: resource reload can fail once; the boundary card reports the handled error and recovery remains available. - Error: storage card shows `⚠ Could not save` with `Retry`; last committed
- Success: action/status and timestamp update visibly. data remains visible.
- Disabled: unavailable actions expose disabled semantics rather than silently ignoring input. - Success: mutation shows `✓ Saved locally` and updates `updated` metadata.
- Disabled: `Finish` is disabled for finished records; `+10%` is disabled at
100%; archive is disabled while loading.
## Content voice ## Content voice
- Tone: concise, operational, and explanatory; labels name the framework capability being demonstrated. - Tone: concise, observant, and useful; never framework-centric in primary UI.
- Terminology: use `Component`, `Context`, `Theme`, `Behavior`, `Action`, `Data`, `Resource`, and `Boundary` consistently. - Terminology: Reading, In progress, Finished, Starred, Saved locally,
- Microcopy: prefer a visible verb + result (`Reload resource`, `Handled: …`, `Selected Beacon`) over generic `Done`. Storage issue.
- Microcopy: action + result (`Mark finished`, `Saved locally`) instead of
generic `Done`.
## Implementation constraints ## Implementation constraints
- Framework/styling: ETAF public View DSL in `.etaf`/`.el`; Ebox renders the result; no HTML/CSS runtime dependency. - Framework/styling: public ETAF View DSL and `etaf-ui`; SQLite through the
- Design tokens: keep palette/spacing tokens in one companion source and reuse them; no scattered ad-hoc colors. sibling `etaf-sqlite` package; no HTML/CSS runtime dependency.
- Performance: ordinary warm p50 <=100 ms, counter/resource p50 <=150 ms, every measured max <=250 ms; one publication per logical action. - Data schema: one typed `reading_items` SQLite table with id/title/author/
- Compatibility: preserve one same-basename `operations-console` pair, safe static reader, public API boundary, mount twice/unmount cleanup, and manifest refs. kind/status/progress/priority/starred/note/updated columns.
- Navigation: the stable page ref is read by the retained router Component; its - Performance: one Data mutation -> one Runtime generation/publication; normal
page Host is the single semantic Range anchor, so page switches replace one warm actions remain under the existing 100ms p50 target.
Range payload instead of rebuilding the shell root. - Compatibility: one same-basename `research-shelf` pair only; old console
- Review: HTML is the visual reference; ETAF needs compact/fullscreen screenshots and dynamic keyboard/mouse checkpoints. examples are removed from the active manifest and compile surface.
- Verification: SQLite temp-file integration tests, pair mount/remount tests,
repeated selection/mutation tests, and clean fullscreen GUI screenshots.
## Open questions ## Open questions
- [ ] User review may adjust palette or page naming after the first HTML/ETAF comparison; behavior and public-ref coverage remain fixed. - [ ] User may rename the product after reviewing the first HTML/ETAF render;
architecture and interaction contracts remain unchanged.

View File

@ -2,94 +2,80 @@
## 来源与状态 ## 来源与状态
- 状态:唯一 reviewed pair 的 active draft - 状态active
- 更新日期2026-08-20 - 更新日期2026-08-22
- 主产品面:`examples/operations-console.etaf` + `examples/operations-console.el` - 主产品面:`examples/research-shelf.etaf` + `examples/research-shelf.el`
- 视觉参考:`design/operations-console.html` - HTML 视觉基线:`design/research-shelf.html`
- 已审查证据pair loader/manifest、`etaf-ui.el`、Ebox viewport 合同及当前 GUI/性能证据。 - 已审查ETAF View/Component/Data API、`etaf-ui` 的公开组件,以及
`etaf-sqlite` 的 typed source 合同。
## 品牌 ## 产品定位
- 气质:安静的操作控制台;准确、温暖、克制地体现技术感。 Research Shelf 是一个真正有用的本地研究/阅读架SQLite 保存书籍、论文、
- 信任信号:状态 chip、更新时间、明确计数、清晰的成功/错误反馈 文章和笔记,用户可以筛选、分页、选择一条记录、更新进度、完成、收藏和归档
- 避免:普通玩具 dashboard、彩虹渐变、无语义状态的装饰控件和杂乱控制区 它不是把 API 名称堆成控制台,而是用一个连贯 workflow 验证 ETAF 的组合能力
## 产品目标 ## 结构与分层
- 用一个好看的 app 同时展示 Component、组合、响应式、Context/Theme、Behavior、事件/Action、Data、Resource、错误边界、焦点和生命周期。 - Shell标题/副标题/主题切换 → 三列 workspace → 持久化状态栏。
- 不扩展第二个 Playground 场景,不伪装成完整生产分析产品,不用假动画掩盖框架问题。 - 三列 workspace筛选栏 / SQLite reading list / selected-item detail inspector。
- HTML 原型和 ETAF 实现保持同一层级、状态文字、交互结果和 compact/fullscreen 几何。 - `.etaf`只保存经过白名单校验的静态结构和文案section、filter、label
- 同名 `.el`:消费 `.etaf`,组合公开 UI Components创建 SQLite schema、Data
Controller、refs、事件处理和生命周期清理。
- 现有可复用组件:`button`、`checkbox`、`label`、`panel`、`data-grid`、
`pagination`;应用只负责把它们组合成产品,不复制一套 UI kit。
## 信息架构 ## Playground 框架边界(必须长期遵守)
- Shell品牌/header → 导航 tabs → page surface → 持久 status footer。 `etaf-playground.el` 是通用 pair playground 框架,不是业务应用模块。
- Overviewhero/status、KPI 卡片、活动/控制 workspace、能力卡片和事件时间线。 `etaf-playground-catalog.el` 是独立、可替换的部署目录;业务 pair 的名字、refs
- Data选择摘要、可交互 DataGrid、selection detail。 和验证元数据放在 catalog不写进 loader 实现。
- Resource资源状态、reload/fail-next、boundary 结果和 cleanup 计数。
- 层级:先显示页面标题和状态,再显示主操作,最后显示诊断/细节。
## 设计原则 框架只负责:
1. 一个视觉层级:每个控件属于有名字的 surface每个 surface 都有可见状态。 - pair 注册语义和 catalog 读取;
2. 状态优先于装饰theme、loading、error、selected、disabled、success 都有文字表达。 - `.etaf` 的惰性、inert、白名单读取
3. 几何稳定compact 下控件保持单行,页面只替换 page surface。 - 按同名 pair 加载 companion
4. API 真实HTML 可以用普通 HTML/JS但 ETAF 只用公开 ETAF/ETAF-UI API。 - mount、reset、close以及测试/GUI 入口。
框架绝不负责:业务 Component、数据库包或表结构、palette、业务 state、
handlers、resources 或应用 refs。它不能 `require etaf-sqlite`,也不能把业务
组件实现塞进 loader/helper。未来新增例子时只增加一个同名 `.etaf`/`.el` pair
和 catalog entry不改变框架语义。
组合和复用是默认设计:优先复用 ETAF/`etaf-ui` 的公开契约,优先拆出清晰的
Component 边界,避免新增 helper 层或抽象泄漏。
## 视觉语言 ## 视觉语言
- 色彩:墨蓝 `#142235`、纸张 `#F6F1E8`、灰蓝 `#526174`、青绿 `#2E8B83`、珊瑚 `#E26D5A`、琥珀 `#D99A3D`、成功绿 `#3E9B72` - 气质editorial、专注、温暖、安静而聪明避免通用 admin dashboard、KPI
- 字体:易读 sans body紧凑 monospace 指标/标签,粗体页面标题。 墙、假图表、彩虹渐变和装饰性噪声。
- 节奏:一行外部节奏、双列 workspace、固定 gap、shell 全宽,卡片有明确内边距。 - 色彩ink `#172033`、paper `#F7F3EA`、cobalt `#3657D6`、mint `#3E9B8F`
- 形状:细深色边框、克制 elevationETAF 不依赖浮层。 coral `#D86B5D`、amber `#C58A3A`、muted `#6D7482`
- 动效:不以动画为正确性前提,状态变化即时且支持 reduced motion。 - 节奏1 格外部节奏、2 格 panel padding、1 格 grid gap控件保持 intrinsic
- 图形:只用文字标记和 Unicode 状态符号,不依赖外部资源。 宽度;`grid` 负责页面骨架,`flex` 负责 toolbar/action group。
- 状态:文字和 Unicode 同时表达状态:`⌕`、`★`、`✓`、`◷`、`↗`、`⚠`、`·`。
- 动效:状态立即可见,不依赖 timer animation默认支持 reduced motion。
## 组件 ## 交互合同
- 复用公开 `button`、`checkbox`、`label`、`panel`、`data-grid`。 - 筛选All、In progress、Unread、Finished、Starred。
- companion 组件:`operations-console-shell`、`hero-status`、`metric-strip`、`activity-card`、`capability-card`、`timeline`、`data-page`、`resource-page`。 - 列表DataGrid 行单选,重复点击可以重新选择;分页按钮在边界处 disabled。
- 状态active/inactive nav、light/dark theme、selected/unselected row、Behavior on/off、resource ready/loading/error、boundary handled、compact/fullscreen。 - 详情:`+ 10%`、`✓ Finish`、`☆/★ Star`、`Archive`;完成或 100% 时正确禁用。
- 所有权:`.etaf` 只放静态 composition 和 section 名称;`.el` 持有 token、props、refs、资源、handlers 和 state。 - 存储:成功显示 `✓ Saved locally`,失败保留当前可用内容并提供 Reload。
- 主题Light/Dark 由 checkbox 控制;文字、对比度和布局都应保持可读。
- 键盘Tab 顺序为筛选 → 行 → 详情动作 → 分页RET 使用同一组公开 refs。
## 无障碍 ## 响应式与验证
- 目标:键盘完整、高对比文字,使用公开组件提供语义 role。 - Wide/fullscreen 使用 rail / list / detail 三列 grid。
- Tab/Shift-Tab 遍历 nav/action/data rowsRET 派发 focused ref移除目标时清焦点。 - Compact 时筛选栏收为顶部工具行detail 位于 list 后面;不使用固定宽度撑坏
- 每个状态都有文字反馈,颜色不是唯一信号;不依赖 hover-only 行为。 viewport所有操作控件仍保持独立 hover/focus 语义。
- Ebox Theme 承载继承的 Ebox 默认属性;应用 palette 仍由 companion 查询, - 用 SQLite 临时文件测试 mount/remount、筛选、分页、重复选行、mutation、错误
Renderer 会在进入 Ebox 几何层前过滤未知 token 名。 状态和 cleanupGUI 用干净 fullscreen 单窗口截图验证真实布局。
- 目标:一次 Data mutation 对应一次 Runtime generation/publicationwarm 交互
## 响应式 维持现有 100ms p50 预算。
- 支持 compact 900940px body 和 fullscreen desktop GUI。
- metric 尽量一行展示workspace 从双列退化为上下堆叠,控件保持 intrinsic 单行宽度。
- 需要分配剩余宽度的行使用 `flex`,固定 intrinsic 控件组才使用 `row`;导航
控件组使用 max-content确保每个 hover/active 区域都是独立控件。
- mouse-1 和键盘激活使用同一组公开 refs。
## 交互状态
- LoadingResource 显示 `Loading…`shell 几何不移动。
- EmptyDataGrid 明确显示 empty选择控件仍可用。
- Errorreload 可故意失败一次Boundary 显示 handled error 且仍可恢复。
- Successaction/status 和 timestamp 可见更新。
- Disabled不可用操作暴露 disabled 语义,不静默吞输入。
## 内容语气
- 简洁、操作化、解释性;标签明确指出正在展示的框架能力。
- 统一使用 `Component`、`Context`、`Theme`、`Behavior`、`Action`、`Data`、`Resource`、`Boundary`。
- 优先“动词 + 结果”(如 `Reload resource`、`Handled: …`、`Selected Beacon`)。
## 实现约束
- 使用 `.etaf`/`.el` 的公开 ETAF View DSL由 Ebox 渲染,不引入 HTML/CSS runtime dependency。
- palette/spacing token 在 companion 单处定义并复用,禁止散落临时色值。
- 性能:普通 warm p50 <=100mscounter/resource p50 <=150ms每个 warm max <=250ms每个逻辑动作一次 publication。
- 保留唯一同名 `operations-console` pair、静态安全读取、公开 API boundary、双 mount/unmount cleanup 和 manifest 语义。
- 导航:稳定的 page ref 由 retained router Component 读取;页面 Host 是唯一 semantic Range 锚点,页面切换只替换一个 Range payload不重建 shell root。
- HTML 是视觉参考ETAF 需要 compact/fullscreen 截图和动态键鼠 checkpoint 证明布局、宽度、焦点和状态。
## 待 review ## 待 review
- [ ] 用户可在第一版 HTML/ETAF 对照后调整 palette 或页面命名;行为和公开 ref 覆盖保持不变。 - [ ] 用户可调整产品命名或 palettepair 边界、组合/复用原则和公开交互合同不变。

View File

@ -1,8 +1,8 @@
EMACS ?= emacs EMACS ?= emacs
LOAD_PATH = -L . -L ../etaf -L ../etaf-ui -L ../ebox -L ../ecss -L ../tp LOAD_PATH = -L . -L ../etaf -L ../etaf-ui -L ../ebox -L ../ecss -L ../tp -L ../etaf-sqlite
EXAMPLE_EL := examples/operations-console.el EXAMPLE_EL := examples/research-shelf.el
.PHONY: all compile examples-read test check checkdoc load clean .PHONY: all compile examples-read test perf check checkdoc load clean
all: check all: check
@ -15,29 +15,36 @@ compile:
$(MAKE) -C ../ebox compile EMACS="$(EMACS)" ECSS_DIR=../ecss TP_DIR=../tp $(MAKE) -C ../ebox compile EMACS="$(EMACS)" ECSS_DIR=../ecss TP_DIR=../tp
$(MAKE) -C ../etaf compile EMACS="$(EMACS)" $(MAKE) -C ../etaf compile EMACS="$(EMACS)"
$(MAKE) -C ../etaf-ui compile EMACS="$(EMACS)" $(MAKE) -C ../etaf-ui compile EMACS="$(EMACS)"
$(MAKE) -C ../etaf-sqlite compile EMACS="$(EMACS)"
rm -f *.elc tests/*.elc examples/*.elc rm -f *.elc tests/*.elc examples/*.elc
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \ $(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
--eval '(load-file "etaf-playground.el")' \ --eval '(load-file "etaf-playground.el")' \
--eval '(dolist (file (quote ($(foreach file,$(EXAMPLE_EL),"$(file)")))) (byte-compile-file file))' \ --eval '(dolist (file (quote ($(foreach file,$(EXAMPLE_EL),"$(file)")))) (byte-compile-file file))' \
--eval '(byte-compile-file "etaf-playground-catalog.el")' \
--eval '(byte-compile-file "etaf-playground.el")' \ --eval '(byte-compile-file "etaf-playground.el")' \
--eval '(byte-compile-file "tests/etaf-playground-tests.el")' --eval '(byte-compile-file "tests/etaf-playground-tests.el")'
examples-read: compile examples-read: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \ $(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
--eval '(require (quote etaf-playground))' \ --eval '(require (quote etaf-playground))' \
--eval '(etaf-playground-read-pair "operations-console")' \ --eval '(load-file "examples/research-shelf.el")' \
--eval '(let ((database (make-temp-file "etaf-playground-read-" nil ".sqlite")) (buffer " *etaf-playground-read*")) (unwind-protect (let ((etaf-research-shelf-database-file database)) (etaf-playground-mount-example buffer "research-shelf")) (etaf-playground-close buffer) (when (file-exists-p database) (delete-file database))))' \
--eval '(princ "ETAF example modules load OK\\n")' --eval '(princ "ETAF example modules load OK\\n")'
test: examples-read test: examples-read
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \ $(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
-l tests/etaf-playground-tests.el -f ert-run-tests-batch-and-exit -l tests/etaf-playground-tests.el -f ert-run-tests-batch-and-exit
perf: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
-l scripts/benchmark-research-shelf.el
load: compile load: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(require (quote etaf-playground))' \ $(EMACS) -Q --batch $(LOAD_PATH) --eval '(require (quote etaf-playground))' \
--eval '(princ "etaf-playground load OK\\n")' --eval '(princ "etaf-playground load OK\\n")'
checkdoc: checkdoc:
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (file (list "etaf-playground.el" "tests/etaf-playground-tests.el" "examples/operations-console.el")) (checkdoc-file file)))' $(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (file (list "etaf-playground.el" "etaf-playground-catalog.el" "tests/etaf-playground-tests.el" "examples/research-shelf.el" "scripts/benchmark-research-shelf.el")) (checkdoc-file file)))'
check: checkdoc test check: checkdoc test

View File

@ -1,11 +1,16 @@
# ETAF Playground # ETAF Playground
The Playground exposes one canonical product example: the The Playground exposes one canonical product example: the
`examples/operations-console.etaf` and `examples/operations-console.el` pair. `examples/research-shelf.etaf` and `examples/research-shelf.el` pair.
The `.etaf` file is inert, validated static composition data. The companion The `.etaf` file is inert, validated structural composition data. The same-
owns Components, state, Context, Theme, Behaviors, Data, Resources, Actions, basename companion owns Components, state, theme, storage, events, and
and lifecycle. `etaf-playground-open`, `etaf-playground-reset`, and lifecycle. `etaf-playground.el` is a reusable pair framework: it owns only
`etaf-playground-close` are the only mounting boundary. registration, safe loading, mounting, reset, close, and verification entry
points. `etaf-playground-catalog.el` is the replaceable deployment catalog;
business Components, database schemas, palettes, refs, and handlers
must stay in the companion. Compose and reuse public ETAF/`etaf-ui` contracts
before adding abstractions.
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`. Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`.
Run `make perf` for the 1413×62 warm row-selection/theme latency gate.

View File

@ -1,10 +1,14 @@
# ETAF Playground # ETAF Playground
Playground 只公开一个标准产品示例: Playground 只公开一个标准产品示例:
`examples/operations-console.etaf` 与 `examples/operations-console.el` 配对。 `examples/research-shelf.etaf` 与 `examples/research-shelf.el` 配对。
`.etaf` 是只读、经过白名单验证的静态组合数据;同名 companion 负责 `.etaf` 是惰性、inert、经过白名单验证的静态结构数据同名 companion 负责
Component、状态、Context、Theme、Behavior、Data、Resource、Action 与生命周期。 Component、状态、主题、SQLite、事件与生命周期。`etaf-playground.el` 是通用
`etaf-playground-open/reset/close` 是唯一挂载边界。 pair playground 框架只负责注册、白名单读取、加载、mount/reset/close 和验证
入口;`etaf-playground-catalog.el` 是可替换的部署目录。业务 Component、数据库
schema、palette、refs 与 handlers 不能泄漏进框架。
默认原则是组合和复用公开 ETAF/`etaf-ui` 契约,而不是再造一套组件或 helper 抽象。
验证命令:`make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`。 验证命令:`make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`。
性能门禁:`make perf`1413×62 viewport 的 warm 选行/主题延迟)。

View File

@ -1,211 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ETAF Operations Console — visual prototype</title>
<style>
:root {
--ink: #142235;
--ink-soft: #526174;
--paper: #f6f1e8;
--paper-strong: #fffdf8;
--line: #c8c1b6;
--teal: #2e8b83;
--teal-soft: #d9eeea;
--coral: #e26d5a;
--coral-soft: #f9ded7;
--amber: #d99a3d;
--amber-soft: #f8e9c9;
--success: #3e9b72;
--shadow: 0 12px 28px rgba(20, 34, 53, .09);
--radius: 14px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--ink);
background: var(--paper);
}
:root[data-theme="dark"] {
--ink: #edf2f4;
--ink-soft: #aab8c2;
--paper: #111923;
--paper-strong: #182433;
--line: #344455;
--teal: #61c6ba;
--teal-soft: #173b3b;
--coral: #ff957f;
--coral-soft: #4b2c31;
--amber: #efc06c;
--amber-soft: #4a3922;
--success: #73d29d;
--shadow: 0 14px 32px rgba(0, 0, 0, .25);
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: var(--paper); }
button { font: inherit; }
.app-shell {
width: min(1240px, calc(100% - 40px));
min-height: 100vh;
margin: 0 auto;
padding: 22px 0 30px;
display: grid;
grid-template-columns: 210px minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
gap: 18px 24px;
}
.topbar { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark { width: 38px; height: 38px; border-radius: 12px; display: grid; place-items: center; color: #fff; background: var(--ink); font-weight: 800; letter-spacing: -.08em; }
.brand small, .eyebrow, .mono { font-family: "SFMono-Regular", Consolas, monospace; letter-spacing: .08em; text-transform: uppercase; }
.brand small { color: var(--ink-soft); font-size: 10px; }
.brand strong { display: block; font-size: 16px; letter-spacing: -.02em; }
.top-actions { display: flex; align-items: center; gap: 10px; }
.status-pill { display: inline-flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 999px; background: var(--paper-strong); font-size: 12px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 18%, transparent); }
.icon-button, .nav-button, .primary-button, .quiet-button { border: 1px solid var(--line); border-radius: 10px; cursor: pointer; color: var(--ink); background: var(--paper-strong); }
.icon-button { width: 36px; height: 36px; }
.primary-button { padding: 10px 14px; color: #fff; border-color: var(--ink); background: var(--ink); }
.quiet-button { padding: 9px 12px; }
button:focus-visible { outline: 3px solid color-mix(in srgb, var(--teal) 60%, transparent); outline-offset: 2px; }
.sidebar { grid-row: 2; display: flex; flex-direction: column; gap: 18px; }
.sidebar-heading { margin: 0 0 4px; color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; text-transform: uppercase; letter-spacing: .14em; }
.nav { display: grid; gap: 6px; }
.nav-button { padding: 11px 12px; text-align: left; display: flex; justify-content: space-between; align-items: center; }
.nav-button[aria-current="page"] { color: #fff; border-color: var(--ink); background: var(--ink); }
.nav-kicker { font: 10px "SFMono-Regular", Consolas, monospace; opacity: .7; }
.side-card { margin-top: auto; padding: 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.side-card strong { display: block; margin-bottom: 6px; }
.side-card p { margin: 0; color: var(--ink-soft); font-size: 12px; line-height: 1.45; }
.main { grid-row: 2; min-width: 0; }
.page { display: none; gap: 18px; }
.page.active { display: grid; }
.hero { min-height: 190px; padding: 24px; display: flex; align-items: flex-end; justify-content: space-between; gap: 22px; color: #fff; border-radius: var(--radius); background: linear-gradient(135deg, #17283d 0%, #1b4f5c 60%, #2e8b83 100%); box-shadow: var(--shadow); overflow: hidden; position: relative; }
.hero::after { content: ""; position: absolute; width: 300px; height: 300px; right: -80px; top: -140px; border: 1px solid rgba(255,255,255,.22); border-radius: 50%; box-shadow: 0 0 0 28px rgba(255,255,255,.05), 0 0 0 58px rgba(255,255,255,.04); }
.hero-copy, .hero-actions { position: relative; z-index: 1; }
.eyebrow { margin: 0 0 10px; font-size: 10px; opacity: .72; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 620px; margin-bottom: 10px; font-size: clamp(24px, 4vw, 40px); line-height: 1.04; letter-spacing: -.05em; }
.hero p { max-width: 570px; margin-bottom: 0; color: rgba(255,255,255,.78); line-height: 1.5; }
.hero-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.hero .primary-button { color: var(--ink); border-color: #fff; background: #fff; }
.hero .quiet-button { color: #fff; border-color: rgba(255,255,255,.35); background: rgba(255,255,255,.08); }
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.metric { padding: 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.metric-label { color: var(--ink-soft); font-size: 12px; }
.metric-value { margin: 8px 0 4px; font: 700 27px "SFMono-Regular", Consolas, monospace; letter-spacing: -.08em; }
.metric-delta { color: var(--success); font-size: 12px; }
.workspace { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, .75fr); gap: 14px; }
.card { min-width: 0; padding: 18px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 15px; }
.card-header h2, .card-header h3 { margin: 0; font-size: 15px; letter-spacing: -.02em; }
.card-header p { margin: 4px 0 0; color: var(--ink-soft); font-size: 12px; }
.activity-list, .timeline { display: grid; gap: 10px; }
.activity-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent); }
.activity-row:last-child { border-bottom: 0; }
.activity-icon { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; color: var(--teal); background: var(--teal-soft); }
.activity-row strong { display: block; font-size: 12px; }
.activity-row small { color: var(--ink-soft); font-size: 11px; }
.activity-time { color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.capabilities { display: grid; gap: 8px; }
.capability { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 9px; align-items: start; padding: 10px; border-radius: 10px; background: color-mix(in srgb, var(--paper) 70%, var(--paper-strong)); }
.capability-mark { color: var(--success); font-weight: 800; }
.capability strong { display: block; font-size: 12px; }
.capability span { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 11px; line-height: 1.35; }
.timeline-card { padding-bottom: 12px; }
.timeline-item { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; gap: 10px; align-items: start; }
.timeline-line { position: relative; display: grid; justify-items: center; }
.timeline-line::before { content: ""; width: 1px; height: 100%; background: var(--line); position: absolute; top: 10px; }
.timeline-dot { position: relative; width: 9px; height: 9px; margin-top: 4px; border-radius: 50%; background: var(--coral); }
.timeline-item strong { font-size: 12px; }
.timeline-item p { margin: 3px 0 0; color: var(--ink-soft); font-size: 11px; }
.timeline-item time { color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.data-layout, .resource-layout { display: grid; gap: 14px; }
.data-toolbar { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: center; }
.data-toolbar strong { font-size: 14px; }
.data-toolbar span { color: var(--ink-soft); font-size: 12px; }
.table { overflow: hidden; border: 1px solid var(--line); border-radius: 10px; }
.table-row { display: grid; grid-template-columns: 70px minmax(0, 1fr) 130px 100px; gap: 10px; align-items: center; padding: 11px 13px; border-bottom: 1px solid var(--line); font-size: 12px; }
.table-row:last-child { border-bottom: 0; }
.table-head { color: var(--ink-soft); background: color-mix(in srgb, var(--paper) 65%, var(--paper-strong)); font: 10px "SFMono-Regular", Consolas, monospace; text-transform: uppercase; letter-spacing: .08em; }
.table-row[data-selected="true"] { color: var(--teal); background: var(--teal-soft); }
.row-button { all: unset; cursor: pointer; display: contents; }
.tag { display: inline-flex; width: max-content; padding: 4px 7px; border-radius: 999px; color: var(--teal); background: var(--teal-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.resource-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 14px; }
.resource-status { display: flex; align-items: center; gap: 10px; padding: 13px; border-radius: 10px; background: var(--teal-soft); }
.resource-status strong { font-size: 13px; }
.resource-status small { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 11px; }
.resource-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.boundary { padding: 14px; border: 1px dashed var(--amber); border-radius: 10px; background: var(--amber-soft); }
.boundary strong { display: block; margin-bottom: 4px; font-size: 12px; }
.boundary p { margin: 0; color: var(--ink-soft); font-size: 12px; line-height: 1.45; }
.footer { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; padding-top: 14px; border-top: 1px solid var(--line); color: var(--ink-soft); font-size: 11px; }
.footer strong { color: var(--ink); }
.toast { position: fixed; right: 22px; bottom: 22px; max-width: 340px; padding: 12px 14px; color: #fff; border-radius: 10px; background: var(--ink); box-shadow: var(--shadow); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .15s ease, transform .15s ease; font-size: 12px; }
.toast.show { opacity: 1; transform: translateY(0); }
@media (max-width: 920px) {
.app-shell { width: min(100% - 24px, 760px); grid-template-columns: 1fr; gap: 14px; }
.sidebar { grid-row: auto; display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 10px; }
.sidebar-heading { display: none; }
.nav { grid-template-columns: repeat(3, 1fr); }
.nav-button { padding: 9px; }
.side-card { display: none; }
.main { grid-row: auto; }
.metrics { grid-template-columns: repeat(2, 1fr); }
.workspace, .resource-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.app-shell { width: calc(100% - 18px); padding-top: 12px; }
.topbar { align-items: flex-start; }
.status-pill { display: none; }
.hero { padding: 18px; display: block; }
.hero-actions { margin-top: 18px; }
.table-row { grid-template-columns: 48px minmax(0, 1fr) 80px; }
.table-row > :last-child { display: none; }
.footer { display: block; line-height: 1.6; }
}
</style>
</head>
<body>
<div class="app-shell" id="app">
<header class="topbar">
<div class="brand"><div class="brand-mark">OC</div><div><small>ETAF / reference workspace</small><strong>Operations Console</strong></div></div>
<div class="top-actions"><span class="status-pill"><i class="status-dot"></i><span id="system-status">All systems nominal</span></span><button class="icon-button" id="theme-toggle" aria-label="Toggle theme"></button></div>
</header>
<aside class="sidebar">
<div><p class="sidebar-heading">Workspace</p><nav class="nav" aria-label="Primary navigation">
<button class="nav-button" data-page="overview" aria-current="page"><span>Overview</span><span class="nav-kicker">01</span></button>
<button class="nav-button" data-page="data"><span>Data desk</span><span class="nav-kicker">02</span></button>
<button class="nav-button" data-page="resource"><span>Resource lab</span><span class="nav-kicker">03</span></button>
</nav></div>
<div class="side-card"><strong>Owned surface</strong><p>One stable shell, several independently reactive panels, and a status line that never loses the source of truth.</p></div>
</aside>
<main class="main">
<section class="page active" id="page-overview" data-page-view="overview">
<div class="hero"><div class="hero-copy"><p class="eyebrow">Tuesday / 09:42 / live review</p><h1>Make complex state feel calm.</h1><p>A single surface for components, context, behavior, data, and recovery — designed to make every ETAF update visible and explainable.</p></div><div class="hero-actions"><button class="primary-button" id="hero-action">Run health check</button><button class="quiet-button" id="hero-note">View event trail</button></div></div>
<div class="metrics" aria-label="Key metrics"><article class="metric"><span class="metric-label">Throughput</span><div class="metric-value" id="metric-throughput">2.4k</div><span class="metric-delta">↑ 18% this week</span></article><article class="metric"><span class="metric-label">Active flows</span><div class="metric-value" id="metric-flows">18</div><span class="metric-delta">+3 since 08:00</span></article><article class="metric"><span class="metric-label">Response</span><div class="metric-value">42ms</div><span class="metric-delta">p95 within target</span></article><article class="metric"><span class="metric-label">Reliability</span><div class="metric-value">99.8%</div><span class="metric-delta">steady</span></article></div>
<div class="workspace"><article class="card"><div class="card-header"><div><h2>Activity stream</h2><p>Reactive events, rendered as a readable story.</p></div><span class="tag" id="activity-count">4 events</span></div><div class="activity-list" id="activity-list"><div class="activity-row"><span class="activity-icon"></span><div><strong>Theme provider promoted</strong><small>Context defaults were refreshed without losing focus.</small></div><span class="activity-time">09:41</span></div><div class="activity-row"><span class="activity-icon"></span><div><strong>Counter alpha crossed 2k</strong><small>One Component-owned update, one visible publication.</small></div><span class="activity-time">09:36</span></div><div class="activity-row"><span class="activity-icon"></span><div><strong>Resource boundary recovered</strong><small>Handled failure left the shell and routes intact.</small></div><span class="activity-time">09:28</span></div></div></article><article class="card"><div class="card-header"><div><h2>Capability map</h2><p>What this surface is proving.</p></div><span class="tag">ETAF</span></div><div class="capabilities"><div class="capability"><b class="capability-mark"></b><div><strong>Component ownership</strong><span>Props, slots, setup state, and retained identity.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Context + Theme</strong><span>Provider defaults, dark mode, and explicit feedback.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Behavior + Actions</strong><span>Keyboard-ready interactions with stable refs.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Data + Resource</strong><span>Selection, loading, failure, boundary, and cleanup.</span></div></div></div></article></div>
<article class="card timeline-card"><div class="card-header"><div><h2>Event trail</h2><p>Every meaningful action leaves a compact, inspectable trace.</p></div><button class="quiet-button" id="clear-trail">Clear trail</button></div><div class="timeline" id="timeline"><div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Surface mounted</strong><p>Operations Console is ready for review.</p></div><time>09:22</time></div><div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Data controller loaded</strong><p>3 records are available in the Data desk.</p></div><time>09:23</time></div></div></article>
</section>
<section class="page" id="page-data" data-page-view="data"><article class="card data-layout"><div class="data-toolbar"><div><strong>Data desk</strong><span> / a small table with real selection state</span></div><span class="tag" id="selection-status">No row selected</span></div><div class="table" role="grid"><div class="table-row table-head"><span>ID</span><span>NAME</span><span>STATUS</span><span>OWNER</span></div><button class="row-button" data-row="1"><div class="table-row" data-selected="false"><span>01</span><strong>Atlas</strong><span><i class="tag">active</i></span><span>Core</span></div></button><button class="row-button" data-row="2"><div class="table-row" data-selected="false"><span>02</span><strong>Beacon</strong><span><i class="tag">review</i></span><span>Ops</span></div></button><button class="row-button" data-row="3"><div class="table-row" data-selected="false"><span>03</span><strong>Cirrus</strong><span><i class="tag">done</i></span><span>Data</span></div></button></div><div class="resource-status"><i class="status-dot"></i><div><strong id="data-detail-title">Select a row to inspect it.</strong><small id="data-detail-copy">Selection updates the status without rebuilding the shell.</small></div></div></article></section>
<section class="page" id="page-resource" data-page-view="resource"><div class="resource-grid"><article class="card"><div class="card-header"><div><h2>Resource lab</h2><p>Loading, failure, recovery, and cleanup are first-class states.</p></div><span class="tag" id="resource-tag">ready</span></div><div class="resource-status"><i class="status-dot" id="resource-dot"></i><div><strong id="resource-title">Loaded value 7</strong><small id="resource-copy">Owned by the current application mount.</small></div></div><div class="resource-actions"><button class="primary-button" id="resource-reload">Reload resource</button><button class="quiet-button" id="resource-fail">Fail next load</button><label class="status-pill"><input type="checkbox" id="resource-check"> fail next</label></div></article><article class="card"><div class="card-header"><div><h2>Error boundary</h2><p>Failure is handled in place.</p></div><span class="tag">safe</span></div><div class="boundary"><strong id="boundary-title">No boundary run yet</strong><p id="boundary-copy">Run the boundary to see a handled error while the rest of the application stays mounted.</p></div><div class="resource-actions"><button class="quiet-button" id="run-boundary">Run handled boundary</button></div></article></div><article class="card"><div class="card-header"><div><h2>Lifecycle ledger</h2><p>Resource cleanup is visible, not inferred.</p></div><span class="metric-value" id="cleanup-count">0</span></div><p class="mono">cleanup calls after mount / reload / unmount</p></article></section>
</main>
<footer class="footer"><span><strong id="footer-status">Ready for review</strong> · state is owned by the current generation</span><span class="mono">operations-console / v0.1</span></footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const root = document.documentElement;
const toast = document.getElementById('toast');
const showToast = message => { toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 2200); };
document.getElementById('theme-toggle').addEventListener('click', () => { root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; showToast(root.dataset.theme === 'dark' ? 'Dark theme enabled' : 'Light theme enabled'); });
document.querySelectorAll('[data-page]').forEach(button => button.addEventListener('click', () => { const page = button.dataset.page; document.querySelectorAll('[data-page]').forEach(item => item.setAttribute('aria-current', item.dataset.page === page ? 'page' : 'false')); document.querySelectorAll('[data-page-view]').forEach(view => view.classList.toggle('active', view.dataset.pageView === page)); document.getElementById('footer-status').textContent = `Viewing ${page === 'data' ? 'Data desk' : page === 'resource' ? 'Resource lab' : 'Overview'}`; }));
document.getElementById('hero-action').addEventListener('click', () => { document.getElementById('system-status').textContent = 'Health check complete'; document.getElementById('footer-status').textContent = 'Health check completed'; showToast('All owned surfaces responded'); });
document.getElementById('hero-note').addEventListener('click', () => document.getElementById('timeline').scrollIntoView({ behavior: 'smooth', block: 'center' }));
document.querySelectorAll('[data-row]').forEach(button => button.addEventListener('click', () => { const row = button.querySelector('.table-row'); document.querySelectorAll('.table-row[data-selected]').forEach(item => item.dataset.selected = 'false'); row.dataset.selected = 'true'; const name = row.querySelector('strong').textContent; document.getElementById('selection-status').textContent = `Selected ${name}`; document.getElementById('data-detail-title').textContent = `${name} is selected`; document.getElementById('data-detail-copy').textContent = 'The Data Controller and DataGrid share one stable row ref.'; showToast(`Selected ${name}`); }));
document.getElementById('clear-trail').addEventListener('click', () => { document.getElementById('timeline').innerHTML = '<div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Trail cleared</strong><p>New actions will append here.</p></div><time>now</time></div>'; showToast('Event trail cleared'); });
let resourceValue = 7; let shouldFail = false; let cleanups = 0;
document.getElementById('resource-check').addEventListener('change', event => { shouldFail = event.target.checked; showToast(shouldFail ? 'Next resource load will fail' : 'Failure flag cleared'); });
document.getElementById('resource-fail').addEventListener('click', () => { shouldFail = true; document.getElementById('resource-check').checked = true; showToast('Next resource load will fail'); });
document.getElementById('resource-reload').addEventListener('click', () => { if (shouldFail) { shouldFail = false; document.getElementById('resource-check').checked = false; document.getElementById('resource-tag').textContent = 'error'; document.getElementById('resource-title').textContent = 'Handled load failure'; document.getElementById('resource-copy').textContent = 'Retry remains available; the shell stayed mounted.'; showToast('Resource error handled'); return; } resourceValue += 1; document.getElementById('resource-tag').textContent = 'ready'; document.getElementById('resource-title').textContent = `Loaded value ${resourceValue}`; document.getElementById('resource-copy').textContent = 'Fresh value promoted through the owned resource.'; showToast('Resource reloaded'); });
document.getElementById('run-boundary').addEventListener('click', () => { document.getElementById('boundary-title').textContent = 'Handled boundary result'; document.getElementById('boundary-copy').textContent = 'The failure was caught and rendered here; navigation and state remain available.'; showToast('Boundary handled safely'); });
</script>
</body>
</html>

138
design/research-shelf.html Normal file
View File

@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Research Shelf</title>
<style>
:root {
--ink: #172033;
--muted: #6d7482;
--paper: #f7f3ea;
--panel: #fffdf8;
--line: #d9d4c9;
--cobalt: #3657d6;
--mint: #3e9b8f;
--coral: #d86b5d;
--amber: #c58a3a;
--shadow: 0 18px 48px rgba(23, 32, 51, .09);
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: var(--paper); }
.shell { max-width: 1440px; margin: 0 auto; padding: 28px 34px 34px; }
.topbar { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 28px; }
.eyebrow { color: var(--cobalt); font: 700 11px/1.2 ui-monospace, monospace; letter-spacing: .16em; text-transform: uppercase; }
h1 { margin: 7px 0 5px; font-size: clamp(28px, 4vw, 48px); letter-spacing: -.05em; line-height: .98; }
.subtitle { margin: 0; color: var(--muted); max-width: 590px; font-size: 14px; }
.sync { display: flex; align-items: center; gap: 9px; color: var(--mint); font: 700 12px ui-monospace, monospace; white-space: nowrap; }
.sync i { width: 9px; height: 9px; border-radius: 50%; background: var(--mint); }
.workspace { display: grid; grid-template-columns: 190px minmax(420px, 1.45fr) minmax(280px, .85fr); gap: 18px; align-items: start; }
.rail, .list-card, .detail-card { border: 1px solid var(--line); background: var(--panel); box-shadow: var(--shadow); }
.rail { padding: 18px 12px; }
.rail h2, .card-head h2 { margin: 0; font-size: 14px; letter-spacing: -.01em; }
.rail-label { margin: 20px 10px 8px; color: var(--muted); font: 700 10px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .12em; }
.filter { width: 100%; display: flex; justify-content: space-between; border: 0; background: transparent; color: var(--ink); padding: 10px; border-radius: 8px; text-align: left; font: inherit; cursor: pointer; }
.filter:hover, .filter.active { color: var(--cobalt); background: #eef1ff; }
.filter span { color: var(--muted); font: 12px ui-monospace, monospace; }
.list-card, .detail-card { overflow: hidden; }
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 18px 20px; border-bottom: 1px solid var(--line); }
.result-count { color: var(--muted); font: 12px ui-monospace, monospace; }
.table-head, .book-row { display: grid; grid-template-columns: minmax(0, 1.6fr) 100px 96px 72px; gap: 12px; align-items: center; }
.table-head { padding: 11px 20px; color: var(--muted); background: #f1eee7; font: 700 10px ui-monospace, monospace; letter-spacing: .08em; text-transform: uppercase; }
.book-row { width: 100%; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: var(--ink); padding: 14px 20px; text-align: left; cursor: pointer; }
.book-row:hover { background: #f7f8ff; }
.book-row.active { box-shadow: inset 4px 0 0 var(--cobalt); background: #eef1ff; }
.book-title { font-weight: 750; }
.book-author, .book-kind { margin-top: 4px; color: var(--muted); font-size: 12px; }
.status { display: inline-flex; width: fit-content; padding: 4px 7px; border-radius: 99px; background: #e7f4ef; color: #267567; font: 700 10px ui-monospace, monospace; }
.status.progress { background: #fff2dc; color: #9a6819; }
.progress { height: 6px; border-radius: 99px; background: #e9e6df; overflow: hidden; }
.progress i { display: block; height: 100%; background: var(--cobalt); border-radius: inherit; }
.detail-card { padding: 22px; }
.detail-kicker { color: var(--cobalt); font: 700 10px ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; }
.detail-card h2 { margin: 8px 0 4px; font-size: 25px; letter-spacing: -.04em; }
.detail-author { color: var(--muted); }
.detail-rule { height: 1px; margin: 20px 0; background: var(--line); }
.meta { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.meta small { display: block; color: var(--muted); font: 10px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .1em; }
.meta strong { display: block; margin-top: 4px; }
.note { padding: 12px; background: #f8f5ed; border-left: 3px solid var(--amber); color: #4b4e57; font-size: 13px; line-height: 1.5; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
.actions button { border: 1px solid var(--cobalt); border-radius: 7px; padding: 9px 12px; background: var(--cobalt); color: white; font-weight: 700; cursor: pointer; }
.actions button.secondary { background: transparent; color: var(--cobalt); }
.footer { display: flex; justify-content: space-between; gap: 14px; margin-top: 16px; color: var(--muted); font: 11px ui-monospace, monospace; }
@media (max-width: 980px) {
.shell { padding: 20px; }
.workspace { grid-template-columns: 160px minmax(0, 1fr); }
.detail-card { grid-column: 1 / -1; }
}
@media (max-width: 680px) {
.topbar { display: block; }
.sync { margin-top: 18px; }
.workspace { display: block; }
.rail { display: flex; gap: 5px; overflow: auto; margin-bottom: 14px; }
.rail h2, .rail-label { display: none; }
.filter { width: auto; white-space: nowrap; }
.list-card { margin-bottom: 14px; }
.table-head { display: none; }
.book-row { grid-template-columns: 1fr auto; gap: 8px; }
.book-row > :nth-child(n+2) { grid-column: 2; }
.book-row > :nth-child(3), .book-row > :nth-child(4) { display: none; }
.footer { display: block; line-height: 1.8; }
}
</style>
</head>
<body>
<main class="shell">
<header class="topbar">
<div>
<div class="eyebrow">A quiet place for unfinished ideas</div>
<h1>Research Shelf</h1>
<p class="subtitle">Keep the next paper, book, and note close enough to become part of the work.</p>
</div>
<div class="sync"><i></i> SQLite · saved locally</div>
</header>
<section class="workspace">
<aside class="rail">
<h2>Library</h2>
<div class="rail-label">Views</div>
<button class="filter active">All <span>12</span></button>
<button class="filter">In progress <span>4</span></button>
<button class="filter">Unread <span>5</span></button>
<button class="filter">Finished <span>3</span></button>
<div class="rail-label">Collections</div>
<button class="filter">★ Starred <span>6</span></button>
<button class="filter">⌕ Essays <span>7</span></button>
<button class="filter">↗ Papers <span>5</span></button>
</aside>
<section class="list-card">
<div class="card-head"><h2>Reading queue</h2><span class="result-count">12 records · page 1</span></div>
<div class="table-head"><span>Title</span><span>Status</span><span>Progress</span><span>Kind</span></div>
<button class="book-row active"><span><b class="book-title">The Shape of Tools</b><small class="book-author">M. Abramson</small></span><span class="status progress">In progress</span><span><div class="progress"><i style="width: 64%"></i></div></span><small class="book-kind">Essay</small></button>
<button class="book-row"><span><b class="book-title">Ways of Seeing</b><small class="book-author">John Berger</small></span><span class="status">Unread</span><span><div class="progress"><i style="width: 0%"></i></div></span><small class="book-kind">Book</small></button>
<button class="book-row"><span><b class="book-title">The Cathedral and the Bazaar</b><small class="book-author">Eric S. Raymond</small></span><span class="status">Finished</span><span><div class="progress"><i style="width: 100%"></i></div></span><small class="book-kind">Paper</small></button>
<button class="book-row"><span><b class="book-title">Designing for Calm</b><small class="book-author">A. Ito</small></span><span class="status progress">In progress</span><span><div class="progress"><i style="width: 32%"></i></div></span><small class="book-kind">Notes</small></button>
<div class="card-head"><span class="result-count">Showing 14 of 12</span><div class="actions"><button class="secondary"></button><button class="secondary"></button></div></div>
</section>
<aside class="detail-card">
<div class="detail-kicker">Selected item · 01</div>
<h2>The Shape of Tools</h2>
<div class="detail-author">M. Abramson · Essay · 2025</div>
<div class="detail-rule"></div>
<div class="meta"><div><small>Status</small><strong>◷ In progress</strong></div><div><small>Priority</small><strong>High</strong></div><div><small>Progress</small><strong>64%</strong></div><div><small>Updated</small><strong>Today</strong></div></div>
<div class="progress" style="margin-top: 18px"><i style="width: 64%"></i></div>
<div class="detail-rule"></div>
<div class="note">“The best tools make attention feel larger, not more fragmented.”</div>
<div class="actions"><button>+ 10%</button><button>✓ Finish</button><button class="secondary">★ Star</button><button class="secondary">Archive</button></div>
</aside>
</section>
<footer class="footer"><span>⌂ ~/.emacs.d/research-shelf.sqlite</span><span>✓ Saved locally · 12 items · last sync just now</span></footer>
</main>
</body>
</html>

View File

@ -0,0 +1,30 @@
;;; etaf-playground-catalog.el --- Reviewed Playground pair catalog -*- lexical-binding: t; -*-
;;; Commentary:
;; Deployment configuration lives outside the generic loader. Adding or
;; replacing an application changes this catalog and its pair, not framework
;; loading, validation, mounting, or lifecycle semantics.
;;; Code:
(defconst etaf-playground-catalog
'((:pair "research-shelf"
:root-component etaf-research-shelf-root
:companion-feature etaf-research-shelf
:static-tags (research-shelf-shell header filters filter main library
detail footer)
:category "SQLite-backed research library"
:capabilities (view component reactivity context theme events data sqlite
pagination mutation lifecycle etaf-ui)
:refs (research-shelf-filter-all research-shelf-filter-reading
research-shelf-filter-unread research-shelf-filter-finished
research-shelf-filter-starred research-shelf-theme-toggle
research-shelf-reload research-shelf-add research-shelf-progress
research-shelf-finish research-shelf-star research-shelf-archive
research-shelf-page-previous research-shelf-page-next)
:gui-checkpoints (compact fullscreen all reading unread detail theme)
:performance (:warm-runs 8 :publication-per-event 1)))
"Reviewed same-basename pair specifications available to the Playground.")
(provide 'etaf-playground-catalog)
;;; etaf-playground-catalog.el ends here

View File

@ -1,97 +1,125 @@
;;; etaf-playground.el --- ETAF operations-console application pair -*- lexical-binding: t; -*- ;;; etaf-playground.el --- Generic ETAF pair playground framework -*- lexical-binding: t; -*-
;;; Commentary: ;;; Commentary:
;; One reviewed pair: inert `.etaf' data plus an executable companion. ;; The Playground owns only pair registration, inert `.etaf' validation,
;; companion loading, mounting, and cleanup. Applications belong in their
;; same-basename companion modules and never leak into this framework.
;;; Code: ;;; Code:
(require 'cl-lib) (require 'cl-lib)
(require 'etaf) (require 'etaf)
(require 'etaf-ui) (require 'etaf-playground-catalog)
(defconst etaf-playground-buffer-name "*ETAF Playground*") ;; `read' consults this dynamically scoped safety switch. Declare it here
;; so the lexical-binding compiler does not mistake the protected binding for
;; an unused lexical local.
(defvar read-eval)
(defconst etaf-playground-buffer-name "*ETAF Playground*"
"Default buffer used by the generic Playground launcher.")
(defconst etaf-playground-example-directory (defconst etaf-playground-example-directory
(expand-file-name "examples" (expand-file-name "examples"
(file-name-directory (or load-file-name buffer-file-name)))) (file-name-directory (or load-file-name buffer-file-name)))
(defconst etaf-playground-scenario-manifest "Directory containing same-basename `.etaf' and `.el' pairs.")
'((:pair "operations-console" :root-component etaf-operations-console-root (defvar etaf-playground-default-pair
:category "Complete application" (plist-get (car etaf-playground-catalog) :pair)
:capabilities (view component reactivity context theme behavior events "Pair opened by `etaf-playground-open'.")
actions data resource lifecycle etaf-ui) (defvar etaf-playground-scenario-manifest
:refs (operations-console-alpha-increment operations-console-alpha-reset (copy-tree etaf-playground-catalog)
operations-console-beta-increment operations-console-beta-reset "Registered Playground pair specifications.
operations-console-nav-overview operations-console-nav-data
operations-console-nav-resource operations-console-theme-toggle Each entry declares the pair name, companion feature, root factory, inert
operations-console-local-callback operations-console-named-action static tags, public refs, and review metadata. The framework does not inspect
operations-console-behavior-toggle operations-console-clear-selection or special-case the application's business model.")
operations-console-row-1 operations-console-row-2 operations-console-row-3 (defvar etaf-playground-example-names
operations-console-fail-next (mapcar (lambda (entry) (plist-get entry :pair))
operations-console-resource-reload operations-console-error-boundary) etaf-playground-scenario-manifest)
:gui-checkpoints (compact fullscreen overview data resource theme focus) "Names of registered Playground pairs.")
:performance (:warm-runs 10 :publication-per-event 1)))
"The sole active Playground pair and its public review metadata.")
(defconst etaf-playground-example-names '("operations-console"))
(defvar-local etaf-playground-current-example nil) (defvar-local etaf-playground-current-example nil)
(defconst etaf-playground--static-tags
'(operations-console-shell header brand summary theme-control navigation tab (defun etaf-playground-register-pair (entry)
main overview hero metric-strip metric workspace activity-panel "Register or replace a generic Playground pair ENTRY.
capability-panel timeline data resource status
column row text spacer panel label viewport-height stretch)) ENTRY must contain a string `:pair', symbol `:root-component', symbol
`:companion-feature', and a proper-list `:static-tags'."
(let ((name (plist-get entry :pair)))
(unless (and (stringp name) (symbolp (plist-get entry :root-component))
(symbolp (plist-get entry :companion-feature))
(proper-list-p (plist-get entry :static-tags)))
(error "Invalid Playground pair manifest entry: %S" entry))
(setq etaf-playground-scenario-manifest
(cons entry
(cl-remove name etaf-playground-scenario-manifest
:key (lambda (item) (plist-get item :pair))
:test #'equal)))
(setq etaf-playground-example-names
(mapcar (lambda (item) (plist-get item :pair))
etaf-playground-scenario-manifest))
entry))
(defun etaf-playground-scenario (name) (defun etaf-playground-scenario (name)
"Return the sole manifest entry named NAME." "Return the registered pair specification named NAME."
(or (and (equal name "operations-console") (or (cl-find name etaf-playground-scenario-manifest
(car etaf-playground-scenario-manifest)) :key (lambda (entry) (plist-get entry :pair))
:test #'equal)
(user-error "Unknown ETAF Playground pair: %s" name))) (user-error "Unknown ETAF Playground pair: %s" name)))
(defun etaf-playground--pair-file (name suffix) (defun etaf-playground--pair-file (name suffix)
"Return NAME pair path ending in SUFFIX." "Return NAME pair path ending in SUFFIX."
(expand-file-name (concat name suffix) etaf-playground-example-directory)) (expand-file-name (concat name suffix) etaf-playground-example-directory))
(defun etaf-playground--validate-static-node (node) (defun etaf-playground--validate-static-node (node tags)
"Validate inert static View NODE and return it." "Validate inert static View NODE against allowlisted TAGS."
(cond (cond
((or (null node) (stringp node) (numberp node) (keywordp node) ((or (null node) (stringp node) (numberp node) (keywordp node)
(memq node '(t stretch viewport-height))) node) (memq node '(t stretch viewport-height))) node)
;; Symbols in a static pair are inert enum/data values. They are not
;; evaluated because only proper-list nodes become View forms; reject
;; private runtime names while keeping the manifest format composable.
((symbolp node)
(unless (or (string-prefix-p "etaf--" (symbol-name node))
(string-prefix-p "ebox--" (symbol-name node)))
node))
((consp node) ((consp node)
(unless (and (proper-list-p node) (symbolp (car node)) (unless (and (proper-list-p node) (symbolp (car node))
(memq (car node) etaf-playground--static-tags) (memq (car node) tags)
(not (string-prefix-p "etaf--" (symbol-name (car node)))) (not (string-prefix-p "etaf--" (symbol-name (car node))))
(not (string-prefix-p "ebox--" (symbol-name (car node))))) (not (string-prefix-p "ebox--" (symbol-name (car node)))))
(error "Unsafe ETAF static View form: %S" node)) (error "Unsafe ETAF static View form: %S" node))
(mapc #'etaf-playground--validate-static-node (cdr node)) (mapc (lambda (child) (etaf-playground--validate-static-node child tags))
(cdr node))
node) node)
(t (error "Unsafe ETAF static View value: %S" node)))) (t (error "Unsafe ETAF static View value: %S" node))))
(defun etaf-playground-read-static (name) (defun etaf-playground-read-static (name)
"Read and validate NAME's single inert `.etaf' form." "Read and validate NAME's single inert `.etaf' form."
(with-temp-buffer (let ((scenario (etaf-playground-scenario name)))
(insert-file-contents (etaf-playground--pair-file name ".etaf")) (with-temp-buffer
(let ((read-eval nil) (insert-file-contents (etaf-playground--pair-file name ".etaf"))
(form (read (current-buffer)))) (let ((read-eval nil))
(ignore read-eval) (ignore read-eval)
(condition-case nil (let ((form (read (current-buffer))))
(progn (read (current-buffer)) (condition-case nil
(error "ETAF static file contains multiple forms")) (progn (read (current-buffer))
(end-of-file nil)) (error "ETAF static file contains multiple forms"))
(etaf-playground--validate-static-node form)))) (end-of-file nil))
(etaf-playground--validate-static-node
form (plist-get scenario :static-tags)))))))
(defun etaf-playground-read-pair (name) (defun etaf-playground-read-pair (name)
"Read NAME data, load its companion, and return its root View." "Read NAME data, load its companion, and return its root View."
(let* ((scenario (etaf-playground-scenario name)) (let* ((scenario (etaf-playground-scenario name))
(form (etaf-playground-read-static name)) (form (etaf-playground-read-static name))
(feature (plist-get scenario :companion-feature))
(root (plist-get scenario :root-component))) (root (plist-get scenario :root-component)))
(unless (featurep 'etaf-operations-console) (unless (featurep feature)
;; Use Emacs's normal source/bytecode resolution. `load-file' forced
;; the 39KB companion through the Lisp interpreter on every fresh
;; process, making the first retained render look like a layout
;; regression. A compiled companion is the production path; source is
;; still the deterministic fallback when no `.elc' exists.
(load (file-name-sans-extension (load (file-name-sans-extension
(etaf-playground--pair-file name ".el")) (etaf-playground--pair-file name ".el")) nil nil nil))
nil nil nil))
(unless (functionp root) (error "Missing root factory: %S" root)) (unless (functionp root) (error "Missing root factory: %S" root))
(funcall root form))) (funcall root form)))
(defun etaf-playground-mount-example (buffer-name name) (defun etaf-playground-mount-example (buffer-name name)
"Mount pair NAME in BUFFER-NAME." "Mount registered pair NAME in BUFFER-NAME."
(when-let ((runtime (etaf-runtime-for-buffer buffer-name))) (when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
(etaf-unmount runtime)) (etaf-unmount runtime))
(let ((buffer (etaf-mount buffer-name (etaf-playground-read-pair name)))) (let ((buffer (etaf-mount buffer-name (etaf-playground-read-pair name))))
@ -105,32 +133,37 @@
;;;###autoload ;;;###autoload
(defun etaf-playground-open (&optional buffer-name) (defun etaf-playground-open (&optional buffer-name)
"Open the operations-console pair in BUFFER-NAME." "Open the registered default pair in BUFFER-NAME."
(interactive) (interactive)
(etaf-playground--display-buffer (etaf-playground--display-buffer
(etaf-playground-mount-example (etaf-playground-mount-example
(or buffer-name etaf-playground-buffer-name) "operations-console"))) (or buffer-name etaf-playground-buffer-name)
etaf-playground-default-pair)))
;;;###autoload ;;;###autoload
(defun etaf-playground-open-example (name &optional buffer-name) (defun etaf-playground-open-example (name &optional buffer-name)
"Open pair NAME in BUFFER-NAME." "Open registered pair NAME in BUFFER-NAME."
(interactive (list (completing-read "Pair: " etaf-playground-example-names)))
(etaf-playground--display-buffer (etaf-playground--display-buffer
(etaf-playground-mount-example (etaf-playground-mount-example
(or buffer-name etaf-playground-buffer-name) name))) (or buffer-name etaf-playground-buffer-name) name)))
;;;###autoload ;;;###autoload
(defun etaf-playground-reset (&optional buffer-name) (defun etaf-playground-reset (&optional buffer-name)
"Remount the active pair in BUFFER-NAME." "Remount the current/default registered pair in BUFFER-NAME."
(interactive) (let* ((name (or buffer-name etaf-playground-buffer-name))
(let ((name (or buffer-name etaf-playground-buffer-name))) (buffer (get-buffer name))
(unless (get-buffer name) (user-error "No Playground pair is mounted")) (pair (or (and buffer
(buffer-local-value 'etaf-playground-current-example
buffer))
etaf-playground-default-pair)))
(unless buffer (user-error "No ETAF Playground pair is mounted"))
(etaf-playground--display-buffer (etaf-playground--display-buffer
(etaf-playground-mount-example name "operations-console")))) (etaf-playground-mount-example name pair))))
;;;###autoload ;;;###autoload
(defun etaf-playground-close (&optional buffer-name) (defun etaf-playground-close (&optional buffer-name)
"Unmount and kill the Playground BUFFER-NAME." "Unmount and kill the Playground BUFFER-NAME."
(interactive)
(let* ((name (or buffer-name etaf-playground-buffer-name)) (let* ((name (or buffer-name etaf-playground-buffer-name))
(buffer (get-buffer name))) (buffer (get-buffer name)))
(when-let ((runtime (and buffer (etaf-runtime-for-buffer buffer)))) (when-let ((runtime (and buffer (etaf-runtime-for-buffer buffer))))

View File

@ -1,756 +0,0 @@
;;; operations-console.el --- one complete ETAF application example -*- lexical-binding: t; -*-
;;; Commentary:
;; This single-file sample shows the public development path: `:view'
;; props/slots/styles, `:setup' state, a normal composable, `expr' for
;; dynamic Views, Context/Theme, callbacks/Actions/Behaviors, Data, Resource
;; cleanup, and public open/close commands.
;;; Code:
(require 'cl-lib)
(require 'etaf)
(require 'etaf-ui)
(defconst etaf-operations-console-buffer "*ETAF Operations Console*"
"Default buffer used by `etaf-operations-console-open'.")
(defvar etaf-operations-console-cleanup-count 0
"Observable count of composable cleanup calls for review and tests.")
;;; Pure `:view' Component: props, named/default slots, and `:styles'.
(etaf-define-component etaf-operations-console-card (&key title)
"Render a styled card with explicit named and default slot outlets."
:styles
(styles
("&" :width stretch :border ((1) solid "#687386") :padding (1 2))
(".operations-console-card-title" :face bold))
:view
(column :class "operations-console-card"
(text :class "operations-console-card-title"
(expr :value title))
(slot :name 'header)
(slot (label :text "This card has no default content."))))
;;; A normal Elisp composable called synchronously from Component `:setup'.
(defun etaf-operations-console-use-counter (name initial-value)
"Create isolated counter state and effects for NAME and INITIAL-VALUE."
(let* ((count (etaf-ref initial-value :name name))
(double (etaf-computed (lambda () (* 2 (etaf-value count)))
:name (intern (format "%s-double" name))))
(updates (etaf-ref 0 :name (intern (format "%s-updates" name))))
(audit-text (format "effect saw %d" (* 2 (or initial-value 0))))
(audit (etaf-ref audit-text
:name (intern (format "%s-audit" name)))))
(etaf-watch count
(lambda (_new _old) (cl-incf (etaf-value updates)))
:immediate nil :name (intern (format "%s-watch" name)))
(etaf-watch-effect
(lambda ()
(let ((value (etaf-value double)))
(let ((next (format "effect saw %d" value)))
(unless (equal next audit-text)
(setq audit-text next)
(setf (etaf-value audit) next)))))
:name (intern (format "%s-effect" name)))
(etaf-on-scope-dispose
(lambda () (cl-incf etaf-operations-console-cleanup-count)))
(list :name name :count count :double double :updates updates
:audit audit)))
(defun etaf-operations-console-counter-ref (name operation)
"Return a stable interactive ref for counter NAME and OPERATION."
(intern (format "operations-console-%s-%s" name operation)))
;;; Stateful Component: its render closure retains the composable instance.
(etaf-define-component etaf-operations-console-counter (&key name initial-value)
"Render one isolated counter Component instance."
:setup
(let* ((instance-name name)
(initial initial-value)
(state (etaf-operations-console-use-counter
(intern (format "operations-console-%s" instance-name)) initial))
(count (plist-get state :count))
(double (plist-get state :double))
(updates (plist-get state :updates))
(audit (plist-get state :audit))
(increment (lambda () (cl-incf (etaf-value count))))
(reset (lambda () (setf (etaf-value count) initial))))
(lambda ()
(etaf-view
(operations-console-card
:title (format "Counter %s" instance-name)
(slot :name 'header
(label :text "State belongs to this Component instance."))
(label :text (format "Value: %d · Double: %d"
(etaf-value count) (etaf-value double)))
(label :text (format "Watch updates: %d · Audit: %s"
(etaf-value updates) (etaf-value audit)))
(flex :gap '(0 (8))
(button :label "Increment"
:ref (etaf-operations-console-counter-ref instance-name "increment")
:on-press increment)
(button :label "Reset"
:ref (etaf-operations-console-counter-ref instance-name "reset")
:on-press reset))
(expr
:value
(when (> (etaf-value count) 0)
(etaf-view
(text "Dynamic child is visible after Increment.")))))))))
;;; Context and Theme: the child consumes a value provided by the shell.
;;; Named Action: state is passed explicitly, so no hidden global app state.
(etaf-action-define etaf-operations-console-named-note (runtime target)
"Publish a message through a named Action."
(ignore runtime)
(setf (etaf-value target) "Named Action: pressed"))
;;; Behavior reuse: ordinary Elisp returns semantic interaction bundles.
(defun etaf-operations-console-use-toggle-behaviors (toggle message)
"Return reusable focus and toggle Behaviors for TOGGLE and MESSAGE."
(list
(etaf-focusable)
(etaf-toggleable
:value toggle
:on-change
(lambda (value)
(setf (etaf-value toggle) value
(etaf-value message)
(if value "Behavior toggle: on" "Behavior toggle: off"))))))
(etaf-define-component etaf-operations-console-action-panel ()
"Show a local callback, a named Action, and reusable Behaviors."
:setup
(let* ((message (etaf-ref "Callback action pending" :name 'operations-console-message))
(toggle (etaf-ref nil :name 'operations-console-toggle))
;; Behavior installers use function identity. Construct this
;; reusable bundle once in setup so state renders retain it.
(behaviors (etaf-operations-console-use-toggle-behaviors toggle message))
(local-callback
(lambda () (setf (etaf-value message) "Local callback: pressed")))
(named-callback
(lambda () (etaf-dispatch 'etaf-operations-console-named-note message))))
(lambda ()
(etaf-view
(operations-console-card
:title "Events, Actions, and Behaviors"
(column :width 'stretch
(label :text (etaf-value message)))
(flex :gap '(0 (8))
(button :label "Local callback"
:ref 'operations-console-local-callback
:on-press local-callback)
(button :label "Named Action"
:ref 'operations-console-named-action
:on-press named-callback)
(button
:label (if (etaf-value toggle) "Behavior: on" "Behavior: off")
:ref 'operations-console-behavior-toggle
:use behaviors)))))))
;;; Application model: detached resources are ready before the first render.
(defun etaf-operations-console-create-model ()
"Create and load the external model before mounting the application."
(let* ((source (etaf-data-memory-source
'((:id 1 :name "Ada" :status "open")
(:id 2 :name "Grace" :status "review")
(:id 3 :name "Lin" :status "done"))
:id-key :id :name 'operations-console-records))
(controller (etaf-data-controller
source :page-size 3 :name 'operations-console-controller))
(fail-next (etaf-ref nil :name 'operations-console-fail-next))
(cleanups (etaf-ref 0 :name 'operations-console-resource-cleanups))
(health (etaf-ref "All systems nominal"
:name 'operations-console-health-status))
(activity (etaf-ref "Theme provider promoted"
:name 'operations-console-activity))
(trail (etaf-ref "Surface mounted · Data controller loaded"
:name 'operations-console-event-trail))
(throughput (etaf-ref 2400 :name 'operations-console-throughput))
(generation 0))
(etaf-data-load controller)
(let ((resource
(etaf-resource
(lambda ()
(cl-incf generation)
(if (etaf-value fail-next)
(progn
(setf (etaf-value fail-next) nil)
(error "Example load failed"))
(etaf-resource-result
(format "Loaded value %d" generation)
:cleanup (lambda () (cl-incf (etaf-value cleanups))))))
:immediate t :name 'operations-console-resource)))
(list :controller controller :resource resource
:fail-next fail-next :cleanups cleanups
:health health :activity activity :trail trail
:throughput throughput
:health-check
(lambda ()
(setf (etaf-value health) "Health check complete"
(etaf-value activity) "Health check completed"
(etaf-value trail) "Health check · all owned surfaces responded"))
:show-trail
(lambda ()
(setf (etaf-value activity) "Event trail focused"
(etaf-value trail) "Event trail opened for inspection"))))))
(defun etaf-operations-console-dispose-model (model)
"Stop MODEL's Data Controller and dispose its Resource."
(etaf-data-stop (plist-get model :controller))
(etaf-resource-dispose (plist-get model :resource)))
(defun etaf-operations-console-data-status (selection items)
"Return visible status derived from SELECTION and ITEMS."
(let ((selected (cl-find (car selection) items :key
(lambda (row) (plist-get row :id))
:test #'equal)))
(if selected
(format "Selected %s" (plist-get selected :name))
"Rows loaded from the public controller.")))
;;; Data Controller + official `data-grid'.
(etaf-define-component etaf-operations-console-data-panel (&key model)
"Render MODEL's loaded Data Controller and interactive DataGrid."
:setup
(let* ((controller (plist-get model :controller))
(view-state
(etaf-computed
(lambda ()
(let ((selection (etaf-value (etaf-data-selection controller)))
(items (etaf-value (etaf-data-items controller))))
(list :selected (car selection)
:status (etaf-operations-console-data-status selection items))))
:name 'operations-console-data-view-state))
(row-key (lambda (row) (plist-get row :id)))
(row-ref (lambda (row)
(intern (format "operations-console-row-%s"
(plist-get row :id)))))
(row-press (lambda (row)
(etaf-data-select-one controller (plist-get row :id))))
(clear-selection
(lambda () (etaf-data-clear-selection controller))))
(lambda ()
(let ((state (etaf-value view-state)))
(etaf-view
(operations-console-card
:title "Data Controller and DataGrid"
(label :text (plist-get state :status))
(data-grid
:controller controller
:columns '((:key :id :label "ID" :width 5)
(:key :name :label "NAME" :width 20)
(:key :status :label "STATUS" :width 10))
:row-key row-key
:row-ref row-ref
:selected-key (plist-get state :selected)
:on-row-press row-press)
(button :label "Clear selection"
:ref 'operations-console-clear-selection
:on-press clear-selection)))))))
;;; Resource + explicit error boundary: load, reload, cleanup, and recovery.
(etaf-define-component etaf-operations-console-resource-panel (&key model)
"Render MODEL's Resource with reload, failure, and cleanup feedback."
:setup
(let* ((instance-model model)
(fail-next (plist-get instance-model :fail-next))
(cleanups (plist-get instance-model :cleanups))
(boundary (etaf-ref "No boundary run yet."
:name 'operations-console-boundary))
(resource (plist-get instance-model :resource))
(fail-change (lambda (value) (setf (etaf-value fail-next) value)))
(reload (lambda () (etaf-resource-load resource)))
(run-boundary
(lambda ()
(setf (etaf-value boundary)
(etaf-error-boundary-run
(lambda () (error "Handled sample"))
(lambda (condition)
(format "Handled: %s"
(error-message-string condition))))))))
(lambda ()
(etaf-view
(operations-console-card
:title "Resource and Error Boundary"
(label :text (format "Status: %s"
(etaf-resource-status resource)))
(label :text (or (etaf-resource-value resource)
(when-let ((condition (etaf-resource-error resource)))
(format "Error: %s"
(error-message-string condition)))))
(label :text (format "Cleanup calls: %d"
(etaf-value cleanups)))
(checkbox :label "Fail next load"
:ref 'operations-console-fail-next
:checked (etaf-value fail-next)
:on-change fail-change)
(flex :gap '(0 (8))
(button :label "Reload resource"
:ref 'operations-console-resource-reload
:on-press reload)
(button :label "Run handled boundary"
:ref 'operations-console-error-boundary
:on-press run-boundary))
(label :text (etaf-value boundary)))))))
;;; Page composition and navigation views.
(defun etaf-operations-console-static-child (form tag)
"Return the first static child named TAG from FORM.
The static `.etaf' form is the composition authority; the companion only
fills these safe named slots with reactive Views."
(cl-find-if (lambda (entry)
(and (consp entry) (eq (car entry) tag)))
(cdr form)))
(defun etaf-operations-console-static-value (form key default)
"Return keyword KEY from static FORM, or DEFAULT.
Keyword values are inert data; executable behavior never comes from `.etaf'."
(let ((tail (member key (cdr form))))
(if tail (cadr tail) default)))
(defun etaf-operations-console-static-child-value (form tag key default)
"Return KEY from child TAG in static FORM, or DEFAULT.
The helper keeps `.etaf' consumption at one validated data boundary instead
of scattering plist reads through the application renderer."
(etaf-operations-console-static-value
(or (etaf-operations-console-static-child form tag) nil)
key default))
(defun etaf-operations-console-static-child-by-value (form tag key value)
"Return the child TAG whose KEY equals VALUE in static FORM."
(cl-find-if (lambda (entry)
(and (consp entry)
(eq (car entry) tag)
(equal (plist-get (cdr entry) key) value)))
(cdr form)))
(defun etaf-operations-console-hero-view (model hero-form)
"Return the visual hero for MODEL using static HERO-FORM copy."
(let ((eyebrow (etaf-operations-console-static-value
hero-form :eyebrow "TUESDAY / 09:42 / LIVE REVIEW"))
(title (etaf-operations-console-static-value
hero-form :title "Make complex state feel calm.")))
(etaf-view
(column :class "operations-console-hero" :width 'stretch
:padding '(2 3) :border "#17374A"
:bgcolor "#17374A" :color "#F6F1E8"
(column :width 'stretch
(label :text eyebrow :face 'bold)
(label :text title :face 'bold)
(label :text "A single surface for components, context, behavior, data, and recovery — designed to make every ETAF update visible and explainable.")
(flex :width 'stretch :gap '(1 (8))
(button :label "Run health check"
:ref 'operations-console-health-check
:on-press (plist-get model :health-check))
(button :label "View event trail"
:ref 'operations-console-show-trail
:on-press (plist-get model :show-trail))))))))
(etaf-define-component etaf-operations-console-metric
(&key label value delta color)
"Render one KPI metric with a retained Component owner."
:setup
(lambda ()
(etaf-view
(column :class "operations-console-metric" :width 'stretch
:padding '(1 2) :border (etaf-current-prop :color)
:bgcolor (etaf-theme-value
:surface "#FFFDF8")
:color (etaf-theme-value :color "#142235")
(label :text (etaf-current-prop :label))
(label :text (etaf-current-prop :value) :face 'bold)
(label :text (etaf-current-prop :delta)
:color (etaf-theme-value
:success "#3E9B72"))))))
(defun etaf-operations-console-metric-strip-view (model)
"Return the four-card KPI strip for MODEL."
(etaf-view
(grid :width 'stretch :grid-template-columns '(1fr 1fr 1fr 1fr)
:gap '(1 (8))
(operations-console-metric
:label "Throughput"
:value (format "%dk" (/ (etaf-value (plist-get model :throughput)) 1000))
:delta "↑ 18% this week"
:color (etaf-theme-value :teal "#2E8B83"))
(operations-console-metric
:label "Active flows" :value "18" :delta "+3 since 08:00"
:color (etaf-theme-value :coral "#E26D5A"))
(operations-console-metric
:label "Response" :value "42ms" :delta "p95 within target"
:color (etaf-theme-value :amber "#D99A3D"))
(operations-console-metric
:label "Reliability" :value "99.8%" :delta "steady"
:color (etaf-theme-value :success "#3E9B72")))))
(defun etaf-operations-console-activity-view (model)
"Return the reactive Activity stream card for MODEL."
(etaf-view
(column :class "operations-console-card" :width 'stretch
:padding '(1 2) :border (etaf-theme-value
:line "#C8C1B6")
:bgcolor (etaf-theme-value :surface "#FFFDF8")
(flex :width 'stretch :align-items 'center
(column :width 'stretch :flex-grow 1 :flex-shrink 1 :min-width 0
(label :text "Activity stream" :face 'bold)
(label :text "Reactive events, rendered as a readable story."))
(label :text "4 EVENTS" :color
(etaf-theme-value :teal "#2E8B83")))
(label :text (format "↗ %s" (etaf-value (plist-get model :activity))))
(label :text "◆ Counter alpha crossed 2k · one Component-owned update")
(label :text "✓ Resource boundary recovered · shell stayed mounted"))))
(defun etaf-operations-console-capability-view ()
"Return the static capability map card."
(etaf-view
(column :class "operations-console-card" :width 'stretch
:padding '(1 2) :border (etaf-theme-value
:line "#C8C1B6")
:bgcolor (etaf-theme-value :surface "#FFFDF8")
(flex :width 'stretch :align-items 'center
(column :width 'stretch :flex-grow 1 :flex-shrink 1 :min-width 0
(label :text "Capability map" :face 'bold)
(label :text "What this surface is proving."))
(label :text "ETAF" :color
(etaf-theme-value :teal "#2E8B83")))
(label :text "✓ Component ownership · props, slots, setup, retained identity")
(label :text "✓ Context + Theme · provider defaults and dark mode")
(label :text "✓ Behavior + Actions · keyboard-ready stable refs")
(label :text "✓ Data + Resource · selection, failure, boundary, cleanup"))))
(defun etaf-operations-console-timeline-view (model)
"Return the event trail card for MODEL."
(etaf-view
(column :class "operations-console-card" :width 'stretch
:padding '(1 2) :border (etaf-theme-value
:line "#C8C1B6")
:bgcolor (etaf-theme-value :surface "#FFFDF8")
(flex :width 'stretch :align-items 'center
(column :width 'stretch :flex-grow 1 :flex-shrink 1 :min-width 0
(label :text "Event trail" :face 'bold)
(label :text "Every meaningful action leaves a compact, inspectable trace."))
(button :label "Clear trail" :ref 'operations-console-clear-trail
:on-press (lambda ()
(setf (etaf-value (plist-get model :trail))
"Trail cleared · new actions append here"))))
(label :text (format "● %s" (etaf-value (plist-get model :trail)))))))
;; These wrappers make the HTML sections real ETAF Components. The plain
;; `*-view' helpers stay small and readable, while the Component boundary
;; gives every dynamic `expr' a reviewed semantic owner.
(etaf-define-component etaf-operations-console-hero (&key model hero-form)
"Render the operations hero as a retained Component."
:setup
(lambda () (etaf-operations-console-hero-view
(etaf-current-prop :model)
(etaf-current-prop :hero-form))))
(etaf-define-component etaf-operations-console-metric-strip (&key model)
"Render the operations KPI strip as a retained Component."
:setup
(lambda () (etaf-operations-console-metric-strip-view
(etaf-current-prop :model))))
(etaf-define-component etaf-operations-console-activity (&key model)
"Render the reactive activity card as a retained Component."
:setup
(lambda () (etaf-operations-console-activity-view
(etaf-current-prop :model))))
(etaf-define-component etaf-operations-console-capabilities ()
"Render the static capability map as a reusable Component."
:setup
(lambda () (etaf-operations-console-capability-view)))
(etaf-define-component etaf-operations-console-timeline (&key model)
"Render the event trail as a retained Component."
:setup
(lambda () (etaf-operations-console-timeline-view
(etaf-current-prop :model))))
(defun etaf-operations-console-overview-page (model main-form)
"Return the Overview page View for MODEL using MAIN-FORM sections."
(let* ((overview-form (etaf-operations-console-static-child
main-form 'overview))
(hero-form (etaf-operations-console-static-child
overview-form 'hero)))
(etaf-view
(column :class "operations-console-page" :width 'stretch
(operations-console-hero :model model :hero-form hero-form)
(operations-console-metric-strip :model model)
(grid :width 'stretch :grid-template-columns '(5fr 3fr)
:gap '(1 (12))
(operations-console-activity :model model)
(operations-console-capabilities))
(operations-console-timeline :model model)
(grid :width 'stretch :grid-template-columns '(1fr 1fr)
:gap '(1 (12))
(operations-console-counter :name "alpha" :initial-value 0)
(operations-console-counter :name "beta" :initial-value 10)
(operations-console-action-panel))))))
(defun etaf-operations-console-data-page (model)
"Return the Data page View for MODEL."
(etaf-view
(column :class "operations-console-page" :width 'stretch
(operations-console-data-panel :model model))))
(defun etaf-operations-console-resource-page (model)
"Return the Resource page View for MODEL."
(etaf-view
(column :class "operations-console-page" :width 'stretch
(operations-console-resource-panel :model model))))
(etaf-define-component etaf-operations-console-overview-surface
(&key model main-form)
"Own the material Overview page surface."
:setup
(lambda ()
(etaf-operations-console-overview-page
(etaf-current-prop :model)
(etaf-current-prop :main-form))))
(etaf-define-component etaf-operations-console-data-surface (&key model)
"Own the material Data page surface."
:setup
(lambda ()
(etaf-operations-console-data-page (etaf-current-prop :model))))
(etaf-define-component etaf-operations-console-resource-surface (&key model)
"Own the material Resource page surface."
:setup
(lambda ()
(etaf-operations-console-resource-page (etaf-current-prop :model))))
(defun etaf-operations-console-page-view (page model main-form)
"Return the selected page Component call for PAGE, MODEL, and MAIN-FORM.
The function is called from an `expr' Range, so the page signal belongs to
the retained route owner while the structural forms remain ordinary compiled
View calls."
(pcase page
('data
(etaf-view (operations-console-data-surface :model model)))
('resource
(etaf-view (operations-console-resource-surface :model model)))
(_
(etaf-view
(operations-console-overview-surface
:model model :main-form main-form)))))
(etaf-define-component etaf-operations-console-page-router
(&key page-ref model main-form)
"Render the current material page from PAGE-REF and MODEL.
The page signal is read by this retained router Component, not by the root
shell, so navigation does not invalidate the whole application generation."
:setup
(lambda ()
;; Keep one material Host as the route owner's layout anchor. Page
;; surfaces are Components, so the route never injects a raw View through
;; an unowned expr and the containing-block width remains explicit.
(let ((model (etaf-current-prop :model))
(main-form (etaf-current-prop :main-form))
(page-ref (etaf-current-prop :page-ref)))
(etaf-view
(fragment
(column :class "operations-console-page-router" :width 'stretch
:ref 'operations-console-page-slot
(expr
:value
(etaf-operations-console-page-view
(etaf-value page-ref) model main-form))))))))
;;; Root shell: Context provider, two composable instances, and viewport.
(etaf-define-component etaf-operations-console-shell
(&key model static-form page-ref)
"Compose the complete public ETAF application."
:styles
(styles
(".operations-console-shell" :width stretch :padding (1 2))
(".operations-console-shell-header" :width stretch :padding (1 2))
(".operations-console-nav" :padding (0 2)
:border ((1) solid "#687386"))
(".operations-console-main" :width stretch :padding (1 2))
(".operations-console-page" :width stretch)
(".operations-console-status" :width stretch :padding (0 2)
:border ((1) solid "#687386")))
:setup
(let* ((instance-model model)
(page-cell page-ref)
(title (etaf-operations-console-static-value
static-form :title "Operations Console"))
(kicker (etaf-operations-console-static-value
static-form :kicker "ETAF / reference workspace"))
(header-form (etaf-operations-console-static-child static-form 'header))
(navigation-form
(etaf-operations-console-static-child static-form 'navigation))
(main-form (etaf-operations-console-static-child static-form 'main))
(status-form (etaf-operations-console-static-child static-form 'status))
(brand-form (etaf-operations-console-static-child header-form 'brand))
(summary-form (etaf-operations-console-static-child header-form 'summary))
(theme-control-form
(etaf-operations-console-static-child header-form 'theme-control))
(overview-tab
(etaf-operations-console-static-child-by-value
navigation-form 'tab :key "overview"))
(data-tab
(etaf-operations-console-static-child-by-value
navigation-form 'tab :key "data"))
(resource-tab
(etaf-operations-console-static-child-by-value
navigation-form 'tab :key "resource"))
(brand-mark (etaf-operations-console-static-value
brand-form :mark "OC"))
(brand-name (etaf-operations-console-static-value
brand-form :name title))
(summary-status (etaf-operations-console-static-value
summary-form :status "All systems nominal"))
(theme-label (etaf-operations-console-static-value
theme-control-form :label "Theme"))
(overview-label (etaf-operations-console-static-value
overview-tab :label "Overview"))
(data-label (etaf-operations-console-static-value
data-tab :label "Data"))
(resource-label (etaf-operations-console-static-value
resource-tab :label "Resource"))
(dark (etaf-ref nil :name 'operations-console-dark-theme))
(theme (etaf-computed
(lambda ()
(if (etaf-value dark)
'(:color "#EDF2F4" :bgcolor "#111923"
:surface "#182433" :line "#344455"
:teal "#61C6BA" :coral "#FF957F"
:amber "#EFC06C" :success "#73D29D")
'(:color "#142235" :bgcolor "#F6F1E8"
:surface "#FFFDF8" :line "#C8C1B6"
:teal "#2E8B83" :coral "#E26D5A"
:amber "#D99A3D" :success "#3E9B72"))))))
(unless (and header-form navigation-form main-form status-form
brand-form summary-form theme-control-form
overview-tab data-tab resource-tab)
(error "Invalid Operations Console shell composition: %S" static-form))
(etaf-provide 'operations-console-theme theme)
(etaf-theme-provide theme)
(etaf-on-unmounted
(lambda () (etaf-operations-console-dispose-model instance-model)))
(lambda ()
(etaf-view
(column :class "operations-console-shell" :width 'stretch
:height '(viewport-height)
(column :class "operations-console-shell-header" :width 'stretch
(flex :width 'stretch :align-items 'center
(column :width 'stretch :flex-grow 1 :flex-shrink 1 :min-width 0
(label :face 'bold :text (format "%s %s" brand-mark brand-name))
(label :text kicker))
(label :text (format "● %s" summary-status)
:color (etaf-theme-value
:success "#3E9B72")))
(label :text "A calm reference surface for complex state, ownership, and recovery."))
(flex :class "operations-console-nav" :width 'max-content
:ref 'operations-console-nav
:align-items 'center :gap '(0 (8))
(button :label overview-label :ref 'operations-console-nav-overview
:on-press (lambda () (setf (etaf-value page-cell) 'overview)))
(button :label data-label :ref 'operations-console-nav-data
:on-press (lambda () (setf (etaf-value page-cell) 'data)))
(button :label resource-label :ref 'operations-console-nav-resource
:on-press (lambda () (setf (etaf-value page-cell) 'resource)))
(checkbox :label theme-label :ref 'operations-console-theme-toggle
:checked (etaf-value dark)
:color (etaf-theme-value :color "#252A2E")
:bgcolor (etaf-theme-value :bgcolor "#F8F5EE")
:on-change (lambda (value)
(setf (etaf-value dark) value)))
;; Reserve the small status slot explicitly so a theme flip changes
;; paint/content inside the control bar without reflowing its parent.
(label :width 12
:text (if (etaf-value dark) "Theme: Dark" "Theme: Light")))
(column :class "operations-console-main" :width 'stretch
:flex-grow 1 :flex-shrink 1 :ref 'operations-console-main
(operations-console-page-router :page-ref page-cell
:model instance-model
:main-form main-form))
(column :class "operations-console-status" :width 'stretch
:flex-shrink 0 :ref 'operations-console-status
(label :text (or (etaf-operations-console-static-value
status-form :label nil)
"Ready for review"))))))))
;;; Public View/open/close entry points.
(defun etaf-operations-console-view (model static-form page-ref)
"Return the root View for MODEL, STATIC-FORM, and stable PAGE-REF."
(etaf-view (operations-console-shell :model model :static-form static-form
:page-ref page-ref)))
(defun etaf-operations-console-root (static-form)
"Consume validated STATIC-FORM and return one owned application root View."
(unless (equal static-form
'(operations-console-shell
:title "Operations Console"
:kicker "ETAF / reference workspace"
(header
(brand :mark "OC" :name "Operations Console")
(summary :status "All systems nominal")
(theme-control :label "Theme"))
(navigation
(tab :key "overview" :label "Overview" :index "01")
(tab :key "data" :label "Data desk" :index "02")
(tab :key "resource" :label "Resource lab" :index "03"))
(main
(overview
(hero :eyebrow "Tuesday / 09:42 / live review"
:title "Make complex state feel calm.")
(metric-strip
(metric :key "throughput" :label "Throughput")
(metric :key "flows" :label "Active flows")
(metric :key "response" :label "Response")
(metric :key "reliability" :label "Reliability"))
(workspace
(activity-panel :title "Activity stream")
(capability-panel :title "Capability map"))
(timeline :title "Event trail"))
(data :title "Data desk")
(resource :title "Resource lab"))
(status :label "Ready for review")))
(error "Unsupported Operations Console static root: %S" static-form))
(let ((model (etaf-operations-console-create-model))
(page (etaf-ref 'overview :name 'operations-console-page)))
(lambda ()
(etaf-operations-console-view
model static-form page))))
;;;###autoload
(defun etaf-operations-console-open (&optional buffer-name)
"Open the canonical Playground pair in BUFFER-NAME."
(interactive)
(require 'etaf-playground)
(etaf-playground-open (or buffer-name etaf-operations-console-buffer)))
;;;###autoload
(defun etaf-operations-console-close (&optional buffer-name)
"Unmount and kill the application buffer named BUFFER-NAME."
(interactive)
(let* ((name (or buffer-name etaf-operations-console-buffer))
(runtime (etaf-runtime-for-buffer name))
(buffer (get-buffer name)))
(when runtime (etaf-unmount runtime))
(when (buffer-live-p buffer) (kill-buffer buffer))
buffer))
(provide 'etaf-operations-console)
;;; operations-console.el ends here

View File

@ -1,29 +0,0 @@
;; Static composition only. The companion owns all state, components, and
;; event handlers; this file is the safe layout contract it consumes.
(operations-console-shell
:title "Operations Console"
:kicker "ETAF / reference workspace"
(header
(brand :mark "OC" :name "Operations Console")
(summary :status "All systems nominal")
(theme-control :label "Theme"))
(navigation
(tab :key "overview" :label "Overview" :index "01")
(tab :key "data" :label "Data desk" :index "02")
(tab :key "resource" :label "Resource lab" :index "03"))
(main
(overview
(hero :eyebrow "Tuesday / 09:42 / live review"
:title "Make complex state feel calm.")
(metric-strip
(metric :key "throughput" :label "Throughput")
(metric :key "flows" :label "Active flows")
(metric :key "response" :label "Response")
(metric :key "reliability" :label "Reliability"))
(workspace
(activity-panel :title "Activity stream")
(capability-panel :title "Capability map"))
(timeline :title "Event trail"))
(data :title "Data desk")
(resource :title "Resource lab"))
(status :label "Ready for review"))

542
examples/research-shelf.el Normal file
View File

@ -0,0 +1,542 @@
;;; research-shelf.el --- SQLite-backed research library -*- lexical-binding: t; -*-
;;; Commentary:
;; A useful ETAF application: a small reading/research shelf whose records
;; survive unmounts because the Data Controller is backed by etaf-sqlite.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'etaf)
(require 'etaf-ui)
(require 'etaf-sqlite)
(require 'etaf-playground)
(defcustom etaf-research-shelf-database-file
(expand-file-name "research-shelf.sqlite" user-emacs-directory)
"SQLite file used by the Research Shelf application."
:type 'file
:group 'etaf)
(defconst etaf-research-shelf-seed-records
'((:id 1 :title "The Shape of Tools" :author "M. Abramson"
:kind "Essay" :status "reading" :progress 64 :priority "High"
:starred 1 :note "The best tools make attention feel larger, not more fragmented." :updated "Today")
(:id 2 :title "Ways of Seeing" :author "John Berger"
:kind "Book" :status "unread" :progress 0 :priority "Medium"
:starred 0 :note "Look again at the relationship between image and power." :updated "Yesterday")
(:id 3 :title "The Cathedral and the Bazaar" :author "Eric S. Raymond"
:kind "Paper" :status "finished" :progress 100 :priority "Low"
:starred 1 :note "A useful contrast between closed and open production." :updated "Mon")
(:id 4 :title "Designing for Calm" :author "A. Ito"
:kind "Notes" :status "reading" :progress 32 :priority "High"
:starred 0 :note "Reduce decisions before asking for attention." :updated "Sun")
(:id 5 :title "The Craftsman" :author "Richard Sennett"
:kind "Book" :status "unread" :progress 0 :priority "Medium"
:starred 1 :note "A long-form companion for making and revising." :updated "Sat")
(:id 6 :title "How Buildings Learn" :author "Stewart Brand"
:kind "Book" :status "reading" :progress 78 :priority "Medium"
:starred 0 :note "Change is a property of useful systems." :updated "Fri")
(:id 7 :title "The Interface Is the Message" :author "N. Hara"
:kind "Essay" :status "finished" :progress 100 :priority "Low"
:starred 0 :note "A short note on visible constraints." :updated "Thu")
(:id 8 :title "Notes on Attention" :author "Simone Weil"
:kind "Notes" :status "unread" :progress 0 :priority "High"
:starred 1 :note "Attention is not force; it is patient availability." :updated "Wed"))
"Seed records installed into a new Research Shelf database once.")
(defun etaf-research-shelf--static-child (form tag)
"Return the first child named TAG from static FORM."
(cl-find-if (lambda (entry) (and (consp entry) (eq (car entry) tag)))
(cdr form)))
(defun etaf-research-shelf--static-value (form key default)
"Return KEY from static FORM, or DEFAULT."
(or (plist-get (cdr form) key) default))
(defun etaf-research-shelf--database ()
"Return the configured typed Research Shelf database description."
(etaf-sqlite-database
etaf-research-shelf-database-file
(etaf-sqlite-table
'reading_items
(list (etaf-sqlite-column :id "id" :type 'integer :primary t)
(etaf-sqlite-column :title "title" :type 'text)
(etaf-sqlite-column :author "author" :type 'text)
(etaf-sqlite-column :kind "kind" :type 'text)
(etaf-sqlite-column :status "status" :type 'text)
(etaf-sqlite-column :progress "progress" :type 'integer)
(etaf-sqlite-column :priority "priority" :type 'text)
(etaf-sqlite-column :starred "starred" :type 'integer)
(etaf-sqlite-column :note "note" :type 'text)
(etaf-sqlite-column :updated "updated" :type 'text))
:id)))
(defun etaf-research-shelf--ensure-database ()
"Initialize the SQLite database and install seed data when empty."
(let* ((database (etaf-research-shelf--database))
(source (etaf-sqlite-source database))
(load (plist-get source :load))
(mutate (plist-get source :mutate)))
(etaf-sqlite-initialize database)
(when (= 0 (plist-get (funcall load nil 1 1) :total))
(dolist (record etaf-research-shelf-seed-records)
(funcall mutate 'insert record)))
database))
(defun etaf-research-shelf--create-model ()
"Create the application model and its SQLite-backed Data Controller."
(let* ((database (etaf-research-shelf--ensure-database))
(controller
(etaf-data-controller
(etaf-sqlite-source database)
:page-size 4 :auto-load nil :name 'etaf-research-shelf))
(filter (etaf-ref 'all :name 'research-shelf-filter))
(dark (etaf-ref nil :name 'research-shelf-dark-theme))
(toast (etaf-ref "Ready to read." :name 'research-shelf-toast))
(next-id (etaf-ref 100 :name 'research-shelf-next-id)))
(list :database database :database-file etaf-research-shelf-database-file
:controller controller :filter filter :dark dark :toast toast
:next-id next-id)))
(defun etaf-research-shelf--controller (model)
"Return MODEL's Data Controller."
(plist-get model :controller))
(defun etaf-research-shelf--query (filter)
"Translate FILTER into an allowlisted SQLite equality query."
(pcase filter
('reading '(:status "reading"))
('unread '(:status "unread"))
('finished '(:status "finished"))
('starred '(:starred 1))
(_ nil)))
(defun etaf-research-shelf--load (model &optional message)
"Load MODEL and expose a visible success/error MESSAGE."
(let ((controller (etaf-research-shelf--controller model)))
(condition-case err
(progn
(etaf-data-load controller)
(setf (etaf-value (plist-get model :toast))
(or message "✓ Saved locally")))
(error
(setf (etaf-value (plist-get model :toast))
(format "⚠ Storage issue: %s" (error-message-string err)))))))
(defun etaf-research-shelf--set-filter (model filter)
"Set MODEL FILTER and reload the SQLite-backed list."
(let ((controller (etaf-research-shelf--controller model)))
(setf (etaf-value (plist-get model :filter)) filter)
(etaf-data-set-query controller (etaf-research-shelf--query filter))
(etaf-data-set-page controller 1)
(etaf-research-shelf--load model
(format "Showing %s"
(capitalize (symbol-name filter))))))
(defun etaf-research-shelf--items (model)
"Return loaded items for MODEL."
(etaf-value (etaf-data-items (etaf-research-shelf--controller model))))
(defun etaf-research-shelf--selected (model)
"Return the selected item in MODEL, or nil."
(let* ((controller (etaf-research-shelf--controller model))
(id (car (etaf-value (etaf-data-selection controller)))))
(cl-find id (etaf-research-shelf--items model)
:key (lambda (row) (plist-get row :id)) :test #'equal)))
(defun etaf-research-shelf--mutate (model operation payload message)
"Apply SQLite OPERATION PAYLOAD for MODEL.
Show MESSAGE on success, or preserve the resulting error state."
(let ((controller (etaf-research-shelf--controller model)))
(condition-case err
(progn
(etaf-data-mutate controller operation payload)
(setf (etaf-value (plist-get model :toast)) message))
(error
(setf (etaf-value (plist-get model :toast))
(format "⚠ Could not save: %s" (error-message-string err)))))))
(defun etaf-research-shelf--update-selected (model fields message)
"Update selected MODEL record with FIELDS and show MESSAGE."
(when-let ((row (etaf-research-shelf--selected model)))
(etaf-research-shelf--mutate
model 'update
(append (list :id (plist-get row :id)) fields)
message)))
(defun etaf-research-shelf--theme (model)
"Return MODEL's current palette."
(if (etaf-value (plist-get model :dark))
'(:ink "#F4F7FF" :paper "#111827" :panel "#182235" :line "#34435A"
:muted "#AAB5C8" :accent "#8EA7FF" :mint "#76D6C3" :coral "#FF9A8E"
:amber "#F0C36A" :soft "#202C42")
'(:ink "#172033" :paper "#F7F3EA" :panel "#FFFDF8" :line "#D9D4C9"
:muted "#6D7482" :accent "#3657D6" :mint "#3E9B8F" :coral "#D86B5D"
:amber "#C58A3A" :soft "#F1EEE7")))
(defun etaf-research-shelf--color (model key)
"Return palette KEY for MODEL."
(plist-get (etaf-research-shelf--theme model) key))
(defun etaf-research-shelf--status-label (status)
"Return a readable status label for STATUS."
(pcase status
("reading" "◷ In progress")
("finished" "✓ Finished")
("archived" "↗ Archived")
(_ "○ Unread")))
(defun etaf-research-shelf--filter-label (model form key fallback)
"Return MODEL's active-aware label for KEY from static FILTER FORM."
(let* ((entry (cl-find-if
(lambda (item) (and (consp item) (eq (car item) 'filter)
(eq (plist-get (cdr item) :key) key)))
(cdr form)))
(label (or (and entry (plist-get (cdr entry) :label)) fallback)))
(if (eq key (etaf-value (plist-get model :filter)))
(concat "" label)
label)))
(defun etaf-research-shelf--filter-view (model form)
"Return the filter rail from MODEL and static FILTER FORM."
(etaf-view
(column :class "research-shelf-filter-rail" :width 'stretch
:padding '(1 1)
:border (etaf-research-shelf--color model :line)
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel)
(label :text "Library" :face 'bold
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel))
(label :text "VIEWS" :color (etaf-research-shelf--color model :muted)
:bgcolor (etaf-research-shelf--color model :panel))
(button :label (etaf-research-shelf--filter-label model form 'all "All")
:ref 'research-shelf-filter-all
:variant (if (eq (etaf-value (plist-get model :filter)) 'all)
'secondary 'ghost)
:on-press (lambda ()
(etaf-research-shelf--set-filter model 'all)))
(button :label (etaf-research-shelf--filter-label
model form 'reading "In progress")
:ref 'research-shelf-filter-reading
:variant (if (eq (etaf-value (plist-get model :filter)) 'reading)
'secondary 'ghost)
:on-press (lambda ()
(etaf-research-shelf--set-filter model 'reading)))
(button :label (etaf-research-shelf--filter-label
model form 'unread "Unread")
:ref 'research-shelf-filter-unread
:variant (if (eq (etaf-value (plist-get model :filter)) 'unread)
'secondary 'ghost)
:on-press (lambda ()
(etaf-research-shelf--set-filter model 'unread)))
(button :label (etaf-research-shelf--filter-label
model form 'finished "Finished")
:ref 'research-shelf-filter-finished
:variant (if (eq (etaf-value (plist-get model :filter)) 'finished)
'secondary 'ghost)
:on-press (lambda ()
(etaf-research-shelf--set-filter model 'finished)))
(button :label (etaf-research-shelf--filter-label
model form 'starred "★ Starred")
:ref 'research-shelf-filter-starred
:variant (if (eq (etaf-value (plist-get model :filter)) 'starred)
'secondary 'ghost)
:on-press (lambda ()
(etaf-research-shelf--set-filter model 'starred)))
(spacer :height 1)
(label :text "STORAGE" :color (etaf-research-shelf--color model :muted)
:bgcolor (etaf-research-shelf--color model :panel))
(label :text (file-name-nondirectory
(plist-get model :database-file))
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel))
(button :label "↻ Reload library" :ref 'research-shelf-reload
:variant 'ghost
:on-press (lambda () (etaf-research-shelf--load model "✓ Library reloaded"))))))
(defun etaf-research-shelf--row-press (model row)
"Select ROW as the single current record in MODEL."
(etaf-data-select-one (etaf-research-shelf--controller model)
(plist-get row :id)))
(defun etaf-research-shelf--reading-list-view (model form)
"Return the main reading list for MODEL and static MAIN FORM."
(let ((controller (etaf-research-shelf--controller model)))
(etaf-view
(column :class "research-shelf-list" :width 'stretch
:padding '(1 2)
:border (etaf-research-shelf--color model :line)
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel)
(flex :width 'stretch :align-items 'center :gap '(1 (8))
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
(label :text (etaf-research-shelf--static-value form :title
"Reading queue")
:face 'bold
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel))
(label :text (format "%d items · SQLite-backed"
(or (etaf-value (etaf-data-total controller)) 0))
:color (etaf-research-shelf--color model :muted)
:bgcolor (etaf-research-shelf--color model :panel)))
(button :label " Add reading" :ref 'research-shelf-add
:variant 'secondary
:on-press
(lambda ()
(let ((id (etaf-value (plist-get model :next-id))))
(etaf-research-shelf--mutate
model 'insert
(list :id id :title (format "New reading %d" id)
:author "You" :kind "Notes" :status "unread"
:progress 0 :priority "Medium" :starred 0
:note "A new note waiting for your attention."
:updated "Just now")
"✓ Added to your shelf")
(setf (etaf-value (plist-get model :next-id)) (1+ id))))))
(data-grid
:controller controller
:columns '((:key :title :label "Title" :width 30)
(:key :author :label "Author" :width 16)
(:key :status :label "Status" :width 14)
(:key :progress :label "Progress" :width 10)
(:key :kind :label "Kind" :width 10))
:row-key (lambda (row) (plist-get row :id))
:row-ref (lambda (row)
(intern (format "research-shelf-row-%s"
(plist-get row :id))))
:row-selected-p
(lambda (row)
(etaf-data-selected-p controller (plist-get row :id)))
:on-row-press (lambda (row) (etaf-research-shelf--row-press model row))
:loading-label "◷ Loading library…"
:error-label "⚠ Could not read the shelf. Use Reload.")
(etaf-pagination :controller controller
:previous-ref 'research-shelf-page-previous
:next-ref 'research-shelf-page-next
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel))))))
(defun etaf-research-shelf--detail-view (model form)
"Return the selected record inspector for MODEL and static FORM."
(etaf-view
(column :class "research-shelf-detail" :width 'stretch
:padding '(2 2)
:border (etaf-research-shelf--color model :line)
:color (etaf-research-shelf--color model :ink)
:bgcolor (etaf-research-shelf--color model :panel)
(label :text (etaf-research-shelf--static-value form :title
"Selected item")
:color (etaf-research-shelf--color model :accent)
:bgcolor (etaf-research-shelf--color model :panel))
(text :face 'bold
(expr :value
(if-let ((row (etaf-research-shelf--selected model)))
(plist-get row :title)
"Choose a record")))
(text :color (etaf-research-shelf--color model :muted)
(expr :value
(if-let ((row (etaf-research-shelf--selected model)))
(format "%s · %s" (plist-get row :author)
(plist-get row :kind))
"Select one row to inspect it.")))
(spacer :height 1)
(text :face 'bold
(expr :value
(if-let ((row (etaf-research-shelf--selected model)))
(etaf-research-shelf--status-label (plist-get row :status))
"○ No selection")))
(text
(expr :value
(if-let ((row (etaf-research-shelf--selected model)))
(format "Progress %d%% · Priority %s"
(or (plist-get row :progress) 0)
(plist-get row :priority))
"Progress — · Priority —")))
(text :color (etaf-research-shelf--color model :accent)
(expr :value
(let* ((row (etaf-research-shelf--selected model))
(progress (or (and row (plist-get row :progress)) 0))
(filled (max 0 (min 10 (/ progress 10)))))
(format "%s %s" (make-string filled ?█)
(make-string (- 10 filled) ?░)))))
(spacer :height 1)
(text :color (etaf-research-shelf--color model :muted)
(expr :value
(if-let ((row (etaf-research-shelf--selected model)))
(concat "" (or (plist-get row :note) "No note yet.") "")
"Your notes and actions will appear here.")))
(spacer :height 1)
(etaf-research-shelf-detail-actions :model model))))
(etaf-define-component etaf-research-shelf-detail-actions (&key model)
"Render stable selected-record actions for MODEL."
:setup
(let ((model (etaf-current-prop :model)))
(lambda ()
(let* ((row (etaf-research-shelf--selected model))
(progress (or (and row (plist-get row :progress)) 0))
(finished (and row (equal (plist-get row :status) "finished")))
(archived (and row (equal (plist-get row :status) "archived"))))
(etaf-view
(flex :width 'stretch :flex-wrap 'wrap :gap '(1 (6))
(button :label "+ 10%" :ref 'research-shelf-progress
:variant 'secondary :disabled (or (null row) (>= progress 100))
:on-press
(lambda ()
(when-let* ((current (etaf-research-shelf--selected model))
(value (or (plist-get current :progress) 0)))
(etaf-research-shelf--update-selected
model (list :progress (min 100 (+ value 10))
:status (if (>= (+ value 10) 100)
"finished" "reading")
:updated "Just now")
"✓ Progress saved"))))
(button :label "✓ Finish" :ref 'research-shelf-finish
:variant 'secondary :disabled (or (null row) finished)
:on-press
(lambda ()
(etaf-research-shelf--update-selected
model '(:progress 100 :status "finished" :updated "Just now")
"✓ Marked finished")))
(button :label "★ Star" :ref 'research-shelf-star :variant 'ghost
:disabled (null row)
:on-press
(lambda ()
(when-let ((current (etaf-research-shelf--selected model)))
(let ((current-starred
(= 1 (or (plist-get current :starred) 0))))
(etaf-research-shelf--update-selected
model (list :starred (if current-starred 0 1)
:updated "Just now")
(if current-starred
"Removed star" "★ Starred locally"))))))
(button :label "Archive" :ref 'research-shelf-archive
:variant 'ghost :disabled (or (null row) archived)
:on-press
(lambda ()
(etaf-research-shelf--update-selected
model '(:status "archived" :updated "Just now")
"↗ Archived")))))))))
(etaf-define-component etaf-research-shelf-filter-rail
(&key model static-form)
"Render the reusable Research Shelf filter rail.
The rail is a Component boundary so its button Components remain ordinary
structural children instead of leaking through a direct material `expr'."
:setup
(let ((model (etaf-current-prop :model))
(static-form (etaf-current-prop :static-form)))
(lambda () (etaf-research-shelf--filter-view model static-form))))
(etaf-define-component etaf-research-shelf-reading-list
(&key model static-form)
"Render the reusable Research Shelf reading list."
:setup
(let ((model (etaf-current-prop :model))
(static-form (etaf-current-prop :static-form)))
(lambda () (etaf-research-shelf--reading-list-view model static-form))))
(etaf-define-component etaf-research-shelf-detail-inspector
(&key model static-form)
"Render the reusable Research Shelf detail inspector."
:setup
(let ((model (etaf-current-prop :model))
(static-form (etaf-current-prop :static-form)))
(lambda () (etaf-research-shelf--detail-view model static-form))))
(etaf-define-component etaf-research-shelf-shell (&key model static-form)
"Compose the SQLite-backed Research Shelf application."
:styles
(styles
(".research-shelf-shell" :width stretch)
(".research-shelf-header" :width stretch :padding (1 0))
(".research-shelf-filter-rail" :width stretch)
(".research-shelf-list" :width stretch)
(".research-shelf-detail" :width stretch)
(".research-shelf-footer" :width stretch :padding (0 1)))
:setup
(let ((model (etaf-current-prop :model))
(static-form (etaf-current-prop :static-form)))
(etaf-on-unmounted
(lambda () (etaf-data-stop (etaf-research-shelf--controller model))))
(lambda ()
(let* ((header (etaf-research-shelf--static-child static-form 'header))
(filters (etaf-research-shelf--static-child static-form 'filters))
(main (etaf-research-shelf--static-child static-form 'main))
(library (etaf-research-shelf--static-child main 'library))
(detail (etaf-research-shelf--static-child main 'detail))
(dark (etaf-value (plist-get model :dark)))
(palette (etaf-research-shelf--theme model)))
(etaf-view
(column :class "research-shelf-shell" :width '(viewport)
:height '(viewport-height)
:color (plist-get palette :ink)
:bgcolor (plist-get palette :paper)
(flex :class "research-shelf-header" :width 'stretch
:align-items 'center :gap '(1 (10))
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
(label :text (etaf-research-shelf--static-value
header :eyebrow "Research Shelf")
:color (plist-get palette :accent)
:bgcolor (plist-get palette :paper))
(label :text (etaf-research-shelf--static-value
static-form :title "Research Shelf") :face 'bold
:color (plist-get palette :ink)
:bgcolor (plist-get palette :paper))
(label :text (etaf-research-shelf--static-value
static-form :subtitle "A quiet place for unfinished ideas")
:color (plist-get palette :muted)
:bgcolor (plist-get palette :paper)))
(checkbox :label "Dark" :ref 'research-shelf-theme-toggle
:checked dark
:on-change (lambda (value)
(setf (etaf-value (plist-get model :dark)) value)
(setf (etaf-value (plist-get model :toast))
(if value "Dark theme" "Light theme")))))
(grid :class "research-shelf-workspace" :width 'stretch
:grid-template-columns '(2fr 5fr 3fr) :gap '(1 (10))
:color (plist-get palette :ink)
:bgcolor (plist-get palette :paper)
(etaf-research-shelf-filter-rail :model model :static-form filters)
(etaf-research-shelf-reading-list :model model :static-form library)
(etaf-research-shelf-detail-inspector :model model :static-form detail))
(flex :class "research-shelf-footer" :width 'stretch :gap '(1 (8))
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
(label :text (or (etaf-value (plist-get model :toast)) "Ready")
:color (plist-get palette :ink)
:bgcolor (plist-get palette :paper)))
(label :text (file-name-nondirectory
(plist-get model :database-file))
:color (plist-get palette :muted)
:bgcolor (plist-get palette :paper)))))))))
(defun etaf-research-shelf-view (model static-form)
"Return the root View for MODEL and validated STATIC-FORM."
(etaf-view (research-shelf-shell :model model :static-form static-form)))
(defun etaf-research-shelf-root (static-form)
"Consume validated STATIC-FORM and return one owned application root View."
(unless (eq (car static-form) 'research-shelf-shell)
(error "Unsupported Research Shelf static root: %S" static-form))
(let ((model (etaf-research-shelf--create-model)))
;; Load before the first retained generation. A DataGrid that is created
;; empty and populated from an on-mounted callback has no committed
;; material artifact to use as its first incremental anchor.
(etaf-research-shelf--load model "✓ SQLite shelf ready")
(lambda () (etaf-research-shelf-view model static-form))))
;;;###autoload
(defun etaf-research-shelf-open (&optional buffer-name)
"Mount and optionally display the Research Shelf application in BUFFER-NAME."
(interactive)
(let ((buffer (etaf-mount (or buffer-name "*ETAF Research Shelf*")
(etaf-research-shelf-root
(etaf-playground-read-static "research-shelf")))))
(when (called-interactively-p 'interactive) (pop-to-buffer buffer))
buffer))
(provide 'etaf-research-shelf)
;;; research-shelf.el ends here

View File

@ -0,0 +1,16 @@
;; Static composition only. Storage, state, Components, and events live in
;; the same-basename `.el' companion.
(research-shelf-shell
:title "Research Shelf"
:subtitle "A quiet place for unfinished ideas"
(header :eyebrow "A quiet place for unfinished ideas")
(filters
(filter :key all :label "All")
(filter :key reading :label "In progress")
(filter :key unread :label "Unread")
(filter :key finished :label "Finished")
(filter :key starred :label "★ Starred"))
(main
(library :title "Reading queue")
(detail :title "Selected item"))
(footer :label "SQLite · saved locally"))

View File

@ -0,0 +1,87 @@
;;; benchmark-research-shelf.el --- Research Shelf latency gate -*- lexical-binding: t; -*-
;;; Commentary:
;; Reproducible fullscreen-equivalent warm interaction benchmark. The process
;; exits nonzero when the declared p50/max budgets are exceeded.
;;; Code:
(require 'cl-lib)
(require 'etaf-playground)
(defconst etaf-research-shelf-benchmark-row-p50-budget-ms 100.0)
(defconst etaf-research-shelf-benchmark-row-max-budget-ms 250.0)
(defconst etaf-research-shelf-benchmark-theme-p50-budget-ms 250.0)
(defconst etaf-research-shelf-benchmark-theme-max-budget-ms 500.0)
(defun etaf-research-shelf-benchmark--percentile (samples percentile)
"Return PERCENTILE from numeric SAMPLES using nearest rank."
(let* ((ordered (sort (copy-sequence samples) #'<))
(index (min (1- (length ordered))
(floor (* percentile (length ordered))))))
(nth index ordered)))
(defun etaf-research-shelf-benchmark--measure (runtime refs runs)
"Measure warm press events through RUNTIME while cycling REFS.
The number of measured events is `RUNS'."
(cl-loop for index below runs
for ref = (nth (% index (length refs)) refs)
collect
(let ((started (float-time)))
(etaf-dispatch-event runtime ref 'press)
(* 1000.0 (- (float-time) started)))))
(defun etaf-research-shelf-benchmark--summary (label samples)
"Print and return LABEL summary for SAMPLES."
(let ((p50 (etaf-research-shelf-benchmark--percentile samples 0.5))
(maximum (apply #'max samples)))
(princ (format "%s p50=%.3fms max=%.3fms samples=%S\n"
label p50 maximum samples))
(list :p50 p50 :max maximum)))
(defun etaf-research-shelf-benchmark-run ()
"Run the Research Shelf evaluator and return non-nil on success."
(load-file (expand-file-name "examples/research-shelf.el"
default-directory))
(let ((database (make-temp-file "etaf-research-shelf-perf-" nil ".sqlite"))
(buffer " *etaf-research-shelf-perf*"))
(unwind-protect
(let ((etaf-research-shelf-database-file database))
(ignore etaf-research-shelf-database-file)
(etaf-playground-open buffer)
(ebox-surface-update-buffer-viewport (get-buffer buffer) 1413 62)
(let ((runtime (etaf-runtime-for-buffer buffer)))
;; Prewarm both structural states before collecting evidence.
(etaf-dispatch-event runtime 'research-shelf-row-1 'press)
(etaf-dispatch-event runtime 'research-shelf-row-2 'press)
(etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press)
(etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press)
(let* ((row (etaf-research-shelf-benchmark--summary
"row-selection"
(etaf-research-shelf-benchmark--measure
runtime '(research-shelf-row-1 research-shelf-row-2) 8)))
(theme (etaf-research-shelf-benchmark--summary
"theme-toggle"
(etaf-research-shelf-benchmark--measure
runtime '(research-shelf-theme-toggle) 8)))
(pass
(and (<= (plist-get row :p50)
etaf-research-shelf-benchmark-row-p50-budget-ms)
(<= (plist-get row :max)
etaf-research-shelf-benchmark-row-max-budget-ms)
(<= (plist-get theme :p50)
etaf-research-shelf-benchmark-theme-p50-budget-ms)
(<= (plist-get theme :max)
etaf-research-shelf-benchmark-theme-max-budget-ms))))
(princ (format "research-shelf-perf %s\n"
(if pass "PASS" "FAIL")))
(unless pass
(error "Research Shelf interaction latency budget exceeded"))
t)))
(when (get-buffer buffer)
(etaf-playground-close buffer))
(when (file-exists-p database)
(delete-file database)))))
(etaf-research-shelf-benchmark-run)
;;; benchmark-research-shelf.el ends here

View File

@ -1,33 +1,79 @@
;;; etaf-playground-tests.el --- operations-console pair contract -*- lexical-binding: t; -*- ;;; etaf-playground-tests.el --- Research Shelf pair contract -*- lexical-binding: t; -*-
;;; Commentary:
;; These tests exercise the generic pair boundary and one real application.
;; The framework tests never reach into the application's model; application
;; tests drive only public refs/events and observe the rendered surface.
;;; Code: ;;; Code:
(require 'cl-lib)
(require 'ert) (require 'ert)
(require 'etaf-playground) (require 'etaf-playground)
(defvar etaf-operations-console-cleanup-count)
(defun etaf-playground-test--ensure-app-loaded ()
"Load the same-basename Research Shelf companion for test setup."
(unless (featurep 'etaf-research-shelf)
(load-file (expand-file-name "examples/research-shelf.el"
default-directory))))
(defun etaf-playground-test--text (buffer) (defun etaf-playground-test--text (buffer)
"Return BUFFER plain text." "Return BUFFER's plain rendered text."
(with-current-buffer buffer (substring-no-properties (buffer-string)))) (with-current-buffer buffer
(substring-no-properties (buffer-string))))
(defun etaf-playground-test--close (buffer) (defun etaf-playground-test--close (buffer)
"Close BUFFER when it exists." "Close BUFFER when it exists."
(when (get-buffer buffer) (etaf-playground-close buffer))) (when (get-buffer buffer)
(etaf-playground-close buffer)))
(ert-deftest etaf-playground-manifest-has-one-reviewed-pair () (defmacro etaf-playground-test--with-app (variables &rest body)
"Expose only the operations-console pair and keep old examples inactive." "Run BODY with VARIABLES.
(should (equal '("operations-console") etaf-playground-example-names)) VARIABLES is a `(BUFFER DATABASE)' list; the macro creates a temporary SQLite
database and mounts a test buffer before running BODY."
(declare (indent 1))
(let ((buffer (car variables))
(database (cadr variables)))
(unless (and (symbolp buffer) (symbolp database))
(error "Expected (BUFFER DATABASE) variables, got %S" variables))
`(let* ((,database (make-temp-file "etaf-research-shelf-" nil ".sqlite"))
(,buffer " *etaf-research-shelf-test*"))
(ignore ,database)
(etaf-playground-test--ensure-app-loaded)
(unwind-protect
(let ((etaf-research-shelf-database-file ,database))
(ignore etaf-research-shelf-database-file)
,@body)
(etaf-playground-test--close ,buffer)
(when (file-exists-p ,database)
(delete-file ,database))))))
(ert-deftest etaf-playground-manifest-describes-one-pair ()
"Expose one reviewed pair through a generic manifest."
(should (equal '("research-shelf") etaf-playground-example-names))
(should (= 1 (length etaf-playground-scenario-manifest))) (should (= 1 (length etaf-playground-scenario-manifest)))
(let ((entry (car etaf-playground-scenario-manifest)) (let ((entry (car etaf-playground-scenario-manifest))
(framework (with-temp-buffer
(insert-file-contents "etaf-playground.el")
(buffer-string)))
(catalog (with-temp-buffer
(insert-file-contents "etaf-playground-catalog.el")
(buffer-string)))
(makefile (with-temp-buffer (makefile (with-temp-buffer
(insert-file-contents "Makefile") (buffer-string)))) (insert-file-contents "Makefile")
(dolist (key '(:pair :root-component :capabilities :refs (buffer-string))))
:gui-checkpoints :performance)) (dolist (key '(:pair :root-component :companion-feature :static-tags
:capabilities :refs :gui-checkpoints :performance))
(should (plist-member entry key))) (should (plist-member entry key)))
(should (string-match-p (should (string-match-p
"EXAMPLE_EL := examples/operations-console.el" makefile)) "EXAMPLE_EL := examples/research-shelf.el" makefile))
(should-not (string-match-p "wildcard examples" makefile)))) (should-not (string-match-p "wildcard examples" makefile))
(should-not (string-match-p "require.*etaf-sqlite" framework))
(should-not (string-match-p "research-shelf" framework))
(should-not (string-match-p "operations-console" framework))
(should (string-match-p "research-shelf" catalog))))
(ert-deftest etaf-playground-compile-builds-bytecode-dependencies () (ert-deftest etaf-playground-compile-builds-the-dependency-graph ()
"Integration builds must not silently fall back to interpreted dependencies." "Integration builds must compile the framework's dependency graph first."
(let ((makefile (with-temp-buffer (let ((makefile (with-temp-buffer
(insert-file-contents "Makefile") (insert-file-contents "Makefile")
(buffer-string)))) (buffer-string))))
@ -35,351 +81,136 @@
"$(MAKE) -C ../tp compile" "$(MAKE) -C ../tp compile"
"$(MAKE) -C ../ebox compile" "$(MAKE) -C ../ebox compile"
"$(MAKE) -C ../etaf compile" "$(MAKE) -C ../etaf compile"
"$(MAKE) -C ../etaf-ui compile")) "$(MAKE) -C ../etaf-ui compile"
"$(MAKE) -C ../etaf-sqlite compile"))
(should (string-match-p (regexp-quote dependency) makefile))))) (should (string-match-p (regexp-quote dependency) makefile)))))
(ert-deftest etaf-playground-static-reader-is-inert-and-strict () (ert-deftest etaf-playground-static-reader-is-inert-and-strict ()
"Read static data without companion execution and reject unsafe AST nodes." "Read pair structure as inert data and reject executable AST nodes."
(when (featurep 'etaf-operations-console) (let* ((form (etaf-playground-read-static "research-shelf"))
(unload-feature 'etaf-operations-console t)) (tags (plist-get (etaf-playground-scenario "research-shelf")
(let ((form (etaf-playground-read-static "operations-console"))) :static-tags)))
(should (equal (car form) 'operations-console-shell)) (should (equal (car form) 'research-shelf-shell))
(should (equal (plist-get (cdr form) :title) "Operations Console")) (should (equal (plist-get (cdr form) :title) "Research Shelf"))
(should (equal (mapcar #'car (should (equal (mapcar #'car
(cl-remove-if-not #'consp (cdr form))) (cl-remove-if-not #'consp (cdr form)))
'(header navigation main status)))) '(header filters main footer)))
(should-not (featurep 'etaf-operations-console)) (let* ((filters (cl-find-if
(dolist (form '((lambda () 1) (eval '(message "x")) (lambda (entry) (and (consp entry)
(etaf--private) (ebox--private) (shell-command "x"))) (eq (car entry) 'filters)))
(should-error (etaf-playground--validate-static-node form) :type 'error))) (cdr form)))
(all-filter (cl-find-if
(lambda (entry) (and (consp entry)
(eq (car entry) 'filter)
(eq (plist-get (cdr entry) :key)
'all)))
(cdr filters))))
(should all-filter))
(dolist (unsafe '((lambda () 1) (eval '(message "x"))
(etaf--private) (ebox--private) (shell-command "x")))
(should-error (etaf-playground--validate-static-node unsafe tags)
:type 'error))))
(ert-deftest etaf-playground-pair-load-and-mount-is-repeatable () (ert-deftest etaf-playground-pair-mounts-sqlite-backed-surface ()
"Load the same pair twice and mount/unmount it twice without stale state." "Mounting the pair initializes SQLite and renders the real app shell."
(let ((buffer " *etaf-operations-console-pair-test*")) (etaf-playground-test--with-app (buffer database)
(unwind-protect (etaf-playground-open buffer)
(dotimes (_iteration 2) (should (etaf-runtime-p (etaf-runtime-for-buffer buffer)))
(let ((form (etaf-playground-read-static "operations-console"))) (should (file-exists-p database))
(should (equal (car form) 'operations-console-shell)) (let ((text (etaf-playground-test--text buffer))
(should (equal (mapcar #'car (runtime (etaf-runtime-for-buffer buffer)))
(cl-remove-if-not #'consp (cdr form))) (should (string-match-p "Research Shelf" text))
'(header navigation main status)))) (should (string-match-p "8 items · SQLite-backed" text))
(etaf-playground-mount-example buffer "operations-console") (should (string-match-p "The Shape of Tools" text))
(should (etaf-runtime-p (etaf-runtime-for-buffer buffer))) (dolist (ref '(research-shelf-filter-all research-shelf-filter-reading
(should (string-match-p "Operations Console" research-shelf-filter-unread research-shelf-filter-finished
(etaf-playground-test--text buffer))) research-shelf-filter-starred research-shelf-reload
(etaf-unmount (etaf-runtime-for-buffer buffer)) research-shelf-add research-shelf-page-next
(when (get-buffer buffer) (kill-buffer buffer)) research-shelf-theme-toggle))
(should-not (etaf-runtime-for-buffer buffer))) (should (etaf-runtime-handler-for runtime ref))))))
(etaf-playground-test--close buffer))
(should-not (etaf-runtime-for-buffer buffer))))
(ert-deftest etaf-playground-operations-console-drives-public-refs () (ert-deftest etaf-playground-row-selection-is-repeatable ()
"Drive representative app capabilities through public event/focus ports." "Repeated DataGrid row presses replace one selected identity."
(let ((buffer " *etaf-operations-console-actions-test*")) (etaf-playground-test--with-app (buffer database)
(unwind-protect (etaf-playground-open buffer)
(progn (let ((runtime (etaf-runtime-for-buffer buffer)))
(dolist (entry (dolist (entry '((research-shelf-row-1 research-shelf-row-2
'((operations-console-alpha-increment) "The best tools make attention feel larger")
(operations-console-alpha-reset) (research-shelf-row-2 research-shelf-row-1
(operations-console-beta-increment) "Look again at the relationship between image and power")
(operations-console-beta-reset) (research-shelf-row-1 research-shelf-row-2
(operations-console-local-callback) "The best tools make attention feel larger")
(operations-console-named-action) (research-shelf-row-2 research-shelf-row-1
(operations-console-behavior-toggle) "Look again at the relationship between image and power")))
(operations-console-theme-toggle) (let ((selected (nth 0 entry))
(operations-console-nav-data) (other (nth 1 entry))
(operations-console-clear-selection operations-console-nav-data) (detail (nth 2 entry)))
(operations-console-row-1 operations-console-nav-data) (should (etaf-runtime-handler-for runtime selected))
(operations-console-row-2 operations-console-nav-data) (etaf-focus runtime selected)
(operations-console-row-3 operations-console-nav-data) (etaf-dispatch-event runtime selected 'press)
(operations-console-nav-resource) (let ((text (etaf-playground-test--text buffer)))
(operations-console-fail-next operations-console-nav-resource) (should (string-match-p (regexp-quote detail) text)))
(operations-console-resource-reload operations-console-nav-resource) (should (string-match-p
(operations-console-error-boundary operations-console-nav-resource) "selected"
(operations-console-nav-overview))) (plist-get (etaf-runtime-host-props-for runtime selected)
(etaf-playground-open buffer) :class)))
(let* ((runtime (etaf-runtime-for-buffer buffer)) (should-not (string-match-p
(ref (car entry)) "selected"
(page-ref (cadr entry))) (plist-get (etaf-runtime-host-props-for runtime other)
(when page-ref :class))))))))
(etaf-dispatch-event runtime page-ref 'press))
(etaf-focus runtime ref)
(should (eq ref (etaf-focused-host-ref runtime)))
(etaf-dispatch-event runtime ref 'press)
(when (eq ref 'operations-console-theme-toggle)
(should (string-match-p
"☑[[:space:]]+Theme"
(etaf-playground-test--text buffer))))
(etaf-playground-close buffer))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-data-grid-row-selection-is-repeatable () (ert-deftest etaf-playground-filters-and-pagination-reload-data ()
"Repeated row presses replace the single selected row without a bar." "Filter and pager refs drive the SQLite-backed Data Controller."
(let ((buffer " *etaf-operations-console-row-selection-test*")) (etaf-playground-test--with-app (buffer database)
(unwind-protect (etaf-playground-open buffer)
(progn (let ((runtime (etaf-runtime-for-buffer buffer)))
(etaf-playground-open buffer) (etaf-dispatch-event runtime 'research-shelf-filter-reading 'press)
(let ((runtime (etaf-runtime-for-buffer buffer))) (let ((text (etaf-playground-test--text buffer)))
(etaf-dispatch-event runtime 'operations-console-nav-data 'press) (should (string-match-p "Showing Reading" text))
(dolist (entry '((operations-console-row-1 . "Selected Ada") (should (string-match-p "Designing for Calm" text))
(operations-console-row-2 . "Selected Grace") (should-not (string-match-p "Ways of Seeing" text)))
(operations-console-row-3 . "Selected Lin") (etaf-dispatch-event runtime 'research-shelf-filter-all 'press)
(operations-console-row-1 . "Selected Ada") (etaf-dispatch-event runtime 'research-shelf-page-next 'press)
(operations-console-row-2 . "Selected Grace"))) (should (string-match-p "Page 2 / 2"
(etaf-focus runtime (car entry)) (etaf-playground-test--text buffer)))
(etaf-dispatch-event runtime (car entry) 'press) (etaf-dispatch-event runtime 'research-shelf-page-previous 'press)
(let ((text (etaf-playground-test--text buffer))) (should (string-match-p "Page 1 / 2"
(should (string-match-p (regexp-quote (cdr entry)) text)) (etaf-playground-test--text buffer))))))
(should-not (string-match-p "#DCEBDD" text))))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-behavior-toggle-uses-one-retained-publication () (ert-deftest etaf-playground-mutations-persist-and-refresh ()
"Behavior content and paint changes stay in two fixed owners per turn." "Add and mutate actions persist through the SQLite source and refresh UI."
(let ((buffer " *etaf-operations-console-behavior-retained-test*")) (etaf-playground-test--with-app (buffer database)
(unwind-protect (etaf-playground-open buffer)
(progn (let ((runtime (etaf-runtime-for-buffer buffer)))
(etaf-playground-open buffer) (etaf-dispatch-event runtime 'research-shelf-row-1 'press)
(let* ((runtime (etaf-runtime-for-buffer buffer)) (etaf-dispatch-event runtime 'research-shelf-progress 'press)
(surface (with-current-buffer (should (string-match-p "Progress saved"
buffer ebox-surface--buffer-surface)) (etaf-playground-test--text buffer)))
(old-commit (symbol-function 'ebox-commit)) (etaf-dispatch-event runtime 'research-shelf-star 'press)
(root-id (plist-get (should (string-match-p "Removed star"
(plist-get (ebox--buffer-render-state buffer) (etaf-playground-test--text buffer)))
:root-node) (etaf-dispatch-event runtime 'research-shelf-add 'press)
:node-id))) (should (string-match-p "Added to your shelf"
(dotimes (index 4) (etaf-playground-test--text buffer)))
(let ((commits 0) (should (file-exists-p database)))))
(before (etaf-runtime-generation runtime)))
(cl-letf (((symbol-function 'ebox-commit)
(lambda (&rest args)
(cl-incf commits)
(apply old-commit args))))
(etaf-dispatch-event runtime
'operations-console-behavior-toggle
'press))
(let ((report (ebox-buffer-update-report buffer)))
(should (= commits 1))
(should (= 1 (- (etaf-runtime-generation runtime)
before)))
(should (eq (plist-get report :projection-kind)
'owner-scoped))
(should-not (plist-get report :tp-full-root))
(should-not (plist-get report :tp-scope-fallback))
(should (= 2 (plist-get report :tp-scope-count)))
(should (= 2 (plist-get report :tp-scope-range-count)))
(should (= 2 (plist-get report :tp-text-operations)))
(should-not (member root-id (plist-get report :owner-ids)))
(should (< (plist-get report :reconciled-objects)
(plist-get (tp-surface-inspect surface)
:object-count))))
(should (string-match-p
(if (cl-evenp index)
"Behavior toggle: on"
"Behavior toggle: off")
(etaf-playground-test--text buffer)))))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-a-theme-toggle-uses-mixed-owner-publication () (ert-deftest etaf-playground-theme-and-lifecycle-are-repeatable ()
"Theme paint plus fixed-slot label changes publish through one mixed owner." "Theme changes and reset/close do not leave a stale mounted runtime."
(let ((buffer " *etaf-operations-console-theme-mixed-test*")) (etaf-playground-test--with-app (buffer database)
(unwind-protect (etaf-playground-open buffer)
(progn (let ((runtime (etaf-runtime-for-buffer buffer)))
(etaf-playground-open buffer) (etaf-dispatch-event runtime 'research-shelf-row-1 'press)
(let* ((runtime (etaf-runtime-for-buffer buffer)) (dotimes (_ 4)
(before (etaf-runtime-generation runtime)) (etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press))
(commits 0) (should (string-match-p "The Shape of Tools"
(old-commit (symbol-function 'ebox-commit))) (etaf-playground-test--text buffer)))
(cl-letf (((symbol-function 'ebox-commit) (should (or (string-match-p "☑ Dark"
(lambda (&rest args) (etaf-playground-test--text buffer))
(cl-incf commits) (string-match-p "☐ Dark"
(apply old-commit args)))) (etaf-playground-test--text buffer)))))
(etaf-dispatch-event runtime (etaf-playground-reset buffer)
'operations-console-theme-toggle (should (etaf-runtime-p (etaf-runtime-for-buffer buffer))))
'press)) (should-not (get-buffer " *etaf-research-shelf-test*")))
(let ((report (ebox-buffer-update-report buffer)))
(should (= commits 1))
(should (= 1 (- (etaf-runtime-generation runtime) before)))
(should (eq (plist-get report :projection-kind)
'mixed-owner-reflow))
(should-not (plist-get report :tp-full-root))
(should-not (plist-get report :tp-scope-fallback))
(should-not
(member (ebox--buffer-root-node-id buffer)
(plist-get report :owner-ids)))
(should (string-match-p "☑[[:space:]]+Theme"
(etaf-playground-test--text buffer)))
(should (string-match-p "Theme: Dark"
(etaf-playground-test--text buffer))))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-a-theme-toggle-after-navigation-retires-nested-range-effects ()
"Theme changes after page navigation must not reuse removed Range anchors."
(let ((buffer " *etaf-operations-console-theme-after-navigation-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(let* ((runtime (etaf-runtime-for-buffer buffer))
(before (etaf-runtime-generation runtime)))
(etaf-dispatch-event runtime 'operations-console-nav-data 'press)
(dotimes (_ 4)
(etaf-dispatch-event runtime
'operations-console-theme-toggle 'press))
(should (= 5 (- (etaf-runtime-generation runtime) before)))
(should (string-match-p "Data desk"
(etaf-playground-test--text buffer)))
(should (string-match-p "Theme: Light"
(etaf-playground-test--text buffer)))
(should (plist-get (ebox-buffer-update-report buffer)
:strategy))
(let* ((generation (etaf-runtime-current-generation runtime))
(nodes (etaf-generation-semantic-nodes generation))
(stale 0))
(dotimes (effect-id (etaf-runtime-next-effect-id runtime))
(when-let ((effect (etaf--generation-effect
generation effect-id)))
(unless (etaf--pvec-get
nodes
(etaf--generation-effect-semantic-id effect))
(cl-incf stale))))
(should (zerop stale)))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-repeated-counter-press-retains-handler ()
"Recreated keyed counter Hosts keep their public handler across turns."
(let ((buffer " *etaf-operations-console-repeat-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(let ((runtime (etaf-runtime-for-buffer buffer))
(old-commit (symbol-function 'ebox-commit)))
(dotimes (expected 3)
(let ((commits 0)
(before (etaf-runtime-generation runtime)))
(cl-letf (((symbol-function 'ebox-commit)
(lambda (&rest args)
(cl-incf commits)
(apply old-commit args))))
(etaf-dispatch-event runtime
'operations-console-alpha-increment 'press))
(should (= commits 1))
(should (= 1 (- (etaf-runtime-generation runtime) before))))
(should (etaf-runtime-handler-for
runtime 'operations-console-alpha-increment))
(should (string-match-p
(format "Value: %d" (1+ expected))
(etaf-playground-test--text buffer))))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-reset-and-close-use-pair-boundary ()
"Reset the sole pair and leave no mounted Runtime after close."
(let ((buffer " *etaf-operations-console-reset-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(etaf-playground-reset buffer)
(should (etaf-runtime-for-buffer buffer))
(etaf-playground-close buffer)
(should-not (get-buffer buffer)))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-navigation-swaps-one-page-subtree ()
"Show exactly the selected Overview, Data, or Resource page."
(let ((buffer " *etaf-operations-console-navigation-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(let* ((runtime (etaf-runtime-for-buffer buffer))
(root-id
(plist-get
(plist-get (ebox--buffer-render-state buffer)
:root-node)
:node-id)))
(should (string-match-p "Counter alpha"
(etaf-playground-test--text buffer)))
(etaf-dispatch-event runtime 'operations-console-nav-data 'press)
(should (string-match-p "Data Controller"
(etaf-playground-test--text buffer)))
(let ((report (ebox-buffer-update-report buffer)))
(should-not (plist-get report :tp-full-root))
(should-not (plist-get report :tp-scope-fallback))
(should (plist-get report :range-metrics))
(should-not (member root-id (plist-get report :owner-ids)))
(should (= 1 (plist-get report :tp-scope-range-count))))
(should-not (string-match-p "Counter alpha"
(etaf-playground-test--text buffer)))
(etaf-dispatch-event runtime 'operations-console-nav-resource 'press)
(should (string-match-p "Resource and Error Boundary"
(etaf-playground-test--text buffer)))
(should-not (string-match-p "Data Controller"
(etaf-playground-test--text buffer)))
(etaf-dispatch-event runtime 'operations-console-nav-overview 'press)
(should (string-match-p "Counter alpha"
(etaf-playground-test--text buffer)))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-controls-use-intrinsic-single-line-widths ()
"Keep navigation and action controls on their owning row without clipping."
(let ((buffer " *etaf-operations-console-control-width-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(let ((runtime (etaf-runtime-for-buffer buffer)))
(let ((nav (etaf-host-ref-bounds runtime 'operations-console-nav))
(main (etaf-host-ref-bounds runtime 'operations-console-main))
(status (etaf-host-ref-bounds runtime
'operations-console-status)))
;; These are vertical shell siblings. If the navigation flex
;; group is accidentally left open, MAIN and STATUS become row
;; children and the whole application is laid out horizontally.
(with-current-buffer buffer
(should (< (line-number-at-pos (car nav))
(line-number-at-pos (car main))))
(should (< (line-number-at-pos (car main))
(line-number-at-pos (car status))))))
(dolist (refs '((operations-console-nav-overview
operations-console-nav-data
operations-console-nav-resource
operations-console-theme-toggle)
(operations-console-local-callback
operations-console-named-action
operations-console-behavior-toggle)))
(let (lines)
(dolist (ref refs)
(let ((bounds (etaf-host-ref-bounds runtime ref)))
(should bounds)
(with-current-buffer buffer
(push (line-number-at-pos (car bounds)) lines)
(should (<= (cdr bounds) (point-max))))))
(should (= 1 (length (delete-dups lines))))
;; The navigation owner declares a pixel gap so adjacent
;; mouse-face ranges remain distinct controls.
(should (equal
(plist-get
(gethash 'operations-console-nav
(etaf-runtime-host-props runtime))
:gap)
'(0 (8))))))))
(etaf-playground-test--close buffer))))
(ert-deftest etaf-playground-fixed-viewport-layout-does-not-double-width ()
"Keep every rendered line inside the containing viewport after reflow."
(let ((buffer " *etaf-operations-console-width-test*"))
(unwind-protect
(progn
(etaf-playground-open buffer)
(ebox-surface-update-buffer-viewport (get-buffer buffer) 1413 60)
(with-current-buffer buffer
(let (widths)
(goto-char (point-min))
(while (< (point) (point-max))
(push (ebox--string-pixel-width
(buffer-substring (point) (line-end-position)))
widths)
(forward-line 1))
(setq widths (nreverse widths))
(should (= 60 (length widths)))
(should (<= (apply #'max widths) 1413)))))
(etaf-playground-test--close buffer))))
(provide 'etaf-playground-tests) (provide 'etaf-playground-tests)
;;; etaf-playground-tests.el ends here ;;; etaf-playground-tests.el ends here