Phase 10~11 전체 구현: 킬러 피처 5종 + 수익화 시스템

Phase 10 킬러 피처:
- MemoService: 태그 CRUD + 마크다운 내보내기 (memo_tags DB)
- VoiceCommandService: 키워드→명령어 매칭, 프리셋 4종
- ScreenContextService: PowerShell 활성 윈도우 + Ctrl+C 선택 텍스트
- ChainService: LLM 명령어 순차 실행 파이프라인
- CaptionService: 6초 청크 연속 전사 + 시스템 오디오 루프백

VoiceModeService 파이프라인 통합:
- 녹음 시작 → 컨텍스트 캡처 → STT → 키워드 매칭 → LLM(체인/컨텍스트 주입) → 삽입

시스템 오디오 캡처:
- setDisplayMediaRequestHandler + audio: 'loopback' (IPC 브릿지)
- electron-audio-loopback 패키지 contextIsolation 호환 불가 → 직접 구현

Phase 11 수익화:
- LicenseService: Free/Pro/Pro+ 3티어, LemonSqueezy API
- Feature Gate: requireFeature/checkFeature/consumeFeature
- 일일 쿼터: Free dictation 20/일, LLM 10/일 (SQLite daily_usage)
- LicenseModal, ProBadge, UpgradePromptModal UI

디자인 보강:
- d3roTypo(13종), d3roShadow(10종), d3roRadius(7종) 토큰 시스템
- ScreenPanel, ButtonGroup DS 컴포넌트 신규
- PhosphorText 4→13종 변형, MetalDial conic-gradient 광택
- 공유 컴포넌트: EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard

기타:
- 자막 핫키 SSOT 전체 연동 (Config→Hotkey→VoiceMode→Caption→Settings)
- StatusBar 자막 LED + 효과음, 자막 로딩 UI
- LLM 상태 이벤트 전파 수정 (폴링 제거 → onStatusChanged)
- 커맨드 팝업 "선택 해제" 항목 추가
This commit is contained in:
Yun Chan 2026-04-05 21:36:09 +09:00
parent 36d77ca224
commit a31f96bbb8
97 changed files with 11853 additions and 1143 deletions

View file

@ -0,0 +1,30 @@
// src/renderer/components/ds/ButtonGroup.tsx
// 시안 A: 인셋 버튼 클러스터 — 레퍼런스의 .button-group 패턴
// 물리 버튼들을 인셋 패널 안에 배치하여 그룹화
import { Box } from '@mui/material'
import { d3roPalette, d3roShadow, d3roRadius } from '../../theme'
interface ButtonGroupProps {
children: React.ReactNode
/** 가로 배치 (기본 세로) */
horizontal?: boolean
}
export function ButtonGroup({ children, horizontal = false }: ButtonGroupProps): React.ReactElement {
return (
<Box
sx={{
bgcolor: d3roPalette.bg.inset,
p: '6px',
borderRadius: d3roRadius.inner,
boxShadow: d3roShadow.inset,
display: 'flex',
flexDirection: horizontal ? 'row' : 'column',
gap: '6px',
}}
>
{children}
</Box>
)
}

View file

