refactor(i18n): 사용자 문구 참조를 중앙화
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { formatThoughtCount, RAIL_ICON } from './constants';
|
||||
|
||||
interface FocusRightRailProps {
|
||||
@@ -25,8 +26,8 @@ export const FocusRightRail = ({
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="인박스 열기"
|
||||
title="인박스"
|
||||
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"
|
||||
>
|
||||
@@ -39,8 +40,8 @@ export const FocusRightRail = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Quick Controls 열기"
|
||||
title="Quick Controls"
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SceneAssetMap } from '@/entities/media';
|
||||
import type { PlanTier } from '@/entities/plan';
|
||||
import type { SceneTheme } from '@/entities/scene';
|
||||
import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { ExitHoldButton } from '@/features/exit-hold';
|
||||
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
|
||||
import { PlanPill } from '@/features/plan-pill';
|
||||
@@ -75,6 +76,7 @@ export const SpaceToolsDockWidget = ({
|
||||
onStatusMessage,
|
||||
onExitRequested,
|
||||
}: SpaceToolsDockWidgetProps) => {
|
||||
const { toolsDock, controlCenter } = copy.space;
|
||||
const [openPopover, setOpenPopover] = useState<SpaceAnchorPopoverId | null>(null);
|
||||
const [utilityPanel, setUtilityPanel] = useState<SpaceUtilityPanelId | null>(null);
|
||||
const [autoHideControls, setAutoHideControls] = useState(true);
|
||||
@@ -86,7 +88,7 @@ export const SpaceToolsDockWidget = ({
|
||||
|
||||
const selectedSoundLabel = useMemo(() => {
|
||||
return (
|
||||
SOUND_PRESETS.find((preset) => preset.id === selectedPresetId)?.label ?? SOUND_PRESETS[0]?.label ?? '기본'
|
||||
SOUND_PRESETS.find((preset) => preset.id === selectedPresetId)?.label ?? SOUND_PRESETS[0]?.label ?? copy.common.default
|
||||
);
|
||||
}, [selectedPresetId]);
|
||||
|
||||
@@ -233,11 +235,11 @@ export const SpaceToolsDockWidget = ({
|
||||
|
||||
setNoteDraft('');
|
||||
onStatusMessage({
|
||||
message: '인박스에 저장됨',
|
||||
message: toolsDock.inboxSaved,
|
||||
durationMs: 4200,
|
||||
priority: 'undo',
|
||||
action: {
|
||||
label: '실행취소',
|
||||
label: toolsDock.undo,
|
||||
onClick: () => {
|
||||
const removed = onDeleteThought(addedThought.id);
|
||||
|
||||
@@ -245,7 +247,7 @@ export const SpaceToolsDockWidget = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onStatusMessage({ message: '저장 취소됨' });
|
||||
onStatusMessage({ message: toolsDock.inboxSaveUndone });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -263,14 +265,14 @@ export const SpaceToolsDockWidget = ({
|
||||
}
|
||||
|
||||
onStatusMessage({
|
||||
message: '삭제됨',
|
||||
message: toolsDock.deleted,
|
||||
durationMs: 4200,
|
||||
priority: 'undo',
|
||||
action: {
|
||||
label: '실행취소',
|
||||
label: toolsDock.undo,
|
||||
onClick: () => {
|
||||
onRestoreThought(removedThought);
|
||||
onStatusMessage({ message: '삭제를 취소했어요.' });
|
||||
onStatusMessage({ message: toolsDock.deleteUndone });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -280,19 +282,19 @@ export const SpaceToolsDockWidget = ({
|
||||
const snapshot = onClearInbox();
|
||||
|
||||
if (snapshot.length === 0) {
|
||||
onStatusMessage({ message: '비울 항목이 없어요.' });
|
||||
onStatusMessage({ message: toolsDock.emptyToClear });
|
||||
return;
|
||||
}
|
||||
|
||||
onStatusMessage({
|
||||
message: '모두 비워짐',
|
||||
message: toolsDock.clearedAll,
|
||||
durationMs: 4200,
|
||||
priority: 'undo',
|
||||
action: {
|
||||
label: '실행취소',
|
||||
label: toolsDock.undo,
|
||||
onClick: () => {
|
||||
onRestoreThoughts(snapshot);
|
||||
onStatusMessage({ message: '복원했어요.' });
|
||||
onStatusMessage({ message: toolsDock.restored });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -304,28 +306,28 @@ export const SpaceToolsDockWidget = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onStatusMessage({ message: 'NORMAL 플랜 사용 중 · 잠금 항목에서만 업그레이드할 수 있어요.' });
|
||||
onStatusMessage({ message: toolsDock.normalPlanInfo });
|
||||
};
|
||||
|
||||
const handleLockedClick = (source: string) => {
|
||||
onStatusMessage({ message: `${source}은(는) PRO 기능이에요.` });
|
||||
onStatusMessage({ message: toolsDock.proFeatureLocked(source) });
|
||||
openUtilityPanel('paywall');
|
||||
};
|
||||
|
||||
const handleSelectProFeature = (featureId: string) => {
|
||||
const label =
|
||||
featureId === 'scene-packs'
|
||||
? 'Scene Packs'
|
||||
? toolsDock.featureLabels.scenePacks
|
||||
: featureId === 'sound-packs'
|
||||
? 'Sound Packs'
|
||||
: 'Profiles';
|
||||
? toolsDock.featureLabels.soundPacks
|
||||
: toolsDock.featureLabels.profiles;
|
||||
|
||||
onStatusMessage({ message: `${label} 준비 중(더미)` });
|
||||
onStatusMessage({ message: toolsDock.proFeaturePending(label) });
|
||||
};
|
||||
|
||||
const handleStartPro = () => {
|
||||
setPlan('pro');
|
||||
onStatusMessage({ message: '결제(더미)' });
|
||||
onStatusMessage({ message: toolsDock.purchaseMock });
|
||||
openUtilityPanel('control-center');
|
||||
};
|
||||
|
||||
@@ -370,7 +372,7 @@ export const SpaceToolsDockWidget = ({
|
||||
{isFocusMode && openPopover ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="팝오버 닫기"
|
||||
aria-label={toolsDock.popoverCloseAria}
|
||||
onClick={() => setOpenPopover(null)}
|
||||
className="fixed inset-0 z-30"
|
||||
/>
|
||||
@@ -417,7 +419,7 @@ export const SpaceToolsDockWidget = ({
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-white/14 bg-black/24 px-2.5 py-1.5 text-[11px] text-white/88 backdrop-blur-md transition-opacity hover:opacity-100"
|
||||
>
|
||||
<span aria-hidden className="text-white/82">{ANCHOR_ICON.notes}</span>
|
||||
<span>Notes {formatThoughtCount(thoughtCount)}</span>
|
||||
<span>{toolsDock.notesButton} {formatThoughtCount(thoughtCount)}</span>
|
||||
<span aria-hidden className="text-white/60">▾</span>
|
||||
</button>
|
||||
|
||||
@@ -485,7 +487,7 @@ export const SpaceToolsDockWidget = ({
|
||||
<SpaceSideSheet
|
||||
open={isFocusMode && utilityPanel !== null}
|
||||
title={utilityPanel ? UTILITY_PANEL_TITLE[utilityPanel] : ''}
|
||||
subtitle={utilityPanel === 'control-center' ? '배경 · 타이머 · 사운드를 그 자리에서 바꿔요.' : undefined}
|
||||
subtitle={utilityPanel === 'control-center' ? controlCenter.sideSheetSubtitle : undefined}
|
||||
headerAction={
|
||||
utilityPanel === 'control-center' ? (
|
||||
<PlanPill plan={plan} onClick={handlePlanPillClick} />
|
||||
@@ -539,8 +541,8 @@ export const SpaceToolsDockWidget = ({
|
||||
{utilityPanel === 'manage-plan' ? (
|
||||
<ManagePlanSheetContent
|
||||
onClose={() => setUtilityPanel(null)}
|
||||
onManage={() => onStatusMessage({ message: '구독 관리(더미)' })}
|
||||
onRestore={() => onStatusMessage({ message: '구매 복원(더미)' })}
|
||||
onManage={() => onStatusMessage({ message: toolsDock.manageSubscriptionMock })}
|
||||
onRestore={() => onStatusMessage({ message: toolsDock.restorePurchaseMock })}
|
||||
/>
|
||||
) : null}
|
||||
</SpaceSideSheet>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SpaceUtilityPanelId } from '../model/types';
|
||||
import { copy } from '@/shared/i18n';
|
||||
|
||||
export const ANCHOR_ICON = {
|
||||
sound: (
|
||||
@@ -66,10 +67,10 @@ export const RAIL_ICON = {
|
||||
};
|
||||
|
||||
export const UTILITY_PANEL_TITLE: Record<SpaceUtilityPanelId, string> = {
|
||||
'control-center': 'Quick Controls',
|
||||
inbox: '인박스',
|
||||
paywall: 'PRO',
|
||||
'manage-plan': '플랜 관리',
|
||||
'control-center': copy.space.toolsDock.utilityPanelTitle['control-center'],
|
||||
inbox: copy.space.toolsDock.utilityPanelTitle.inbox,
|
||||
paywall: copy.space.toolsDock.utilityPanelTitle.paywall,
|
||||
'manage-plan': copy.space.toolsDock.utilityPanelTitle['manage-plan'],
|
||||
};
|
||||
|
||||
export const formatThoughtCount = (count: number) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import type { RecentThought } from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { InboxList } from '@/features/inbox';
|
||||
|
||||
interface InboxToolPanelProps {
|
||||
@@ -20,13 +21,13 @@ export const InboxToolPanel = ({
|
||||
return (
|
||||
<div className="relative space-y-3.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs text-white/58">나중에 모아보는 읽기 전용 인박스</p>
|
||||
<p className="text-xs text-white/58">{copy.space.inbox.readOnly}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
className="rounded-full border border-white/20 bg-white/8 px-2.5 py-1 text-[11px] text-white/74 transition-colors hover:bg-white/14 hover:text-white"
|
||||
>
|
||||
모두 비우기
|
||||
{copy.space.inbox.clearAll}
|
||||
</button>
|
||||
</div>
|
||||
<InboxList
|
||||
@@ -38,15 +39,15 @@ export const InboxToolPanel = ({
|
||||
{confirmOpen ? (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-2xl bg-slate-950/70 px-3 backdrop-blur-sm">
|
||||
<div className="w-full max-w-[272px] rounded-2xl border border-white/14 bg-slate-900/92 p-3.5 shadow-xl shadow-slate-950/45">
|
||||
<p className="text-sm font-medium text-white/92">정말 인박스를 비울까요?</p>
|
||||
<p className="mt-1 text-[11px] text-white/60">실수라면 토스트에서 실행취소할 수 있어요.</p>
|
||||
<p className="text-sm font-medium text-white/92">{copy.space.inbox.clearConfirmTitle}</p>
|
||||
<p className="mt-1 text-[11px] text-white/60">{copy.space.inbox.clearConfirmDescription}</p>
|
||||
<div className="mt-3 flex justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmOpen(false)}
|
||||
className="rounded-full border border-white/20 bg-white/8 px-2.5 py-1 text-[11px] text-white/74 transition-colors hover:bg-white/14 hover:text-white"
|
||||
>
|
||||
취소
|
||||
{copy.common.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -56,7 +57,7 @@ export const InboxToolPanel = ({
|
||||
}}
|
||||
className="rounded-full border border-rose-200/34 bg-rose-200/16 px-2.5 py-1 text-[11px] text-rose-100/92 transition-colors hover:bg-rose-200/24"
|
||||
>
|
||||
비우기
|
||||
{copy.space.inbox.clearButton}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import type { SceneTheme } from '@/entities/scene';
|
||||
import type { TimerPreset } from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { DEFAULT_PRESET_OPTIONS } from '@/shared/config/settingsOptions';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
@@ -23,6 +24,7 @@ export const SettingsToolPanel = ({
|
||||
onSelectScene,
|
||||
onSelectTimer,
|
||||
}: SettingsToolPanelProps) => {
|
||||
const { settingsPanel } = copy.space;
|
||||
const [reduceMotion, setReduceMotion] = useState(false);
|
||||
const [defaultPresetId, setDefaultPresetId] = useState<
|
||||
(typeof DEFAULT_PRESET_OPTIONS)[number]['id']
|
||||
@@ -33,8 +35,8 @@ export const SettingsToolPanel = ({
|
||||
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Reduce Motion</p>
|
||||
<p className="mt-1 text-xs text-white/58">화면 전환을 조금 더 차분하게 표시합니다.</p>
|
||||
<p className="text-sm font-medium text-white">{settingsPanel.reduceMotion}</p>
|
||||
<p className="mt-1 text-xs text-white/58">{settingsPanel.reduceMotionDescription}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -59,8 +61,8 @@ export const SettingsToolPanel = ({
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
||||
<p className="text-sm font-medium text-white">배경</p>
|
||||
<p className="mt-1 text-xs text-white/58">몰입 중에도 배경 scene을 바꿀 수 있어요.</p>
|
||||
<p className="text-sm font-medium text-white">{settingsPanel.background}</p>
|
||||
<p className="mt-1 text-xs text-white/58">{settingsPanel.backgroundDescription}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{scenes.slice(0, 4).map((scene) => {
|
||||
const selected = scene.id === selectedSceneId;
|
||||
@@ -85,8 +87,8 @@ export const SettingsToolPanel = ({
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
||||
<p className="text-sm font-medium text-white">타이머 프리셋</p>
|
||||
<p className="mt-1 text-xs text-white/58">기본 프리셋만 빠르게 고를 수 있어요.</p>
|
||||
<p className="text-sm font-medium text-white">{settingsPanel.timerPreset}</p>
|
||||
<p className="mt-1 text-xs text-white/58">{settingsPanel.timerPresetDescription}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{timerPresets.slice(0, 3).map((preset) => {
|
||||
const selected = preset.label === selectedTimerLabel;
|
||||
@@ -111,7 +113,7 @@ export const SettingsToolPanel = ({
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
||||
<p className="text-sm font-medium text-white">기본 프리셋</p>
|
||||
<p className="text-sm font-medium text-white">{settingsPanel.defaultPreset}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{DEFAULT_PRESET_OPTIONS.map((preset) => (
|
||||
<button
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { TODAY_STATS, WEEKLY_STATS } from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
|
||||
export const StatsToolPanel = () => {
|
||||
const previewStats = [TODAY_STATS[0], TODAY_STATS[1], WEEKLY_STATS[0], WEEKLY_STATS[2]];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-white/58">오늘 흐름과 최근 7일 리듬을 가볍게 확인하세요.</p>
|
||||
<p className="text-xs text-white/58">{copy.space.statsPanel.description}</p>
|
||||
|
||||
<section className="grid gap-2.5 sm:grid-cols-2">
|
||||
{previewStats.map((stat) => (
|
||||
@@ -19,7 +20,7 @@ export const StatsToolPanel = () => {
|
||||
|
||||
<section className="rounded-2xl border border-white/14 bg-white/6 p-3.5">
|
||||
<div className="h-28 rounded-xl border border-dashed border-white/20 bg-[linear-gradient(180deg,rgba(148,163,184,0.14),rgba(148,163,184,0.02))]" />
|
||||
<p className="mt-2 text-[11px] text-white/54">그래프 플레이스홀더</p>
|
||||
<p className="mt-2 text-[11px] text-white/54">{copy.space.statsPanel.graphPlaceholder}</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { copy } from '@/shared/i18n';
|
||||
|
||||
interface QuickNotesPopoverProps {
|
||||
noteDraft: string;
|
||||
onDraftChange: (value: string) => void;
|
||||
@@ -16,7 +18,7 @@ export const QuickNotesPopover = ({
|
||||
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 }}
|
||||
>
|
||||
<p className="text-[11px] text-white/56">떠오른 생각을 잠깐 주차해요</p>
|
||||
<p className="text-[11px] text-white/56">{copy.space.quickNotes.title}</p>
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<input
|
||||
value={noteDraft}
|
||||
@@ -29,7 +31,7 @@ export const QuickNotesPopover = ({
|
||||
event.preventDefault();
|
||||
onDraftEnter();
|
||||
}}
|
||||
placeholder="떠오른 생각을 잠깐 주차…"
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
@@ -37,10 +39,10 @@ export const QuickNotesPopover = ({
|
||||
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>
|
||||
<p className="mt-2 text-[11px] text-white/52">나중에 인박스에서 정리해요.</p>
|
||||
<p className="mt-2 text-[11px] text-white/52">{copy.space.quickNotes.hint}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
import type { SoundPreset } from '@/entities/session';
|
||||
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
import { copy } from '@/shared/i18n';
|
||||
|
||||
interface QuickSoundPopoverProps {
|
||||
selectedSoundLabel: string;
|
||||
@@ -32,14 +33,14 @@ export const QuickSoundPopover = ({
|
||||
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 }}
|
||||
>
|
||||
<p className="text-[11px] text-white/56">현재 사운드</p>
|
||||
<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="mt-3 rounded-xl border border-white/14 bg-white/[0.04] px-2.5 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isSoundMuted ? '음소거 해제' : '음소거'}
|
||||
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]"
|
||||
>
|
||||
@@ -53,7 +54,7 @@ export const QuickSoundPopover = ({
|
||||
value={soundVolume}
|
||||
onChange={(event) => onVolumeChange(Number(event.target.value))}
|
||||
onKeyDown={onVolumeKeyDown}
|
||||
aria-label="사운드 볼륨"
|
||||
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">
|
||||
@@ -62,7 +63,7 @@ export const QuickSoundPopover = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-[11px] text-white/56">빠른 전환</p>
|
||||
<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;
|
||||
|
||||
Reference in New Issue
Block a user