fix: restore generic Ebox DSL file playground

Move the maintained gallery into a readable .ebox fixture and make ebox-playground own generic parsing, file mode, and public rendering. Keep the startup mode trigger deferred through the user's use-package configuration.

Verified with ebox-playground make check (6/6), etaf-playground make check (6/6), exact use-package startup harness, and fullscreen GUI capture at /tmp/ebox-playground-dsl-gallery.png.
This commit is contained in:
Kinneyzhang 2026-08-05 19:46:45 +08:00
parent 35f8eb80ef
commit 748a578c31
9 changed files with 256 additions and 138 deletions

View File

@ -3,8 +3,8 @@
## Source of truth
- Status: Active
- Last refreshed: 2026-08-05
- Primary product surfaces: The buffer opened by `ebox-playground-open` and its public layout example tree.
- Evidence reviewed: `ebox-playground.el`, `tests/ebox-playground-tests.el`, `README.md`, `README.zh-CN.md`, the rendered current example, and `../emacs-box/examples/playground/flex-reference.ebox` rendered fullscreen in GUI Emacs.
- Primary product surfaces: The `.ebox` files under `examples/`, the buffer opened by `ebox-playground-open`, and the generic file mode.
- Evidence reviewed: `ebox-playground.el`, `examples/public-layout-gallery.ebox`, `tests/ebox-playground-tests.el`, `README.md`, `README.zh-CN.md`, and the rendered current example.
## Brand
- Personality: Precise, modern, restrained, and educational.
@ -29,7 +29,7 @@
## Design principles
- Principle 1: Mirror the Flex reference's title—explanation—example rhythm.
- Principle 2: Use one restrained semantic accent per capability section.
- Principle 3: Keep the example public-only and small enough to understand from one source file.
- Principle 3: Keep the runner generic; put each concrete layout in a readable `.ebox` source file.
- Tradeoffs: Prefer a compact representative gallery over exhaustive property coverage.
## Visual language
@ -44,7 +44,7 @@
- Existing components to reuse: `ebox-create`, `ebox-column`, `ebox-row`, `ebox-flex`, `ebox-grid`, and `ebox-spacer`.
- New/changed components: Header band, capability section band, fixed/fractional track cards, placement cards, and public-contract footer.
- Variants and states: Static semantic color families only.
- Token/component ownership: Palette constants and example composition remain in `ebox-playground.el`; Ebox owns rendering.
- Token/component ownership: Palette values and example composition live in `.ebox` fixtures; the runner owns only parsing and rendering.
## Accessibility
- Target standard: High contrast and readable labels in ordinary GUI Emacs themes.
@ -79,4 +79,4 @@
- Test/screenshot expectations: `make check` passes and a fullscreen one-window screenshot shows complete, unclipped labeled sections.
## Open questions
- [ ] Add more standalone examples only when each teaches a distinct public Ebox responsibility / maintainer / prevents fixture sprawl.
- [ ] Add more standalone `.ebox` fixtures only when each teaches a distinct public Ebox responsibility / maintainer / prevents fixture sprawl.

View File

