style(space): 스테이지 중심 첫 인상과 Setup/Focus 톤을 정리
맥락: - /space 첫 화면이 그라데이션 안내 화면처럼 보이며 공간 서비스의 무대감이 약했습니다. - Setup 안내 카드/상단 크롬/도크 존재감이 커서 몰입형 인상(Portal/LifeAt 톤)을 해치고 있었습니다. 변경사항: - 배경을 실제 공간 프리뷰 이미지 우선 렌더로 전환하고, 실패 시 그라데이션 fallback만 남기도록 조정했습니다. - 배경 오버레이를 과한 비네팅 대신 단일 규칙(얕은 읽기용 필터 + 고정 그레인)으로 정리했습니다. - Setup 상태의 중앙 안내 카드를 제거하고, 진입 시 Setup Drawer 자동 오픈 흐름만 남겼습니다. - Setup Drawer 헤더 안내 문구를 1줄로 축약하고 섹션을 3단(Space/Goal/Sound) 번호 체계로 고정했습니다. - Setup 상태에서는 Drawer 닫기를 막아 설명 박스 없이도 자연스러운 입력 흐름을 유지했습니다. - 상단 크롬을 최소화하고 Focus 상태의 Setup 열기 버튼을 약한 보조 액션으로 낮췄습니다. - 오른쪽 도크 레일의 폭/간격/아이콘 박스를 정돈하고 Focus 기본 opacity를 낮춰 몰입 방해를 줄였습니다. 검증: - npx tsc --noEmit - npm run build 세션-상태: /space 첫 진입이 장면+드로어 중심으로 정리되어 설명 없이도 시작 흐름이 읽힙니다. 세션-다음: 필요 시 Setup Drawer 내부 타이포 스케일과 칩 밀도를 추가 미세 조정합니다. 세션-리스크: 외부 이미지 소스 품질 편차에 따라 장면 밝기 체감이 달라질 수 있습니다.
This commit is contained in:
@@ -233,9 +233,10 @@ export const getRoomCardBackgroundStyle = (room: RoomTheme): CSSProperties => {
|
|||||||
|
|
||||||
export const getRoomBackgroundStyle = (room: RoomTheme): CSSProperties => {
|
export const getRoomBackgroundStyle = (room: RoomTheme): CSSProperties => {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `${room.previewGradient}, url('${room.previewImage}')`,
|
backgroundImage: `url('${getRoomCardPhotoUrl(room)}'), linear-gradient(160deg, #1e293b 0%, #0f172a 100%)`,
|
||||||
backgroundSize: 'cover, cover',
|
backgroundSize: 'cover, cover',
|
||||||
backgroundPosition: 'center, center',
|
backgroundPosition: 'center, center',
|
||||||
|
backgroundRepeat: 'no-repeat, no-repeat',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SpaceSideSheet } from '@/widgets/space-sheet-shell';
|
|||||||
|
|
||||||
interface SpaceSetupDrawerWidgetProps {
|
interface SpaceSetupDrawerWidgetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
dismissible?: boolean;
|
||||||
rooms: RoomTheme[];
|
rooms: RoomTheme[];
|
||||||
selectedRoomId: string;
|
selectedRoomId: string;
|
||||||
goalInput: string;
|
goalInput: string;
|
||||||
@@ -26,6 +27,7 @@ interface SpaceSetupDrawerWidgetProps {
|
|||||||
|
|
||||||
export const SpaceSetupDrawerWidget = ({
|
export const SpaceSetupDrawerWidget = ({
|
||||||
open,
|
open,
|
||||||
|
dismissible = true,
|
||||||
rooms,
|
rooms,
|
||||||
selectedRoomId,
|
selectedRoomId,
|
||||||
goalInput,
|
goalInput,
|
||||||
@@ -45,8 +47,9 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
<SpaceSideSheet
|
<SpaceSideSheet
|
||||||
open={open}
|
open={open}
|
||||||
title="Setup"
|
title="Setup"
|
||||||
subtitle="공간 선택 → 목표(필수) → 사운드(선택)"
|
subtitle="공간을 고르고, 한 줄 목표를 적어주세요."
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
dismissible={dismissible}
|
||||||
widthClassName="w-[min(360px,94vw)]"
|
widthClassName="w-[min(360px,94vw)]"
|
||||||
footer={(
|
footer={(
|
||||||
<Button
|
<Button
|
||||||
@@ -63,10 +66,9 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<section className="space-y-2">
|
<section className="space-y-2.5">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">Space</p>
|
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">1) Space</p>
|
||||||
<p className="text-xs text-white/62">오늘 머물 공간을 하나 고르세요.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<SpaceSelectCarousel
|
<SpaceSelectCarousel
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
@@ -75,10 +77,9 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2.5">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">Goal</p>
|
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">2) Goal</p>
|
||||||
<p className="text-xs text-white/62">스킵 없이 한 줄 목표를 남겨주세요.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<SessionGoalField
|
<SessionGoalField
|
||||||
goalInput={goalInput}
|
goalInput={goalInput}
|
||||||
@@ -89,10 +90,9 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2.5">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">Sound</p>
|
<p className="text-[11px] uppercase tracking-[0.16em] text-white/55">3) Sound</p>
|
||||||
<p className="text-xs text-white/62">선택 항목이에요. 필요 없으면 그대로 시작해도 됩니다.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface SpaceSideSheetProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
widthClassName?: string;
|
widthClassName?: string;
|
||||||
|
dismissible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceSideSheet = ({
|
export const SpaceSideSheet = ({
|
||||||
@@ -21,6 +22,7 @@ export const SpaceSideSheet = ({
|
|||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
widthClassName,
|
widthClassName,
|
||||||
|
dismissible = true,
|
||||||
}: SpaceSideSheetProps) => {
|
}: SpaceSideSheetProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -46,12 +48,16 @@ export const SpaceSideSheet = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
{dismissible ? (
|
||||||
type="button"
|
<button
|
||||||
aria-label="시트 닫기"
|
type="button"
|
||||||
onClick={onClose}
|
aria-label="시트 닫기"
|
||||||
className="fixed inset-0 z-40 bg-slate-950/22 backdrop-blur-[1px]"
|
onClick={onClose}
|
||||||
/>
|
className="fixed inset-0 z-40 bg-slate-950/18 backdrop-blur-[1px]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div aria-hidden className="fixed inset-0 z-40 bg-slate-950/12 backdrop-blur-[1px]" />
|
||||||
|
)}
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -59,20 +65,22 @@ export const SpaceSideSheet = ({
|
|||||||
widthClassName ?? 'w-[min(360px,92vw)]',
|
widthClassName ?? 'w-[min(360px,92vw)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col overflow-hidden rounded-3xl border border-white/16 bg-slate-950/68 text-white shadow-[0_24px_70px_rgba(2,6,23,0.55)] backdrop-blur-2xl">
|
<div className="flex h-full flex-col overflow-hidden rounded-3xl border border-white/20 bg-slate-950/58 text-white shadow-[0_20px_60px_rgba(2,6,23,0.42)] backdrop-blur-2xl">
|
||||||
<header className="flex items-start justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-5">
|
<header className="flex items-start justify-between gap-3 border-b border-white/10 px-4 py-3 sm:px-5">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold text-white">{title}</h2>
|
<h2 className="text-base font-semibold text-white">{title}</h2>
|
||||||
{subtitle ? <p className="mt-1 text-xs text-white/58">{subtitle}</p> : null}
|
{subtitle ? <p className="mt-1 text-xs text-white/58">{subtitle}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
<button
|
{dismissible ? (
|
||||||
type="button"
|
<button
|
||||||
onClick={onClose}
|
type="button"
|
||||||
aria-label="닫기"
|
onClick={onClose}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/8 text-sm text-white/80 transition-colors hover:bg-white/14 hover:text-white"
|
aria-label="닫기"
|
||||||
>
|
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/8 text-sm text-white/80 transition-colors hover:bg-white/14 hover:text-white"
|
||||||
✕
|
>
|
||||||
</button>
|
✕
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-5">{children}</div>
|
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-5">{children}</div>
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ export const SpaceToolsDockWidget = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed right-3 top-1/2 z-30 -translate-y-1/2">
|
<div className="fixed right-3.5 top-1/2 z-30 -translate-y-1/2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-12 flex-col items-center gap-2 rounded-2xl border py-2 backdrop-blur-xl transition-opacity',
|
'flex w-11 flex-col items-center gap-1.5 rounded-[18px] border py-1.5 backdrop-blur-lg transition-opacity',
|
||||||
isFocusMode && activePanel === null
|
isFocusMode && activePanel === null
|
||||||
? 'border-white/14 bg-slate-950/34 opacity-56 hover:opacity-100'
|
? 'border-white/12 bg-slate-950/22 opacity-40 hover:opacity-92'
|
||||||
: 'border-white/18 bg-slate-950/50 opacity-100',
|
: 'border-white/16 bg-slate-950/34 opacity-92',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{TOOL_ITEMS.map((item) => {
|
{TOOL_ITEMS.map((item) => {
|
||||||
@@ -95,15 +95,15 @@ export const SpaceToolsDockWidget = ({
|
|||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
onClick={() => setActivePanel(item.id)}
|
onClick={() => setActivePanel(item.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex h-9 w-9 items-center justify-center rounded-xl border text-base transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75',
|
'relative inline-flex h-8 w-8 items-center justify-center rounded-[10px] border text-[15px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75',
|
||||||
selected
|
selected
|
||||||
? 'border-sky-200/64 bg-sky-200/22'
|
? 'border-sky-200/58 bg-sky-200/18 shadow-[0_0_0_1px_rgba(186,230,253,0.28)]'
|
||||||
: 'border-white/18 bg-white/8 hover:bg-white/14',
|
: 'border-white/14 bg-white/7 hover:bg-white/13',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span aria-hidden>{item.icon}</span>
|
<span aria-hidden>{item.icon}</span>
|
||||||
{item.id === 'inbox' && thoughtCount > 0 ? (
|
{item.id === 'inbox' && thoughtCount > 0 ? (
|
||||||
<span className="absolute -right-1 -top-1 inline-flex min-w-[1rem] items-center justify-center rounded-full bg-sky-200/28 px-1 py-0.5 text-[9px] font-semibold text-sky-50">
|
<span className="absolute -right-1 -top-1 inline-flex min-w-[0.95rem] items-center justify-center rounded-full bg-sky-200/26 px-1 py-0.5 text-[8px] font-semibold text-sky-50">
|
||||||
{thoughtCount > 99 ? '99+' : `${thoughtCount}`}
|
{thoughtCount > 99 ? '99+' : `${thoughtCount}`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
type GoalChip,
|
type GoalChip,
|
||||||
} from '@/entities/session';
|
} from '@/entities/session';
|
||||||
import { useSoundPresetSelection } from '@/features/sound-preset';
|
import { useSoundPresetSelection } from '@/features/sound-preset';
|
||||||
import { cn } from '@/shared/lib/cn';
|
|
||||||
import { useToast } from '@/shared/ui';
|
import { useToast } from '@/shared/ui';
|
||||||
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
||||||
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
||||||
@@ -102,7 +101,6 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSetup = () => {
|
const handleOpenSetup = () => {
|
||||||
setWorkspaceMode('setup');
|
|
||||||
setSetupDrawerOpen(true);
|
setSetupDrawerOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,70 +109,48 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
<div aria-hidden className="absolute inset-0" style={getRoomBackgroundStyle(selectedRoom)} />
|
<div aria-hidden className="absolute inset-0" style={getRoomBackgroundStyle(selectedRoom)} />
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className="absolute inset-0"
|
||||||
'absolute inset-0 transition-colors',
|
|
||||||
isFocusMode ? 'bg-slate-900/44' : 'bg-slate-900/36',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className={cn(
|
|
||||||
'absolute inset-0 transition-opacity',
|
|
||||||
isFocusMode ? 'opacity-58' : 'opacity-68',
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'radial-gradient(108% 86% at 18% 0%, rgba(148,163,184,0.32) 0%, rgba(2,6,23,0) 46%), radial-gradient(96% 80% at 88% 12%, rgba(125,211,252,0.2) 0%, rgba(2,6,23,0) 54%), linear-gradient(180deg, rgba(2,6,23,0.18) 0%, rgba(2,6,23,0.62) 100%)',
|
'linear-gradient(180deg, rgba(15,23,42,0.16) 0%, rgba(15,23,42,0.32) 100%)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn('absolute inset-0', isFocusMode ? 'opacity-[0.16]' : 'opacity-[0.12]')}
|
className="absolute inset-0 opacity-[0.08]"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
"url('/textures/grain.png'), repeating-linear-gradient(0deg, rgba(255,255,255,0.016) 0 1px, transparent 1px 2px)",
|
'repeating-linear-gradient(0deg, rgba(255,255,255,0.016) 0 1px, transparent 1px 2px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative z-10 flex min-h-screen flex-col pr-[4.25rem]">
|
<div className="relative z-10 flex min-h-screen flex-col pr-[4.25rem]">
|
||||||
<header className="flex items-start justify-between px-4 pt-4 sm:px-6">
|
<header className="flex items-center justify-between px-4 pt-3.5 sm:px-6">
|
||||||
<div className="rounded-2xl border border-white/18 bg-slate-950/44 px-3.5 py-2 backdrop-blur-xl">
|
{!isFocusMode ? (
|
||||||
<p className="text-sm font-semibold tracking-tight text-white/92">VibeRoom</p>
|
<div className="rounded-full border border-white/16 bg-slate-950/32 px-3 py-1.5 backdrop-blur-xl">
|
||||||
<p className="text-[11px] text-white/62">
|
<p className="text-xs font-semibold tracking-tight text-white/88">VibeRoom</p>
|
||||||
{selectedRoom.name} · {selectedRoom.vibeLabel}
|
</div>
|
||||||
</p>
|
) : (
|
||||||
</div>
|
<div aria-hidden className="h-7" />
|
||||||
|
)}
|
||||||
|
|
||||||
{!isSetupDrawerOpen ? (
|
{isFocusMode && !isSetupDrawerOpen ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenSetup}
|
onClick={handleOpenSetup}
|
||||||
className="rounded-full border border-white/20 bg-slate-950/44 px-3 py-1.5 text-xs text-white/80 transition-colors hover:bg-slate-950/62 hover:text-white"
|
className="rounded-full border border-white/16 bg-slate-950/24 px-2.5 py-1 text-[11px] text-white/58 transition-colors hover:bg-slate-950/40 hover:text-white/84"
|
||||||
>
|
>
|
||||||
Setup 열기
|
Setup 열기
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="relative flex-1">
|
<main className="relative flex-1" />
|
||||||
{isFocusMode ? null : (
|
|
||||||
<div className="pointer-events-none absolute inset-x-4 top-1/2 -translate-y-1/2 sm:inset-x-6">
|
|
||||||
<div className="max-w-xl rounded-3xl border border-white/14 bg-slate-950/38 px-6 py-5 backdrop-blur-sm">
|
|
||||||
<p className="text-xs uppercase tracking-[0.18em] text-white/56">Workspace</p>
|
|
||||||
<h1 className="mt-2 text-2xl font-semibold tracking-tight text-white sm:text-3xl">
|
|
||||||
공간을 고르고 시작하세요
|
|
||||||
</h1>
|
|
||||||
<p className="mt-2 text-sm text-white/68">
|
|
||||||
오른쪽 Setup에서 목표를 입력하면 바로 몰입 화면으로 전환됩니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpaceSetupDrawerWidget
|
<SpaceSetupDrawerWidget
|
||||||
open={isSetupDrawerOpen}
|
open={isSetupDrawerOpen}
|
||||||
|
dismissible={isFocusMode}
|
||||||
rooms={ROOM_THEMES}
|
rooms={ROOM_THEMES}
|
||||||
selectedRoomId={selectedRoom.id}
|
selectedRoomId={selectedRoom.id}
|
||||||
goalInput={goalInput}
|
goalInput={goalInput}
|
||||||
@@ -183,7 +159,11 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
goalChips={GOAL_CHIPS}
|
goalChips={GOAL_CHIPS}
|
||||||
soundPresets={SOUND_PRESETS}
|
soundPresets={SOUND_PRESETS}
|
||||||
canStart={canStart}
|
canStart={canStart}
|
||||||
onClose={() => setSetupDrawerOpen(false)}
|
onClose={() => {
|
||||||
|
if (isFocusMode) {
|
||||||
|
setSetupDrawerOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onRoomSelect={setSelectedRoomId}
|
onRoomSelect={setSelectedRoomId}
|
||||||
onGoalChange={handleGoalChange}
|
onGoalChange={handleGoalChange}
|
||||||
onGoalChipSelect={handleGoalChipSelect}
|
onGoalChipSelect={handleGoalChipSelect}
|
||||||
|
|||||||
Reference in New Issue
Block a user