@ -3,7 +3,8 @@
import { useRef, useEffect, useCallback } from 'react'
import { Box } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../../theme'
import { useTheme } from '@mui/material/styles'
import { d3roPalette, d3roFontMono, d3roShadow } from '../../theme'
// ── WebGL 유틸 ─────────────────────────────────────────
@ -112,6 +113,8 @@ interface CrtDisplayProps {
frequency?: number
/** 글리치 트리거 (변경 시 글리치 발생) */
glitchTrigger?: number
/** 실시간 오디오 레벨 (0.0~1.0) — 파형 진폭에 반영 */
audioLevel?: number
/** 오버레이 콘텐츠 (인광 텍스트 등) */
children?: React.ReactNode
/** 높이 (기본 280px) */
@ -122,9 +125,12 @@ export function CrtDisplay({
amplitude = 0.1,
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
@ -138,15 +144,20 @@ export function CrtDisplay({
const glitchRef = useRef(0)
const ampRef = useRef(amplitude)
const freqRef = useRef(frequency)
const audioLevelRef = useRef(audioLevel)
const currentAmpRef = useRef(amplitude)
const currentFreqRef = useRef(frequency)
// amplitude/frequency 변경 추적
// amplitude/frequency/audioLevel 변경 추적
useEffect(() => {
ampRef.current = amplitude
freqRef.current = frequency
}, [amplitude, frequency])
useEffect(() => {
audioLevelRef.current = audioLevel
}, [audioLevel])
// 글리치 트리거
useEffect(() => {
if (glitchTrigger > 0) {
@ -160,8 +171,11 @@ export function CrtDisplay({
const { gl, uTime, uGlitch, uAmp, uFreq } = ctx
// audioLevel → amplitude 반영: 기본 amplitude + 오디오 레벨로 증폭
const targetAmp = ampRef.current + audioLevelRef.current * 0.35
// Smoothing
currentAmpRef.current += (ampRef.current - currentAmpRef.current) * 0.1
currentAmpRef.current += (targetAmp - currentAmpRef.current) * 0.15
currentFreqRef.current += (freqRef.current - currentFreqRef.current) * 0.1
glitchRef.current *= 0.85
@ -221,7 +235,7 @@ export function CrtDisplay({
height,
bgcolor: d3roPalette.bg.crtBezel,
borderRadius: '8px',
boxShadow: '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)',
boxShadow: d3roShadow.insetDeep,
overflow: 'hidden',
}}
>
@ -233,12 +247,19 @@ export function CrtDisplay({
borderRadius: '6px',
bgcolor: d3roPalette.bg.crtGlass,
overflow: 'hidden',
boxShadow: 'inset 0 0 20px rgba(0,0,0,0.8)',
boxShadow: d3roShadow.screenGlow,
}}
>
<canvas
ref={canvasRef}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
style={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
// 라이트 모드에서 WebGL 셰이더(다크 전용)를 반전하여 밝은 배경에 어울리게 조정
...(isLight && { filter: 'invert(0.88) hue-rotate(180deg)', opacity: 0.9 }),
}}
/>
{/* Glass reflection */}
@ -246,7 +267,9 @@ export function CrtDisplay({
sx={{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: '50%',
background: 'linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0) 100%)',
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,
}}

View file

@ -2,7 +2,8 @@
// 시안 A: 메탈 섀시 컨테이너 — 노이즈 텍스처, 각인 텍스트, 물리적 존재감
import { Box, Typography } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../../theme'
import { useTheme } from '@mui/material/styles'
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
interface InstrumentPanelProps {
children: React.ReactNode
@ -25,16 +26,15 @@ export function InstrumentPanel({
sx={{
position: 'relative',
bgcolor: d3roPalette.bg.chassis,
borderRadius: '24px',
borderRadius: d3roRadius.outer,
p: 3,
boxShadow:
`0 60px 100px -20px rgba(0,0,0,0.8), 0 12px 0 ${d3roPalette.led.off}, 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)`,
boxShadow: d3roShadow.chassis,
// 메탈 노이즈는 CSS로 시뮬레이션
'&::before': {
content: '""',
position: 'absolute',
inset: 0,
borderRadius: '24px',
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',
@ -57,14 +57,18 @@ 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: '9px',
letterSpacing: '1.5px',
fontSize: d3roTypo.engrave.size,
letterSpacing: d3roTypo.engrave.spacing,
color: d3roPalette.text.engraving,
textShadow: '0 1px 0 rgba(255,255,255,0.08)',
textShadow: isLight
? '0 -1px 0 rgba(0,0,0,0.1)'
: '0 1px 0 rgba(255,255,255,0.08)',
fontWeight: 700,
fontFamily: d3roFontMono,
zIndex: 2,

View file

@ -1,8 +1,9 @@
// src/renderer/components/ds/MetalCard.tsx
// 시안 A+B 융합: 메탈 카드 컨테이너 — 섀시 느낌의 인셋 패널
// 토큰 적용: d3roShadow, d3roRadius
import { Box } from '@mui/material'
import { d3roPalette } from '../../theme'
import { d3roPalette, d3roShadow, d3roRadius } from '../../theme'
interface MetalCardProps {
children: React.ReactNode
@ -14,11 +15,9 @@ export function MetalCard({ children, inset = false }: MetalCardProps): React.Re
<Box
sx={{
bgcolor: inset ? d3roPalette.bg.inset : d3roPalette.bg.card,
borderRadius: inset ? '12px' : '22px',
borderRadius: inset ? d3roRadius.inner : d3roRadius.card,
borderTop: inset ? 'none' : `1px solid ${d3roPalette.border.subtle}`,
boxShadow: inset
? 'inset 0 2px 6px rgba(0,0,0,0.6), 0 1px 1px rgba(255,255,255,0.05)'
: '0 8px 30px rgba(0,0,0,0.3)',
boxShadow: inset ? d3roShadow.inset : d3roShadow.card,
p: inset ? '6px' : 3,
transition: 'background-color 0.2s ease',
'&:hover': inset ? {} : { bgcolor: d3roPalette.bg.cardHover },

View file

@ -1,8 +1,10 @@
// src/renderer/components/ds/MetalDial.tsx
// 시안 A: 메탈 다이얼 — 정밀기기 회전 노브, 동심원 그루브, LED 인디케이터
// 시안 A: 메탈 다이얼 — 정밀기기 회전 노브, 동심원 그루브, 금속 광택, LED 인디케이터
// 보강: 레퍼런스의 conic-gradient 정적 라이팅 + 방향성 그림자 추가
import { Box, Typography } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../../theme'
import { Box } from '@mui/material'
import { d3roPalette, d3roFontMono, d3roTypo, d3roShadow } from '../../theme'
import { PhosphorText } from './PhosphorText'
interface MetalDialProps {
/** 0.0 ~ 1.0 값 (다이얼 위치) */
@ -22,97 +24,110 @@ export function MetalDial({
ledColor = d3roPalette.accent.amber,
}: MetalDialProps): React.ReactElement {
const rotation = value * 270 - 135 // -135° ~ +135° 범위
const knobSize = size - 12 // 웰 패딩
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
{/* 다이얼 외곽 */}
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1.5 }}>
{/* 다이얼 웰 (inset well) */}
<Box
sx={{
width: size,
height: size,
borderRadius: '50%',
bgcolor: d3roPalette.bg.chassis,
bgcolor: d3roPalette.bg.crtBezel,
boxShadow: `
0 4px 12px rgba(0,0,0,0.5),
inset 0 2px 4px rgba(255,255,255,0.08),
inset 0 -2px 4px rgba(0,0,0,0.3)
inset 0 3px 8px rgba(0,0,0,0.8),
inset 0 -1px 2px rgba(255,255,255,0.08),
0 1px 1px rgba(255,255,255,0.05)
`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
p: '6px',
}}
>
{/* 동심원 그루브 (CSS로 시뮬레이션) */}
{/* 노브 회전체 (동심원 그루브) */}
<Box
sx={{
width: size - 16,
height: size - 16,
width: knobSize,
height: knobSize,
borderRadius: '50%',
position: 'absolute',
top: 6,
left: 6,
background: `
repeating-radial-gradient(
circle at center,
${d3roPalette.bg.chassis} 0px,
${d3roPalette.bg.card} 1px,
${d3roPalette.bg.chassis} 2px
circle at 50% 50%,
#e6e7e9 0px,
#e6e7e9 1px,
#b0b2b5 1.5px,
#b0b2b5 2.5px
)
`,
boxShadow: `
inset 0 1px 3px rgba(0,0,0,0.6),
0 1px 1px rgba(255,255,255,0.05)
`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: `rotate(${rotation}deg)`,
transition: 'transform 0.2s ease-out',
}}
>
{/* 포인터 인디케이터 */}
{/* 포인터 인디케이터 점 */}
<Box
sx={{
position: 'absolute',
top: 8,
top: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 4,
height: 4,
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: ledColor,
boxShadow: `0 0 6px ${ledColor}`,
bgcolor: d3roPalette.bg.crtBezel,
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&::after': {
content: '""',
width: 4,
height: 4,
borderRadius: '50%',
bgcolor: ledColor,
},
}}
/>
</Box>
{/* LED 인디케이터 (우측 상단) */}
{/* 정적 금속 광택 오버레이 (노브와 별개, 회전 안 함) */}
<Box
sx={{
position: 'absolute',
top: 4,
right: size * 0.25,
width: 5,
height: 5,
inset: '6px',
borderRadius: '50%',
bgcolor: ledColor,
boxShadow: `0 0 4px ${ledColor}`,
pointerEvents: 'none',
// 레퍼런스의 핵심: directional light + conic reflections
background: `
linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0) 40%, rgba(0,0,0,0.6) 100%),
conic-gradient(from 180deg at 50% 50%,
rgba(255,255,255,0) 0deg,
rgba(255,255,255,0.4) 45deg,
rgba(255,255,255,0) 90deg,
rgba(255,255,255,0.2) 180deg,
rgba(255,255,255,0) 270deg,
rgba(255,255,255,0.4) 315deg,
rgba(255,255,255,0) 360deg
)
`,
mixBlendMode: 'overlay',
// 방향성 그림자: 좌상 하이라이트 + 우하 쉐이드
boxShadow: `
-4px -4px 8px rgba(255,255,255,0.3),
12px 16px 20px rgba(0,0,0,0.7),
inset 0 2px 3px rgba(255,255,255,0.8)
`,
}}
/>
</Box>
{/* 라벨 */}
{label && (
<Typography
sx={{
fontFamily: d3roFontMono,
fontSize: '9px',
fontWeight: 700,
letterSpacing: '1.5px',
color: d3roPalette.text.inactive,
textTransform: 'uppercase',
}}
>
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
{label}
</Typography>
</PhosphorText>
)}
</Box>
)

View file

@ -1,16 +1,44 @@
// src/renderer/components/ds/PhosphorText.tsx
// 시안 A: 인광 텍스트 — 앰버 glow, 모노 폰트, CRT 느낌
// 확장: title/stat/body/compact/meta/engrave/micro/nano 변형 추가
import { Typography, type TypographyProps } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../../theme'
import { d3roPalette, d3roFontMono, d3roTypo } from '../../theme'
type PhosphorVariant = 'hero' | 'value' | 'label' | 'dim'
type PhosphorVariant =
| 'hero' | 'title' | 'value' | 'heading'
| 'body' | 'compact' | 'small'
| 'meta' | 'label' | 'dim'
| 'engrave' | 'micro' | 'nano'
const VARIANTS: Record<PhosphorVariant, { fontSize: string; color: string; glow: string; fontWeight: number }> = {
hero: { fontSize: '42px', color: d3roPalette.accent.amber, glow: 'rgba(242, 91, 41, 0.4)', fontWeight: 300 },
value: { fontSize: '20px', color: d3roPalette.accent.amber, glow: 'rgba(242, 91, 41, 0.3)', fontWeight: 400 },
label: { fontSize: '10px', color: d3roPalette.text.dimLabel, glow: 'none', fontWeight: 700 },
dim: { fontSize: '10px', color: d3roPalette.text.inactive, glow: 'none', fontWeight: 400 },
interface VariantDef {
fontSize: string
color: string
glow: string
fontWeight: number
letterSpacing: string
lineHeight: number
textTransform?: 'uppercase' | 'none'
}
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' },
}
interface PhosphorTextProps extends Omit<TypographyProps, 'variant'> {
@ -28,11 +56,11 @@ export function PhosphorText({ variant = 'value', sx, ...props }: PhosphorTextPr
fontSize: v.fontSize,
fontWeight: v.fontWeight,
color: v.color,
textShadow: v.glow !== 'none' ? `0 0 6px ${v.glow}` : 'none',
letterSpacing: variant === 'label' ? '2px' : variant === 'hero' ? '-2px' : '0.02em',
lineHeight: 1,
textShadow: v.glow !== 'none' ? v.glow : 'none',
letterSpacing: v.letterSpacing,
lineHeight: v.lineHeight,
fontVariantNumeric: 'tabular-nums',
textTransform: variant === 'label' ? 'uppercase' : 'none',
textTransform: v.textTransform ?? 'none',
...sx,
}}
/>

View file

@ -1,8 +1,9 @@
// src/renderer/components/ds/PhysicalButton.tsx
// 시안 A: 물리 버튼 — 돌출 그림자, 눌림 피드백, 선택 상태
// 토큰 적용: d3roShadow, d3roRadius, d3roTypo
import { Button, type ButtonProps } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../../theme'
import { d3roPalette, d3roFontMono, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
interface PhysicalButtonProps extends Omit<ButtonProps, 'variant'> {
selected?: boolean
@ -16,26 +17,24 @@ export function PhysicalButton({ selected = false, sx, ...props }: PhysicalButto
height: 44,
bgcolor: selected ? d3roPalette.bg.crtBezel : d3roPalette.bg.chassis,
border: 'none',
borderRadius: '6px',
borderRadius: d3roRadius.small,
color: selected ? d3roPalette.accent.amber : d3roPalette.text.inactive,
fontFamily: d3roFontMono,
fontSize: '12px',
fontWeight: 600,
fontSize: d3roTypo.small.size,
fontWeight: d3roTypo.small.weight,
cursor: 'pointer',
boxShadow: selected
? 'inset 0 2px 6px rgba(0,0,0,0.8), inset 0 0 0 1px #000'
: '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)',
boxShadow: selected ? d3roShadow.buttonPressed : d3roShadow.buttonRaised,
transform: selected ? 'translateY(1px)' : 'none',
transition: 'all 0.05s linear',
'&:active': {
transform: 'translateY(2px)',
boxShadow: '0 1px 2px rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,0,0,0.3)',
boxShadow: d3roShadow.buttonActive,
},
'&:hover': {
bgcolor: selected ? d3roPalette.bg.crtBezel : d3roPalette.bg.cardHover,
},
textTransform: 'uppercase',
letterSpacing: '0.5px',
letterSpacing: d3roTypo.label.spacing,
minWidth: 0,
...sx,
}}

View file

@ -0,0 +1,69 @@
// src/renderer/components/ds/ScreenPanel.tsx
// 시안 A: CRT 없는 순수 스크린 패널 — 인셋 베젤 + 글래스 반사 + 인광 텍스트용
// 레퍼런스의 .display-module > .screen-glass 패턴
import { Box } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { d3roPalette, d3roShadow } from '../../theme'
interface ScreenPanelProps {
children: React.ReactNode
/** 전체 높이 (px 또는 CSS 값) */
height?: number | string
}
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,
overflow: 'hidden',
height,
}}
>
{/* 글래스 배경 */}
<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',
zIndex: 2,
p: 2,
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{children}
</Box>
</Box>
)
}

View file

@ -8,3 +8,5 @@ export { PhysicalButton } from './PhysicalButton'
export { MetalCard } from './MetalCard'
export { PhosphorText } from './PhosphorText'
export { MetalDial } from './MetalDial'
export { ScreenPanel } from './ScreenPanel'
export { ButtonGroup } from './ButtonGroup'