Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions apps/web/src/components/molecules/RecordButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,33 @@ export function RecordButton({
<div className="relative">
{/* 背景の光彩エフェクト */}
<div
className={`absolute inset-0 ${getBackgroundGradient()} rounded-full opacity-50 blur-2xl ${getAnimationClass()}`}
className={`absolute inset-0 ${getBackgroundGradient()} rounded-full blur-2xl opacity-50 ${getAnimationClass()}`}
/>

{/* メインボタン */}
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`relative h-32 w-32 rounded-full ${getBackgroundGradient()}shadow-2xl transition-all duration-300 ease-out hover:scale-105 hover:shadow-3xl active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 ${getRingEffect()}
${getAnimationClass()}group`}
className={`
relative w-32 h-32 rounded-full
${getBackgroundGradient()}
shadow-2xl
transition-all duration-300 ease-out
hover:scale-105 hover:shadow-3xl
active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed
${getRingEffect()}
${getAnimationClass()}
group
`}
aria-label="録音"
>
{/* グラスエフェクト */}
<div className="absolute inset-0 rounded-full bg-white/10 backdrop-blur-sm" />

{/* 内部の光沢 */}
<div className="absolute top-2 right-2 left-2 h-1/3 rounded-t-full bg-gradient-to-b from-white/30 to-transparent" />
<div className="absolute top-2 left-2 right-2 h-1/3 bg-gradient-to-b from-white/30 to-transparent rounded-t-full" />

{/* コンテンツ */}
<div className="relative z-10">
Expand All @@ -100,8 +110,8 @@ export function RecordButton({
{/* 波紋エフェクト(録音中) */}
{status === "recording" && (
<>
<div className="absolute inset-0 animate-ping-slow rounded-full border-2 border-red-400/50" />
<div className="absolute inset-0 animate-ping-slower rounded-full border-2 border-red-400/30" />
<div className="absolute inset-0 rounded-full border-2 border-red-400/50 animate-ping-slow" />
<div className="absolute inset-0 rounded-full border-2 border-red-400/30 animate-ping-slower" />
</>
)}
</div>
Expand Down
47 changes: 47 additions & 0 deletions apps/web/src/components/molecules/RecordingControls/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { motion } from "framer-motion";
import { RippleEffect } from "../../atoms/RippleEffect";
import type { RecordingControlsProps } from "./types";

/**
* 録音コントロールコンポーネント
*
* @description
* 録音の一時停止・停止ボタンを提供するコンポーネント
*
* @param onStop 停止ボタンクリック時のコールバック
* @param isRecording 録音中かどうか
*/
export function RecordingControls({
onStop,
isRecording,
}: RecordingControlsProps) {
return (
<motion.div
className="fixed left-0 right-0 bottom-20 sm:bottom-10 md:bottom-12 flex justify-center"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
delay: 0.3,
type: "spring",
stiffness: 200,
}}
>
<motion.button
onClick={onStop}
className="relative w-20 h-20 sm:w-24 sm:h-24 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-all duration-300 shadow-lg touch-manipulation z-50"
whileTap={{ scale: 0.95 }}
>
{/* 一時停止アイコン */}
<div className="flex items-center gap-1.5">
<div className="w-1 h-8 sm:h-10 bg-gray-900 rounded-full" />
<div className="w-1 h-8 sm:h-10 bg-gray-900 rounded-full" />
</div>

{/* リップルエフェクト */}
<RippleEffect isActive={isRecording} borderColor="border-gray-400" />
</motion.button>
</motion.div>
);
}
14 changes: 14 additions & 0 deletions apps/web/src/components/molecules/RecordingControls/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* RecordingControlsコンポーネントのプロパティ型定義
*/
export interface RecordingControlsProps {
/**
* 停止ボタンクリック時のコールバック
*/
onStop: () => void;

/**
* 録音中かどうか
*/
isRecording: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import { motion } from "framer-motion";
import { RecordingControls } from "../RecordingControls";
import { RecordingHeader } from "../RecordingHeader";
import { RecordingTimer } from "../RecordingTimer";
import { WaveformDisplay } from "../WaveformDisplay";
import type { RecordingExpandedDisplayProps } from "./types";

/**
* 録音拡大表示コンポーネント
*
* @description
* 録音インターフェースの展開時に表示されるコンポーネント
*
* @param status 録音状態
* @param recordingTime 録音時間
* @param waveformData 波形データ
* @param formatTime 時間フォーマット関数
* @param onCancel キャンセルボタンクリック時のコールバック
* @param onNext 次へボタンクリック時のコールバック
* @param onStop 停止ボタンクリック時のコールバック
*/
export function RecordingExpandedDisplay({
status,
recordingTime,
waveformData,
formatTime,
onCancel,
onNext,
onStop,
}: RecordingExpandedDisplayProps) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex-1 flex flex-col"
>
{/* ヘッダー部分 */}
<RecordingHeader
isRecording={status === "recording"}
onCancel={onCancel}
onNext={onNext}
/>

{/* メインコンテンツエリア */}
<div className="flex-1 flex flex-col items-center justify-start pt-4 px-6 sm:px-8">
{/* タイマー表示 */}
<RecordingTimer time={recordingTime} formatTime={formatTime} />

{/* 波形表示 */}
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2 }}
className="w-full max-w-2xl px-4 mb-4"
>
<WaveformDisplay
isRecording={status === "recording"}
isCompleted={status === "completed"}
recordingTime={recordingTime}
waveformData={waveformData}
height={160}
className="h-32 sm:h-40"
waveColor="#000000"
backgroundColor="#f3f4f6"
key={`expanded-${status}-${recordingTime}`}
/>
</motion.div>
</div>

