| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- import React, { useState, useEffect, useRef } from 'react';
- import { isFormatSupportedForPreview } from '../constants/fileSupport';
- import { PDFStatus } from '../types';
- import { pdfPreviewService } from '../services/pdfPreviewService';
- import { useToast } from '../contexts/ToastContext';
- import { useConfirm } from '../contexts/ConfirmContext';
- import { X, FileText, Loader, AlertCircle, Maximize2, Eye, Download, ExternalLink, RefreshCw, Scissors, ChevronLeft, ChevronRight } from 'lucide-react';
- import { PDFSelectionTool } from './PDFSelectionTool';
- import { CreateNoteFromPDFDialog } from './CreateNoteFromPDFDialog';
- import { noteService } from '../services/noteService';
- import { useLanguage } from '../contexts/LanguageContext';
- import { knowledgeBaseService } from '../services/knowledgeBaseService';
- import * as pdfjs from 'pdfjs-dist';
- // Set worker path for PDF.js
- pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`;
- interface PDFPreviewProps {
- fileId: string;
- fileName: string;
- authToken: string;
- groupId?: string;
- onClose: () => void;
- }
- export const PDFPreview: React.FC<PDFPreviewProps> = ({ fileId, fileName, authToken, groupId, onClose }) => {
- const [status, setStatus] = useState<PDFStatus>({ status: 'pending' });
- const [loading, setLoading] = useState(true);
- const [isFullscreen, setIsFullscreen] = useState(false);
- const [pdfUrl, setPdfUrl] = useState<string>('');
- const [iframeError, setIframeError] = useState(false);
- const [isSelectionMode, setIsSelectionMode] = useState(false);
- const [currentPage, setCurrentPage] = useState(1);
- const [pdfBlob, setPdfBlob] = useState<Blob | null>(null);
- const [selectionData, setSelectionData] = useState<{ screenshot: Blob; text: string } | null>(null);
- const [numPages, setNumPages] = useState<number>(0);
- const [pdfDoc, setPdfDoc] = useState<pdfjs.PDFDocumentProxy | null>(null);
- const [zoomLevel, setZoomLevel] = useState<number>(1.0); // ズームレベルの状態を追加
- const currentRenderTask = useRef<pdfjs.RenderTask | null>(null); // 現在のレンダリングタスクを保存
- const scrollContainerRef = useRef<HTMLDivElement>(null);
- const flipDirection = useRef<'next' | 'prev' | null>(null);
- const lastFlipTime = useRef<number>(0);
- const { showToast } = useToast();
- const { confirm } = useConfirm();
- const { t, language } = useLanguage();
- const containerRef = React.useRef<HTMLDivElement>(null);
- const canvasRef = useRef<HTMLCanvasElement>(null);
- useEffect(() => {
- if (status.status === 'ready') {
- pdfPreviewService.getPDFUrl(fileId)
- .then(result => {
- setPdfUrl(result.url); // ダウンロード用にpdfUrlを設定
- // PDFデータを取得してblob URLを作成
- fetch(result.url)
- .then(async response => {
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- throw new Error(errorData.message || 'Failed to fetch PDF data');
- }
- return response.blob();
- })
- .then(blob => {
- setPdfBlob(blob);
- // PDF文書の読み込みとレンダリングを開始
- loadAndRenderPDF(blob);
- })
- .catch((err) => {
- console.error('PDF fetch error:', err);
- setIframeError(true);
- setStatus({ status: 'failed', error: err.message });
- });
- })
- .catch((err) => {
- console.error('getPDFUrl error:', err);
- setIframeError(true);
- setStatus({ status: 'failed', error: err.message });
- });
- }
- }, [status.status, fileId]);
- useEffect(() => {
- if (pdfDoc && currentPage) {
- // ページ切り替えまたはズームレベル変更時に再レンダリング
- renderCurrentPage(pdfDoc, currentPage);
- }
- }, [currentPage, pdfDoc, zoomLevel]);
- const isSupported = isFormatSupportedForPreview(fileName);
- useEffect(() => {
- if (isSupported) {
- checkPDFStatus();
- const interval = setInterval(checkPDFStatus, 3000);
- return () => clearInterval(interval);
- } else {
- setLoading(false);
- setStatus({ status: 'failed', error: t('previewNotSupported') });
- }
- }, [fileId, isSupported]);
- const checkPDFStatus = async () => {
- try {
- const pdfStatus = await pdfPreviewService.getPDFStatus(fileId);
- setStatus(pdfStatus);
- // ステータスがpendingの場合、変換を能動的にトリガー
- if (pdfStatus.status === 'pending') {
- setStatus({ status: 'converting' });
- try {
- // PDF URLにアクセスして変換をトリガー
- await pdfPreviewService.preloadPDF(fileId);
- } catch (error) {
- console.log('Preload triggered, conversion should start');
- }
- }
- if (pdfStatus.status === 'ready' || pdfStatus.status === 'failed') {
- setLoading(false);
- }
- } catch (error: any) {
- setLoading(false);
- const errorMessage = error.message || t('checkPDFStatusFailed');
- setStatus({ status: 'failed', error: errorMessage });
- showToast(errorMessage, 'error');
- }
- };
- const loadAndRenderPDF = async (blob: Blob) => {
- try {
- const pdfData = await blob.arrayBuffer();
- const pdf = await pdfjs.getDocument({ data: pdfData }).promise;
- setPdfDoc(pdf);
- setNumPages(pdf.numPages);
- if (currentPage > pdf.numPages) {
- setCurrentPage(pdf.numPages);
- }
- renderCurrentPage(pdf, currentPage);
- } catch (error) {
- console.error('Failed to load PDF:', error);
- setIframeError(true);
- }
- };
- const handleZoomIn = () => {
- setZoomLevel(prev => Math.min(3.0, prev + 0.1));
- };
- const handleZoomOut = () => {
- setZoomLevel(prev => Math.max(0.5, prev - 0.1));
- };
- const handleResetZoom = () => {
- setZoomLevel(1.0);
- };
- const renderCurrentPage = async (pdf: pdfjs.PDFDocumentProxy, pageNum: number) => {
- if (!canvasRef.current) return;
- try {
- // 進行中のレンダリングタスクが存在する場合、キャンセルする
- if (currentRenderTask.current) {
- currentRenderTask.current.cancel();
- }
- const page = await pdf.getPage(pageNum);
- const canvas = canvasRef.current;
- const context = canvas.getContext('2d');
- if (!context) return;
- // Get container dimensions
- if (!containerRef.current) return;
- const container = containerRef.current;
- const containerWidth = container.clientWidth;
- const containerHeight = container.clientHeight;
- // Handle high DPI displays
- const devicePixelRatio = window.devicePixelRatio || 1;
- // Calculate scale to fit page in container width while maintaining aspect ratio
- const viewport = page.getViewport({ scale: 1 });
- const baseScale = (containerWidth - 48) / viewport.width; // Add padding for width
- // Apply zoom level to base scale
- const scale = baseScale * zoomLevel;
- const finalScale = scale * devicePixelRatio;
- const scaledViewport = page.getViewport({ scale: finalScale });
- const cssViewport = page.getViewport({ scale: scale });
- // Set canvas dimensions with device pixel ratio
- canvas.width = scaledViewport.width;
- canvas.height = scaledViewport.height;
- // Set CSS dimensions explicitly for high-DPI
- canvas.style.width = `${cssViewport.width}px`;
- canvas.style.height = `${cssViewport.height}px`;
- // Reset any previous transforms
- context.setTransform(1, 0, 0, 1, 0, 0);
- // Clear canvas
- context.clearRect(0, 0, canvas.width, canvas.height);
- // Fill with white background
- context.fillStyle = 'white';
- context.fillRect(0, 0, canvas.width, canvas.height);
- // Set proper transform for high DPI
- context.scale(devicePixelRatio, devicePixelRatio);
- // Create and save the new render task
- const renderContext = {
- canvasContext: context,
- viewport: page.getViewport({ scale: scale }),
- };
- // Save the render task to allow for cancellation
- currentRenderTask.current = page.render(renderContext);
- // Wait for rendering to complete
- await currentRenderTask.current.promise;
- // Clear the current render task
- currentRenderTask.current = null;
- // ページめくり後のスクロール位置調整
- if (flipDirection.current && scrollContainerRef.current) {
- const container = scrollContainerRef.current;
- if (flipDirection.current === 'next') {
- container.scrollTop = 0;
- } else if (flipDirection.current === 'prev') {
- container.scrollTop = container.scrollHeight;
- }
- flipDirection.current = null;
- }
- } catch (error) {
- if (error instanceof Error && error.name !== 'RenderingCancelledException') {
- console.error('Failed to render PDF page:', error);
- }
- // Clear the current render task even if there's an error
- currentRenderTask.current = null;
- }
- };
- const handleFullscreen = () => {
- setIsFullscreen(!isFullscreen);
- };
- const handleDownload = () => {
- if (pdfUrl) {
- // pdfUrlが既にある場合、直接ダウンロード
- const link = document.createElement('a');
- link.href = pdfUrl;
- link.download = fileName.replace(/\.[^/.]+$/, '.pdf');
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- } else {
- // pdfUrlがない場合、直接取得してダウンロードを試みる
- pdfPreviewService.getPDFUrl(fileId)
- .then(result => {
- const link = document.createElement('a');
- link.href = result.url;
- link.download = fileName.replace(/\.[^/.]+$/, '.pdf');
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- })
- .catch(error => {
- console.error('Failed to download PDF:', error);
- showToast('error', t('downloadPDFFailed'));
- });
- }
- };
- const handleOpenInNewTab = () => {
- if (pdfUrl) {
- window.open(pdfUrl, '_blank');
- } else {
- // pdfUrlがない場合、直接取得して開くことを試みる
- pdfPreviewService.getPDFUrl(fileId)
- .then(result => {
- window.open(result.url, '_blank');
- })
- .catch(error => {
- console.error('Failed to open PDF in new tab:', error);
- showToast('error', t('openPDFInNewTabFailed'));
- });
- }
- };
- const handleRegenerate = async () => {
- if (await confirm(t('confirmRegeneratePDF'))) {
- setStatus({ status: 'converting' });
- setLoading(true);
- try {
- await pdfPreviewService.preloadPDF(fileId, true);
- // 状態をリセットして再読み込みをトリガー
- setPdfUrl('');
- setIframeError(false);
- setPdfDoc(null);
- setPdfBlob(null);
- setNumPages(0);
- } catch (error) {
- showToast('error', t('requestRegenerationFailed'));
- setStatus({ status: 'failed', error: t('requestRegenerationFailed') });
- }
- }
- };
- const handleIframeError = () => {
- setIframeError(true);
- };
- const handleWheel = (e: React.WheelEvent) => {
- if (!scrollContainerRef.current || isSelectionMode) return;
- const container = scrollContainerRef.current;
- const { scrollTop, scrollHeight, clientHeight } = container;
- const now = Date.now();
- const throttleMs = 600; // 連続ページめくりを防止
- // 下にスクロールして次のページへ
- if (e.deltaY > 0 && scrollTop + clientHeight >= scrollHeight - 1) {
- if (currentPage < numPages && now - lastFlipTime.current > throttleMs) {
- flipDirection.current = 'next';
- lastFlipTime.current = now;
- setCurrentPage(prev => prev + 1);
- }
- }
- // 上にスクロールして前のページへ
- else if (e.deltaY < 0 && scrollTop <= 1) {
- if (currentPage > 1 && now - lastFlipTime.current > throttleMs) {
- flipDirection.current = 'prev';
- lastFlipTime.current = now;
- setCurrentPage(prev => prev - 1);
- }
- }
- };
- const handleSelectionComplete = (screenshot: Blob, text: string) => {
- // Set preliminary data and open dialog
- setSelectionData({ screenshot, text });
- setIsSelectionMode(false);
- };
- const handleSaveNote = async (title: string, content: string, selectedGroupId?: string) => {
- if (!authToken || !selectionData) return;
- try {
- await noteService.createFromPDFSelection(
- authToken,
- fileId,
- selectionData.screenshot,
- selectedGroupId || groupId,
- currentPage
- );
- showToast('success', t('noteCreatedSuccess'));
- setSelectionData(null);
- } catch (error) {
- console.error('Failed to create note:', error);
- showToast('error', t('noteCreatedFailed'));
- }
- };
- const renderContent = () => {
- switch (status.status) {
- case 'pending':
- return (
- <div className="flex flex-col items-center justify-center h-full text-gray-500">
- <Loader size={48} className="animate-spin mb-4" />
- <div className="text-lg font-medium mb-2">{t('preparingPDFConversion')}</div>
- <div className="text-sm">{t('pleaseWait')}</div>
- </div>
- );
- case 'converting':
- return (
- <div className="flex flex-col items-center justify-center h-full text-gray-500">
- <Loader size={48} className="animate-spin mb-4" />
- <div className="text-lg font-medium mb-2">{t('convertingPDF')}</div>
- <div className="text-sm">{t('pleaseWait')}</div>
- </div>
- );
- case 'failed':
- return (
- <div className="flex flex-col items-center justify-center h-full text-red-500">
- <AlertCircle size={48} className="mb-4" />
- <div className="text-lg font-medium mb-2">{t('pdfConversionFailed')}</div>
- <div className="text-sm text-gray-500 text-center max-w-md">
- {status.error || t('pdfConversionError')}
- </div>
- <button
- onClick={checkPDFStatus}
- className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
- >
- {t('retry')}
- </button>
- </div>
- );
- case 'ready':
- if (iframeError) {
- return (
- <div className="flex flex-col items-center justify-center h-full text-gray-500">
- <AlertCircle size={48} className="mb-4" />
- <div className="text-lg font-medium mb-2">{t('pdfLoadFailed')}</div>
- <div className="text-sm text-gray-500 mb-4">{t('pdfLoadError')}</div>
- <div className="flex gap-2">
- <button
- onClick={handleDownload}
- className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
- >
- <Download size={16} />
- {t('downloadPDF')}
- </button>
- <button
- onClick={handleOpenInNewTab}
- className="flex items-center gap-2 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
- >
- <ExternalLink size={16} />
- {t('openInNewWindow')}
- </button>
- </div>
- </div>
- );
- }
- if (!pdfDoc) {
- return (
- <div className="flex flex-col items-center justify-center h-full text-gray-500">
- <Loader size={48} className="animate-spin mb-4" />
- <div className="text-lg font-medium mb-2">{t('loadingPDF')}</div>
- </div>
- );
- }
- return (
- <div className="relative w-full h-full flex flex-col" ref={containerRef}>
- <div
- ref={scrollContainerRef}
- onWheel={handleWheel}
- className="flex-grow overflow-auto pdf-canvas-container bg-gray-100"
- >
- <div className="flex flex-col items-center py-12 pb-32 min-h-full">
- <canvas
- ref={canvasRef}
- className="bg-white shadow-xl max-w-full"
- />
- </div>
- </div>
- {isSelectionMode && (
- <PDFSelectionTool
- containerRef={containerRef}
- canvasRef={canvasRef}
- pdfBlob={pdfBlob}
- pageNumber={currentPage}
- authToken={authToken}
- zoomLevel={zoomLevel}
- onSelectionComplete={handleSelectionComplete}
- onCancel={() => setIsSelectionMode(false)}
- />
- )}
- <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 bg-white/90 border border-slate-200 px-3 py-1.5 rounded-full shadow-lg z-40">
- <div className="flex items-center border-r border-slate-200 pr-2 mr-2">
- <button
- onClick={handleZoomOut}
- className="p-1 hover:bg-slate-100 rounded text-slate-600 min-w-[28px] flex items-center justify-center"
- title={t('zoomOut')}
- >
- <span className="text-lg">−</span>
- </button>
- <span className="mx-1 text-sm text-slate-600 min-w-[40px] text-center">{Math.round(zoomLevel * 100)}%</span>
- <button
- onClick={handleZoomIn}
- className="p-1 hover:bg-slate-100 rounded text-slate-600 min-w-[28px] flex items-center justify-center"
- title={t('zoomIn')}
- >
- <span className="text-lg">+</span>
- </button>
- <button
- onClick={handleResetZoom}
- className="ml-1 p-1 px-2 hover:bg-slate-100 rounded text-slate-600 text-xs"
- title={t('resetZoom')}
- >
- 100%
- </button>
- </div>
- <button
- onClick={() => {
- const newPage = Math.max(1, currentPage - 1);
- setCurrentPage(newPage);
- }}
- className="p-1 hover:bg-slate-100 rounded text-slate-600"
- >
- <ChevronLeft size={16} />
- </button>
- <div className="flex items-center gap-1">
- <input
- type="number"
- value={currentPage}
- onChange={(e) => {
- const val = Math.max(1, Math.min(numPages, parseInt(e.target.value) || 1));
- setCurrentPage(val);
- }}
- className="w-12 text-center text-sm border-none focus:ring-0 bg-transparent font-medium"
- />
- <span className="text-sm text-slate-500">/ {numPages}</span>
- </div>
- <button
- onClick={() => {
- const newPage = Math.min(numPages, currentPage + 1);
- setCurrentPage(newPage);
- }}
- className="p-1 hover:bg-slate-100 rounded text-slate-600"
- >
- <ChevronRight size={16} />
- </button>
- </div>
- {selectionData && (
- <CreateNoteFromPDFDialog
- screenshot={selectionData.screenshot}
- extractedText={selectionData.text}
- authToken={authToken}
- initialGroupId={groupId}
- initialPageNumber={currentPage}
- onSave={handleSaveNote}
- onCancel={() => setSelectionData(null)}
- />
- )}
- </div>
- );
- default:
- return null;
- }
- };
- return (
- <div className={`fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ${isFullscreen ? 'p-0' : 'p-4'
- }`}>
- <div className={`bg-white rounded-lg overflow-hidden flex flex-col ${isFullscreen ? 'w-full h-full' : 'w-full max-w-4xl h-5/6'
- }`}>
- {/* 头部 */}
- <div className="flex items-center justify-between p-4 border-b bg-gray-50">
- <div className="flex items-center space-x-3">
- <FileText size={20} className="text-gray-600" />
- <div>
- <div className="font-medium text-gray-900">{fileName}</div>
- <div className="text-sm text-gray-500">{t('pdfPreview')}</div>
- </div>
- </div>
- <div className="flex items-center space-x-2">
- {status.status === 'ready' && !iframeError && (
- <>
- <div className="flex items-center gap-2 mr-2 border-r pr-2">
- <span className="text-sm text-gray-500">{t('selectPageNumber')}</span>
- <input
- type="number"
- min={1}
- max={numPages}
- value={currentPage}
- onChange={(e) => {
- const val = Math.max(1, Math.min(numPages, parseInt(e.target.value) || 1));
- setCurrentPage(val);
- }}
- className="w-16 px-2 py-1 border rounded text-sm"
- title={t('enterPageNumber')}
- />
- </div>
- <button
- onClick={() => setIsSelectionMode(!isSelectionMode)}
- className={`p-2 transition-colors ${isSelectionMode ? 'bg-blue-100 text-blue-600 rounded' : 'text-gray-400 hover:text-blue-600'}`}
- title={isSelectionMode ? t('exitSelectionMode') : t('clickToSelectAndNote')}
- >
- <Scissors size={18} />
- </button>
- <button
- onClick={handleRegenerate}
- className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
- title={t('regeneratePDF')}
- >
- <RefreshCw size={18} />
- </button>
- <button
- onClick={handleDownload}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- title={t('downloadPDF')}
- >
- <Download size={18} />
- </button>
- <button
- onClick={handleOpenInNewTab}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- title={t('openInNewWindow')}
- >
- <ExternalLink size={18} />
- </button>
- <button
- onClick={handleFullscreen}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- title={isFullscreen ? t('exitFullscreen') : t('fullscreenDisplay')}
- >
- <Maximize2 size={18} />
- </button>
- </>
- )}
- <button
- onClick={onClose}
- className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
- >
- <X size={18} />
- </button>
- </div>
- </div>
- {/* 内容区域 */}
- <div className="flex-1 h-full">
- {renderContent()}
- </div>
- </div>
- </div>
- );
- };
- interface PDFPreviewButtonProps {
- fileId: string;
- fileName: string;
- onPreview: () => void;
- }
- export const PDFPreviewButton: React.FC<PDFPreviewButtonProps> = ({
- fileId,
- fileName,
- onPreview
- }) => {
- const [status, setStatus] = useState<PDFStatus>({ status: 'pending' });
- const [loading, setLoading] = useState(true);
- const { t } = useLanguage();
- const isSupported = isFormatSupportedForPreview(fileName);
- useEffect(() => {
- if (isSupported) {
- checkStatus();
- } else {
- setLoading(false);
- }
- }, [fileId, isSupported]);
- const checkStatus = async () => {
- try {
- const pdfStatus = await pdfPreviewService.getPDFStatus(fileId);
- setStatus(pdfStatus);
- } catch (error) {
- // エラーを無視し、デフォルト状態を使用
- } finally {
- setLoading(false);
- }
- };
- const getIcon = () => {
- if (!isSupported) {
- return <Eye className="w-3 h-3 text-slate-200" />;
- }
- if (loading || status.status === 'converting') {
- return <Loader className="w-3 h-3 animate-spin" />;
- }
- if (status.status === 'failed') {
- return <AlertCircle className="w-3 h-3" />;
- }
- return <Eye className="w-3 h-3" />;
- };
- const getTitle = () => {
- if (!isSupported) return t('previewNotSupported');
- switch (status.status) {
- case 'ready': return t('pdfPreviewReady');
- case 'converting': return t('convertingInProgress');
- case 'failed': return t('conversionFailed');
- default: return t('generatePDFPreviewButton');
- }
- };
- return (
- <button
- onClick={onPreview}
- disabled={loading || status.status === 'converting' || !isSupported}
- className={`p-1 rounded transition-colors ${!isSupported
- ? 'text-slate-200 cursor-not-allowed'
- : status.status === 'failed'
- ? 'text-red-400 hover:text-red-500 hover:bg-red-50'
- : 'text-slate-400 hover:text-blue-500 hover:bg-blue-50'
- } disabled:opacity-50 disabled:cursor-not-allowed`}
- title={getTitle()}
- >
- {getIcon()}
- </button>
- );
- };
|