e2e спринтов и бэклога, тестовый хаб с досками, документация (#108)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
611ed830c4
commit
fe1d5926a5
@ -101,6 +101,11 @@
|
|||||||
страницы модуля — `/app/<модуль>/…` (`/app/jira/links`, `/app/jira/issues[/{key}]`, `/app/jira/feed`, `/app/wiki`); прежние `/app/board`, `/app/jira/board`,
|
страницы модуля — `/app/<модуль>/…` (`/app/jira/links`, `/app/jira/issues[/{key}]`, `/app/jira/feed`, `/app/wiki`); прежние `/app/board`, `/app/jira/board`,
|
||||||
`/app/issues`, `/app/issues/{key}`, `/app/feed` перенаправляются с сохранением `search` и `replace`. Пункты меню могут зависеть от проекта (`NavItem.when`, `visibleNav` в `shell/nav.tsx`): пока проект не определён, условный пункт скрыт; «не найдено» перечисляет только безусловные.
|
`/app/issues`, `/app/issues/{key}`, `/app/feed` перенаправляются с сохранением `search` и `replace`. Пункты меню могут зависеть от проекта (`NavItem.when`, `visibleNav` в `shell/nav.tsx`): пока проект не определён, условный пункт скрыт; «не найдено» перечисляет только безусловные.
|
||||||
|
|
||||||
|
Экраны доски модуля jira — `web/src/modules/jira/agile/`: `model` (даты Jira, «осталось N дн.», склонение, состояние данных
|
||||||
|
`boardState`), `columns` (раскладка задач по колонкам доски, «Без колонки»), `AgileGate` (тип доски проекта, загрузка, ошибка, три
|
||||||
|
состояния данных), `BoardColumns` и страницы `BacklogPage` (`/jira/backlog`), `SprintPage` (`/jira/sprint`), `KanbanPage`
|
||||||
|
(`/jira/kanban`). Данные — один запрос `GET /agile?project=<метка>` (`modules/jira/agile.py`); страницы только смотрят.
|
||||||
|
|
||||||
Модуль `wiki` (`web/src/modules/wiki/index.tsx`): страница «Вики» (`/app/wiki`): дерево (`Tree`) с поиском (`Search`), страница
|
Модуль `wiki` (`web/src/modules/wiki/index.tsx`): страница «Вики» (`/app/wiki`): дерево (`Tree`) с поиском (`Search`), страница
|
||||||
(`WikiPage`, `PageView` — `react-markdown` + `remark-gfm`, без `rehype-raw`; ссылки `page:` ведут на страницу вики, `attachment:`
|
(`WikiPage`, `PageView` — `react-markdown` + `remark-gfm`, без `rehype-raw`; ссылки `page:` ведут на страницу вики, `attachment:`
|
||||||
скачивают файл через `download.ts`, `urlTransform.ts` пропускает только эти схемы и http(s)), форма корня без привязки (`RootForm`),
|
скачивают файл через `download.ts`, `urlTransform.ts` пропускает только эти схемы и http(s)), форма корня без привязки (`RootForm`),
|
||||||
|
|||||||
@ -26,7 +26,7 @@ SDLC-хаб с модулями jira и wiki. Мост Jira для АРМ КА:
|
|||||||
- `uv sync` — установка окружения.
|
- `uv sync` — установка окружения.
|
||||||
- `uv run sdlc-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов.
|
- `uv run sdlc-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов.
|
||||||
- `uv run sdlc-hub serve [--host] [--port]` — локальный запуск хаба.
|
- `uv run sdlc-hub serve [--host] [--port]` — локальный запуск хаба.
|
||||||
- Веб-приложение — `http://127.0.0.1:8790/app/` («Обзор» — `/app/?project=<метка>`, «Связи» (доска связей) — `/app/jira/links?project=<метка>`, задачи — `/app/jira/issues?project=<метка>`, лента изменений — `/app/jira/feed?project=<метка>`, вики — `/app/wiki?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на «Обзор», прежние `/app/board`, `/app/jira/board`, `/app/issues`, `/app/feed` — на страницы модуля Jira; пункты меню могут зависеть от проекта (`NavItem.when`).
|
- Веб-приложение — `http://127.0.0.1:8790/app/` («Обзор» — `/app/?project=<метка>`, «Связи» (доска связей) — `/app/jira/links?project=<метка>`, задачи — `/app/jira/issues?project=<метка>`, «Бэклог» — `/app/jira/backlog?project=<метка>`, «Спринт» — `/app/jira/sprint?project=<метка>`, «Kanban» — `/app/jira/kanban?project=<метка>`, лента изменений — `/app/jira/feed?project=<метка>`, вики — `/app/wiki?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на «Обзор», прежние `/app/board`, `/app/jira/board`, `/app/issues`, `/app/feed` — на страницы модуля Jira; пункты меню могут зависеть от проекта (`NavItem.when`).
|
||||||
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
|
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
|
||||||
- `uv run sdlc-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run sdlc-hub mcp <модуль> [--project <метка>]` — MCP модуля по stdio (модуль обязателен: `jira` — сервер `sdlc-jira`, `wiki` — сервер `sdlc-wiki`; с привязкой сессии к проекту); `uv run sdlc-hub tokens` — показать токены (клиентский и по агенту на модуль).
|
- `uv run sdlc-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run sdlc-hub mcp <модуль> [--project <метка>]` — MCP модуля по stdio (модуль обязателен: `jira` — сервер `sdlc-jira`, `wiki` — сервер `sdlc-wiki`; с привязкой сессии к проекту); `uv run sdlc-hub tokens` — показать токены (клиентский и по агенту на модуль).
|
||||||
- `uv run sdlc-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`).
|
- `uv run sdlc-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`).
|
||||||
|
|||||||
14
README.md
14
README.md
@ -262,6 +262,15 @@ make web
|
|||||||
переводят её в новую схему, а хаб на прежнем коде с такой базой не работает. Старый фронт с новым API совместим, сборка может подождать. Откат на копию v6 из `backups/` —
|
переводят её в новую схему, а хаб на прежнем коде с такой базой не работает. Старый фронт с новым API совместим, сборка может подождать. Откат на копию v6 из `backups/` —
|
||||||
вместе с возвратом `release/jira/*` в `release/` (прежняя раскладка), иначе старый код не найдёт публикацию агента.
|
вместе с возвратом `release/jira/*` в `release/` (прежняя раскладка), иначе старый код не найдёт публикацию агента.
|
||||||
|
|
||||||
|
Экраны доски (только просмотр: ни перетаскивания, ни кнопок спринтов, ни смены статуса или ранга): «Бэклог»
|
||||||
|
`/app/jira/backlog?project=<метка>` (активные и будущие спринты, бэклог, «Прошлые спринты (N)»), «Спринт»
|
||||||
|
`/app/jira/sprint?project=<метка>[&sprint=<id>]` (колонки доски активного спринта; устаревший `sprint` — первый активный),
|
||||||
|
«Kanban» `/app/jira/kanban?project=<метка>` (колонки доски, «Бэклог (N)» при включённом бэклоге Jira, выполненное за 14 дней).
|
||||||
|
Пункты меню зависят от типа доски проекта: у scrum-проекта «Бэклог» и «Спринт», у kanban-проекта «Kanban»; пока тип не известен,
|
||||||
|
пунктов нет. Адрес страницы чужого типа показывает «У проекта нет scrum-доски» / «У проекта нет kanban-доски» со ссылкой
|
||||||
|
«Связи». Три состояния данных: «Нужен агент v15 и обновление из Jira» (доска ещё не прочитана), «Не удалось прочитать настройки
|
||||||
|
доски из Jira» (колонок нет), сами данные. Карточка задачи открывается окном поверх экрана.
|
||||||
|
|
||||||
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
|
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
|
||||||
(метка, название, необязательно номер доски Jira): после создания приложение переходит в новый проект и показывает ход
|
(метка, название, необязательно номер доски Jira): после создания приложение переходит в новый проект и показывает ход
|
||||||
пакета обновления. Текущий проект — параметр `project` в адресе; без него приложение подставляет последний выбранный
|
пакета обновления. Текущий проект — параметр `project` в адресе; без него приложение подставляет последний выбранный
|
||||||
@ -349,6 +358,11 @@ make web
|
|||||||
|
|
||||||
## Эксплуатация
|
## Эксплуатация
|
||||||
|
|
||||||
|
- Выкатка спринтов и бэклога (порядок обязателен): `git pull` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub`
|
||||||
|
(владелец; миграция схемы v9) → `make web` → `/mcp` reconnect → `agent publish` агента v15 (`uv run sdlc-hub agent publish
|
||||||
|
../jira-agent/src/agent.py`) → проверка полей Jira банка (ранг, оценка, спринты) → `field_ids.story_points` в настройках,
|
||||||
|
если агент поле оценки не нашёл. `min_agent_version` поднимает владелец после выкатки v15 и после того, как `agent_status`
|
||||||
|
покажет новую версию.
|
||||||
- Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-<batch>.sqlite`
|
- Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-<batch>.sqlite`
|
||||||
и хранит последние `backup_keep` копий (по умолчанию 30).
|
и хранит последние `backup_keep` копий (по умолчанию 30).
|
||||||
- Файлы вложений: `files/<sha256>`, одна копия на содержимое, в резервные копии базы не входят. Раз в сутки (первый
|
- Файлы вложений: `files/<sha256>`, одна копия на содержимое, в резервные копии базы не входят. Раз в сутки (первый
|
||||||
|
|||||||
81
web/e2e/agile.spec.ts
Normal file
81
web/e2e/agile.spec.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { login } from "./hub.ts";
|
||||||
|
|
||||||
|
// экраны доски: «Бэклог», «Спринт», «Kanban» (spec web-app «Спринты и бэклог»); данные — сид тестового хаба (R-27)
|
||||||
|
const side = (page: import("@playwright/test").Page) => page.getByRole("complementary", { name: "Панель приложения" });
|
||||||
|
|
||||||
|
test("«Бэклог» AWPCA: секции, «Прошлые спринты (1)», щелчок открывает карточку", async ({ page }) => {
|
||||||
|
await login(page, "/app/jira/backlog?project=AWPCA");
|
||||||
|
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 3" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("heading", { name: "Бэклог", exact: true, level: 2 })).toBeVisible();
|
||||||
|
const past = page.getByText("Прошлые спринты (1)");
|
||||||
|
await expect(past).toBeVisible();
|
||||||
|
await past.click();
|
||||||
|
await page.getByText("AWPCA_Спринт 1").first().click();
|
||||||
|
const row = page.locator(".agile-row").filter({ hasText: "AURORA-9001" }).last();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click();
|
||||||
|
await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/);
|
||||||
|
await expect(page.getByRole("dialog")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page).toHaveURL(/\/app\/jira\/backlog/);
|
||||||
|
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("«Спринт» AWPCA: колонки доски, «осталось 7 дн.», карточка окном", async ({ page }) => {
|
||||||
|
await login(page, "/app/jira/sprint?project=AWPCA");
|
||||||
|
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible();
|
||||||
|
await expect(page.getByText("осталось 7 дн.")).toBeVisible();
|
||||||
|
for (const name of ["К выполнению", "В работе", "Готово"]) {
|
||||||
|
await expect(page.locator(".agile-col__name", { hasText: name })).toBeVisible();
|
||||||
|
}
|
||||||
|
await page.locator(".agile-card").filter({ hasText: "AURORA-9001" }).click();
|
||||||
|
await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/);
|
||||||
|
await expect(page.getByRole("dialog")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page).toHaveURL(/\/app\/jira\/sprint/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("«Kanban»: «Бэклог (1)», колонки «В работе» и «Готово», подпись «за 14 дней»", async ({ page }) => {
|
||||||
|
await login(page, "/app/jira/kanban?project=KANBAN_E2E");
|
||||||
|
await expect(page.getByText("Бэклог (1)")).toBeVisible();
|
||||||
|
await expect(page.locator(".agile-col__name", { hasText: "В работе" })).toBeVisible();
|
||||||
|
await expect(page.locator(".agile-col__name", { hasText: "Готово" })).toBeVisible();
|
||||||
|
await expect(page.locator(".agile-col__name", { hasText: "Бэклог" })).toHaveCount(0);
|
||||||
|
await expect(page.getByText("за 14 дней")).toBeVisible();
|
||||||
|
await expect(page.locator(".agile-card").filter({ hasText: "AURORA-9102" })).toBeVisible();
|
||||||
|
await expect(page.locator(".agile-card").filter({ hasText: "AURORA-9103" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("без scrum-доски: /app/jira/backlog?project=KANBAN_E2E — «У проекта нет scrum-доски», ссылка «Связи»", async ({ page }) => {
|
||||||
|
await login(page, "/app/jira/backlog?project=KANBAN_E2E");
|
||||||
|
await expect(page.getByText("У проекта нет scrum-доски")).toBeVisible();
|
||||||
|
await page.getByRole("link", { name: "Связи", exact: true }).last().click();
|
||||||
|
await expect(page).toHaveURL(/\/app\/jira\/links\?project=KANBAN_E2E/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("меню по проекту: у AWPCA «Бэклог» и «Спринт», у KANBAN_E2E — «Kanban»", async ({ page }) => {
|
||||||
|
await login(page, "/app/jira/links?project=AWPCA");
|
||||||
|
await expect(side(page).getByRole("link", { name: "Бэклог" })).toBeVisible();
|
||||||
|
await expect(side(page).getByRole("link", { name: "Спринт" })).toBeVisible();
|
||||||
|
await expect(side(page).getByRole("link", { name: "Kanban" })).toHaveCount(0);
|
||||||
|
await page.goto("/app/jira/links?project=KANBAN_E2E");
|
||||||
|
await expect(side(page).getByRole("link", { name: "Kanban" })).toBeVisible();
|
||||||
|
await expect(side(page).getByRole("link", { name: "Бэклог" })).toHaveCount(0);
|
||||||
|
await expect(side(page).getByRole("link", { name: "Спринт" })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("900 px: колонки «Спринта» прокручиваются внутри доски, страница без горизонтальной прокрутки", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 900, height: 900 });
|
||||||
|
await login(page, "/app/jira/sprint?project=AWPCA");
|
||||||
|
await expect(page.locator(".agile-col").first()).toBeVisible();
|
||||||
|
const m = await page.evaluate(() => {
|
||||||
|
const d = document.documentElement, c = document.querySelector<HTMLElement>(".agile-columns")!;
|
||||||
|
return { doc: [d.scrollWidth, d.clientWidth], cols: [c.scrollWidth, c.clientWidth],
|
||||||
|
gutter: c.getBoundingClientRect().left - document.querySelector(".agile")!.getBoundingClientRect().left };
|
||||||
|
});
|
||||||
|
expect(m.gutter, "колонки не получают второй отступ внутри страницы (T10.1)").toBe(0);
|
||||||
|
expect(m.doc[0]).toBeLessThanOrEqual(m.doc[1]);
|
||||||
|
expect(m.cols[0]).toBeGreaterThan(m.cols[1]);
|
||||||
|
});
|
||||||
@ -3,10 +3,11 @@
|
|||||||
Дополнения только для тестового хаба (описание, комментарии, команды) — из seed-extras.json рядом со скриптом."""
|
Дополнения только для тестового хаба (описание, комментарии, команды) — из seed-extras.json рядом со скриптом."""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import date, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sdlc_hub.core import db, paths, settings
|
from sdlc_hub.core import db, paths, settings
|
||||||
from sdlc_hub.modules.jira import mirror
|
from sdlc_hub.modules.jira import mirror, projects as jira_projects
|
||||||
from sdlc_hub.modules.wiki import paths as wiki_paths, search as wiki_search, sync as wiki_sync
|
from sdlc_hub.modules.wiki import paths as wiki_paths, search as wiki_search, sync as wiki_sync
|
||||||
|
|
||||||
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||||
@ -25,7 +26,35 @@ def own_labels(i):
|
|||||||
issues = [{k: v for k, v in i.items() if k not in ("work_kind", "neighbor")} | {"id": i["key"]}
|
issues = [{k: v for k, v in i.items() if k not in ("work_kind", "neighbor")} | {"id": i["key"]}
|
||||||
| {"labels": own_labels(i)} | extra_issues.get(i["key"], {}) for i in snap["issues"]]
|
| {"labels": own_labels(i)} | extra_issues.get(i["key"], {}) for i in snap["issues"]]
|
||||||
links = [{"source_key": l["source"], "target_key": l["target"], "link_type": l["link_type"]} for l in snap["links"]]
|
links = [{"source_key": l["source"], "target_key": l["target"], "link_type": l["link_type"]} for l in snap["links"]]
|
||||||
mirror.apply_state(conn, issues, links, neighbor_keys=[i["key"] for i in snap["issues"] if i["neighbor"]])
|
# доски для экранов «Бэклог», «Спринт», «Kanban» (R-27): AWPCA — scrum 4216, KANBAN_E2E — kanban 5120
|
||||||
|
SPRINTS = {"AWPCA_Спринт 1": 101, "AWPCA_Спринт 2": 102, "AWPCA_Спринт 3": 103}
|
||||||
|
for n, i in enumerate(issues, 1):
|
||||||
|
i["rank"] = f"0|i{n:04d}:"
|
||||||
|
sid = SPRINTS.get(i.get("sprint"))
|
||||||
|
i["sprint_ids"] = [] if sid is None else [101, 102] if i["key"] == "AURORA-9001" else [sid]
|
||||||
|
if n % 2:
|
||||||
|
i["story_points"] = float(n % 5 + 1)
|
||||||
|
today = date.today()
|
||||||
|
iso = lambda d, hhmm: f"{d.isoformat()}T{hhmm}:00.000+03:00"
|
||||||
|
now_iso = iso(today, "12:00")
|
||||||
|
issues += [{"key": k, "id": k, "summary": f"Задача канбана {k[-4:]}", "issue_type": "Задача", "status": st, "priority": "Средний",
|
||||||
|
"labels": ["KANBAN_E2E"], "rank": f"0|k{n:04d}:", "sprint_ids": [], **({"jira_updated": now_iso} if st == "Готово" else {})}
|
||||||
|
for n, (k, st) in enumerate([("AURORA-9101", "Backlog"), ("AURORA-9102", "В работе"), ("AURORA-9103", "Готово")], 1)]
|
||||||
|
jira_projects.create(conn, "KANBAN_E2E", "Канбан e2e", 5120)
|
||||||
|
boards = [
|
||||||
|
{"id": 4216, "type": "scrum", "kanban_backlog": None,
|
||||||
|
"columns": [{"name": "К выполнению", "statuses": ["Backlog", "К выполнению", "Ожидание", "Ожидание разработки"]},
|
||||||
|
{"name": "В работе", "statuses": ["В работе"]}, {"name": "Готово", "statuses": ["Готово"]}],
|
||||||
|
"sprints": [{"id": 101, "name": "AWPCA_Спринт 1", "state": "closed", "start_date": iso(today - timedelta(days=21), "09:00"),
|
||||||
|
"end_date": iso(today - timedelta(days=8), "09:00"), "complete_date": iso(today - timedelta(days=8), "18:00"), "goal": None},
|
||||||
|
{"id": 102, "name": "AWPCA_Спринт 2", "state": "active", "start_date": iso(today - timedelta(days=7), "09:00"),
|
||||||
|
"end_date": iso(today + timedelta(days=7), "12:00"), "complete_date": None, "goal": "Тестовая цель спринта"},
|
||||||
|
{"id": 103, "name": "AWPCA_Спринт 3", "state": "future", "start_date": None, "end_date": None, "complete_date": None, "goal": None}]},
|
||||||
|
{"id": 5120, "type": "kanban", "kanban_backlog": True,
|
||||||
|
"columns": [{"name": "Бэклог", "statuses": ["Backlog"]}, {"name": "В работе", "statuses": ["В работе"]},
|
||||||
|
{"name": "Готово", "statuses": ["Готово"]}]},
|
||||||
|
]
|
||||||
|
mirror.apply_state(conn, issues, links, neighbor_keys=[i["key"] for i in snap["issues"] if i["neighbor"]], boards=boards)
|
||||||
for i in snap["issues"]:
|
for i in snap["issues"]:
|
||||||
note = {k: i[k] for k in ("work_kind",) if i.get(k)}
|
note = {k: i[k] for k in ("work_kind",) if i.get(k)}
|
||||||
if note:
|
if note:
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
.agile-row__assignee{flex-basis:100%}
|
.agile-row__assignee{flex-basis:100%}
|
||||||
}
|
}
|
||||||
/* «Спринт»: страница во всю ширину, колонки прокручиваются внутри контейнера */
|
/* «Спринт»: страница во всю ширину, колонки прокручиваются внутри контейнера */
|
||||||
.agile--wide{padding-inline:var(--space-6);min-width:0}
|
.agile--wide{min-width:0}
|
||||||
.agile-columns{display:flex;align-items:flex-start;gap:var(--space-3);overflow-x:auto;padding-bottom:var(--space-2);min-width:0}
|
.agile-columns{display:flex;align-items:flex-start;gap:var(--space-3);overflow-x:auto;padding-bottom:var(--space-2);min-width:0}
|
||||||
.agile-col{flex:0 0 280px;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);
|
.agile-col{flex:0 0 280px;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);
|
||||||
background:var(--color-bg-subtle);border:1px solid var(--color-border-default);border-radius:var(--radius-xl);padding:var(--space-3)}
|
background:var(--color-bg-subtle);border:1px solid var(--color-border-default);border-radius:var(--radius-xl);padding:var(--space-3)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user