diff --git a/apps/web/src/components/page-header.tsx b/apps/web/src/components/page-header.tsx
new file mode 100644
index 0000000..ce11dc8
--- /dev/null
+++ b/apps/web/src/components/page-header.tsx
@@ -0,0 +1,45 @@
+import type { ReactNode } from 'react'
+import { cn } from '@cfdm/ui/lib/utils'
+
+interface PageHeaderProps {
+ title: string
+ description?: ReactNode
+ actions?: ReactNode
+ className?: string
+}
+
+/**
+ * Page-level section header (h1) — shared by dashboard, lists, settings.
+ * Distinct from FrameTitle (text-sm inside Frame panels).
+ */
+export function PageHeader({
+ title,
+ description,
+ actions,
+ className,
+}: PageHeaderProps) {
+ return (
+
+
+
+ {title}
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {actions ? (
+
+ {actions}
+
+ ) : null}
+
+ )
+}
diff --git a/apps/web/src/components/reui-kit/ops-dashboard.tsx b/apps/web/src/components/reui-kit/ops-dashboard.tsx
index 6a86c10..c53fcc1 100644
--- a/apps/web/src/components/reui-kit/ops-dashboard.tsx
+++ b/apps/web/src/components/reui-kit/ops-dashboard.tsx
@@ -7,6 +7,7 @@ import {
FrameTitle,
} from '@/components/reui/frame'
import { Skeleton } from '@cfdm/ui/components/skeleton'
+import { PageHeader } from '@/components/page-header'
import { KpiStatGrid, type KpiStatCard } from './kpi-stat-grid'
export type OpsKpiCard = KpiStatCard
@@ -54,14 +55,7 @@ export function OpsDashboard({
return (
-
-
-
{title}
-
- {description}
-
-
-
+
diff --git a/apps/web/src/components/reui-kit/settings-shell.tsx b/apps/web/src/components/reui-kit/settings-shell.tsx
index 2be7145..519280a 100644
--- a/apps/web/src/components/reui-kit/settings-shell.tsx
+++ b/apps/web/src/components/reui-kit/settings-shell.tsx
@@ -5,6 +5,7 @@ import { SettingsIcon } from 'lucide-react'
import { useIsMobile } from '@cfdm/ui/hooks/use-mobile'
import { cn } from '@cfdm/ui/lib/utils'
import { PageShell } from '@/components/page-shell'
+import { PageHeader } from '@/components/page-header'
import { Separator } from '@cfdm/ui/components/separator'
export interface SettingsTabConfig {
@@ -40,15 +41,10 @@ export function SettingsShell({
return (
-
-
-
{title}
-
- {description}
-
-
+
void
@@ -210,6 +211,7 @@ export function ServicesGroupedCatalog({
domainLabel,
isLoading = false,
primaryAction,
+ hideHeader = false,
togglingId,
activeTab: controlledTab,
onTabChange,
@@ -347,21 +349,23 @@ export function ServicesGroupedCatalog({
tableLayout={{ dense: true }}
>
-
-
- Сервисы
-
- {domainLabel
- ? `Каталог сервисов с привязками к ${domainLabel}`
- : 'Группы, FQDN и доступность сервисов'}
-
-
- {primaryAction ? (
-
- {primaryAction}
+ {!hideHeader ? (
+
+
+ Сервисы
+
+ {domainLabel
+ ? `Каталог сервисов с привязками к ${domainLabel}`
+ : 'Группы, FQDN и доступность сервисов'}
+
- ) : null}
-
+ {primaryAction ? (
+
+ {primaryAction}
+
+ ) : null}
+
+ ) : null}
diff --git a/apps/web/src/routes/_auth/certificates.tsx b/apps/web/src/routes/_auth/certificates.tsx
index a57c84a..acb1c1b 100644
--- a/apps/web/src/routes/_auth/certificates.tsx
+++ b/apps/web/src/routes/_auth/certificates.tsx
@@ -6,6 +6,7 @@ import type { Filter } from '@/components/reui/filters'
import { certificatesQueryOptions, certKeys, certSummaryQueryOptions } from '@/queries'
import { api } from '@/lib/api-client'
import { PageShell } from '@/components/page-shell'
+import { PageHeader } from '@/components/page-header'
import { ResourcePage } from '@/components/reui-kit'
import { CertKpiCards } from '@/components/cert-kpi-cards'
import {
@@ -71,6 +72,11 @@ function CertificatesPage() {
return (
+
({ ...tab }))}
activeTab={activeTab}
onTabChange={setActiveTab}
@@ -97,7 +104,6 @@ function CertificatesPage() {
isError={isError}
error={error}
onRetry={refetch}
- primaryAction={primaryAction}
emptyState={{
title: 'Сертификаты не найдены',
description: 'Сертификаты появятся после проверки доменов',
diff --git a/apps/web/src/routes/_auth/domains/index.tsx b/apps/web/src/routes/_auth/domains/index.tsx
index 3266b9f..216740a 100644
--- a/apps/web/src/routes/_auth/domains/index.tsx
+++ b/apps/web/src/routes/_auth/domains/index.tsx
@@ -15,6 +15,7 @@ import {
serviceBindingKeys,
} from '@/queries'
import { PageShell } from '@/components/page-shell'
+import { PageHeader } from '@/components/page-header'
import { DomainKpiCards } from '@/components/domain-kpi-cards'
import { ResourcePage } from '@/components/reui-kit'
import {
@@ -149,6 +150,11 @@ function DomainsPage() {
return (
+
({ ...tab }))}
activeTab={activeTab}
onTabChange={setActiveTab}
@@ -174,7 +181,6 @@ function DomainsPage() {
isError={isError}
error={error}
onRetry={refetch}
- primaryAction={primaryAction}
enableRowSelection
selectionToolbar={({ selectedIds, clearSelection }) => (
)
+ const headerActions = (
+ <>
+ {createButton}
+
+ >
+ )
+
if (isLoading) {
return (
+
{view === 'board' ? (
) : (
+
+
{view === 'board' ? (
(groups?.length ?? 0) === 0 && (domains?.length ?? 0) === 0 ? (
- {createButton}
-
- >
- }
renderCard={(domain, ctx) => (
({ ...tab }))}
activeTab={activeTab}
onTabChange={setActiveTab}
@@ -301,12 +323,6 @@ function GroupsPage() {
columns={columns}
data={catalogRows}
getRowId={(row) => String(row.id)}
- primaryAction={
- <>
- {createButton}
-
- >
- }
emptyState={{
title: 'Группы не найдены',
description: 'Создайте группу для организации доменов.',
diff --git a/apps/web/src/routes/_auth/services.tsx b/apps/web/src/routes/_auth/services.tsx
index c0e63af..f56a7c5 100644
--- a/apps/web/src/routes/_auth/services.tsx
+++ b/apps/web/src/routes/_auth/services.tsx
@@ -21,6 +21,7 @@ import type {
UpdateServiceConfigInput,
} from '@/lib/schemas'
import { PageShell } from '@/components/page-shell'
+import { PageHeader } from '@/components/page-header'
import { ServiceKpiCards } from '@/components/service-kpi-cards'
import { EmptyState } from '@/components/empty-state'
import { ConfirmDialog } from '@/components/confirm-dialog'
@@ -437,6 +438,17 @@ function ServicesPage() {
/>
)
+ const headerActions = (
+ <>
+ {addMenu}
+
+ >
+ )
+
+ const pageDescription = filteredDomain
+ ? `Сервисы с привязками к домену ${filteredDomain.zone_name}`
+ : 'Группы, FQDN и доступность сервисов'
+
const sheets = (
<>
+
{view === 'board' ? (
) : (
{}}
onDeleteService={() => {}}
@@ -551,6 +569,7 @@ function ServicesPage() {
if (isError) {
return (
+
+
{view === 'board' ? (
<>
{addMenu}
-
}
/>
@@ -603,12 +626,6 @@ function ServicesPage() {
columns={columnConfigs}
value={kanbanValue}
onValueChange={dragDisabled ? () => {} : handleKanbanValueChange}
- toolbarActions={
- <>
- {addMenu}
-
- >
- }
renderCard={(service, ctx) => (
- {addMenu}
-
- >
- }
emptyAction={
{addMenu}