feat(modal): 添加居中模态框组件及游戏相关弹窗功能

- 新增 CenterModal 组件,支持标题、关闭按钮、背景图片等功能
- 添加键盘 ESC 键关闭模态框和页面滚动锁定功能
- 创建桌面端登录、注册和用户信息三个业务模态框
- 集成 lottie-web 动画库并创建 LottiePlayer 组件
- 在样式文件中添加模态框相关的 CSS 类和输入框样式
- 更新桌面入口文件集成登录和用户信息模态框
- 调整桌面控制组件间距和样式配置
This commit is contained in:
JiaJun
2026-05-07 16:16:41 +08:00
parent 9ee681168e
commit f98710d375
20 changed files with 1390 additions and 7 deletions

View File

@@ -32,6 +32,7 @@
"dayjs": "^1.11.20",
"i18next": "^26.0.5",
"ky": "^2.0.1",
"lottie-web": "^5.13.0",
"lucide-react": "^1.9.0",
"motion": "^12.38.0",
"react": "^19.2.4",

8
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
ky:
specifier: ^2.0.1
version: 2.0.1
lottie-web:
specifier: ^5.13.0
version: 5.13.0
lucide-react:
specifier: ^1.9.0
version: 1.9.0(react@19.2.5)
@@ -1565,6 +1568,9 @@ packages:
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
engines: {node: '>=0.10.0'}
lottie-web@5.13.0:
resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -3387,6 +3393,8 @@ snapshots:
longest@2.0.1: {}
lottie-web@5.13.0: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1

537
src/assets/lottie/test.json Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

View File

@@ -0,0 +1,124 @@
import { type ReactNode, useEffect } from 'react'
import { createPortal } from 'react-dom'
import modalBg from '@/assets/system/modal-bg.webp'
import modalClose from '@/assets/system/modal-close.webp'
import modalNormalBg from '@/assets/system/modal-normal-bg.png'
import { cn } from '@/lib/untils'
interface CenterModalProps {
open: boolean
onClose?: () => void
title?: ReactNode
titleAlign?: 'left' | 'center'
isNormalBg?: boolean
children?: ReactNode
className?: string
}
const MODAL_HEADER_HEIGHT = 'calc(100% * 80 / 640)'
export function CenterModal({
open,
onClose,
title,
titleAlign = 'left',
isNormalBg = false,
children,
className,
}: CenterModalProps) {
const handleClose = () => {
onClose?.()
}
useEffect(() => {
if (!open || !onClose || typeof document === 'undefined') {
return
}
const previousOverflow = document.body.style.overflow
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}
document.body.style.overflow = 'hidden'
window.addEventListener('keydown', handleKeyDown)
return () => {
document.body.style.overflow = previousOverflow
window.removeEventListener('keydown', handleKeyDown)
}
}, [open, onClose])
if (!open || typeof document === 'undefined') {
return null
}
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/72 backdrop-blur-sm">
<div
role="dialog"
aria-modal="true"
aria-label={typeof title === 'string' ? title : 'Modal'}
className={cn(
'relative flex h-design-640 w-design-720 flex-col overflow-hidden rounded-[calc(var(--design-unit)*28)] px-design-20 text-white',
className,
)}
style={{
backgroundImage: `url(${isNormalBg ? modalNormalBg : modalBg})`,
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%',
}}
>
<div
className="relative w-full shrink-0 mt-design-15 px-design-20"
style={{ height: MODAL_HEADER_HEIGHT }}
>
{title ? (
<div
className={cn(
'flex h-full w-full items-center text-design-32 font-semibold tracking-[0.08em] text-cyan-50 sm:text-design-38',
titleAlign === 'center'
? 'justify-center text-center'
: 'justify-start text-left',
)}
>
{title}
</div>
) : null}
{onClose ? (
<button
type="button"
aria-label="Close modal"
onClick={handleClose}
className={cn(
'absolute top-1/2 inline-flex h-design-60 w-design-60 -translate-y-1/2 items-center justify-center rounded-full transition hover:scale-105 active:scale-95',
isNormalBg ? 'right-5' : 'right-10',
)}
>
<img
src={modalClose}
alt=""
aria-hidden="true"
className="modal-close-glow relative z-10 h-design-60 w-design-48 object-contain cursor-pointer"
/>
</button>
) : null}
</div>
<div
className={cn(
'w-full min-h-0 flex-1 overflow-y-auto',
title ? '' : 'pt-design-24',
)}
>
{children}
</div>
</div>
</div>,
document.body,
)
}

View File

@@ -0,0 +1,290 @@
import lottie, {
type AnimationConfigWithData,
type AnimationConfigWithPath,
type AnimationDirection,
type AnimationEventCallback,
type AnimationEvents,
type AnimationItem,
type AnimationSegment,
type CanvasRendererConfig,
type HTMLRendererConfig,
type RendererType,
type SVGRendererConfig,
} from 'lottie-web'
import {
forwardRef,
type HTMLAttributes,
useEffect,
useImperativeHandle,
useRef,
} from 'react'
import { cn } from '@/lib/untils'
type LottieRendererSettings =
| SVGRendererConfig
| CanvasRendererConfig
| HTMLRendererConfig
export interface LottiePlayerHandle {
getInstance: () => AnimationItem | null
play: () => void
pause: () => void
stop: () => void
destroy: () => void
setSpeed: (speed: number) => void
setDirection: (direction: AnimationDirection) => void
goToAndPlay: (value: number | string, isFrame?: boolean) => void
goToAndStop: (value: number | string, isFrame?: boolean) => void
}
export interface LottiePlayerProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
animationData?: AnimationConfigWithData['animationData']
path?: AnimationConfigWithPath['path']
renderer?: RendererType
loop?: boolean | number
autoplay?: boolean
isPaused?: boolean
speed?: number
direction?: AnimationDirection
initialSegment?: AnimationSegment
segments?: AnimationSegment | AnimationSegment[]
forceSegments?: boolean
name?: string
assetsPath?: string
rendererSettings?: LottieRendererSettings
onComplete?: AnimationEventCallback<AnimationEvents['complete']>
onLoopComplete?: AnimationEventCallback<AnimationEvents['loopComplete']>
onEnterFrame?: AnimationEventCallback<AnimationEvents['enterFrame']>
onDOMLoaded?: AnimationEventCallback<AnimationEvents['DOMLoaded']>
onDataReady?: AnimationEventCallback<AnimationEvents['data_ready']>
onDataFailed?: AnimationEventCallback<AnimationEvents['data_failed']>
}
export const LottiePlayer = forwardRef<LottiePlayerHandle, LottiePlayerProps>(
function LottiePlayer(
{
animationData,
path,
renderer = 'svg',
loop = true,
autoplay = true,
isPaused = false,
speed = 1,
direction = 1,
initialSegment,
segments,
forceSegments = true,
name,
assetsPath,
rendererSettings,
onComplete,
onLoopComplete,
onEnterFrame,
onDOMLoaded,
onDataReady,
onDataFailed,
className,
...divProps
},
ref,
) {
const containerRef = useRef<HTMLDivElement | null>(null)
const animationRef = useRef<AnimationItem | null>(null)
const hasSyncedPauseStateRef = useRef(false)
const loopRef = useRef(loop)
const autoplayRef = useRef(autoplay)
const speedRef = useRef(speed)
const directionRef = useRef(direction)
const segmentsRef = useRef(segments)
const forceSegmentsRef = useRef(forceSegments)
const isPausedRef = useRef(isPaused)
const completeCallbackRef = useRef(onComplete)
const loopCompleteCallbackRef = useRef(onLoopComplete)
const enterFrameCallbackRef = useRef(onEnterFrame)
const domLoadedCallbackRef = useRef(onDOMLoaded)
const dataReadyCallbackRef = useRef(onDataReady)
const dataFailedCallbackRef = useRef(onDataFailed)
loopRef.current = loop
autoplayRef.current = autoplay
speedRef.current = speed
directionRef.current = direction
segmentsRef.current = segments
forceSegmentsRef.current = forceSegments
isPausedRef.current = isPaused
completeCallbackRef.current = onComplete
loopCompleteCallbackRef.current = onLoopComplete
enterFrameCallbackRef.current = onEnterFrame
domLoadedCallbackRef.current = onDOMLoaded
dataReadyCallbackRef.current = onDataReady
dataFailedCallbackRef.current = onDataFailed
useImperativeHandle(
ref,
() => ({
getInstance: () => animationRef.current,
play: () => animationRef.current?.play(),
pause: () => animationRef.current?.pause(),
stop: () => animationRef.current?.stop(),
destroy: () => {
animationRef.current?.destroy()
animationRef.current = null
},
setSpeed: (nextSpeed) => animationRef.current?.setSpeed(nextSpeed),
setDirection: (nextDirection) =>
animationRef.current?.setDirection(nextDirection),
goToAndPlay: (value, isFrame) =>
animationRef.current?.goToAndPlay(value, isFrame),
goToAndStop: (value, isFrame) =>
animationRef.current?.goToAndStop(value, isFrame),
}),
[],
)
useEffect(() => {
const container = containerRef.current
if (!container || (!animationData && !path)) {
return
}
const animation = lottie.loadAnimation({
container,
renderer,
loop: loopRef.current,
autoplay: autoplayRef.current,
initialSegment,
name,
assetsPath,
rendererSettings: rendererSettings as
| SVGRendererConfig
| CanvasRendererConfig
| HTMLRendererConfig
| undefined,
...(animationData ? { animationData } : {}),
...(path ? { path } : {}),
})
animationRef.current = animation
animation.setSpeed(speedRef.current)
animation.setDirection(directionRef.current)
animation.addEventListener('complete', (event) => {
completeCallbackRef.current?.(event)
})
animation.addEventListener('loopComplete', (event) => {
loopCompleteCallbackRef.current?.(event)
})
animation.addEventListener('enterFrame', (event) => {
enterFrameCallbackRef.current?.(event)
})
animation.addEventListener('DOMLoaded', (event) => {
domLoadedCallbackRef.current?.(event)
})
animation.addEventListener('data_ready', (event) => {
dataReadyCallbackRef.current?.(event)
})
animation.addEventListener('data_failed', (event) => {
dataFailedCallbackRef.current?.(event)
})
if (segmentsRef.current) {
animation.playSegments(segmentsRef.current, forceSegmentsRef.current)
}
if (isPausedRef.current) {
animation.pause()
}
hasSyncedPauseStateRef.current = false
return () => {
animation.destroy()
animationRef.current = null
hasSyncedPauseStateRef.current = false
}
}, [
animationData,
path,
renderer,
initialSegment,
name,
assetsPath,
rendererSettings,
])
useEffect(() => {
const animation = animationRef.current
if (!animation) {
return
}
animation.setSpeed(speed)
}, [speed])
useEffect(() => {
const animation = animationRef.current
if (!animation) {
return
}
animation.setDirection(direction)
}, [direction])
useEffect(() => {
const animation = animationRef.current
if (!animation) {
return
}
if (typeof loop === 'boolean') {
animation.setLoop(loop)
return
}
animation.loop = loop
}, [loop])
useEffect(() => {
const animation = animationRef.current
if (!animation || !segments) {
return
}
animation.playSegments(segments, forceSegments)
}, [segments, forceSegments])
useEffect(() => {
const animation = animationRef.current
if (!animation) {
return
}
if (!hasSyncedPauseStateRef.current) {
hasSyncedPauseStateRef.current = true
return
}
if (isPaused) {
animation.pause()
return
}
animation.play()
}, [isPaused])
return (
<div
ref={containerRef}
className={cn('relative h-full w-full overflow-hidden', className)}
{...divProps}
/>
)
},
)

View File

@@ -20,6 +20,7 @@ export interface SmartImageProps
imgClassName?: string
skeletonClassName?: string
fallbackClassName?: string
fallback?: ReactNode
fallbackSrc?: string
placeholderSrc?: string

View File

@@ -63,7 +63,7 @@ export function DesktopControl() {
return (
<div
className={
'flex h-design-100 w-full items-center gap-design-16 overflow-hidden'
'flex h-design-100 w-full items-center gap-design-12 overflow-hidden'
}
>
<div

View File

@@ -3,6 +3,9 @@ import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
import DesktopLoginModal from '@/features/game/modal/desktop/desktop-login-modal.tsx'
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx'
import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
export function PcEntry() {
return (
@@ -36,6 +39,12 @@ export function PcEntry() {
>
<DesktopControl />
</div>
{/*登录弹窗*/}
<DesktopLoginModal />
{/*注册弹窗 */}
{/*<DesktopRegisterModal />*/}
{/* 用户信息弹窗 */}
<DesktopUserInfoModal />
</>
)
}

View File

@@ -0,0 +1,101 @@
import { motion } from 'motion/react'
import { useState } from 'react'
import loginBg from '@/assets/system/login-bg.webp'
import rightImg from '@/assets/system/right.webp'
import { CenterModal } from '@/components/center-modal.tsx'
import { SmartImage } from '@/components/smart-image.tsx'
function DesktopLoginModal() {
const [open, setOpen] = useState(false)
function handleSubmit() {
setOpen(false)
}
return (
<CenterModal
open={open}
onClose={() => {}}
title={<div className={'modal-title-glow'}></div>}
titleAlign="center"
className={'w-design-980 h-design-540'}
>
<div
className={
'flex flex-col items-center justify-between gap-design-20 px-design-20'
}
>
<div
className={
'h-design-375 flex flex-col gap-design-45 w-full bg-[#060B0F]/50 p-design-50'
}
>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Akun/TEL:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
/>
</div>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Kata Sandi:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
</div>
<div className={'flex items-center justify-around'}>
<div className={'flex items-center gap-design-10'}>
<div
className={
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
}
>
<SmartImage alt={'right'} src={rightImg} />
</div>
<div className={'text-[#549195]'}>Daftar Akun</div>
</div>
<div className={'flex items-center gap-design-10'}>
<div
className={
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
}
>
<SmartImage alt={'right'} src={rightImg} />
</div>
<div className={'text-[#549195]'}>Ingat Kata Sandi</div>
</div>
</div>
</div>
<motion.div
onClick={handleSubmit}
whileTap={{ scale: 0.95 }}
style={{
backgroundImage: `url(${loginBg})`,
backgroundSize: '100% 100%',
}}
className={
'w-design-390 h-design-110 flex items-center justify-center text-design-32 modal-title-glow font-bold cursor-pointer'
}
>
MASUK
</motion.div>
</div>
</CenterModal>
)
}
export default DesktopLoginModal

View File

@@ -0,0 +1,125 @@
import { motion } from 'motion/react'
import { useState } from 'react'
import loginBg from '@/assets/system/login-bg.webp'
import rightImg from '@/assets/system/right.webp'
import { CenterModal } from '@/components/center-modal.tsx'
import { SmartImage } from '@/components/smart-image.tsx'
function DesktopRegisterModal() {
const [open, setOpen] = useState(true)
function handleSubmit() {
setOpen(false)
}
return (
<CenterModal
open={open}
onClose={() => {}}
title={<div className={'modal-title-glow'}></div>}
titleAlign="center"
className={'w-design-980 h-design-740'}
>
<div
className={'flex flex-col items-center justify-between px-design-20'}
>
<div
className={
'h-design-490 flex flex-col gap-design-30 w-full bg-[#060B0F]/50 p-design-50'
}
>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Akun/TEL:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
/>
</div>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Kata Sandi:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
</div>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Kata Sandi:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
</div>
<div className={'flex items-center'}>
<div
className={
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
}
>
Kata Sandi:
</div>
<input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
</div>
<div className={'flex items-center justify-around'}>
<div className={'flex items-center gap-design-10'}>
<div
className={
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
}
>
<SmartImage alt={'right'} src={rightImg} />
</div>
<div className={'text-[#549195]'}>Daftar Akun</div>
</div>
<div className={'flex items-center gap-design-10'}>
<div
className={
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
}
>
<SmartImage alt={'right'} src={rightImg} />
</div>
<div className={'text-[#549195]'}>Ingat Kata Sandi</div>
</div>
</div>
</div>
<motion.div
onClick={handleSubmit}
whileTap={{ scale: 0.95 }}
style={{
backgroundImage: `url(${loginBg})`,
backgroundSize: '100% 100%',
}}
className={
'w-design-390 h-design-110 flex items-center justify-center text-design-32 modal-title-glow font-bold cursor-pointer'
}
>
MASUK
</motion.div>
</div>
</CenterModal>
)
}
export default DesktopRegisterModal

View File

@@ -0,0 +1,136 @@
import { CircleUserRound, Mail } from 'lucide-react'
import { useState } from 'react'
import userInfoBg from '@/assets/system/userInfo-bg.webp'
import { CenterModal } from '@/components/center-modal.tsx'
import { cn } from '@/lib/untils'
type UserInfoTabKey = 'profile' | 'message'
const USER_INFO_TABS: Array<{
key: UserInfoTabKey
label: string
icon: typeof CircleUserRound
}> = [
{
key: 'profile',
label: '个人信息',
icon: CircleUserRound,
},
{
key: 'message',
label: '站内消息',
icon: Mail,
},
]
function DesktopUserInfoModal() {
const [open, setOpen] = useState(true)
const [activeTab, setActiveTab] = useState<UserInfoTabKey>('profile')
function handleSubmit() {
setOpen(false)
}
return (
<CenterModal
open={open}
onClose={handleSubmit}
title={
<div className={'modal-title-glow text-design-26'}>Biomond Balance</div>
}
isNormalBg={true}
titleAlign="left"
className={'w-design-980 h-design-590'}
>
<div className={'relative flex h-[96%] w-full'}>
<div className={'relative w-design-230 shrink-0'}>
<div
aria-hidden="true"
className="absolute right-0 top-0 h-full w-[calc(var(--design-unit)*2)] bg-[linear-gradient(180deg,rgba(68,244,255,0)_0%,rgba(68,244,255,0.55)_12%,rgba(130,255,255,0.95)_50%,rgba(68,244,255,0.55)_88%,rgba(68,244,255,0)_100%)] shadow-[0_0_calc(var(--design-unit)*8)_rgba(49,208,255,0.45)]"
/>
{USER_INFO_TABS.map((tab) => {
const Icon = tab.icon
const isActive = tab.key === activeTab
return (
<button
key={tab.key}
type="button"
onClick={() => setActiveTab(tab.key)}
className={cn(
'relative flex h-design-150 w-full flex-col items-center justify-center gap-design-10 overflow-hidden transition',
isActive
? 'text-[#FEEEB0]'
: 'text-[#58ADAF] hover:text-[#BFEAEC]',
)}
>
{isActive ? (
<span
aria-hidden="true"
className="absolute inset-y-0 right-0 w-full bg-[linear-gradient(to_left,rgba(254,238,176,0.46)_0%,rgba(254,238,176,0.28)_42%,rgba(254,238,176,0.12)_68%,rgba(254,238,176,0)_100%)]"
/>
) : null}
{isActive ? (
<span
aria-hidden="true"
className="absolute right-0 top-1/2 h-[72%] w-[calc(var(--design-unit)*3)] -translate-y-1/2 rounded-l-full bg-[linear-gradient(180deg,rgba(255,248,214,0.96)_0%,rgba(254,238,176,0.92)_48%,rgba(232,188,112,0.88)_100%)] shadow-[-2px_0_calc(var(--design-unit)*8)_rgba(254,238,176,0.36)]"
/>
) : null}
<Icon
size={60}
className={cn(
'relative z-10 transition',
isActive &&
'drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(254,238,176,0.5)]',
)}
/>
<div
className={cn(
'relative z-10 text-design-24',
isActive && 'modal-title-gold-glow',
)}
>
{tab.label}
</div>
</button>
)
})}
</div>
<div className={'flex-1'}>
<div
className={
'flex h-full w-full items-start justify-start bg-top bg-no-repeat px-design-40 py-design-32'
}
style={{
backgroundImage: `url(${userInfoBg})`,
backgroundSize: '120% 100%',
}}
>
{activeTab === 'profile' ? (
<div className="space-y-3 text-design-22 text-[#D7FAFF]">
<div className="modal-title-gold-glow text-design-28">
</div>
<div>UID: 10009231</div>
<div>账户等级: VIP 3</div>
<div>绑定手机: 138****8899</div>
</div>
) : (
<div className="space-y-3 text-design-22 text-[#D7FAFF]">
<div className="modal-title-gold-glow text-design-28">
</div>
<div></div>
</div>
)}
</div>
</div>
</div>
</CenterModal>
)
}
export default DesktopUserInfoModal

View File

@@ -42,10 +42,6 @@
min-width: calc(var(--design-unit) * --value(integer));
}
@utility max-w-design-* {
max-width: calc(var(--design-unit) * --value(integer));
}
@utility text-design-* {
font-size: calc(var(--design-unit) * --value(integer));
}
@@ -54,8 +50,13 @@
gap: calc(var(--design-unit) * --value(integer));
}
@utility pt-design-* {
padding-top: calc(var(--design-unit) * --value(integer));
}
@utility px-design-* {
padding-inline: calc(var(--design-unit) * --value(integer));
padding-left: calc(var(--design-unit) * --value(integer));
padding-right: calc(var(--design-unit) * --value(integer));
}
@utility p-design-* {
@@ -108,6 +109,10 @@
top: calc(var(--design-unit) * --value(integer));
}
@utility right-design-* {
right: calc(var(--design-unit) * --value(integer));
}
@layer base {
html {
@apply h-full w-full text-design-16;
@@ -132,6 +137,25 @@
linear-gradient(180deg, #07111f 0%, #040812 100%);
color: #f8fafc;
}
input {
@apply border border-transparent bg-[#135E65] text-[#D9FFFF] py-design-15 px-design-30 text-design-20 rounded-md outline-none transition;
}
input::placeholder {
color: rgba(116, 173, 175, 0.72);
}
input:focus,
input:focus-visible {
border-color: rgba(110, 255, 255, 0.72);
outline: none;
box-shadow:
0 0 0 calc(var(--design-unit) * 1.5) rgba(110, 255, 255, 0.16),
0 0 calc(var(--design-unit) * 8) rgba(48, 214, 255, 0.36),
0 0 calc(var(--design-unit) * 18) rgba(18, 162, 255, 0.22),
inset 0 0 calc(var(--design-unit) * 6) rgba(110, 255, 255, 0.08);
}
}
@layer utilities {
@@ -198,6 +222,28 @@
drop-shadow(0 0 calc(var(--design-unit) * 14) rgba(49, 208, 255, 0.28));
}
.modal-close-glow {
filter: drop-shadow(
0 0 calc(var(--design-unit) * 4) rgba(110, 255, 255, 0.95)
)
drop-shadow(0 0 calc(var(--design-unit) * 12) rgba(48, 214, 255, 0.78))
drop-shadow(0 0 calc(var(--design-unit) * 24) rgba(18, 162, 255, 0.5));
}
.modal-title-glow {
text-shadow:
0 0 calc(var(--design-unit) * 2) rgba(224, 255, 255, 0.95),
0 0 calc(var(--design-unit) * 6) rgba(108, 247, 255, 0.82),
0 0 calc(var(--design-unit) * 14) rgba(48, 214, 255, 0.58);
}
.modal-title-gold-glow {
text-shadow:
0 0 calc(var(--design-unit) * 2) rgba(255, 248, 235, 0.9),
0 0 calc(var(--design-unit) * 8) rgba(254, 238, 176, 0.64),
0 0 calc(var(--design-unit) * 16) rgba(254, 238, 176, 0.38);
}
.desktop-control-chip {
margin-left: calc(var(--design-unit) * -4);
margin-right: calc(var(--design-unit) * -18);
@@ -209,7 +255,7 @@
}
.desktop-control-actions {
margin-left: calc(var(--design-unit) * -18);
margin-left: calc(var(--design-unit) * -16);
margin-right: 0;
}

View File

@@ -12,6 +12,11 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 9999,
host: '0.0.0.0',
allowedHosts: ['darlena-nonexpiring-cathie.ngrok-free.dev'],
},
plugins: [
tanstackRouter({
target: 'react',