{/* 停止ボタン */}
<RecordingControls onStop={onStop} isRecording={status === "recording"} />
</motion.div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* RecordingExpandedDisplayコンポーネントのプロパティ型定義
*/
export interface RecordingExpandedDisplayProps {
/**
* 録音状態
*/
status: "idle" | "recording" | "completed";

/**
* 録音時間
*/
recordingTime: number;

/**
* 波形データ
*/
waveformData: number[];

/**
* 時間フォーマット関数
* @param time - 録音時間(秒)
* @returns フォーマットされた時間文字列
*/
formatTime: (time: number) => string;

/**
* キャンセルボタンクリック時のコールバック
*/
onCancel: () => void;

/**
* 次へボタンクリック時のコールバック
*/
onNext: () => void;

/**
* 停止ボタンクリック時のコールバック
*/
onStop: () => void;
}
49 changes: 49 additions & 0 deletions apps/web/src/components/molecules/RecordingHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { BlinkingIndicator } from "../../atoms/BlinkingIndicator";
import type { RecordingHeaderProps } from "./types";

/**
* 録音インターフェースのヘッダーコンポーネント
*
* @description
* 録音中の状態表示と操作ボタンを含むヘッダー
*
* @param isRecording 録音中かどうか
* @param onCancel キャンセルボタンクリック時のコールバック
* @param onNext 次へボタンクリック時のコールバック
*/
export function RecordingHeader({
isRecording,
onCancel,
onNext,
}: RecordingHeaderProps) {
return (
<div className="flex justify-between items-center px-6 sm:px-8 py-4 pb-2 sm:pb-3 relative">
<button
onClick={onCancel}
className="text-gray-600 hover:text-gray-900 font-medium text-base sm:text-lg transition-colors touch-manipulation"
>
キャンセル
</button>

<div className="absolute left-1/2 transform -translate-x-1/2 flex items-center gap-2">
<BlinkingIndicator
isActive={isRecording}
size="w-2 h-2"
color="bg-red-500"
/>
<span className="text-gray-900 font-medium text-base sm:text-lg">
録音中
</span>
</div>

<button
onClick={onNext}
className="text-gray-900 hover:text-gray-700 font-medium text-base sm:text-lg transition-colors touch-manipulation"
>
次へ
</button>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/web/src/components/molecules/RecordingHeader/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* RecordingHeaderコンポーネントのプロパティ型定義
*/
export interface RecordingHeaderProps {
/**
* 録音中かどうか
*/
isRecording: boolean;

/**
* キャンセルボタンクリック時のコールバック
*/
onCancel: () => void;

/**
* 次へボタンクリック時のコールバック
*/
onNext: () => void;
}
26 changes: 26 additions & 0 deletions apps/web/src/components/molecules/RecordingInitialState/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { motion } from "framer-motion";
import { MdMic } from "react-icons/md";
import type { RecordingInitialStateProps } from "./types";

/**
* 録音初期状態コンポーネント
*
* @description
* 録音開始前の初期状態を表示するコンポーネント
*
* @param onClick 録音ボタンクリック時のコールバック
*/
export function RecordingInitialState({ onClick }: RecordingInitialStateProps) {
return (
<motion.button
onClick={onClick}
className="w-48 h-16 sm:w-20 sm:h-20 mb-5 rounded-full bg-black hover:bg-gray-800 flex items-center justify-center shadow-2xl transition-all duration-300 touch-manipulation"
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.05 }}
>
<MdMic className="w-7 h-7 sm:w-8 sm:h-8 text-white" />
</motion.button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* RecordingInitialStateコンポーネントのプロパティ型定義
*/
export interface RecordingInitialStateProps {
/**
* 録音ボタンクリック時のコールバック
*/
onClick: () => void;
}
Loading
Loading