boxframe 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. boxframe-0.1.0/.github/workflows/publish.yml +49 -0
  2. boxframe-0.1.0/.github/workflows/static.yml +43 -0
  3. boxframe-0.1.0/.gitignore +16 -0
  4. boxframe-0.1.0/AGENTS.md +206 -0
  5. boxframe-0.1.0/Dockerfile +12 -0
  6. boxframe-0.1.0/PKG-INFO +370 -0
  7. boxframe-0.1.0/README.md +349 -0
  8. boxframe-0.1.0/boxframe/__init__.py +8 -0
  9. boxframe-0.1.0/boxframe/api/__init__.py +4 -0
  10. boxframe-0.1.0/boxframe/api/layouts.py +349 -0
  11. boxframe-0.1.0/boxframe/api/projects.py +143 -0
  12. boxframe-0.1.0/boxframe/api/test_cleanup.py +62 -0
  13. boxframe-0.1.0/boxframe/config.py +14 -0
  14. boxframe-0.1.0/boxframe/database.py +62 -0
  15. boxframe-0.1.0/boxframe/main.py +177 -0
  16. boxframe-0.1.0/boxframe/models/__init__.py +5 -0
  17. boxframe-0.1.0/boxframe/models/block.py +51 -0
  18. boxframe-0.1.0/boxframe/models/layout.py +29 -0
  19. boxframe-0.1.0/boxframe/models/project.py +18 -0
  20. boxframe-0.1.0/boxframe/services/__init__.py +11 -0
  21. boxframe-0.1.0/boxframe/services/layout_service.py +339 -0
  22. boxframe-0.1.0/boxframe/services/renderer.py +546 -0
  23. boxframe-0.1.0/boxframe/static/css/editor.css +553 -0
  24. boxframe-0.1.0/boxframe/static/css/style.css +211 -0
  25. boxframe-0.1.0/boxframe/static/editor/index.html +183 -0
  26. boxframe-0.1.0/boxframe/static/index.html +23 -0
  27. boxframe-0.1.0/boxframe/static/js/alpine.min.js +5 -0
  28. boxframe-0.1.0/boxframe/static/js/core/history.js +368 -0
  29. boxframe-0.1.0/boxframe/static/js/core/renderer.js +570 -0
  30. boxframe-0.1.0/boxframe/static/js/core/store.js +388 -0
  31. boxframe-0.1.0/boxframe/static/js/editor.js +1081 -0
  32. boxframe-0.1.0/boxframe/templates/pages/404.html +11 -0
  33. boxframe-0.1.0/boxframe/templates/pages/base.html +28 -0
  34. boxframe-0.1.0/boxframe/templates/pages/editor.html +190 -0
  35. boxframe-0.1.0/boxframe/templates/pages/project.html +76 -0
  36. boxframe-0.1.0/boxframe/templates/pages/projects.html +55 -0
  37. boxframe-0.1.0/index.html +22 -0
  38. boxframe-0.1.0/pyproject.toml +40 -0
  39. boxframe-0.1.0/requirements.txt +9 -0
  40. boxframe-0.1.0/tasks/0000-template.md +25 -0
  41. boxframe-0.1.0/tasks/0001-0010/0001-fix-startup-and-add-testing.md +78 -0
  42. boxframe-0.1.0/tasks/0001-0010/0002-e2e-tests-cleanup.md +52 -0
  43. boxframe-0.1.0/tasks/0001-0010/0003-fix-new-layout-button.md +94 -0
  44. boxframe-0.1.0/tasks/0001-0010/0004-e2e-test-data-cleanup.md +50 -0
  45. boxframe-0.1.0/tasks/0001-0010/0005-add-server-logging.md +41 -0
  46. boxframe-0.1.0/tasks/0001-0010/0006-drag-drop-and-project-page-fix.md +84 -0
  47. boxframe-0.1.0/tasks/0001-0010/0007-add-block-resize-by-corner.md +85 -0
  48. boxframe-0.1.0/tasks/0001-0010/0008-add-block-z-order.md +115 -0
  49. boxframe-0.1.0/tasks/0001-0010/0009-auto-order-assignment.md +98 -0
  50. boxframe-0.1.0/tasks/0001-0010/0010-boxframe-layout.md +139 -0
  51. boxframe-0.1.0/tasks/0011-0020/0011-delete-layout-from-project.md +85 -0
  52. boxframe-0.1.0/tasks/0011-0020/0012-edit-block-content-inline.md +112 -0
  53. boxframe-0.1.0/tasks/0011-0020/0013-word-wrap-in-pseudo-graphics.md +81 -0
  54. boxframe-0.1.0/tasks/0011-0020/0014-line-blocks-and-border-styles.md +133 -0
  55. boxframe-0.1.0/tasks/0011-0020/0015-show-frame-for-none-border.md +87 -0
  56. boxframe-0.1.0/tasks/0011-0020/0016-single-click-block-selection.md +135 -0
  57. boxframe-0.1.0/tasks/0011-0020/0017-block-properties-panel.md +118 -0
  58. boxframe-0.1.0/tasks/0011-0020/0018-static-editor.md +143 -0
  59. boxframe-0.1.0/tasks/0011-0020/0019-unrestricted-block-placement.md +134 -0
  60. boxframe-0.1.0/tasks/0011-0020/0020-canvas-art-overlay-alignment.md +127 -0
  61. boxframe-0.1.0/tasks/0021-0030/0021-word-wrap-preserve-spaces.md +111 -0
  62. boxframe-0.1.0/tasks/0021-0030/0022-button.md +146 -0
  63. boxframe-0.1.0/tasks/0021-0030/0023-editor-canvas-ux.md +158 -0
  64. boxframe-0.1.0/tasks/0021-0030/0024-optional-layout-dimensions.md +183 -0
  65. boxframe-0.1.0/tasks/0021-0030/0025-static-editor-persistence.md +155 -0
  66. boxframe-0.1.0/tasks/0021-0030/0026-web-editor-clear-layout.md +155 -0
  67. boxframe-0.1.0/tasks/0021-0030/0027-editor-undo-redo.md +224 -0
  68. boxframe-0.1.0/tasks/0021-0030/0028-persistent-undo-buffer.md +168 -0
  69. boxframe-0.1.0/tasks/0021-0030/0029-pypi-publishing.md +170 -0
  70. boxframe-0.1.0/tasks/README.md +53 -0
  71. boxframe-0.1.0/tasks/backlog/todo-0001-component-variants-completion.md +130 -0
  72. boxframe-0.1.0/tests/__init__.py +0 -0
  73. boxframe-0.1.0/tests/api/__init__.py +0 -0
  74. boxframe-0.1.0/tests/api/test_layouts.py +748 -0
  75. boxframe-0.1.0/tests/api/test_projects.py +90 -0
  76. boxframe-0.1.0/tests/conftest.py +81 -0
  77. boxframe-0.1.0/tests/e2e/__init__.py +0 -0
  78. boxframe-0.1.0/tests/e2e/conftest.py +44 -0
  79. boxframe-0.1.0/tests/e2e/test_drag_drop.py +221 -0
  80. boxframe-0.1.0/tests/e2e/test_editor.py +543 -0
  81. boxframe-0.1.0/tests/e2e/test_projects.py +147 -0
  82. boxframe-0.1.0/tests/js/test_history.js +482 -0
  83. boxframe-0.1.0/tests/js/test_renderer.js +732 -0
  84. boxframe-0.1.0/tests/js/test_store.js +156 -0
  85. boxframe-0.1.0/tests/test_js_renderer_parity.py +170 -0
  86. boxframe-0.1.0/tests/test_renderer.py +817 -0
