feat(space/app): app 진입부 및 space 몰입 환경(HUD/Tools) 프리미엄 UI 리팩토링
맥락: - 기존 app 대시보드와 space 화면의 UI가 SaaS 툴처럼 딱딱하고 투박하여, 유저가 기꺼이 지갑을 열 만한 몰입감과 고급스러움(Premium feel)이 부족함. - 인지적 과부하를 줄이기 위해 제안된 '첫 5분 행동(Micro-step)'이 타이머 영역에 묻혀 있어 행동 유발 효과가 미미함. 변경사항: - app: 컨테이너 박스를 제거하고 전체 배경 화면(Immersive Background)과 Glassmorphism을 활용한 1.5 Step 진입 플로우로 전면 개편. - space/hud: 하단의 두꺼운 타이머 패널을 초박형(Slim) 글라스 알약 형태로 축소하여 배경 씬의 개방감 확보. - space/hud: 목표(Goal)와 첫 단계(Micro-step)를 분리하여 좌측 상단의 우아한 Floating UI로 재배치하고, 체크 완료 시 사라지는 도파민 인터랙션 추가. - space/tools: 흩어져 있던 노트, 사운드, 설정 도구들을 우측 레일(Right-Rail)로 통합하고 팝오버 디자인을 고급화함. - ui/contrast: 밝은 배경에서도 텍스트가 잘 보이도록 좌측 상단 비네팅(Vignette) 및 다중 텍스트 그림자(Multi-layered Shadow) 효과 적용. 검증: - npm run build 정상 통과 확인. - 브라우저 상에서 micro-step 완료 애니메이션 및 도구막대 팝오버 슬라이드 동작 확인. 세션-상태: app 진입부터 space 몰입까지의 코어 UX/UI 하이엔드 개편 완료. 세션-다음: 프로 요금제(PRO) 전환 유도(Paywall) 흐름 및 상세 분석 리포트(Analytics) 뷰 구현. 세션-리스크: 없음.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
import { usePlanTier } from '@/entities/plan';
|
||||
import type { RecentThought } from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
||||
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from './types';
|
||||
import type { PlanTier } from '@/entities/plan';
|
||||
|
||||
interface UseSpaceToolsDockHandlersParams {
|
||||
setIdle: (idle: boolean) => void;
|
||||
@@ -44,7 +44,7 @@ export const useSpaceToolsDockHandlers = ({
|
||||
}: UseSpaceToolsDockHandlersParams) => {
|
||||
const { toolsDock } = copy.space;
|
||||
const [noteDraft, setNoteDraft] = useState('');
|
||||
const [plan, setPlan] = useState<PlanTier>('normal');
|
||||
const { plan, setPlan } = usePlanTier();
|
||||
|
||||
const openUtilityPanel = useCallback((panel: SpaceUtilityPanelId) => {
|
||||
setIdle(false);
|
||||
@@ -148,11 +148,11 @@ export const useSpaceToolsDockHandlers = ({
|
||||
|
||||
const handleSelectProFeature = useCallback((featureId: string) => {
|
||||
const label =
|
||||
featureId === 'scene-packs'
|
||||
? toolsDock.featureLabels.scenePacks
|
||||
: featureId === 'sound-packs'
|
||||
? toolsDock.featureLabels.soundPacks
|
||||
: toolsDock.featureLabels.profiles;
|
||||
featureId === 'daily-plan'
|
||||
? toolsDock.featureLabels.dailyPlan
|
||||
: featureId === 'rituals'
|
||||
? toolsDock.featureLabels.rituals
|
||||
: toolsDock.featureLabels.weeklyReview;
|
||||
|
||||
onStatusMessage({ message: toolsDock.proFeaturePending(label) });
|
||||
}, [onStatusMessage, toolsDock.featureLabels]);
|
||||
|
||||
@@ -79,82 +79,32 @@ export const FocusModeAnchors = ({
|
||||
type="button"
|
||||
aria-label={copy.space.toolsDock.popoverCloseAria}
|
||||
onClick={onClosePopover}
|
||||
className="fixed inset-0 z-30"
|
||||
className="fixed inset-0 z-30 cursor-default"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FocusRightRail
|
||||
isIdle={isIdle}
|
||||
thoughtCount={thoughtCount}
|
||||
openPopover={openPopover}
|
||||
noteDraft={noteDraft}
|
||||
selectedSoundLabel={selectedSoundLabel}
|
||||
isSoundMuted={isSoundMuted}
|
||||
soundVolume={soundVolume}
|
||||
volumeFeedback={volumeFeedback}
|
||||
quickSoundPresets={quickSoundPresets}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onOpenInbox={onOpenInbox}
|
||||
onOpenControlCenter={onOpenControlCenter}
|
||||
onToggleNotes={onToggleNotes}
|
||||
onToggleSound={onToggleSound}
|
||||
onNoteDraftChange={onNoteDraftChange}
|
||||
onNoteSubmit={onNoteSubmit}
|
||||
onToggleMute={onToggleMute}
|
||||
onVolumeChange={onVolumeChange}
|
||||
onVolumeKeyDown={onVolumeKeyDown}
|
||||
onSelectPreset={onSelectPreset}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
anchorContainerClassName,
|
||||
'left-[calc(env(safe-area-inset-left,0px)+0.75rem)]',
|
||||
isIdle ? 'opacity-34' : 'opacity-82',
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div aria-hidden className={anchorHaloClassName} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleNotes}
|
||||
className={anchorButtonClassName}
|
||||
>
|
||||
<span aria-hidden className="text-white/82">{ANCHOR_ICON.notes}</span>
|
||||
<span>{copy.space.toolsDock.notesButton} {formatThoughtCount(thoughtCount)}</span>
|
||||
<span aria-hidden className="text-white/60">▾</span>
|
||||
</button>
|
||||
|
||||
{openPopover === 'notes' ? (
|
||||
<QuickNotesPopover
|
||||
noteDraft={noteDraft}
|
||||
onDraftChange={onNoteDraftChange}
|
||||
onDraftEnter={onNoteSubmit}
|
||||
onSubmit={onNoteSubmit}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
anchorContainerClassName,
|
||||
'right-[calc(env(safe-area-inset-right,0px)+0.75rem)]',
|
||||
isIdle ? 'opacity-34' : 'opacity-82',
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div aria-hidden className={anchorHaloClassName} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSound}
|
||||
className={anchorButtonClassName}
|
||||
>
|
||||
<span aria-hidden className="text-white/82">{ANCHOR_ICON.sound}</span>
|
||||
<span className="max-w-[132px] truncate">{selectedSoundLabel}</span>
|
||||
<span aria-hidden className="text-white/60">▾</span>
|
||||
</button>
|
||||
|
||||
{openPopover === 'sound' ? (
|
||||
<QuickSoundPopover
|
||||
selectedSoundLabel={selectedSoundLabel}
|
||||
isSoundMuted={isSoundMuted}
|
||||
soundVolume={soundVolume}
|
||||
volumeFeedback={volumeFeedback}
|
||||
quickSoundPresets={quickSoundPresets}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onToggleMute={onToggleMute}
|
||||
onVolumeChange={onVolumeChange}
|
||||
onVolumeKeyDown={onVolumeKeyDown}
|
||||
onSelectPreset={onSelectPreset}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,53 +1,179 @@
|
||||
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
import type { SoundPreset } from '@/entities/session';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { formatThoughtCount, RAIL_ICON } from './constants';
|
||||
import type { SpaceAnchorPopoverId } from '../model/types';
|
||||
import { formatThoughtCount, RAIL_ICON, ANCHOR_ICON } from './constants';
|
||||
import { QuickNotesPopover } from './popovers/QuickNotesPopover';
|
||||
import { QuickSoundPopover } from './popovers/QuickSoundPopover';
|
||||
|
||||
interface FocusRightRailProps {
|
||||
isIdle: boolean;
|
||||
thoughtCount: number;
|
||||
openPopover: SpaceAnchorPopoverId | null;
|
||||
noteDraft: string;
|
||||
selectedSoundLabel: string;
|
||||
isSoundMuted: boolean;
|
||||
soundVolume: number;
|
||||
volumeFeedback: string | null;
|
||||
quickSoundPresets: SoundPreset[];
|
||||
selectedPresetId: string;
|
||||
onOpenInbox: () => void;
|
||||
onOpenControlCenter: () => void;
|
||||
onToggleNotes: () => void;
|
||||
onToggleSound: () => void;
|
||||
onNoteDraftChange: (value: string) => void;
|
||||
onNoteSubmit: () => void;
|
||||
onToggleMute: () => void;
|
||||
onVolumeChange: (nextVolume: number) => void;
|
||||
onVolumeKeyDown: (event: ReactKeyboardEvent<HTMLInputElement>) => void;
|
||||
onSelectPreset: (presetId: string) => void;
|
||||
}
|
||||
|
||||
export const FocusRightRail = ({
|
||||
isIdle,
|
||||
thoughtCount,
|
||||
openPopover,
|
||||
noteDraft,
|
||||
selectedSoundLabel,
|
||||
isSoundMuted,
|
||||
soundVolume,
|
||||
volumeFeedback,
|
||||
quickSoundPresets,
|
||||
selectedPresetId,
|
||||
onOpenInbox,
|
||||
onOpenControlCenter,
|
||||
onToggleNotes,
|
||||
onToggleSound,
|
||||
onNoteDraftChange,
|
||||
onNoteSubmit,
|
||||
onToggleMute,
|
||||
onVolumeChange,
|
||||
onVolumeKeyDown,
|
||||
onSelectPreset,
|
||||
}: FocusRightRailProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-30 transition-opacity right-[calc(env(safe-area-inset-right,0px)+0.75rem)] top-1/2 -translate-y-1/2',
|
||||
isIdle ? 'opacity-34' : 'opacity-78',
|
||||
'fixed z-30 transition-all duration-500 right-[calc(env(safe-area-inset-right,0px)+1.5rem)] top-1/2 -translate-y-1/2',
|
||||
isIdle ? 'opacity-0 translate-x-4 pointer-events-none' : 'opacity-100 translate-x-0',
|
||||
)}
|
||||
>
|
||||
<div className="rounded-2xl border border-white/14 bg-black/22 p-1.5 backdrop-blur-md">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative flex flex-col gap-2 rounded-full border border-white/10 bg-black/20 p-2.5 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.2)]">
|
||||
|
||||
{/* Notes Toggle */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={copy.space.toolsDock.notesButton}
|
||||
onClick={onToggleNotes}
|
||||
className={cn(
|
||||
"relative inline-flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200",
|
||||
openPopover === 'notes' ? "bg-white/20 text-white shadow-inner" : "bg-transparent text-white/70 hover:bg-white/10 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{ANCHOR_ICON.notes}
|
||||
</button>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="bg-black/60 backdrop-blur-md text-white text-[11px] px-2 py-1 rounded-md whitespace-nowrap">
|
||||
{copy.space.toolsDock.notesButton}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{openPopover === 'notes' ? (
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-4">
|
||||
<QuickNotesPopover
|
||||
noteDraft={noteDraft}
|
||||
onDraftChange={onNoteDraftChange}
|
||||
onDraftEnter={onNoteSubmit}
|
||||
onSubmit={onNoteSubmit}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Sound Toggle */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="사운드"
|
||||
onClick={onToggleSound}
|
||||
className={cn(
|
||||
"relative inline-flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200",
|
||||
openPopover === 'sound' ? "bg-white/20 text-white shadow-inner" : "bg-transparent text-white/70 hover:bg-white/10 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{ANCHOR_ICON.sound}
|
||||
</button>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="bg-black/60 backdrop-blur-md text-white text-[11px] px-2 py-1 rounded-md whitespace-nowrap">
|
||||
사운드
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{openPopover === 'sound' ? (
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-4">
|
||||
<QuickSoundPopover
|
||||
selectedSoundLabel={selectedSoundLabel}
|
||||
isSoundMuted={isSoundMuted}
|
||||
soundVolume={soundVolume}
|
||||
volumeFeedback={volumeFeedback}
|
||||
quickSoundPresets={quickSoundPresets}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onToggleMute={onToggleMute}
|
||||
onVolumeChange={onVolumeChange}
|
||||
onVolumeKeyDown={onVolumeKeyDown}
|
||||
onSelectPreset={onSelectPreset}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="w-6 h-px bg-white/10 mx-auto my-1" />
|
||||
|
||||
{/* Inbox Button */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={copy.space.inbox.openInboxAriaLabel}
|
||||
title={copy.space.inbox.openInboxTitle}
|
||||
onClick={onOpenInbox}
|
||||
className="relative inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
|
||||
className="relative inline-flex h-10 w-10 items-center justify-center rounded-full bg-transparent text-white/70 transition-colors hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
{RAIL_ICON.inbox}
|
||||
{thoughtCount > 0 ? (
|
||||
<span className="absolute -right-1 -top-1 inline-flex min-w-[0.95rem] items-center justify-center rounded-full bg-sky-200/28 px-1 py-0.5 text-[8px] font-semibold text-sky-50">
|
||||
<span className="absolute 0 top-0 right-0 inline-flex min-w-[1rem] items-center justify-center rounded-full bg-brand-primary px-1 py-0.5 text-[9px] font-bold text-white shadow-sm ring-2 ring-black/20">
|
||||
{formatThoughtCount(thoughtCount)}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="bg-black/60 backdrop-blur-md text-white text-[11px] px-2 py-1 rounded-md whitespace-nowrap">
|
||||
{copy.space.inbox.openInboxTitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Center Button */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={copy.space.rightRail.openQuickControlsAriaLabel}
|
||||
title={copy.space.rightRail.openQuickControlsTitle}
|
||||
onClick={onOpenControlCenter}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-transparent text-white/70 transition-colors hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
{RAIL_ICON.controlCenter}
|
||||
</button>
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span className="bg-black/60 backdrop-blur-md text-white text-[11px] px-2 py-1 rounded-md whitespace-nowrap">
|
||||
{copy.space.rightRail.openQuickControlsTitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,11 +15,10 @@ export const QuickNotesPopover = ({
|
||||
}: QuickNotesPopoverProps) => {
|
||||
return (
|
||||
<div
|
||||
className="mb-2 w-[min(320px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
|
||||
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', left: 0 }}
|
||||
className="mb-2 w-[320px] rounded-[1.5rem] border border-white/10 bg-black/30 p-5 shadow-[0_24px_60px_rgba(0,0,0,0.4)] backdrop-blur-2xl animate-in fade-in zoom-in-95 slide-in-from-right-4 duration-300 origin-right"
|
||||
>
|
||||
<p className="text-[11px] text-white/56">{copy.space.quickNotes.title}</p>
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<p className="text-[11px] font-medium uppercase tracking-widest text-white/50">{copy.space.quickNotes.title}</p>
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<input
|
||||
value={noteDraft}
|
||||
onChange={(event) => onDraftChange(event.target.value)}
|
||||
@@ -32,17 +31,21 @@ export const QuickNotesPopover = ({
|
||||
onDraftEnter();
|
||||
}}
|
||||
placeholder={copy.space.quickNotes.placeholder}
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-white/14 bg-white/[0.04] px-2.5 text-xs text-white placeholder:text-white/38 focus:border-sky-200/42 focus:outline-none"
|
||||
autoFocus
|
||||
className="w-full border-b border-white/20 bg-transparent pb-2 text-sm text-white placeholder:text-white/30 transition-colors focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
className="h-8 rounded-lg border border-sky-200/34 bg-sky-200/14 px-2.5 text-xs text-white/88"
|
||||
>
|
||||
{copy.space.quickNotes.submit}
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] text-white/40">{copy.space.quickNotes.hint}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={!noteDraft.trim()}
|
||||
className="rounded-full bg-white/10 px-4 py-1.5 text-xs font-medium text-white transition-all hover:bg-white/20 active:scale-95 disabled:opacity-30"
|
||||
>
|
||||
{copy.space.quickNotes.submit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-white/52">{copy.space.quickNotes.hint}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,60 +30,71 @@ export const QuickSoundPopover = ({
|
||||
}: QuickSoundPopoverProps) => {
|
||||
return (
|
||||
<div
|
||||
className="mb-2 w-[min(288px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
|
||||
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', right: 0 }}
|
||||
className="mb-2 w-[320px] rounded-[1.5rem] border border-white/10 bg-black/30 p-5 shadow-[0_24px_60px_rgba(0,0,0,0.4)] backdrop-blur-2xl animate-in fade-in zoom-in-95 slide-in-from-right-4 duration-300 origin-right"
|
||||
>
|
||||
<p className="text-[11px] text-white/56">{copy.space.quickSound.currentSound}</p>
|
||||
<p className="mt-1 truncate text-sm font-medium text-white/88">{selectedSoundLabel}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[11px] font-medium uppercase tracking-widest text-white/50">{copy.space.quickSound.currentSound}</p>
|
||||
<span className="text-[11px] font-medium text-white/90 bg-white/10 px-2 py-0.5 rounded-md">
|
||||
{volumeFeedback ?? (isSoundMuted ? '0%' : `${soundVolume}%`)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 truncate text-base font-medium text-white/90">{selectedSoundLabel}</p>
|
||||
|
||||
<div className="mt-3 rounded-xl border border-white/14 bg-white/[0.04] px-2.5 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isSoundMuted ? copy.space.quickSound.unmuteAriaLabel : copy.space.quickSound.muteAriaLabel}
|
||||
onClick={onToggleMute}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/16 bg-white/[0.05] text-xs text-white/80 transition-colors hover:bg-white/[0.12]"
|
||||
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/10 text-[13px] text-white/80 transition-all hover:bg-white/20 active:scale-95"
|
||||
>
|
||||
🔇
|
||||
{isSoundMuted ? '🔇' : '🔊'}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={soundVolume}
|
||||
onChange={(event) => onVolumeChange(Number(event.target.value))}
|
||||
onKeyDown={onVolumeKeyDown}
|
||||
aria-label={copy.space.quickSound.volumeAriaLabel}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-white/18 accent-sky-200"
|
||||
/>
|
||||
<span className="w-9 text-right text-[11px] text-white/66">
|
||||
{volumeFeedback ?? (isSoundMuted ? '0%' : `${soundVolume}%`)}
|
||||
</span>
|
||||
<div className="relative flex w-full items-center">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={soundVolume}
|
||||
onChange={(event) => onVolumeChange(Number(event.target.value))}
|
||||
onKeyDown={onVolumeKeyDown}
|
||||
aria-label={copy.space.quickSound.volumeAriaLabel}
|
||||
className="absolute z-10 w-full cursor-pointer appearance-none bg-transparent accent-white outline-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md"
|
||||
/>
|
||||
<div className="h-1.5 w-full rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-white/90 transition-all duration-150 ease-out"
|
||||
style={{ width: `${soundVolume}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-[11px] text-white/56">{copy.space.quickSound.quickSwitch}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{quickSoundPresets.map((preset) => {
|
||||
const selected = preset.id === selectedPresetId;
|
||||
<div className="mt-6">
|
||||
<p className="text-[10px] font-medium uppercase tracking-widest text-white/40 mb-3">{copy.space.quickSound.quickSwitch}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{quickSoundPresets.map((preset) => {
|
||||
const selected = preset.id === selectedPresetId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => onSelectPreset(preset.id)}
|
||||
className={cn(
|
||||
'rounded-full border px-2.5 py-0.5 text-[10px] transition-colors',
|
||||
selected
|
||||
? 'border-sky-200/34 bg-sky-200/14 text-white/90'
|
||||
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => onSelectPreset(preset.id)}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-[11px] font-medium transition-all active:scale-95',
|
||||
selected
|
||||
? 'border-white/40 bg-white/20 text-white shadow-sm'
|
||||
: 'border-transparent bg-white/5 text-white/60 hover:bg-white/15 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user