feat(app-hub): 스테이지 카드 칩 제거와 추천 공간 캐러셀 적용
맥락: - 허브 스테이지 내부에 메모/전체공간 칩이 동시에 노출되어 시선이 분산되고 조작 밀도가 높았습니다. - 추천 공간은 단순 스크롤보다 회전 가능한 캐러셀 인터랙션이 필요했습니다. 변경사항: - 오늘의 공간 카드 헤더에서 메모 칩과 전체공간 칩을 제거했습니다. - 추천 공간 섹션에 이전/다음 컨트롤을 추가해 캐러셀 회전이 가능하도록 변경했습니다. - 전체 공간 진입은 칩 대신 텍스트 링크로 정리했습니다. - 메모 진입 컴포넌트는 스테이지 카드 밖(하단)으로 이동해 카드 내부 밀도를 낮췄습니다. - 스테이지 오버레이 구성을 유지하기 위해 관련 위젯 조합을 함께 정리했습니다. 검증: - npx tsc --noEmit - npm run build 세션-상태: 스테이지 카드 내부 칩 제거 및 추천 공간 캐러셀 반영 완료 세션-다음: 캐러셀 전환 모션(페이드/슬라이드) 미세 조정 세션-리스크: 추천 공간 수가 1개 이하일 때 회전 UI 효용이 낮아질 수 있음
This commit is contained in:
@@ -29,8 +29,8 @@ export const RoomPreviewCard = ({
|
||||
className={cn(
|
||||
'group relative overflow-hidden rounded-3xl border text-left transition-all duration-250 motion-reduce:transition-none',
|
||||
compact
|
||||
? 'h-[112px] min-w-[164px] snap-start p-2.5 sm:min-w-0 sm:aspect-[4/3]'
|
||||
: 'h-[338px] p-3 sm:h-[420px] sm:p-4',
|
||||
? 'h-[98px] min-w-[152px] snap-start p-2 sm:min-w-0 sm:aspect-[5/4]'
|
||||
: 'h-[292px] p-3 sm:h-[334px] sm:p-4 lg:h-[356px]',
|
||||
selected
|
||||
? cinematic
|
||||
? 'border-sky-200/44 shadow-[0_0_0_1px_rgba(186,230,253,0.34),0_20px_50px_rgba(2,6,23,0.32)]'
|
||||
@@ -86,14 +86,14 @@ export const RoomPreviewCard = ({
|
||||
) : null}
|
||||
|
||||
{compact ? (
|
||||
<div className="w-full rounded-lg bg-slate-950/44 px-2.5 py-2">
|
||||
<div className="w-full rounded-lg bg-slate-950/44 px-2 py-1.5">
|
||||
<p className="truncate text-sm font-semibold text-white">{room.name}</p>
|
||||
<p className="mt-0.5 truncate text-[11px] text-white/70">{room.vibeLabel}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-[440px] rounded-2xl border border-white/14 bg-slate-950/46 px-4 py-3 backdrop-blur-md">
|
||||
<div className="w-full max-w-[420px] rounded-2xl border border-white/14 bg-slate-950/46 px-4 py-3 backdrop-blur-md">
|
||||
<p className="text-[11px] uppercase tracking-[0.16em] text-white/56">Selected Space</p>
|
||||
<h3 className="mt-1 text-2xl font-semibold tracking-tight text-white sm:text-[2rem]">
|
||||
<h3 className="mt-1 text-2xl font-semibold tracking-tight text-white sm:text-[1.85rem]">
|
||||
{room.name}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-white/74">{room.description}</p>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getRoomCardBackgroundStyle, ROOM_THEMES } from '@/entities/room';
|
||||
import { ROOM_THEMES } from '@/entities/room';
|
||||
import {
|
||||
GOAL_CHIPS,
|
||||
SOUND_PRESETS,
|
||||
@@ -52,7 +52,7 @@ export const AppHubWidget = () => {
|
||||
const router = useRouter();
|
||||
const { pushToast } = useToast();
|
||||
const { thoughts, thoughtCount, clearThoughts } = useThoughtInbox();
|
||||
const { selectedRoom, selectedRoomId, selectRoom } = useRoomSelection(
|
||||
const { selectedRoomId, selectRoom } = useRoomSelection(
|
||||
ROOM_THEMES[0].id,
|
||||
);
|
||||
|
||||
@@ -108,15 +108,11 @@ export const AppHubWidget = () => {
|
||||
<div className="relative min-h-screen overflow-hidden text-white">
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute inset-0',
|
||||
cinematic && 'scale-[1.01]',
|
||||
)}
|
||||
className={cn('absolute inset-0')}
|
||||
style={{
|
||||
...getRoomCardBackgroundStyle(selectedRoom),
|
||||
filter: cinematic
|
||||
? 'brightness(0.86) saturate(1.06) contrast(1.03)'
|
||||
: 'brightness(0.94) saturate(0.96)',
|
||||
backgroundImage: cinematic
|
||||
? 'radial-gradient(132% 94% at 8% 0%, rgba(148,163,184,0.34) 0%, rgba(15,23,42,0) 46%), radial-gradient(116% 88% at 94% 6%, rgba(125,211,252,0.18) 0%, rgba(15,23,42,0) 52%), linear-gradient(162deg, #0f172a 0%, #111827 42%, #0b1220 100%)'
|
||||
: 'radial-gradient(130% 92% at 8% 0%, rgba(148,163,184,0.22) 0%, rgba(248,250,252,0) 48%), radial-gradient(104% 86% at 90% 8%, rgba(125,211,252,0.2) 0%, rgba(248,250,252,0) 54%), linear-gradient(164deg, #f8fafc 0%, #e2e8f0 48%, #dbe7f5 100%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -124,8 +120,8 @@ export const AppHubWidget = () => {
|
||||
className={cn(
|
||||
'absolute inset-0',
|
||||
cinematic
|
||||
? 'bg-[linear-gradient(180deg,rgba(2,6,23,0.22)_0%,rgba(2,6,23,0.34)_50%,rgba(2,6,23,0.52)_100%)]'
|
||||
: 'bg-[linear-gradient(180deg,rgba(248,250,252,0.44)_0%,rgba(241,245,249,0.22)_42%,rgba(15,23,42,0.3)_100%)]',
|
||||
? 'bg-[radial-gradient(circle_at_24%_16%,rgba(148,163,184,0.14),transparent_36%),radial-gradient(circle_at_78%_14%,rgba(125,211,252,0.1),transparent_34%),linear-gradient(180deg,rgba(2,6,23,0.16)_0%,rgba(2,6,23,0.32)_52%,rgba(2,6,23,0.5)_100%)]'
|
||||
: 'bg-[radial-gradient(circle_at_20%_14%,rgba(148,163,184,0.16),transparent_38%),radial-gradient(circle_at_82%_16%,rgba(125,211,252,0.12),transparent_38%),linear-gradient(180deg,rgba(248,250,252,0.32)_0%,rgba(241,245,249,0.12)_40%,rgba(15,23,42,0.26)_100%)]',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
@@ -157,8 +153,12 @@ export const AppHubWidget = () => {
|
||||
/>
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 pb-8 pt-5 sm:px-6 sm:pt-6 lg:px-8 lg:pb-10">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,360px)_minmax(0,1fr)] lg:gap-5">
|
||||
<section className="order-1">
|
||||
<RoomsGalleryWidget
|
||||
visualMode={visualMode}
|
||||
rooms={ROOM_THEMES}
|
||||
selectedRoomId={selectedRoomId}
|
||||
onRoomSelect={selectRoom}
|
||||
startPanel={(
|
||||
<StartRitualWidget
|
||||
visualMode={visualMode}
|
||||
goalInput={goalInput}
|
||||
@@ -168,25 +168,16 @@ export const AppHubWidget = () => {
|
||||
onGoalChipSelect={handleGoalChipSelect}
|
||||
onQuickEnter={handleQuickEnter}
|
||||
onOpenCustomEntry={() => setCustomEntryOpen(true)}
|
||||
inStage
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="order-2 lg:row-span-2">
|
||||
<RoomsGalleryWidget
|
||||
visualMode={visualMode}
|
||||
rooms={ROOM_THEMES}
|
||||
selectedRoomId={selectedRoomId}
|
||||
onRoomSelect={selectRoom}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="order-3">
|
||||
<ThoughtSummaryEntryWidget
|
||||
visualMode={visualMode}
|
||||
thoughtCount={thoughtCount}
|
||||
onOpen={() => setThoughtInboxOpen(true)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-3 sm:mt-4">
|
||||
<ThoughtSummaryEntryWidget
|
||||
visualMode={visualMode}
|
||||
thoughtCount={thoughtCount}
|
||||
onOpen={() => setThoughtInboxOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { getHubRoomSections, type RoomTheme } from '@/entities/room';
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import {
|
||||
getHubRoomSections,
|
||||
getRoomCardBackgroundStyle,
|
||||
type RoomTheme,
|
||||
} from '@/entities/room';
|
||||
import { RoomPreviewCard } from '@/features/room-select';
|
||||
import type { AppHubVisualMode } from '@/shared/config/appHubVisualMode';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
@@ -10,6 +14,7 @@ interface RoomsGalleryWidgetProps {
|
||||
rooms: RoomTheme[];
|
||||
selectedRoomId: string;
|
||||
onRoomSelect: (roomId: string) => void;
|
||||
startPanel: ReactNode;
|
||||
}
|
||||
|
||||
export const RoomsGalleryWidget = ({
|
||||
@@ -17,79 +22,148 @@ export const RoomsGalleryWidget = ({
|
||||
rooms,
|
||||
selectedRoomId,
|
||||
onRoomSelect,
|
||||
startPanel,
|
||||
}: RoomsGalleryWidgetProps) => {
|
||||
const [isCatalogOpen, setCatalogOpen] = useState(false);
|
||||
const [carouselOffset, setCarouselOffset] = useState(0);
|
||||
const cinematic = visualMode === 'cinematic';
|
||||
const selectedRoom = rooms.find((room) => room.id === selectedRoomId) ?? rooms[0];
|
||||
const { recommendedRooms, allRooms } = getHubRoomSections(rooms, selectedRoom.id, 5);
|
||||
const { recommendedRooms, allRooms } = getHubRoomSections(rooms, selectedRoom.id, 6);
|
||||
const canRotate = recommendedRooms.length > 1;
|
||||
const rotatedRecommendedRooms = useMemo(() => {
|
||||
if (recommendedRooms.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedOffset =
|
||||
((carouselOffset % recommendedRooms.length) + recommendedRooms.length) %
|
||||
recommendedRooms.length;
|
||||
|
||||
return [
|
||||
...recommendedRooms.slice(normalizedOffset),
|
||||
...recommendedRooms.slice(0, normalizedOffset),
|
||||
];
|
||||
}, [carouselOffset, recommendedRooms]);
|
||||
|
||||
useEffect(() => {
|
||||
setCarouselOffset(0);
|
||||
}, [selectedRoomId]);
|
||||
|
||||
return (
|
||||
<section className="space-y-3.5">
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
'text-[11px] uppercase tracking-[0.14em]',
|
||||
cinematic ? 'text-white/52' : 'text-brand-dark/48',
|
||||
)}
|
||||
>
|
||||
Scene
|
||||
</p>
|
||||
<h2
|
||||
className={cn(
|
||||
'mt-1 text-xl font-semibold tracking-tight',
|
||||
cinematic ? 'text-white' : 'text-brand-dark',
|
||||
)}
|
||||
>
|
||||
오늘의 공간
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
<section className="space-y-3">
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-[2rem] border',
|
||||
cinematic
|
||||
? 'border-white/16 bg-slate-950/34 shadow-[0_24px_70px_rgba(2,6,23,0.5)]'
|
||||
: 'border-brand-dark/12 bg-white/78 shadow-[0_20px_64px_rgba(15,23,42,0.2)]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0"
|
||||
style={getRoomCardBackgroundStyle(selectedRoom)}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'text-xs font-medium transition-colors',
|
||||
'absolute inset-0',
|
||||
cinematic
|
||||
? 'text-white/56 hover:text-white/84'
|
||||
: 'text-brand-dark/56 hover:text-brand-dark/84',
|
||||
? 'bg-[linear-gradient(180deg,rgba(2,6,23,0.22)_0%,rgba(2,6,23,0.44)_58%,rgba(2,6,23,0.84)_100%)]'
|
||||
: 'bg-[linear-gradient(180deg,rgba(15,23,42,0.16)_0%,rgba(15,23,42,0.4)_56%,rgba(15,23,42,0.74)_100%)]',
|
||||
)}
|
||||
>
|
||||
더 보기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<RoomPreviewCard
|
||||
room={selectedRoom}
|
||||
visualMode={visualMode}
|
||||
variant="hero"
|
||||
selected
|
||||
onSelect={onRoomSelect}
|
||||
/>
|
||||
|
||||
<section className="space-y-2.5">
|
||||
<h3
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'text-xs font-medium tracking-[0.06em]',
|
||||
cinematic ? 'text-white/66' : 'text-brand-dark/60',
|
||||
'absolute inset-0',
|
||||
cinematic
|
||||
? 'bg-[radial-gradient(circle_at_20%_12%,rgba(125,211,252,0.14),transparent_35%),radial-gradient(circle_at_78%_10%,rgba(196,181,253,0.13),transparent_36%)]'
|
||||
: 'bg-[radial-gradient(circle_at_20%_12%,rgba(148,163,184,0.14),transparent_35%),radial-gradient(circle_at_78%_10%,rgba(125,211,252,0.1),transparent_36%)]',
|
||||
)}
|
||||
>
|
||||
추천 공간
|
||||
</h3>
|
||||
<div className="-mx-1 snap-x overflow-x-auto px-1 sm:mx-0 sm:overflow-visible sm:px-0">
|
||||
<div className="flex gap-3 sm:grid sm:grid-cols-5">
|
||||
{recommendedRooms.map((room) => (
|
||||
<RoomPreviewCard
|
||||
key={`recommended-${room.id}`}
|
||||
room={room}
|
||||
visualMode={visualMode}
|
||||
variant="compact"
|
||||
selected={room.id === selectedRoomId}
|
||||
onSelect={onRoomSelect}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-[0.16]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/textures/grain.png'), repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 2px)",
|
||||
mixBlendMode: 'soft-light',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex min-h-[640px] flex-col px-4 py-4 sm:px-6 sm:py-6 lg:px-7 lg:py-7">
|
||||
<header className="flex items-start gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] uppercase tracking-[0.16em] text-white/58">Hub Stage</p>
|
||||
<h2 className="mt-1 text-2xl font-semibold tracking-tight text-white">오늘의 공간</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mt-auto space-y-4">
|
||||
<div className="max-w-[560px] rounded-2xl border border-white/16 bg-slate-950/46 px-4 py-3 backdrop-blur-md sm:px-5 sm:py-4">
|
||||
<p className="text-[11px] uppercase tracking-[0.18em] text-white/56">Selected Space</p>
|
||||
<h3 className="mt-1 text-3xl font-semibold tracking-tight text-white sm:text-[2.25rem]">
|
||||
{selectedRoom.name}
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm text-white/78 sm:text-base">{selectedRoom.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,360px)_minmax(0,1fr)] lg:items-end">
|
||||
<div className="lg:max-w-[360px]">{startPanel}</div>
|
||||
|
||||
<section className="rounded-2xl border border-white/14 bg-slate-950/44 p-3 backdrop-blur-md sm:p-3.5">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-[11px] font-medium tracking-[0.12em] text-white/66">추천 공간</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(true)}
|
||||
className="text-[11px] text-white/60 transition-colors hover:text-white/86"
|
||||
>
|
||||
공간 전체 보기
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRotate}
|
||||
onClick={() => setCarouselOffset((current) => current - 1)}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/20 bg-white/8 text-sm text-white/82 transition hover:bg-white/14 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="추천 공간 이전"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRotate}
|
||||
onClick={() => setCarouselOffset((current) => current + 1)}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/20 bg-white/8 text-sm text-white/82 transition hover:bg-white/14 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label="추천 공간 다음"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-1 snap-x overflow-x-auto px-1 pb-1">
|
||||
<div className="flex gap-2.5">
|
||||
{rotatedRecommendedRooms.map((room) => (
|
||||
<RoomPreviewCard
|
||||
key={`recommended-${room.id}`}
|
||||
room={room}
|
||||
visualMode={visualMode}
|
||||
variant="compact"
|
||||
selected={room.id === selectedRoomId}
|
||||
onSelect={onRoomSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<RoomCatalogSheet
|
||||
isOpen={isCatalogOpen}
|
||||
|
||||
@@ -12,6 +12,7 @@ interface StartRitualWidgetProps {
|
||||
onGoalChipSelect: (chip: GoalChip) => void;
|
||||
onQuickEnter: () => void;
|
||||
onOpenCustomEntry: () => void;
|
||||
inStage?: boolean;
|
||||
}
|
||||
|
||||
export const StartRitualWidget = ({
|
||||
@@ -23,16 +24,20 @@ export const StartRitualWidget = ({
|
||||
onGoalChipSelect,
|
||||
onQuickEnter,
|
||||
onOpenCustomEntry,
|
||||
inStage = false,
|
||||
}: StartRitualWidgetProps) => {
|
||||
const cinematic = visualMode === 'cinematic';
|
||||
const visibleGoalChips = inStage ? goalChips.slice(0, 2) : goalChips.slice(0, 3);
|
||||
|
||||
return (
|
||||
<GlassCard
|
||||
elevated
|
||||
className={cn(
|
||||
'space-y-4 p-4 sm:p-5',
|
||||
inStage ? 'space-y-3 p-3.5 sm:p-4' : 'space-y-3.5 p-4 sm:p-5',
|
||||
cinematic
|
||||
? 'border-white/16 bg-slate-950/48 text-white shadow-[0_20px_44px_rgba(2,6,23,0.36)] backdrop-blur-xl'
|
||||
? inStage
|
||||
? 'border-white/18 bg-slate-950/62 text-white shadow-[0_18px_36px_rgba(2,6,23,0.36)] backdrop-blur-xl'
|
||||
: 'border-white/16 bg-slate-950/48 text-white shadow-[0_20px_44px_rgba(2,6,23,0.36)] backdrop-blur-xl'
|
||||
: 'border-brand-dark/10 bg-white/80 text-brand-dark shadow-[0_16px_40px_rgba(15,23,42,0.12)] backdrop-blur-md',
|
||||
)}
|
||||
>
|
||||
@@ -43,11 +48,11 @@ export const StartRitualWidget = ({
|
||||
cinematic ? 'text-white/48' : 'text-brand-dark/46',
|
||||
)}
|
||||
>
|
||||
Start Ritual
|
||||
{inStage ? 'Quick Entry' : 'Start Ritual'}
|
||||
</p>
|
||||
<h1
|
||||
className={cn(
|
||||
'text-2xl font-semibold tracking-tight',
|
||||
inStage ? 'text-xl font-semibold tracking-tight' : 'text-2xl font-semibold tracking-tight',
|
||||
cinematic ? 'text-white' : 'text-brand-dark',
|
||||
)}
|
||||
>
|
||||
@@ -59,7 +64,7 @@ export const StartRitualWidget = ({
|
||||
cinematic ? 'text-white/72' : 'text-brand-dark/68',
|
||||
)}
|
||||
>
|
||||
비어 있어도 괜찮아요. 필요하면 공간 안에서 수정할 수 있어요.
|
||||
비어 있어도 바로 입장할 수 있어요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +81,7 @@ export const StartRitualWidget = ({
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{goalChips.slice(0, 4).map((chip) => (
|
||||
{visibleGoalChips.map((chip) => (
|
||||
<Chip
|
||||
key={chip.id}
|
||||
active={selectedGoalId === chip.id}
|
||||
@@ -92,10 +97,17 @@ export const StartRitualWidget = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-2.5 sm:flex-row sm:items-center',
|
||||
inStage ? 'sm:justify-start' : 'sm:justify-end',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
'w-full px-6 sm:w-auto sm:min-w-[176px]',
|
||||
inStage
|
||||
? 'w-full px-5 sm:min-w-[148px] sm:flex-1'
|
||||
: 'w-full px-6 sm:w-auto sm:min-w-[168px]',
|
||||
cinematic &&
|
||||
'!bg-sky-200 !text-slate-900 shadow-[0_12px_24px_rgba(125,211,252,0.22)] hover:!bg-sky-100',
|
||||
)}
|
||||
@@ -107,14 +119,16 @@ export const StartRitualWidget = ({
|
||||
type="button"
|
||||
onClick={onOpenCustomEntry}
|
||||
className={cn(
|
||||
'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-xl px-4 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75 sm:h-10 sm:w-auto',
|
||||
inStage
|
||||
? 'inline-flex h-10 w-full items-center justify-center gap-1.5 rounded-xl px-3.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75 sm:w-auto sm:min-w-[118px]'
|
||||
: 'inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-xl px-4 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75 sm:h-10 sm:w-auto sm:min-w-[122px] sm:px-3.5',
|
||||
cinematic
|
||||
? 'text-white/70 hover:bg-white/8 hover:text-white'
|
||||
: 'text-brand-dark/64 hover:bg-brand-dark/5 hover:text-brand-dark',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden>⚙</span>
|
||||
<span>설정하고 입장</span>
|
||||
<span className="whitespace-nowrap">설정하고 입장</span>
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
@@ -5,14 +5,45 @@ interface ThoughtSummaryEntryWidgetProps {
|
||||
visualMode: AppHubVisualMode;
|
||||
thoughtCount: number;
|
||||
onOpen: () => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const ThoughtSummaryEntryWidget = ({
|
||||
visualMode,
|
||||
thoughtCount,
|
||||
onOpen,
|
||||
compact = false,
|
||||
}: ThoughtSummaryEntryWidgetProps) => {
|
||||
const cinematic = visualMode === 'cinematic';
|
||||
const countLabel = thoughtCount > 99 ? '99+' : `${thoughtCount}`;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className={cn(
|
||||
'group inline-flex h-10 items-center gap-2 rounded-full border px-3 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/75',
|
||||
cinematic
|
||||
? 'border-white/22 bg-white/10 text-white/86 hover:bg-white/16'
|
||||
: 'border-brand-dark/14 bg-white/72 text-brand-dark/80 hover:bg-white/90',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden className="text-sm">✍</span>
|
||||
<span>메모</span>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-[1.25rem] items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
cinematic
|
||||
? 'bg-sky-200/20 text-sky-100'
|
||||
: 'bg-brand-primary/14 text-brand-primary/86',
|
||||
)}
|
||||
>
|
||||
{countLabel}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -53,7 +84,7 @@ export const ThoughtSummaryEntryWidget = ({
|
||||
: 'bg-brand-primary/14 text-brand-primary/86',
|
||||
)}
|
||||
>
|
||||
{thoughtCount > 99 ? '99+' : `${thoughtCount}`}
|
||||
{countLabel}
|
||||
</span>
|
||||
<span className={cn('text-xs transition-transform group-hover:translate-x-0.5', cinematic ? 'text-white/58' : 'text-brand-dark/52')}>
|
||||
열기 →
|
||||
|
||||
Reference in New Issue
Block a user