diff --git a/projects/social_platform/src/app/domain/program/program-launch.model.ts b/projects/social_platform/src/app/domain/program/program-launch.model.ts
index b42a99a9f..babbcf7c0 100644
--- a/projects/social_platform/src/app/domain/program/program-launch.model.ts
+++ b/projects/social_platform/src/app/domain/program/program-launch.model.ts
@@ -1,6 +1,7 @@
/** @format */
import { Program } from "./program.model";
import { ManagementPage } from "./program-management.model";
+import { ProgramImageFraming } from "./program-image-framing.model";
export type ProgramFormat = "case_championship" | "hackathon" | "noncompetitive";
export const PROGRAM_FORMATS: { id: ProgramFormat; label: string; description: string }[] = [
@@ -24,6 +25,8 @@ export interface ManagedProgram {
id: number;
name: string;
draft: boolean;
+ imageAddress?: string | null;
+ imageFraming?: ProgramImageFraming | null;
programFormat: ProgramFormat | null;
solutionStorageMode?: "legacy_project" | "solution";
updatedAt: string;
diff --git a/projects/social_platform/src/app/ui/pages/program/main/main.component.scss b/projects/social_platform/src/app/ui/pages/program/main/main.component.scss
index 86cd8625a..f45591400 100644
--- a/projects/social_platform/src/app/ui/pages/program/main/main.component.scss
+++ b/projects/social_platform/src/app/ui/pages/program/main/main.component.scss
@@ -1,5 +1,11 @@
@use "styles/responsive";
+:host {
+ display: block;
+ width: 100%;
+ min-width: 0;
+}
+
.programs {
display: grid;
grid-template-columns: 1fr;
diff --git a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.html b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.html
index 491f35d5c..1e8a68a86 100644
--- a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.html
+++ b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.html
@@ -1,94 +1,120 @@
-
-@if (desktopLayout()) {
- @if (program()) {
+
+ @if (desktopLayout()) {
-
-
{{ program().name }}
-
+
+ {{ program().name }}
+
-
- {{
- registerDateExpired
- ? "регистрация завершена"
- : program().isUserMember
- ? "ты уже участвуешь!"
- : "Регистрация до " + (program().datetimeRegistrationEnds | date: "dd MMMM")
- }}
+
-
-
- {{ program().datetimeStarted | date: "dd.MM.yyyy" }}
-
-
-
-
- {{ program().datetimeFinished | date: "dd.MM.yyyy" }}
-
-
-
• для всей России
+ @if (program().datetimeStarted && program().datetimeFinished) {
+
+ {{ program().datetimeStarted | date: "dd.MM.yyyy" }}
+
+
-
+
+ {{ program().datetimeFinished | date: "dd.MM.yyyy" }}
+
+
• для всей России
+ } @else {
+
+ {{
+ program().datetimeStarted || program().datetimeFinished
+ ? "Сроки заполнены не полностью"
+ : "Сроки не заданы"
+ }}
+
+ }
- }
-} @else {
- @if (program()) {
-
+ } @else {
+
-
{{ program().name }}
-
-
- {{
- registerDateExpired
- ? "регистрация завершена"
- : program().isUserMember
- ? "ты уже участвуешь!"
- : "Регистрация до " + (program().datetimeRegistrationEnds | date: "dd MMMM")
- }}
+
-
-
- {{ program().datetimeStarted | date: "dd.MM.yyyy" }} -
- {{ program().datetimeFinished | date: "dd.MM.yyyy" }}
-
-
-
для всей России
+ @if (program().datetimeStarted && program().datetimeFinished) {
+
+ {{ program().datetimeStarted | date: "dd.MM.yyyy" }} -
+ {{ program().datetimeFinished | date: "dd.MM.yyyy" }}
+
+
для всей России
+ } @else {
+
+ {{
+ program().datetimeStarted || program().datetimeFinished
+ ? "Сроки заполнены не полностью"
+ : "Сроки не заданы"
+ }}
+
+ }
}
-}
+ @if (management()) {
+
+ }
+
+
+ @switch (registrationState()) {
+ @case ("draft") {
+ Черновик
+ }
+ @case ("ended") {
+ регистрация завершена
+ }
+ @case ("participating") {
+ ты уже участвуешь!
+ }
+ @case ("actual") {
+ Регистрация до {{ program().datetimeRegistrationEnds | date: "dd MMMM" }}
+ }
+ @default {
+ Срок регистрации не задан
+ }
+ }
+
diff --git a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.scss b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.scss
index e66ff0477..811b5a279 100644
--- a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.scss
+++ b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.scss
@@ -2,8 +2,77 @@
@use "styles/ui-foundation";
@use "styles/responsive";
+@use "styles/program-management-controls";
+
+:host {
+ display: block;
+ min-width: 0;
+}
+
+.program-card {
+ &__actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 8px;
+ }
+
+ &__publication {
+ min-width: 0;
+ margin: 0;
+ font-size: 10px;
+ line-height: 13px;
+ color: var(--dark-grey);
+ }
+
+ .ui-button.ui-button {
+ @include ui-foundation.button;
+ @include program-management-controls.compact-action;
+
+ flex-shrink: 0;
+ min-width: 110px;
+ }
+
+ .program-card__continue {
+ width: 188px;
+ }
+}
+
+.card__registration--draft,
+.card__registration--not-set {
+ background: var(--dark-grey);
+}
@media screen and (max-width: 999px) {
+ .program-card--managed {
+ @include ui-foundation.card;
+
+ .card {
+ padding: 0;
+ background: transparent;
+ border: 0;
+ box-shadow: none;
+ }
+ }
+
+ .program-card__actions {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 8px;
+
+ .ui-button.ui-button {
+ justify-self: stretch;
+ width: 100%;
+ min-height: 44px;
+ }
+ }
+
+ .program-card__publication {
+ font-size: 12px;
+ line-height: 18px;
+ }
+
.card {
@include ui-foundation.card;
@@ -72,6 +141,21 @@
}
@media screen and (min-width: 1000px) {
+ .program-card {
+ max-width: 333px;
+ }
+
+ .program-card--managed {
+ .card__info {
+ flex: 1;
+ }
+
+ .ui-button.ui-button.program-card__continue {
+ padding-inline: 12px;
+ white-space: nowrap;
+ }
+ }
+
.card {
position: relative;
display: flex;
diff --git a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.spec.ts
index 9d4d92341..d4b839adb 100644
--- a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.spec.ts
+++ b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.spec.ts
@@ -1,28 +1,88 @@
/** @format */
-
+import { signal } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { ProgramCardComponent } from "./program-card.component";
+import { provideRouter } from "@angular/router";
+import { DesktopLayoutService } from "@uilib";
+import { ProgramCardComponent, ProgramCardData } from "./program-card.component";
describe("ProgramCardComponent", () => {
- let component: ProgramCardComponent;
let fixture: ComponentFixture
;
-
+ const desktop = signal(true);
+ const program: ProgramCardData = {
+ id: 7,
+ name: "Программа",
+ datetimeStarted: "2099-11-10T00:00:00Z",
+ datetimeFinished: "2099-11-30T00:00:00Z",
+ datetimeRegistrationEnds: "2099-11-15T00:00:00Z",
+ };
+ const setProgram = (patch: Partial) => {
+ fixture.componentRef.setInput("program", { ...program, ...patch });
+ fixture.detectChanges();
+ };
+ const element = () => fixture.nativeElement as HTMLElement;
beforeEach(async () => {
+ desktop.set(true);
await TestBed.configureTestingModule({
imports: [ProgramCardComponent],
+ providers: [provideRouter([]), { provide: DesktopLayoutService, useValue: { desktop } }],
}).compileComponents();
-
fixture = TestBed.createComponent(ProgramCardComponent);
- component = fixture.componentInstance;
- fixture.componentRef.setInput("program", {
- id: 1,
- name: "Test Program",
- datetimeRegistrationEnds: new Date(Date.now() + 86400000).toISOString(),
+ setProgram({});
+ });
+ it("keeps the public card free of added actions", () => {
+ expect(element().querySelector(".card__name")?.textContent).toContain(program.name);
+ expect(element().querySelector(".program-card__actions")).toBeNull();
+ expect(element().querySelector("a")).toBeNull();
+ });
+ it("keeps expiry ahead of membership and updates reused inputs", () => {
+ setProgram({ isUserMember: true });
+ expect(element().querySelector(".card__registration--participating")).not.toBeNull();
+ setProgram({ isUserMember: true, datetimeRegistrationEnds: "2000-01-01T00:00:00Z" });
+ expect(element().querySelector(".card__registration--ended")?.textContent).toContain(
+ "регистрация завершена",
+ );
+ expect(element().querySelector(".card__registration--participating")).toBeNull();
+ });
+ it("continues a draft without inventing dates or an open registration", () => {
+ fixture.componentRef.setInput("management", true);
+ setProgram({
+ draft: true,
+ datetimeStarted: null,
+ datetimeFinished: null,
+ datetimeRegistrationEnds: null,
});
- fixture.detectChanges();
+ expect(element().querySelector(".card__registration")?.textContent).toContain("Черновик");
+ expect(element().querySelector(".card__dates")?.textContent).toContain("Сроки не заданы");
+ expect(element().textContent).toContain("Не виден участникам");
+ expect(element().querySelector("a")?.getAttribute("href")).toBe("/office/program/7/launch");
});
-
- it("should create", () => {
- expect(component).toBeTruthy();
+ it("routes a published program to management with its own publication status", () => {
+ fixture.componentRef.setInput("management", true);
+ setProgram({ draft: false });
+ expect(element().querySelector(".card__registration")?.textContent).toContain("Регистрация до");
+ expect(element().textContent).toContain("Опубликована");
+ expect(element().querySelector("a")?.getAttribute("href")).toBe(
+ "/office/program/7/management/participants",
+ );
+ });
+ it("uses the saved image and keeps the native mobile avatar", () => {
+ desktop.set(false);
+ setProgram({
+ imageAddress: "https://example.com/logo.png",
+ imageFraming: { logo: { x: 0.2, y: 0.7, zoom: 1.5 } },
+ });
+ expect(element().querySelector(".card__photo img")?.getAttribute("src")).toBe(
+ "https://example.com/logo.png",
+ );
+ expect(element().querySelector(".card__photo .avatar")?.getAttribute("style")).toContain(
+ "64px",
+ );
+ expect(element().querySelector("h3")?.getAttribute("title")).toBe(program.name);
+ });
+ it("does not imply registration is open without a deadline", () => {
+ setProgram({ datetimeRegistrationEnds: null });
+ expect(element().querySelector(".card__registration--not-set")?.textContent).toContain(
+ "Срок регистрации не задан",
+ );
});
});
diff --git a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.ts b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.ts
index de20162b1..a89f26ba8 100644
--- a/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.ts
+++ b/projects/social_platform/src/app/ui/pages/program/main/program-card/program-card.component.ts
@@ -1,33 +1,55 @@
/** @format */
-import { DesktopLayoutService } from "@uilib";
-/** @format */
-
-import { CardDirective } from "@uilib";
-/** @format */
-
-import { ChangeDetectionStrategy, Component, inject, input, Input, OnInit } from "@angular/core";
+import { ButtonDirective, CardDirective, DesktopLayoutService } from "@uilib";
+import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
import { AvatarComponent } from "@ui/primitives/avatar/avatar.component";
-import { DatePipe, NgClass } from "@angular/common";
-import { Program } from "@domain/program/program.model";
+import { DatePipe, NgClass, NgTemplateOutlet } from "@angular/common";
+import { RouterLink } from "@angular/router";
+import { ProgramImageFraming } from "@domain/program/program-image-framing.model";
+import { AppRoutes } from "@api/paths/app-routes";
+
+/** Shared saved identity for the public catalog and organizer cards. */
+export interface ProgramCardData {
+ id: number;
+ name: string;
+ imageAddress?: string | null;
+ imageFraming?: ProgramImageFraming | null;
+ datetimeRegistrationEnds: string | null;
+ datetimeStarted: string | null;
+ datetimeFinished: string | null;
+ isUserMember?: boolean;
+ draft?: boolean;
+}
/** Карточка программы с краткой информацией для списков. */
@Component({
selector: "app-program-card",
templateUrl: "./program-card.component.html",
styleUrl: "./program-card.component.scss",
- imports: [CardDirective, AvatarComponent, DatePipe, NgClass],
+ imports: [
+ CardDirective,
+ AvatarComponent,
+ DatePipe,
+ NgClass,
+ NgTemplateOutlet,
+ ButtonDirective,
+ RouterLink,
+ ],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ProgramCardComponent implements OnInit {
+export class ProgramCardComponent {
protected readonly desktopLayout = inject(DesktopLayoutService).desktop;
-
- readonly program = input.required();
-
- ngOnInit(): void {
+ protected readonly paths = AppRoutes;
+ readonly program = input.required();
+ readonly management = input(false);
+ readonly registerDateExpired = computed(() => {
const deadline = this.program().datetimeRegistrationEnds;
- this.registerDateExpired = Boolean(deadline && Date.now() > Date.parse(deadline));
- }
-
- registerDateExpired?: boolean;
+ return Boolean(deadline && Date.now() > Date.parse(deadline));
+ });
+ readonly registrationState = computed(() => {
+ if (this.management() && this.program().draft) return "draft";
+ if (this.registerDateExpired()) return "ended";
+ if (!this.management() && this.program().isUserMember) return "participating";
+ return this.program().datetimeRegistrationEnds ? "actual" : "not-set";
+ });
}
diff --git a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.html b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.html
index f23f17142..e50253e59 100644
--- a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.html
+++ b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.html
@@ -1,8 +1,14 @@
-
+
@if (loading()) {
Загружаем программы…
} @else if (error()) {
@@ -12,36 +18,26 @@
} @else if (data(); as data) {
@for (program of data.results; track program.id) {
-
-
-
{{ program.name }}
-
{{ program.draft ? "Черновик · Не виден участникам" : "Опубликована" }}
-
- {{ program.draft ? "Продолжить настройку" : "Управлять" }}
-
+
} @empty {
- @if (search.value) {
-
Программы не найдены
-
Попробуйте изменить название в поиске.
-
- } @else if (offset()) {
-
На этой странице программ нет.
-
- } @else {
-
Программы пока не созданы
-
Здесь появятся программы, которыми вы управляете, включая незавершенные черновики.
- @if (canCreate()) {
-
Создать программу
+
+ @if (search.value) {
+
Программы не найдены
+
Попробуйте изменить название в поиске.
+
+ } @else if (offset()) {
+
На этой странице программ нет.
+
+ } @else {
+
Программы пока не созданы
+
+ Здесь появятся программы, которыми вы управляете, включая незавершенные черновики.
+
+ @if (canCreate()) {
+
Создать программу
+ }
}
- }
+
}
@if (data.count > 10 || offset()) {
diff --git a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.scss b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.scss
index 7342229d8..0fae55272 100644
--- a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.scss
+++ b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.scss
@@ -4,6 +4,7 @@
:host {
display: block;
+ width: 100%;
min-width: 0;
}
@@ -31,50 +32,23 @@
}
&__search {
- display: grid;
- gap: 8px;
- max-width: 480px;
+ width: 100%;
margin-bottom: 24px;
}
&__list {
display: grid;
gap: 24px;
- padding: 20px;
- background: var(--light-white);
- border-radius: 15px;
+ align-items: start;
}
- &__row {
- display: flex;
- gap: 24px;
- align-items: center;
- justify-content: space-between;
-
- > div {
- min-width: 0;
- }
-
- > a {
- flex-shrink: 0;
- }
+ &__empty {
+ grid-column: 1 / -1;
}
}
-@media (max-width: 999px) {
- .managed {
- &__list {
- padding: 16px;
- }
-
- &__row {
- display: grid;
- gap: 12px;
-
- > a {
- justify-self: start;
- margin-block: 6px;
- }
- }
+@media (min-width: 1000px) {
+ .managed__list {
+ grid-template-columns: repeat(2, minmax(0, 333px));
}
}
diff --git a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.spec.ts
new file mode 100644
index 000000000..164a4b897
--- /dev/null
+++ b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.spec.ts
@@ -0,0 +1,49 @@
+/** @format */
+import { TestBed } from "@angular/core/testing";
+import { provideRouter, Router } from "@angular/router";
+import { RouterTestingHarness } from "@angular/router/testing";
+import { of } from "rxjs";
+import { ProgramLaunchUseCase } from "@api/program/use-cases/program-launch.use-case";
+import { ManagedProgramsComponent } from "./managed.component";
+
+describe("ManagedProgramsComponent", () => {
+ const api = {
+ capabilities: vi.fn(() => of({ canCreateProgram: false })),
+ managed: vi.fn(() => of({ count: 0, next: null, previous: null, results: [] })),
+ };
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ await TestBed.configureTestingModule({
+ providers: [
+ provideRouter([{ path: "managed", component: ManagedProgramsComponent }]),
+ { provide: ProgramLaunchUseCase, useValue: api },
+ ],
+ }).compileComponents();
+ });
+ it("keeps search in the URL, resets offset and preserves it on pagination", async () => {
+ const harness = await RouterTestingHarness.create();
+ const component = await harness.navigateByUrl(
+ "/managed?search=исходный&offset=10",
+ ManagedProgramsComponent,
+ );
+ expect(api.managed).toHaveBeenLastCalledWith({ search: "исходный", offset: 10, limit: 10 });
+ component.search.setValue("новый");
+ const router = TestBed.inject(Router);
+ await vi.waitFor(() => {
+ expect(router.parseUrl(router.url).queryParams).toEqual({ search: "новый" });
+ expect(api.managed).toHaveBeenLastCalledWith({ search: "новый", offset: 0, limit: 10 });
+ });
+ component.page(1);
+ await vi.waitFor(() =>
+ expect(router.parseUrl(router.url).queryParams).toEqual({ search: "новый", offset: "10" }),
+ );
+ });
+ it("does not offer creation when the separate permission is absent", async () => {
+ const harness = await RouterTestingHarness.create();
+ await harness.navigateByUrl("/managed", ManagedProgramsComponent);
+ expect(harness.routeNativeElement?.textContent).toContain("Программы пока не созданы");
+ expect(
+ harness.routeNativeElement?.querySelector('a[href="/office/program/create"]'),
+ ).toBeNull();
+ });
+});
diff --git a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.ts b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.ts
index 0aea84bd4..c9b4bf8d7 100644
--- a/projects/social_platform/src/app/ui/pages/program/managed/managed.component.ts
+++ b/projects/social_platform/src/app/ui/pages/program/managed/managed.component.ts
@@ -27,6 +27,7 @@ import { ProgramLaunchUseCase } from "@api/program/use-cases/program-launch.use-
import { ManagedProgramsPage } from "@domain/program/program-launch.model";
import { AppRoutes } from "@api/paths/app-routes";
import { evaluationError } from "@utils/evaluation-workspace";
+import { ProgramCardComponent } from "../main/program-card/program-card.component";
@Component({
selector: "app-managed-programs",
@@ -37,6 +38,7 @@ import { evaluationError } from "@utils/evaluation-workspace";
AlertDirective,
ButtonDirective,
PaginationComponent,
+ ProgramCardComponent,
],
templateUrl: "./managed.component.html",
styleUrl: "./managed.component.scss",
diff --git a/projects/social_platform/src/app/ui/pages/program/program.component.html b/projects/social_platform/src/app/ui/pages/program/program.component.html
index 720815a54..97c82e6f2 100644
--- a/projects/social_platform/src/app/ui/pages/program/program.component.html
+++ b/projects/social_platform/src/app/ui/pages/program/program.component.html
@@ -10,26 +10,24 @@
>
}
-
- @if (!isManaged()) {
-
- }
-
+
+ @if (!isManaged()) {
+
+ }
diff --git a/projects/social_platform/src/app/ui/pages/program/program.component.scss b/projects/social_platform/src/app/ui/pages/program/program.component.scss
index 7299b997d..f4d45c78d 100644
--- a/projects/social_platform/src/app/ui/pages/program/program.component.scss
+++ b/projects/social_platform/src/app/ui/pages/program/program.component.scss
@@ -49,15 +49,14 @@
}
.program-navigation {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
+ display: block;
+ width: 100%;
margin-bottom: 24px;
+}
- &__active.ui-button {
- color: var(--white);
- background: var(--accent);
- }
+.page__search {
+ width: 100%;
+ margin-bottom: 24px;
}
@media (max-width: 650px) {
@@ -69,8 +68,4 @@
margin-block: 6px;
}
}
-
- .program-navigation > a {
- margin-block: 6px;
- }
}
diff --git a/projects/social_platform/src/app/ui/pages/program/program.component.ts b/projects/social_platform/src/app/ui/pages/program/program.component.ts
index 1867bb260..070f765f6 100644
--- a/projects/social_platform/src/app/ui/pages/program/program.component.ts
+++ b/projects/social_platform/src/app/ui/pages/program/program.component.ts
@@ -8,7 +8,7 @@ import {
OnInit,
signal,
} from "@angular/core";
-import { NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from "@angular/router";
+import { NavigationEnd, Router, RouterLink, RouterOutlet } from "@angular/router";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { filter, startWith } from "rxjs";
import { ReactiveFormsModule } from "@angular/forms";
@@ -19,6 +19,7 @@ import {
FormLayoutDirective,
IconComponent,
PageHeaderComponent,
+ TabsComponent,
} from "@uilib";
import { AppRoutes } from "@api/paths/app-routes";
import { ProgramLaunchUseCase } from "@api/program/use-cases/program-launch.use-case";
@@ -38,7 +39,7 @@ import { ProgramMainUIInfoService } from "@api/program/facades/ui/program-main-u
RouterOutlet,
BackComponent,
RouterLink,
- RouterLinkActive,
+ TabsComponent,
ButtonDirective,
IconComponent,
],
@@ -50,6 +51,10 @@ export class ProgramComponent implements OnInit {
private readonly destroy = inject(DestroyRef);
private readonly router = inject(Router);
readonly paths = AppRoutes;
+ readonly programTabs = [
+ { link: AppRoutes.program.list(), linkText: "Все программы", isRouterLinkActiveOptions: true },
+ { link: AppRoutes.program.managed(), linkText: "Я организую", isRouterLinkActiveOptions: true },
+ ];
readonly canCreate = signal(false);
readonly isManaged = signal(false);
private readonly programInfoService = inject(ProgramInfoService);
diff --git a/projects/social_platform/src/app/ui/primitives/search/search.component.html b/projects/social_platform/src/app/ui/primitives/search/search.component.html
index 95a887c88..5a8252a8b 100644
--- a/projects/social_platform/src/app/ui/primitives/search/search.component.html
+++ b/projects/social_platform/src/app/ui/primitives/search/search.component.html
@@ -1,6 +1,11 @@
-
+
diff --git a/projects/social_platform/src/app/ui/primitives/search/search.component.scss b/projects/social_platform/src/app/ui/primitives/search/search.component.scss
index a4416338a..520142c62 100644
--- a/projects/social_platform/src/app/ui/primitives/search/search.component.scss
+++ b/projects/social_platform/src/app/ui/primitives/search/search.component.scss
@@ -157,3 +157,22 @@
}
}
}
+
+// Always-open inline search, matching the existing underlined search in the UI kit.
+.search--inline .search__input {
+ width: 100%;
+
+ input {
+ min-height: 44px;
+ padding: 0;
+ font-size: 12px;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: -2px;
+ }
+ }
+}
diff --git a/projects/social_platform/src/app/ui/primitives/search/search.component.ts b/projects/social_platform/src/app/ui/primitives/search/search.component.ts
index b7a05f985..d83602ecb 100644
--- a/projects/social_platform/src/app/ui/primitives/search/search.component.ts
+++ b/projects/social_platform/src/app/ui/primitives/search/search.component.ts
@@ -54,6 +54,8 @@ import { IconComponent } from "../icon/icon.component";
export class SearchComponent implements ControlValueAccessor, OnInit {
/** Текст подсказки */
placeholder = input("");
+ readonly ariaLabel = input("Поиск");
+ readonly appearance = input<"default" | "inline">("default");
/** Тип поля ввода */
type = input<"text" | "password" | "email">("text");
diff --git a/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss b/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss
index ddc649f0d..592b7d4fe 100644
--- a/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss
+++ b/projects/ui/src/lib/components/primitives/tabs/tabs.component.scss
@@ -39,6 +39,55 @@
}
}
+// Opt-in route navigation matching the UI kit's Tabs / Underline.
+.tabs.tabs--underline {
+ box-sizing: border-box;
+ display: flex;
+ gap: 0;
+ width: 100%;
+ min-height: 63px;
+ overflow: hidden;
+ background: var(--white);
+ border: 1px solid var(--medium-grey-for-outline);
+ border-radius: var(--ui-radius-card);
+
+ .tabs__item {
+ display: flex;
+ flex: 1 1 0;
+ gap: 8px;
+ align-items: center;
+ justify-content: flex-start;
+ min-width: 0;
+ min-height: 61px;
+ padding: 18px 20px;
+ margin: 0;
+ font: inherit;
+ font-size: 12px;
+ line-height: 16px;
+ color: var(--grey-for-text);
+ white-space: normal;
+ background: transparent;
+ border: 0;
+ border-bottom: 2px solid transparent;
+ border-radius: 0;
+
+ &--active,
+ &:hover:not(:disabled) {
+ color: var(--black);
+ background: transparent;
+ }
+
+ &--active {
+ border-bottom-color: var(--black);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: -3px;
+ }
+ }
+}
+
@media screen and (max-width: 999px) {
:host {
display: block;
diff --git a/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts b/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts
index b72d90db3..eba5eed6a 100644
--- a/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts
+++ b/projects/ui/src/lib/components/primitives/tabs/tabs.component.ts
@@ -18,7 +18,12 @@ export interface TabItem {
selector: "app-tabs",
imports: [RouterLink, RouterLinkActive, IconComponent],
template: `
-