@ -3,8 +3,8 @@
## 真相来源
- 状态:有效
- 最近更新2026-08-05
- 主要产品界面:`ebox-playground-open` 打开的缓冲区及其公开布局示例树
- 已审阅证据:`ebox-playground.el`、`tests/ebox-playground-tests.el`、`README.md`、`README.zh-CN.md`、当前示例的实际渲染,以及在 GUI Emacs 中全屏渲染的 `../emacs-box/examples/playground/flex-reference.ebox`
- 主要产品界面:`examples/` 下的 `.ebox` 文件、`ebox-playground-open` 打开的缓冲区,以及通用文件模式
- 已审阅证据:`ebox-playground.el`、`examples/public-layout-gallery.ebox`、`tests/ebox-playground-tests.el`、`README.md`、`README.zh-CN.md` 和当前示例的实际渲染
## 品牌
- 个性:精确、现代、克制、适合教学。
@ -29,7 +29,7 @@
## 设计原则
- 原则 1延续 Flex 参考示例的“标题—解释—示例”节奏。
- 原则 2每个能力分区只使用一组克制的语义强调色。
- 原则 3示例只使用公开 API并保持在一个源文件内即可理解的规模
- 原则 3运行器保持通用;每个具体布局都放在易读的 `.ebox` 源文件中
- 取舍:优先紧凑且有代表性的画廊,而不是穷举所有属性。
## 视觉语言
@ -44,7 +44,7 @@
- 复用的现有组件:`ebox-create`、`ebox-column`、`ebox-row`、`ebox-flex`、`ebox-grid` 和 `ebox-spacer`
- 新增/调整的组件:标题带、能力分区标题带、固定/分数轨道卡片、放置卡片和公开契约页脚。
- 变体与状态:仅使用静态语义色组。
- Token/组件归属:配色常量和示例组合保留在 `ebox-playground.el`Ebox 负责渲染。
- Token/组件归属:配色值和示例组合放在 `.ebox` fixture 中;运行器只负责解析和渲染。
## 无障碍
- 目标标准:在常见 GUI Emacs 主题下保持高对比度和标签可读性。
@ -79,4 +79,4 @@
- 测试/截图要求:`make check` 通过;单窗口 GUI 截图显示完整且未裁剪的标签分区。
## 开放问题
- [ ] 只有当每个新增示例都能讲清一种独立的公开 Ebox 责任时才继续扩展示例,避免 fixture 膨胀。
- [ ] 只有当每个新增 `.ebox` 示例都能讲清一种独立的公开 Ebox 责任时才继续扩展,避免 fixture 膨胀。

View File

@ -1,6 +1,6 @@
# ebox-playground
`ebox-playground` is the independent developer example package for Ebox. It uses only public Ebox constructors and presents a polished three-part gallery for fixed and fractional tracks, explicit Grid placement, and public Column/Flex/Grid composition. Its restrained terracotta, sage, blue, and violet palette follows the visual standard of the original Flex reference without depending on theme-specific faces.
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. The maintained gallery demonstrates fixed and fractional tracks, explicit Grid placement, and public Column/Flex/Grid composition with a restrained terracotta, sage, blue, and violet palette.
The gallery owns one definite 720 px canvas so its nested Grid examples cannot each reinterpret the surrounding viewport width. It fits a compact GUI Emacs window with at least 760 px of body width. `etaf-playground` remains the separate higher-level Component and Runtime showcase.
@ -9,4 +9,10 @@ The gallery owns one definite 720 px canvas so its nested Grid examples cannot e
(ebox-playground-open)
```
The package also owns the `.ebox` file boundary. Visiting an Ebox DSL file selects `ebox-dsl-mode`; press `C-c C-c` to build the single DSL form and render a preview through the public Ebox API.
```elisp
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
```
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` from this directory.

View File

