Шапка на телефоне: одна строка — вкладки, масштаб и шестерёнка с меню остального (#616)

На окне не шире 480 px шапка карточки и страницы бокового меню занимает одну
строку ≤ 56 px: вкладки пространств (без шестерёнок и «+», прокрутка вбок,
текущая вкладка докручивается в видимую область), масштаб, одна шестерёнка
«Действия и настройки» и у админа слот крестика #647. Заголовок скрыт.

Меню шестерёнки (src/header-menu.ts): редакторы, настройки и добавление
пространства, общие настройки, PDF, помощь, сводная панель и объёмный вид
(alpha) — в фиксированном порядке и с теми же условиями и действиями, что у
прежних кнопок. Пункт закрывает меню; Escape возвращает фокус на шестерёнку;
касание вне меню поглощается прозрачной подложкой. На > 480 px шапка прежняя,
в киоске меню нет.

- три кнопки шапки вынесены в renderHeaderActions — шапка карточки не растёт;
- сводная панель отдаёт свои пункты методом menuItems();
- i18n: title.header_menu (en/ru/de/fr);
- потолок initial View 291 000 → 292 600 (замер 291 635), база bundleBytes;
- смок smoke_mobile_view_header, юнит header-menu, 10 мутантов; смоки
  support_feedback, gear_tabs, modes, toolbar_stable_width переведены на
  меню на ≤ 480 px; подпись вкладки в span.tabtitle без pointer-events;
- USER-GUIDE.ru, UX-MODES, TOUCH-SUPPORT, STYLING-HOOKS, data-hp-contract,
  оба changelog.

Issue: #616
User-Visible: yes
This commit is contained in:
Claude
2026-09-25 14:22:36 +03:00
parent dc6018ed5a
commit 255330b9bc
97 changed files with 1640 additions and 742 deletions
+7 -1
View File
@@ -340,8 +340,14 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
* он передаёт начавшийся на кнопке жест существующему pinch-навигационному
* контуру. Факт оставляет 978 Б сверху и 1 022 Б до нижней границы; общий
* бюджет не меняется.
*
* 2026-09-25, #616: 291 000 → 292 600 (замер 291 635). Компактная шапка
* телефона — строка, меню шестерёнки и его стили — часть View домочадца на
* телефоне и обязана работать до загрузки ленивого редактора: у не-админа
* редактор не грузится вовсе. Факт оставляет 965 Б сверху и 1 035 Б до нижней
* границы; общий бюджет не меняется.
*/
export const INITIAL_VIEW_GZIP_CEILING = 291_000;
export const INITIAL_VIEW_GZIP_CEILING = 292_600;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
+1 -1
View File
@@ -4,5 +4,5 @@
"hostRefs": 4883,
"portPrivates": 94,
"harnessPrivates": 101,
"bundleBytes": 2521909
"bundleBytes": 2528415
}
+101
View File
@@ -7384,6 +7384,107 @@ const MUTANT_DEFINITIONS = [
replace: " .editor-close-slot .closex ha-icon { --mdc-icon-size: 13px; }\n @media (max-width: 1100px) { .editor-close-slot .closex { display: none; } }",
}],
},
{
id: 'phone-header-shows-title',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC1: on a phone the card title alone takes a row; the one-row header must hide it',
patches: [{
file: 'src/styles/chrome.styles.ts',
find: ' .head > .title, .head > .modes, .head > .spacer, .head > .header-action,',
replace: ' .head > .modes, .head > .spacer, .head > .header-action,',
}],
},
{
id: 'tab-editing-without-write-access',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC1: a household member has no write access — per-tab gears must not render for them at any width',
patches: [{
file: 'src/houseplan-card.ts',
find: ' <span class="tabtitle">${s.title}</span>${this._norm && this._canEdit\n',
replace: ' <span class="tabtitle">${s.title}</span>${this._norm\n',
}],
},
{
id: 'phone-header-wraps',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC2: the phone header is one row of at most 56 px; wrapping brings back the three-row chrome',
patches: [{
file: 'src/styles/chrome.styles.ts',
find: ' .hdr > .head { flex-wrap: nowrap; padding: 5px 8px; gap: 6px; }',
replace: ' .hdr > .head { flex-wrap: wrap; padding: 5px 8px; gap: 6px; }',
}],
},
{
id: 'header-menu-drops-pdf',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/header-menu.test.mjs',
because: '#616 AC3: every header action the phone row drops must be an item of the gear menu',
patches: [{
file: 'src/header-menu.ts',
find: " { id: 'pdf', icon: 'mdi:printer-outline', label: t('title.export_pdf'), run: actions.pdf },\n",
replace: '',
}],
},
{
id: 'header-menu-item-keeps-menu-open',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC3: an item does what its button did and closes the menu; a menu left open covers the plan',
patches: [{
file: 'src/header-menu.ts',
find: ' const run = (item: HeaderMenuItem) => { this.close(false); item.run(); };',
replace: ' const run = (item: HeaderMenuItem) => { item.run(); };',
}],
},
{
id: 'header-menu-outside-tap-reaches-plan',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC4: the tap that dismisses the menu must not toggle a light under it',
patches: [{
file: 'src/styles/chrome.styles.ts',
find: ' position: fixed;\n inset: 0;\n z-index: 1;\n background: transparent;',
replace: ' position: fixed;\n inset: auto;\n z-index: 1;\n background: transparent;',
}],
},
{
id: 'header-menu-escape-ignored',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC4: Escape closes the menu and returns focus to the gear',
patches: [{
file: 'src/header-menu.ts',
find: " if (event.key !== 'Escape' || !this.open) return;",
replace: " if (event.key !== 'Esc' || !this.open) return;",
}],
},
{
id: 'active-tab-not-revealed',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC5: with many spaces the active tab must be scrolled into view on load and on switch',
patches: [{
file: 'src/header-menu.ts',
find: ' if (!nav || !tab || nav.clientWidth === 0) return;',
replace: ' if (!nav || !tab || nav.clientWidth >= 0) return;',
}],
},
{
id: 'header-menu-item-below-44',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC6: menu items are touch targets of at least 44 px',
patches: [{
file: 'src/styles/chrome.styles.ts',
find: ' min-height: 44px;\n padding: 0 var(--sp-4);',
replace: ' min-height: 30px;\n padding: 0 var(--sp-4);',
}],
},
{
id: 'phone-header-at-tablet-width',
guard: 'node demo/smoke_mobile_view_header.mjs',
because: '#616 AC7: above 480 px the ordinary header stays exactly as it was',
patches: [{
file: 'src/styles/chrome.styles.ts',
find: ' @media (max-width: 480px) {\n .hdr > .head',
replace: ' @media (max-width: 768px) {\n .hdr > .head',
}],
},
{
id: 'tab-reorder-not-persisted',
guard: 'node demo/smoke_space_tab_reorder.mjs',
+9 -2
View File
@@ -29,10 +29,17 @@
export const SMOKE_LINKS = [
{
symbols: ['chromeStyles', 'dialogsStyles'],
smokes: ['smoke_toolbar_stable_width.mjs', 'smoke_editor_tabs.mjs'],
smokes: ['smoke_toolbar_stable_width.mjs', 'smoke_editor_tabs.mjs', 'smoke_mobile_view_header.mjs'],
because: '#647: the header X slot (.editor-close-slot) and the removed .head .count rule are '
+ 'CSS in these sheets; the smokes measure the rendered header width, tab offsets, the X '
+ 'hit target and its visibility at 1400/1000/768/390 px — no TS symbol names that layout',
+ 'hit target and its visibility at 1400/1000/768/390 px — no TS symbol names that layout; '
+ '#616: the phone one-row header rules live in chromeStyles too',
},
{
symbols: ['HeaderMenu', 'headerMenuItems', 'renderHeaderActions'],
smokes: ['smoke_mobile_view_header.mjs', 'smoke_support_feedback.mjs', 'smoke_gear_tabs.mjs'],
because: '#616: the one-row phone header (<= 480 px) and its gear menu are a media query plus '
+ 'these symbols; the smokes tap the real gear and items at 320/390 px and prove >480 px is unchanged',
},
{
symbols: ['confirmIcon', 'HpConfirmRequest'],