feat: CalenTIQ Admin branding (wordmark, favicon, titles). Refs EventHub/EventHubSpec#10
CI / test (push) Successful in 47s
CI / push-image (push) Successful in 1m46s
CI / deploy-ift (push) Successful in 7s
CI / ci-done (push) Successful in 0s
CI / e2e-ift (push) Successful in 20s
CI / deploy-stage (push) Successful in 1m29s
CI / e2e-stage (push) Successful in 23s

This commit is contained in:
2026-07-26 22:25:25 +03:00
parent 6d647dd037
commit 8f931db0f0
10 changed files with 133 additions and 9 deletions
+2 -1
View File
@@ -3,8 +3,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EventHub Admin</title> <title>CalenTIQ Admin</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#000000" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#000000"/>
<circle cx="816.06" cy="207.94" r="18" fill="#000000"/>
<circle cx="884.39" cy="297.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="400.71" r="18" fill="#000000"/>
<circle cx="942.00" cy="512.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="623.29" r="18" fill="#000000"/>
<circle cx="884.39" cy="727.00" r="18" fill="#000000"/>
<circle cx="816.06" cy="816.06" r="18" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

+15 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 765 B

+75
View File
@@ -0,0 +1,75 @@
import React from 'react';
import { cn } from '@/lib/utils';
type Size = 'sm' | 'md' | 'lg';
type Props = {
size?: Size;
className?: string;
/** Show Time Arc mark next to wordmark (default true). */
withMark?: boolean;
/** Append muted "Admin" after the brand (sidebar / login). */
withAdmin?: boolean;
};
const sizeMap: Record<Size, { mark: string; text: string; admin: string }> = {
sm: { mark: 'h-5 w-5', text: 'text-sm', admin: 'text-[11px]' },
md: { mark: 'h-7 w-7', text: 'text-2xl', admin: 'text-sm' },
lg: { mark: 'h-10 w-10', text: 'text-4xl', admin: 'text-base' },
};
/**
* Public brand lockup: Time Arc + CalenTIQ (Admin).
* Uses currentColor so the mark follows theme foreground.
*/
const BrandWordmark: React.FC<Props> = ({
size = 'md',
className,
withMark = true,
withAdmin = false,
}) => {
const s = sizeMap[size];
return (
<div
className={cn('inline-flex items-center gap-2 text-foreground', className)}
data-testid="brand-wordmark"
aria-label={withAdmin ? 'CalenTIQ Admin' : 'CalenTIQ'}
>
{withMark && (
<svg
className={cn('shrink-0', s.mark)}
viewBox="0 0 1024 1024"
fill="none"
aria-hidden
>
<g
stroke="currentColor"
strokeWidth="48"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M512 132 A380 380 0 1 0 512 892" />
<path d="M512 512 L377.77 434.50" />
<path d="M512 512 L698.20 404.50" />
</g>
<circle cx="512" cy="512" r="24" fill="currentColor" />
<circle cx="816.06" cy="207.94" r="18" fill="currentColor" />
<circle cx="884.39" cy="297.00" r="18" fill="currentColor" />
<circle cx="927.35" cy="400.71" r="18" fill="currentColor" />
<circle cx="942.00" cy="512.00" r="18" fill="currentColor" />
<circle cx="927.35" cy="623.29" r="18" fill="currentColor" />
<circle cx="884.39" cy="727.00" r="18" fill="currentColor" />
<circle cx="816.06" cy="816.06" r="18" fill="currentColor" />
</svg>
)}
<p className={cn('leading-none font-extrabold tracking-tight', s.text)}>
Calen<span className="text-primary">TIQ</span>
{withAdmin && (
<span className={cn('ml-1.5 font-semibold opacity-70', s.admin)}>Admin</span>
)}
</p>
</div>
);
};
export default BrandWordmark;
+7 -4
View File
@@ -38,6 +38,7 @@ import {
} from '@/config/adminAccess'; } from '@/config/adminAccess';
import { notify } from '@/lib/notify'; import { notify } from '@/lib/notify';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import BrandWordmark from '@/components/BrandWordmark';
import { testid } from '@/lib/testid'; import { testid } from '@/lib/testid';
import { getTableDensity, toggleTableDensity, type TableDensity } from '@/lib/density'; import { getTableDensity, toggleTableDensity, type TableDensity } from '@/lib/density';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -251,11 +252,13 @@ const ControlCenterLayout: React.FC = () => {
collapsed ? 'w-16' : 'w-60' collapsed ? 'w-16' : 'w-60'
)} )}
> >
<div className="flex h-14 items-center justify-between px-3 border-b border-white/10"> <div className="flex h-14 items-center justify-between gap-1 px-3 border-b border-white/10">
{!collapsed && ( {!collapsed && (
<span className="font-semibold text-sm truncate"> <BrandWordmark
{import.meta.env.VITE_APP_TITLE || t('common.appTitle')} size="sm"
</span> withAdmin
className="min-w-0 truncate text-sidebar-foreground [&_span.text-primary]:text-sidebar-foreground"
/>
)} )}
<Button <Button
variant="ghost" variant="ghost"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"common": { "common": {
"appTitle": "EventHub", "appTitle": "CalenTIQ Admin",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"saving": "Saving…", "saving": "Saving…",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"common": { "common": {
"appTitle": "EventHub", "appTitle": "CalenTIQ Admin",
"cancel": "Отмена", "cancel": "Отмена",
"save": "Сохранить", "save": "Сохранить",
"saving": "Сохранение…", "saving": "Сохранение…",
+2 -1
View File
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import BrandWordmark from '@/components/BrandWordmark';
const LoginPage: React.FC = () => { const LoginPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -61,7 +62,7 @@ const LoginPage: React.FC = () => {
/> />
<Card className="relative z-10 w-full max-w-md border-border/80 shadow-lg shadow-slate-900/5 backdrop-blur-sm"> <Card className="relative z-10 w-full max-w-md border-border/80 shadow-lg shadow-slate-900/5 backdrop-blur-sm">
<CardHeader className="space-y-3"> <CardHeader className="space-y-3">
<p className="text-3xl font-semibold tracking-tight text-foreground">EventHub</p> <BrandWordmark size="md" withAdmin />
<div> <div>
<CardTitle className="text-xl">{t('login.title')}</CardTitle> <CardTitle className="text-xl">{t('login.title')}</CardTitle>
<CardDescription className="mt-1">{t('login.subtitle')}</CardDescription> <CardDescription className="mt-1">{t('login.subtitle')}</CardDescription>