feat: polish the responsive ETAF showcase
Use a single display-safe viewport owner, responsive Flex sections, and the warm Ebox Flex reference visual language across the Showcase and compact examples.\n\nVerification: make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs; GUI single-window screenshots for compact/fullscreen Overview, Work items interaction, dark Theme, core, and optional UI views.
This commit is contained in:
parent
092ea9d631
commit
082eff2f6c
82
DESIGN.md
Normal file
82
DESIGN.md
Normal file
@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Open questions
|
||||
- [ ] Expand the official `etaf-ui` catalog before presenting the catalog example as a migration-complete component gallery / maintainer / blocks parity claims.
|
||||
82
DESIGN.zh-CN.md
Normal file
82
DESIGN.zh-CN.md
Normal file
@ -0,0 +1,82 @@
|
||||
# 设计规范
|
||||
|
||||
## 事实来源
|
||||
- 状态:生效中
|
||||
- 最近更新: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 渲染。
|
||||
|
||||
## 品牌
|
||||
- 气质:精确、克制、现代、技术感强,并且明显经过设计,而不是通用框架默认外观。
|
||||
- 可信信号:精确对齐、清晰对比度、更新时稳定的几何结构、一致的语义颜色,以及能够真实演示 ETAF 公共模型的例子。
|
||||
- 避免:未加样式的默认 Emacs 文本、过度饱和的仪表盘颜色、浅色背景上的低对比文字、装饰噪音、无意义的大面积留白,以及与普通正文无法区分的控件。
|
||||
|
||||
## 产品目标
|
||||
- 目标:通过精致、可运行的应用体现 ETAF 的能力;用可见结构讲清 View、Component、Context、Data、Grid、保留状态与事件;所有例子只使用公共 API。
|
||||
- 非目标:模仿浏览器、使用位图装饰、另建样式框架、加入仅供示例使用的隐藏渲染路径,或假装当前较小的 `etaf-ui` 目录已经功能完整。
|
||||
- 成功信号:首屏具有明确的应用结构;导航、数据和主题更新保持层级与几何稳定;截图中没有裁切、续行标记、意外换行或低对比文字。
|
||||
|
||||
## 用户与任务
|
||||
- 主要用户:ETAF 应用作者、Ebox/ETAF 维护者,以及评估新架构是否可用的人。
|
||||
- 用户任务:快速理解框架、复制公共 API 模式、检查真实交互流程,并确认更新后视觉结构稳定。
|
||||
- 主要环境:GUI Emacs 单应用缓冲区、全屏演示,以及批处理/ERT 验证。
|
||||
|
||||
## 信息架构
|
||||
- 主导航:固定的横向命令条,包含 Overview、Work items 与 Theme。
|
||||
- 核心界面:Overview 仪表盘、交互式 Work items 表格、Theme and semantics 色彩页、紧凑的核心计数器例子,以及可选的 `etaf-ui` 组件目录例子。
|
||||
- 内容层级:品牌应用标题、固定导航、页面标题与说明、主要卡片或表格、上下文操作区,以及安静的状态栏。
|
||||
|
||||
## 设计原则
|
||||
- 原则一:通过可见分组教学;每项能力都放在有标题、有边界的区域中,而不是混成一串文本。
|
||||
- 原则二:沿用 Flex 参考示例的暖色、克制配色与一像素边框,不依赖阴影或浏览器专有视觉效果。
|
||||
- 原则三:几何正确性就是功能正确性;viewport owner 必须包含自身边框,子区域必须适配 owner 的内容盒,更新不得引入换行或截断标记。
|
||||
- 取舍:优先采用适合桌面 Emacs 的紧凑构图,而不是过量留白;内部响应式容器省略宽度,让 Ebox Flex 分配和换行,避免脆弱的多层 viewport 算术。
|
||||
|
||||
## 视觉语言
|
||||
- 颜色:暖白画布 `#F8F5EE`;纸张 `#FFFDF8`;正文 `#252A2E`;弱化文字 `#66706A`;激活陶土色 `#B84F35`;淡陶土色 `#F1D4C9`;鼠尾草绿 `#DCEBDD`;蓝色 `#D9EAF2`;紫色 `#E7E2F1`;暗色模式使用深中性色表面和同一组语义色的亮色版本。
|
||||
- 字体:沿用 Emacs 配置的等宽字体;粗体只用于标题、标签、关键数值和激活控件。
|
||||
- 间距:纵向以一行作为节奏;横向间距 12–16px;区块内边距 16–24px;卡片与表格边缘对齐。
|
||||
- 形状与层级:方正的文本原生表面、一像素边框,不模拟圆角或阴影。
|
||||
- 动效:状态直接发布,并在 redisplay 后保持稳定;不使用装饰动画。
|
||||
- 图像与图标:只使用文字与克制的语义符号,不使用 logo、emoji 装饰或位图外框。
|
||||
|
||||
## 组件
|
||||
- 复用能力: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 即可使用。
|
||||
|
||||
## 响应式行为
|
||||
- 支持范围:约 900px 宽的 GUI Emacs 窗口到全屏桌面宽度;验证时还使用 777px 可用宽度作为更严格下限。
|
||||
- 布局适配:宿主测量窗口正文,并在右侧预留一个字符单元;最外层 shell 拥有 viewport 宽度;直接的 header/main/status 区域在其中 stretch;嵌套响应式容器省略宽度,由 Ebox Flex 分配或换行。
|
||||
- 触摸/悬停:不适用;键盘与鼠标 press 共用公共事件模型。
|
||||
|
||||
## 交互状态
|
||||
- Loading:有标签、有边界,并显示明确状态。
|
||||
- Empty:安静的解释区域;可用时给出下一步操作。
|
||||
- Error:高对比 danger 区域;错误保持可见,不静默回退。
|
||||
- Success:鼠尾草绿强调色与明确反馈文本。
|
||||
- Disabled:弱化前景色,并移除回调与 tab stop。
|
||||
- 离线/慢网络:当前不适用,因为示例使用同步内存数据源。
|
||||
|
||||
## 内容语气
|
||||
- 风格:直接、技术化、简洁、自解释。
|
||||
- 术语:统一使用 ETAF 词汇:View、Component、Host、Context、Data Controller、Runtime、event 与 public API。
|
||||
- 文案规则:说清正在演示的能力和操作后的可见结果,避免宣传式填充文字。
|
||||
|
||||
## 实现约束
|
||||
- 技术边界: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` 目录后,才能把目录示例表述为完成迁移的组件画廊;维护者;阻止错误的功能对等声明。
|
||||
@ -14,6 +14,8 @@ For the complete interactive example, open the Showcase:
|
||||
(etaf-playground-open-showcase)
|
||||
```
|
||||
|
||||
The Showcase is intentionally built only from public ETAF APIs. It demonstrates a Grid 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. The `etaf-playground-open-ui` entry remains the smaller optional example for the official `etaf-ui` catalog.
|
||||
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.
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
(etaf-playground-open-showcase)
|
||||
```
|
||||
|
||||
Showcase 只使用 ETAF 公共 API,演示 Grid 外壳、根画布背景与局部面板背景的分层、基于 Context 的主题值、响应式状态、内存 Data Controller、带 key 的行以及公共事件分发。`etaf-playground-open-ui` 仍然是展示官方 `etaf-ui` 目录的较小可选示例。
|
||||
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`。
|
||||
|
||||
@ -32,39 +32,59 @@
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-playground-counter"
|
||||
(text :face 'bold (expr :value title))
|
||||
(text (expr :value (format "Count: %d" (etaf-value count))))
|
||||
(row
|
||||
: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 "#E8EEF7" :bgcolor "#151A23" :surface "#202938"
|
||||
:surface-alt "#29364A" :muted "#AAB7CC" :accent "#79B8FF"
|
||||
:accent-strong "#A7D0FF" :success "#70D6A0" :warning "#F5CA78"
|
||||
:danger "#FF8A8A" :border "#52637E")
|
||||
'(:color "#263244" :bgcolor "#F4F6FB" :surface "#FFFFFF"
|
||||
:surface-alt "#E8EDF5" :muted "#687386" :accent "#245EA8"
|
||||
:accent-strong "#173D70" :success "#247A50" :warning "#986B13"
|
||||
:danger "#B83C45" :border "#8A93A6")))
|
||||
'(: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 (window-pixel-width window))
|
||||
(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)))))
|
||||
@ -105,31 +125,38 @@ any frame lifecycle state."
|
||||
:padding '(0 (10))
|
||||
(expr :value label))))
|
||||
|
||||
(defun etaf-playground--showcase-metric (label value palette)
|
||||
"Return a metric card for LABEL, VALUE, and PALETTE."
|
||||
(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"
|
||||
:width '(280)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
: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 :muted)
|
||||
:color (etaf-playground--showcase-palette-value palette :color)
|
||||
(expr :value label))
|
||||
(text
|
||||
:face 'bold
|
||||
:color (etaf-playground--showcase-palette-value palette :accent)
|
||||
: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
|
||||
(column
|
||||
:class "etaf-showcase-sidebar"
|
||||
:width '(220)
|
||||
(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 "NAVIGATION")
|
||||
(text :face 'bold :flex-shrink 0 "NAVIGATION")
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
"Overview" 'showcase-nav-overview
|
||||
@ -146,8 +173,10 @@ any frame lifecycle state."
|
||||
(lambda () (funcall on-page 'theme)) palette
|
||||
(eq page 'theme)))
|
||||
(text
|
||||
:flex-grow 1
|
||||
:flex-basis '(180)
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"\nKeyboard: Tab, RET"))))
|
||||
"Keyboard: Tab, RET"))))
|
||||
|
||||
(defun etaf-playground--showcase-page-title (page)
|
||||
"Return the display title for Showcase PAGE."
|
||||
@ -173,10 +202,10 @@ any frame lifecycle state."
|
||||
:bgcolor (etaf-playground--showcase-palette-value
|
||||
palette (if selected-p :surface-alt :surface))
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :width '(110) (expr :value id))
|
||||
(text :width '(330) (expr :value (plist-get task :title)))
|
||||
(text :width '(170) (expr :value (plist-get task :owner)))
|
||||
(text :width '(150)
|
||||
(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")
|
||||
@ -192,10 +221,10 @@ any frame lifecycle state."
|
||||
(row
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface-alt)
|
||||
:border (etaf-playground--showcase-border palette)
|
||||
(text :face 'bold :width '(110) "ID")
|
||||
(text :face 'bold :width '(330) "TASK")
|
||||
(text :face 'bold :width '(170) "OWNER")
|
||||
(text :face 'bold :width '(150) "STATUS"))
|
||||
(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)
|
||||
@ -217,27 +246,32 @@ PALETTE, CONTROLLER, NOTICE, and ACTIONS provide its live data and behavior."
|
||||
(text :face 'bold "A complete text application in one View tree")
|
||||
(text
|
||||
:color (etaf-playground--showcase-palette-value palette :muted)
|
||||
"The shell, Grid cards, retained state, Data Controller, and events all share one model.")
|
||||
(grid
|
||||
:width '(880)
|
||||
:grid-template-columns '((280) (280) (280))
|
||||
"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)) palette))
|
||||
"VISIBLE WORK ITEMS" (format "%d" (length items))
|
||||
:terracotta palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-metric
|
||||
"SELECTED" (format "%d" selected-count) palette))
|
||||
"SELECTED" (format "%d" selected-count) :sage palette))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-metric
|
||||
"RUNTIME STATUS"
|
||||
(capitalize (symbol-name (etaf-value
|
||||
(etaf-data-status controller))))
|
||||
palette)))
|
||||
(row
|
||||
:gap '(0 (12))
|
||||
:blue palette)))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (12))
|
||||
(column
|
||||
:width '(430)
|
||||
: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")
|
||||
@ -248,8 +282,12 @@ PALETTE, CONTROLLER, NOTICE, and ACTIONS provide its live data and behavior."
|
||||
"Open Work items" 'showcase-open-work-items
|
||||
(plist-get actions :open-work-items) palette)))
|
||||
(column
|
||||
:width '(430)
|
||||
:bgcolor (etaf-playground--showcase-palette-value palette :surface)
|
||||
: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
|
||||
@ -270,9 +308,13 @@ live state."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-section"
|
||||
(row
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (10))
|
||||
(column
|
||||
:width '(520)
|
||||
: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"
|
||||
@ -291,7 +333,8 @@ live state."
|
||||
(expr :value
|
||||
(etaf-playground--showcase-task-table
|
||||
items selected-id palette (plist-get actions :select-task)))
|
||||
(row
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(0 (12))
|
||||
(expr :value
|
||||
(etaf-playground--showcase-button
|
||||
@ -306,6 +349,7 @@ live state."
|
||||
"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")
|
||||
@ -330,8 +374,12 @@ live state."
|
||||
"Return a color swatch for LABEL, COLOR, FOREGROUND, and PALETTE."
|
||||
(etaf-view
|
||||
(column
|
||||
:width '(280)
|
||||
: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)
|
||||
@ -347,16 +395,17 @@ live state."
|
||||
(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.")
|
||||
(row
|
||||
(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)))
|
||||
(grid
|
||||
:width '(880)
|
||||
:grid-template-columns '((280) (280) (280))
|
||||
(flex
|
||||
:flex-flow '(row wrap)
|
||||
:gap '(1 (12))
|
||||
(expr :value
|
||||
(mapcar
|
||||
@ -373,6 +422,11 @@ live state."
|
||||
(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")
|
||||
@ -389,23 +443,26 @@ and TITLE provide its live state and presentation."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-showcase-root"
|
||||
:width '(viewport)
|
||||
: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)
|
||||
(row
|
||||
(flex
|
||||
:class "etaf-showcase-header"
|
||||
:width '(viewport)
|
||||
: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 :width '(220)
|
||||
(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 :width '(650)
|
||||
(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)))
|
||||
@ -417,27 +474,28 @@ and TITLE provide its live state and presentation."
|
||||
(etaf-playground--showcase-button
|
||||
(if dark-p "Light" "Dark") 'showcase-header-theme-toggle
|
||||
(plist-get actions :toggle-theme) palette)))
|
||||
(row
|
||||
:width '(viewport)
|
||||
(column
|
||||
:width 'stretch
|
||||
:color (etaf-playground--showcase-palette-value palette :color)
|
||||
(expr :value
|
||||
(etaf-playground--showcase-nav
|
||||
page palette (plist-get actions :set-page)))
|
||||
(column
|
||||
:width '(880)
|
||||
(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))))))
|
||||
(row
|
||||
(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 '(viewport)
|
||||
: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)))
|
||||
@ -567,11 +625,15 @@ and TITLE provide its live state and presentation."
|
||||
(etaf-view
|
||||
(column
|
||||
:class "etaf-playground-root"
|
||||
(text :face 'bold "ETAF Playground")
|
||||
(text :color "#687386"
|
||||
: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 "#687386"
|
||||
(text :color "#66706A"
|
||||
"Press + or - through the public event dispatch path."))))
|
||||
|
||||
;;;###autoload
|
||||
@ -580,6 +642,10 @@ and TITLE provide its live state and presentation."
|
||||
(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")))
|
||||
@ -612,8 +678,9 @@ and TITLE provide its live state and presentation."
|
||||
"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 Grid layout, Context theme defaults, reactive state,
|
||||
Data Controller updates, stable row keys, and public event dispatch."
|
||||
Showcase demonstrates responsive Flex layout, Context theme defaults,
|
||||
reactive state, Data Controller updates, stable row keys, and public event
|
||||
dispatch."
|
||||
(interactive)
|
||||
(let* ((buffer
|
||||
(etaf-mount (or buffer-name etaf-playground-showcase-buffer-name)
|
||||
|
||||
@ -3,6 +3,12 @@
|
||||
(require 'ert)
|
||||
(require 'etaf-playground)
|
||||
|
||||
(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)))
|
||||
|
||||
(ert-deftest etaf-playground-mounts-and-dispatches-core-example ()
|
||||
"Mount the public playground and update its retained counter."
|
||||
(let ((buffer-name " *etaf-playground-test*"))
|
||||
@ -83,16 +89,67 @@
|
||||
(root (etaf-runtime-root-node runtime))
|
||||
(layout (plist-get root :ebox-content-node))
|
||||
(children (plist-get layout :children)))
|
||||
(should (equal (plist-get root :bgcolor) "#F4F6FB"))
|
||||
(should (equal (plist-get root :bgcolor) "#F8F5EE"))
|
||||
(should-not (plist-member root :surface-properties))
|
||||
(should (equal (mapcar (lambda (node) (plist-get node :bgcolor))
|
||||
(should (equal (mapcar (lambda (node)
|
||||
(etaf-playground-test--layout-property
|
||||
node :bgcolor))
|
||||
children)
|
||||
'("#FFFFFF" nil "#FFFFFF")))))
|
||||
'("#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)))))
|
||||
|
||||
(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))
|
||||
(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)))))
|
||||
|
||||
(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))))))
|
||||
|
||||
(provide 'etaf-playground-tests)
|
||||
|
||||
;;; etaf-playground-tests.el ends here
|
||||
|
||||
Loading…
Reference in New Issue
Block a user