add icons

This commit is contained in:
TronoSfera 2026-03-02 20:26:57 +03:00
parent a9704d77b0
commit e2dbf530bd
19 changed files with 291 additions and 107 deletions

View file

@ -53,6 +53,17 @@
.logo a { .logo a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.brand-mark {
width: 24px;
height: 24px;
display: inline-block;
flex-shrink: 0;
border-radius: 50%;
} }
.menu { .menu {
@ -275,6 +286,13 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.section-head-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.config-head-actions { .config-head-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
@ -282,16 +300,16 @@
align-items: center; align-items: center;
} }
.btn-icon-only { .ui-glyph {
width: 40px; width: 17px;
min-width: 40px; height: 17px;
height: 40px; display: block;
min-height: 40px; stroke: currentColor;
padding: 0; fill: none;
display: inline-grid; stroke-width: 1.9;
place-items: center; stroke-linecap: round;
font-size: 1.06rem; stroke-linejoin: round;
line-height: 1; vector-effect: non-scaling-stroke;
} }
.muted { .muted {
@ -1205,6 +1223,43 @@
margin-top: -1px; margin-top: -1px;
} }
.table-footer-actions {
display: inline-flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
margin-left: auto;
}
.table-control-btn {
width: 40px;
min-width: 40px;
height: 40px;
min-height: 40px;
padding: 0;
display: inline-grid;
place-items: center;
line-height: 1;
}
.table-control-loadall {
width: auto;
min-width: 58px;
padding: 0 0.62rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.33rem;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.table-control-loadall .ui-glyph {
width: 15px;
height: 15px;
}
.filter-toolbar + .table-scroll-region { .filter-toolbar + .table-scroll-region {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
@ -1273,31 +1328,6 @@
margin-left: auto; margin-left: auto;
} }
.config-control-btn {
width: 40px;
min-width: 40px;
height: 40px;
min-height: 40px;
padding: 0;
display: inline-grid;
place-items: center;
font-size: 1.05rem;
line-height: 1;
}
.config-control-btn-loadall {
width: auto;
min-width: 58px;
padding: 0 0.62rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.33rem;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.block { .block {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 12px; border-radius: 12px;

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Административная панель • Правовой трекер</title> <title>Административная панель • Правовой трекер</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<link rel="stylesheet" href="/admin.css?v=20260225-12"> <link rel="stylesheet" href="/admin.css?v=20260225-12">
</head> </head>
<body> <body>

View file

@ -60,6 +60,7 @@ import {
translateApiError, translateApiError,
userInitials, userInitials,
} from "./admin/shared/utils.js"; } from "./admin/shared/utils.js";
import { AddIcon, DownloadIcon, FilterIcon, NextIcon, PrevIcon, RefreshIcon } from "./admin/shared/icons.jsx";
import QRCode from "qrcode"; import QRCode from "qrcode";
(function () { (function () {
@ -120,7 +121,7 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
); );
} }
function TablePager({ tableState, onPrev, onNext, onLoadAll }) { function TablePager({ tableState, onPrev, onNext, onLoadAll, onRefresh, onCreate, onOpenFilter }) {
return ( return (
<div className="pager table-footer-bar"> <div className="pager table-footer-bar">
<div> <div>
@ -128,25 +129,45 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
? "Всего: " + tableState.total + " • показаны все записи" ? "Всего: " + tableState.total + " • показаны все записи"
: "Всего: " + tableState.total + " • смещение: " + tableState.offset} : "Всего: " + tableState.total + " • смещение: " + tableState.offset}
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}> <div className="table-footer-actions">
<button <button
className="btn secondary" className="btn secondary table-control-btn table-control-loadall"
type="button" type="button"
onClick={onLoadAll} onClick={onLoadAll}
disabled={tableState.total === 0 || tableState.showAll || tableState.rows.length >= tableState.total} disabled={tableState.total === 0 || tableState.showAll || tableState.rows.length >= tableState.total}
title={"Загрузить все " + tableState.total}
aria-label={"Загрузить все " + tableState.total}
> >
{"Загрузить все " + tableState.total} <DownloadIcon />
<span>{tableState.total}</span>
</button> </button>
<button className="btn secondary" type="button" onClick={onPrev} disabled={tableState.showAll || tableState.offset <= 0}> {onRefresh ? (
Назад <button className="btn secondary table-control-btn" type="button" onClick={onRefresh} title="Обновить" aria-label="Обновить">
<RefreshIcon />
</button>
) : null}
{onCreate ? (
<button className="btn secondary table-control-btn" type="button" onClick={onCreate} title="Добавить" aria-label="Добавить">
<AddIcon />
</button>
) : null}
{onOpenFilter ? (
<button className="btn secondary table-control-btn" type="button" onClick={onOpenFilter} title="Фильтр" aria-label="Фильтр">
<FilterIcon />
</button>
) : null}
<button className="btn secondary table-control-btn" type="button" onClick={onPrev} disabled={tableState.showAll || tableState.offset <= 0} title="Назад" aria-label="Назад">
<PrevIcon />
</button> </button>
<button <button
className="btn secondary" className="btn secondary table-control-btn"
type="button" type="button"
onClick={onNext} onClick={onNext}
disabled={tableState.showAll || tableState.offset + PAGE_SIZE >= tableState.total} disabled={tableState.showAll || tableState.offset + PAGE_SIZE >= tableState.total}
title="Вперед"
aria-label="Вперед"
> >
Вперед <NextIcon />
</button> </button>
</div> </div>
</div> </div>
@ -192,8 +213,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
</div> </div>
{!hideAction ? ( {!hideAction ? (
<div className="filter-action"> <div className="filter-action">
<button className="btn secondary" type="button" onClick={onOpen}> <button className="btn secondary table-control-btn" type="button" onClick={onOpen} title="Фильтр" aria-label="Фильтр">
Фильтр <FilterIcon />
</button> </button>
</div> </div>
) : null} ) : null}
@ -3448,7 +3469,10 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
<div className="layout"> <div className="layout">
<aside className="sidebar"> <aside className="sidebar">
<div className="logo"> <div className="logo">
<a href="/">Правовой трекер</a> <a href="/">
<img className="brand-mark" src="/brand-mark.svg" alt="" width="24" height="24" />
<span>Правовой трекер</span>
</a>
</div> </div>
<nav className="menu"> <nav className="menu">
{menuItems.map((item) => ( {menuItems.map((item) => (

View file

@ -1,4 +1,5 @@
import { KNOWN_CONFIG_TABLE_KEYS, OPERATOR_LABELS, PAGE_SIZE, TABLE_SERVER_CONFIG } from "../../shared/constants.js"; import { KNOWN_CONFIG_TABLE_KEYS, OPERATOR_LABELS, PAGE_SIZE, TABLE_SERVER_CONFIG } from "../../shared/constants.js";
import { AddIcon, DownloadIcon, FilterIcon, NextIcon, PrevIcon, RefreshIcon } from "../../shared/icons.jsx";
import { boolLabel, fmtDate, listPreview, normalizeReferenceMeta, roleLabel, statusKindLabel, statusLabel } from "../../shared/utils.js"; import { boolLabel, fmtDate, listPreview, normalizeReferenceMeta, roleLabel, statusKindLabel, statusLabel } from "../../shared/utils.js";
function fmtBalance(value) { function fmtBalance(value) {
@ -67,7 +68,6 @@ export function ConfigSection(props) {
const StatusLine = StatusLineComponent; const StatusLine = StatusLineComponent;
const IconButton = IconButtonComponent; const IconButton = IconButtonComponent;
const UserAvatar = UserAvatarComponent; const UserAvatar = UserAvatarComponent;
const canOpenFilter = Boolean(configActiveKey);
const canRefresh = Boolean(configActiveKey); const canRefresh = Boolean(configActiveKey);
const canCreateRecord = Boolean(canCreateInConfig && configActiveKey); const canCreateRecord = Boolean(canCreateInConfig && configActiveKey);
const canLoadAllRows = Boolean( const canLoadAllRows = Boolean(
@ -102,16 +102,6 @@ export function ConfigSection(props) {
Баланс Баланс
</button> </button>
) : null} ) : null}
<button
className="btn secondary btn-icon-only"
type="button"
onClick={() => openFilterModal(configActiveKey)}
disabled={!canOpenFilter}
title="Фильтр"
aria-label="Фильтр"
>
</button>
</div> </div>
</div> </div>
<div className="config-layout"> <div className="config-layout">
@ -606,55 +596,65 @@ export function ConfigSection(props) {
</div> </div>
<div className="config-controls-actions"> <div className="config-controls-actions">
<button <button
className="btn secondary config-control-btn config-control-btn-loadall" className="btn secondary table-control-btn table-control-loadall"
type="button" type="button"
onClick={() => loadAllRows(configActiveKey)} onClick={() => loadAllRows(configActiveKey)}
disabled={!canLoadAllRows} disabled={!canLoadAllRows}
title={"Загрузить все " + activeConfigTableState.total} title={"Загрузить все " + activeConfigTableState.total}
aria-label={"Загрузить все " + activeConfigTableState.total} aria-label={"Загрузить все " + activeConfigTableState.total}
> >
<span aria-hidden="true"></span> <DownloadIcon />
<span>{activeConfigTableState.total}</span> <span>{activeConfigTableState.total}</span>
</button> </button>
<button <button
className="btn secondary btn-icon-only config-control-btn" className="btn secondary table-control-btn"
type="button" type="button"
onClick={() => loadCurrentConfigTable(true)} onClick={() => loadCurrentConfigTable(true)}
disabled={!canRefresh} disabled={!canRefresh}
title="Обновить" title="Обновить"
aria-label="Обновить" aria-label="Обновить"
> >
<RefreshIcon />
</button> </button>
<button <button
className="btn secondary btn-icon-only config-control-btn" className="btn secondary table-control-btn"
type="button" type="button"
onClick={() => openCreateRecordModal(configActiveKey)} onClick={() => openCreateRecordModal(configActiveKey)}
disabled={!canCreateRecord} disabled={!canCreateRecord}
title="Добавить" title="Добавить"
aria-label="Добавить" aria-label="Добавить"
> >
+ <AddIcon />
</button> </button>
<button <button
className="btn secondary btn-icon-only config-control-btn" className="btn secondary table-control-btn"
type="button"
onClick={() => openFilterModal(configActiveKey)}
disabled={!configActiveKey}
title="Фильтр"
aria-label="Фильтр"
>
<FilterIcon />
</button>
<button
className="btn secondary table-control-btn"
type="button" type="button"
onClick={() => loadPrevPage(configActiveKey)} onClick={() => loadPrevPage(configActiveKey)}
disabled={!canLoadPrev} disabled={!canLoadPrev}
title="Назад" title="Назад"
aria-label="Назад" aria-label="Назад"
> >
<PrevIcon />
</button> </button>
<button <button
className="btn secondary btn-icon-only config-control-btn" className="btn secondary table-control-btn"
type="button" type="button"
onClick={() => loadNextPage(configActiveKey)} onClick={() => loadNextPage(configActiveKey)}
disabled={!canLoadNext} disabled={!canLoadNext}
title="Вперед" title="Вперед"
aria-label="Вперед" aria-label="Вперед"
> >
<NextIcon />
</button> </button>
</div> </div>
</div> </div>

View file

@ -40,20 +40,13 @@ export function InvoicesSection({
<h2>Счета</h2> <h2>Счета</h2>
<p className="muted">Выставленные счета клиентам, статусы оплаты и выгрузка PDF.</p> <p className="muted">Выставленные счета клиентам, статусы оплаты и выгрузка PDF.</p>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button className="btn secondary" type="button" onClick={onRefresh}>
Обновить
</button>
<button className="btn" type="button" onClick={onCreate}>
Новый счет
</button>
</div>
</div> </div>
<FilterToolbar <FilterToolbar
filters={tableState.filters} filters={tableState.filters}
onOpen={onOpenFilter} onOpen={onOpenFilter}
onRemove={onRemoveFilter} onRemove={onRemoveFilter}
onEdit={onEditFilter} onEdit={onEditFilter}
hideAction
getChipLabel={(clause) => { getChipLabel={(clause) => {
const fieldDef = getFieldDef("invoices", clause.field); const fieldDef = getFieldDef("invoices", clause.field);
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("invoices", clause); return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("invoices", clause);
@ -112,7 +105,15 @@ export function InvoicesSection({
</tr> </tr>
)} )}
/> />
<TablePager tableState={tableState} onPrev={onPrev} onNext={onNext} onLoadAll={onLoadAll} /> <TablePager
tableState={tableState}
onPrev={onPrev}
onNext={onNext}
onLoadAll={onLoadAll}
onRefresh={onRefresh}
onCreate={onCreate}
onOpenFilter={onOpenFilter}
/>
<StatusLine status={status} /> <StatusLine status={status} />
</> </>
); );

View file

@ -1,4 +1,5 @@
import { KANBAN_GROUPS } from "../../shared/constants.js"; import { KANBAN_GROUPS } from "../../shared/constants.js";
import { FilterIcon, RefreshIcon } from "../../shared/icons.jsx";
import { fallbackStatusGroup, fmtKanbanDate, resolveDeadlineTone, statusLabel } from "../../shared/utils.js"; import { fallbackStatusGroup, fmtKanbanDate, resolveDeadlineTone, statusLabel } from "../../shared/utils.js";
export function KanbanBoard({ export function KanbanBoard({
@ -70,12 +71,15 @@ export function KanbanBoard({
<h2>Канбан заявок</h2> <h2>Канбан заявок</h2>
<p className="muted">Группировка по группам статусов и серверная фильтрация карточек.</p> <p className="muted">Группировка по группам статусов и серверная фильтрация карточек.</p>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}> <div className="section-head-actions">
<button className={"btn secondary" + (sortActive ? " active-success" : "")} type="button" onClick={onOpenSort}> <button className={"btn secondary" + (sortActive ? " active-success" : "")} type="button" onClick={onOpenSort}>
Сортировка Сортировка
</button> </button>
<button className="btn secondary" type="button" onClick={onRefresh} disabled={loading}> <button className="btn secondary table-control-btn" type="button" onClick={onRefresh} disabled={loading} title="Обновить" aria-label="Обновить">
Обновить <RefreshIcon />
</button>
<button className="btn secondary table-control-btn" type="button" onClick={onOpenFilter} title="Фильтр" aria-label="Фильтр">
<FilterIcon />
</button> </button>
</div> </div>
</div> </div>
@ -85,6 +89,7 @@ export function KanbanBoard({
onOpen={onOpenFilter} onOpen={onOpenFilter}
onRemove={onRemoveFilter} onRemove={onRemoveFilter}
onEdit={onEditFilter} onEdit={onEditFilter}
hideAction
getChipLabel={getFilterChipLabel} getChipLabel={getFilterChipLabel}
/> />
) : null} ) : null}

View file

@ -37,20 +37,13 @@ export function QuotesSection({
<h2>Цитаты</h2> <h2>Цитаты</h2>
<p className="muted">Управление публичной лентой цитат с серверными фильтрами.</p> <p className="muted">Управление публичной лентой цитат с серверными фильтрами.</p>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button className="btn secondary" type="button" onClick={onRefresh}>
Обновить
</button>
<button className="btn" type="button" onClick={onCreate}>
Новая цитата
</button>
</div>
</div> </div>
<FilterToolbar <FilterToolbar
filters={tableState.filters} filters={tableState.filters}
onOpen={onOpenFilter} onOpen={onOpenFilter}
onRemove={onRemoveFilter} onRemove={onRemoveFilter}
onEdit={onEditFilter} onEdit={onEditFilter}
hideAction
getChipLabel={(clause) => { getChipLabel={(clause) => {
const fieldDef = getFieldDef("quotes", clause.field); const fieldDef = getFieldDef("quotes", clause.field);
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("quotes", clause); return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("quotes", clause);
@ -87,7 +80,15 @@ export function QuotesSection({
</tr> </tr>
)} )}
/> />
<TablePager tableState={tableState} onPrev={onPrev} onNext={onNext} onLoadAll={onLoadAll} /> <TablePager
tableState={tableState}
onPrev={onPrev}
onNext={onNext}
onLoadAll={onLoadAll}
onRefresh={onRefresh}
onCreate={onCreate}
onOpenFilter={onOpenFilter}
/>
<StatusLine status={status} /> <StatusLine status={status} />
</> </>
); );

View file

@ -122,20 +122,13 @@ export function RequestsSection({
<h2>Заявки</h2> <h2>Заявки</h2>
<p className="muted">Серверная фильтрация и просмотр клиентских заявок.</p> <p className="muted">Серверная фильтрация и просмотр клиентских заявок.</p>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button className="btn secondary" type="button" onClick={onRefresh}>
Обновить
</button>
<button className="btn" type="button" onClick={onCreate}>
Новая заявка
</button>
</div>
</div> </div>
<FilterToolbar <FilterToolbar
filters={tableState.filters} filters={tableState.filters}
onOpen={onOpenFilter} onOpen={onOpenFilter}
onRemove={onRemoveFilter} onRemove={onRemoveFilter}
onEdit={onEditFilter} onEdit={onEditFilter}
hideAction
getChipLabel={(clause) => { getChipLabel={(clause) => {
const fieldDef = getFieldDef("requests", clause.field); const fieldDef = getFieldDef("requests", clause.field);
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("requests", clause); return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("requests", clause);
@ -195,7 +188,15 @@ export function RequestsSection({
</tr> </tr>
)} )}
/> />
<TablePager tableState={tableState} onPrev={onPrev} onNext={onNext} onLoadAll={onLoadAll} /> <TablePager
tableState={tableState}
onPrev={onPrev}
onNext={onNext}
onLoadAll={onLoadAll}
onRefresh={onRefresh}
onCreate={onCreate}
onOpenFilter={onOpenFilter}
/>
<StatusLine status={status || (typeof getStatus === "function" ? getStatus("requests") : null)} /> <StatusLine status={status || (typeof getStatus === "function" ? getStatus("requests") : null)} />
</> </>
); );

View file

@ -63,17 +63,13 @@ export function ServiceRequestsSection({
<h2>Запросы</h2> <h2>Запросы</h2>
<p className="muted">Запросы клиента к куратору и обращения на смену юриста.</p> <p className="muted">Запросы клиента к куратору и обращения на смену юриста.</p>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}>
<button className="btn secondary" type="button" onClick={onRefresh}>
Обновить
</button>
</div>
</div> </div>
<FilterToolbar <FilterToolbar
filters={tableState.filters} filters={tableState.filters}
onOpen={onOpenFilter} onOpen={onOpenFilter}
onRemove={onRemoveFilter} onRemove={onRemoveFilter}
onEdit={onEditFilter} onEdit={onEditFilter}
hideAction
getChipLabel={(clause) => { getChipLabel={(clause) => {
const fieldDef = getFieldDef("serviceRequests", clause.field); const fieldDef = getFieldDef("serviceRequests", clause.field);
return ( return (
@ -129,7 +125,14 @@ export function ServiceRequestsSection({
</tr> </tr>
)} )}
/> />
<TablePager tableState={tableState} onPrev={onPrev} onNext={onNext} onLoadAll={onLoadAll} /> <TablePager
tableState={tableState}
onPrev={onPrev}
onNext={onNext}
onLoadAll={onLoadAll}
onRefresh={onRefresh}
onOpenFilter={onOpenFilter}
/>
<StatusLine status={status || (typeof getStatus === "function" ? getStatus("serviceRequests") : null)} /> <StatusLine status={status || (typeof getStatus === "function" ? getStatus("serviceRequests") : null)} />
</> </>
); );

View file

@ -1,4 +1,5 @@
import { boolLabel, fmtDate } from "../../shared/utils.js"; import { boolLabel, fmtDate } from "../../shared/utils.js";
import { RefreshIcon } from "../../shared/icons.jsx";
export function AvailableTablesSection({ export function AvailableTablesSection({
tables, tables,
@ -21,8 +22,8 @@ export function AvailableTablesSection({
<h2>Доступность таблиц</h2> <h2>Доступность таблиц</h2>
<p className="muted">Скрытая служебная вкладка. Доступ только для администратора по прямой ссылке.</p> <p className="muted">Скрытая служебная вкладка. Доступ только для администратора по прямой ссылке.</p>
</div> </div>
<button className="btn secondary" type="button" onClick={onRefresh}> <button className="btn secondary table-control-btn" type="button" onClick={onRefresh} title="Обновить" aria-label="Обновить">
Обновить <RefreshIcon />
</button> </button>
</div> </div>
<DataTable <DataTable

View file

@ -0,0 +1,51 @@
export function RefreshIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
<polyline points="21 3 21 9 15 9" />
</svg>
);
}
export function FilterIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 5h18l-7 8v5l-4 2v-7z" />
</svg>
);
}
export function AddIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5v14" />
<path d="M5 12h14" />
</svg>
);
}
export function PrevIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M15 18l-6-6 6-6" />
</svg>
);
}
export function NextIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 18l6-6-6-6" />
</svg>
);
}
export function DownloadIcon() {
return (
<svg className="ui-glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 4v11" />
<path d="M8 11l4 4 4-4" />
<path d="M5 20h14" />
</svg>
);
}

16
app/web/brand-mark.svg Normal file
View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="law-gold" x1="14" y1="10" x2="50" y2="54" gradientUnits="userSpaceOnUse">
<stop stop-color="#E4C28D" />
<stop offset="1" stop-color="#C8924A" />
</linearGradient>
</defs>
<circle cx="32" cy="32" r="29" fill="#101A25" stroke="url(#law-gold)" stroke-width="2.5" />
<path d="M22 20h20" stroke="url(#law-gold)" stroke-width="2.8" stroke-linecap="round" />
<path d="M32 20v21" stroke="url(#law-gold)" stroke-width="2.8" stroke-linecap="round" />
<path d="M26 20l-5 10" stroke="url(#law-gold)" stroke-width="2.4" stroke-linecap="round" />
<path d="M38 20l5 10" stroke="url(#law-gold)" stroke-width="2.4" stroke-linecap="round" />
<path d="M17.5 33.5h7a4 4 0 0 1-7 0Z" fill="url(#law-gold)" />
<path d="M39.5 33.5h7a4 4 0 0 1-7 0Z" fill="url(#law-gold)" />
<path d="M25 45h14" stroke="url(#law-gold)" stroke-width="2.8" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 1,009 B

View file

@ -12,6 +12,21 @@
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
} }
.client-title-row {
display: inline-flex;
align-items: center;
gap: 0.52rem;
}
.client-title-row .brand-mark {
width: 24px;
height: 24px;
}
.client-title-row h1 {
margin: 0;
}
.client-section { .client-section {
display: block; display: block;
} }

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Страница клиента • Правовой трекер</title> <title>Страница клиента • Правовой трекер</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<link rel="stylesheet" href="/admin.css?v=20260227-01"> <link rel="stylesheet" href="/admin.css?v=20260227-01">
<link rel="stylesheet" href="/client.css"> <link rel="stylesheet" href="/client.css">
</head> </head>

View file

@ -770,7 +770,10 @@ import { detectAttachmentPreviewKind, fmtShortDateTime } from "./admin/shared/ut
<main className="main client-main"> <main className="main client-main">
<div className="topbar client-topbar"> <div className="topbar client-topbar">
<div> <div>
<h1>Кабинет клиента</h1> <div className="client-title-row">
<img className="brand-mark" src="/brand-mark.svg" alt="" width="24" height="24" />
<h1>Кабинет клиента</h1>
</div>
<p className="muted">Работа с заявками: статусы, чат, файлы и обращения.</p> <p className="muted">Работа с заявками: статусы, чат, файлы и обращения.</p>
</div> </div>
<button <button

16
app/web/favicon.svg Normal file
View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="law-gold-favicon" x1="14" y1="10" x2="50" y2="54" gradientUnits="userSpaceOnUse">
<stop stop-color="#E4C28D" />
<stop offset="1" stop-color="#C8924A" />
</linearGradient>
</defs>
<circle cx="32" cy="32" r="29" fill="#101A25" stroke="url(#law-gold-favicon)" stroke-width="2.5" />
<path d="M22 20h20" stroke="url(#law-gold-favicon)" stroke-width="2.8" stroke-linecap="round" />
<path d="M32 20v21" stroke="url(#law-gold-favicon)" stroke-width="2.8" stroke-linecap="round" />
<path d="M26 20l-5 10" stroke="url(#law-gold-favicon)" stroke-width="2.4" stroke-linecap="round" />
<path d="M38 20l5 10" stroke="url(#law-gold-favicon)" stroke-width="2.4" stroke-linecap="round" />
<path d="M17.5 33.5h7a4 4 0 0 1-7 0Z" fill="url(#law-gold-favicon)" />
<path d="M39.5 33.5h7a4 4 0 0 1-7 0Z" fill="url(#law-gold-favicon)" />
<path d="M25 45h14" stroke="url(#law-gold-favicon)" stroke-width="2.8" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -65,6 +65,9 @@
} }
.brand { .brand {
display: inline-flex;
align-items: center;
gap: 0.48rem;
font-size: 0.84rem; font-size: 0.84rem;
letter-spacing: 0.12em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
@ -73,6 +76,13 @@
color: #eef4ff; color: #eef4ff;
} }
.brand .brand-mark {
width: 28px;
height: 28px;
flex-shrink: 0;
border-radius: 50%;
}
.nav { .nav {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -5,12 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Аудиторы корпоративной безопасности</title> <title>Аудиторы корпоративной безопасности</title>
<meta name="description" content="Юридический консалтинг и судебное сопровождение для сложных бизнес-ситуаций."> <meta name="description" content="Юридический консалтинг и судебное сопровождение для сложных бизнес-ситуаций.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<link rel="stylesheet" href="/landing.css"> <link rel="stylesheet" href="/landing.css">
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
<div class="wrap topbar-inner"> <div class="wrap topbar-inner">
<div class="brand">Аудиторы корпоративной безопасности</div> <div class="brand">
<img class="brand-mark" src="/brand-mark.svg" alt="" width="28" height="28">
<span>Аудиторы корпоративной безопасности</span>
</div>
<nav class="nav"> <nav class="nav">
<a href="#practices">Компетенции</a> <a href="#practices">Компетенции</a>
<a href="#approach">Подход</a> <a href="#approach">Подход</a>

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Политика обработки персональных данных</title> <title>Политика обработки персональных данных</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
<style> <style>
body { body {
margin: 0; margin: 0;