@ -1,6 +1,6 @@
# ebox-playground
`ebox-playground` 是 Ebox 的独立开发示例包,只使用 Ebox 公共构造函数,并通过一套精心设计的三段式画廊展示固定与分数轨道、显式 Grid 放置,以及公开的 Column/Flex/Grid 组合能力。克制的陶土色、鼠尾草绿、蓝色与紫色配色延续了原 Flex 参考示例的视觉标准,同时不依赖特定主题 face
`ebox-playground` 是 Ebox DSL 的独立通用文件运行器,只使用 Ebox 公共 API具体布局都放在 `examples/` 下易读的 `.ebox` fixture 中。维护中的画廊通过这些源文件展示固定与分数轨道、显式 Grid 放置,以及公开的 Column/Flex/Grid 组合能力,并使用克制的陶土色、鼠尾草绿、蓝色与紫色配色。
画廊统一拥有一个明确的 720 px 画布,因此嵌套 Grid 不会各自重新解释外层视口宽度。只要 GUI Emacs 的正文区域不小于 760 px它就能完整显示。高层 Component/Runtime 展示仍由独立的 `etaf-playground` 提供。
@ -9,4 +9,10 @@
(ebox-playground-open)
```
这个包也拥有 `.ebox` 文件入口。访问 Ebox DSL 文件时会自动进入 `ebox-dsl-mode`;按 `C-c C-c` 会通过 Ebox 公开 API 构建单个 DSL form 并渲染预览。
```elisp
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
```
在该目录运行 `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`

View File

@ -9,150 +9,115 @@
;;; Commentary:
;; This package is intentionally small and public-only. It demonstrates the
;; Ebox contract without reaching into ebox-- internals. ETAF's application
;; examples live in the sibling `etaf-playground' package.
;; Ebox contract without reaching into internal implementation details. ETAF's
;; application examples live in the sibling `etaf-playground' package.
;;; Code:
(require 'ebox)
(require 'elisp-mode)
(require 'subr-x)
(defvar read-eval)
(defconst ebox-playground-buffer-name "*Ebox Playground*"
"Default buffer name used by `ebox-playground-open'.")
(defun ebox-playground--header ()
"Return the gallery header."
(ebox-create
:content "Ebox Playground\nPixel-precise layout, composed only from public nodes."
:width '(720)
:padding '(1 (18))
:border "#8F432F"
:color "#252A2E"
:bgcolor "#FFFDF8"
:face 'bold
:text-align 'center))
(defvar ebox-dsl-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map emacs-lisp-mode-map)
(define-key map (kbd "C-c C-c") #'ebox-dsl-render)
map)
"Keymap used by `ebox-dsl-mode'.")
(defun ebox-playground--section-heading
(number title copy border background)
"Return a section heading for NUMBER, TITLE, COPY, BORDER, and BACKGROUND."
(ebox-create
:content (format "%s %s\n%s" number title copy)
:width '(720)
:padding '(0 (16))
:border border
:border-bottom-p nil
:color "#252A2E"
:bgcolor background
:wrap-mode 'word))
(defun ebox-playground--track-grid ()
"Return the fixed and fractional track example."
(ebox-grid
:width '(718)
:grid-template-columns '((220) 1fr)
:gap '(1 (12))
:border "#C97252"
:bgcolor "#FFF9F5"
(ebox-create :content "FIXED TRACK\n220 px stays exact"
:width '(220) :padding '(1 0) :text-align 'center
:color "#252A2E" :bgcolor "#F1D4C9")
(ebox-create :content "FRACTIONAL TRACK\n1fr receives the remaining width"
:width '(486) :padding '(1 0) :text-align 'center
:color "#252A2E" :bgcolor "#F9E8E0")
(ebox-create :content "The Grid owns one definite width; its tracks divide that space without competing viewport calculations."
:grid-column '(1 :span 2) :width '(718) :padding '(1 0)
:color "#663322" :bgcolor "#FFFDF8"
:text-align 'center :wrap-mode 'word)))
(defun ebox-playground--placement-grid ()
"Return the explicit Grid placement example."
(ebox-grid
:width '(718)
:grid-template-columns '((210) (210) 1fr)
:gap '(1 (10))
:border "#6D8A73"
:bgcolor "#F7FBF7"
(ebox-create :content "SPAN 1 → 3" :grid-column '(1 :span 2)
:width '(430) :padding '(1 0) :text-align 'center
:color "#24422D" :bgcolor "#DCEBDD")
(ebox-create :content "COLUMN 3" :grid-column 3 :grid-row 1
:width '(278) :padding '(1 0) :text-align 'center
:color "#24422D" :bgcolor "#C8DDCB")
(ebox-create :content "ROW 2 / COL 1" :grid-column 1 :grid-row 2
:width '(210) :padding '(1 0) :text-align 'center
:color "#24422D" :bgcolor "#EDF5EE")
(ebox-create :content "AUTO PLACED" :width '(210) :padding '(1 0)
:text-align 'center
:color "#24422D" :bgcolor "#E3F0E5")
(ebox-create :content "STABLE CELLS" :width '(278) :padding '(1 0)
:text-align 'center
:color "#24422D" :bgcolor "#D4E6D7")))
(defun ebox-playground--composition-flex ()
"Return the public node composition example."
(ebox-flex
:width '(720)
:flex-flow '(row wrap)
:gap '(1 (12))
:padding '(1 (12))
:border "#4E7890"
:bgcolor "#F7FAFC"
(ebox-create :content "COLUMN\nvertical composition"
:flex-grow 1 :flex-shrink 1 :flex-basis '(200)
:min-width '(180) :padding '(1 (12))
:color "#193846" :bgcolor "#D9EAF2")
(ebox-create :content "FLEX\nwrap and distribute"
:flex-grow 1 :flex-shrink 1 :flex-basis '(200)
:min-width '(180) :padding '(1 (12))
:color "#3F3655" :bgcolor "#E7E2F1")
(ebox-create :content "GRID\nplace in two dimensions"
:flex-grow 1 :flex-shrink 1 :flex-basis '(200)
:min-width '(180) :padding '(1 (12))
:color "#6B3020" :bgcolor "#F1D4C9")))
(defun ebox-playground--footer ()
"Return the public-contract footer."
(ebox-create
:content "PUBLIC CONTRACT ebox-create · ebox-column · ebox-flex · ebox-grid · ebox-spacer"
:width '(720)
:padding '(0 (16))
:border "#8D887F"
:color "#4D5651"
:bgcolor "#EEEAE2"
:wrap-mode 'word))
(defun ebox-playground--source-form ()
"Read the single Ebox DSL form from the current buffer."
(ebox-playground--read-form
(buffer-substring-no-properties (point-min) (point-max))))
;;;###autoload
(defun ebox-playground-view ()
"Return a public Ebox example layout node without buffer side effects."
(ebox-column
(ebox-playground--header)
(ebox-spacer :height 1)
(ebox-playground--section-heading
"01" "FIXED + FRACTIONAL"
"A definite Grid width divides into one exact track and one flexible track."
"#C97252" "#F1D4C9")
(ebox-playground--track-grid)
(ebox-spacer :height 1)
(ebox-playground--section-heading
"02" "EXPLICIT PLACEMENT"
"Span columns, target cells, and leave the remaining nodes to auto placement."
"#6D8A73" "#DCEBDD")
(ebox-playground--placement-grid)
(ebox-spacer :height 1)
(ebox-playground--section-heading
"03" "PUBLIC COMPOSITION"
"Column, Flex, and Grid share the same public node contract."
"#4E7890" "#D9EAF2")
(ebox-playground--composition-flex)
(ebox-spacer :height 1)
(ebox-playground--footer)))
(define-derived-mode ebox-dsl-mode emacs-lisp-mode "EboxDSL"
"Major mode for Ebox DSL source files."
(setq-local truncate-lines nil)
(setq-local bidi-display-reordering nil)
(setq-local bidi-paragraph-direction 'left-to-right))
;;;###autoload
(defun ebox-dsl-render ()
"Render the current Ebox DSL buffer into a preview buffer."
(interactive)
(let* ((source (ebox-playground--source-form))
(name (format "*Ebox Preview: %s*"
(or (buffer-file-name) (buffer-name))))
(node (ebox-build source)))
(ebox-render-to-buffer name node)
(message "Rendered Ebox DSL into %s" name)
(get-buffer name)))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
(defgroup ebox-playground nil
"Run Ebox DSL examples from source files."
:group 'ebox)
(defconst ebox-playground-directory
(file-name-directory (or load-file-name buffer-file-name))
"Directory containing the Ebox Playground package.")
(defcustom ebox-playground-default-file
(expand-file-name "examples/public-layout-gallery.ebox"
ebox-playground-directory)
"Ebox DSL file rendered by `ebox-playground-open'."
:type 'file
:group 'ebox-playground)
(defun ebox-playground--read-form (source)
"Read one Ebox DSL form from SOURCE."
(when (string-empty-p (string-trim source))
(user-error "The Ebox DSL source is empty"))
(with-temp-buffer
(insert source)
(goto-char (point-min))
(let ((read-eval nil)
(form (read (current-buffer))))
(condition-case nil
(progn
(read (current-buffer))
(user-error "The Ebox DSL source must contain one form"))
(end-of-file form)))))
(defun ebox-playground--read-file (file)
"Read one Ebox DSL form from FILE."
(unless (file-readable-p file)
(user-error "Ebox DSL file is not readable: %s" file))
(with-temp-buffer
(insert-file-contents file)
(ebox-playground--read-form
(buffer-substring-no-properties (point-min) (point-max)))))
;;;###autoload
(defun ebox-playground-view (&optional file)
"Build the Ebox DSL form in FILE without buffer side effects."
(ebox-build
(ebox-playground--read-file
(expand-file-name (or file ebox-playground-default-file)))))
;;;###autoload
(defun ebox-playground-open (&optional buffer-name)
"Render the public Ebox example into BUFFER-NAME and return its buffer."
"Render the default Ebox DSL example into BUFFER-NAME and return its buffer."
(interactive)
(ebox-render-to-buffer (or buffer-name ebox-playground-buffer-name)
(ebox-playground-view)))
;;;###autoload
(defun ebox-playground-open-file (file &optional buffer-name)
"Render Ebox DSL FILE into BUFFER-NAME and return its buffer."
(interactive "fEbox DSL file: ")
(ebox-render-to-buffer
(or buffer-name
(format "*Ebox Preview: %s*" (file-name-nondirectory file)))
(ebox-playground-view file)))
;;;###autoload
(defun ebox-playground-close (&optional buffer-name)
"Kill the playground BUFFER-NAME and return its former buffer."

5
examples/README.md Normal file
View File

@ -0,0 +1,5 @@
# Ebox Playground examples
The `.ebox` files in this directory are the concrete layouts consumed by the generic `ebox-playground` runner. They are intentionally ordinary, reader-valid Ebox DSL forms so authors can copy, edit, and open them directly.
`public-layout-gallery.ebox` is the default fixture used by `M-x ebox-playground-open`. To render another fixture, visit it and press `C-c C-c`, or call `ebox-playground-open-file`.

5
examples/README.zh-CN.md Normal file
View File

@ -0,0 +1,5 @@
# Ebox Playground 示例
此目录下的 `.ebox` 文件是通用 `ebox-playground` 运行器读取的具体布局。它们保持为普通、可读、可被 reader 读取的 Ebox DSL form便于直接复制、编辑和打开。
`public-layout-gallery.ebox``M-x ebox-playground-open` 使用的默认 fixture。要渲染其他 fixture可以访问该文件后按 `C-c C-c`,或调用 `ebox-playground-open-file`

View File

@ -0,0 +1,98 @@
(column
(box :content "Ebox Playground\nPixel-precise layout, composed only from public nodes."
:width (720)
:padding (1 (18))
:border "#8F432F"
:color "#252A2E"
:bgcolor "#FFFDF8"
:face bold
:text-align center)
(spacer :height 1)
(box :content "01 FIXED + FRACTIONAL\nA definite Grid width divides into one exact track and one flexible track."
:width (720)
:padding (0 (16))
:border "#C97252"
:border-bottom-p nil
:color "#252A2E"
:bgcolor "#F1D4C9"
:wrap-mode word)
(grid
:width (718)
:grid-template-columns ((220) 1fr)
:gap (1 (12))
:border "#C97252"
:bgcolor "#FFF9F5"
(box :content "FIXED TRACK\n220 px stays exact"
:width (220) :padding (1 0) :text-align center
:color "#252A2E" :bgcolor "#F1D4C9")
(box :content "FRACTIONAL TRACK\n1fr receives the remaining width"
:width (486) :padding (1 0) :text-align center
:color "#252A2E" :bgcolor "#F9E8E0")
(box :content "The Grid owns one definite width; its tracks divide that space without competing viewport calculations."
:grid-column (1 :span 2) :width (718) :padding (1 0)
:color "#663322" :bgcolor "#FFFDF8"
:text-align center :wrap-mode word))
(spacer :height 1)
(box :content "02 EXPLICIT PLACEMENT\nSpan columns, target cells, and leave the remaining nodes to auto placement."
:width (720)
:padding (0 (16))
:border "#6D8A73"
:border-bottom-p nil
:color "#252A2E"
:bgcolor "#DCEBDD"
:wrap-mode word)
(grid
:width (718)
:grid-template-columns ((210) (210) 1fr)
:gap (1 (10))
:border "#6D8A73"
:bgcolor "#F7FBF7"
(box :content "SPAN 1 → 3" :grid-column (1 :span 2)
:width (430) :padding (1 0) :text-align center
:color "#24422D" :bgcolor "#DCEBDD")
(box :content "COLUMN 3" :grid-column 3 :grid-row 1
:width (278) :padding (1 0) :text-align center
:color "#24422D" :bgcolor "#C8DDCB")
(box :content "ROW 2 / COL 1" :grid-column 1 :grid-row 2
:width (210) :padding (1 0) :text-align center
:color "#24422D" :bgcolor "#EDF5EE")
(box :content "AUTO PLACED" :width (210) :padding (1 0)
:text-align center :color "#24422D" :bgcolor "#E3F0E5")
(box :content "STABLE CELLS" :width (278) :padding (1 0)
:text-align center :color "#24422D" :bgcolor "#D4E6D7"))
(spacer :height 1)
(box :content "03 PUBLIC COMPOSITION\nColumn, Flex, and Grid share the same public node contract."
:width (720)
:padding (0 (16))
:border "#4E7890"
:border-bottom-p nil
:color "#252A2E"
:bgcolor "#D9EAF2"
:wrap-mode word)
(flex
:width (720)
:flex-flow (row wrap)
:gap (1 (12))
:padding (1 (12))
:border "#4E7890"
:bgcolor "#F7FAFC"
(box :content "COLUMN\nvertical composition"
:flex-grow 1 :flex-shrink 1 :flex-basis (200)
:min-width (180) :padding (1 (12))
:color "#193846" :bgcolor "#D9EAF2")
(box :content "FLEX\nwrap and distribute"
:flex-grow 1 :flex-shrink 1 :flex-basis (200)
:min-width (180) :padding (1 (12))
:color "#3F3655" :bgcolor "#E7E2F1")
(box :content "GRID\nplace in two dimensions"
:flex-grow 1 :flex-shrink 1 :flex-basis (200)
:min-width (180) :padding (1 (12))
:color "#6B3020" :bgcolor "#F1D4C9"))
(spacer :height 1)
(box :content "PUBLIC CONTRACT ebox-create · ebox-column · ebox-flex · ebox-grid · ebox-spacer"
:width (720)
:padding (0 (16))
:border "#8D887F"
:color "#4D5651"
:bgcolor "#EEEAE2"
:wrap-mode word))

View File

@ -12,6 +12,18 @@
"EXPLICIT PLACEMENT" "PUBLIC COMPOSITION"))
(should (string-match-p (regexp-quote label) rendered)))))
(ert-deftest ebox-playground-keeps-layout-in-ebox-source ()
"Keep concrete gallery content out of the generic Elisp runner."
(let ((runner (with-temp-buffer
(insert-file-contents
(expand-file-name "ebox-playground.el"
ebox-playground-directory))
(buffer-string))))
(should (file-readable-p ebox-playground-default-file))
(should (string-suffix-p ".ebox" ebox-playground-default-file))
(should-not (string-match-p "FIXED TRACK" runner))
(should-not (string-match-p "PUBLIC COMPOSITION" runner))))
(ert-deftest ebox-playground-opens-and-closes-buffer ()
"The public open and close commands should own their buffer lifecycle."
(let ((name " *ebox-playground-test*"))
@ -34,6 +46,27 @@
(should (= maximum gallery-width))
(should (<= gallery-width 760))))
(ert-deftest ebox-playground-registers-ebox-file-mode ()
"Open Ebox DSL files in the package-owned major mode."
(with-temp-buffer
(setq buffer-file-name "/tmp/ebox-playground-mode-test.ebox")
(set-auto-mode)
(should (eq major-mode 'ebox-dsl-mode))))
(ert-deftest ebox-playground-renders-ebox-file-mode-source ()
"Render a file-mode buffer through the public Ebox build path."
(let ((preview "*Ebox Preview: /tmp/ebox-playground-render-test.ebox*"))
(unwind-protect
(with-temp-buffer
(setq buffer-file-name "/tmp/ebox-playground-render-test.ebox")
(insert "(box :content \"Rendered from .ebox\" :width (240))")
(ebox-dsl-mode)
(ebox-dsl-render)
(with-current-buffer preview
(should (string-match-p "Rendered from .ebox" (buffer-string)))))
(when (get-buffer preview)
(kill-buffer preview)))))
(provide 'ebox-playground-tests)
;;; ebox-playground-tests.el ends here