feat(projects): explicit organization (executor) selector
Backend:
- POST /api/projects accepts optional organizationId; default = active org
- GET /api/projects (list) and GET /:id include organization {id,name,shortName}
- /:id and /:id/documents no longer filter by active org — direct link works
across organizations; UI offers to switch active to project's org
Frontend:
- Project create modal: «Фирма (исполнитель)» dropdown, default = active
- Projects list shows «Фирма» column when user has >1 organization
- ProjectEdit shows org-banner with project's organization;
if active org differs, banner has «переключить активную на эту →» button
- ProjectEdit fetches BankAccounts from project's org (not active)
- Bumped clients fetch limit to 1000 to match API max
Org of an existing project cannot be changed (would orphan documents/lines);
to switch fenced — create new project under desired org.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,11 @@ const ProjectUpsert = z.object({
|
|||||||
notes: optionalText(2000),
|
notes: optionalText(2000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// При создании можно явно указать компанию-исполнителя; если не задана — берётся активная.
|
||||||
|
const ProjectCreate = ProjectUpsert.extend({
|
||||||
|
organizationId: z.string().uuid().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
const ListQuery = z.object({
|
const ListQuery = z.object({
|
||||||
status: z.enum(STATUSES).optional(),
|
status: z.enum(STATUSES).optional(),
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
@@ -23,7 +28,7 @@ const ListQuery = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export async function projectsRoutes(app: FastifyInstance) {
|
export async function projectsRoutes(app: FastifyInstance) {
|
||||||
// ---- list ----
|
// ---- list (по активной компании) ----
|
||||||
app.get('/api/projects', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
app.get('/api/projects', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
const orgId = getOrganizationId(req);
|
||||||
const parsed = ListQuery.safeParse(req.query);
|
const parsed = ListQuery.safeParse(req.query);
|
||||||
@@ -40,6 +45,7 @@ export async function projectsRoutes(app: FastifyInstance) {
|
|||||||
...(q ? { name: { contains: q, mode: 'insensitive' as const } } : {}),
|
...(q ? { name: { contains: q, mode: 'insensitive' as const } } : {}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
organization: { select: { id: true, name: true, shortName: true } },
|
||||||
defaultClient: { select: { id: true, name: true, kind: true } },
|
defaultClient: { select: { id: true, name: true, kind: true } },
|
||||||
defaultTemplate: { select: { id: true, name: true, docType: true } },
|
defaultTemplate: { select: { id: true, name: true, docType: true } },
|
||||||
defaultBankAccount: { select: { id: true, name: true } },
|
defaultBankAccount: { select: { id: true, name: true } },
|
||||||
@@ -52,12 +58,14 @@ export async function projectsRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---- get one ----
|
// ---- get one ----
|
||||||
|
// Не фильтруем по active org — позволяем открыть проект другой компании по прямой ссылке;
|
||||||
|
// фронт может предложить переключить активную, чтобы увидеть его в списке.
|
||||||
app.get('/api/projects/:id', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
app.get('/api/projects/:id', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const project = await prisma.project.findFirst({
|
const project = await prisma.project.findFirst({
|
||||||
where: { id, organizationId: orgId },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
|
organization: { select: { id: true, name: true, shortName: true } },
|
||||||
defaultClient: true,
|
defaultClient: true,
|
||||||
defaultTemplate: true,
|
defaultTemplate: true,
|
||||||
defaultBankAccount: true,
|
defaultBankAccount: true,
|
||||||
@@ -72,15 +80,14 @@ export async function projectsRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// ---- documents under project ----
|
// ---- documents under project ----
|
||||||
app.get('/api/projects/:id/documents', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
app.get('/api/projects/:id/documents', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const project = await prisma.project.findFirst({ where: { id, organizationId: orgId } });
|
const project = await prisma.project.findFirst({ where: { id } });
|
||||||
if (!project) {
|
if (!project) {
|
||||||
reply.code(404).send({ error: 'not_found' });
|
reply.code(404).send({ error: 'not_found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const items = await prisma.document.findMany({
|
const items = await prisma.document.findMany({
|
||||||
where: { projectId: id, organizationId: orgId },
|
where: { projectId: id, organizationId: project.organizationId },
|
||||||
include: { client: { select: { id: true, name: true, kind: true } } },
|
include: { client: { select: { id: true, name: true, kind: true } } },
|
||||||
orderBy: [{ issuedAt: { sort: 'desc', nulls: 'last' } }, { createdAt: 'desc' }],
|
orderBy: [{ issuedAt: { sort: 'desc', nulls: 'last' } }, { createdAt: 'desc' }],
|
||||||
});
|
});
|
||||||
@@ -89,28 +96,35 @@ export async function projectsRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// ---- create ----
|
// ---- create ----
|
||||||
app.post('/api/projects', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
app.post('/api/projects', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
const activeOrgId = getOrganizationId(req);
|
||||||
const parsed = ProjectUpsert.safeParse(req.body);
|
const parsed = ProjectCreate.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() });
|
reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { organizationId: requestedOrgId, ...rest } = parsed.data;
|
||||||
|
const orgId = requestedOrgId ?? activeOrgId;
|
||||||
|
const org = await prisma.organization.findFirst({ where: { id: orgId, archivedAt: null } });
|
||||||
|
if (!org) {
|
||||||
|
reply.code(400).send({ error: 'invalid_organization' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const created = await prisma.project.create({
|
const created = await prisma.project.create({
|
||||||
data: { ...parsed.data, organizationId: orgId },
|
data: { ...rest, organizationId: orgId },
|
||||||
});
|
});
|
||||||
reply.code(201).send(created);
|
reply.code(201).send(created);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- update ----
|
// ---- update ----
|
||||||
|
// organizationId менять не даём — иначе придётся переносить связанные документы и счета.
|
||||||
app.put('/api/projects/:id', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
app.put('/api/projects/:id', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const parsed = ProjectUpsert.safeParse(req.body);
|
const parsed = ProjectUpsert.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() });
|
reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existing = await prisma.project.findFirst({ where: { id, organizationId: orgId } });
|
const existing = await prisma.project.findFirst({ where: { id } });
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
reply.code(404).send({ error: 'not_found' });
|
reply.code(404).send({ error: 'not_found' });
|
||||||
return;
|
return;
|
||||||
@@ -120,9 +134,8 @@ export async function projectsRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// ---- archive ----
|
// ---- archive ----
|
||||||
app.delete('/api/projects/:id', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
app.delete('/api/projects/:id', { preHandler: app.requireDocPermission('user') }, async (req, reply) => {
|
||||||
const orgId = getOrganizationId(req);
|
|
||||||
const { id } = req.params as { id: string };
|
const { id } = req.params as { id: string };
|
||||||
const existing = await prisma.project.findFirst({ where: { id, organizationId: orgId } });
|
const existing = await prisma.project.findFirst({ where: { id } });
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
reply.code(404).send({ error: 'not_found' });
|
reply.code(404).send({ error: 'not_found' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export type ProjectStatus = 'active' | 'completed' | 'cancelled';
|
|||||||
export type ProjectSummary = {
|
export type ProjectSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
organization: { id: string; name: string; shortName: string | null } | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
defaultClientId: string | null;
|
defaultClientId: string | null;
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ export function ProjectEditPage() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [savedAt, setSavedAt] = useState<Date | null>(null);
|
const [savedAt, setSavedAt] = useState<Date | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [orgId, setOrgId] = useState<string | null>(null);
|
const [activeOrgId, setActiveOrgId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
api.get<Project>(`/api/projects/${id}`),
|
api.get<Project>(`/api/projects/${id}`),
|
||||||
api.get<{ items: DocumentSummary[] }>(`/api/projects/${id}/documents`),
|
api.get<{ items: DocumentSummary[] }>(`/api/projects/${id}/documents`),
|
||||||
api.get<{ items: Client[] }>('/api/clients?limit=500'),
|
api.get<{ items: Client[] }>('/api/clients?limit=1000'),
|
||||||
api.get<{ items: DocumentTemplate[] }>('/api/templates'),
|
api.get<{ items: DocumentTemplate[] }>('/api/templates'),
|
||||||
api.get<{ id: string }>('/api/active-organization'),
|
api.get<{ id: string }>('/api/active-organization'),
|
||||||
])
|
])
|
||||||
@@ -54,13 +54,26 @@ export function ProjectEditPage() {
|
|||||||
setDocs(ds.items);
|
setDocs(ds.items);
|
||||||
setClients(cs.items);
|
setClients(cs.items);
|
||||||
setTemplates(ts.items);
|
setTemplates(ts.items);
|
||||||
setOrgId(ao.id);
|
setActiveOrgId(ao.id);
|
||||||
const ba = await api.get<{ items: BankAccount[] }>(`/api/organizations/${ao.id}/bank-accounts`);
|
// Банк-счета и шаблоны проекта берём из ФИРМЫ ПРОЕКТА, а не из активной.
|
||||||
|
const ba = await api.get<{ items: BankAccount[] }>(
|
||||||
|
`/api/organizations/${p.organizationId}/bank-accounts`,
|
||||||
|
);
|
||||||
setBankAccounts(ba.items);
|
setBankAccounts(ba.items);
|
||||||
})
|
})
|
||||||
.catch((e) => setError(String(e)));
|
.catch((e) => setError(String(e)));
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
async function switchToProjectOrg() {
|
||||||
|
if (!project) return;
|
||||||
|
try {
|
||||||
|
await api.post('/api/active-organization', { id: project.organizationId });
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!id || !draft) return;
|
if (!id || !draft) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -94,6 +107,8 @@ export function ProjectEditPage() {
|
|||||||
if (error && !project) return <main className="content"><div className="error-text">{error}</div></main>;
|
if (error && !project) return <main className="content"><div className="error-text">{error}</div></main>;
|
||||||
if (!project) return <main className="content"><p className="hint">Загрузка…</p></main>;
|
if (!project) return <main className="content"><p className="hint">Загрузка…</p></main>;
|
||||||
|
|
||||||
|
const isCrossOrg = activeOrgId && project.organizationId && activeOrgId !== project.organizationId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="content">
|
<main className="content">
|
||||||
<header className="page-head">
|
<header className="page-head">
|
||||||
@@ -101,6 +116,16 @@ export function ProjectEditPage() {
|
|||||||
<Button onClick={() => navigate('/projects')}>← К проектам</Button>
|
<Button onClick={() => navigate('/projects')}>← К проектам</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div className="org-banner">
|
||||||
|
<span className="hint">Фирма-исполнитель:</span>
|
||||||
|
<b>{project.organization?.shortName || project.organization?.name || '—'}</b>
|
||||||
|
{isCrossOrg ? (
|
||||||
|
<Button variant="ghost" onClick={switchToProjectOrg}>
|
||||||
|
переключить активную на эту →
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="form-grid">
|
<section className="form-grid">
|
||||||
<Field
|
<Field
|
||||||
label="Название"
|
label="Название"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { api, ApiError, type ProjectSummary, type ProjectStatus } from '../api.js';
|
import { api, ApiError, type Organization, type ProjectStatus, type ProjectSummary } from '../api.js';
|
||||||
import { Button, EmptyState, Field, Modal, Select } from '../components/ui.js';
|
import { Button, EmptyState, Field, Modal, Select } from '../components/ui.js';
|
||||||
|
|
||||||
const STATUS_LABEL: Record<ProjectStatus, string> = {
|
const STATUS_LABEL: Record<ProjectStatus, string> = {
|
||||||
@@ -11,9 +11,11 @@ const STATUS_LABEL: Record<ProjectStatus, string> = {
|
|||||||
|
|
||||||
export function ProjectsPage() {
|
export function ProjectsPage() {
|
||||||
const [items, setItems] = useState<ProjectSummary[] | null>(null);
|
const [items, setItems] = useState<ProjectSummary[] | null>(null);
|
||||||
|
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||||
|
const [activeOrgId, setActiveOrgId] = useState<string | null>(null);
|
||||||
const [status, setStatus] = useState<ProjectStatus | ''>('');
|
const [status, setStatus] = useState<ProjectStatus | ''>('');
|
||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
const [creating, setCreating] = useState<{ name: string } | null>(null);
|
const [creating, setCreating] = useState<{ name: string; organizationId: string } | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -33,6 +35,18 @@ export function ProjectsPage() {
|
|||||||
|
|
||||||
useEffect(() => { void load(); /* eslint-disable-next-line */ }, [status, q]);
|
useEffect(() => { void load(); /* eslint-disable-next-line */ }, [status, q]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void Promise.all([
|
||||||
|
api.get<{ items: Organization[] }>('/api/organizations'),
|
||||||
|
api.get<{ id: string }>('/api/active-organization'),
|
||||||
|
])
|
||||||
|
.then(([list, active]) => {
|
||||||
|
setOrgs(list.items);
|
||||||
|
setActiveOrgId(active.id);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
if (!creating) return;
|
if (!creating) return;
|
||||||
if (!creating.name.trim()) {
|
if (!creating.name.trim()) {
|
||||||
@@ -43,6 +57,7 @@ export function ProjectsPage() {
|
|||||||
try {
|
try {
|
||||||
const p = await api.post<ProjectSummary>('/api/projects', {
|
const p = await api.post<ProjectSummary>('/api/projects', {
|
||||||
name: creating.name.trim(),
|
name: creating.name.trim(),
|
||||||
|
organizationId: creating.organizationId,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
defaultClientId: null,
|
defaultClientId: null,
|
||||||
defaultTemplateId: null,
|
defaultTemplateId: null,
|
||||||
@@ -72,11 +87,18 @@ export function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showOrgColumn = orgs.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="content">
|
<main className="content">
|
||||||
<header className="page-head">
|
<header className="page-head">
|
||||||
<h2>Проекты</h2>
|
<h2>Проекты</h2>
|
||||||
<Button variant="primary" onClick={() => setCreating({ name: '' })}>+ Новый проект</Button>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => setCreating({ name: '', organizationId: activeOrgId ?? '' })}
|
||||||
|
>
|
||||||
|
+ Новый проект
|
||||||
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
@@ -105,13 +127,14 @@ export function ProjectsPage() {
|
|||||||
<p className="hint">Загрузка…</p>
|
<p className="hint">Загрузка…</p>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
Проектов пока нет. Создайте первый — внутри проекта зададите дефолтного клиента, шаблон и банк-счёт, чтобы создавать документы быстро.
|
Проектов пока нет в активной фирме. Создай первый — внутри зададишь клиента по умолчанию, шаблон, банк-счёт.
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
) : (
|
) : (
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Название</th>
|
<th>Название</th>
|
||||||
|
{showOrgColumn ? <th>Фирма</th> : null}
|
||||||
<th>Клиент по умолчанию</th>
|
<th>Клиент по умолчанию</th>
|
||||||
<th>Шаблон</th>
|
<th>Шаблон</th>
|
||||||
<th>Документов</th>
|
<th>Документов</th>
|
||||||
@@ -123,6 +146,7 @@ export function ProjectsPage() {
|
|||||||
{items.map((p) => (
|
{items.map((p) => (
|
||||||
<tr key={p.id}>
|
<tr key={p.id}>
|
||||||
<td><Link to={`/projects/${p.id}`}>{p.name}</Link></td>
|
<td><Link to={`/projects/${p.id}`}>{p.name}</Link></td>
|
||||||
|
{showOrgColumn ? <td className="hint">{p.organization?.shortName || p.organization?.name || '—'}</td> : null}
|
||||||
<td>{p.defaultClient?.name ?? '—'}</td>
|
<td>{p.defaultClient?.name ?? '—'}</td>
|
||||||
<td>{p.defaultTemplate?.name ?? '—'}</td>
|
<td>{p.defaultTemplate?.name ?? '—'}</td>
|
||||||
<td>{p._count?.documents ?? 0}</td>
|
<td>{p._count?.documents ?? 0}</td>
|
||||||
@@ -153,16 +177,35 @@ export function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{creating ? (
|
{creating ? (
|
||||||
|
<div className="form-grid">
|
||||||
<Field
|
<Field
|
||||||
label="Название"
|
label="Название"
|
||||||
value={creating.name}
|
value={creating.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCreating({ name: e.target.value });
|
setCreating({ ...creating, name: e.target.value });
|
||||||
if (fieldErrors.name) setFieldErrors((fe) => { const n = { ...fe }; delete n.name; return n; });
|
if (fieldErrors.name) setFieldErrors((fe) => { const n = { ...fe }; delete n.name; return n; });
|
||||||
}}
|
}}
|
||||||
placeholder="напр. Свадьба Ивановых, июнь 2026"
|
placeholder="напр. Свадьба Ивановых, июнь 2026"
|
||||||
error={fieldErrors.name}
|
error={fieldErrors.name}
|
||||||
/>
|
/>
|
||||||
|
<label className="field">
|
||||||
|
<span className="field__label">Фирма (исполнитель)</span>
|
||||||
|
<select
|
||||||
|
className="field__input"
|
||||||
|
value={creating.organizationId}
|
||||||
|
onChange={(e) => setCreating({ ...creating, organizationId: e.target.value })}
|
||||||
|
>
|
||||||
|
{orgs.map((o) => (
|
||||||
|
<option key={o.id} value={o.id}>
|
||||||
|
{o.shortName || o.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p className="hint" style={{ gridColumn: '1 / -1' }}>
|
||||||
|
Документы внутри проекта будут выставляться от этой компании. После создания фирму поменять нельзя — заведи новый проект, если нужно из другой фирмы.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -302,6 +302,19 @@ body {
|
|||||||
.import-banner { background: #14213d; border-color: #1e3a8a; color: #93c5fd; }
|
.import-banner { background: #14213d; border-color: #1e3a8a; color: #93c5fd; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === org banner (project page) === */
|
||||||
|
.org-banner {
|
||||||
|
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||||
|
padding: 8px 12px; margin-bottom: 16px;
|
||||||
|
background: #f1f5f9; border: 1px solid #cbd5e1; border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.org-banner b { color: #1e3a8a; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.org-banner { background: #1c1f24; border-color: #2a2e35; }
|
||||||
|
.org-banner b { color: #93c5fd; }
|
||||||
|
}
|
||||||
|
|
||||||
/* === inn lookup === */
|
/* === inn lookup === */
|
||||||
.inn-lookup { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
|
.inn-lookup { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
|
||||||
.inn-lookup__error { font-size: 12px; color: #c0392b; }
|
.inn-lookup__error { font-size: 12px; color: #c0392b; }
|
||||||
|
|||||||
Reference in New Issue
Block a user