refactor(quick-controls): 사운드 역할 분리와 패널 레이아웃·오버레이 개선
This commit is contained in:
@@ -106,11 +106,14 @@ export const ControlCenterSheetWidget = ({
|
|||||||
}, [selectedSoundPresetId, soundPresets]);
|
}, [selectedSoundPresetId, soundPresets]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-5">
|
||||||
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/22 p-3 backdrop-blur-md">
|
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
||||||
<SectionTitle title="Scene" description={selectedRoom?.name ?? '공간'} />
|
<SectionTitle title="Scene" description={selectedRoom?.name ?? '공간'} />
|
||||||
<div
|
<div
|
||||||
className={cn('-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 snap-x snap-mandatory', reducedMotion ? '' : 'scroll-smooth')}
|
className={cn(
|
||||||
|
'-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 snap-x snap-mandatory',
|
||||||
|
reducedMotion ? '' : 'scroll-smooth',
|
||||||
|
)}
|
||||||
style={{ scrollBehavior: reducedMotion ? 'auto' : 'smooth' }}
|
style={{ scrollBehavior: reducedMotion ? 'auto' : 'smooth' }}
|
||||||
>
|
>
|
||||||
{rooms.slice(0, 6).map((room) => {
|
{rooms.slice(0, 6).map((room) => {
|
||||||
@@ -149,9 +152,9 @@ export const ControlCenterSheetWidget = ({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/22 p-3 backdrop-blur-md">
|
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
||||||
<SectionTitle title="Time" description={selectedTimerLabel} />
|
<SectionTitle title="Time" description={selectedTimerLabel} />
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{timerPresets.slice(0, 3).map((preset) => {
|
{timerPresets.slice(0, 3).map((preset) => {
|
||||||
const selected = preset.label === selectedTimerLabel;
|
const selected = preset.label === selectedTimerLabel;
|
||||||
const locked = !isPro && PRO_LOCKED_TIMER_LABELS.includes(preset.label);
|
const locked = !isPro && PRO_LOCKED_TIMER_LABELS.includes(preset.label);
|
||||||
@@ -169,7 +172,7 @@ export const ControlCenterSheetWidget = ({
|
|||||||
onSelectTimer(preset.label);
|
onSelectTimer(preset.label);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative rounded-xl border px-2 py-2 text-xs',
|
'relative rounded-xl border px-3 py-2.5 text-xs',
|
||||||
colorMotionClass,
|
colorMotionClass,
|
||||||
selected
|
selected
|
||||||
? 'border-sky-200/42 bg-sky-200/16 text-white'
|
? 'border-sky-200/42 bg-sky-200/16 text-white'
|
||||||
@@ -184,9 +187,9 @@ export const ControlCenterSheetWidget = ({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/22 p-3 backdrop-blur-md">
|
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
||||||
<SectionTitle title="Sound" description={selectedSound?.label ?? '기본'} />
|
<SectionTitle title="Sound" description={selectedSound?.label ?? '기본'} />
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{soundPresets.slice(0, 6).map((preset) => {
|
{soundPresets.slice(0, 6).map((preset) => {
|
||||||
const selected = preset.id === selectedSoundPresetId;
|
const selected = preset.id === selectedSoundPresetId;
|
||||||
const locked = !isPro && PRO_LOCKED_SOUND_IDS.includes(preset.id);
|
const locked = !isPro && PRO_LOCKED_SOUND_IDS.includes(preset.id);
|
||||||
@@ -204,7 +207,7 @@ export const ControlCenterSheetWidget = ({
|
|||||||
onSelectSound(preset.id);
|
onSelectSound(preset.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full border px-3 py-1.5 text-xs',
|
'rounded-xl border px-3 py-2 text-xs',
|
||||||
colorMotionClass,
|
colorMotionClass,
|
||||||
selected
|
selected
|
||||||
? 'border-sky-200/42 bg-sky-200/16 text-white'
|
? 'border-sky-200/42 bg-sky-200/16 text-white'
|
||||||
@@ -219,9 +222,9 @@ export const ControlCenterSheetWidget = ({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/22 p-3 backdrop-blur-md">
|
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
||||||
<SectionTitle title="Preset Packs" description="원탭 조합" />
|
<SectionTitle title="Preset Packs" description="원탭 조합" />
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-2.5">
|
||||||
{QUICK_PACKS.map((pack) => {
|
{QUICK_PACKS.map((pack) => {
|
||||||
const locked = !isPro && pack.locked;
|
const locked = !isPro && pack.locked;
|
||||||
|
|
||||||
@@ -238,7 +241,7 @@ export const ControlCenterSheetWidget = ({
|
|||||||
onApplyPack(pack.id);
|
onApplyPack(pack.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative rounded-xl border border-white/16 bg-white/[0.04] px-3 py-2 text-left hover:bg-white/[0.1]',
|
'relative rounded-xl border border-white/16 bg-white/[0.04] px-3.5 py-2.5 text-left hover:bg-white/[0.1]',
|
||||||
colorMotionClass,
|
colorMotionClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface SpaceSideSheetProps {
|
|||||||
widthClassName?: string;
|
widthClassName?: string;
|
||||||
dismissible?: boolean;
|
dismissible?: boolean;
|
||||||
headerAction?: ReactNode;
|
headerAction?: ReactNode;
|
||||||
|
overlayClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceSideSheet = ({
|
export const SpaceSideSheet = ({
|
||||||
@@ -26,6 +27,7 @@ export const SpaceSideSheet = ({
|
|||||||
widthClassName,
|
widthClassName,
|
||||||
dismissible = true,
|
dismissible = true,
|
||||||
headerAction,
|
headerAction,
|
||||||
|
overlayClassName,
|
||||||
}: SpaceSideSheetProps) => {
|
}: SpaceSideSheetProps) => {
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const transitionMs = reducedMotion ? 0 : 260;
|
const transitionMs = reducedMotion ? 0 : 260;
|
||||||
@@ -97,6 +99,7 @@ export const SpaceSideSheet = ({
|
|||||||
'fixed inset-0 z-40 bg-slate-950/14 backdrop-blur-[1px] transition-opacity',
|
'fixed inset-0 z-40 bg-slate-950/14 backdrop-blur-[1px] transition-opacity',
|
||||||
reducedMotion ? 'duration-0' : 'duration-[240ms]',
|
reducedMotion ? 'duration-0' : 'duration-[240ms]',
|
||||||
visible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
visible ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||||
|
overlayClassName,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -106,6 +109,7 @@ export const SpaceSideSheet = ({
|
|||||||
'fixed inset-0 z-40 bg-slate-950/8 backdrop-blur-[1px] transition-opacity',
|
'fixed inset-0 z-40 bg-slate-950/8 backdrop-blur-[1px] transition-opacity',
|
||||||
reducedMotion ? 'duration-0' : 'duration-[240ms]',
|
reducedMotion ? 'duration-0' : 'duration-[240ms]',
|
||||||
visible ? 'opacity-100' : 'opacity-0',
|
visible ? 'opacity-100' : 'opacity-0',
|
||||||
|
overlayClassName,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
21
src/widgets/space-tools-dock/model/getQuickSoundPresets.ts
Normal file
21
src/widgets/space-tools-dock/model/getQuickSoundPresets.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { PRO_LOCKED_SOUND_IDS } from '@/entities/plan';
|
||||||
|
import type { SoundPreset } from '@/entities/session';
|
||||||
|
|
||||||
|
const QUICK_SOUND_FALLBACK_IDS = ['rain-focus', 'deep-white', 'silent'] as const;
|
||||||
|
|
||||||
|
const isQuickAvailablePreset = (preset: SoundPreset | undefined): preset is SoundPreset => {
|
||||||
|
if (!preset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !PRO_LOCKED_SOUND_IDS.includes(preset.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuickSoundPresets = (presets: SoundPreset[], selectedPresetId: string) => {
|
||||||
|
const uniquePresetIds = Array.from(new Set([selectedPresetId, ...QUICK_SOUND_FALLBACK_IDS]));
|
||||||
|
|
||||||
|
return uniquePresetIds
|
||||||
|
.map((presetId) => presets.find((preset) => preset.id === presetId))
|
||||||
|
.filter(isQuickAvailablePreset)
|
||||||
|
.slice(0, 3);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import { ControlCenterSheetWidget } from '@/widgets/control-center-sheet';
|
|||||||
import { SpaceSideSheet } from '@/widgets/space-sheet-shell';
|
import { SpaceSideSheet } from '@/widgets/space-sheet-shell';
|
||||||
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types';
|
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from '../model/types';
|
||||||
import { applyQuickPack } from '../model/applyQuickPack';
|
import { applyQuickPack } from '../model/applyQuickPack';
|
||||||
|
import { getQuickSoundPresets } from '../model/getQuickSoundPresets';
|
||||||
import { ANCHOR_ICON, formatThoughtCount, RAIL_ICON, UTILITY_PANEL_TITLE } from './constants';
|
import { ANCHOR_ICON, formatThoughtCount, RAIL_ICON, UTILITY_PANEL_TITLE } from './constants';
|
||||||
import { InboxToolPanel } from './panels/InboxToolPanel';
|
import { InboxToolPanel } from './panels/InboxToolPanel';
|
||||||
interface SpaceToolsDockWidgetProps {
|
interface SpaceToolsDockWidgetProps {
|
||||||
@@ -68,6 +69,10 @@ export const SpaceToolsDockWidget = ({
|
|||||||
);
|
);
|
||||||
}, [selectedPresetId]);
|
}, [selectedPresetId]);
|
||||||
|
|
||||||
|
const quickSoundPresets = useMemo(() => {
|
||||||
|
return getQuickSoundPresets(SOUND_PRESETS, selectedPresetId);
|
||||||
|
}, [selectedPresetId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!openPopover) {
|
if (!openPopover) {
|
||||||
return;
|
return;
|
||||||
@@ -389,12 +394,12 @@ export const SpaceToolsDockWidget = ({
|
|||||||
|
|
||||||
{openPopover === 'sound' ? (
|
{openPopover === 'sound' ? (
|
||||||
<div
|
<div
|
||||||
className="mb-2 w-[min(300px,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"
|
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 }}
|
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', right: 0 }}
|
||||||
>
|
>
|
||||||
<p className="text-[11px] text-white/56">사운드 프리셋</p>
|
<p className="text-[11px] text-white/56">빠른 사운드 전환</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
{SOUND_PRESETS.slice(0, 6).map((preset) => {
|
{quickSoundPresets.map((preset) => {
|
||||||
const selected = preset.id === selectedPresetId;
|
const selected = preset.id === selectedPresetId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -403,6 +408,7 @@ export const SpaceToolsDockWidget = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectPreset(preset.id);
|
onSelectPreset(preset.id);
|
||||||
|
pushToast({ title: `${preset.label}로 전환했어요.` });
|
||||||
setOpenPopover(null);
|
setOpenPopover(null);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -417,15 +423,6 @@ export const SpaceToolsDockWidget = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openUtilityPanel('control-center')}
|
|
||||||
className="text-[11px] text-white/62 transition-colors hover:text-white/88"
|
|
||||||
>
|
|
||||||
Control Center
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -442,6 +439,8 @@ export const SpaceToolsDockWidget = ({
|
|||||||
<PlanPill plan={plan} onClick={handlePlanPillClick} />
|
<PlanPill plan={plan} onClick={handlePlanPillClick} />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
widthClassName={utilityPanel === 'control-center' ? 'w-[min(408px,94vw)]' : undefined}
|
||||||
|
overlayClassName={utilityPanel === 'control-center' ? 'bg-slate-950/6 backdrop-blur-none' : undefined}
|
||||||
onClose={() => setUtilityPanel(null)}
|
onClose={() => setUtilityPanel(null)}
|
||||||
>
|
>
|
||||||
{utilityPanel === 'control-center' ? (
|
{utilityPanel === 'control-center' ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user