feat(ui): Midnight Glass v2 전면 리디자인 — 기반+셸+대시보드
- theme.ts 재작성: glass/gradient/glow 토큰군, 기본 dark=Midnight(블루), 6종 테마 변형 전부 파생 토큰 유지 (테마 시스템 보존) - DS 리스킨(API 호환): 글래스 카드/버튼, Pretendard 타이포, 소프트 LED - 신규: GradientWave(canvas 스펙트럼 음파), StatRing(컬러 링 타일) - 보더리스: frame:false + 커스텀 TitleBar, 네이티브 스크롤바 제거(폭 불변) + OverlayScrollbars 오버레이 스크롤 - AppLayout 와이드 사이드바(lucide, 그라디언트 액티브 필), 대시보드 재구축, StatusBar(브랜드/로컬시간), Pretendard 번들, i18n +20키(ko/en)
This commit is contained in:
parent
f3ca0eafd4
commit
a8a1d6e546
25 changed files with 1638 additions and 960 deletions
|
|
@ -33,6 +33,24 @@
|
|||
"dashboard.noHistory": "No history — press your hotkey to start recording",
|
||||
"dashboard.copy": "Copy",
|
||||
"dashboard.entries": "{{count}} entries",
|
||||
"dashboard.heroIdle": "Ready for voice recognition",
|
||||
"dashboard.viewStats": "View Stats",
|
||||
"dashboard.systemSettings": "System Settings",
|
||||
"dashboard.keySetup": "Key Setup",
|
||||
"dashboard.systemOk": "System OK",
|
||||
"dashboard.systemIssue": "Needs Attention",
|
||||
"dashboard.captionPlaceholder": "Recognized text will appear here in real time.",
|
||||
"dashboard.captionStandby": "Standby",
|
||||
"dashboard.inputLevel": "Input Level",
|
||||
"dashboard.recordingTime": "Recording Time",
|
||||
"dashboard.recognizedWords": "Words Recognized",
|
||||
"dashboard.streakUse": "Streak",
|
||||
"dashboard.modelLabel": "Model",
|
||||
"dashboard.statusLabel": "Status",
|
||||
"dashboard.localConnected": "Local",
|
||||
"dashboard.planFree": "Free Plan",
|
||||
"dashboard.planFeatureLocal": "Unlimited local backend",
|
||||
"dashboard.planFeatureDaily": "Unlimited daily transcription",
|
||||
"dashboard.stat": "Stats",
|
||||
"dashboard.sys": "System",
|
||||
"dashboard.currentBackend": "Current Backend",
|
||||
|
|
@ -155,6 +173,7 @@
|
|||
"settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)",
|
||||
"settings.about.description": "A fully local AI voice assistant built on Speakly reverse-engineering insights. Runs entirely offline with no cloud dependencies.",
|
||||
"settings.about.restartOnboarding": "Restart Setup Wizard",
|
||||
"status.localTime": "Local Time",
|
||||
"status.ollama": "OLLAMA",
|
||||
"status.offline": "OFFLINE",
|
||||
"status.nudge.title": "Ollama is not running",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,24 @@
|
|||
"dashboard.noHistory": "히스토리 없음 — 핫키를 눌러 녹음을 시작하세요",
|
||||
"dashboard.copy": "복사",
|
||||
"dashboard.entries": "{{count}}건",
|
||||
"dashboard.heroIdle": "실시간 음성 인식 대기 중",
|
||||
"dashboard.viewStats": "통계 보기",
|
||||
"dashboard.systemSettings": "시스템 설정",
|
||||
"dashboard.keySetup": "키 설정",
|
||||
"dashboard.systemOk": "시스템 정상",
|
||||
"dashboard.systemIssue": "점검 필요",
|
||||
"dashboard.captionPlaceholder": "여기에 실시간 인식된 텍스트가 표시됩니다.",
|
||||
"dashboard.captionStandby": "대기 중",
|
||||
"dashboard.inputLevel": "입력 레벨",
|
||||
"dashboard.recordingTime": "녹음 시간",
|
||||
"dashboard.recognizedWords": "인식 단어 수",
|
||||
"dashboard.streakUse": "연속 사용",
|
||||
"dashboard.modelLabel": "모델",
|
||||
"dashboard.statusLabel": "상태",
|
||||
"dashboard.localConnected": "로컬 연결",
|
||||
"dashboard.planFree": "무료 플랜",
|
||||
"dashboard.planFeatureLocal": "로컬 백엔드 무제한",
|
||||
"dashboard.planFeatureDaily": "일일 전사 무제한",
|
||||
"dashboard.stat": "통계",
|
||||
"dashboard.sys": "시스템",
|
||||
"dashboard.currentBackend": "현재 백엔드",
|
||||
|
|
@ -156,6 +174,7 @@
|
|||
"settings.about.voiceEngineValue": "STT: faster-whisper (로컬) / LLM: Ollama (로컬)",
|
||||
"settings.about.description": "Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트. 클라우드 의존성 없이 완전 로컬로 동작합니다.",
|
||||
"settings.about.restartOnboarding": "초기 설정 안내 다시 보기",
|
||||
"status.localTime": "로컬 시간",
|
||||
"status.ollama": "OLLAMA",
|
||||
"status.offline": "OFFLINE",
|
||||
"status.nudge.title": "Ollama가 실행되지 않고 있어요",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/ButtonGroup.tsx
|
||||
// 시안 A: 인셋 버튼 클러스터 — 레퍼런스의 .button-group 패턴
|
||||
// 물리 버튼들을 인셋 패널 안에 배치하여 그룹화
|
||||
// v2 "Midnight Glass": 세그먼트 글래스 그룹 — 반투명 표면에 버튼 클러스터
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { d3roPalette, d3roShadow, d3roRadius } from '../../theme'
|
||||
import { d3roPalette, d3roRadius } from '../../theme'
|
||||
|
||||
interface ButtonGroupProps {
|
||||
children: React.ReactNode
|
||||
|
|
@ -20,7 +19,7 @@ export function ButtonGroup({ children, horizontal = false }: ButtonGroupProps):
|
|||
bgcolor: d3roPalette.bg.inset,
|
||||
p: '6px',
|
||||
borderRadius: d3roRadius.inner,
|
||||
boxShadow: d3roShadow.inset,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
display: 'flex',
|
||||
flexDirection: horizontal ? 'row' : 'column',
|
||||
gap: '6px',
|
||||
|
|
|
|||
|
|
@ -1,284 +1,98 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/CrtDisplay.tsx
|
||||
// 시안 A: CRT 디스플레이 — WebGL 셰이더 (스캔라인, 비네팅, 노이즈, 글리치, 파형)
|
||||
// v2 "Midnight Glass": 히어로 디스플레이 — 딥 글래스 패널 + 그라디언트 스펙트럼 웨이브.
|
||||
// (v1 WebGL 오실로스코프를 대체. props API는 v1과 호환 유지.)
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow } from '../../theme'
|
||||
|
||||
// ── WebGL 유틸 ─────────────────────────────────────────
|
||||
|
||||
function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
|
||||
const shader = gl.createShader(type)
|
||||
if (!shader) return null
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
gl.deleteShader(shader)
|
||||
return null
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
function createProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {
|
||||
const vs = createShader(gl, gl.VERTEX_SHADER, vsSource)
|
||||
const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource)
|
||||
if (!vs || !fs) return null
|
||||
const prog = gl.createProgram()
|
||||
if (!prog) return null
|
||||
gl.attachShader(prog, vs)
|
||||
gl.attachShader(prog, fs)
|
||||
gl.linkProgram(prog)
|
||||
return prog
|
||||
}
|
||||
|
||||
const VERTEX_SHADER = `
|
||||
attribute vec2 position;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
vUv = position * 0.5 + 0.5;
|
||||
}
|
||||
`
|
||||
|
||||
const FRAGMENT_SHADER = `
|
||||
precision highp float;
|
||||
varying vec2 vUv;
|
||||
uniform float u_time;
|
||||
uniform float u_glitch;
|
||||
uniform float u_amp;
|
||||
uniform float u_freq;
|
||||
|
||||
float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
|
||||
// Glitch displacement
|
||||
if (u_glitch > 0.0) {
|
||||
float gOffset = (random(vec2(uv.y * 5.0, u_time)) - 0.5) * u_glitch * 0.1;
|
||||
uv.x += gOffset;
|
||||
}
|
||||
|
||||
// Base phosphor background
|
||||
vec3 color = vec3(0.02, 0.015, 0.01);
|
||||
|
||||
// Background Grid
|
||||
float gridX = step(0.98, fract(uv.x * 15.0));
|
||||
float gridY = step(0.98, fract(uv.y * 10.0));
|
||||
color += vec3(0.1, 0.04, 0.02) * max(gridX, gridY) * (1.0 - u_glitch);
|
||||
|
||||
// Oscilloscope Waveform
|
||||
float t = u_time * 0.5 + u_glitch * random(uv) * 0.1;
|
||||
float waveY = sin((uv.x * u_freq) + t) * u_amp;
|
||||
float waveDist = abs((uv.y - 0.5) - waveY);
|
||||
float lineThick = 0.005 + (u_glitch * 0.02);
|
||||
float waveGlow = smoothstep(lineThick * 4.0, 0.0, waveDist);
|
||||
float waveCore = smoothstep(lineThick, 0.0, waveDist);
|
||||
|
||||
// Amber wave (#f25b29)
|
||||
vec3 waveColor = vec3(0.95, 0.35, 0.16);
|
||||
color += waveColor * waveCore;
|
||||
color += waveColor * 0.4 * waveGlow;
|
||||
|
||||
// Scanlines
|
||||
float scanline = sin(uv.y * 800.0 - u_time * 10.0) * 0.04;
|
||||
color -= scanline;
|
||||
|
||||
// Noise
|
||||
float noise = random(uv + u_time) * 0.08;
|
||||
color += noise;
|
||||
|
||||
// Vignette
|
||||
float dist = distance(vUv, vec2(0.5));
|
||||
float vig = smoothstep(0.8, 0.4, dist);
|
||||
color *= vig;
|
||||
|
||||
// Edge darkening
|
||||
color *= smoothstep(0.0, 0.02, vUv.x) * smoothstep(1.0, 0.98, vUv.x);
|
||||
color *= smoothstep(0.0, 0.05, vUv.y) * smoothstep(1.0, 0.95, vUv.y);
|
||||
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
const QUAD = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1])
|
||||
|
||||
// ── Props ──────────────────────────────────────────────
|
||||
import { d3roPalette, d3roFontSans, d3roRadius } from '../../theme'
|
||||
import { GradientWave } from './GradientWave'
|
||||
|
||||
interface CrtDisplayProps {
|
||||
/** 파형 진폭 (0.0 ~ 1.0) */
|
||||
/** 유휴 파형 진폭 (0.0 ~ 1.0) */
|
||||
amplitude?: number
|
||||
/** 파형 주파수 */
|
||||
/** 파형 속도 배율 (v1 주파수 개념 호환 — 8 기준 1배) */
|
||||
frequency?: number
|
||||
/** 글리치 트리거 (변경 시 글리치 발생) */
|
||||
/** 글리치 트리거 (변경 시 플래시 발생) */
|
||||
glitchTrigger?: number
|
||||
/** 실시간 오디오 레벨 (0.0~1.0) — 파형 진폭에 반영 */
|
||||
audioLevel?: number
|
||||
/** 오버레이 콘텐츠 (인광 텍스트 등) */
|
||||
/** 오버레이 콘텐츠 */
|
||||
children?: React.ReactNode
|
||||
/** 높이 (기본 280px) */
|
||||
height?: number | string
|
||||
}
|
||||
|
||||
export function CrtDisplay({
|
||||
amplitude = 0.1,
|
||||
amplitude = 0.25,
|
||||
frequency = 8.0,
|
||||
glitchTrigger = 0,
|
||||
audioLevel = 0,
|
||||
children,
|
||||
height = 280,
|
||||
}: CrtDisplayProps): React.ReactElement {
|
||||
const theme = useTheme()
|
||||
const isLight = theme.palette.mode === 'light'
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const glRef = useRef<{
|
||||
gl: WebGLRenderingContext
|
||||
uTime: WebGLUniformLocation | null
|
||||
uGlitch: WebGLUniformLocation | null
|
||||
uAmp: WebGLUniformLocation | null
|
||||
uFreq: WebGLUniformLocation | null
|
||||
} | null>(null)
|
||||
const animRef = useRef<number>(0)
|
||||
const startTimeRef = useRef(Date.now())
|
||||
const glitchRef = useRef(0)
|
||||
const ampRef = useRef(amplitude)
|
||||
const freqRef = useRef(frequency)
|
||||
const audioLevelRef = useRef(audioLevel)
|
||||
const currentAmpRef = useRef(amplitude)
|
||||
const currentFreqRef = useRef(frequency)
|
||||
const [flash, setFlash] = useState(false)
|
||||
|
||||
// amplitude/frequency/audioLevel 변경 추적
|
||||
useEffect(() => {
|
||||
ampRef.current = amplitude
|
||||
freqRef.current = frequency
|
||||
}, [amplitude, frequency])
|
||||
|
||||
useEffect(() => {
|
||||
audioLevelRef.current = audioLevel
|
||||
}, [audioLevel])
|
||||
|
||||
// 글리치 트리거
|
||||
// v1 글리치 → v2 소프트 플래시
|
||||
useEffect(() => {
|
||||
if (glitchTrigger > 0) {
|
||||
glitchRef.current = 1.0
|
||||
setFlash(true)
|
||||
const timer = setTimeout(() => setFlash(false), 220)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
return undefined
|
||||
}, [glitchTrigger])
|
||||
|
||||
const render = useCallback(() => {
|
||||
const ctx = glRef.current
|
||||
if (!ctx) return
|
||||
|
||||
const { gl, uTime, uGlitch, uAmp, uFreq } = ctx
|
||||
|
||||
// audioLevel → amplitude 반영: 기본 amplitude + 오디오 레벨로 증폭
|
||||
const targetAmp = ampRef.current + audioLevelRef.current * 0.35
|
||||
|
||||
// Smoothing
|
||||
currentAmpRef.current += (targetAmp - currentAmpRef.current) * 0.15
|
||||
currentFreqRef.current += (freqRef.current - currentFreqRef.current) * 0.1
|
||||
glitchRef.current *= 0.85
|
||||
|
||||
gl.uniform1f(uTime, (Date.now() - startTimeRef.current) / 1000)
|
||||
gl.uniform1f(uGlitch, glitchRef.current)
|
||||
gl.uniform1f(uAmp, currentAmpRef.current)
|
||||
gl.uniform1f(uFreq, currentFreqRef.current)
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
animRef.current = requestAnimationFrame(render)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const gl = canvas.getContext('webgl', { alpha: true })
|
||||
if (!gl) return
|
||||
|
||||
canvas.width = canvas.clientWidth * 2
|
||||
canvas.height = canvas.clientHeight * 2
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const prog = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER)
|
||||
if (!prog) return
|
||||
|
||||
gl.useProgram(prog)
|
||||
|
||||
const buffer = gl.createBuffer()
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, QUAD, gl.STATIC_DRAW)
|
||||
|
||||
const posLoc = gl.getAttribLocation(prog, 'position')
|
||||
gl.enableVertexAttribArray(posLoc)
|
||||
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0)
|
||||
|
||||
glRef.current = {
|
||||
gl,
|
||||
uTime: gl.getUniformLocation(prog, 'u_time'),
|
||||
uGlitch: gl.getUniformLocation(prog, 'u_glitch'),
|
||||
uAmp: gl.getUniformLocation(prog, 'u_amp'),
|
||||
uFreq: gl.getUniformLocation(prog, 'u_freq'),
|
||||
}
|
||||
|
||||
startTimeRef.current = Date.now()
|
||||
animRef.current = requestAnimationFrame(render)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animRef.current)
|
||||
}
|
||||
}, [render])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height,
|
||||
bgcolor: d3roPalette.bg.crtBezel,
|
||||
borderRadius: '8px',
|
||||
boxShadow: d3roShadow.insetDeep,
|
||||
bgcolor: d3roPalette.bg.crtGlass,
|
||||
borderRadius: d3roRadius.inner,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Glass surface */}
|
||||
{/* 앰비언트 배경 글로우 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: '2px',
|
||||
borderRadius: '6px',
|
||||
bgcolor: d3roPalette.bg.crtGlass,
|
||||
overflow: 'hidden',
|
||||
boxShadow: d3roShadow.screenGlow,
|
||||
inset: 0,
|
||||
background: `radial-gradient(ellipse 70% 55% at 50% 100%, ${d3roPalette.accent.dim} 0%, transparent 70%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
// 라이트 모드에서 WebGL 셰이더(다크 전용)를 반전하여 밝은 배경에 어울리게 조정
|
||||
...(isLight && { filter: 'invert(0.88) hue-rotate(180deg)', opacity: 0.9 }),
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
|
||||
{/* Glass reflection */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, right: 0, bottom: '50%',
|
||||
background: isLight
|
||||
? 'linear-gradient(180deg, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0) 100%)'
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0) 100%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
}}
|
||||
/>
|
||||
{/* 그라디언트 스펙트럼 웨이브 — 하단 영역 */}
|
||||
<Box sx={{ position: 'absolute', left: 12, right: 12, bottom: 8, height: '38%' }}>
|
||||
<GradientWave audioLevel={audioLevel} idleAmplitude={amplitude} speed={frequency / 8} />
|
||||
</Box>
|
||||
|
||||
{/* Content overlay (phosphor text) */}
|
||||
{/* 상단 시인 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: d3roPalette.glass.sheen,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 플래시 오버레이 (글리치 호환) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
bgcolor: d3roPalette.accent.dim,
|
||||
opacity: flash ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 콘텐츠 오버레이 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
|
|
@ -287,10 +101,9 @@ export function CrtDisplay({
|
|||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
color: d3roPalette.accent.amber,
|
||||
textShadow: '0 0 6px rgba(242, 91, 41, 0.4)',
|
||||
color: d3roPalette.text.primary,
|
||||
pointerEvents: 'none',
|
||||
fontFamily: d3roFontMono,
|
||||
fontFamily: d3roFontSans,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
165
packages/ui/src/components/ds/GradientWave.tsx
Normal file
165
packages/ui/src/components/ds/GradientWave.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/GradientWave.tsx
|
||||
// v2 시그니처 비주얼: 시안→블루→퍼플→마젠타 그라디언트 스펙트럼 웨이브.
|
||||
// canvas 2D — 테마 CSS 변수(--d3-gradient-wave1..4)를 읽어 테마 전환에 자동 반응.
|
||||
// 미러형(중앙선 기준 상하 대칭) 바 스펙트럼 + 소프트 글로우 + 유휴 애니메이션.
|
||||
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
interface GradientWaveProps {
|
||||
/** 실시간 오디오 레벨 0~1 (없으면 유휴 애니메이션만) */
|
||||
audioLevel?: number
|
||||
/** 유휴 상태 기본 진폭 0~1 (기본 0.25) */
|
||||
idleAmplitude?: number
|
||||
/** 애니메이션 속도 배율 (기본 1) */
|
||||
speed?: number
|
||||
/** 바 폭 px (기본 3) */
|
||||
barWidth?: number
|
||||
/** 바 간격 px (기본 3) */
|
||||
barGap?: number
|
||||
/** 글로우 강도 px (기본 8, 0이면 비활성) */
|
||||
glow?: number
|
||||
/** 높이 (CSS 값, 기본 '100%') */
|
||||
height?: number | string
|
||||
}
|
||||
|
||||
/** :root에 주입된 웨이브 그라디언트 스톱을 읽는다 */
|
||||
function readWaveColors(): [string, string, string, string] {
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const read = (name: string, fallback: string): string =>
|
||||
style.getPropertyValue(name).trim() || fallback
|
||||
return [
|
||||
read('--d3-gradient-wave1', '#22d3ee'),
|
||||
read('--d3-gradient-wave2', '#3b82f6'),
|
||||
read('--d3-gradient-wave3', '#8b5cf6'),
|
||||
read('--d3-gradient-wave4', '#e879f9'),
|
||||
]
|
||||
}
|
||||
|
||||
/** 결정적 의사난수 (바 인덱스별 고정 시드) */
|
||||
function seeded(i: number): number {
|
||||
const x = Math.sin(i * 127.1 + 311.7) * 43758.5453
|
||||
return x - Math.floor(x)
|
||||
}
|
||||
|
||||
export function GradientWave({
|
||||
audioLevel = 0,
|
||||
idleAmplitude = 0.25,
|
||||
speed = 1,
|
||||
barWidth = 3,
|
||||
barGap = 3,
|
||||
glow = 8,
|
||||
height = '100%',
|
||||
}: GradientWaveProps): React.ReactElement {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const levelRef = useRef(audioLevel)
|
||||
const smoothedRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
levelRef.current = audioLevel
|
||||
}, [audioLevel])
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
let raf = 0
|
||||
let width = 0
|
||||
let heightPx = 0
|
||||
let colors = readWaveColors()
|
||||
let gradient: CanvasGradient | null = null
|
||||
let frame = 0
|
||||
const start = performance.now()
|
||||
|
||||
const rebuildGradient = (): void => {
|
||||
gradient = ctx.createLinearGradient(0, 0, width, 0)
|
||||
gradient.addColorStop(0, colors[0])
|
||||
gradient.addColorStop(0.38, colors[1])
|
||||
gradient.addColorStop(0.7, colors[2])
|
||||
gradient.addColorStop(1, colors[3])
|
||||
}
|
||||
|
||||
const resize = (): void => {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
width = canvas.clientWidth
|
||||
heightPx = canvas.clientHeight
|
||||
canvas.width = Math.max(1, Math.round(width * dpr))
|
||||
canvas.height = Math.max(1, Math.round(heightPx * dpr))
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
rebuildGradient()
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(resize)
|
||||
observer.observe(canvas)
|
||||
resize()
|
||||
|
||||
const render = (): void => {
|
||||
frame += 1
|
||||
// 테마 전환 대응: 1초마다 색 재판독
|
||||
if (frame % 60 === 0) {
|
||||
const next = readWaveColors()
|
||||
if (next.join() !== colors.join()) {
|
||||
colors = next
|
||||
rebuildGradient()
|
||||
}
|
||||
}
|
||||
|
||||
// 레벨 스무딩 (상승 빠르게, 하강 느리게)
|
||||
const target = Math.min(1, levelRef.current)
|
||||
const k = target > smoothedRef.current ? 0.4 : 0.08
|
||||
smoothedRef.current += (target - smoothedRef.current) * k
|
||||
|
||||
ctx.clearRect(0, 0, width, heightPx)
|
||||
if (!gradient || width <= 0) {
|
||||
raf = requestAnimationFrame(render)
|
||||
return
|
||||
}
|
||||
|
||||
const t = ((performance.now() - start) / 1000) * speed
|
||||
const step = barWidth + barGap
|
||||
const count = Math.max(1, Math.floor(width / step))
|
||||
const centerY = heightPx / 2
|
||||
const maxHalf = heightPx / 2 - 1
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
if (glow > 0) {
|
||||
ctx.shadowBlur = glow
|
||||
ctx.shadowColor = colors[1]
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x = i * step
|
||||
const s = seeded(i)
|
||||
// 유휴: 느리게 흐르는 다중 사인 + 바별 시드 → 오가닉한 스펙트럼
|
||||
const idle =
|
||||
(Math.sin(t * 1.4 + i * 0.35) * 0.5 + 0.5) * 0.55 +
|
||||
(Math.sin(t * 0.7 + i * 0.11 + s * 6.28) * 0.5 + 0.5) * 0.45
|
||||
const envelope = idleAmplitude * (0.25 + idle * 0.75) + smoothedRef.current * (0.4 + s * 0.6)
|
||||
const half = Math.max(1.5, Math.min(1, envelope) * maxHalf)
|
||||
const radius = Math.min(barWidth / 2, half)
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, centerY - half, barWidth, half * 2, radius)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.shadowBlur = 0
|
||||
raf = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(render)
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [idleAmplitude, speed, barWidth, barGap, glow])
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height, minHeight: 0 }}>
|
||||
<canvas ref={canvasRef} style={{ display: 'block', width: '100%', height: '100%' }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/InstrumentPanel.tsx
|
||||
// 시안 A: 메탈 섀시 컨테이너 — 노이즈 텍스처, 각인 텍스트, 물리적 존재감
|
||||
// v2 "Midnight Glass": 히어로 글래스 패널 — 앰비언트 그라디언트 배경 + 헤어라인.
|
||||
// v1의 각인(engraving) props는 호환 유지하되 모서리 메타 라벨로 은은하게 렌더.
|
||||
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
|
||||
|
||||
interface InstrumentPanelProps {
|
||||
children: React.ReactNode
|
||||
/** 상단 좌측 각인 */
|
||||
/** 상단 좌측 메타 라벨 */
|
||||
engravingLeft?: string
|
||||
/** 상단 우측 각인 */
|
||||
/** 상단 우측 메타 라벨 */
|
||||
engravingRight?: string
|
||||
/** 하단 중앙 각인 */
|
||||
/** 하단 중앙 메타 라벨 */
|
||||
engravingBottom?: string
|
||||
}
|
||||
|
||||
|
|
@ -27,30 +27,30 @@ export function InstrumentPanel({
|
|||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
bgcolor: d3roPalette.bg.chassis,
|
||||
bgcolor: d3roPalette.glass.surface,
|
||||
backdropFilter: `blur(${d3roPalette.glass.blur})`,
|
||||
borderRadius: d3roRadius.outer,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
p: 3,
|
||||
boxShadow: d3roShadow.chassis,
|
||||
// 메탈 노이즈는 CSS로 시뮬레이션
|
||||
boxShadow: d3roShadow.card,
|
||||
overflow: 'hidden',
|
||||
// 앰비언트 액센트 글로우 (좌상단 은은한 빛)
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: d3roRadius.outer,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
opacity: 0.04,
|
||||
mixBlendMode: 'overlay',
|
||||
borderRadius: 'inherit',
|
||||
background: `radial-gradient(ellipse 60% 40% at 18% 0%, ${d3roPalette.accent.dim} 0%, transparent 60%)`,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* 각인 텍스트 */}
|
||||
{/* 모서리 메타 라벨 */}
|
||||
<Engraving sx={{ top: 12, left: 24 }}>{engravingLeft}</Engraving>
|
||||
<Engraving sx={{ top: 12, right: 24 }}>{engravingRight}</Engraving>
|
||||
<Engraving sx={{ bottom: 12, left: '50%', transform: 'translateX(-50%)' }}>{engravingBottom}</Engraving>
|
||||
|
||||
{/* 콘텐츠 (z-index 5로 노이즈 위) */}
|
||||
<Box sx={{ position: 'relative', zIndex: 5 }}>
|
||||
{children}
|
||||
</Box>
|
||||
|
|
@ -59,20 +59,17 @@ export function InstrumentPanel({
|
|||
}
|
||||
|
||||
function Engraving({ children, sx }: { children: string; sx: Record<string, unknown> }): React.ReactElement {
|
||||
const theme = useTheme()
|
||||
const isLight = theme.palette.mode === 'light'
|
||||
return (
|
||||
<Typography
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
fontSize: d3roTypo.engrave.size,
|
||||
letterSpacing: d3roTypo.engrave.spacing,
|
||||
color: d3roPalette.text.engraving,
|
||||
textShadow: isLight
|
||||
? '0 -1px 0 rgba(0,0,0,0.1)'
|
||||
: '0 1px 0 rgba(255,255,255,0.08)',
|
||||
color: d3roPalette.text.dimLabel,
|
||||
opacity: 0.7,
|
||||
fontWeight: 700,
|
||||
fontFamily: d3roFontMono,
|
||||
textTransform: 'uppercase',
|
||||
zIndex: 2,
|
||||
userSelect: 'none',
|
||||
...sx,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/Led.tsx
|
||||
// 시안 A: LED 인디케이터 — 물리적 LED, 활성 시 glow + pulse
|
||||
// v2 "Midnight Glass": 상태 도트 — 소프트 글로우, 활성 시 pulse
|
||||
// (레퍼런스의 "● READY" / "● 시스템 정상" 인디케이터)
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { d3roPalette } from '../../theme'
|
||||
|
||||
type LedColor = 'amber' | 'green' | 'red' | 'orange' | 'off'
|
||||
type LedColor = 'amber' | 'green' | 'red' | 'orange' | 'blue' | 'purple' | 'off'
|
||||
|
||||
const LED_COLORS: Record<LedColor, { bg: string; glow: string }> = {
|
||||
amber: { bg: d3roPalette.accent.amber, glow: d3roPalette.accent.amberGlow },
|
||||
// 'amber'는 v1 호환 별칭 — 현재 테마의 프라이머리 액센트 색으로 렌더
|
||||
amber: { bg: d3roPalette.accent.main, glow: d3roPalette.accent.glow },
|
||||
green: { bg: d3roPalette.tag.green, glow: d3roPalette.tag.greenGlow },
|
||||
red: { bg: d3roPalette.tag.red, glow: d3roPalette.tag.redGlow },
|
||||
orange: { bg: d3roPalette.tag.orange, glow: d3roPalette.tag.orangeGlow },
|
||||
blue: { bg: d3roPalette.tag.blue, glow: d3roPalette.tag.blueGlow },
|
||||
purple: { bg: d3roPalette.tag.purple, glow: d3roPalette.tag.purpleGlow },
|
||||
off: { bg: d3roPalette.led.off, glow: 'transparent' },
|
||||
}
|
||||
|
||||
|
|
@ -34,16 +38,14 @@ export function Led({ color = 'off', pulse = false, size = 8 }: LedProps): React
|
|||
borderRadius: '50%',
|
||||
bgcolor: c.bg,
|
||||
flexShrink: 0,
|
||||
boxShadow: isActive
|
||||
? `inset 0 1px 2px rgba(255,255,255,0.5), 0 0 10px ${c.glow}`
|
||||
: 'inset 0 1px 3px rgba(0,0,0,0.9), 0 1px 0 rgba(255,255,255,0.05)',
|
||||
transition: 'all 0.1s',
|
||||
boxShadow: isActive ? `0 0 ${size * 1.2}px ${c.glow}` : 'none',
|
||||
transition: 'all 0.15s ease',
|
||||
...(pulse && isActive
|
||||
? {
|
||||
animation: 'led-pulse 1.5s ease-in-out infinite',
|
||||
animation: 'led-pulse 1.6s ease-in-out infinite',
|
||||
'@keyframes led-pulse': {
|
||||
'0%, 100%': { opacity: 1 },
|
||||
'50%': { opacity: 0.5 },
|
||||
'50%': { opacity: 0.45 },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
'use client'
|
||||
|
||||
// packages/ui/src/components/ds/MetalCard.tsx
|
||||
// 시안 A+B 융합: 메탈 카드 컨테이너 — 섀시 느낌의 인셋 패널
|
||||
// 토큰 적용: d3roShadow, d3roRadius
|
||||
// packages/ui/src/components/ds/MetalCard.tssx
|
||||
// v2 "Midnight Glass": 글래스모피즘 카드 — 반투명 표면 + 옅은 헤어라인 + 상단 시인
|
||||
// 토큰 적용: d3roPalette.glass, d3roShadow, d3roRadius
|
||||
|
||||
import { Box, type BoxProps } from '@mui/material'
|
||||
import { d3roPalette, d3roShadow, d3roRadius } from '../../theme'
|
||||
|
||||
interface MetalCardProps extends Omit<BoxProps, 'component'> {
|
||||
/** 카드 내부의 상승 표면 (카드 안의 카드) */
|
||||
inset?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -21,14 +22,27 @@ export function MetalCard({
|
|||
<Box
|
||||
{...boxProps}
|
||||
sx={{
|
||||
bgcolor: inset ? d3roPalette.bg.inset : d3roPalette.bg.card,
|
||||
position: 'relative',
|
||||
bgcolor: inset ? d3roPalette.glass.raised : d3roPalette.glass.surface,
|
||||
backdropFilter: `blur(${d3roPalette.glass.blur})`,
|
||||
borderRadius: inset ? d3roRadius.inner : d3roRadius.card,
|
||||
borderTop: inset ? 'none' : `1px solid ${d3roPalette.border.subtle}`,
|
||||
boxShadow: inset ? d3roShadow.inset : d3roShadow.card,
|
||||
p: inset ? '6px' : 3,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
boxShadow: inset ? 'none' : d3roShadow.card,
|
||||
p: inset ? 2 : 3,
|
||||
overflow: 'hidden',
|
||||
transition: 'background-color 0.2s ease',
|
||||
'&:hover': inset ? {} : { bgcolor: d3roPalette.bg.cardHover },
|
||||
transition: 'border-color 0.2s ease',
|
||||
// 상단 시인(광택) 오버레이
|
||||
'&::before': inset
|
||||
? {}
|
||||
: {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
background: d3roPalette.glass.sheen,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:hover': inset ? {} : { borderColor: d3roPalette.glass.hairlineStrong },
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/PhosphorText.tsx
|
||||
// 시안 A: 인광 텍스트 — 앰버 glow, 모노 폰트, CRT 느낌
|
||||
// 확장: title/stat/body/compact/meta/engrave/micro/nano 변형 추가
|
||||
// v2 "Midnight Glass": 클린 타이포그래피 — 산세리프(Pretendard) 기반.
|
||||
// 헤드라인/수치는 프라이머리 화이트, 메타/라벨류만 모노+대문자 유지.
|
||||
// (변형 API는 v1과 동일 — 전 페이지 호환)
|
||||
|
||||
import { Typography, type TypographyProps } from '@mui/material'
|
||||
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
|
||||
import { d3roPalette, d3roFontSans, d3roFontMono, d3roTypo } from '../../theme'
|
||||
|
||||
type PhosphorVariant =
|
||||
| 'hero' | 'title' | 'value' | 'heading'
|
||||
|
|
@ -16,31 +17,27 @@ type PhosphorVariant =
|
|||
interface VariantDef {
|
||||
fontSize: string
|
||||
color: string
|
||||
glow: string
|
||||
fontWeight: number
|
||||
letterSpacing: string
|
||||
lineHeight: number
|
||||
textTransform?: 'uppercase' | 'none'
|
||||
mono?: boolean
|
||||
}
|
||||
|
||||
const amberGlowStrong = `0 0 8px ${d3roPalette.accent.amberGlow}`
|
||||
const amberGlowMedium = `0 0 6px rgba(242, 91, 41, 0.4)`
|
||||
const amberGlowSoft = `0 0 4px rgba(242, 91, 41, 0.3)`
|
||||
|
||||
const VARIANTS: Record<PhosphorVariant, VariantDef> = {
|
||||
hero: { fontSize: d3roTypo.hero.size, color: d3roPalette.accent.amber, glow: amberGlowStrong, fontWeight: d3roTypo.hero.weight, letterSpacing: d3roTypo.hero.spacing, lineHeight: d3roTypo.hero.line },
|
||||
title: { fontSize: d3roTypo.title.size, color: d3roPalette.accent.amber, glow: amberGlowMedium, fontWeight: d3roTypo.title.weight, letterSpacing: d3roTypo.title.spacing, lineHeight: d3roTypo.title.line },
|
||||
value: { fontSize: d3roTypo.value.size, color: d3roPalette.accent.amber, glow: amberGlowSoft, fontWeight: d3roTypo.value.weight, letterSpacing: d3roTypo.value.spacing, lineHeight: d3roTypo.value.line },
|
||||
heading: { fontSize: d3roTypo.heading.size, color: d3roPalette.text.primary, glow: 'none', fontWeight: d3roTypo.heading.weight, letterSpacing: d3roTypo.heading.spacing, lineHeight: d3roTypo.heading.line },
|
||||
body: { fontSize: d3roTypo.body.size, color: d3roPalette.text.secondary, glow: 'none', fontWeight: d3roTypo.body.weight, letterSpacing: d3roTypo.body.spacing, lineHeight: d3roTypo.body.line },
|
||||
compact: { fontSize: d3roTypo.compact.size, color: d3roPalette.text.primary, glow: 'none', fontWeight: d3roTypo.compact.weight, letterSpacing: d3roTypo.compact.spacing, lineHeight: d3roTypo.compact.line },
|
||||
small: { fontSize: d3roTypo.small.size, color: d3roPalette.text.inactive, glow: 'none', fontWeight: d3roTypo.small.weight, letterSpacing: d3roTypo.small.spacing, lineHeight: d3roTypo.small.line },
|
||||
meta: { fontSize: d3roTypo.meta.size, color: d3roPalette.text.inactive, glow: 'none', fontWeight: d3roTypo.meta.weight, letterSpacing: d3roTypo.meta.spacing, lineHeight: d3roTypo.meta.line, textTransform: 'uppercase' },
|
||||
label: { fontSize: d3roTypo.label.size, color: d3roPalette.text.dimLabel, glow: 'none', fontWeight: d3roTypo.label.weight, letterSpacing: d3roTypo.label.spacing, lineHeight: d3roTypo.label.line, textTransform: 'uppercase' },
|
||||
dim: { fontSize: d3roTypo.label.size, color: d3roPalette.text.inactive, glow: 'none', fontWeight: d3roTypo.body.weight, letterSpacing: d3roTypo.label.spacing, lineHeight: d3roTypo.label.line },
|
||||
engrave: { fontSize: d3roTypo.engrave.size, color: d3roPalette.text.engraving, glow: 'none', fontWeight: d3roTypo.engrave.weight, letterSpacing: d3roTypo.engrave.spacing, lineHeight: d3roTypo.engrave.line, textTransform: 'uppercase' },
|
||||
micro: { fontSize: d3roTypo.micro.size, color: d3roPalette.text.dimLabel, glow: 'none', fontWeight: d3roTypo.micro.weight, letterSpacing: d3roTypo.micro.spacing, lineHeight: d3roTypo.micro.line, textTransform: 'uppercase' },
|
||||
nano: { fontSize: d3roTypo.nano.size, color: d3roPalette.text.inactive, glow: 'none', fontWeight: d3roTypo.nano.weight, letterSpacing: d3roTypo.nano.spacing, lineHeight: d3roTypo.nano.line, textTransform: 'uppercase' },
|
||||
hero: { fontSize: d3roTypo.hero.size, color: d3roPalette.text.primary, fontWeight: d3roTypo.hero.weight, letterSpacing: d3roTypo.hero.spacing, lineHeight: d3roTypo.hero.line },
|
||||
title: { fontSize: d3roTypo.title.size, color: d3roPalette.text.primary, fontWeight: d3roTypo.title.weight, letterSpacing: d3roTypo.title.spacing, lineHeight: d3roTypo.title.line },
|
||||
value: { fontSize: d3roTypo.value.size, color: d3roPalette.text.primary, fontWeight: d3roTypo.value.weight, letterSpacing: d3roTypo.value.spacing, lineHeight: d3roTypo.value.line },
|
||||
heading: { fontSize: d3roTypo.heading.size, color: d3roPalette.text.primary, fontWeight: d3roTypo.heading.weight, letterSpacing: d3roTypo.heading.spacing, lineHeight: d3roTypo.heading.line },
|
||||
body: { fontSize: d3roTypo.body.size, color: d3roPalette.text.secondary, fontWeight: d3roTypo.body.weight, letterSpacing: d3roTypo.body.spacing, lineHeight: d3roTypo.body.line },
|
||||
compact: { fontSize: d3roTypo.compact.size, color: d3roPalette.text.primary, fontWeight: d3roTypo.compact.weight, letterSpacing: d3roTypo.compact.spacing, lineHeight: d3roTypo.compact.line },
|
||||
small: { fontSize: d3roTypo.small.size, color: d3roPalette.text.inactive, fontWeight: d3roTypo.small.weight, letterSpacing: d3roTypo.small.spacing, lineHeight: d3roTypo.small.line },
|
||||
meta: { fontSize: d3roTypo.meta.size, color: d3roPalette.text.inactive, fontWeight: d3roTypo.meta.weight, letterSpacing: d3roTypo.meta.spacing, lineHeight: d3roTypo.meta.line, textTransform: 'uppercase', mono: true },
|
||||
label: { fontSize: d3roTypo.label.size, color: d3roPalette.text.label, fontWeight: d3roTypo.label.weight, letterSpacing: d3roTypo.label.spacing, lineHeight: d3roTypo.label.line, textTransform: 'uppercase' },
|
||||
dim: { fontSize: d3roTypo.label.size, color: d3roPalette.text.inactive, fontWeight: d3roTypo.body.weight, letterSpacing: d3roTypo.label.spacing, lineHeight: d3roTypo.label.line },
|
||||
engrave: { fontSize: d3roTypo.engrave.size, color: d3roPalette.text.dimLabel, fontWeight: d3roTypo.engrave.weight, letterSpacing: d3roTypo.engrave.spacing, lineHeight: d3roTypo.engrave.line, textTransform: 'uppercase', mono: true },
|
||||
micro: { fontSize: d3roTypo.micro.size, color: d3roPalette.text.dimLabel, fontWeight: d3roTypo.micro.weight, letterSpacing: d3roTypo.micro.spacing, lineHeight: d3roTypo.micro.line, textTransform: 'uppercase', mono: true },
|
||||
nano: { fontSize: d3roTypo.nano.size, color: d3roPalette.text.inactive, fontWeight: d3roTypo.nano.weight, letterSpacing: d3roTypo.nano.spacing, lineHeight: d3roTypo.nano.line, textTransform: 'uppercase', mono: true },
|
||||
}
|
||||
|
||||
interface PhosphorTextProps extends Omit<TypographyProps, 'variant'> {
|
||||
|
|
@ -54,11 +51,10 @@ export function PhosphorText({ variant = 'value', sx, ...props }: PhosphorTextPr
|
|||
<Typography
|
||||
{...props}
|
||||
sx={{
|
||||
fontFamily: d3roFontMono,
|
||||
fontFamily: v.mono ? d3roFontMono : d3roFontSans,
|
||||
fontSize: v.fontSize,
|
||||
fontWeight: v.fontWeight,
|
||||
color: v.color,
|
||||
textShadow: v.glow !== 'none' ? v.glow : 'none',
|
||||
letterSpacing: v.letterSpacing,
|
||||
lineHeight: v.lineHeight,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/PhysicalButton.tsx
|
||||
// 시안 A: 물리 버튼 — 돌출 그림자, 눌림 피드백, 선택 상태
|
||||
// 토큰 적용: d3roShadow, d3roRadius, d3roTypo
|
||||
// v2 "Midnight Glass": 글래스 버튼 — 기본은 반투명 표면 + 헤어라인,
|
||||
// selected 시 액센트 그라디언트 + 글로우 (레퍼런스 프라이머리 버튼)
|
||||
|
||||
import { Button, type ButtonProps } from '@mui/material'
|
||||
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
|
||||
import { d3roPalette, d3roFontSans, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
|
||||
|
||||
interface PhysicalButtonProps extends Omit<ButtonProps, 'variant'> {
|
||||
selected?: boolean
|
||||
|
|
@ -16,27 +16,29 @@ export function PhysicalButton({ selected = false, sx, ...props }: PhysicalButto
|
|||
<Button
|
||||
{...props}
|
||||
sx={{
|
||||
height: 44,
|
||||
bgcolor: selected ? d3roPalette.bg.crtBezel : d3roPalette.bg.chassis,
|
||||
border: 'none',
|
||||
borderRadius: d3roRadius.small,
|
||||
color: selected ? d3roPalette.accent.amber : d3roPalette.text.inactive,
|
||||
fontFamily: d3roFontMono,
|
||||
height: 42,
|
||||
bgcolor: selected ? 'transparent' : d3roPalette.glass.raised,
|
||||
backgroundImage: selected ? d3roPalette.gradient.accent : 'none',
|
||||
border: `1px solid ${selected ? 'transparent' : d3roPalette.glass.hairline}`,
|
||||
borderRadius: d3roRadius.button,
|
||||
color: selected ? '#fff' : d3roPalette.text.secondary,
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: d3roTypo.small.size,
|
||||
fontWeight: d3roTypo.small.weight,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
boxShadow: selected ? d3roShadow.buttonPressed : d3roShadow.buttonRaised,
|
||||
transform: selected ? 'translateY(1px)' : 'none',
|
||||
transition: 'all 0.05s linear',
|
||||
boxShadow: selected ? d3roShadow.glowAccent : 'none',
|
||||
transition: 'filter 0.15s ease, border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease',
|
||||
'&:active': {
|
||||
transform: 'translateY(2px)',
|
||||
boxShadow: d3roShadow.buttonActive,
|
||||
filter: 'brightness(0.92)',
|
||||
},
|
||||
'&:hover': {
|
||||
bgcolor: selected ? d3roPalette.bg.crtBezel : d3roPalette.bg.cardHover,
|
||||
bgcolor: selected ? 'transparent' : d3roPalette.bg.cardHover,
|
||||
borderColor: selected ? 'transparent' : d3roPalette.glass.hairlineStrong,
|
||||
color: selected ? '#fff' : d3roPalette.text.primary,
|
||||
filter: selected ? 'brightness(1.08)' : 'none',
|
||||
},
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: d3roTypo.label.spacing,
|
||||
textTransform: 'none',
|
||||
letterSpacing: d3roTypo.small.spacing,
|
||||
minWidth: 0,
|
||||
...sx,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/ScreenPanel.tsx
|
||||
// 시안 A: CRT 없는 순수 스크린 패널 — 인셋 베젤 + 글래스 반사 + 인광 텍스트용
|
||||
// 레퍼런스의 .display-module > .screen-glass 패턴
|
||||
// v2 "Midnight Glass": 딥 글래스 패널 — 카드 내부의 어두운 정보 표시 영역
|
||||
// (레퍼런스의 히어로 카드 내부 / "현재 백엔드" 내부 패널 패턴)
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { d3roPalette, d3roShadow } from '../../theme'
|
||||
import { d3roPalette, d3roRadius } from '../../theme'
|
||||
|
||||
interface ScreenPanelProps {
|
||||
children: React.ReactNode
|
||||
|
|
@ -15,45 +14,26 @@ interface ScreenPanelProps {
|
|||
}
|
||||
|
||||
export function ScreenPanel({ children, height }: ScreenPanelProps): React.ReactElement {
|
||||
const theme = useTheme()
|
||||
const isLight = theme.palette.mode === 'light'
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
bgcolor: d3roPalette.bg.crtBezel,
|
||||
borderRadius: '12px',
|
||||
boxShadow: d3roShadow.insetDeep,
|
||||
bgcolor: d3roPalette.bg.crtGlass,
|
||||
borderRadius: d3roRadius.inner,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
overflow: 'hidden',
|
||||
height,
|
||||
// 상단 시인
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: d3roPalette.glass.sheen,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* 글래스 배경 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: '2px',
|
||||
borderRadius: '10px',
|
||||
bgcolor: d3roPalette.bg.crtGlass,
|
||||
boxShadow: d3roShadow.screenGlow,
|
||||
// 상단 반사 (레퍼런스의 .screen-glass::after)
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '50%',
|
||||
background: isLight
|
||||
? 'linear-gradient(180deg, rgba(255,255,255,0.40) 0%, rgba(255,255,255,0) 100%)'
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0) 100%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 콘텐츠 오버레이 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
|
|
|
|||
66
packages/ui/src/components/ds/StatRing.tsx
Normal file
66
packages/ui/src/components/ds/StatRing.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/StatRing.tsx
|
||||
// v2 신규: 컬러 그라디언트 링 아이콘 — 스탯 타일의 원형 링 (레퍼런스 4타일 패턴)
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { d3roPalette } from '../../theme'
|
||||
|
||||
type RingColor = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'accent'
|
||||
|
||||
const RING_COLORS: Record<RingColor, { from: string; to: string; glow: string; icon: string }> = {
|
||||
blue: { from: '#22d3ee', to: '#3b82f6', glow: 'rgba(59,130,246,0.35)', icon: d3roPalette.tag.blue },
|
||||
purple: { from: '#8b5cf6', to: '#d946ef', glow: 'rgba(167,139,250,0.35)', icon: d3roPalette.tag.purple },
|
||||
green: { from: '#34d399', to: '#10b981', glow: 'rgba(52,211,153,0.35)', icon: d3roPalette.tag.green },
|
||||
orange: { from: '#fbbf24', to: '#f97316', glow: 'rgba(251,146,60,0.35)', icon: d3roPalette.tag.orange },
|
||||
red: { from: '#f87171', to: '#ef4444', glow: 'rgba(248,113,113,0.35)', icon: d3roPalette.tag.red },
|
||||
accent: { from: 'var(--d3-accent-light)', to: 'var(--d3-accent-dark)', glow: 'var(--d3-accent-dim)', icon: d3roPalette.accent.light },
|
||||
}
|
||||
|
||||
interface StatRingProps {
|
||||
/** 링 색 (기본 blue) */
|
||||
color?: RingColor
|
||||
/** 외경 px (기본 56) */
|
||||
size?: number
|
||||
/** 링 두께 px (기본 2) */
|
||||
thickness?: number
|
||||
/** 중앙 아이콘/콘텐츠 */
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function StatRing({
|
||||
color = 'blue',
|
||||
size = 56,
|
||||
thickness = 2,
|
||||
children,
|
||||
}: StatRingProps): React.ReactElement {
|
||||
const c = RING_COLORS[color]
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
p: `${thickness}px`,
|
||||
background: `conic-gradient(from 210deg, ${c.from}, ${c.to}, ${c.from})`,
|
||||
boxShadow: `0 0 ${size / 3.5}px ${c.glow}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
bgcolor: d3roPalette.bg.crtGlass,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: c.icon,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,3 +10,6 @@ export { PhosphorText } from './PhosphorText'
|
|||
export { MetalDial } from './MetalDial'
|
||||
export { ScreenPanel } from './ScreenPanel'
|
||||
export { ButtonGroup } from './ButtonGroup'
|
||||
// v2 "Midnight Glass" 신규
|
||||
export { GradientWave } from './GradientWave'
|
||||
export { StatRing } from './StatRing'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
// src/renderer/theme.ts
|
||||
// 08-design-system.md SSOT 기반 MUI 테마.
|
||||
// D3RO 디자인 시스템 SSOT — "Midnight Glass" v2.
|
||||
// CSS Custom Properties 기반: d3roPalette가 var()를 사용하여 테마 전환 시 자동 반응.
|
||||
//
|
||||
// v2 디자인 언어 (레퍼런스: 미드나이트 네이비 + 글래스모피즘):
|
||||
// - 딥 네이비 베이스 + 반투명 글래스 카드 + 옅은 헤어라인
|
||||
// - 시안→블루→퍼플→마젠타 그라디언트 (웨이브/프로그레스/로고)
|
||||
// - 컬러 링 스탯 타일, 소프트 글로우, 보더리스 UI
|
||||
// 테마 시스템: 6종(dark/light/nord/solarized/catppuccin/dracula) 전부
|
||||
// 동일 토큰 구조를 가지며 glass/gradient/glow도 테마별로 파생된다.
|
||||
|
||||
import { createTheme, type Theme } from '@mui/material/styles'
|
||||
import type { ThemeMode } from '@d3ro/core/types'
|
||||
|
|
@ -58,120 +65,268 @@ interface RawTheme {
|
|||
crtPhosphor: string
|
||||
crtPhosphorDim: string
|
||||
}
|
||||
/** v2: 글래스모피즘 표면 */
|
||||
glass: {
|
||||
/** 카드 표면 (반투명) */
|
||||
surface: string
|
||||
/** 카드 내부 상승 표면 */
|
||||
raised: string
|
||||
/** 헤어라인 보더 */
|
||||
hairline: string
|
||||
hairlineStrong: string
|
||||
/** 카드 상단 시인(광택) 오버레이 */
|
||||
sheen: string
|
||||
/** backdrop-filter blur 반경 */
|
||||
blur: string
|
||||
}
|
||||
/** v2: 그라디언트 */
|
||||
gradient: {
|
||||
/** 프라이머리 버튼/액티브 요소 */
|
||||
accent: string
|
||||
/** 로고/브랜드 텍스트 */
|
||||
logo: string
|
||||
/** 프로그레스 바 (시안→블루) */
|
||||
bar: string
|
||||
/** 프로그레스 바 대체 (퍼플→마젠타) */
|
||||
barAlt: string
|
||||
/** 웨이브폼 컬러 스톱 4개 (canvas에서 getComputedStyle로 읽음) */
|
||||
wave1: string
|
||||
wave2: string
|
||||
wave3: string
|
||||
wave4: string
|
||||
}
|
||||
/** v2: 글로우 */
|
||||
glow: {
|
||||
/** 프라이머리 버튼/액티브 글로우 */
|
||||
accent: string
|
||||
soft: string
|
||||
}
|
||||
}
|
||||
|
||||
// ── 원시 색상값 (raw values) ────────────────────────────────────
|
||||
// CSS 변수에 주입되는 실제 색상값. 컴포넌트에서 직접 사용하지 않는다.
|
||||
const RAW: Record<ThemeKey, RawTheme> = {
|
||||
// ── Midnight (기본 다크) — 레퍼런스 시안 ────────────────────
|
||||
dark: {
|
||||
bg: { app: '#19191b', card: '#242427', cardHover: '#2a2a2d', elevated: '#2e2e32', input: '#1e1e21', sidebar: '#1e1f21', inset: '#1b1c1e', chassis: '#242528', crtBezel: '#1a1a1c', crtGlass: '#050605' },
|
||||
text: { primary: '#ffffff', secondary: '#8e8e93', label: '#7c7c82', disabled: '#4a4a4e', engraving: '#1a1a1c', inactive: '#77797c', dimLabel: '#5c2615', muted: '#3a3b3f', hover: '#aaaaaa' },
|
||||
border: { subtle: 'rgba(255,255,255,0.04)', default: 'rgba(255,255,255,0.08)', strong: 'rgba(255,255,255,0.12)' },
|
||||
bg: { app: '#0a0e1c', card: '#111a30', cardHover: '#152039', elevated: '#1a2540', input: '#0d1526', sidebar: '#0b101f', inset: '#0a111f', chassis: '#111a30', crtBezel: '#0d1424', crtGlass: '#070b16' },
|
||||
text: { primary: '#eef2fb', secondary: '#93a4c8', label: '#67789e', disabled: '#3c4763', engraving: '#0a0e1c', inactive: '#7385ab', dimLabel: '#4e5f85', muted: '#2b3550', hover: '#c9d5f2' },
|
||||
border: { subtle: 'rgba(148,180,255,0.07)', default: 'rgba(148,180,255,0.12)', strong: 'rgba(148,180,255,0.20)' },
|
||||
shadow: {
|
||||
card: '0 8px 30px rgba(0,0,0,0.3)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
chassis: '0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 #111111, 0 13px 4px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255,255,255,0.15), inset 0 -1px 2px rgba(0,0,0,0.4)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.05)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.9), inset 0 0 0 1px #000, 0 1px 1px rgba(255,255,255,0.1)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.1), inset 0 -1px 2px rgba(0,0,0,0.2)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #000',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.8)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
card: '0 12px 40px rgba(3,7,18,0.5)',
|
||||
buttonBase: '0 1px 2px rgba(3,7,18,0.4)',
|
||||
chassis: '0 24px 80px rgba(3,7,18,0.6)',
|
||||
inset: 'inset 0 1px 4px rgba(3,7,18,0.5)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(3,7,18,0.7)',
|
||||
buttonRaised: '0 2px 8px rgba(3,7,18,0.35), inset 0 1px 0 rgba(148,180,255,0.06)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(3,7,18,0.6)',
|
||||
screenGlow: 'inset 0 0 24px rgba(3,7,18,0.6)',
|
||||
tooltip: '0 8px 24px rgba(3,7,18,0.5)',
|
||||
},
|
||||
accent: { main: '#3b82f6', dim: 'rgba(59,130,246,0.14)', glow: 'rgba(59,130,246,0.5)', light: '#60a5fa', dark: '#2563eb', crtPhosphor: '#60a5fa', crtPhosphorDim: '#3564b4' },
|
||||
glass: {
|
||||
surface: 'rgba(17,26,48,0.62)',
|
||||
raised: 'rgba(26,38,68,0.55)',
|
||||
hairline: 'rgba(148,180,255,0.10)',
|
||||
hairlineStrong: 'rgba(148,180,255,0.18)',
|
||||
sheen: 'linear-gradient(180deg, rgba(148,180,255,0.06) 0%, rgba(148,180,255,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 55%, #60a5fa 100%)',
|
||||
logo: 'linear-gradient(90deg, #60a5fa 0%, #818cf8 100%)',
|
||||
bar: 'linear-gradient(90deg, #22d3ee 0%, #3b82f6 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #8b5cf6 0%, #d946ef 100%)',
|
||||
wave1: '#22d3ee', wave2: '#3b82f6', wave3: '#8b5cf6', wave4: '#e879f9',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(59,130,246,0.35), 0 8px 24px rgba(37,99,235,0.35)',
|
||||
soft: '0 0 16px rgba(59,130,246,0.25)',
|
||||
},
|
||||
accent: { main: '#f25b29', dim: 'rgba(242,91,41,0.15)', glow: 'rgba(242,91,41,0.6)', light: '#ff7a4d', dark: '#c44a22', crtPhosphor: '#f25b29', crtPhosphorDim: '#c44a22' },
|
||||
},
|
||||
// ── Light — 블루 액센트 유지, 밝은 글래스 ────────────────────
|
||||
light: {
|
||||
bg: { app: '#f5f5f7', card: '#ffffff', cardHover: '#f7f7f9', elevated: '#f0f0f2', input: '#ffffff', sidebar: '#eeeef0', inset: '#e8e8ea', chassis: '#e2e2e5', crtBezel: '#d5d5d8', crtGlass: '#f0f0f2' },
|
||||
text: { primary: '#1a1a1c', secondary: '#6e6e73', label: '#8e8e93', disabled: '#c7c7cc', engraving: '#d0d0d3', inactive: '#8e8e93', dimLabel: '#b07040', muted: '#b0b0b4', hover: '#555555' },
|
||||
border: { subtle: 'rgba(0,0,0,0.06)', default: 'rgba(0,0,0,0.10)', strong: 'rgba(0,0,0,0.16)' },
|
||||
bg: { app: '#f2f5fb', card: '#ffffff', cardHover: '#f7f9fd', elevated: '#eef2f9', input: '#ffffff', sidebar: '#e9eef7', inset: '#e4e9f2', chassis: '#ffffff', crtBezel: '#dde3ee', crtGlass: '#eef2f9' },
|
||||
text: { primary: '#111827', secondary: '#4b5a75', label: '#6b7a95', disabled: '#b5bfd1', engraving: '#dde3ee', inactive: '#8593ab', dimLabel: '#5a6f96', muted: '#c3cddd', hover: '#2b3a55' },
|
||||
border: { subtle: 'rgba(30,58,138,0.07)', default: 'rgba(30,58,138,0.12)', strong: 'rgba(30,58,138,0.20)' },
|
||||
shadow: {
|
||||
card: '0 4px 20px rgba(0,0,0,0.06)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.8)',
|
||||
chassis: '0 40px 80px -20px rgba(0,0,0,0.08), 0 12px 0 #d5d5d8, 0 13px 4px rgba(0,0,0,0.06), inset 0 1px 1px rgba(255,255,255,0.8), inset 0 -1px 2px rgba(0,0,0,0.04)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.08), 0 1px 1px rgba(255,255,255,0.6)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.12), inset 0 0 0 1px rgba(0,0,0,0.06), 0 1px 1px rgba(255,255,255,0.8)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.08), inset 0 1px 1px rgba(255,255,255,0.8), inset 0 -1px 2px rgba(0,0,0,0.04)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.12), inset 0 0 0 1px rgba(0,0,0,0.06)',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.06)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.08)',
|
||||
card: '0 8px 30px rgba(30,58,138,0.08)',
|
||||
buttonBase: '0 1px 2px rgba(30,58,138,0.08)',
|
||||
chassis: '0 24px 80px rgba(30,58,138,0.10)',
|
||||
inset: 'inset 0 1px 4px rgba(30,58,138,0.06)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(30,58,138,0.10)',
|
||||
buttonRaised: '0 2px 8px rgba(30,58,138,0.08), inset 0 1px 0 rgba(255,255,255,0.8)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(30,58,138,0.10)',
|
||||
screenGlow: 'inset 0 0 24px rgba(30,58,138,0.05)',
|
||||
tooltip: '0 8px 24px rgba(30,58,138,0.10)',
|
||||
},
|
||||
accent: { main: '#2563eb', dim: 'rgba(37,99,235,0.10)', glow: 'rgba(37,99,235,0.35)', light: '#3b82f6', dark: '#1d4ed8', crtPhosphor: '#2563eb', crtPhosphorDim: '#6f96e8' },
|
||||
glass: {
|
||||
surface: 'rgba(255,255,255,0.72)',
|
||||
raised: 'rgba(255,255,255,0.85)',
|
||||
hairline: 'rgba(30,58,138,0.10)',
|
||||
hairlineStrong: 'rgba(30,58,138,0.18)',
|
||||
sheen: 'linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #1d4ed8 0%, #2563eb 55%, #3b82f6 100%)',
|
||||
logo: 'linear-gradient(90deg, #2563eb 0%, #6366f1 100%)',
|
||||
bar: 'linear-gradient(90deg, #06b6d4 0%, #2563eb 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #7c3aed 0%, #c026d3 100%)',
|
||||
wave1: '#06b6d4', wave2: '#2563eb', wave3: '#7c3aed', wave4: '#d946ef',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(37,99,235,0.25), 0 8px 24px rgba(37,99,235,0.25)',
|
||||
soft: '0 0 16px rgba(37,99,235,0.18)',
|
||||
},
|
||||
accent: { main: '#f25b29', dim: 'rgba(242,91,41,0.15)', glow: 'rgba(242,91,41,0.6)', light: '#ff7a4d', dark: '#c44a22', crtPhosphor: '#f25b29', crtPhosphorDim: '#c44a22' },
|
||||
},
|
||||
// ── Nord (https://www.nordtheme.com/) ──────────────────────
|
||||
nord: {
|
||||
bg: { app: '#2e3440', card: '#3b4252', cardHover: '#434c5e', elevated: '#3b4252', input: '#3b4252', sidebar: '#2e3440', inset: '#2e3440', chassis: '#3b4252', crtBezel: '#2e3440', crtGlass: '#242831' },
|
||||
text: { primary: '#eceff4', secondary: '#d8dee9', label: '#adb5c7', disabled: '#4c566a', engraving: '#2e3440', inactive: '#7b88a1', dimLabel: '#5e81ac', muted: '#434c5e', hover: '#e5e9f0' },
|
||||
bg: { app: '#242933', card: '#2e3440', cardHover: '#343b49', elevated: '#3b4252', input: '#2a303c', sidebar: '#272c37', inset: '#232830', chassis: '#2e3440', crtBezel: '#272c37', crtGlass: '#1f242d' },
|
||||
text: { primary: '#eceff4', secondary: '#aeb8cc', label: '#8b96ad', disabled: '#4c566a', engraving: '#242933', inactive: '#7b88a1', dimLabel: '#5e81ac', muted: '#434c5e', hover: '#e5e9f0' },
|
||||
border: { subtle: 'rgba(216,222,233,0.06)', default: 'rgba(216,222,233,0.10)', strong: 'rgba(216,222,233,0.16)' },
|
||||
shadow: {
|
||||
card: '0 8px 30px rgba(0,0,0,0.35)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
chassis: '0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 #242831, 0 13px 4px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255,255,255,0.1), inset 0 -1px 2px rgba(0,0,0,0.4)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.5), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.8), inset 0 0 0 1px #242831, 0 1px 1px rgba(255,255,255,0.08)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.35), inset 0 1px 1px rgba(255,255,255,0.08), inset 0 -1px 2px rgba(0,0,0,0.2)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.7), inset 0 0 0 1px #242831',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.6)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
card: '0 12px 40px rgba(10,13,18,0.45)',
|
||||
buttonBase: '0 1px 2px rgba(10,13,18,0.4)',
|
||||
chassis: '0 24px 80px rgba(10,13,18,0.55)',
|
||||
inset: 'inset 0 1px 4px rgba(10,13,18,0.45)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(10,13,18,0.65)',
|
||||
buttonRaised: '0 2px 8px rgba(10,13,18,0.3), inset 0 1px 0 rgba(216,222,233,0.06)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(10,13,18,0.55)',
|
||||
screenGlow: 'inset 0 0 24px rgba(10,13,18,0.5)',
|
||||
tooltip: '0 8px 24px rgba(10,13,18,0.45)',
|
||||
},
|
||||
accent: { main: '#88c0d0', dim: 'rgba(136,192,208,0.14)', glow: 'rgba(136,192,208,0.5)', light: '#a3d0de', dark: '#6aacbe', crtPhosphor: '#88c0d0', crtPhosphorDim: '#5e9daf' },
|
||||
glass: {
|
||||
surface: 'rgba(46,52,64,0.62)',
|
||||
raised: 'rgba(59,66,82,0.55)',
|
||||
hairline: 'rgba(216,222,233,0.09)',
|
||||
hairlineStrong: 'rgba(216,222,233,0.16)',
|
||||
sheen: 'linear-gradient(180deg, rgba(216,222,233,0.05) 0%, rgba(216,222,233,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #5e81ac 0%, #81a1c1 55%, #88c0d0 100%)',
|
||||
logo: 'linear-gradient(90deg, #88c0d0 0%, #81a1c1 100%)',
|
||||
bar: 'linear-gradient(90deg, #8fbcbb 0%, #5e81ac 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #b48ead 0%, #d08770 100%)',
|
||||
wave1: '#8fbcbb', wave2: '#88c0d0', wave3: '#81a1c1', wave4: '#b48ead',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(136,192,208,0.3), 0 8px 24px rgba(94,129,172,0.35)',
|
||||
soft: '0 0 16px rgba(136,192,208,0.22)',
|
||||
},
|
||||
// Nord Frost: #88c0d0 (차가운 시안)
|
||||
accent: { main: '#88c0d0', dim: 'rgba(136,192,208,0.15)', glow: 'rgba(136,192,208,0.6)', light: '#a3d0de', dark: '#6aacbe', crtPhosphor: '#88c0d0', crtPhosphorDim: '#5e9daf' },
|
||||
},
|
||||
// ── Solarized Dark (https://ethanschoonover.com/solarized/) ─
|
||||
solarized: {
|
||||
bg: { app: '#002b36', card: '#073642', cardHover: '#0d4250', elevated: '#073642', input: '#073642', sidebar: '#002b36', inset: '#001f28', chassis: '#073642', crtBezel: '#002b36', crtGlass: '#001f28' },
|
||||
text: { primary: '#eee8d5', secondary: '#93a1a1', label: '#839496', disabled: '#586e75', engraving: '#002b36', inactive: '#657b83', dimLabel: '#7a6c2e', muted: '#073642', hover: '#b2c0bf' },
|
||||
bg: { app: '#00212b', card: '#073642', cardHover: '#0a3e4c', elevated: '#0d4250', input: '#03303c', sidebar: '#012731', inset: '#001f28', chassis: '#073642', crtBezel: '#012731', crtGlass: '#001b22' },
|
||||
text: { primary: '#eee8d5', secondary: '#93a1a1', label: '#839496', disabled: '#586e75', engraving: '#00212b', inactive: '#657b83', dimLabel: '#7a6c2e', muted: '#073642', hover: '#b2c0bf' },
|
||||
border: { subtle: 'rgba(131,148,150,0.08)', default: 'rgba(131,148,150,0.13)', strong: 'rgba(131,148,150,0.20)' },
|
||||
shadow: {
|
||||
card: '0 8px 30px rgba(0,0,0,0.4)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05)',
|
||||
chassis: '0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 #001f28, 0 13px 4px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255,255,255,0.08), inset 0 -1px 2px rgba(0,0,0,0.4)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.9), inset 0 0 0 1px #001f28, 0 1px 1px rgba(255,255,255,0.06)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.06), inset 0 -1px 2px rgba(0,0,0,0.2)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #001f28',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.7)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
card: '0 12px 40px rgba(0,10,13,0.5)',
|
||||
buttonBase: '0 1px 2px rgba(0,10,13,0.45)',
|
||||
chassis: '0 24px 80px rgba(0,10,13,0.6)',
|
||||
inset: 'inset 0 1px 4px rgba(0,10,13,0.5)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(0,10,13,0.7)',
|
||||
buttonRaised: '0 2px 8px rgba(0,10,13,0.35), inset 0 1px 0 rgba(238,232,213,0.05)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,10,13,0.6)',
|
||||
screenGlow: 'inset 0 0 24px rgba(0,10,13,0.55)',
|
||||
tooltip: '0 8px 24px rgba(0,10,13,0.5)',
|
||||
},
|
||||
accent: { main: '#b58900', dim: 'rgba(181,137,0,0.14)', glow: 'rgba(181,137,0,0.5)', light: '#d4a017', dark: '#8a6800', crtPhosphor: '#b58900', crtPhosphorDim: '#8a6800' },
|
||||
glass: {
|
||||
surface: 'rgba(7,54,66,0.62)',
|
||||
raised: 'rgba(13,66,80,0.55)',
|
||||
hairline: 'rgba(131,148,150,0.11)',
|
||||
hairlineStrong: 'rgba(131,148,150,0.19)',
|
||||
sheen: 'linear-gradient(180deg, rgba(238,232,213,0.04) 0%, rgba(238,232,213,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #8a6800 0%, #b58900 55%, #d4a017 100%)',
|
||||
logo: 'linear-gradient(90deg, #d4a017 0%, #cb4b16 100%)',
|
||||
bar: 'linear-gradient(90deg, #2aa198 0%, #268bd2 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #6c71c4 0%, #d33682 100%)',
|
||||
wave1: '#2aa198', wave2: '#268bd2', wave3: '#6c71c4', wave4: '#d33682',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(181,137,0,0.3), 0 8px 24px rgba(181,137,0,0.3)',
|
||||
soft: '0 0 16px rgba(181,137,0,0.22)',
|
||||
},
|
||||
// Solarized yellow: #b58900 (따뜻한 골드)
|
||||
accent: { main: '#b58900', dim: 'rgba(181,137,0,0.15)', glow: 'rgba(181,137,0,0.6)', light: '#d4a017', dark: '#8a6800', crtPhosphor: '#b58900', crtPhosphorDim: '#8a6800' },
|
||||
},
|
||||
// ── Catppuccin Mocha (https://catppuccin.com/) ──────────────
|
||||
catppuccin: {
|
||||
bg: { app: '#1e1e2e', card: '#313244', cardHover: '#3a3a54', elevated: '#313244', input: '#181825', sidebar: '#181825', inset: '#11111b', chassis: '#313244', crtBezel: '#1e1e2e', crtGlass: '#11111b' },
|
||||
text: { primary: '#cdd6f4', secondary: '#bac2de', label: '#a6adc8', disabled: '#585b70', engraving: '#11111b', inactive: '#7f849c', dimLabel: '#585b70', muted: '#313244', hover: '#e6e9f8' },
|
||||
border: { subtle: 'rgba(205,214,244,0.04)', default: 'rgba(205,214,244,0.08)', strong: 'rgba(205,214,244,0.14)' },
|
||||
bg: { app: '#181825', card: '#1e1e2e', cardHover: '#242438', elevated: '#313244', input: '#191926', sidebar: '#161622', inset: '#11111b', chassis: '#1e1e2e', crtBezel: '#181825', crtGlass: '#0f0f18' },
|
||||
text: { primary: '#cdd6f4', secondary: '#a8b2d8', label: '#8a94bc', disabled: '#585b70', engraving: '#11111b', inactive: '#7f849c', dimLabel: '#585b70', muted: '#313244', hover: '#e6e9f8' },
|
||||
border: { subtle: 'rgba(205,214,244,0.05)', default: 'rgba(205,214,244,0.09)', strong: 'rgba(205,214,244,0.15)' },
|
||||
shadow: {
|
||||
card: '0 8px 30px rgba(0,0,0,0.4)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05)',
|
||||
chassis: '0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 #11111b, 0 13px 4px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255,255,255,0.08), inset 0 -1px 2px rgba(0,0,0,0.4)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.9), inset 0 0 0 1px #11111b, 0 1px 1px rgba(255,255,255,0.06)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.06), inset 0 -1px 2px rgba(0,0,0,0.2)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #11111b',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.7)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
card: '0 12px 40px rgba(5,5,12,0.5)',
|
||||
buttonBase: '0 1px 2px rgba(5,5,12,0.45)',
|
||||
chassis: '0 24px 80px rgba(5,5,12,0.6)',
|
||||
inset: 'inset 0 1px 4px rgba(5,5,12,0.5)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(5,5,12,0.7)',
|
||||
buttonRaised: '0 2px 8px rgba(5,5,12,0.35), inset 0 1px 0 rgba(205,214,244,0.05)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(5,5,12,0.6)',
|
||||
screenGlow: 'inset 0 0 24px rgba(5,5,12,0.55)',
|
||||
tooltip: '0 8px 24px rgba(5,5,12,0.5)',
|
||||
},
|
||||
accent: { main: '#cba6f7', dim: 'rgba(203,166,247,0.14)', glow: 'rgba(203,166,247,0.5)', light: '#dfc0ff', dark: '#a67fd4', crtPhosphor: '#cba6f7', crtPhosphorDim: '#a67fd4' },
|
||||
glass: {
|
||||
surface: 'rgba(30,30,46,0.62)',
|
||||
raised: 'rgba(49,50,68,0.55)',
|
||||
hairline: 'rgba(205,214,244,0.08)',
|
||||
hairlineStrong: 'rgba(205,214,244,0.15)',
|
||||
sheen: 'linear-gradient(180deg, rgba(205,214,244,0.05) 0%, rgba(205,214,244,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #a67fd4 0%, #cba6f7 55%, #f5c2e7 100%)',
|
||||
logo: 'linear-gradient(90deg, #89b4fa 0%, #cba6f7 100%)',
|
||||
bar: 'linear-gradient(90deg, #89dceb 0%, #89b4fa 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #cba6f7 0%, #f5c2e7 100%)',
|
||||
wave1: '#89dceb', wave2: '#89b4fa', wave3: '#cba6f7', wave4: '#f5c2e7',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(203,166,247,0.3), 0 8px 24px rgba(203,166,247,0.3)',
|
||||
soft: '0 0 16px rgba(203,166,247,0.22)',
|
||||
},
|
||||
// Catppuccin Mauve: #cba6f7 (보라 파스텔)
|
||||
accent: { main: '#cba6f7', dim: 'rgba(203,166,247,0.15)', glow: 'rgba(203,166,247,0.6)', light: '#dfc0ff', dark: '#a67fd4', crtPhosphor: '#cba6f7', crtPhosphorDim: '#a67fd4' },
|
||||
},
|
||||
// ── Dracula (https://draculatheme.com/) ─────────────────────
|
||||
dracula: {
|
||||
bg: { app: '#282a36', card: '#44475a', cardHover: '#4f5266', elevated: '#383a4a', input: '#383a4a', sidebar: '#21222c', inset: '#21222c', chassis: '#44475a', crtBezel: '#282a36', crtGlass: '#1e2029' },
|
||||
text: { primary: '#f8f8f2', secondary: '#a9b0d0', label: '#8891b5', disabled: '#6272a4', engraving: '#21222c', inactive: '#6272a4', dimLabel: '#6272a4', muted: '#44475a', hover: '#ffffff' },
|
||||
border: { subtle: 'rgba(248,248,242,0.04)', default: 'rgba(248,248,242,0.08)', strong: 'rgba(248,248,242,0.14)' },
|
||||
bg: { app: '#1e2029', card: '#282a36', cardHover: '#2e3040', elevated: '#383a4a', input: '#232530', sidebar: '#21222c', inset: '#1b1c25', chassis: '#282a36', crtBezel: '#21222c', crtGlass: '#181a21' },
|
||||
text: { primary: '#f8f8f2', secondary: '#b6bdd9', label: '#8891b5', disabled: '#6272a4', engraving: '#21222c', inactive: '#6272a4', dimLabel: '#6272a4', muted: '#44475a', hover: '#ffffff' },
|
||||
border: { subtle: 'rgba(248,248,242,0.05)', default: 'rgba(248,248,242,0.09)', strong: 'rgba(248,248,242,0.15)' },
|
||||
shadow: {
|
||||
card: '0 8px 30px rgba(0,0,0,0.4)',
|
||||
buttonBase: '0 2px 0 rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
chassis: '0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 #1e2029, 0 13px 4px rgba(0,0,0,0.5), inset 0 1px 1px rgba(255,255,255,0.1), inset 0 -1px 2px rgba(0,0,0,0.4)',
|
||||
inset: 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.04)',
|
||||
insetDeep: 'inset 0 4px 12px rgba(0,0,0,0.9), inset 0 0 0 1px #1e2029, 0 1px 1px rgba(255,255,255,0.08)',
|
||||
buttonRaised: '0 3px 6px rgba(0,0,0,0.4), inset 0 1px 1px rgba(255,255,255,0.08), inset 0 -1px 2px rgba(0,0,0,0.2)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #1e2029',
|
||||
screenGlow: 'inset 0 0 20px rgba(0,0,0,0.7)',
|
||||
tooltip: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
card: '0 12px 40px rgba(8,9,13,0.5)',
|
||||
buttonBase: '0 1px 2px rgba(8,9,13,0.45)',
|
||||
chassis: '0 24px 80px rgba(8,9,13,0.6)',
|
||||
inset: 'inset 0 1px 4px rgba(8,9,13,0.5)',
|
||||
insetDeep: 'inset 0 2px 8px rgba(8,9,13,0.7)',
|
||||
buttonRaised: '0 2px 8px rgba(8,9,13,0.35), inset 0 1px 0 rgba(248,248,242,0.06)',
|
||||
buttonPressed: 'inset 0 2px 6px rgba(8,9,13,0.6)',
|
||||
screenGlow: 'inset 0 0 24px rgba(8,9,13,0.55)',
|
||||
tooltip: '0 8px 24px rgba(8,9,13,0.5)',
|
||||
},
|
||||
accent: { main: '#bd93f9', dim: 'rgba(189,147,249,0.14)', glow: 'rgba(189,147,249,0.5)', light: '#d4b8ff', dark: '#9a70e0', crtPhosphor: '#bd93f9', crtPhosphorDim: '#9a70e0' },
|
||||
glass: {
|
||||
surface: 'rgba(40,42,54,0.62)',
|
||||
raised: 'rgba(56,58,74,0.55)',
|
||||
hairline: 'rgba(248,248,242,0.08)',
|
||||
hairlineStrong: 'rgba(248,248,242,0.15)',
|
||||
sheen: 'linear-gradient(180deg, rgba(248,248,242,0.05) 0%, rgba(248,248,242,0) 42%)',
|
||||
blur: '24px',
|
||||
},
|
||||
gradient: {
|
||||
accent: 'linear-gradient(135deg, #9a70e0 0%, #bd93f9 55%, #ff79c6 100%)',
|
||||
logo: 'linear-gradient(90deg, #8be9fd 0%, #bd93f9 100%)',
|
||||
bar: 'linear-gradient(90deg, #8be9fd 0%, #bd93f9 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #ff79c6 0%, #ffb86c 100%)',
|
||||
wave1: '#8be9fd', wave2: '#bd93f9', wave3: '#ff79c6', wave4: '#ffb86c',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(189,147,249,0.3), 0 8px 24px rgba(189,147,249,0.3)',
|
||||
soft: '0 0 16px rgba(189,147,249,0.22)',
|
||||
},
|
||||
// Dracula Purple: #bd93f9 (시그니처 퍼플)
|
||||
accent: { main: '#bd93f9', dim: 'rgba(189,147,249,0.15)', glow: 'rgba(189,147,249,0.6)', light: '#d4b8ff', dark: '#9a70e0', crtPhosphor: '#bd93f9', crtPhosphorDim: '#9a70e0' },
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -191,24 +346,58 @@ export const d3roPalette = {
|
|||
crtGlass: 'var(--d3-bg-crtGlass)',
|
||||
},
|
||||
accent: {
|
||||
/** 프라이머리 액센트 (v2 별칭 — amber와 동일 var) */
|
||||
main: 'var(--d3-accent-main)',
|
||||
light: 'var(--d3-accent-light)',
|
||||
dark: 'var(--d3-accent-dark)',
|
||||
dim: 'var(--d3-accent-dim)',
|
||||
/** @deprecated v2에서 main으로 개명 — 기존 사용처 호환용 별칭 */
|
||||
amber: 'var(--d3-accent-main)',
|
||||
amberDim: 'var(--d3-accent-dim)',
|
||||
amberGlow: 'var(--d3-accent-glow)',
|
||||
glow: 'var(--d3-accent-glow)',
|
||||
},
|
||||
/** v2: 글래스 표면 토큰 */
|
||||
glass: {
|
||||
surface: 'var(--d3-glass-surface)',
|
||||
raised: 'var(--d3-glass-raised)',
|
||||
hairline: 'var(--d3-glass-hairline)',
|
||||
hairlineStrong: 'var(--d3-glass-hairlineStrong)',
|
||||
sheen: 'var(--d3-glass-sheen)',
|
||||
blur: 'var(--d3-glass-blur)',
|
||||
},
|
||||
/** v2: 그라디언트 토큰 */
|
||||
gradient: {
|
||||
accent: 'var(--d3-gradient-accent)',
|
||||
logo: 'var(--d3-gradient-logo)',
|
||||
bar: 'var(--d3-gradient-bar)',
|
||||
barAlt: 'var(--d3-gradient-barAlt)',
|
||||
wave1: 'var(--d3-gradient-wave1)',
|
||||
wave2: 'var(--d3-gradient-wave2)',
|
||||
wave3: 'var(--d3-gradient-wave3)',
|
||||
wave4: 'var(--d3-gradient-wave4)',
|
||||
},
|
||||
/** v2: 글로우 토큰 */
|
||||
glow: {
|
||||
accent: 'var(--d3-glow-accent)',
|
||||
soft: 'var(--d3-glow-soft)',
|
||||
},
|
||||
tag: {
|
||||
purple: '#b854f5',
|
||||
purpleBg: 'rgba(184, 84, 245, 0.12)',
|
||||
orange: '#f59e0b',
|
||||
orangeBg: 'rgba(245, 158, 11, 0.12)',
|
||||
red: '#ef4444',
|
||||
redBg: 'rgba(239, 68, 68, 0.12)',
|
||||
green: '#22c55e',
|
||||
greenBg: 'rgba(34, 197, 94, 0.12)',
|
||||
blue: '#3b82f6',
|
||||
blueBg: 'rgba(59, 130, 246, 0.12)',
|
||||
greenGlow: 'rgba(34, 197, 94, 0.6)',
|
||||
redGlow: 'rgba(239, 68, 68, 0.6)',
|
||||
orangeGlow: 'rgba(245, 158, 11, 0.6)',
|
||||
purple: '#a78bfa',
|
||||
purpleBg: 'rgba(167, 139, 250, 0.12)',
|
||||
orange: '#fb923c',
|
||||
orangeBg: 'rgba(251, 146, 60, 0.12)',
|
||||
red: '#f87171',
|
||||
redBg: 'rgba(248, 113, 113, 0.12)',
|
||||
green: '#34d399',
|
||||
greenBg: 'rgba(52, 211, 153, 0.12)',
|
||||
blue: '#60a5fa',
|
||||
blueBg: 'rgba(96, 165, 250, 0.12)',
|
||||
greenGlow: 'rgba(52, 211, 153, 0.55)',
|
||||
redGlow: 'rgba(248, 113, 113, 0.55)',
|
||||
orangeGlow: 'rgba(251, 146, 60, 0.55)',
|
||||
purpleGlow: 'rgba(167, 139, 250, 0.55)',
|
||||
blueGlow: 'rgba(96, 165, 250, 0.55)',
|
||||
},
|
||||
text: {
|
||||
primary: 'var(--d3-text-primary)',
|
||||
|
|
@ -233,13 +422,13 @@ export const d3roPalette = {
|
|||
bg: 'var(--d3-bg-chassis)',
|
||||
},
|
||||
led: {
|
||||
off: '#111111',
|
||||
off: '#1c2336',
|
||||
},
|
||||
} as const
|
||||
|
||||
export const d3roFontSans = [
|
||||
'-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto',
|
||||
'"Helvetica Neue"', 'Arial', 'sans-serif',
|
||||
'"Pretendard Variable"', 'Pretendard', '-apple-system', 'BlinkMacSystemFont',
|
||||
'"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif',
|
||||
].join(',')
|
||||
|
||||
export const d3roFontMono = [
|
||||
|
|
@ -248,19 +437,20 @@ export const d3roFontMono = [
|
|||
].join(',')
|
||||
|
||||
// ── SSOT: 타이포그래피 토큰 ─────────────────────────────
|
||||
// v2: 클린 산세리프(Pretendard) 기반, 굵은 헤드라인 + 큰 수치
|
||||
export const d3roTypo = {
|
||||
hero: { size: '42px', weight: 300, spacing: '-2px', line: 1 },
|
||||
title: { size: '28px', weight: 300, spacing: '-1px', line: 1.2 },
|
||||
value: { size: '20px', weight: 400, spacing: '0.02em', line: 1 },
|
||||
heading: { size: '16px', weight: 600, spacing: '0.02em', line: 1.4 },
|
||||
body: { size: '14px', weight: 400, spacing: '0.01em', line: 1.5 },
|
||||
compact: { size: '13px', weight: 400, spacing: '0.01em', line: 1.5 },
|
||||
small: { size: '12px', weight: 600, spacing: '0.03em', line: 1.4 },
|
||||
meta: { size: '11px', weight: 600, spacing: '0.05em', line: 1.3 },
|
||||
label: { size: '10px', weight: 700, spacing: '2px', line: 1.2 },
|
||||
engrave: { size: '9px', weight: 700, spacing: '1.5px', line: 1 },
|
||||
micro: { size: '8px', weight: 700, spacing: '1.5px', line: 1 },
|
||||
nano: { size: '7px', weight: 700, spacing: '0.5px', line: 1 },
|
||||
hero: { size: '32px', weight: 700, spacing: '-0.5px', line: 1.2 },
|
||||
title: { size: '24px', weight: 700, spacing: '-0.3px', line: 1.25 },
|
||||
value: { size: '20px', weight: 600, spacing: '0', line: 1.1 },
|
||||
heading: { size: '16px', weight: 600, spacing: '0', line: 1.4 },
|
||||
body: { size: '14px', weight: 400, spacing: '0', line: 1.55 },
|
||||
compact: { size: '13px', weight: 400, spacing: '0', line: 1.5 },
|
||||
small: { size: '12px', weight: 500, spacing: '0.01em', line: 1.4 },
|
||||
meta: { size: '11px', weight: 600, spacing: '0.04em', line: 1.3 },
|
||||
label: { size: '11px', weight: 600, spacing: '0.08em', line: 1.2 },
|
||||
engrave: { size: '9px', weight: 700, spacing: '1px', line: 1 },
|
||||
micro: { size: '9px', weight: 700, spacing: '1px', line: 1 },
|
||||
nano: { size: '8px', weight: 700, spacing: '0.5px', line: 1 },
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
@ -294,16 +484,19 @@ export const d3roShadow = {
|
|||
dialog: 'var(--d3-shadow-card)',
|
||||
tooltip: 'var(--d3-shadow-tooltip)',
|
||||
screenGlow: 'var(--d3-shadow-screenGlow)',
|
||||
/** v2: 액센트 글로우 */
|
||||
glowAccent: 'var(--d3-glow-accent)',
|
||||
glowSoft: 'var(--d3-glow-soft)',
|
||||
} as const
|
||||
|
||||
// ── SSOT: 반경 토큰 ────────────────────────────────────
|
||||
export const d3roRadius = {
|
||||
outer: '24px',
|
||||
card: '22px',
|
||||
outer: '20px',
|
||||
card: '16px',
|
||||
inner: '12px',
|
||||
button: '8px',
|
||||
small: '6px',
|
||||
xs: '4px',
|
||||
button: '10px',
|
||||
small: '8px',
|
||||
xs: '6px',
|
||||
pill: '999px',
|
||||
} as const
|
||||
|
||||
|
|
@ -349,6 +542,22 @@ function buildCssVars(key: ThemeKey): Record<string, string> {
|
|||
'--d3-accent-dark': r.accent.dark,
|
||||
'--d3-accent-crtPhosphor': r.accent.crtPhosphor,
|
||||
'--d3-accent-crtPhosphorDim': r.accent.crtPhosphorDim,
|
||||
'--d3-glass-surface': r.glass.surface,
|
||||
'--d3-glass-raised': r.glass.raised,
|
||||
'--d3-glass-hairline': r.glass.hairline,
|
||||
'--d3-glass-hairlineStrong': r.glass.hairlineStrong,
|
||||
'--d3-glass-sheen': r.glass.sheen,
|
||||
'--d3-glass-blur': r.glass.blur,
|
||||
'--d3-gradient-accent': r.gradient.accent,
|
||||
'--d3-gradient-logo': r.gradient.logo,
|
||||
'--d3-gradient-bar': r.gradient.bar,
|
||||
'--d3-gradient-barAlt': r.gradient.barAlt,
|
||||
'--d3-gradient-wave1': r.gradient.wave1,
|
||||
'--d3-gradient-wave2': r.gradient.wave2,
|
||||
'--d3-gradient-wave3': r.gradient.wave3,
|
||||
'--d3-gradient-wave4': r.gradient.wave4,
|
||||
'--d3-glow-accent': r.glow.accent,
|
||||
'--d3-glow-soft': r.glow.soft,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +578,7 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
palette: {
|
||||
mode: muiMode,
|
||||
primary: { main: accentMain, light: accentLight, dark: accentDark, contrastText: '#fff' },
|
||||
secondary: { main: d3roPalette.tag.purple, light: '#d084ff', dark: '#8a3cc4' },
|
||||
secondary: { main: d3roPalette.tag.purple, light: '#c4b5fd', dark: '#8b5cf6' },
|
||||
error: { main: d3roPalette.tag.red },
|
||||
warning: { main: d3roPalette.tag.orange },
|
||||
success: { main: d3roPalette.tag.green },
|
||||
|
|
@ -386,15 +595,18 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
body1: { fontSize: '14px', lineHeight: 1.5 },
|
||||
body2: { fontSize: '12px', lineHeight: 1.4 },
|
||||
button: { textTransform: 'none' as const, fontWeight: 600, fontSize: '14px' },
|
||||
caption: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, color: r.text.label },
|
||||
overline: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, lineHeight: 1.2 },
|
||||
caption: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.06em', color: r.text.label },
|
||||
overline: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase' as const, lineHeight: 1.2 },
|
||||
},
|
||||
shape: { borderRadius: 22 },
|
||||
shape: { borderRadius: 16 },
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
':root': cssVars,
|
||||
body: { backgroundColor: r.bg.app, color: r.text.primary },
|
||||
// 보더리스 UI: 네이티브 스크롤바 제거 (레이아웃 폭 불변).
|
||||
// 시각적 스크롤 인디케이터는 OverlayScrollbars(고유 스타일)가 담당.
|
||||
'*::-webkit-scrollbar': { width: 0, height: 0, background: 'transparent' },
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
|
|
@ -402,23 +614,27 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none', fontWeight: 600, borderRadius: 10, padding: '10px 20px',
|
||||
transition: 'transform 0.05s linear, box-shadow 0.05s linear',
|
||||
boxShadow: r.shadow.buttonBase,
|
||||
'&:active': { transform: 'translateY(2px)', boxShadow: 'var(--d3-shadow-buttonPressed)' },
|
||||
transition: 'filter 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease',
|
||||
},
|
||||
containedPrimary: {
|
||||
color: '#fff',
|
||||
'&:hover': { backgroundColor: accentDark },
|
||||
backgroundImage: 'var(--d3-gradient-accent)',
|
||||
boxShadow: 'var(--d3-glow-accent)',
|
||||
'&:hover': { filter: 'brightness(1.1)', boxShadow: 'var(--d3-glow-accent)' },
|
||||
'&:active': { filter: 'brightness(0.95)' },
|
||||
'&.Mui-disabled': { backgroundImage: 'none' },
|
||||
},
|
||||
outlined: {
|
||||
borderColor: r.border.strong,
|
||||
borderColor: 'var(--d3-glass-hairlineStrong)',
|
||||
backgroundColor: 'var(--d3-glass-raised)',
|
||||
color: r.text.primary,
|
||||
'&:hover': { borderColor: accentMain, color: accentMain },
|
||||
'&:hover': { borderColor: accentMain, color: accentLight, backgroundColor: 'var(--d3-glass-raised)' },
|
||||
},
|
||||
containedSecondary: {
|
||||
backgroundColor: r.bg.elevated,
|
||||
color: r.text.primary,
|
||||
'&:hover': { backgroundColor: muiMode === 'dark' ? '#353539' : '#e5e5e7' },
|
||||
border: `1px solid ${r.glass.hairline}`,
|
||||
'&:hover': { backgroundColor: r.bg.cardHover },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -426,42 +642,54 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
defaultProps: { elevation: 0 },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: r.bg.card, borderRadius: 22,
|
||||
borderTop: `1px solid ${r.border.subtle}`,
|
||||
backgroundColor: 'var(--d3-glass-surface)',
|
||||
backdropFilter: 'blur(var(--d3-glass-blur))',
|
||||
borderRadius: 16,
|
||||
border: `1px solid ${r.glass.hairline}`,
|
||||
boxShadow: r.shadow.card,
|
||||
transition: 'background-color 0.2s ease',
|
||||
'&:hover': { backgroundColor: r.bg.cardHover },
|
||||
transition: 'border-color 0.2s ease',
|
||||
'&:hover': { borderColor: r.glass.hairlineStrong },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: { borderRadius: 999, fontWeight: 700, fontSize: '11px', letterSpacing: '0.1em', textTransform: 'uppercase', height: 24 },
|
||||
colorPrimary: { backgroundColor: accentDim, color: accentMain },
|
||||
root: { borderRadius: 999, fontWeight: 600, fontSize: '11px', letterSpacing: '0.04em', height: 24 },
|
||||
colorPrimary: { backgroundColor: accentDim, color: accentLight },
|
||||
colorSecondary: { backgroundColor: d3roPalette.tag.purpleBg, color: d3roPalette.tag.purple },
|
||||
colorSuccess: { backgroundColor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green },
|
||||
colorError: { backgroundColor: d3roPalette.tag.redBg, color: d3roPalette.tag.red },
|
||||
colorWarning: { backgroundColor: d3roPalette.tag.orangeBg, color: d3roPalette.tag.orange },
|
||||
},
|
||||
},
|
||||
MuiDrawer: { styleOverrides: { paper: { width: 240, backgroundColor: r.bg.app, borderRight: `1px solid ${r.border.subtle}` } } },
|
||||
MuiDrawer: { styleOverrides: { paper: { width: 240, backgroundColor: r.bg.sidebar, borderRight: `1px solid ${r.glass.hairline}` } } },
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 10, marginLeft: 8, marginRight: 8,
|
||||
'&.Mui-selected': { backgroundColor: accentDim, color: accentMain, fontWeight: 600, '&:hover': { backgroundColor: accentDim } },
|
||||
borderRadius: 12, marginLeft: 8, marginRight: 8,
|
||||
'&.Mui-selected': { backgroundColor: accentDim, color: accentLight, fontWeight: 600, '&:hover': { backgroundColor: accentDim } },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundColor: r.bg.card, borderRadius: 22,
|
||||
border: `1px solid ${r.border.subtle}`,
|
||||
backgroundColor: r.bg.card, borderRadius: 20,
|
||||
backgroundImage: 'var(--d3-glass-sheen)',
|
||||
border: `1px solid ${r.glass.hairline}`,
|
||||
boxShadow: 'var(--d3-shadow-card)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiBackdrop: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: muiMode === 'dark' ? 'rgba(4,8,20,0.6)' : 'rgba(30,58,138,0.18)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
'&.MuiBackdrop-invisible': { backgroundColor: 'transparent', backdropFilter: 'none' },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
defaultProps: { size: 'small', variant: 'outlined' },
|
||||
styleOverrides: {
|
||||
|
|
@ -469,8 +697,8 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
'& .MuiOutlinedInput-root': {
|
||||
backgroundColor: r.bg.input, borderRadius: 10,
|
||||
color: r.text.primary,
|
||||
'& fieldset': { borderColor: r.border.default },
|
||||
'&:hover fieldset': { borderColor: r.border.strong },
|
||||
'& fieldset': { borderColor: r.glass.hairline },
|
||||
'&:hover fieldset': { borderColor: r.glass.hairlineStrong },
|
||||
'&.Mui-focused fieldset': { borderColor: accentMain },
|
||||
},
|
||||
},
|
||||
|
|
@ -512,11 +740,16 @@ function createD3ROTheme(key: ThemeKey): Theme {
|
|||
MuiTooltip: {
|
||||
defaultProps: { arrow: true },
|
||||
styleOverrides: {
|
||||
tooltip: { backgroundColor: r.bg.elevated, color: r.text.primary, fontSize: '12px', borderRadius: 8, border: `1px solid ${r.border.subtle}` },
|
||||
tooltip: { backgroundColor: r.bg.elevated, color: r.text.primary, fontSize: '12px', borderRadius: 8, border: `1px solid ${r.glass.hairline}` },
|
||||
},
|
||||
},
|
||||
MuiTabs: { styleOverrides: { indicator: { backgroundColor: accentMain, height: 2, borderRadius: 2 } } },
|
||||
MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, fontSize: '14px', '&.Mui-selected': { color: accentLight, fontWeight: 600 } } } },
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: { backgroundImage: 'none' },
|
||||
},
|
||||
},
|
||||
MuiTabs: { styleOverrides: { indicator: { backgroundColor: accentMain } } },
|
||||
MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, fontSize: '14px', '&.Mui-selected': { color: accentMain, fontWeight: 600 } } } },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue