Merge branch 'bug/1-login-layout'
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# Баг 1. Окно входа растянуто на весь экран
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
|
||||||
|
После фазы 38 экран «Вход» прижат к верхнему левому углу: заголовок и подпись «Пароль альфы» слева сверху, поле пароля — тонкая полоса на всю ширину окна, кнопка «Продолжить» маленькая под левым краем. Большая часть экрана пустая.
|
||||||
|
|
||||||
|
Ожидали компактную форму по центру, как диалог создания школы, а не полноширинный `.screen`.
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
|
||||||
|
Фаза 38 повесила `class="screen session-gate"` и `field__input`, но стилей для `.session-gate` нет, а класс `.field__input` нигде не определён — живой контрол это `.input`. Экран тянется на 100% ширины (`#app`), нативное поле без паддинга заполняет окно, колонка стартует с верхнего края (у `.screen.menu` как раз есть `justify-content: center` и `max-width`).
|
||||||
|
|
||||||
|
## Путь
|
||||||
|
|
||||||
|
Стили `.session-gate`: карточка размера диалога по центру окна; поля перевести на `.input`. Не делать `<dialog>` — за формой ничего нет, это единственный экран, не модалка поверх меню.
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
- [x] Карточка входа по центру, ширина как у `.dialog` (~340–360px), не на всю `#app`
|
||||||
|
- [x] Поля пароля и имени — класс `input`
|
||||||
|
- [x] Тест на классы гейта и `input`
|
||||||
|
|
||||||
|
## Тест, без которого не закрыт
|
||||||
|
|
||||||
|
Клиентский Vitest: без куки монтируется `.session-gate` с панелью, поле пароля несёт `input`, не `field__input`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix src/HSchool.Client test -- src/ui/sessionGate.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Стоп
|
||||||
|
|
||||||
|
Протокол, куку, шаги пароль/имя и HTTP сессии не трогать. Локаль на гейте не подписывать — это соседнее.
|
||||||
+1
-2
@@ -9,5 +9,4 @@
|
|||||||
|
|
||||||
| Баг | Статус | Симптом |
|
| Баг | Статус | Симптом |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| [1. Окно входа](01-login-layout.md) | ✅ | Форма входа прижата к углу, поле пароля на всю ширину |
|
||||||
Пока пусто. Первую строку пишет `/bug-work`.
|
|
||||||
|
|||||||
@@ -63,6 +63,35 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Phase 38 dropped the gate in as a full-width .screen and never gave it a column. Without a cap
|
||||||
|
* the password field spans the window; without centering it clings to the top-left. Same tokens as
|
||||||
|
* .dialog — this is one short form, not a management screen.
|
||||||
|
*/
|
||||||
|
.screen.session-gate {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-gate__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: min(360px, 100%);
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-gate .form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-gate .button {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* No cap. A reading column has a sensible maximum width; a management screen does not — it is a
|
* No cap. A reading column has a sensible maximum width; a management screen does not — it is a
|
||||||
* tree, a table and a timetable grid, and every pixel taken away from them is a pixel of scrolling.
|
* tree, a table and a timetable grid, and every pixel taken away from them is a pixel of scrolling.
|
||||||
|
|||||||
@@ -37,4 +37,19 @@ describe('session gate', () => {
|
|||||||
|
|
||||||
pending.catch(() => undefined);
|
pending.catch(() => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('mounts a centred panel and the shared input class, not a raw full-width field', async () => {
|
||||||
|
vi.mocked(fetchSession).mockRejectedValue(new ApiError(401, 'unknown', 'Unauthorized'));
|
||||||
|
|
||||||
|
const pending = ensureSession();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const gate = document.querySelector('.session-gate');
|
||||||
|
const password = document.querySelector<HTMLInputElement>('.session-gate input[type="password"]');
|
||||||
|
expect(gate?.querySelector('.session-gate__panel')).not.toBeNull();
|
||||||
|
expect(password?.className).toBe('input');
|
||||||
|
expect(password?.className).not.toContain('field__input');
|
||||||
|
|
||||||
|
pending.catch(() => undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,16 +27,17 @@ function showSessionGate(): Promise<string> {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let password = '';
|
let password = '';
|
||||||
const root = el('section', { class: 'screen session-gate' });
|
const root = el('section', { class: 'screen session-gate' });
|
||||||
|
const panel = el('div', { class: 'session-gate__panel' });
|
||||||
const title = el('h1', { class: 'screen__title' });
|
const title = el('h1', { class: 'screen__title' });
|
||||||
const passwordLabel = el('label', { class: 'field' });
|
const passwordLabel = el('label', { class: 'field' });
|
||||||
const passwordInput = el('input', {
|
const passwordInput = el('input', {
|
||||||
class: 'field__input',
|
class: 'input',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
autocomplete: 'current-password',
|
autocomplete: 'current-password',
|
||||||
}) as HTMLInputElement;
|
}) as HTMLInputElement;
|
||||||
const nameLabel = el('label', { class: 'field' });
|
const nameLabel = el('label', { class: 'field' });
|
||||||
const nameInput = el('input', {
|
const nameInput = el('input', {
|
||||||
class: 'field__input',
|
class: 'input',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
autocomplete: 'username',
|
autocomplete: 'username',
|
||||||
maxlength: '40',
|
maxlength: '40',
|
||||||
@@ -51,7 +52,7 @@ function showSessionGate(): Promise<string> {
|
|||||||
|
|
||||||
const form = el(
|
const form = el(
|
||||||
'form',
|
'form',
|
||||||
{ class: 'session-gate__form' },
|
{ class: 'form' },
|
||||||
passwordLabel,
|
passwordLabel,
|
||||||
nameLabel,
|
nameLabel,
|
||||||
error,
|
error,
|
||||||
@@ -63,7 +64,8 @@ function showSessionGate(): Promise<string> {
|
|||||||
void submitStep();
|
void submitStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
root.append(title, form);
|
panel.append(title, form);
|
||||||
|
root.append(panel);
|
||||||
app.replaceChildren(root);
|
app.replaceChildren(root);
|
||||||
paint();
|
paint();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user