예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase) 위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는 인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음. 인증/세션 - 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지 - ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로 로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example) - Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel 기능 복원 (실데이터) - Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력) - Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계 - License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용), 개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록 - Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움 - 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반) - 사용자 상세 티어별 기능 배지(pro_plus 조건부) .NET - SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
204 lines
6.1 KiB
TypeScript
204 lines
6.1 KiB
TypeScript
// apps/admin/src/lib/console-theme.ts
|
|
// D3RO Voice Admin CRM — "Midnight Glass v2" Design Tokens & UI Helpers
|
|
// Fully aligned with @d3ro/ui/theme SSOT & High-End Awwwards/Linear aesthetic
|
|
|
|
import { d3roFontSans, d3roFontMono } from '@d3ro/ui/theme'
|
|
|
|
export const C = {
|
|
// backgrounds
|
|
base: '#070b16',
|
|
app: '#0a0e1c',
|
|
card: '#111a30',
|
|
cardHover: '#152039',
|
|
elevated: '#1a2540',
|
|
input: '#0d1526',
|
|
sidebar: '#0b101f',
|
|
inset: '#0a111f',
|
|
chassis: '#111a30',
|
|
|
|
// borders
|
|
border: 'rgba(148, 180, 255, 0.08)',
|
|
borderHl: 'rgba(148, 180, 255, 0.16)',
|
|
borderStrong: 'rgba(148, 180, 255, 0.24)',
|
|
|
|
// text
|
|
dim: '#67789e',
|
|
text: '#93a4c8',
|
|
bright: '#eef2fb',
|
|
muted: '#3c4763',
|
|
|
|
// accents & gradients
|
|
accent: '#3b82f6',
|
|
accentLight: '#60a5fa',
|
|
accentDark: '#1d4ed8',
|
|
cyan: '#06b6d4',
|
|
cyanLight: '#22d3ee',
|
|
purple: '#8b5cf6',
|
|
purple400: '#a78bfa',
|
|
green: '#10b981',
|
|
green400: '#34d399',
|
|
orange: '#f59e0b',
|
|
orange400: '#fbbf24',
|
|
red: '#ef4444',
|
|
red400: '#f87171',
|
|
} as const
|
|
|
|
export const FONT_SANS = d3roFontSans
|
|
export const FONT_MONO = d3roFontMono
|
|
|
|
/** High-End Double-Bezel Glass Panel Style */
|
|
export const panelSx = {
|
|
position: 'relative' as const,
|
|
bgcolor: 'rgba(17, 26, 48, 0.65)',
|
|
backdropFilter: 'blur(24px)',
|
|
border: `1px solid ${C.border}`,
|
|
borderRadius: '20px',
|
|
overflow: 'hidden',
|
|
boxShadow: '0 16px 40px rgba(3, 7, 18, 0.5), inset 0 1px 0 rgba(148, 180, 255, 0.08)',
|
|
transition: 'border-color 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease, transform 0.25s ease',
|
|
'&:hover': {
|
|
borderColor: C.borderHl,
|
|
boxShadow: '0 20px 48px rgba(3, 7, 18, 0.6), inset 0 1px 0 rgba(148, 180, 255, 0.15)',
|
|
},
|
|
}
|
|
|
|
/** Inner Glass Core Style (Double-Bezel nested architecture) */
|
|
export const innerCoreSx = {
|
|
position: 'relative' as const,
|
|
borderRadius: '14px',
|
|
bgcolor: 'rgba(13, 21, 38, 0.75)',
|
|
backdropFilter: 'blur(16px)',
|
|
border: `1px solid ${C.border}`,
|
|
p: 2.5,
|
|
boxShadow: 'inset 0 2px 6px rgba(3, 7, 18, 0.5)',
|
|
}
|
|
|
|
/** Interactive Table Style */
|
|
export const tableSx = {
|
|
width: '100%',
|
|
borderCollapse: 'separate' as const,
|
|
borderSpacing: '0 6px',
|
|
fontFamily: FONT_SANS,
|
|
fontSize: '13px',
|
|
'& th': {
|
|
px: 2,
|
|
pb: 1.5,
|
|
fontWeight: 500,
|
|
fontSize: '11px',
|
|
textTransform: 'uppercase' as const,
|
|
letterSpacing: '0.08em',
|
|
color: C.dim,
|
|
textAlign: 'left' as const,
|
|
borderBottom: `1px solid ${C.borderHl}`,
|
|
},
|
|
'& td': {
|
|
px: 2,
|
|
py: 1.75,
|
|
textAlign: 'left' as const,
|
|
color: C.text,
|
|
bgcolor: 'rgba(17, 26, 48, 0.45)',
|
|
borderTop: `1px solid ${C.border}`,
|
|
borderBottom: `1px solid ${C.border}`,
|
|
transition: 'all 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
'&:first-of-type': {
|
|
borderLeft: `1px solid ${C.border}`,
|
|
borderTopLeftRadius: '10px',
|
|
borderBottomLeftRadius: '10px',
|
|
},
|
|
'&:last-of-type': {
|
|
borderRight: `1px solid ${C.border}`,
|
|
borderTopRightRadius: '10px',
|
|
borderBottomRightRadius: '10px',
|
|
},
|
|
},
|
|
'& tr:hover td': {
|
|
bgcolor: 'rgba(26, 38, 68, 0.75)',
|
|
borderColor: C.borderHl,
|
|
color: C.bright,
|
|
},
|
|
}
|
|
|
|
/** Filter Pill Button Style */
|
|
export const filterBtnSx = (active: boolean) => ({
|
|
px: 2,
|
|
py: 0.75,
|
|
borderRadius: '999px',
|
|
fontFamily: FONT_SANS,
|
|
fontSize: '11px',
|
|
fontWeight: 500,
|
|
letterSpacing: '0.04em',
|
|
textTransform: 'none' as const,
|
|
cursor: 'pointer',
|
|
border: `1px solid ${active ? 'rgba(59, 130, 246, 0.4)' : C.border}`,
|
|
bgcolor: active ? 'rgba(59, 130, 246, 0.16)' : 'rgba(17, 26, 48, 0.5)',
|
|
color: active ? C.bright : C.dim,
|
|
boxShadow: active ? '0 0 16px rgba(59, 130, 246, 0.25)' : 'none',
|
|
backdropFilter: 'blur(12px)',
|
|
'&:hover': {
|
|
bgcolor: active ? 'rgba(59, 130, 246, 0.22)' : 'rgba(26, 38, 68, 0.8)',
|
|
borderColor: active ? C.accentLight : C.borderHl,
|
|
color: C.bright,
|
|
transform: 'translateY(-1px)',
|
|
},
|
|
'&:active': {
|
|
transform: 'translateY(0) scale(0.98)',
|
|
},
|
|
transition: 'all 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
})
|
|
|
|
/** Semantic Status Badge Style */
|
|
export function statusBadgeSx(variant: 'green' | 'red' | 'orange' | 'blue' | 'purple' | 'cyan') {
|
|
const colorMap = {
|
|
green: { bg: 'rgba(16, 185, 129, 0.12)', fg: C.green400, border: 'rgba(16, 185, 129, 0.3)', glow: '0 0 12px rgba(16, 185, 129, 0.2)' },
|
|
red: { bg: 'rgba(239, 68, 68, 0.12)', fg: C.red400, border: 'rgba(239, 68, 68, 0.3)', glow: '0 0 12px rgba(239, 68, 68, 0.2)' },
|
|
orange: { bg: 'rgba(245, 158, 11, 0.12)', fg: C.orange400, border: 'rgba(245, 158, 11, 0.3)', glow: '0 0 12px rgba(245, 158, 11, 0.2)' },
|
|
blue: { bg: 'rgba(59, 130, 246, 0.12)', fg: C.accentLight, border: 'rgba(59, 130, 246, 0.3)', glow: '0 0 12px rgba(59, 130, 246, 0.2)' },
|
|
purple: { bg: 'rgba(139, 92, 246, 0.12)', fg: C.purple400, border: 'rgba(139, 92, 246, 0.3)', glow: '0 0 12px rgba(139, 92, 246, 0.2)' },
|
|
cyan: { bg: 'rgba(6, 182, 212, 0.12)', fg: C.cyanLight, border: 'rgba(6, 182, 212, 0.3)', glow: '0 0 12px rgba(6, 182, 212, 0.2)' },
|
|
}
|
|
const c = colorMap[variant]
|
|
return {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: 0.75,
|
|
px: 1.25,
|
|
py: 0.4,
|
|
borderRadius: '999px',
|
|
fontSize: '11px',
|
|
fontFamily: FONT_SANS,
|
|
fontWeight: 500,
|
|
letterSpacing: '0.04em',
|
|
bgcolor: c.bg,
|
|
color: c.fg,
|
|
border: `1px solid ${c.border}`,
|
|
boxShadow: c.glow,
|
|
backdropFilter: 'blur(8px)',
|
|
}
|
|
}
|
|
|
|
/** Primary Action Button Style with Gradient & Glow */
|
|
export const primaryButtonSx = {
|
|
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 55%, #60a5fa 100%)',
|
|
color: '#ffffff',
|
|
fontFamily: FONT_SANS,
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
borderRadius: '10px',
|
|
px: 2.5,
|
|
py: 1,
|
|
boxShadow: '0 0 0 1px rgba(59,130,246,0.35), 0 8px 24px rgba(37,99,235,0.35)',
|
|
textTransform: 'none' as const,
|
|
transition: 'all 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
'&:hover': {
|
|
filter: 'brightness(1.12)',
|
|
boxShadow: '0 0 0 1px rgba(96,165,250,0.5), 0 12px 32px rgba(59,130,246,0.5)',
|
|
transform: 'translateY(-1px)',
|
|
},
|
|
'&:active': {
|
|
transform: 'translateY(0) scale(0.98)',
|
|
},
|
|
}
|
|
|
|
// Backward compatibility alias for FONT
|
|
export const FONT = FONT_MONO
|
|
|