@@ -0,0 +1,49 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+ inputs:
8
+ repository:
9
+ description: "Target repository"
10
+ type: choice
11
+ default: testpypi
12
+ options:
13
+ - testpypi
14
+ - pypi
15
+
16
+ permissions:
17
+ id-token: write
18
+ contents: read
19
+
20
+ jobs:
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+ - uses: actions/setup-node@v4
29
+ with:
30
+ node-version: "24"
31
+ - run: pip install -e ".[dev]"
32
+ - run: pytest tests/ --ignore=tests/e2e
33
+ # unquoted glob: bash expands it; node < 21 does not glob on its own
34
+ - run: node --test tests/js/*.js
35
+
36
+ publish:
37
+ needs: test
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+ - run: pip install build
45
+ - run: python -m build
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ # tag push → pypi; manual run → input (default: testpypi)
49
+ repository: ${{ github.event.inputs.repository || 'pypi' }}
@@ -0,0 +1,43 @@
1
+ # Simple workflow for deploying static content to GitHub Pages
2
+ name: Deploy static content to Pages
3
+
4
+ on:
5
+ # Runs on pushes targeting the default branch
6
+ push:
7
+ branches: ["develop"]
8
+
9
+ # Allows you to run this workflow manually from the Actions tab
10
+ workflow_dispatch:
11
+
12
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13
+ permissions:
14
+ contents: read
15
+ pages: write
16
+ id-token: write
17
+
18
+ # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19
+ # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20
+ concurrency:
21
+ group: "pages"
22
+ cancel-in-progress: false
23
+
24
+ jobs:
25
+ # Single deploy job since we're just deploying
26
+ deploy:
27
+ environment:
28
+ name: github-pages
29
+ url: ${{ steps.deployment.outputs.page_url }}
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@v4
34
+ - name: Setup Pages
35
+ uses: actions/configure-pages@v5
36
+ - name: Upload artifact
37
+ uses: actions/upload-pages-artifact@v3
38
+ with:
39
+ # Upload entire repository
40
+ path: 'boxframe/static'
41
+ - name: Deploy to GitHub Pages
42
+ id: deployment
43
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ venv/
8
+ .env
9
+ *.db
10
+ *.db-journal
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ /log/
14
+ /tmp/
15
+ /.qwen/
16
+ /.idea/
@@ -0,0 +1,206 @@
1
+ # AGENTS.md — boxframe
2
+
3
+ ## О проекте
4
+
5
+ **boxframe** — редактор UI-макетов в псевдографике (ASCII/Unicode box-drawing). Макеты машиночитаемы, самодокументируемы и оптимизированы для потребления ИИ-агентами.
6
+
7
+ **Стек:** Python 3.11+, FastAPI, SQLAlchemy (aiosqlite), Jinja2, HTMX 2, Alpine.js 3.
8
+
9
+ ## Структура
10
+
11
+ ```
12
+ boxframe/
13
+ ├── boxframe/
14
+ │ ├── api/ # FastAPI роутеры
15
+ │ │ ├── projects.py # CRUD проектов
16
+ │ │ └── layouts.py # CRUD layout/blocks + render + export
17
+ │ ├── models/ # SQLAlchemy ORM
18
+ │ │ ├── project.py # Project
19
+ │ │ ├── layout.py # Layout (width×height)
20
+ │ │ └── block.py # Block (x,y,w,h, type, content, border)
21
+ │ ├── services/ # Бизнес-логика
22
+ │ │ ├── layout_service.py # CRUD + render + export
23
+ │ │ └── renderer.py # Псевдографика (Unicode box-drawing)
24
+ │ ├── static/ # Статика
25
+ │ │ ├── css/style.css # Dark theme
26
+ │ │ ├── css/editor.css # Стили редактора (общие для web и static)
27
+ │ │ ├── editor/index.html # Статический редактор (без бэкенда)
28
+ │ │ └── js/
29
+ │ │ ├── alpine.min.js # Alpine.js (вендор, без CDN)
30
+ │ │ ├── core/renderer.js# JS-порт рендерера (static-режим)
31
+ │ │ ├── core/store.js # FetchStore / LocalStorageStore / MemoryStore
32
+ │ │ ├── core/history.js # withHistory — undo/redo (snapshot-стеки)
33
+ │ │ └── editor.js # Alpine.js editor app (общий)
34
+ │ ├── templates/pages/ # Jinja2 шаблоны
35
+ │ │ ├── base.html
36
+ │ │ ├── projects.html
37
+ │ │ ├── project.html
38
+ │ │ └── editor.html
39
+ │ ├── config.py # Pydantic Settings
40
+ │ ├── database.py # Async engine + session
41
+ │ └── main.py # FastAPI app + page routes
42
+ ├── tests/
43
+ │ ├── api/ # API unit-тесты (httpx + TestClient)
44
+ │ │ ├── test_projects.py # CRUD проектов
45
+ │ │ └── test_layouts.py # CRUD layout/blocks + render + export
46
+ │ ├── e2e/ # End-to-end тесты (Playwright)
47
+ │ │ ├── conftest.py # fixture reset_page_state
48
+ │ │ ├── test_editor.py # Загрузка страниц editor/project/index + static
49
+ │ │ └── test_projects.py # New Project / New Layout кнопки + форма
50
+ │ ├── js/test_renderer.js # Тесты JS-рендерера (node:test)
51
+ │ ├── js/test_store.js # Тесты JS-хранилищ (node:test)
52
+ │ ├── js/test_history.js # Тесты undo/redo-истории (node:test)
53
+ │ ├── conftest.py # API-файстуры (async engine + TestClient)
54
+ │ ├── test_js_renderer_parity.py # Parity Python↔JS рендереры
55
+ │ └── test_renderer.py # Тесты рендерера (ASCII + HTML-оверлей)
56
+ ├── Dockerfile
57
+ ├── pyproject.toml
58
+ ├── requirements.txt
59
+ └── README.md
60
+ ```
61
+
62
+ ## Ключевые концепции
63
+
64
+ ### Блоки
65
+
66
+ Типы: `box`, `header`, `footer`, `sidebar`, `content`, `button`, `input`, `textarea`, `image`, `divider`, `hline`, `vline`, `text`, `grid`
67
+
68
+ Стили границ: `solid` (─│), `dashed` (┄┆), `dotted` (┈┊), `double` (═║), `none` — применяются и к рамкам блоков, и к линиям (`hline`/`vline` рисуются символом выбранного стиля)
69
+
70
+ `button` — особая форма в зависимости от высоты (спецификация в `tasks/0021-0030/0022-button.md`):
71
+ - `h=1`: `[label ]`
72
+ - `h=2`: `│label │` + `└──────┘` (без верхней рамки)
73
+ - `h>=3`: полная рамка, label по центру по вертикали (для чётной h — верхняя средняя строка)
74
+ - label — одна строка: обрезается по внутренней ширине, перенос слов не применяется; `border_style=none` — только label
75
+
76
+ Дерево: `Block.parent_id → Block.children`, хранится в SQLite.
77
+
78
+ ### Рендерер
79
+
80
+ `PseudoGraphicRenderer` — преобразует дерево блоков в ASCII-арт на сетке.
81
+ - Класс `RenderBlock` — плоское представление для рендера
82
+ - `render_simple()` — convenience-метод из raw dicts
83
+ - `render()` — рисует на 2D-сетке, обрабатывает вложенность
84
+
85
+ ### Статический режим
86
+
87
+ Редактор работает без бэкенда: `static/editor/index.html` открывается
88
+ напрямую в браузере (даже через `file://`), макет хранится в localStorage.
89
+
90
+ - `core/store.js` — единый интерфейс хранилища: `FetchStore` (API, web-режим),
91
+ `LocalStorageStore` (static-режим, persist в localStorage, key
92
+ `boxframe.static.layout`, `options.storage` — для тестов) и `MemoryStore`
93
+ (in-memory, fallback); `editor.js` не знает о fetch. `clear()` (очистка
94
+ всего макета) есть только у memory/local-сторов
95
+ - `core/renderer.js` — JS-порт `services/renderer.py`: ASCII + HTML-оверлей;
96
+ вывод должен совпадать с Python побайтово (parity-тесты)
97
+ - Режим определяется по `data-layout-id` в DOM: есть → `FetchStore`,
98
+ нет → `LocalStorageStore`
99
+ - Блоки в JS — плоский список с `parent_id` (x/y детей — относительные,
100
+ как в БД); константы `BLOCK_TYPES`/`BORDER_STYLES` дублируются в
101
+ `core/renderer.js` (держать в синхроне с `models/block.py`)
102
+
103
+ ### API
104
+
105
+ | Method | Path | Description |
106
+ |--------|------|-------------|
107
+ | GET | `/api/projects/` | Список проектов |
108
+ | POST | `/api/projects/` | Создать проект |
109
+ | GET | `/api/projects/{id}/info` | Метаданные + доступные типы |
110
+ | DELETE | `/api/projects/{id}` | Удалить проект |
111
+ | POST | `/api/projects/{id}/layouts` | Создать layout |
112
+ | GET | `/api/layouts/{id}` | Layout + блоки |
113
+ | POST | `/api/layouts/{id}/blocks` | Добавить блок |
114
+ | PUT | `/api/layouts/{id}/blocks/{bid}` | Обновить блок |
115
+ | DELETE | `/api/layouts/{id}/blocks/{bid}` | Удалить блок |
116
+ | DELETE | `/api/layouts/{id}/blocks` | Удалить все блоки (layout остаётся) |
117
+ | PUT | `/api/layouts/{id}/blocks` | Полная замена набора блоков (undo/redo; id из payload сохраняются) |
118
+ | GET | `/api/layouts/{id}/render` | ASCII + HTML preview |
119
+ | GET | `/api/layouts/{id}/export` | JSON / Markdown / ASCII |
120
+
121
+ ### Фронтенд
122
+
123
+ - **HTMX** — серверные рендеры страниц, без SPA
124
+ - **Alpine.js** — реактивность редактора (`x-data="editorApp()"`), подключается локально (`static/js/alpine.min.js`)
125
+ - **Редактор** — сайдбар (палитра + список элементов) + canvas-превью; общий для web- и static-режимов
126
+ - **Undo/Redo** — `core/history.js`: `withHistory(store)` оборачивает store,
127
+ хранит snapshot-стеки (без лимита) и восстанавливает состояние через
128
+ `replaceState` (local) / batch `PUT /blocks` (web). Снапшот берётся из
129
+ кэша состояния (seed от `load()`, синк от результатов мутаций) — без
130
+ доп. запросов. Coalescing: consecutive-обновления одного блока в окне
131
+ 500 мс = одна запись. Хоткеи Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y (не
132
+ перехватываются в текстовых полях)
133
+ - Экспорт: скачивание файла (JSON/MD/ASCII)
134
+
135
+ ## Соглашения
136
+
137
+ - **Python:** 100 символов строка, type hints, dataclasses для DTO
138
+ - **Именование:** snake_case для кода, kebab-case для URL
139
+ - **Тесты:** `tests/test_<module>.py`, pytest
140
+ - **БД:** асинхронная SQLAlchemy 2.0+, `async_session` через Depends
141
+ - **Безопасность:** CSRF не требуется (не авторизован), валидация через Pydantic
142
+
143
+ ## Типичные задачи
144
+
145
+ ### Добавить новый тип блока
146
+
147
+ 1. Добавить в `BLOCK_TYPES` в `models/block.py` **и** в `static/js/core/renderer.js`
148
+ 2. Добавить обработку в `renderer.py` и `core/renderer.js` (если нужна особая отрисовка)
149
+ 3. Добавить кнопку в палитру `editor.html`
150
+ 4. Добавить тесты в `tests/test_renderer.py` **и** `tests/js/test_renderer.js`
151
+
152
+ ### Изменить API
153
+
154
+ 1. Добавить роут в `api/layouts.py` или `api/projects.py`
155
+ 2. Обновить `LayoutService` при необходимости
156
+ 3. Добавить Pydantic-схему (create/update/out)
157
+ 4. Протестировать через `httpx` в pytest
158
+
159
+ ### Изменить рендерер
160
+
161
+ 1. Править `services/renderer.py` **и** `static/js/core/renderer.js`
162
+ (порт должен давать идентичный вывод)
163
+ 2. Убедиться, что `render_simple()` и `render()` согласованы
164
+ 3. Добавить/обновить тесты в `tests/test_renderer.py` **и** `tests/js/test_renderer.js`
165
+ 4. Прогнать parity-тесты: `pytest tests/test_js_renderer_parity.py`
166
+ 5. Проверить, что `LayoutService.render_layout()` использует новый рендер
167
+
168
+ ### Добавить страницу
169
+
170
+ 1. Создать шаблон в `templates/pages/`
171
+ 2. Добавить route в `main.py`
172
+ 3. Добавить ссылки в существующие шаблоны
173
+
174
+ ## Запуск
175
+
176
+ ```bash
177
+ pip install -r requirements.txt
178
+ uvicorn boxframe.main:app --reload
179
+ # http://localhost:8000
180
+ ```
181
+
182
+ Тесты:
183
+ ```bash
184
+ pytest tests/ -v # unit + API
185
+ node --test "tests/js/*.js" # JS-рендерер (нужен Node.js)
186
+ pytest tests/e2e/ -v # e2e: нужен запущенный сервер на :8000
187
+ ```
188
+
189
+ Внимание: e2e и API-тесты запускать **раздельными командами** (playwright
190
+ sync API конфликтует с pytest-asyncio в одном процессе).
191
+
192
+ Docker:
193
+ ```bash
194
+ docker build -t boxframe .
195
+ docker run -p 8000:8000 boxframe
196
+ ```
197
+
198
+ ## Чего НЕ делать
199
+
200
+ - Не менять формат псевдографики — это ядро продукта (AI-readable format)
201
+ - Не расхожить Python- и JS-рендереры — формат один; править оба + оба
202
+ набора тестов + parity
203
+ - Не добавлять авторизацию в MVP
204
+ - Не использовать сборщики (Webpack/Vite) — HTMX рендерит на сервере,
205
+ JS-файлы — обычные скрипты (должны работать из `file://`)
206
+ - Не менять SQLite на другую БЗ без веской причины (MVP-файловая БД — фича)
@@ -0,0 +1,12 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "boxframe.main:app", "--host", "0.0.0.0", "--port", "8000"]