Ship operations-console ETAF playground pair
This commit is contained in:
parent
082eff2f6c
commit
f128eba656
129
DESIGN.md
129
DESIGN.md
@ -1,82 +1,105 @@
|
||||
# Design
|
||||
|
||||
## Source of truth
|
||||
- Status: Active
|
||||
- Last refreshed: 2026-08-05
|
||||
- Primary product surfaces: `etaf-playground-open`, `etaf-playground-open-ui`, and the interactive `etaf-playground-open-showcase` application.
|
||||
- Evidence reviewed: `etaf-playground.el`, `tests/etaf-playground-tests.el`, `README.md`, `README.zh-CN.md`, the rendered current Showcase, and `../emacs-box/examples/playground/flex-reference.ebox` rendered fullscreen in GUI Emacs.
|
||||
|
||||
- Status: Active draft for the sole reviewed pair
|
||||
- Last refreshed: 2026-08-20
|
||||
- Primary product surface: `examples/operations-console.etaf` + `examples/operations-console.el`
|
||||
- Visual reference: `design/operations-console.html`
|
||||
- Evidence reviewed: current pair loader/manifest, `etaf-ui.el`, Ebox viewport contract, and the current GUI/performance evidence.
|
||||
|
||||
## Brand
|
||||
- Personality: Precise, calm, modern, technical, and intentionally crafted rather than framework-generic.
|
||||
- Trust signals: Exact alignment, readable contrast, stable geometry during updates, consistent semantic colors, and examples that visibly exercise the public ETAF model.
|
||||
- Avoid: Default-Emacs-looking unstyled text, saturated dashboard colors, pastel text on tinted backgrounds, decorative noise, huge empty regions, and controls that look indistinguishable from ordinary copy.
|
||||
|
||||
- Personality: calm operations cockpit; precise, warm, and quietly technical.
|
||||
- Trust signals: explicit status chips, visible last-updated time, deterministic counts, clear success/error feedback.
|
||||
- Avoid: generic toy dashboard, rainbow gradients, dense ungrouped controls, decorative widgets with no semantic state.
|
||||
|
||||
## Product goals
|
||||
- Goals: Make ETAF feel capable through a polished runnable application; teach View, Component, Context, Data, Grid, retained state, and events through visible structure; keep every example usable through public APIs only.
|
||||
- Non-goals: Browser imitation, bitmap artwork, a separate styling framework, hidden example-only rendering paths, or pretending the small current `etaf-ui` catalog is feature-complete.
|
||||
- Success signals: The first frame reads as a deliberate application; navigation, data changes, and theme changes preserve hierarchy and geometry; screenshots show no clipping, continuation indicators, unintended wrapping, or low-contrast text.
|
||||
|
||||
- Make one attractive app prove ETAF's Component, composition, reactive, Context/Theme, Behavior, event/Action, Data, Resource, error-boundary, focus, and lifecycle contracts.
|
||||
- Do not add another Playground scenario or hide framework limitations behind fake animation.
|
||||
- Keep the HTML prototype and ETAF rendering aligned in hierarchy, state labels, interaction outcomes, and compact/fullscreen geometry.
|
||||
|
||||
## Personas and jobs
|
||||
- Primary personas: ETAF application authors, Ebox/ETAF maintainers, and evaluators deciding whether the new architecture is usable.
|
||||
- User jobs: Understand the framework quickly, copy public patterns, inspect a realistic interactive flow, and verify that updates remain visually stable.
|
||||
- Key contexts of use: GUI Emacs in a single application buffer, fullscreen demonstrations, and batch/ERT verification.
|
||||
|
||||
- Primary personas: ETAF framework reviewer and application author learning the public API.
|
||||
- User jobs: understand the layout model, trigger each capability, inspect visible state changes, and verify recovery/cleanup behavior.
|
||||
- Key contexts: 900–940 px compact GUI, fullscreen GUI, keyboard-only review, and batch semantic tests.
|
||||
|
||||
## Information architecture
|
||||
- Primary navigation: A persistent horizontal command strip with Overview, Work items, and Theme destinations.
|
||||
- Core routes/screens: Overview dashboard, interactive Work items table, Theme and semantics gallery, compact core counter example, and optional `etaf-ui` catalog example.
|
||||
- Content hierarchy: Branded application header, persistent navigation, page title and supporting copy, primary cards/table, contextual action strip, and a quiet status bar.
|
||||
|
||||
- Shell: brand/header -> navigation tabs -> page surface -> persistent status footer.
|
||||
- Overview: hero/status strip, KPI cards, activity/control workspace, capability cards, and event timeline.
|
||||
- Data: query/selection summary, interactive DataGrid, and selection detail panel.
|
||||
- Resource: resource status, reload/fail-next controls, boundary result, and cleanup counter.
|
||||
- Content hierarchy: page title and status first; primary action second; diagnostics/details third.
|
||||
|
||||
## Design principles
|
||||
- Principle 1: Teach through visible grouping; every capability receives a labeled, bounded surface instead of becoming an undifferentiated text stream.
|
||||
- Principle 2: Use the Flex reference's warm restrained palette and one-pixel borders to create hierarchy without relying on shadows or browser-only effects.
|
||||
- Principle 3: Geometry is part of correctness; viewport-sized owners contain their borders, children fit the owner's content box, and updates must not introduce wrapping or truncation markers.
|
||||
- Tradeoffs: Prefer a dense desktop Emacs composition over excessive whitespace; prefer fixed readable card widths inside a responsive outer shell over fragile full-viewport arithmetic on every child.
|
||||
|
||||
1. One visual hierarchy: every control belongs to a named surface and every surface has a visible status.
|
||||
2. State before decoration: theme, loading, error, selected, disabled, and success states are readable as text as well as color.
|
||||
3. Stable geometry: controls remain single-line at compact width; page changes replace only the page surface.
|
||||
4. Public API truth: the prototype may use ordinary HTML/JS, but the ETAF port uses only public ETAF/ETAF-UI APIs.
|
||||
|
||||
## Visual language
|
||||
- Color: Warm canvas `#F8F5EE`; paper `#FFFDF8`; ink `#252A2E`; muted ink `#66706A`; active terracotta `#B84F35`; tinted terracotta `#F1D4C9`; sage `#DCEBDD`; blue `#D9EAF2`; violet `#E7E2F1`; dark mode uses deep neutral surfaces with lighter versions of the same semantic accents.
|
||||
- Typography: Emacs' configured monospace face; bold is reserved for titles, labels, key values, and active controls.
|
||||
- Spacing/layout rhythm: One-line vertical section rhythm, 12–16 px horizontal gaps, 16–24 px section padding, and aligned card/table edges.
|
||||
- Shape/radius/elevation: Square text-native surfaces, one-pixel borders, no fake radius or shadow.
|
||||
- Motion: State changes publish directly and remain stable after redisplay; no decorative animation.
|
||||
- Imagery/iconography: Text and restrained semantic glyphs only; no logos, emoji decoration, or bitmap chrome.
|
||||
|
||||
- Color: ink navy `#142235`, paper `#F6F1E8`, slate `#526174`, teal `#2E8B83`, coral `#E26D5A`, amber `#D99A3D`, success `#3E9B72`.
|
||||
- Typography: readable sans body, compact monospace labels/metrics, bold page titles.
|
||||
- Spacing/layout rhythm: one-line outer rhythm, two-column workspace, fixed gaps, full-width shell, deliberate card padding.
|
||||
- Shape/elevation: thin dark borders and restrained card elevation; no floating overlay is required by the ETAF port.
|
||||
- Motion: no animation is required; state changes are immediate and reduced-motion safe.
|
||||
- Imagery/iconography: text marks and Unicode status glyphs only; no external asset dependency.
|
||||
|
||||
## Components
|
||||
- Existing components to reuse: ETAF core Hosts, retained Components, Grid, Context theme values, Data Controller, and public event dispatch.
|
||||
- New/changed components: Showcase section headers, metric cards, navigation items, task rows, action buttons, color swatches, compact status bar, and polished wrappers for the core/UI examples.
|
||||
- Variants and states: Active navigation, selected task, enabled/disabled action, light/dark theme, success/review/open status, loading, empty, and error.
|
||||
- Token/component ownership: Palette tokens live in `etaf-playground--showcase-palette`; each helper owns the surfaces it renders; ETAF/Ebox remain the layout and rendering owners.
|
||||
|
||||
- Existing components to reuse: public `button`, `checkbox`, `label`, `panel`, and `data-grid` from `etaf-ui`.
|
||||
- New/changed components: `operations-console-shell`, `hero-status`, `metric-strip`, `activity-card`, `capability-card`, `timeline`, `data-page`, and `resource-page` in the companion.
|
||||
- Variants/states: active/inactive nav, light/dark theme, selected/unselected row, Behavior on/off, resource ready/loading/error, boundary handled, compact/fullscreen.
|
||||
- Ownership: `.etaf` owns static composition and section names; `.el` owns tokens, props, refs, resources, handlers, and state.
|
||||
|
||||
## Accessibility
|
||||
- Target standard: High-contrast keyboard-operable Emacs UI with explicit semantic roles and refs.
|
||||
- Keyboard/focus behavior: Every actionable surface keeps `:role 'button`, `:tab-index 0`, stable refs, and visible active/selected styling.
|
||||
- Contrast/readability: Every tinted surface has an explicit foreground; muted text remains readable on both canvas and paper; active controls use both color and border/background changes.
|
||||
- Screen-reader semantics: Preserve labels, roles, refs, and `aria-label` where the framework exposes them.
|
||||
- Reduced motion and sensory considerations: No animation is required; updates must remain usable with ordinary redisplay.
|
||||
|
||||
- Target: keyboard-complete, high-contrast text, semantic roles through public UI components.
|
||||
- Keyboard/focus: Tab and Shift-Tab traverse nav/actions/data rows; RET dispatches the focused ref; focus clears when a target is removed.
|
||||
- Every state has text feedback; color is never the only signal. No hover-only behavior.
|
||||
- Ebox Theme carries inherited Ebox defaults; application palette lookups remain
|
||||
in the companion, while the Renderer filters unknown token names before they
|
||||
reach Ebox geometry.
|
||||
|
||||
## Responsive behavior
|
||||
- Supported breakpoints/devices: GUI Emacs windows from approximately 900 px wide through fullscreen desktop widths.
|
||||
- Layout adaptations: The host measures the window body and reserves one character cell at the right edge; the outer shell owns viewport width; direct header/main/status surfaces stretch inside it; nested responsive containers omit width and let Ebox Flex allocate or wrap content.
|
||||
- Touch/hover differences: None; keyboard and mouse press paths share the public event model.
|
||||
|
||||
- Supported surfaces: compact 900–940 px GUI body and fullscreen desktop GUI.
|
||||
- Metric cards remain in one row when possible; workspace falls back from two columns to stacked sections; controls keep intrinsic single-line widths.
|
||||
- Use `flex` for rows that distribute remaining width; reserve `row` for fixed
|
||||
intrinsic control groups. The navigation control group is max-content so
|
||||
each hover/active range remains an independent control.
|
||||
- Mouse-1 and keyboard activation use the same public refs.
|
||||
|
||||
## Interaction states
|
||||
- Loading: Labeled bounded surface with visible status.
|
||||
- Empty: Quiet explanatory surface with the next useful action when available.
|
||||
- Error: High-contrast danger surface; errors remain visible instead of falling back silently.
|
||||
- Success: Sage accent and explicit feedback text.
|
||||
- Disabled: Muted foreground plus missing callback/tab stop.
|
||||
- Offline/slow network, if applicable: Not currently applicable because the example uses the synchronous memory source.
|
||||
|
||||
- Loading: resource card reports `Loading…` without moving the shell.
|
||||
- Empty: DataGrid reports an explicit empty state and keeps selection controls available.
|
||||
- Error: resource reload can fail once; the boundary card reports the handled error and recovery remains available.
|
||||
- Success: action/status and timestamp update visibly.
|
||||
- Disabled: unavailable actions expose disabled semantics rather than silently ignoring input.
|
||||
|
||||
## Content voice
|
||||
- Tone: Direct, technical, concise, and self-explaining.
|
||||
- Terminology: Use the canonical ETAF vocabulary: View, Component, Host, Context, Data Controller, Runtime, event, and public API.
|
||||
- Microcopy rules: Name the capability being demonstrated, state the visible effect of actions, and avoid promotional filler.
|
||||
|
||||
- Tone: concise, operational, and explanatory; labels name the framework capability being demonstrated.
|
||||
- Terminology: use `Component`, `Context`, `Theme`, `Behavior`, `Action`, `Data`, `Resource`, and `Boundary` consistently.
|
||||
- Microcopy: prefer a visible verb + result (`Reload resource`, `Handled: …`, `Selected Beacon`) over generic `Done`.
|
||||
|
||||
## Implementation constraints
|
||||
- Framework/styling system: Emacs 29.1+, ETAF public View/Component APIs, and Ebox public layout/style properties only.
|
||||
- Design-token constraints: Reuse the palette helper and semantic token keys; do not add a parallel theme or CSS layer.
|
||||
- Performance constraints: Interactive actions remain synchronous and bounded; visual polish must not add hidden full-tree work or timers beyond the existing viewport sync.
|
||||
- Compatibility constraints: `etaf-playground` must remain independent from `ebox-playground` and must not call `ebox--*` private APIs.
|
||||
- Test/screenshot expectations: `make check` passes; fullscreen GUI screenshots use one target buffer window; dynamic checks cover navigation, selection, insertion, theme toggle, and return to Overview; no visible truncation/continuation indicators or accidental wrapping are accepted.
|
||||
|
||||
- Framework/styling: ETAF public View DSL in `.etaf`/`.el`; Ebox renders the result; no HTML/CSS runtime dependency.
|
||||
- Design tokens: keep palette/spacing tokens in one companion source and reuse them; no scattered ad-hoc colors.
|
||||
- Performance: ordinary warm p50 <=100 ms, counter/resource p50 <=150 ms, every measured max <=250 ms; one publication per logical action.
|
||||
- Compatibility: preserve one same-basename `operations-console` pair, safe static reader, public API boundary, mount twice/unmount cleanup, and manifest refs.
|
||||
- Navigation: the stable page ref is read by the retained router Component; its
|
||||
page Host is the single semantic Range anchor, so page switches replace one
|
||||
Range payload instead of rebuilding the shell root.
|
||||
- Review: HTML is the visual reference; ETAF needs compact/fullscreen screenshots and dynamic keyboard/mouse checkpoints.
|
||||
|
||||
## Open questions
|
||||
- [ ] Expand the official `etaf-ui` catalog before presenting the catalog example as a migration-complete component gallery / maintainer / blocks parity claims.
|
||||
|
||||
- [ ] User review may adjust palette or page naming after the first HTML/ETAF comparison; behavior and public-ref coverage remain fixed.
|
||||
|
||||
131
DESIGN.zh-CN.md
131
DESIGN.zh-CN.md
@ -1,82 +1,95 @@
|
||||
# 设计规范
|
||||
# 设计
|
||||
|
||||
## 事实来源
|
||||
- 状态:生效中
|
||||
- 最近更新:2026-08-05
|
||||
- 主要产品界面:`etaf-playground-open`、`etaf-playground-open-ui` 与交互式应用 `etaf-playground-open-showcase`。
|
||||
- 已审阅证据:`etaf-playground.el`、`tests/etaf-playground-tests.el`、中英文 README、当前 Showcase 的真实 GUI 渲染,以及 `../emacs-box/examples/playground/flex-reference.ebox` 的全屏 GUI 渲染。
|
||||
## 来源与状态
|
||||
|
||||
- 状态:唯一 reviewed pair 的 active draft
|
||||
- 更新日期:2026-08-20
|
||||
- 主产品面:`examples/operations-console.etaf` + `examples/operations-console.el`
|
||||
- 视觉参考:`design/operations-console.html`
|
||||
- 已审查证据:pair loader/manifest、`etaf-ui.el`、Ebox viewport 合同及当前 GUI/性能证据。
|
||||
|
||||
## 品牌
|
||||
- 气质:精确、克制、现代、技术感强,并且明显经过设计,而不是通用框架默认外观。
|
||||
- 可信信号:精确对齐、清晰对比度、更新时稳定的几何结构、一致的语义颜色,以及能够真实演示 ETAF 公共模型的例子。
|
||||
- 避免:未加样式的默认 Emacs 文本、过度饱和的仪表盘颜色、浅色背景上的低对比文字、装饰噪音、无意义的大面积留白,以及与普通正文无法区分的控件。
|
||||
|
||||
- 气质:安静的操作控制台;准确、温暖、克制地体现技术感。
|
||||
- 信任信号:状态 chip、更新时间、明确计数、清晰的成功/错误反馈。
|
||||
- 避免:普通玩具 dashboard、彩虹渐变、无语义状态的装饰控件和杂乱控制区。
|
||||
|
||||
## 产品目标
|
||||
- 目标:通过精致、可运行的应用体现 ETAF 的能力;用可见结构讲清 View、Component、Context、Data、Grid、保留状态与事件;所有例子只使用公共 API。
|
||||
- 非目标:模仿浏览器、使用位图装饰、另建样式框架、加入仅供示例使用的隐藏渲染路径,或假装当前较小的 `etaf-ui` 目录已经功能完整。
|
||||
- 成功信号:首屏具有明确的应用结构;导航、数据和主题更新保持层级与几何稳定;截图中没有裁切、续行标记、意外换行或低对比文字。
|
||||
|
||||
## 用户与任务
|
||||
- 主要用户:ETAF 应用作者、Ebox/ETAF 维护者,以及评估新架构是否可用的人。
|
||||
- 用户任务:快速理解框架、复制公共 API 模式、检查真实交互流程,并确认更新后视觉结构稳定。
|
||||
- 主要环境:GUI Emacs 单应用缓冲区、全屏演示,以及批处理/ERT 验证。
|
||||
- 用一个好看的 app 同时展示 Component、组合、响应式、Context/Theme、Behavior、事件/Action、Data、Resource、错误边界、焦点和生命周期。
|
||||
- 不扩展第二个 Playground 场景,不伪装成完整生产分析产品,不用假动画掩盖框架问题。
|
||||
- HTML 原型和 ETAF 实现保持同一层级、状态文字、交互结果和 compact/fullscreen 几何。
|
||||
|
||||
## 信息架构
|
||||
- 主导航:固定的横向命令条,包含 Overview、Work items 与 Theme。
|
||||
- 核心界面:Overview 仪表盘、交互式 Work items 表格、Theme and semantics 色彩页、紧凑的核心计数器例子,以及可选的 `etaf-ui` 组件目录例子。
|
||||
- 内容层级:品牌应用标题、固定导航、页面标题与说明、主要卡片或表格、上下文操作区,以及安静的状态栏。
|
||||
|
||||
- Shell:品牌/header → 导航 tabs → page surface → 持久 status footer。
|
||||
- Overview:hero/status、KPI 卡片、活动/控制 workspace、能力卡片和事件时间线。
|
||||
- Data:选择摘要、可交互 DataGrid、selection detail。
|
||||
- Resource:资源状态、reload/fail-next、boundary 结果和 cleanup 计数。
|
||||
- 层级:先显示页面标题和状态,再显示主操作,最后显示诊断/细节。
|
||||
|
||||
## 设计原则
|
||||
- 原则一:通过可见分组教学;每项能力都放在有标题、有边界的区域中,而不是混成一串文本。
|
||||
- 原则二:沿用 Flex 参考示例的暖色、克制配色与一像素边框,不依赖阴影或浏览器专有视觉效果。
|
||||
- 原则三:几何正确性就是功能正确性;viewport owner 必须包含自身边框,子区域必须适配 owner 的内容盒,更新不得引入换行或截断标记。
|
||||
- 取舍:优先采用适合桌面 Emacs 的紧凑构图,而不是过量留白;内部响应式容器省略宽度,让 Ebox Flex 分配和换行,避免脆弱的多层 viewport 算术。
|
||||
|
||||
1. 一个视觉层级:每个控件属于有名字的 surface,每个 surface 都有可见状态。
|
||||
2. 状态优先于装饰:theme、loading、error、selected、disabled、success 都有文字表达。
|
||||
3. 几何稳定:compact 下控件保持单行,页面只替换 page surface。
|
||||
4. API 真实:HTML 可以用普通 HTML/JS,但 ETAF 只用公开 ETAF/ETAF-UI API。
|
||||
|
||||
## 视觉语言
|
||||
- 颜色:暖白画布 `#F8F5EE`;纸张 `#FFFDF8`;正文 `#252A2E`;弱化文字 `#66706A`;激活陶土色 `#B84F35`;淡陶土色 `#F1D4C9`;鼠尾草绿 `#DCEBDD`;蓝色 `#D9EAF2`;紫色 `#E7E2F1`;暗色模式使用深中性色表面和同一组语义色的亮色版本。
|
||||
- 字体:沿用 Emacs 配置的等宽字体;粗体只用于标题、标签、关键数值和激活控件。
|
||||
- 间距:纵向以一行作为节奏;横向间距 12–16px;区块内边距 16–24px;卡片与表格边缘对齐。
|
||||
- 形状与层级:方正的文本原生表面、一像素边框,不模拟圆角或阴影。
|
||||
- 动效:状态直接发布,并在 redisplay 后保持稳定;不使用装饰动画。
|
||||
- 图像与图标:只使用文字与克制的语义符号,不使用 logo、emoji 装饰或位图外框。
|
||||
|
||||
- 色彩:墨蓝 `#142235`、纸张 `#F6F1E8`、灰蓝 `#526174`、青绿 `#2E8B83`、珊瑚 `#E26D5A`、琥珀 `#D99A3D`、成功绿 `#3E9B72`。
|
||||
- 字体:易读 sans body,紧凑 monospace 指标/标签,粗体页面标题。
|
||||
- 节奏:一行外部节奏、双列 workspace、固定 gap、shell 全宽,卡片有明确内边距。
|
||||
- 形状:细深色边框、克制 elevation;ETAF 不依赖浮层。
|
||||
- 动效:不以动画为正确性前提,状态变化即时且支持 reduced motion。
|
||||
- 图形:只用文字标记和 Unicode 状态符号,不依赖外部资源。
|
||||
|
||||
## 组件
|
||||
- 复用能力:ETAF 核心 Hosts、保留式 Components、Grid、Context 主题值、Data Controller 与公共事件分发。
|
||||
- 新增或调整:Showcase 区块标题、指标卡、导航项、任务行、操作按钮、色板、紧凑状态栏,以及核心/UI 例子的精致外壳。
|
||||
- 状态:激活导航、选中任务、启用/禁用操作、亮色/暗色主题、success/review/open、loading、empty 与 error。
|
||||
- 所有权:色彩 token 位于 `etaf-playground--showcase-palette`;各 helper 拥有自己的展示表面;ETAF/Ebox 继续拥有布局与渲染。
|
||||
|
||||
## 可访问性
|
||||
- 目标:高对比、可用键盘操作的 Emacs UI,并保留明确的语义 role 与 ref。
|
||||
- 键盘与焦点:所有可操作表面保留 `:role 'button`、`:tab-index 0`、稳定 ref,以及可见的激活/选中样式。
|
||||
- 对比度:所有着色表面都有明确前景色;弱化文字在画布与纸张上都可读;激活控件同时使用颜色、边框或背景变化。
|
||||
- 语义:框架支持时保留 label、role、ref 与 `aria-label`。
|
||||
- 动效:不依赖动画,普通 redisplay 即可使用。
|
||||
- 复用公开 `button`、`checkbox`、`label`、`panel`、`data-grid`。
|
||||
- companion 组件:`operations-console-shell`、`hero-status`、`metric-strip`、`activity-card`、`capability-card`、`timeline`、`data-page`、`resource-page`。
|
||||
- 状态:active/inactive nav、light/dark theme、selected/unselected row、Behavior on/off、resource ready/loading/error、boundary handled、compact/fullscreen。
|
||||
- 所有权:`.etaf` 只放静态 composition 和 section 名称;`.el` 持有 token、props、refs、资源、handlers 和 state。
|
||||
|
||||
## 响应式行为
|
||||
- 支持范围:约 900px 宽的 GUI Emacs 窗口到全屏桌面宽度;验证时还使用 777px 可用宽度作为更严格下限。
|
||||
- 布局适配:宿主测量窗口正文,并在右侧预留一个字符单元;最外层 shell 拥有 viewport 宽度;直接的 header/main/status 区域在其中 stretch;嵌套响应式容器省略宽度,由 Ebox Flex 分配或换行。
|
||||
- 触摸/悬停:不适用;键盘与鼠标 press 共用公共事件模型。
|
||||
## 无障碍
|
||||
|
||||
- 目标:键盘完整、高对比文字,使用公开组件提供语义 role。
|
||||
- Tab/Shift-Tab 遍历 nav/action/data rows;RET 派发 focused ref;移除目标时清焦点。
|
||||
- 每个状态都有文字反馈,颜色不是唯一信号;不依赖 hover-only 行为。
|
||||
- Ebox Theme 承载继承的 Ebox 默认属性;应用 palette 仍由 companion 查询,
|
||||
Renderer 会在进入 Ebox 几何层前过滤未知 token 名。
|
||||
|
||||
## 响应式
|
||||
|
||||
- 支持 compact 900–940px body 和 fullscreen desktop GUI。
|
||||
- metric 尽量一行展示,workspace 从双列退化为上下堆叠,控件保持 intrinsic 单行宽度。
|
||||
- 需要分配剩余宽度的行使用 `flex`,固定 intrinsic 控件组才使用 `row`;导航
|
||||
控件组使用 max-content,确保每个 hover/active 区域都是独立控件。
|
||||
- mouse-1 和键盘激活使用同一组公开 refs。
|
||||
|
||||
## 交互状态
|
||||
- Loading:有标签、有边界,并显示明确状态。
|
||||
- Empty:安静的解释区域;可用时给出下一步操作。
|
||||
- Error:高对比 danger 区域;错误保持可见,不静默回退。
|
||||
- Success:鼠尾草绿强调色与明确反馈文本。
|
||||
- Disabled:弱化前景色,并移除回调与 tab stop。
|
||||
- 离线/慢网络:当前不适用,因为示例使用同步内存数据源。
|
||||
|
||||
- Loading:Resource 显示 `Loading…`,shell 几何不移动。
|
||||
- Empty:DataGrid 明确显示 empty,选择控件仍可用。
|
||||
- Error:reload 可故意失败一次,Boundary 显示 handled error 且仍可恢复。
|
||||
- Success:action/status 和 timestamp 可见更新。
|
||||
- Disabled:不可用操作暴露 disabled 语义,不静默吞输入。
|
||||
|
||||
## 内容语气
|
||||
- 风格:直接、技术化、简洁、自解释。
|
||||
- 术语:统一使用 ETAF 词汇:View、Component、Host、Context、Data Controller、Runtime、event 与 public API。
|
||||
- 文案规则:说清正在演示的能力和操作后的可见结果,避免宣传式填充文字。
|
||||
|
||||
- 简洁、操作化、解释性;标签明确指出正在展示的框架能力。
|
||||
- 统一使用 `Component`、`Context`、`Theme`、`Behavior`、`Action`、`Data`、`Resource`、`Boundary`。
|
||||
- 优先“动词 + 结果”(如 `Reload resource`、`Handled: …`、`Selected Beacon`)。
|
||||
|
||||
## 实现约束
|
||||
- 技术边界:Emacs 29.1+、ETAF 公共 View/Component API,以及 Ebox 公共布局/样式属性。
|
||||
- Token:复用 palette helper 与语义 token key;不新建平行主题或 CSS 层。
|
||||
- 性能:交互保持同步且有界;视觉改进不得增加隐藏的全树工作或除现有 viewport sync 外的新 timer。
|
||||
- 兼容性:`etaf-playground` 必须独立于 `ebox-playground`,不得调用 `ebox--*` 私有 API。
|
||||
- 验证:`make check` 通过;GUI 截图只显示一个目标缓冲区窗口;动态检查覆盖导航、选择、新增、主题切换与返回 Overview;不接受可见截断/续行标记或意外换行。
|
||||
|
||||
## 待解决问题
|
||||
- [ ] 扩展官方 `etaf-ui` 目录后,才能把目录示例表述为完成迁移的组件画廊;维护者;阻止错误的功能对等声明。
|
||||
- 使用 `.etaf`/`.el` 的公开 ETAF View DSL,由 Ebox 渲染,不引入 HTML/CSS runtime dependency。
|
||||
- palette/spacing token 在 companion 单处定义并复用,禁止散落临时色值。
|
||||
- 性能:普通 warm p50 <=100ms,counter/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
|
||||
|
||||
- [ ] 用户可在第一版 HTML/ETAF 对照后调整 palette 或页面命名;行为和公开 ref 覆盖保持不变。
|
||||
|
||||
35
Makefile
35
Makefile
@ -1,28 +1,45 @@
|
||||
EMACS ?= emacs
|
||||
LOAD_PATH = -L . -L ../etaf -L ../etaf-ui -L ../ebox
|
||||
LOAD_PATH = -L . -L ../etaf -L ../etaf-ui -L ../ebox -L ../ecss -L ../tp
|
||||
EXAMPLE_EL := examples/operations-console.el
|
||||
|
||||
.PHONY: all compile test check checkdoc load clean
|
||||
.PHONY: all compile examples-read test check checkdoc load clean
|
||||
|
||||
all: check
|
||||
|
||||
compile:
|
||||
rm -f *.elc tests/*.elc
|
||||
# The Playground is an integration surface. Compile its dependency graph
|
||||
# before compiling the pair; cleaning dependencies without rebuilding them
|
||||
# silently forced every Component and Ebox layout through the interpreter.
|
||||
$(MAKE) -C ../ecss compile EMACS="$(EMACS)"
|
||||
$(MAKE) -C ../tp compile EMACS="$(EMACS)"
|
||||
$(MAKE) -C ../ebox compile EMACS="$(EMACS)" ECSS_DIR=../ecss TP_DIR=../tp
|
||||
$(MAKE) -C ../etaf compile EMACS="$(EMACS)"
|
||||
$(MAKE) -C ../etaf-ui compile EMACS="$(EMACS)"
|
||||
rm -f *.elc tests/*.elc examples/*.elc
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
|
||||
--eval '(load-file "etaf-playground.el")' \
|
||||
--eval '(byte-compile-file "etaf-playground.el")'
|
||||
--eval '(dolist (file (quote ($(foreach file,$(EXAMPLE_EL),"$(file)")))) (byte-compile-file file))' \
|
||||
--eval '(byte-compile-file "etaf-playground.el")' \
|
||||
--eval '(byte-compile-file "tests/etaf-playground-tests.el")'
|
||||
|
||||
test: compile
|
||||
examples-read: compile
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
|
||||
--eval '(require (quote etaf-playground))' \
|
||||
--eval '(etaf-playground-read-pair "operations-console")' \
|
||||
--eval '(princ "ETAF example modules load OK\\n")'
|
||||
|
||||
test: examples-read
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
|
||||
-l tests/etaf-playground-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
load: compile
|
||||
$(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:
|
||||
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (file (directory-files "." t)) (when (string-suffix-p ".el" file) (checkdoc-file file))))'
|
||||
$(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)))'
|
||||
|
||||
check: checkdoc compile test
|
||||
check: checkdoc test
|
||||
|
||||
clean:
|
||||
rm -f *.elc tests/*.elc
|
||||
rm -f *.elc tests/*.elc examples/*.elc
|
||||
|
||||
26
README.md
26
README.md
@ -1,21 +1,11 @@
|
||||
# etaf-playground
|
||||
# ETAF Playground
|
||||
|
||||
`etaf-playground` is the runnable example application for ETAF. The core example depends only on `etaf`; the optional UI example loads `etaf-ui` when requested. It never imports Ebox internals and it is independent from `ebox-playground`.
|
||||
The Playground exposes one canonical product example: the
|
||||
`examples/operations-console.etaf` and `examples/operations-console.el` pair.
|
||||
|
||||
```elisp
|
||||
(require 'etaf-playground)
|
||||
(etaf-playground-open)
|
||||
```
|
||||
The `.etaf` file is inert, validated static composition data. The companion
|
||||
owns Components, state, Context, Theme, Behaviors, Data, Resources, Actions,
|
||||
and lifecycle. `etaf-playground-open`, `etaf-playground-reset`, and
|
||||
`etaf-playground-close` are the only mounting boundary.
|
||||
|
||||
For the complete interactive example, open the Showcase:
|
||||
|
||||
```elisp
|
||||
(require 'etaf-playground)
|
||||
(etaf-playground-open-showcase)
|
||||
```
|
||||
|
||||
The Showcase is intentionally built only from public ETAF APIs. It demonstrates a responsive Flex shell, a root canvas background with local panel surfaces, Context-based theme values, reactive state, a memory Data Controller, keyed rows, and public event dispatch. Its warm paper, terracotta, sage, blue, and violet visual language follows the Ebox Flex reference while remaining readable in both light and dark themes.
|
||||
|
||||
The outer shell is the only viewport-width owner. It measures the visible window body, reserves one character cell for Emacs' right-edge continuation indicator, and lets internal Flex sections wrap without repeating viewport arithmetic. The layout is visually verified from compact GUI frames through fullscreen desktop widths. The `etaf-playground-open-ui` entry remains the smaller optional example for the official `etaf-ui` catalog; it is not a claim that the current catalog matches the legacy component inventory.
|
||||
|
||||
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` from this directory.
|
||||
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`.
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
# etaf-playground
|
||||
# ETAF Playground
|
||||
|
||||
`etaf-playground` 是 ETAF 的可运行示例应用。核心示例只依赖 `etaf`;需要时才通过可选入口加载 `etaf-ui`。它不调用 Ebox 私有实现,也不依赖 `ebox-playground`。
|
||||
Playground 只公开一个标准产品示例:
|
||||
`examples/operations-console.etaf` 与 `examples/operations-console.el` 配对。
|
||||
|
||||
```elisp
|
||||
(require 'etaf-playground)
|
||||
(etaf-playground-open)
|
||||
```
|
||||
`.etaf` 是只读、经过白名单验证的静态组合数据;同名 companion 负责
|
||||
Component、状态、Context、Theme、Behavior、Data、Resource、Action 与生命周期。
|
||||
`etaf-playground-open/reset/close` 是唯一挂载边界。
|
||||
|
||||
要打开完整的交互示例,使用 Showcase:
|
||||
|
||||
```elisp
|
||||
(require 'etaf-playground)
|
||||
(etaf-playground-open-showcase)
|
||||
```
|
||||
|
||||
Showcase 只使用 ETAF 公共 API,演示响应式 Flex 外壳、根画布背景与局部面板背景的分层、基于 Context 的主题值、响应式状态、内存 Data Controller、带 key 的行以及公共事件分发。它沿用 Ebox Flex 参考示例的暖白纸张、陶土色、鼠尾草绿、蓝色与紫色视觉语言,同时保证亮色和暗色主题中的文字可读性。
|
||||
|
||||
最外层 shell 是唯一的 viewport 宽度 owner。它测量窗口正文的真实可用宽度,并为 Emacs 右侧续行标记预留一个字符单元;内部 Flex 区域通过换行适配,不再重复计算 viewport 宽度。该布局会在紧凑 GUI 窗口到全屏桌面宽度之间进行视觉验证。`etaf-playground-open-ui` 仍然是展示官方 `etaf-ui` 目录的较小可选示例;它不代表当前组件目录已经达到旧架构的组件数量与能力。
|
||||
|
||||
在该目录运行 `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`。
|
||||
验证命令:`make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`。
|
||||
|
||||
211
design/operations-console.html
Normal file
211
design/operations-console.html
Normal file
@ -0,0 +1,211 @@
|
||||
<!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>
|
||||
@ -1,711 +1,142 @@
|
||||
;;; etaf-playground.el --- ETAF public examples -*- lexical-binding: t; -*-
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
;; Author: ETAF contributors
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1") (etaf "0.1.0"))
|
||||
;; URL: https://github.com/ginqi7/etaf-playground
|
||||
|
||||
;;; etaf-playground.el --- ETAF operations-console application pair -*- lexical-binding: t; -*-
|
||||
;;; Commentary:
|
||||
|
||||
;; This package is a runnable example application, not a second framework
|
||||
;; layer. The core example depends only on ETAF. The optional catalog demo
|
||||
;; loads `etaf-ui' only when its public command is called.
|
||||
|
||||
;; One reviewed pair: inert `.etaf' data plus an executable companion.
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'etaf)
|
||||
|
||||
(defconst etaf-playground-buffer-name "*ETAF Playground*"
|
||||
"Default buffer name used by `etaf-playground-open'.")
|
||||
|
||||
(defconst etaf-playground-showcase-buffer-name "*ETAF Showcase*"
|
||||
"Default buffer name used by `etaf-playground-open-showcase'.")
|
||||
|
||||
;;;###autoload
|
||||
(etaf-define-component etaf-playground-counter (&key title)
|
||||
"Render a retained counter titled TITLE."
|
||||
:setup
|
||||
(let ((count (etaf-ref 0 :name 'playground-count)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-playground-counter"
|
||||
:width 'stretch
|
||||
:max-width '(560)
|
||||
:padding '(1 (14))
|
||||
:bgcolor "#FFFDF8"
|
||||
:border '((1) solid "#A8A297")
|
||||
(text :face 'bold :color "#7B301F" (expr :value title))
|
||||
(text :face 'bold :color "#252A2E"
|
||||
(expr :value (format "Count: %d" (etaf-value count))))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (10))
|
||||
(text :ref 'decrement :role 'button :tab-index 0
|
||||
:on-press (lambda () (cl-decf (etaf-value count)))
|
||||
:color "#FFFDF8" :bgcolor "#7B301F"
|
||||
:border '((1) solid "#7B301F") :padding '(0 (12))
|
||||
"−")
|
||||
(text :ref 'increment :role 'button :tab-index 0
|
||||
:on-press (lambda () (cl-incf (etaf-value count)))
|
||||
:color "#FFFDF8" :bgcolor "#3F684B"
|
||||
:border '((1) solid "#3F684B") :padding '(0 (12))
|
||||
"+")))))))
|
||||
|
||||
(defun etaf-playground--showcase-palette (dark-p)
|
||||
"Return the Showcase palette for DARK-P."
|
||||
(if dark-p
|
||||
'(:color "#F2EEE4" :bgcolor "#1B1F20" :surface "#252B2C"
|
||||
:surface-alt "#333B3B" :muted "#B2BBB4" :accent "#E58A6A"
|
||||
:accent-strong "#F3B49D" :success "#8BC79A" :warning "#F2C56B"
|
||||
:danger "#F29A93" :border "#596362" :terracotta "#5A342B"
|
||||
:sage "#30483A" :blue "#294352" :violet "#403A55")
|
||||
'(:color "#252A2E" :bgcolor "#F8F5EE" :surface "#FFFDF8"
|
||||
:surface-alt "#EEEAE2" :muted "#66706A" :accent "#B84F35"
|
||||
:accent-strong "#7B301F" :success "#3F684B" :warning "#8A5B10"
|
||||
:danger "#9E3F3F" :border "#A8A297" :terracotta "#F1D4C9"
|
||||
:sage "#DCEBDD" :blue "#D9EAF2" :violet "#E7E2F1")))
|
||||
|
||||
(defun etaf-playground--showcase-palette-value (palette key)
|
||||
"Return KEY from reactive Showcase PALETTE."
|
||||
(plist-get (etaf-value palette) key))
|
||||
|
||||
(defun etaf-playground--showcase-window-width (window)
|
||||
"Return the display-safe Ebox viewport width for WINDOW."
|
||||
(let ((body-width (window-body-width window t))
|
||||
(reserve (max 1 (frame-char-width (window-frame window)))))
|
||||
(max 1 (- body-width reserve))))
|
||||
|
||||
(defun etaf-playground--showcase-sync-viewport (buffer)
|
||||
"Rerender Showcase BUFFER against its visible window dimensions.
|
||||
|
||||
This keeps viewport-height shells and full-surface theme changes correct after
|
||||
the buffer is displayed. Return nil when BUFFER is not visible yet."
|
||||
(when-let ((window (get-buffer-window buffer t)))
|
||||
(let ((width (etaf-playground--showcase-window-width window))
|
||||
(height (window-body-height window)))
|
||||
(when (and (> width 0) (> height 0))
|
||||
(ebox-rerender-buffer-with-context buffer width height)))))
|
||||
|
||||
(defun etaf-playground--showcase-schedule-viewport-sync (buffer)
|
||||
"Schedule a viewport sync for visible Showcase BUFFER.
|
||||
|
||||
The first window geometry reported while a frame is being displayed can be
|
||||
stale. Deferring one sync to the next event-loop turn lets Ebox receive the
|
||||
window dimensions that are actually visible without making the component own
|
||||
any frame lifecycle state."
|
||||
(run-at-time
|
||||
0 nil
|
||||
(lambda ()
|
||||
(when (buffer-live-p buffer)
|
||||
(etaf-playground--showcase-sync-viewport buffer)))))
|
||||
|
||||
(defun etaf-playground--showcase-border (palette)
|
||||
"Return a border using the current Showcase PALETTE."
|
||||
(list '(1) 'solid
|
||||
(etaf-playground--showcase-palette-value palette :border)))
|
||||
|
||||
(defun etaf-playground--showcase-button
|
||||
(label ref callback palette &optional active-p)
|
||||
"Return a Showcase button for LABEL, REF, CALLBACK, PALETTE, and ACTIVE-P."
|
||||
(etaf-view
|
||||
(text
|
||||
:class (if active-p "etaf-showcase-button active" "etaf-showcase-button")
|
||||
:ref ref
|
||||
:role 'button
|
||||
:tab-index 0
|
||||
:on-press callback
|
||||
:color (etaf-playground--showcase-palette-value
|
||||
palette (if active-p :bgcolor :accent-strong))
|
||||
:bgcolor (etaf-playground--showcase-palette-value
|
||||
palette (if active-p :accent :surface-alt))
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
:padding '(0 (10))
|
||||
(expr :value label))))
|
||||
|
||||
(defun etaf-playground--showcase-metric (label value tone palette)
|
||||
"Return a metric card for LABEL, VALUE, TONE, and PALETTE."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-metric"
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(220)
|
||||
:min-width '(200)
|
||||
:padding '(1 (12))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette tone)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :color)
|
||||
(expr :value label))
|
||||
(text
|
||||
:face 'bold
|
||||
:color (etaf-playground--showcase-palette-value palette :accent-strong)
|
||||
(expr :value value)))))
|
||||
|
||||
(defun etaf-playground--showcase-nav (page palette on-page)
|
||||
"Return the Showcase navigation for PAGE, PALETTE, and ON-PAGE."
|
||||
(etaf-view
|
||||
(flex
|
||||
:class "etaf-showcase-navigation"
|
||||
:width 'stretch
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (10))
|
||||
:padding '(1 (16))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold :flex-shrink 0 "NAVIGATION")
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Overview" 'showcase-nav-overview
|
||||
(lambda () (funcall on-page 'overview)) palette
|
||||
(eq page 'overview)))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Work items" 'showcase-nav-work-items
|
||||
(lambda () (funcall on-page 'work-items)) palette
|
||||
(eq page 'work-items)))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Theme" 'showcase-nav-theme
|
||||
(lambda () (funcall on-page 'theme)) palette
|
||||
(eq page 'theme)))
|
||||
(text
|
||||
:flex-grow 1
|
||||
:flex-basis '(180)
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"Keyboard: Tab, RET"))))
|
||||
|
||||
(defun etaf-playground--showcase-page-title (page)
|
||||
"Return the display title for Showcase PAGE."
|
||||
(or (cdr (assq page '((overview . "Overview")
|
||||
(work-items . "Work items")
|
||||
(theme . "Theme and semantics"))))
|
||||
"Overview"))
|
||||
|
||||
(defun etaf-playground--showcase-task-row
|
||||
(task selected-id palette on-select)
|
||||
"Return one interactive table row for TASK, SELECTED-ID, PALETTE, and ON-SELECT."
|
||||
(let ((id (plist-get task :id))
|
||||
(selected-p (equal (plist-get task :id) selected-id)))
|
||||
(etaf-view
|
||||
(row
|
||||
:key id
|
||||
:ref (intern (format "showcase-task-%s" id))
|
||||
:role 'button
|
||||
:tab-index 0
|
||||
:on-press (lambda () (funcall on-select id))
|
||||
:class (if selected-p "etaf-showcase-task selected"
|
||||
"etaf-showcase-task")
|
||||
:bgcolor (etaf-playground--showcase-palette-value
|
||||
palette (if selected-p :surface-alt :surface))
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :width '(80) (expr :value id))
|
||||
(text :width '(300) (expr :value (plist-get task :title)))
|
||||
(text :width '(140) (expr :value (plist-get task :owner)))
|
||||
(text :width '(120)
|
||||
:color (etaf-playground--showcase-palette-value
|
||||
palette
|
||||
(if (equal (plist-get task :status) "done")
|
||||
:success :accent))
|
||||
(expr :value (capitalize (plist-get task :status))))))))
|
||||
|
||||
(defun etaf-playground--showcase-task-table
|
||||
(items selected-id palette on-select)
|
||||
"Return the task table for ITEMS, SELECTED-ID, PALETTE, and ON-SELECT."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-table"
|
||||
(row
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface-alt)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold :width '(80) "ID")
|
||||
(text :face 'bold :width '(300) "TASK")
|
||||
(text :face 'bold :width '(140) "OWNER")
|
||||
(text :face 'bold :width '(120) "STATUS"))
|
||||
(expr :value
|
||||
(mapcar
|
||||
(lambda (task)
|
||||
(etaf-playground--showcase-task-row
|
||||
task selected-id palette on-select))
|
||||
items)))))
|
||||
|
||||
(defun etaf-playground--showcase-overview
|
||||
(palette controller notice actions)
|
||||
"Return the Showcase overview page.
|
||||
|
||||
PALETTE, CONTROLLER, NOTICE, and ACTIONS provide its live data and behavior."
|
||||
(let ((items (etaf-value (etaf-data-items controller)))
|
||||
(selected-count
|
||||
(length (etaf-value (etaf-data-selection controller)))))
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-section"
|
||||
(text :face 'bold "A complete text application in one View tree")
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"The shell, Flex cards, retained state, Data Controller, and events all share one model.")
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (12))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-metric
|
||||
"VISIBLE WORK ITEMS" (format "%d" (length items))
|
||||
:terracotta palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-metric
|
||||
"SELECTED" (format "%d" selected-count) :sage palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-metric
|
||||
"RUNTIME STATUS"
|
||||
(capitalize (symbol-name (etaf-value
|
||||
(etaf-data-status controller))))
|
||||
:blue palette)))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (12))
|
||||
(column
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(320)
|
||||
:min-width '(280)
|
||||
:padding '(1 (12))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold "Try the interaction path")
|
||||
(text :color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"Open Work items, select a row, add a task, then toggle Theme.")
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Open Work items" 'showcase-open-work-items
|
||||
(plist-get actions :open-work-items) palette)))
|
||||
(column
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(320)
|
||||
:min-width '(280)
|
||||
:padding '(1 (12))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :violet)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold "Latest application feedback")
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :accent)
|
||||
(expr :value notice))))))))
|
||||
|
||||
(defun etaf-playground--showcase-work-items
|
||||
(palette controller filter selected-id notice actions)
|
||||
"Return the Showcase work-items page.
|
||||
|
||||
PALETTE, CONTROLLER, FILTER, SELECTED-ID, NOTICE, and ACTIONS provide its
|
||||
live state."
|
||||
(let* ((items (etaf-value (etaf-data-items controller)))
|
||||
(page (etaf-value (etaf-data-page controller)))
|
||||
(page-size (etaf-value (etaf-data-page-size controller)))
|
||||
(total (etaf-value (etaf-data-total controller)))
|
||||
(last-page (max 1 (ceiling (/ (float total) page-size)))))
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-section"
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (10))
|
||||
(column
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(360)
|
||||
(text :face 'bold "Work items")
|
||||
(text :color (etaf-playground--showcase-palette-value palette :muted)
|
||||
(expr :value (format "%d matching records · page %d/%d"
|
||||
total page last-page))))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
(format "Filter: %s" (capitalize (symbol-name filter)))
|
||||
'showcase-filter (plist-get actions :cycle-filter) palette
|
||||
(not (eq filter 'all))))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Add task" 'showcase-add-task
|
||||
(plist-get actions :add-task) palette)))
|
||||
(text :color (etaf-playground--showcase-palette-value palette :muted)
|
||||
(expr :value notice))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-task-table
|
||||
items selected-id palette (plist-get actions :select-task)))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(0 (12))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Previous" 'showcase-previous-page
|
||||
(plist-get actions :previous-page) palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Next" 'showcase-next-page
|
||||
(plist-get actions :next-page) palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Clear selection" 'showcase-clear-selection
|
||||
(plist-get actions :clear-selection) palette)))
|
||||
(column
|
||||
:padding '(1 (12))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold "Selected record")
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :accent)
|
||||
(expr :value
|
||||
(or (and selected-id
|
||||
(format "%s · use the row key to keep identity stable"
|
||||
selected-id))
|
||||
"No row selected"))))))))
|
||||
|
||||
(defun etaf-playground--showcase-swatch-foreground (dark-p label palette)
|
||||
"Return a readable swatch foreground for DARK-P, LABEL, and PALETTE."
|
||||
(if (equal label "Surface")
|
||||
(etaf-playground--showcase-palette-value palette :color)
|
||||
(if dark-p
|
||||
(etaf-playground--showcase-palette-value palette :bgcolor)
|
||||
"#FFFFFF")))
|
||||
|
||||
(defun etaf-playground--showcase-swatch
|
||||
(label color foreground palette)
|
||||
"Return a color swatch for LABEL, COLOR, FOREGROUND, and PALETTE."
|
||||
(etaf-view
|
||||
(column
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(220)
|
||||
:min-width '(200)
|
||||
:height 3
|
||||
:padding '(1 (12))
|
||||
:bgcolor color
|
||||
:color foreground
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold (expr :value label))
|
||||
(text (expr :value color)))))
|
||||
|
||||
(defun etaf-playground--showcase-theme (dark-p palette actions)
|
||||
"Return the Showcase theme page for DARK-P, PALETTE, and ACTIONS."
|
||||
(let ((colors (etaf-value palette)))
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-section"
|
||||
(text :face 'bold "Theme and semantics")
|
||||
(text :color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"Theme is an inherited Context value; explicit Host properties remain local overrides.")
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (10))
|
||||
(text :face 'bold (expr :value (if dark-p "Dark theme" "Light theme")))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
(if dark-p "Switch to light" "Switch to dark")
|
||||
'showcase-theme-toggle
|
||||
(plist-get actions :toggle-theme) palette)))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (12))
|
||||
(expr :value
|
||||
(mapcar
|
||||
(lambda (entry)
|
||||
(etaf-playground--showcase-swatch
|
||||
(car entry) (cdr entry)
|
||||
(etaf-playground--showcase-swatch-foreground
|
||||
dark-p (car entry) palette)
|
||||
palette))
|
||||
(list (cons "Surface" (plist-get colors :surface))
|
||||
(cons "Accent" (plist-get colors :accent))
|
||||
(cons "Success" (plist-get colors :success))
|
||||
(cons "Warning" (plist-get colors :warning))
|
||||
(cons "Danger" (plist-get colors :danger))
|
||||
(cons "Muted" (plist-get colors :muted)))))
|
||||
(column
|
||||
:flex-grow 1
|
||||
:flex-shrink 1
|
||||
:flex-basis '(320)
|
||||
:min-width '(280)
|
||||
:padding '(1 (12))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold "Why this is useful")
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"The same Component grammar handles static structure, computed values, state, data, and interaction.")))))))
|
||||
|
||||
(defun etaf-playground--showcase-shell
|
||||
(page dark-p filter controller selected-id notice palette actions title)
|
||||
"Return the complete Showcase shell.
|
||||
|
||||
PAGE, DARK-P, FILTER, CONTROLLER, SELECTED-ID, NOTICE, PALETTE, ACTIONS,
|
||||
and TITLE provide its live state and presentation."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-root"
|
||||
:width 'stretch
|
||||
:height '(viewport-height)
|
||||
:color (etaf-playground--showcase-palette-value palette :color)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :bgcolor)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(flex
|
||||
:class "etaf-showcase-header"
|
||||
:width 'stretch
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (16))
|
||||
:padding '(1 (16))
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(column :flex-shrink 0 :flex-basis '(210)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
(text :face 'bold (expr :value title))
|
||||
(text :color (etaf-playground--showcase-palette-value
|
||||
palette :muted)
|
||||
"A runnable composition"))
|
||||
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(420)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
(text :face 'bold
|
||||
(expr :value (etaf-playground--showcase-page-title page)))
|
||||
(text :color (etaf-playground--showcase-palette-value
|
||||
palette :muted)
|
||||
(expr :value
|
||||
(if dark-p "Dark semantic surface" "Light semantic surface"))))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
(if dark-p "Light" "Dark") 'showcase-header-theme-toggle
|
||||
(plist-get actions :toggle-theme) palette)))
|
||||
(column
|
||||
:width 'stretch
|
||||
:color (etaf-playground--showcase-palette-value palette :color)
|
||||
(expr :value
|
||||
(etaf-playground--showcase-nav
|
||||
page palette (plist-get actions :set-page)))
|
||||
(expr :value
|
||||
(cond
|
||||
((eq page 'work-items)
|
||||
(etaf-playground--showcase-work-items
|
||||
palette controller filter selected-id notice actions))
|
||||
((eq page 'theme)
|
||||
(etaf-playground--showcase-theme dark-p palette actions))
|
||||
(t
|
||||
(etaf-playground--showcase-overview
|
||||
palette controller notice actions)))))
|
||||
(flex
|
||||
:class "etaf-showcase-status"
|
||||
:width 'stretch
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(0 (12))
|
||||
:padding '(0 (16))
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
(text (expr :value (format "STATUS %s" notice)))
|
||||
(text (expr :value (format " · %s records" (etaf-value
|
||||
(etaf-data-total controller)))))))))
|
||||
|
||||
;;;###autoload
|
||||
(etaf-define-component etaf-playground-showcase (&key title)
|
||||
"Render a complex interactive ETAF application showcase."
|
||||
:setup
|
||||
(let* ((page (etaf-ref 'overview :name 'showcase-page))
|
||||
(dark-p (etaf-ref nil :name 'showcase-dark-theme))
|
||||
(filter (etaf-ref 'all :name 'showcase-filter))
|
||||
(selected-id (etaf-ref nil :name 'showcase-selected-id))
|
||||
(notice (etaf-ref "Ready · choose a page" :name 'showcase-notice))
|
||||
(next-id (etaf-ref 107 :name 'showcase-next-id))
|
||||
(source
|
||||
(etaf-data-memory-source
|
||||
'((:id "T-101" :title "Stabilize incremental commits"
|
||||
:owner "Ada" :status "open")
|
||||
(:id "T-102" :title "Verify Grid track sizing"
|
||||
:owner "Grace" :status "review")
|
||||
(:id "T-103" :title "Document the public View grammar"
|
||||
:owner "Lin" :status "done")
|
||||
(:id "T-104" :title "Exercise Context inheritance"
|
||||
:owner "Edsger" :status "open")
|
||||
(:id "T-105" :title "Measure native fallback behavior"
|
||||
:owner "Margaret" :status "review")
|
||||
(:id "T-106" :title "Polish keyboard focus paths"
|
||||
:owner "Alan" :status "open"))
|
||||
:id-key :id
|
||||
:name 'showcase-work-items))
|
||||
(controller
|
||||
(etaf-data-controller source :page-size 4))
|
||||
(palette
|
||||
(etaf-computed
|
||||
(lambda ()
|
||||
(etaf-playground--showcase-palette (etaf-value dark-p)))))
|
||||
(theme
|
||||
(etaf-computed
|
||||
(lambda ()
|
||||
(let ((colors (etaf-value palette)))
|
||||
(list :color (plist-get colors :color))))))
|
||||
(runtime (etaf-current-runtime))
|
||||
(last-rendered-dark-p nil)
|
||||
actions)
|
||||
(etaf-theme-provide theme)
|
||||
(etaf-on-mounted (lambda () (etaf-data-load controller)))
|
||||
(etaf-on-unmounted (lambda () (etaf-data-stop controller)))
|
||||
(etaf-on-updated
|
||||
(lambda ()
|
||||
(let ((current-dark-p (etaf-value dark-p)))
|
||||
(when (not (eq current-dark-p last-rendered-dark-p))
|
||||
(setq last-rendered-dark-p current-dark-p)
|
||||
(etaf-playground--showcase-schedule-viewport-sync
|
||||
(etaf-runtime-buffer runtime))))))
|
||||
(setq actions
|
||||
(list
|
||||
:set-page (lambda (next-page)
|
||||
(setf (etaf-value page) next-page
|
||||
(etaf-value notice)
|
||||
(format "Opened %s"
|
||||
(etaf-playground--showcase-page-title
|
||||
next-page))))
|
||||
:open-work-items (lambda ()
|
||||
(setf (etaf-value page) 'work-items
|
||||
(etaf-value notice) "Opened Work items"))
|
||||
:toggle-theme (lambda ()
|
||||
(setf (etaf-value dark-p) (not (etaf-value dark-p))
|
||||
(etaf-value notice)
|
||||
(if (etaf-value dark-p)
|
||||
"Dark theme enabled"
|
||||
"Light theme enabled")))
|
||||
:add-task (lambda ()
|
||||
(let* ((number (etaf-value next-id))
|
||||
(id (format "T-%d" number)))
|
||||
(cl-incf (etaf-value next-id))
|
||||
(etaf-data-mutate
|
||||
controller 'insert
|
||||
(list :id id
|
||||
:title "New task from the interaction path"
|
||||
:owner "You"
|
||||
:status "open"))
|
||||
(setf (etaf-value notice)
|
||||
(format "Added %s" id))))
|
||||
:cycle-filter (lambda ()
|
||||
(let ((next (pcase (etaf-value filter)
|
||||
('all 'open)
|
||||
('open 'review)
|
||||
(_ 'all))))
|
||||
(setf (etaf-value filter) next)
|
||||
(etaf-data-set-query
|
||||
controller
|
||||
(pcase next
|
||||
('all nil)
|
||||
(_ (symbol-name next))))
|
||||
(etaf-data-load controller)
|
||||
(setf (etaf-value notice)
|
||||
(format "Filter: %s"
|
||||
(capitalize (symbol-name next))))))
|
||||
:previous-page (lambda ()
|
||||
(etaf-data-previous-page controller)
|
||||
(etaf-data-load controller)
|
||||
(setf (etaf-value notice) "Moved to the previous page"))
|
||||
:next-page (lambda ()
|
||||
(etaf-data-next-page controller)
|
||||
(etaf-data-load controller)
|
||||
(setf (etaf-value notice) "Moved to the next page"))
|
||||
:clear-selection (lambda ()
|
||||
(etaf-data-clear-selection controller)
|
||||
(setf (etaf-value selected-id) nil
|
||||
(etaf-value notice) "Selection cleared"))
|
||||
:select-task (lambda (id)
|
||||
(etaf-data-select controller id)
|
||||
(setf (etaf-value selected-id) id
|
||||
(etaf-value notice)
|
||||
(format "Selected %s" id)))))
|
||||
(lambda ()
|
||||
(etaf-playground--showcase-shell
|
||||
(etaf-value page) (etaf-value dark-p) (etaf-value filter)
|
||||
controller (etaf-value selected-id) (etaf-value notice)
|
||||
palette actions title))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-view ()
|
||||
"Return the core ETAF playground View without side effects."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-playground-root"
|
||||
:width 'stretch
|
||||
:padding '(1 (16))
|
||||
:color "#252A2E"
|
||||
:bgcolor "#F8F5EE"
|
||||
(text :face 'bold :color "#7B301F" "ETAF Playground")
|
||||
(text :color "#66706A"
|
||||
"One grammar: Hosts, Components, props, children, and expr.")
|
||||
(etaf-playground-counter :title "Reactive counter")
|
||||
(text :color "#66706A"
|
||||
"Press + or - through the public event dispatch path."))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-ui-view ()
|
||||
"Return a View demonstrating the optional official `etaf-ui' catalog."
|
||||
(require 'etaf-ui)
|
||||
(etaf-view
|
||||
(column
|
||||
:width 'stretch
|
||||
:padding '(1 (16))
|
||||
:color "#252A2E"
|
||||
:bgcolor "#F8F5EE"
|
||||
(etaf-panel :title "Official Components"
|
||||
(etaf-button :label "A public Component"
|
||||
:on-press (lambda () (message "ETAF UI button pressed")))
|
||||
(etaf-checkbox :label "Controlled checkbox"
|
||||
:on-change (lambda (value)
|
||||
(message "Checkbox value: %s" value))))
|
||||
(etaf-playground-counter :title "Core counter"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-showcase-view ()
|
||||
"Return the complex ETAF Showcase View without side effects."
|
||||
(etaf-view (etaf-playground-showcase :title "ETAF Showcase")))
|
||||
(defconst etaf-playground-buffer-name "*ETAF Playground*")
|
||||
(defconst etaf-playground-example-directory
|
||||
(expand-file-name "examples"
|
||||
(file-name-directory (or load-file-name buffer-file-name))))
|
||||
(defconst etaf-playground-scenario-manifest
|
||||
'((:pair "operations-console" :root-component etaf-operations-console-root
|
||||
:category "Complete application"
|
||||
:capabilities (view component reactivity context theme behavior events
|
||||
actions data resource lifecycle etaf-ui)
|
||||
:refs (operations-console-alpha-increment operations-console-alpha-reset
|
||||
operations-console-beta-increment operations-console-beta-reset
|
||||
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 operations-console-clear-selection
|
||||
operations-console-row-1 operations-console-row-2 operations-console-row-3
|
||||
operations-console-fail-next
|
||||
operations-console-resource-reload operations-console-error-boundary)
|
||||
:gui-checkpoints (compact fullscreen overview data resource theme focus)
|
||||
: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)
|
||||
(defconst etaf-playground--static-tags
|
||||
'(operations-console-shell header brand summary theme-control navigation tab
|
||||
main overview hero metric-strip metric workspace activity-panel
|
||||
capability-panel timeline data resource status
|
||||
column row text spacer panel label viewport-height stretch))
|
||||
|
||||
(defun etaf-playground-scenario (name)
|
||||
"Return the sole manifest entry named NAME."
|
||||
(or (and (equal name "operations-console")
|
||||
(car etaf-playground-scenario-manifest))
|
||||
(user-error "Unknown ETAF Playground pair: %s" name)))
|
||||
|
||||
(defun etaf-playground--pair-file (name suffix)
|
||||
"Return NAME pair path ending in SUFFIX."
|
||||
(expand-file-name (concat name suffix) etaf-playground-example-directory))
|
||||
|
||||
(defun etaf-playground--validate-static-node (node)
|
||||
"Validate inert static View NODE and return it."
|
||||
(cond
|
||||
((or (null node) (stringp node) (numberp node) (keywordp node)
|
||||
(memq node '(t stretch viewport-height))) node)
|
||||
((consp node)
|
||||
(unless (and (proper-list-p node) (symbolp (car node))
|
||||
(memq (car node) etaf-playground--static-tags)
|
||||
(not (string-prefix-p "etaf--" (symbol-name (car node))))
|
||||
(not (string-prefix-p "ebox--" (symbol-name (car node)))))
|
||||
(error "Unsafe ETAF static View form: %S" node))
|
||||
(mapc #'etaf-playground--validate-static-node (cdr node))
|
||||
node)
|
||||
(t (error "Unsafe ETAF static View value: %S" node))))
|
||||
|
||||
(defun etaf-playground-read-static (name)
|
||||
"Read and validate NAME's single inert `.etaf' form."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents (etaf-playground--pair-file name ".etaf"))
|
||||
(let ((read-eval nil)
|
||||
(form (read (current-buffer))))
|
||||
(ignore read-eval)
|
||||
(condition-case nil
|
||||
(progn (read (current-buffer))
|
||||
(error "ETAF static file contains multiple forms"))
|
||||
(end-of-file nil))
|
||||
(etaf-playground--validate-static-node form))))
|
||||
|
||||
(defun etaf-playground-read-pair (name)
|
||||
"Read NAME data, load its companion, and return its root View."
|
||||
(let* ((scenario (etaf-playground-scenario name))
|
||||
(form (etaf-playground-read-static name))
|
||||
(root (plist-get scenario :root-component)))
|
||||
(unless (featurep 'etaf-operations-console)
|
||||
;; 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
|
||||
(etaf-playground--pair-file name ".el"))
|
||||
nil nil nil))
|
||||
(unless (functionp root) (error "Missing root factory: %S" root))
|
||||
(funcall root form)))
|
||||
|
||||
(defun etaf-playground-mount-example (buffer-name name)
|
||||
"Mount pair NAME in BUFFER-NAME."
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(let ((buffer (etaf-mount buffer-name (etaf-playground-read-pair name))))
|
||||
(with-current-buffer buffer (setq etaf-playground-current-example name))
|
||||
buffer))
|
||||
|
||||
(defun etaf-playground--display-buffer (buffer)
|
||||
"Display BUFFER interactively and return it."
|
||||
(unless noninteractive (pop-to-buffer buffer))
|
||||
buffer)
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-open (&optional buffer-name)
|
||||
"Mount the core playground in BUFFER-NAME and return its buffer."
|
||||
"Open the operations-console pair in BUFFER-NAME."
|
||||
(interactive)
|
||||
(etaf-mount (or buffer-name etaf-playground-buffer-name)
|
||||
(etaf-playground-view)))
|
||||
(etaf-playground--display-buffer
|
||||
(etaf-playground-mount-example
|
||||
(or buffer-name etaf-playground-buffer-name) "operations-console")))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-open-ui (&optional buffer-name)
|
||||
"Mount the playground with the catalog in BUFFER-NAME."
|
||||
(interactive)
|
||||
(etaf-mount (or buffer-name etaf-playground-buffer-name)
|
||||
(etaf-playground-ui-view)))
|
||||
(defun etaf-playground-open-example (name &optional buffer-name)
|
||||
"Open pair NAME in BUFFER-NAME."
|
||||
(etaf-playground--display-buffer
|
||||
(etaf-playground-mount-example
|
||||
(or buffer-name etaf-playground-buffer-name) name)))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-open-showcase (&optional buffer-name)
|
||||
"Mount the interactive Showcase in BUFFER-NAME and return its buffer.
|
||||
|
||||
Use the visible navigation and buttons with the mouse or keyboard. The
|
||||
Showcase demonstrates responsive Flex layout, Context theme defaults,
|
||||
reactive state, Data Controller updates, stable row keys, and public event
|
||||
dispatch."
|
||||
(defun etaf-playground-reset (&optional buffer-name)
|
||||
"Remount the active pair in BUFFER-NAME."
|
||||
(interactive)
|
||||
(let* ((buffer
|
||||
(etaf-mount (or buffer-name etaf-playground-showcase-buffer-name)
|
||||
(etaf-playground-showcase-view)))
|
||||
(window (or (get-buffer-window buffer t)
|
||||
(progn
|
||||
(pop-to-buffer buffer)
|
||||
(get-buffer-window buffer t)))))
|
||||
(when (window-live-p window)
|
||||
(etaf-playground--showcase-sync-viewport buffer)
|
||||
(etaf-playground--showcase-schedule-viewport-sync buffer))
|
||||
buffer))
|
||||
(let ((name (or buffer-name etaf-playground-buffer-name)))
|
||||
(unless (get-buffer name) (user-error "No Playground pair is mounted"))
|
||||
(etaf-playground--display-buffer
|
||||
(etaf-playground-mount-example name "operations-console"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun etaf-playground-close (&optional buffer-name)
|
||||
"Unmount and kill the playground BUFFER-NAME, returning its buffer."
|
||||
"Unmount and kill the Playground BUFFER-NAME."
|
||||
(interactive)
|
||||
(let* ((name (or buffer-name etaf-playground-buffer-name))
|
||||
(buffer (get-buffer name)))
|
||||
(when-let ((runtime (and buffer (etaf-runtime-for-buffer buffer))))
|
||||
(etaf-unmount runtime))
|
||||
(when buffer
|
||||
(kill-buffer buffer))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer))
|
||||
buffer))
|
||||
|
||||
(provide 'etaf-playground)
|
||||
|
||||
;;; etaf-playground.el ends here
|
||||
|
||||
756
examples/operations-console.el
Normal file
756
examples/operations-console.el
Normal file
@ -0,0 +1,756 @@
|
||||
;;; 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 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
|
||||
29
examples/operations-console.etaf
Normal file
29
examples/operations-console.etaf
Normal file
@ -0,0 +1,29 @@
|
||||
;; 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"))
|
||||
56
postmortem/2026-08-19-etaf-ui-paired-examples.md
Normal file
56
postmortem/2026-08-19-etaf-ui-paired-examples.md
Normal file
@ -0,0 +1,56 @@
|
||||
# ETAF UI paired examples — postmortem / 组件配对示例复盘
|
||||
|
||||
## English
|
||||
|
||||
### Context
|
||||
|
||||
The playground originally mixed a large hand-written Host tree with an
|
||||
optional catalog entry. That made the primary examples fail to teach the
|
||||
official Component contract and encouraged example-only button/grid helpers.
|
||||
|
||||
### Decision and why
|
||||
|
||||
Each runnable example is now a same-basename `examples/<name>.etaf` + `.el`
|
||||
pair. The `.etaf` file is safe static data and the companion owns state,
|
||||
controllers, and callbacks through the public pair loader. This preserves the
|
||||
formal data boundary and makes source/build/mount behavior testable. `etaf-ui`
|
||||
is a required dependency because the examples are intended to prove the
|
||||
official Button, Checkbox, Label, Panel, and DataGrid semantics—not a fallback
|
||||
catalog.
|
||||
|
||||
The semantic behavior belongs in `etaf-ui`, not in playground compensation:
|
||||
disabled controls lose callbacks and tab stops, and interactive DataGrid rows
|
||||
receive stable refs, roles, and tab indexes. The Showcase layout uses a
|
||||
vertical growable main region with a bottom status region, so geometry remains
|
||||
the framework/layout contract rather than a collection of timing hacks.
|
||||
|
||||
### Follow-up
|
||||
|
||||
Keep pair files small and explicit. Extend the public catalog or core API when
|
||||
a reusable semantic capability is missing; do not copy a private implementation
|
||||
into an example.
|
||||
|
||||
## 中文
|
||||
|
||||
### 背景
|
||||
|
||||
旧 playground 把大段手写 Host 树与可选组件目录混在一起,主示例没有真正教授
|
||||
官方 Component 契约,也容易催生只供示例使用的 button/grid helper。
|
||||
|
||||
### 决策与原因
|
||||
|
||||
现在每个可运行示例都是同名的 `examples/<name>.etaf` + `.el` 配对。`.etaf`
|
||||
是安全的静态数据,companion 通过公共 pair loader 持有状态、controller 与
|
||||
callback。这样保留正式数据边界,并且可以测试 source/build/mount 全流程。
|
||||
`etaf-ui` 改为必需依赖,因为示例的目的就是证明官方 Button、Checkbox、Label、
|
||||
Panel、DataGrid 的语义,而不是提供备用目录。
|
||||
|
||||
语义行为归 `etaf-ui` 所有,而不是由 playground 补偿:disabled 控件移除
|
||||
callback 和 tab stop;可交互 DataGrid 行拥有稳定 ref、role 与 tab-index。
|
||||
Showcase 使用可增长的纵向 main 和底部 status,使几何成为框架/布局契约,而
|
||||
不是一组 timing hack。
|
||||
|
||||
### 后续
|
||||
|
||||
保持配对文件小而明确。若缺少可复用语义能力,应扩展公共目录或核心 API;不要
|
||||
把私有实现复制进示例。
|
||||
@ -1,155 +1,365 @@
|
||||
;;; etaf-playground-tests.el --- ETAF example tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; etaf-playground-tests.el --- operations-console pair contract -*- lexical-binding: t; -*-
|
||||
;;; Code:
|
||||
(require 'ert)
|
||||
(require 'etaf-playground)
|
||||
(defvar etaf-operations-console-cleanup-count)
|
||||
|
||||
(defun etaf-playground-test--layout-property (node key)
|
||||
"Return layout KEY from ordinary or Flex NODE."
|
||||
(if (plist-member node key)
|
||||
(plist-get node key)
|
||||
(plist-get (plist-get node :box) key)))
|
||||
(defun etaf-playground-test--text (buffer)
|
||||
"Return BUFFER plain text."
|
||||
(with-current-buffer buffer (substring-no-properties (buffer-string))))
|
||||
|
||||
(ert-deftest etaf-playground-mounts-and-dispatches-core-example ()
|
||||
"Mount the public playground and update its retained counter."
|
||||
(let ((buffer-name " *etaf-playground-test*"))
|
||||
(defun etaf-playground-test--close (buffer)
|
||||
"Close BUFFER when it exists."
|
||||
(when (get-buffer buffer) (etaf-playground-close buffer)))
|
||||
|
||||
(ert-deftest etaf-playground-manifest-has-one-reviewed-pair ()
|
||||
"Expose only the operations-console pair and keep old examples inactive."
|
||||
(should (equal '("operations-console") etaf-playground-example-names))
|
||||
(should (= 1 (length etaf-playground-scenario-manifest)))
|
||||
(let ((entry (car etaf-playground-scenario-manifest))
|
||||
(makefile (with-temp-buffer
|
||||
(insert-file-contents "Makefile") (buffer-string))))
|
||||
(dolist (key '(:pair :root-component :capabilities :refs
|
||||
:gui-checkpoints :performance))
|
||||
(should (plist-member entry key)))
|
||||
(should (string-match-p
|
||||
"EXAMPLE_EL := examples/operations-console.el" makefile))
|
||||
(should-not (string-match-p "wildcard examples" makefile))))
|
||||
|
||||
(ert-deftest etaf-playground-compile-builds-bytecode-dependencies ()
|
||||
"Integration builds must not silently fall back to interpreted dependencies."
|
||||
(let ((makefile (with-temp-buffer
|
||||
(insert-file-contents "Makefile")
|
||||
(buffer-string))))
|
||||
(dolist (dependency '("$(MAKE) -C ../ecss compile"
|
||||
"$(MAKE) -C ../tp compile"
|
||||
"$(MAKE) -C ../ebox compile"
|
||||
"$(MAKE) -C ../etaf compile"
|
||||
"$(MAKE) -C ../etaf-ui compile"))
|
||||
(should (string-match-p (regexp-quote dependency) makefile)))))
|
||||
|
||||
(ert-deftest etaf-playground-static-reader-is-inert-and-strict ()
|
||||
"Read static data without companion execution and reject unsafe AST nodes."
|
||||
(when (featurep 'etaf-operations-console)
|
||||
(unload-feature 'etaf-operations-console t))
|
||||
(let ((form (etaf-playground-read-static "operations-console")))
|
||||
(should (equal (car form) 'operations-console-shell))
|
||||
(should (equal (plist-get (cdr form) :title) "Operations Console"))
|
||||
(should (equal (mapcar #'car
|
||||
(cl-remove-if-not #'consp (cdr form)))
|
||||
'(header navigation main status))))
|
||||
(should-not (featurep 'etaf-operations-console))
|
||||
(dolist (form '((lambda () 1) (eval '(message "x"))
|
||||
(etaf--private) (ebox--private) (shell-command "x")))
|
||||
(should-error (etaf-playground--validate-static-node form) :type 'error)))
|
||||
|
||||
(ert-deftest etaf-playground-pair-load-and-mount-is-repeatable ()
|
||||
"Load the same pair twice and mount/unmount it twice without stale state."
|
||||
(let ((buffer " *etaf-operations-console-pair-test*"))
|
||||
(unwind-protect
|
||||
(dotimes (_iteration 2)
|
||||
(let ((form (etaf-playground-read-static "operations-console")))
|
||||
(should (equal (car form) 'operations-console-shell))
|
||||
(should (equal (mapcar #'car
|
||||
(cl-remove-if-not #'consp (cdr form)))
|
||||
'(header navigation main status))))
|
||||
(etaf-playground-mount-example buffer "operations-console")
|
||||
(should (etaf-runtime-p (etaf-runtime-for-buffer buffer)))
|
||||
(should (string-match-p "Operations Console"
|
||||
(etaf-playground-test--text buffer)))
|
||||
(etaf-unmount (etaf-runtime-for-buffer buffer))
|
||||
(when (get-buffer buffer) (kill-buffer buffer))
|
||||
(should-not (etaf-runtime-for-buffer buffer)))
|
||||
(etaf-playground-test--close buffer))
|
||||
(should-not (etaf-runtime-for-buffer buffer))))
|
||||
|
||||
(ert-deftest etaf-playground-operations-console-drives-public-refs ()
|
||||
"Drive representative app capabilities through public event/focus ports."
|
||||
(let ((buffer " *etaf-operations-console-actions-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount buffer-name (etaf-playground-view))
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "ETAF Playground" (buffer-string)))
|
||||
(should (string-match-p "Count: 0" (buffer-string))))
|
||||
(etaf-dispatch-event (etaf-runtime-for-buffer buffer-name)
|
||||
'increment 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Count: 1" (buffer-string)))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
(dolist (entry
|
||||
'((operations-console-alpha-increment)
|
||||
(operations-console-alpha-reset)
|
||||
(operations-console-beta-increment)
|
||||
(operations-console-beta-reset)
|
||||
(operations-console-local-callback)
|
||||
(operations-console-named-action)
|
||||
(operations-console-behavior-toggle)
|
||||
(operations-console-theme-toggle)
|
||||
(operations-console-nav-data)
|
||||
(operations-console-clear-selection operations-console-nav-data)
|
||||
(operations-console-row-1 operations-console-nav-data)
|
||||
(operations-console-row-2 operations-console-nav-data)
|
||||
(operations-console-row-3 operations-console-nav-data)
|
||||
(operations-console-nav-resource)
|
||||
(operations-console-fail-next operations-console-nav-resource)
|
||||
(operations-console-resource-reload operations-console-nav-resource)
|
||||
(operations-console-error-boundary operations-console-nav-resource)
|
||||
(operations-console-nav-overview)))
|
||||
(etaf-playground-open buffer)
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer))
|
||||
(ref (car entry))
|
||||
(page-ref (cadr entry)))
|
||||
(when page-ref
|
||||
(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-ui-entry-keeps-catalog-optional ()
|
||||
"Load the catalog only through the optional UI entry point."
|
||||
(require 'etaf-ui)
|
||||
(let ((buffer-name " *etaf-playground-ui-test*"))
|
||||
(ert-deftest etaf-playground-behavior-toggle-uses-one-retained-publication ()
|
||||
"Behavior content and paint changes stay in two fixed owners per turn."
|
||||
(let ((buffer " *etaf-operations-console-behavior-retained-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount buffer-name (etaf-playground-ui-view))
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Official Components" (buffer-string)))
|
||||
(should (string-match-p "Controlled checkbox" (buffer-string)))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
(etaf-playground-open buffer)
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer))
|
||||
(surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(old-commit (symbol-function 'ebox-commit))
|
||||
(root-id (plist-get
|
||||
(plist-get (ebox--buffer-render-state buffer)
|
||||
:root-node)
|
||||
:node-id)))
|
||||
(dotimes (index 4)
|
||||
(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-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-showcase-covers-public-interaction-path ()
|
||||
"Mount the Showcase and exercise its navigation, data, and theme events."
|
||||
(let ((buffer-name " *etaf-playground-showcase-test*"))
|
||||
(ert-deftest etaf-playground-a-theme-toggle-uses-mixed-owner-publication ()
|
||||
"Theme paint plus fixed-slot label changes publish through one mixed owner."
|
||||
(let ((buffer " *etaf-operations-console-theme-mixed-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount buffer-name (etaf-playground-showcase-view))
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "ETAF Showcase" (buffer-string)))
|
||||
(should (string-match-p "VISIBLE WORK ITEMS" (buffer-string)))
|
||||
(should (string-match-p "4" (buffer-string))))
|
||||
(let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-dispatch-event runtime 'showcase-open-work-items 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Work items" (buffer-string)))
|
||||
(should (string-match-p "T-101" (buffer-string))))
|
||||
(etaf-dispatch-event runtime 'showcase-task-T-101 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Selected T-101" (buffer-string))))
|
||||
(etaf-dispatch-event runtime 'showcase-add-task 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "T-107" (buffer-string))))
|
||||
(etaf-dispatch-event runtime 'showcase-nav-theme 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Theme and semantics" (buffer-string))))
|
||||
(etaf-dispatch-event runtime 'showcase-theme-toggle 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Dark theme" (buffer-string)))
|
||||
(should (string-match-p "Dark semantic surface" (buffer-string))))
|
||||
(etaf-dispatch-event runtime 'showcase-nav-overview 'press)
|
||||
(with-current-buffer buffer-name
|
||||
(should (string-match-p "Overview" (buffer-string)))
|
||||
(should (string-match-p "Dark semantic surface" (buffer-string))))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
(etaf-playground-open buffer)
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer))
|
||||
(before (etaf-runtime-generation runtime))
|
||||
(commits 0)
|
||||
(old-commit (symbol-function 'ebox-commit)))
|
||||
(cl-letf (((symbol-function 'ebox-commit)
|
||||
(lambda (&rest args)
|
||||
(cl-incf commits)
|
||||
(apply old-commit args))))
|
||||
(etaf-dispatch-event runtime
|
||||
'operations-console-theme-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)
|
||||
'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-showcase-keeps-backgrounds-layered ()
|
||||
"Keep the root canvas background separate from semantic surface boxes."
|
||||
(let ((buffer-name " *etaf-playground-showcase-layering-test*"))
|
||||
(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-mount buffer-name (etaf-playground-showcase-view))
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer-name))
|
||||
(root (etaf-runtime-root-node runtime))
|
||||
(layout (plist-get root :ebox-content-node))
|
||||
(children (plist-get layout :children)))
|
||||
(should (equal (plist-get root :bgcolor) "#F8F5EE"))
|
||||
(should-not (plist-member root :surface-properties))
|
||||
(should (equal (mapcar (lambda (node)
|
||||
(etaf-playground-test--layout-property
|
||||
node :bgcolor))
|
||||
children)
|
||||
'("#FFFDF8" nil "#FFFDF8")))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
(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-showcase-delegates-viewport-width-to-host ()
|
||||
"Let the host own viewport width and stretch each shell section inside it."
|
||||
(let ((buffer-name " *etaf-playground-width-owner-test*")
|
||||
(ebox-viewport-width 900)
|
||||
(ebox-viewport-height 40))
|
||||
(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-mount buffer-name (etaf-playground-showcase-view))
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer-name))
|
||||
(root (etaf-runtime-root-node runtime))
|
||||
(layout (plist-get root :ebox-content-node))
|
||||
(children (plist-get layout :children))
|
||||
(main (nth 1 children)))
|
||||
(should (eq (plist-get root :width) 'stretch))
|
||||
(should (eq (plist-get main :ebox-type) 'box))
|
||||
(should (eq (plist-get (plist-get main :ebox-content-node)
|
||||
:ebox-type)
|
||||
'stack))
|
||||
(should (cl-every
|
||||
(lambda (node)
|
||||
(eq (etaf-playground-test--layout-property node :width)
|
||||
'stretch))
|
||||
children))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
(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-showcase-syncs-to-window-body-width ()
|
||||
"Exclude fringes and other window chrome from the Ebox viewport width."
|
||||
(let (rerender-arguments)
|
||||
(cl-letf (((symbol-function 'get-buffer-window)
|
||||
(lambda (_buffer _all-frames) 'showcase-window))
|
||||
((symbol-function 'window-pixel-width)
|
||||
(lambda (_window) 900))
|
||||
((symbol-function 'window-body-width)
|
||||
(lambda (_window &optional pixelwise) (and pixelwise 880)))
|
||||
((symbol-function 'window-body-height)
|
||||
(lambda (_window) 40))
|
||||
((symbol-function 'window-frame)
|
||||
(lambda (_window) 'showcase-frame))
|
||||
((symbol-function 'frame-char-width)
|
||||
(lambda (_frame) 8))
|
||||
((symbol-function 'ebox-rerender-buffer-with-context)
|
||||
(lambda (&rest arguments)
|
||||
(setq rerender-arguments arguments))))
|
||||
(etaf-playground--showcase-sync-viewport "*Showcase*")
|
||||
(should (equal rerender-arguments '("*Showcase*" 872 40))))))
|
||||
(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)
|
||||
|
||||
;;; etaf-playground-tests.el ends here
|
||||
|
||||
Loading…
Reference in New Issue
Block a user