feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
170
packages/ui/src/components/ds/AudioVisualizerBar.tsx
Normal file
170
packages/ui/src/components/ds/AudioVisualizerBar.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
'use client'
|
||||
|
||||
// packages/ui/src/components/ds/AudioVisualizerBar.tsx
|
||||
// Multi-band Audio Equalizer & Dynamic Spectrum Canvas Visualizer
|
||||
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { Box, type BoxProps } from '@mui/material'
|
||||
import { d3roRadius } from '../../theme'
|
||||
|
||||
export interface AudioVisualizerBarProps extends Omit<BoxProps, 'children'> {
|
||||
/** Real-time normalized audio RMS level (0.0 to 1.0) */
|
||||
audioLevel?: number
|
||||
/** Total number of equalizer vertical bars (default: 32) */
|
||||
bars?: number
|
||||
/** Equalizer height in pixels (default: 48) */
|
||||
height?: number
|
||||
/** Width of each individual equalizer bar in px (default: 3) */
|
||||
barWidth?: number
|
||||
/** Spacing between equalizer bars in px (default: 2) */
|
||||
gap?: number
|
||||
/** Whether the visualizer is actively processing audio */
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
function readVisualizerColors(): [string, string, string, string] {
|
||||
if (typeof document === 'undefined') return ['#60a5fa', '#3b82f6', '#818cf8', '#a78bfa']
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const read = (name: string, fallback: string): string => style.getPropertyValue(name).trim() || fallback
|
||||
return [
|
||||
read('--d3-gradient-wave1', '#60a5fa'),
|
||||
read('--d3-gradient-wave2', '#3b82f6'),
|
||||
read('--d3-gradient-wave3', '#818cf8'),
|
||||
read('--d3-gradient-wave4', '#a78bfa'),
|
||||
]
|
||||
}
|
||||
|
||||
export function AudioVisualizerBar({
|
||||
audioLevel = 0,
|
||||
bars = 32,
|
||||
height = 48,
|
||||
barWidth = 3,
|
||||
gap = 2,
|
||||
active = true,
|
||||
sx,
|
||||
...boxProps
|
||||
}: AudioVisualizerBarProps): 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 = height
|
||||
let colors = readVisualizerColors()
|
||||
let frame = 0
|
||||
|
||||
const resize = (): void => {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
width = canvas.clientWidth
|
||||
heightPx = canvas.clientHeight || height
|
||||
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)
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(resize)
|
||||
observer.observe(canvas)
|
||||
resize()
|
||||
|
||||
const render = (): void => {
|
||||
frame += 1
|
||||
if (frame % 60 === 0) {
|
||||
colors = readVisualizerColors()
|
||||
}
|
||||
|
||||
// Smooth audio RMS response
|
||||
const target = Math.min(1, levelRef.current)
|
||||
const k = target > smoothedRef.current ? 0.35 : 0.08
|
||||
smoothedRef.current += (target - smoothedRef.current) * k
|
||||
|
||||
ctx.clearRect(0, 0, width, heightPx)
|
||||
|
||||
if (width <= 0 || heightPx <= 0) {
|
||||
raf = requestAnimationFrame(render)
|
||||
return
|
||||
}
|
||||
|
||||
const totalStep = barWidth + gap
|
||||
const actualBars = Math.min(bars, Math.floor(width / totalStep))
|
||||
const totalWidth = actualBars * totalStep - gap
|
||||
const startX = (width - totalWidth) / 2
|
||||
const centerY = heightPx / 2
|
||||
const t = performance.now() / 1000
|
||||
|
||||
// Create harmonious gradient for bars
|
||||
const grad = ctx.createLinearGradient(0, 0, width, 0)
|
||||
grad.addColorStop(0, colors[0])
|
||||
grad.addColorStop(0.35, colors[1])
|
||||
grad.addColorStop(0.7, colors[2])
|
||||
grad.addColorStop(1, colors[3])
|
||||
|
||||
ctx.fillStyle = grad
|
||||
|
||||
for (let i = 0; i < actualBars; i++) {
|
||||
const x = startX + i * totalStep
|
||||
// Symmetrical curve from center
|
||||
const centerDist = Math.abs(i - (actualBars - 1) / 2) / (actualBars / 2)
|
||||
const bellCurve = Math.cos(centerDist * (Math.PI / 2))
|
||||
|
||||
// Harmonic oscillation
|
||||
const idleWave = active
|
||||
? Math.sin(t * 2.5 + i * 0.3) * 0.3 + Math.cos(t * 1.5 + i * 0.15) * 0.2 + 0.5
|
||||
: 0.15
|
||||
|
||||
const amp = smoothedRef.current > 0.02
|
||||
? Math.min(1, smoothedRef.current * (0.4 + bellCurve * 0.6) + idleWave * 0.1)
|
||||
: idleWave * (active ? 0.28 : 0.08)
|
||||
|
||||
const barHeight = Math.max(3, amp * heightPx * 0.9)
|
||||
const half = barHeight / 2
|
||||
const radius = Math.min(barWidth / 2, half)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, centerY - half, barWidth, half * 2, radius)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(render)
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [bars, height, barWidth, gap, active])
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...boxProps}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: d3roRadius.inner,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
101
packages/ui/src/components/ds/DoubleBezelCard.tsx
Normal file
101
packages/ui/src/components/ds/DoubleBezelCard.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
'use client'
|
||||
|
||||
// packages/ui/src/components/ds/DoubleBezelCard.tsx
|
||||
// High-End Agency / Awwwards-Tier Double-Bezel (Doppelrand) Enclosure Component
|
||||
// Outer Shell: Precision machined tray with outer radius & ambient hairline
|
||||
// Inner Core: Floating glass plate with concentric inner radius, specular highlight & blur
|
||||
|
||||
import React from 'react'
|
||||
import { Box, type BoxProps } from '@mui/material'
|
||||
import { d3roPalette, d3roShadow, d3roRadius } from '../../theme'
|
||||
|
||||
export interface DoubleBezelCardProps extends Omit<BoxProps, 'component'> {
|
||||
/** Outer padding between tray and core (default: 6px) */
|
||||
bezelPadding?: number | string
|
||||
/** Whether the card shows an interactive lift / glow on hover */
|
||||
interactive?: boolean
|
||||
/** Inner core padding (default: 3 / 24px) */
|
||||
innerPadding?: number | string
|
||||
/** Content to render inside the inner core */
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function DoubleBezelCard({
|
||||
bezelPadding = '6px',
|
||||
interactive = false,
|
||||
innerPadding = 3,
|
||||
children,
|
||||
sx,
|
||||
...boxProps
|
||||
}: DoubleBezelCardProps): React.ReactElement {
|
||||
return (
|
||||
<Box
|
||||
{...boxProps}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: d3roRadius.doubleBezelOuter,
|
||||
p: bezelPadding,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
boxShadow: interactive ? d3roShadow.card : 'none',
|
||||
transition: 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease, border-color 0.3s ease',
|
||||
...(interactive
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
borderColor: d3roPalette.glass.hairlineStrong,
|
||||
boxShadow: d3roShadow.glowCardHover,
|
||||
transform: 'translateY(-2px)',
|
||||
'& .d3-inner-core': {
|
||||
bgcolor: d3roPalette.bg.cardHover,
|
||||
},
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0px) scale(0.99)',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{/* Inner Core Container */}
|
||||
<Box
|
||||
className="d3-inner-core"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: d3roRadius.doubleBezelInner,
|
||||
bgcolor: d3roPalette.glass.surface,
|
||||
backdropFilter: `blur(${d3roPalette.glass.blur})`,
|
||||
boxShadow: `${d3roShadow.inset}, ${d3roShadow.glowCard}`,
|
||||
p: innerPadding,
|
||||
overflow: 'hidden',
|
||||
transition: 'background-color 0.25s ease',
|
||||
// Top Sheen + Specular highlight
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
background: d3roPalette.glass.sheen,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
// 1px Gradient Hairline Rim
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: 'inherit',
|
||||
padding: '1px',
|
||||
background: d3roPalette.glass.borderGradient,
|
||||
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
WebkitMaskComposite: 'xor',
|
||||
maskComposite: 'exclude',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative', zIndex: 1 }}>{children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -27,14 +27,15 @@ interface GradientWaveProps {
|
|||
|
||||
/** :root에 주입된 웨이브 그라디언트 스톱을 읽는다 */
|
||||
function readWaveColors(): [string, string, string, string] {
|
||||
if (typeof document === 'undefined') return ['#60a5fa', '#3b82f6', '#818cf8', '#a78bfa']
|
||||
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-wave1', '#60a5fa'),
|
||||
read('--d3-gradient-wave2', '#3b82f6'),
|
||||
read('--d3-gradient-wave3', '#8b5cf6'),
|
||||
read('--d3-gradient-wave4', '#e879f9'),
|
||||
read('--d3-gradient-wave3', '#818cf8'),
|
||||
read('--d3-gradient-wave4', '#a78bfa'),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
// src/renderer/components/ds/PhosphorText.tsx
|
||||
// v2 "Midnight Glass": 클린 타이포그래피 — 산세리프(Pretendard) 기반.
|
||||
// 헤드라인/수치는 프라이머리 화이트, 메타/라벨류만 모노+대문자 유지.
|
||||
// (변형 API는 v1과 동일 — 전 페이지 호환)
|
||||
|
||||
import { Typography, type TypographyProps } from '@mui/material'
|
||||
import { d3roPalette, d3roFontSans, d3roFontMono, d3roTypo } from '../../theme'
|
||||
|
|
@ -44,11 +43,12 @@ interface PhosphorTextProps extends Omit<TypographyProps, 'variant'> {
|
|||
variant?: PhosphorVariant
|
||||
}
|
||||
|
||||
export function PhosphorText({ variant = 'value', sx, ...props }: PhosphorTextProps): React.ReactElement {
|
||||
export function PhosphorText({ variant = 'value', component = 'span', sx, ...props }: PhosphorTextProps): React.ReactElement {
|
||||
const v = VARIANTS[variant]
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component={component}
|
||||
{...props}
|
||||
sx={{
|
||||
fontFamily: v.mono ? d3roFontMono : d3roFontSans,
|
||||
|
|
|
|||
|
|
@ -1,47 +1,121 @@
|
|||
'use client'
|
||||
|
||||
// src/renderer/components/ds/PhysicalButton.tsx
|
||||
// v2 "Midnight Glass": 글래스 버튼 — 기본은 반투명 표면 + 헤어라인,
|
||||
// selected 시 액센트 그라디언트 + 글로우 (레퍼런스 프라이머리 버튼)
|
||||
// packages/ui/src/components/ds/PhysicalButton.tsx
|
||||
// High-End Tactile Button with Button-in-Button Trailing Icon & Spring Physics
|
||||
|
||||
import { Button, type ButtonProps } from '@mui/material'
|
||||
import React from 'react'
|
||||
import { Button, Box, type ButtonProps } from '@mui/material'
|
||||
import { d3roPalette, d3roFontSans, d3roShadow, d3roRadius, d3roTypo } from '../../theme'
|
||||
|
||||
interface PhysicalButtonProps extends Omit<ButtonProps, 'variant'> {
|
||||
export interface PhysicalButtonProps extends Omit<ButtonProps, 'variant'> {
|
||||
/** Active selected state (renders primary accent gradient) */
|
||||
selected?: boolean
|
||||
/** Visual button tone */
|
||||
tone?: 'accent' | 'glass' | 'danger' | 'success' | 'ghost'
|
||||
/** Trailing icon rendered in a nested circular capsule (Button-in-Button pattern) */
|
||||
trailingIcon?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PhysicalButton({ selected = false, sx, ...props }: PhysicalButtonProps): React.ReactElement {
|
||||
export function PhysicalButton({
|
||||
selected = false,
|
||||
tone = 'glass',
|
||||
trailingIcon,
|
||||
children,
|
||||
sx,
|
||||
...props
|
||||
}: PhysicalButtonProps): React.ReactElement {
|
||||
const isAccent = selected || tone === 'accent'
|
||||
const isDanger = tone === 'danger'
|
||||
const isSuccess = tone === 'success'
|
||||
|
||||
let bg: string = d3roPalette.glass.raised
|
||||
let bgImage: string = 'none'
|
||||
let textColor: string = d3roPalette.text.primary
|
||||
let borderColor: string = d3roPalette.glass.hairline
|
||||
let shadow: string = 'none'
|
||||
|
||||
if (isAccent) {
|
||||
bg = 'transparent'
|
||||
bgImage = d3roPalette.gradient.accent
|
||||
textColor = d3roPalette.text.inverse
|
||||
borderColor = 'transparent'
|
||||
shadow = d3roShadow.glowAccent
|
||||
} else if (isDanger) {
|
||||
bg = d3roPalette.tag.redBg
|
||||
textColor = d3roPalette.tag.red
|
||||
borderColor = d3roPalette.tag.redGlow
|
||||
} else if (isSuccess) {
|
||||
bg = d3roPalette.tag.greenBg
|
||||
textColor = d3roPalette.tag.green
|
||||
borderColor = d3roPalette.tag.greenGlow
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
sx={{
|
||||
height: 42,
|
||||
bgcolor: selected ? 'transparent' : d3roPalette.glass.raised,
|
||||
backgroundImage: selected ? d3roPalette.gradient.accent : 'none',
|
||||
border: `1px solid ${selected ? 'transparent' : d3roPalette.glass.hairline}`,
|
||||
height: 40,
|
||||
px: trailingIcon ? 2 : 2.5,
|
||||
bgcolor: bg,
|
||||
backgroundImage: bgImage,
|
||||
border: `1px solid ${borderColor}`,
|
||||
borderRadius: d3roRadius.button,
|
||||
color: selected ? '#fff' : d3roPalette.text.secondary,
|
||||
color: textColor,
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: d3roTypo.small.size,
|
||||
fontSize: d3roTypo.compact.size,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
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': {
|
||||
filter: 'brightness(0.92)',
|
||||
},
|
||||
'&:hover': {
|
||||
bgcolor: selected ? 'transparent' : d3roPalette.bg.cardHover,
|
||||
borderColor: selected ? 'transparent' : d3roPalette.glass.hairlineStrong,
|
||||
color: selected ? '#fff' : d3roPalette.text.primary,
|
||||
filter: selected ? 'brightness(1.08)' : 'none',
|
||||
},
|
||||
boxShadow: shadow,
|
||||
textTransform: 'none',
|
||||
letterSpacing: d3roTypo.small.spacing,
|
||||
letterSpacing: '0.01em',
|
||||
minWidth: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1.25,
|
||||
transition: 'all 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
'&:hover': {
|
||||
bgcolor: isAccent ? undefined : d3roPalette.bg.cardHover,
|
||||
borderColor: isAccent ? 'transparent' : d3roPalette.glass.hairlineStrong,
|
||||
color: isAccent ? d3roPalette.text.inverse : d3roPalette.text.primary,
|
||||
filter: isAccent ? 'brightness(1.08)' : 'none',
|
||||
boxShadow: isAccent ? d3roShadow.glowAccent : d3roShadow.card,
|
||||
transform: 'translateY(-1px)',
|
||||
'& .d3-button-trailing-icon': {
|
||||
transform: 'translateX(2px)',
|
||||
},
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0) scale(0.98)',
|
||||
filter: 'brightness(0.95)',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
|
||||
{children}
|
||||
</Box>
|
||||
{trailingIcon && (
|
||||
<Box
|
||||
className="d3-button-trailing-icon"
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
bgcolor: isAccent ? 'rgba(255, 255, 255, 0.18)' : d3roPalette.bg.inset,
|
||||
border: `1px solid ${isAccent ? 'rgba(255, 255, 255, 0.2)' : d3roPalette.border.subtle}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: isAccent ? d3roPalette.text.inverse : d3roPalette.text.secondary,
|
||||
transition: 'transform 0.2s ease',
|
||||
flexShrink: 0,
|
||||
'& svg': { width: 14, height: 14 },
|
||||
}}
|
||||
>
|
||||
{trailingIcon}
|
||||
</Box>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
123
packages/ui/src/components/ds/SegmentControl.tsx
Normal file
123
packages/ui/src/components/ds/SegmentControl.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
'use client'
|
||||
|
||||
// packages/ui/src/components/ds/SegmentControl.tsx
|
||||
// Linear / Apple-tier segmented pill control with tactile active state
|
||||
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { d3roPalette, d3roRadius, d3roShadow, typoSx } from '../../theme'
|
||||
|
||||
export interface SegmentOption<T extends string = string> {
|
||||
value: T
|
||||
label: string
|
||||
icon?: React.ReactNode
|
||||
badge?: string | number
|
||||
}
|
||||
|
||||
export interface SegmentControlProps<T extends string = string> {
|
||||
options?: SegmentOption<T>[]
|
||||
/** Alias for options */
|
||||
items?: SegmentOption<T>[]
|
||||
value: T
|
||||
onChange: (value: T) => void
|
||||
size?: 'small' | 'medium'
|
||||
sx?: object
|
||||
}
|
||||
|
||||
export function SegmentControl<T extends string = string>({
|
||||
options,
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
size = 'medium',
|
||||
sx,
|
||||
}: SegmentControlProps<T>): React.ReactElement {
|
||||
const isSmall = size === 'small'
|
||||
const list = options || items || []
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
p: '3px',
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
borderRadius: d3roRadius.pill,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
gap: '2px',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{list.map((opt) => {
|
||||
const isSelected = opt.value === value
|
||||
return (
|
||||
<Box
|
||||
key={opt.value}
|
||||
onClick={() => onChange(opt.value)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: isSmall ? 1.5 : 2,
|
||||
py: isSmall ? 0.5 : 0.75,
|
||||
borderRadius: d3roRadius.pill,
|
||||
bgcolor: isSelected ? d3roPalette.glass.raised : 'transparent',
|
||||
color: isSelected ? d3roPalette.text.primary : d3roPalette.text.secondary,
|
||||
border: isSelected ? `1px solid ${d3roPalette.glass.hairlineStrong}` : '1px solid transparent',
|
||||
boxShadow: isSelected ? d3roShadow.buttonRaised : 'none',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
transition: 'all 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
'&:hover': {
|
||||
color: d3roPalette.text.primary,
|
||||
bgcolor: isSelected ? d3roPalette.glass.raised : d3roPalette.glass.surface,
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.97)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{opt.icon && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: isSelected ? d3roPalette.accent.light : 'inherit',
|
||||
'& svg': { width: isSmall ? 14 : 16, height: isSmall ? 14 : 16 },
|
||||
}}
|
||||
>
|
||||
{opt.icon}
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
...typoSx('compact'),
|
||||
fontSize: isSmall ? '12px' : '13px',
|
||||
fontWeight: isSelected ? 600 : 500,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Typography>
|
||||
{opt.badge !== undefined && (
|
||||
<Box
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
borderRadius: d3roRadius.pill,
|
||||
bgcolor: isSelected ? d3roPalette.accent.dim : d3roPalette.bg.chassis,
|
||||
color: isSelected ? d3roPalette.accent.light : d3roPalette.text.dimLabel,
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{opt.badge}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
113
packages/ui/src/components/ds/TactileBadge.tsx
Normal file
113
packages/ui/src/components/ds/TactileBadge.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
'use client'
|
||||
|
||||
// packages/ui/src/components/ds/TactileBadge.tsx
|
||||
// Hardware-grade telemetry badge / pill chip with optional LED bead and keycap rendering
|
||||
|
||||
import React from 'react'
|
||||
import { Box, type BoxProps } from '@mui/material'
|
||||
import { Led } from './Led'
|
||||
import { d3roPalette, d3roFontSans, d3roFontMono, d3roRadius, typoSx } from '../../theme'
|
||||
|
||||
export interface TactileBadgeProps extends Omit<BoxProps, 'component'> {
|
||||
/** Optional LED indicator color */
|
||||
ledColor?: 'green' | 'amber' | 'red' | 'blue' | 'orange' | 'purple' | 'off'
|
||||
/** Whether the LED should pulse */
|
||||
ledPulse?: boolean
|
||||
/** Semantic tone for border and background */
|
||||
tone?: 'default' | 'accent' | 'success' | 'warning' | 'error' | 'mono'
|
||||
/** Whether to use monospace font */
|
||||
mono?: boolean
|
||||
/** Interactive clickable chip */
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
/** Content to display */
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function TactileBadge({
|
||||
ledColor,
|
||||
ledPulse = false,
|
||||
tone = 'default',
|
||||
mono = false,
|
||||
onClick,
|
||||
children,
|
||||
sx,
|
||||
...boxProps
|
||||
}: TactileBadgeProps): React.ReactElement {
|
||||
const isClickable = Boolean(onClick)
|
||||
|
||||
let textColor: string = d3roPalette.text.secondary
|
||||
let borderColor: string = d3roPalette.glass.hairlineStrong
|
||||
let bgColor: string = d3roPalette.glass.raised
|
||||
|
||||
switch (tone) {
|
||||
case 'accent':
|
||||
textColor = d3roPalette.accent.light
|
||||
borderColor = d3roPalette.accent.dim
|
||||
bgColor = d3roPalette.accent.dim
|
||||
break
|
||||
case 'success':
|
||||
textColor = d3roPalette.tag.green
|
||||
borderColor = d3roPalette.tag.greenBg
|
||||
bgColor = d3roPalette.tag.greenBg
|
||||
break
|
||||
case 'warning':
|
||||
textColor = d3roPalette.tag.orange
|
||||
borderColor = d3roPalette.tag.orangeBg
|
||||
bgColor = d3roPalette.tag.orangeBg
|
||||
break
|
||||
case 'error':
|
||||
textColor = d3roPalette.tag.red
|
||||
borderColor = d3roPalette.tag.redBg
|
||||
bgColor = d3roPalette.tag.redBg
|
||||
break
|
||||
case 'mono':
|
||||
textColor = d3roPalette.text.primary
|
||||
borderColor = d3roPalette.border.default
|
||||
bgColor = d3roPalette.bg.inset
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...boxProps}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.85,
|
||||
px: 1.25,
|
||||
py: 0.45,
|
||||
borderRadius: d3roRadius.badge,
|
||||
border: `1px solid ${borderColor}`,
|
||||
bgcolor: bgColor,
|
||||
color: textColor,
|
||||
...typoSx('meta'),
|
||||
fontFamily: mono ? d3roFontMono : d3roFontSans,
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: isClickable ? 'pointer' : 'default',
|
||||
transition: 'all 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
...(isClickable
|
||||
? {
|
||||
'&:hover': {
|
||||
borderColor: d3roPalette.accent.main,
|
||||
color: d3roPalette.text.primary,
|
||||
filter: 'brightness(1.15)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0) scale(0.97)',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{ledColor && <Led color={ledColor} pulse={ledPulse} size={6} />}
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// src/renderer/components/ds/index.ts
|
||||
// packages/ui/src/components/ds/index.ts
|
||||
// 디자인 시스템 컴포넌트 SSOT barrel export
|
||||
|
||||
export { InstrumentPanel } from './InstrumentPanel'
|
||||
|
|
@ -8,6 +8,11 @@ export { MetalCard } from './MetalCard'
|
|||
export { TiltCard } from './TiltCard'
|
||||
export { PhosphorText } from './PhosphorText'
|
||||
export { ScreenPanel } from './ScreenPanel'
|
||||
// v2 "Midnight Glass" 신규
|
||||
// v2 "Midnight Glass"
|
||||
export { GradientWave } from './GradientWave'
|
||||
export { StatRing } from './StatRing'
|
||||
// v3 "Tactile Liquid & Double-Bezel" 신규
|
||||
export { DoubleBezelCard } from './DoubleBezelCard'
|
||||
export { TactileBadge } from './TactileBadge'
|
||||
export { AudioVisualizerBar } from './AudioVisualizerBar'
|
||||
export { SegmentControl } from './SegmentControl'
|
||||
|
|
|
|||
|
|
@ -83,17 +83,17 @@ const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
|
|||
'--d3-action-btn-hover-bg': 'rgba(59,130,246,0.14)',
|
||||
'--d3-action-btn-hover': '#93a4c8',
|
||||
// wave-bar 9단계: theme.ts RAW.dark.gradient.wave1-4 보간
|
||||
'--d3-wave-1': '#22d3ee',
|
||||
'--d3-wave-2': '#28bef0',
|
||||
'--d3-wave-3': '#2fa9f2',
|
||||
'--d3-wave-4': '#3596f4',
|
||||
'--d3-wave-1': '#93c5fd',
|
||||
'--d3-wave-2': '#60a5fa',
|
||||
'--d3-wave-3': '#4f8dfa',
|
||||
'--d3-wave-4': '#3b82f6',
|
||||
'--d3-wave-5': '#3b82f6',
|
||||
'--d3-wave-6': '#4f78f6',
|
||||
'--d3-wave-7': '#636ff6',
|
||||
'--d3-wave-8': '#7765f6',
|
||||
'--d3-wave-9': '#8b5cf6',
|
||||
'--d3-wave-6': '#5b75f7',
|
||||
'--d3-wave-7': '#6e71f7',
|
||||
'--d3-wave-8': '#818cf8',
|
||||
'--d3-wave-9': '#a78bfa',
|
||||
'--d3-status-error': '#ef4444',
|
||||
'--d3-status-success': '#34d399',
|
||||
'--d3-status-success': '#10b981',
|
||||
},
|
||||
light: {
|
||||
'--d3-bg-card': '#ffffff',
|
||||
|
|
|
|||
|
|
@ -144,9 +144,9 @@ const RAW: Record<ThemeKey, RawTheme> = {
|
|||
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',
|
||||
bar: 'linear-gradient(90deg, #60a5fa 0%, #3b82f6 100%)',
|
||||
barAlt: 'linear-gradient(90deg, #818cf8 0%, #a78bfa 100%)',
|
||||
wave1: '#60a5fa', wave2: '#3b82f6', wave3: '#818cf8', wave4: '#a78bfa',
|
||||
},
|
||||
glow: {
|
||||
accent: '0 0 0 1px rgba(59,130,246,0.35), 0 8px 24px rgba(37,99,235,0.35)',
|
||||
|
|
@ -415,25 +415,26 @@ export const d3roPalette = {
|
|||
cardHover: 'var(--d3-glow-cardHover)',
|
||||
},
|
||||
tag: {
|
||||
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)',
|
||||
purple: '#8b5cf6',
|
||||
purpleBg: 'rgba(139, 92, 246, 0.12)',
|
||||
orange: '#f59e0b',
|
||||
orangeBg: 'rgba(245, 158, 11, 0.12)',
|
||||
red: '#ef4444',
|
||||
redBg: 'rgba(239, 68, 68, 0.12)',
|
||||
green: '#10b981',
|
||||
greenBg: 'rgba(16, 185, 129, 0.12)',
|
||||
blue: '#3b82f6',
|
||||
blueBg: 'rgba(59, 130, 246, 0.12)',
|
||||
greenGlow: 'rgba(16, 185, 129, 0.35)',
|
||||
redGlow: 'rgba(239, 68, 68, 0.35)',
|
||||
orangeGlow: 'rgba(245, 158, 11, 0.35)',
|
||||
purpleGlow: 'rgba(139, 92, 246, 0.35)',
|
||||
blueGlow: 'rgba(59, 130, 246, 0.35)',
|
||||
},
|
||||
text: {
|
||||
primary: 'var(--d3-text-primary)',
|
||||
secondary: 'var(--d3-text-secondary)',
|
||||
inverse: 'var(--d3-text-inverse, #ffffff)',
|
||||
label: 'var(--d3-text-label)',
|
||||
disabled: 'var(--d3-text-disabled)',
|
||||
engraving: 'var(--d3-text-engraving)',
|
||||
|
|
@ -516,21 +517,25 @@ export const d3roShadow = {
|
|||
dialog: 'var(--d3-shadow-card)',
|
||||
tooltip: 'var(--d3-shadow-tooltip)',
|
||||
screenGlow: 'var(--d3-shadow-screenGlow)',
|
||||
/** v2: 액센트 글로우 */
|
||||
/** v2/v3: 액센트 글로우 및 아일랜드 플로트 */
|
||||
glowAccent: 'var(--d3-glow-accent)',
|
||||
glowSoft: 'var(--d3-glow-soft)',
|
||||
glowCard: 'var(--d3-glow-card)',
|
||||
glowCardHover: 'var(--d3-glow-cardHover)',
|
||||
islandFloat: '0 16px 36px -8px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
} as const
|
||||
|
||||
// ── SSOT: 반경 토큰 ────────────────────────────────────
|
||||
export const d3roRadius = {
|
||||
doubleBezelOuter: '22px',
|
||||
doubleBezelInner: '14px',
|
||||
outer: '20px',
|
||||
card: '16px',
|
||||
inner: '12px',
|
||||
button: '10px',
|
||||
small: '8px',
|
||||
xs: '6px',
|
||||
badge: '999px',
|
||||
pill: '999px',
|
||||
} as const
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue