mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 10:03:45 +03:00
add icons
This commit is contained in:
parent
a9704d77b0
commit
e2dbf530bd
19 changed files with 291 additions and 107 deletions
|
|
@ -53,6 +53,17 @@
|
|||
.logo a {
|
||||
color: inherit;
|
||||
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 {
|
||||
|
|
@ -275,6 +286,13 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.section-head-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-head-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -282,16 +300,16 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-icon-only {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
font-size: 1.06rem;
|
||||
line-height: 1;
|
||||
.ui-glyph {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: block;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.9;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.muted {
|
||||
|
|
@ -1205,6 +1223,43 @@
|
|||
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 {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
|
@ -1273,31 +1328,6 @@
|
|||
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 {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Административная панель • Правовой трекер</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
|
||||
<link rel="stylesheet" href="/admin.css?v=20260225-12">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import {
|
|||
translateApiError,
|
||||
userInitials,
|
||||
} from "./admin/shared/utils.js";
|
||||
import { AddIcon, DownloadIcon, FilterIcon, NextIcon, PrevIcon, RefreshIcon } from "./admin/shared/icons.jsx";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
(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 (
|
||||
<div className="pager table-footer-bar">
|
||||
<div>
|
||||
|
|
@ -128,25 +129,45 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
|||
? "Всего: " + tableState.total + " • показаны все записи"
|
||||
: "Всего: " + tableState.total + " • смещение: " + tableState.offset}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<div className="table-footer-actions">
|
||||
<button
|
||||
className="btn secondary"
|
||||
className="btn secondary table-control-btn table-control-loadall"
|
||||
type="button"
|
||||
onClick={onLoadAll}
|
||||
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 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
|
||||
className="btn secondary"
|
||||
className="btn secondary table-control-btn"
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
disabled={tableState.showAll || tableState.offset + PAGE_SIZE >= tableState.total}
|
||||
title="Вперед"
|
||||
aria-label="Вперед"
|
||||
>
|
||||
Вперед
|
||||
<NextIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -192,8 +213,8 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
|||
</div>
|
||||
{!hideAction ? (
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -3448,7 +3469,10 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
|||
<div className="layout">
|
||||
<aside className="sidebar">
|
||||
<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>
|
||||
<nav className="menu">
|
||||
{menuItems.map((item) => (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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";
|
||||
|
||||
function fmtBalance(value) {
|
||||
|
|
@ -67,7 +68,6 @@ export function ConfigSection(props) {
|
|||
const StatusLine = StatusLineComponent;
|
||||
const IconButton = IconButtonComponent;
|
||||
const UserAvatar = UserAvatarComponent;
|
||||
const canOpenFilter = Boolean(configActiveKey);
|
||||
const canRefresh = Boolean(configActiveKey);
|
||||
const canCreateRecord = Boolean(canCreateInConfig && configActiveKey);
|
||||
const canLoadAllRows = Boolean(
|
||||
|
|
@ -102,16 +102,6 @@ export function ConfigSection(props) {
|
|||
Баланс
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
className="btn secondary btn-icon-only"
|
||||
type="button"
|
||||
onClick={() => openFilterModal(configActiveKey)}
|
||||
disabled={!canOpenFilter}
|
||||
title="Фильтр"
|
||||
aria-label="Фильтр"
|
||||
>
|
||||
⚲
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="config-layout">
|
||||
|
|
@ -606,55 +596,65 @@ export function ConfigSection(props) {
|
|||
</div>
|
||||
<div className="config-controls-actions">
|
||||
<button
|
||||
className="btn secondary config-control-btn config-control-btn-loadall"
|
||||
className="btn secondary table-control-btn table-control-loadall"
|
||||
type="button"
|
||||
onClick={() => loadAllRows(configActiveKey)}
|
||||
disabled={!canLoadAllRows}
|
||||
title={"Загрузить все " + activeConfigTableState.total}
|
||||
aria-label={"Загрузить все " + activeConfigTableState.total}
|
||||
>
|
||||
<span aria-hidden="true">⤓</span>
|
||||
<DownloadIcon />
|
||||
<span>{activeConfigTableState.total}</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn secondary btn-icon-only config-control-btn"
|
||||
className="btn secondary table-control-btn"
|
||||
type="button"
|
||||
onClick={() => loadCurrentConfigTable(true)}
|
||||
disabled={!canRefresh}
|
||||
title="Обновить"
|
||||
aria-label="Обновить"
|
||||
>
|
||||
↻
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
<button
|
||||
className="btn secondary btn-icon-only config-control-btn"
|
||||
className="btn secondary table-control-btn"
|
||||
type="button"
|
||||
onClick={() => openCreateRecordModal(configActiveKey)}
|
||||
disabled={!canCreateRecord}
|
||||
title="Добавить"
|
||||
aria-label="Добавить"
|
||||
>
|
||||
+
|
||||
<AddIcon />
|
||||
</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"
|
||||
onClick={() => loadPrevPage(configActiveKey)}
|
||||
disabled={!canLoadPrev}
|
||||
title="Назад"
|
||||
aria-label="Назад"
|
||||
>
|
||||
←
|
||||
<PrevIcon />
|
||||
</button>
|
||||
<button
|
||||
className="btn secondary btn-icon-only config-control-btn"
|
||||
className="btn secondary table-control-btn"
|
||||
type="button"
|
||||
onClick={() => loadNextPage(configActiveKey)}
|
||||
disabled={!canLoadNext}
|
||||
title="Вперед"
|
||||
aria-label="Вперед"
|
||||
>
|
||||
→
|
||||
<NextIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,20 +40,13 @@ export function InvoicesSection({
|
|||
<h2>Счета</h2>
|
||||
<p className="muted">Выставленные счета клиентам, статусы оплаты и выгрузка PDF.</p>
|
||||
</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>
|
||||
<FilterToolbar
|
||||
filters={tableState.filters}
|
||||
onOpen={onOpenFilter}
|
||||
onRemove={onRemoveFilter}
|
||||
onEdit={onEditFilter}
|
||||
hideAction
|
||||
getChipLabel={(clause) => {
|
||||
const fieldDef = getFieldDef("invoices", clause.field);
|
||||
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("invoices", clause);
|
||||
|
|
@ -112,7 +105,15 @@ export function InvoicesSection({
|
|||
</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} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { KANBAN_GROUPS } from "../../shared/constants.js";
|
||||
import { FilterIcon, RefreshIcon } from "../../shared/icons.jsx";
|
||||
import { fallbackStatusGroup, fmtKanbanDate, resolveDeadlineTone, statusLabel } from "../../shared/utils.js";
|
||||
|
||||
export function KanbanBoard({
|
||||
|
|
@ -70,12 +71,15 @@ export function KanbanBoard({
|
|||
<h2>Канбан заявок</h2>
|
||||
<p className="muted">Группировка по группам статусов и серверная фильтрация карточек.</p>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,6 +89,7 @@ export function KanbanBoard({
|
|||
onOpen={onOpenFilter}
|
||||
onRemove={onRemoveFilter}
|
||||
onEdit={onEditFilter}
|
||||
hideAction
|
||||
getChipLabel={getFilterChipLabel}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -37,20 +37,13 @@ export function QuotesSection({
|
|||
<h2>Цитаты</h2>
|
||||
<p className="muted">Управление публичной лентой цитат с серверными фильтрами.</p>
|
||||
</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>
|
||||
<FilterToolbar
|
||||
filters={tableState.filters}
|
||||
onOpen={onOpenFilter}
|
||||
onRemove={onRemoveFilter}
|
||||
onEdit={onEditFilter}
|
||||
hideAction
|
||||
getChipLabel={(clause) => {
|
||||
const fieldDef = getFieldDef("quotes", clause.field);
|
||||
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("quotes", clause);
|
||||
|
|
@ -87,7 +80,15 @@ export function QuotesSection({
|
|||
</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} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -122,20 +122,13 @@ export function RequestsSection({
|
|||
<h2>Заявки</h2>
|
||||
<p className="muted">Серверная фильтрация и просмотр клиентских заявок.</p>
|
||||
</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>
|
||||
<FilterToolbar
|
||||
filters={tableState.filters}
|
||||
onOpen={onOpenFilter}
|
||||
onRemove={onRemoveFilter}
|
||||
onEdit={onEditFilter}
|
||||
hideAction
|
||||
getChipLabel={(clause) => {
|
||||
const fieldDef = getFieldDef("requests", clause.field);
|
||||
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("requests", clause);
|
||||
|
|
@ -195,7 +188,15 @@ export function RequestsSection({
|
|||
</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)} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -63,17 +63,13 @@ export function ServiceRequestsSection({
|
|||
<h2>Запросы</h2>
|
||||
<p className="muted">Запросы клиента к куратору и обращения на смену юриста.</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<button className="btn secondary" type="button" onClick={onRefresh}>
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<FilterToolbar
|
||||
filters={tableState.filters}
|
||||
onOpen={onOpenFilter}
|
||||
onRemove={onRemoveFilter}
|
||||
onEdit={onEditFilter}
|
||||
hideAction
|
||||
getChipLabel={(clause) => {
|
||||
const fieldDef = getFieldDef("serviceRequests", clause.field);
|
||||
return (
|
||||
|
|
@ -129,7 +125,14 @@ export function ServiceRequestsSection({
|
|||
</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)} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { boolLabel, fmtDate } from "../../shared/utils.js";
|
||||
import { RefreshIcon } from "../../shared/icons.jsx";
|
||||
|
||||
export function AvailableTablesSection({
|
||||
tables,
|
||||
|
|
@ -21,8 +22,8 @@ export function AvailableTablesSection({
|
|||
<h2>Доступность таблиц</h2>
|
||||
<p className="muted">Скрытая служебная вкладка. Доступ только для администратора по прямой ссылке.</p>
|
||||
</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>
|
||||
</div>
|
||||
<DataTable
|
||||
|
|
|
|||
51
app/web/admin/shared/icons.jsx
Normal file
51
app/web/admin/shared/icons.jsx
Normal 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
16
app/web/brand-mark.svg
Normal 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 |
|
|
@ -12,6 +12,21 @@
|
|||
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 {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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="/client.css">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -770,7 +770,10 @@ import { detectAttachmentPreviewKind, fmtShortDateTime } from "./admin/shared/ut
|
|||
<main className="main client-main">
|
||||
<div className="topbar client-topbar">
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
16
app/web/favicon.svg
Normal file
16
app/web/favicon.svg
Normal 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 |
|
|
@ -65,6 +65,9 @@
|
|||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.48rem;
|
||||
font-size: 0.84rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
|
|
@ -73,6 +76,13 @@
|
|||
color: #eef4ff;
|
||||
}
|
||||
|
||||
.brand .brand-mark {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Аудиторы корпоративной безопасности</title>
|
||||
<meta name="description" content="Юридический консалтинг и судебное сопровождение для сложных бизнес-ситуаций.">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
|
||||
<link rel="stylesheet" href="/landing.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<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">
|
||||
<a href="#practices">Компетенции</a>
|
||||
<a href="#approach">Подход</a>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Политика обработки персональных данных</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260302-01">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue