diff --git a/.skaro/tasks/T-007-vynesti-menyu-razdelov-nastroek-v-obschi.md b/.skaro/tasks/T-007-vynesti-menyu-razdelov-nastroek-v-obschi.md new file mode 100644 index 0000000..de2168c --- /dev/null +++ b/.skaro/tasks/T-007-vynesti-menyu-razdelov-nastroek-v-obschi.md @@ -0,0 +1,30 @@ +--- +id: T-007 +title: Вынести меню разделов настроек в общий компонент +milestone: M01 +status: done +depends_on: [] +order: 1 +spec: "0002" +created: 2026-09-29 +branch: skaro/T-007-vynesti-menyu-razdelov-nastroe +--- + +## Цель + +Страница подключения и настройки портала должны пользоваться одним меню разделов, а не двумя копиями. Меню портала выносится в общий компонент без изменения вида. + +## Критерии приёмки + +- [x] Меню разделов (пункты с иконкой, подсказкой, разделителем, опасный пункт) — общий компонент в shared/, настройки портала используют его +- [x] Вид настроек портала (кадры PT4–PT6 из «Порталы Baseline») не изменился +- [x] Классы portal-settings-nav* в новой странице не дублируются +- [x] Тесты и tsc по затронутым файлам проходят + +## Заметки + +Сейчас меню собрано в features/support-portals/PortalSettings.tsx и sections.ts. Правило «один стандарт на элемент» из AGENTS.md. + +## Итог + +Меню настроек портала вынесено в общий SectionMenu и CSS в shared, сохранены пункты с иконкой, подсказкой, разделителем и опасным пунктом. На штатном Docker dev-контуре установлен демо-набор, портал проверен в Chromium на кадрах PT4–PT6; целевой тест, tsc и сборка прошли. diff --git a/apps/internal-ui/src/features/support-portals/PortalSettings.tsx b/apps/internal-ui/src/features/support-portals/PortalSettings.tsx index 6916f93..2870672 100644 --- a/apps/internal-ui/src/features/support-portals/PortalSettings.tsx +++ b/apps/internal-ui/src/features/support-portals/PortalSettings.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Icon } from "../../shared/icons"; +import { SectionMenu } from "../../shared/SectionMenu"; import { listPortalWidgets, type PortalAddressConfig, @@ -47,32 +47,19 @@ export function PortalSettings({ const current = PORTAL_SETTINGS_SECTIONS.find((item) => item.key === section) ?? PORTAL_SETTINGS_SECTIONS[0]; const domainLive = Boolean(portal.customDomain && portal.customDomainVerifiedAt); - const hints: Partial> = { - domain: domainLive ? { text: t("portals.working"), tone: "ok" } : undefined, - }; + const menuItems = PORTAL_SETTINGS_SECTIONS.map((item) => ({ + ...item, + hint: item.key === "domain" && domainLive ? { text: t("portals.working"), tone: "ok" as const } : undefined, + })); return (
- +
diff --git a/apps/internal-ui/src/features/support-portals/sections.ts b/apps/internal-ui/src/features/support-portals/sections.ts index 0187bfc..fe62053 100644 --- a/apps/internal-ui/src/features/support-portals/sections.ts +++ b/apps/internal-ui/src/features/support-portals/sections.ts @@ -1,8 +1,6 @@ -import type { Icon } from "../../shared/icons"; +import type { SectionMenuItem } from "../../shared/SectionMenu"; import { t } from "../../i18n"; -type IconName = Parameters[0]["name"]; - export type PortalSettingsSectionKey = | "basics" | "domain" @@ -10,14 +8,9 @@ export type PortalSettingsSectionKey = | "widget" | "danger"; -export type PortalSettingsSection = { - key: PortalSettingsSectionKey; - label: string; - icon: IconName; +export type PortalSettingsSection = SectionMenuItem & { heading: string; lead: string; - // Разделитель под пунктом — как в макете, перед сноской субменю. - divider?: boolean; }; // Субменю настроек портала (дизайн-базлайн v2, кадры PT4–PT6). Модалки нет: @@ -58,6 +51,7 @@ export const PORTAL_SETTINGS_SECTIONS: PortalSettingsSection[] = [ heading: t("portals.publishing_archive"), lead: t("portals.publishing_opens_material_visitors_archive"), divider: true, + danger: true, }, ]; diff --git a/apps/internal-ui/src/features/support-portals/styles-settings.css b/apps/internal-ui/src/features/support-portals/styles-settings.css index 1087a99..572d68c 100644 --- a/apps/internal-ui/src/features/support-portals/styles-settings.css +++ b/apps/internal-ui/src/features/support-portals/styles-settings.css @@ -8,74 +8,6 @@ min-height: 0; } -.portal-settings-nav { - display: flex; - width: 250px; - flex: none; - flex-direction: column; - gap: 1px; - padding: 14px 10px; - border-right: 1px solid var(--n-8); - background: var(--surface-card); -} - -.portal-settings-nav > span { - display: block; -} - -.portal-settings-nav-item { - display: flex; - width: 100%; - min-height: 34px; - align-items: center; - gap: 10px; - padding: 0 10px; - border: 0; - border-radius: 8px; - background: transparent; - color: var(--n-2); - font-family: inherit; - font-size: 13px; - font-weight: 500; - text-align: left; - cursor: pointer; -} - -.portal-settings-nav-item:hover { background: var(--n-9); } - -.portal-settings-nav-item.is-active { - background: var(--primary-bg); - color: var(--primary-text); - font-weight: 600; -} - -.portal-settings-nav-item > span { flex: 1; } - -.portal-settings-nav-item small { - color: var(--n-5); - font-size: 11px; - font-weight: 600; -} - -.portal-settings-nav-item small.is-ok { color: var(--success-text); } - -.portal-settings-nav-divider { - display: block; - height: 1px; - margin: 8px 6px; - background: var(--n-8); -} - -.portal-settings-nav-gap { flex: 1; } - -.portal-settings-nav p { - margin: 0; - padding: 0 10px; - color: var(--n-4); - font-size: 11.5px; - line-height: 1.5; -} - .portal-settings-content { min-width: 0; min-height: 0; diff --git a/apps/internal-ui/src/shared/SectionMenu.test.tsx b/apps/internal-ui/src/shared/SectionMenu.test.tsx new file mode 100644 index 0000000..3aa8e63 --- /dev/null +++ b/apps/internal-ui/src/shared/SectionMenu.test.tsx @@ -0,0 +1,32 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +import { t } from "../i18n"; +import { PORTAL_SETTINGS_SECTIONS } from "../features/support-portals/sections"; +import { SectionMenu } from "./SectionMenu"; + +describe("SectionMenu", () => { + it("renders the portal sections, hint, divider, and dangerous item", () => { + const items = PORTAL_SETTINGS_SECTIONS.map((item) => ({ + ...item, + hint: item.key === "domain" ? { text: t("portals.working"), tone: "ok" as const } : undefined, + })); + const markup = renderToStaticMarkup( + undefined} + />, + ); + + expect(markup).toContain('class="section-menu"'); + expect(markup.match(/class="section-menu-item/g)).toHaveLength(5); + expect(markup).toContain('class="section-menu-item is-active"'); + expect(markup).toContain('class="section-menu-item is-danger"'); + expect(markup).toContain('class="section-menu-divider"'); + expect(markup).toContain('class="is-ok">' + t("portals.working")); + expect(markup).toContain(t("portals.changes_reach_public_pages_as")); + expect(markup).toContain(" = { + key: Key; + label: string; + icon: Parameters[0]["name"]; + hint?: { text: string; tone?: "ok" | "muted" }; + divider?: boolean; + danger?: boolean; +}; + +export function SectionMenu({ items, activeKey, note, onSelect }: { + items: readonly SectionMenuItem[]; + activeKey: Key; + note?: string; + onSelect: (key: Key) => void; +}) { + return ( + + ); +} diff --git a/apps/internal-ui/src/shared/section-menu.css b/apps/internal-ui/src/shared/section-menu.css new file mode 100644 index 0000000..ba60677 --- /dev/null +++ b/apps/internal-ui/src/shared/section-menu.css @@ -0,0 +1,67 @@ +.section-menu { + display: flex; + width: 250px; + flex: none; + flex-direction: column; + gap: 1px; + padding: 14px 10px; + border-right: 1px solid var(--n-8); + background: var(--surface-card); +} + +.section-menu > span { + display: block; +} + +.section-menu-item { + display: flex; + width: 100%; + min-height: 34px; + align-items: center; + gap: 10px; + padding: 0 10px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--n-2); + font-family: inherit; + font-size: 13px; + font-weight: 500; + text-align: left; + cursor: pointer; +} + +.section-menu-item:hover { background: var(--n-9); } + +.section-menu-item.is-active { + background: var(--primary-bg); + color: var(--primary-text); + font-weight: 600; +} + +.section-menu-item > span { flex: 1; } + +.section-menu-item small { + color: var(--n-5); + font-size: 11px; + font-weight: 600; +} + +.section-menu-item small.is-ok { color: var(--success-text); } + +.section-menu-divider { + display: block; + height: 1px; + margin: 8px 6px; + background: var(--n-8); +} + +.section-menu-gap { flex: 1; } + +.section-menu p { + margin: 0; + padding: 0 10px; + color: var(--n-4); + font-size: 11.5px; + line-height: 1.5; +} diff --git a/apps/internal-ui/src/styles.css b/apps/internal-ui/src/styles.css index 14d75c2..4928407 100644 --- a/apps/internal-ui/src/styles.css +++ b/apps/internal-ui/src/styles.css @@ -12,6 +12,7 @@ @import "./features/conversations/contact-edit.css"; @import "./shared/table-controls.css"; @import "./shared/menu.css"; +@import "./shared/section-menu.css"; @import "./shared/content-library/styles.css"; @import "./shared/links.css"; @import "./features/employees/styles.css";