Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/mobile-production/fixes-20261016-member-spacing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!-- @format -->

# Отступ между навыками и кнопкой профиля

16.10.2026. Ветка `fix/prod-member-card-spacing-20261016` от production master
`6f12e33579f8015504a26b27f3dd371a6b8b3707`.

Причина: `margin-top:auto` у кнопки при автоматической высоте карточки давал 0px между навыками и действием.
Только в max-width:999px добавлены `margin-top:var(--ui-space-sm)` (8px) и `flex-shrink:0` для действия.
Шаблон, API, навыки/счётчик, ссылка и desktop стили сохранены.

## Проверки и review

- Checked by source: runtime diff — две строки SCSS внутри существующего мобильного media query. Review: actionable findings нет; исправление ограничено сообщённым дефектом.
- Checked in browser: 320/360/390/480/580/640/749/750/768/999px, по четыре карточки на ширине — 20 навыков (+19), четыре (+3), длинное имя/навык и пустые навыки. Было 0px, стало 8px. Кнопка высотой >=44px находится внутри карточки; две колонки и ссылки сохранены.
- Desktop 1000/1440/1920px: 3/3 точных RGB comparisons, 0 отличающихся пикселей. Исходный зазор 15px сохранён.
- Checked by tests: `npm run test:ci -- projects/social_platform/src/app/ui/pages/members/member-card/member-card.component.spec.ts projects/social_platform/src/app/ui/pages/members/members.component.spec.ts` — 2 файла, 8 тестов, exit 0. Новые unit tests для двух строк CSS не добавлялись; регрессия проверяется в браузере.
- Checked by build: `npm run build:prod` — exit 0; прежние CommonJS warnings. Baseline и исправленная сборка используют одинаковый tracked sprite с symbol nodes; CSS/JS bundles не менялись после сборки.
- Static: Stylelint изменённого SCSS и `git diff --check` — exit 0; Prettier проверен для полного staged index с LF, как на CI.

[До](./baseline-580.png) · [После](./verified-580.png) · [Метрики](./spacing-comparison.json) · [Desktop comparison](./desktop-comparison.json).

Воспроизведение на synthetic API: `QA_BUILD_ROOT` указывает на соответствующий production build,
`PLAYWRIGHT_MODULE` и `CHROME_PATH` — как в [общем harness](../README.md#воспроизведение-browser-gate).
Запуск: `node docs/mobile-production/member-card-spacing.cjs baseline`, затем с исправленной сборкой `verified`.

Not checked: физические устройства, Safari/WebKit и live API. Проверено в Chrome/Windows с synthetic API без live mutations.
Merge/deploy выполняет пользователь; миграции не требуются.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"screen": "members-spacing-1000",
"beforeSize": [1000, 844],
"afterSize": [1000, 844],
"exactChangedPixels": 0,
"maxChannelDelta": 0,
"passed": true
},
{
"screen": "members-spacing-1440",
"beforeSize": [1440, 844],
"afterSize": [1440, 844],
"exactChangedPixels": 0,
"maxChannelDelta": 0,
"passed": true
},
{
"screen": "members-spacing-1920",
"beforeSize": [1920, 844],
"afterSize": [1920, 844],
"exactChangedPixels": 0,
"maxChannelDelta": 0,
"passed": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
[
{
"width": 320,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 360,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 390,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 480,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 580,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 640,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 749,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 750,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 768,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 999,
"before": [0, 0, 0, 0],
"after": [8, 8, 8, 8],
"cards": 4,
"passed": true
},
{
"width": 1000,
"before": [15, 15, 15, 15],
"after": [15, 15, 15, 15],
"cards": 4,
"passed": true
},
{
"width": 1440,
"before": [15, 15, 15, 15],
"after": [15, 15, 15, 15],
"cards": 4,
"passed": true
},
{
"width": 1920,
"before": [15, 15, 15, 15],
"after": [15, 15, 15, 15],
"cards": 4,
"passed": true
}
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions docs/mobile-production/member-card-spacing.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/** @format */

const fs = require("node:fs");
const path = require("node:path");
const assert = require("node:assert/strict");
const fixtures = require("./mobile-fixtures.cjs");
const { launch, assertViewport } = require("./browser-harness.cjs");
const preview = require("./preview.cjs");
const [phase, widthsText = "320,360,390,480,580,640,749,750,768,999,1000,1440,1920", outputText] =
process.argv.slice(2);
const output = path.resolve(outputText || ".release-mobile/member-spacing-20261016/screenshots");
const baseUrl = "http://127.0.0.1:4486";
const original = fixtures.response;
const skill = { id: 1, name: "Apache Kafka", category: { id: 1, name: "Back-end" }, approves: [] };
const variants = [
{
firstName: "Сергей",
lastName: "Яксанов",
speciality: "Fullstack SWE",
count: 20,
name: "Apache Kafka",
},
{
firstName: "Михаил",
lastName: "Хромов",
speciality: "Django developer",
count: 4,
name: "Python",
},
{
firstName: "ОченьДлинноеИмяУчастника",
lastName: "ОченьДлиннаяФамилия",
speciality: "Проектирование распределённых систем и сервисов",
count: 1,
name: "ДлинныйНавыкБезПробеловДляПроверкиСжатияКарточки",
},
{ firstName: "Анна", lastName: "Иванова", speciality: "", count: 0 },
];
fixtures.response = (endpoint, method, query, state) => {
if (/\/auth\/public-users\/$/.test(endpoint))
return {
count: variants.length,
next: null,
previous: null,
results: variants.map((v, i) => ({
...fixtures.user,
id: i + 10,
firstName: v.firstName,
lastName: v.lastName,
avatar: "/assets/images/profile/main.svg",
speciality: v.speciality,
skills: Array.from({ length: v.count }, (_, index) => ({
...skill,
id: index + 1,
name: index ? "Навык " + index : v.name,
})),
})),
};
return original(endpoint, method, query, state);
};

(async () => {
fs.mkdirSync(path.join(output, phase), { recursive: true });
const rows = [];
for (const width of widthsText.split(",").map(Number)) {
const h = await launch(width, { baseUrl, hasTouch: width < 1000, isMobile: width < 750 });
await preview(h.context, baseUrl);
try {
const p = h.page;
await p.goto(baseUrl + "/office/members");
await p.locator("app-member-card").nth(3).waitFor({ state: "visible" });
await p.waitForLoadState("networkidle");
await p.evaluate(async () => {
for (const image of document.images) image.loading = "eager";
await document.fonts.ready;
await Promise.all([...document.fonts].map(font => font.load().catch(() => {})));
await Promise.all([...document.images].map(image => image.decode().catch(() => {})));
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
await p.addStyleTag({
content:
"*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}",
});
const cards = await p.locator(".member-card").evaluateAll(elements =>
elements.map(el => {
const box = node => {
const r = node.getBoundingClientRect();
return {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
right: r.right,
bottom: r.bottom,
};
};
const action = el.querySelector(".member-card__action"),
skills = el.querySelector(".member-card__skills-area");
return {
card: box(el),
action: box(action),
skills: box(skills),
gap: box(action).y - box(skills).bottom,
more: el.querySelector(".member-card__more")?.textContent.trim() || "",
url: el.getAttribute("href"),
};
}),
);
assert.equal(cards.length, 4);
assert.equal(cards[0].more, "+19");
assert.equal(cards[1].more, "+3");
if (phase !== "baseline" && width < 1000) {
await assertViewport(p);
assert.equal(cards[0].card.y, cards[1].card.y);
for (const [i, c] of cards.entries()) {
assert.ok(c.gap >= 7.99, `${width} card ${i}: gap ${c.gap}`);
assert.ok(c.action.height >= 44);
assert.ok(c.action.bottom <= c.card.bottom - 12);
assert.ok(c.action.x >= c.card.x && c.action.right <= c.card.right);
assert.equal(c.url, "/office/profile/" + (i + 10));
}
}
assert.deepEqual(h.errors, []);
await p.screenshot({
path: path.join(output, phase, `members-spacing-${width}.png`),
fullPage: true,
});
rows.push({ width, cards, passed: true });
console.log(`${phase}: ${width}, gaps ${cards.map(c => c.gap).join(", ")}`);
} finally {
await h.browser.close();
}
}
fs.writeFileSync(path.join(output, `${phase}.json`), JSON.stringify(rows, null, 2) + "\n");
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@
}

.member-card__action {
flex-shrink: 0;
align-self: center;
margin-top: var(--ui-space-sm);
}

.member-card__skill {
Expand Down