PreviewModalShell.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { useEffect, type ReactNode } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Maximize2, Minimize2, X } from 'lucide-react';
  4. import type { PreviewFullscreen } from '../hooks/usePreviewFullscreen';
  5. // One size for every file preview (#2976). Each modal used to carry its own
  6. // max-width (4xl/5xl/6xl at 80-85vh), which left most of a wide screen empty
  7. // for a window the user opened precisely to look at something closely.
  8. // min() keeps the panel off the edges on a laptop and stops it stretching to
  9. // a wall of pixels on an ultrawide.
  10. export const PREVIEW_PANEL_SIZE_CLASS = 'w-[min(1800px,96vw)] h-[94vh]';
  11. // Header icon buttons of every preview.
  12. export const previewIconButtonClass =
  13. 'p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-white transition-colors';
  14. interface PreviewModalShellProps {
  15. title: string;
  16. fullscreen: PreviewFullscreen;
  17. onClose: () => void;
  18. /** Type icon left of the title. */
  19. icon?: ReactNode;
  20. /** Rendered after the title, e.g. the 3D viewer's object-count badge. */
  21. titleExtra?: ReactNode;
  22. /** This preview's own header buttons, left of the fullscreen/close pair. */
  23. actions?: ReactNode;
  24. /** The 3D viewer closes on a backdrop click; the document previews do not. */
  25. closeOnBackdropClick?: boolean;
  26. /** Panel content: tabs, body and footers, as direct flex children. */
  27. children: ReactNode;
  28. }
  29. /**
  30. * Backdrop, panel sizing and header row shared by every file preview (#2976).
  31. *
  32. * Esc closes the modal, except while the browser owns it: in fullscreen Esc
  33. * leaves fullscreen and the preview stays open. Double-click-to-fullscreen is
  34. * wired by each modal on its own content area, because only the modal knows
  35. * which part of the panel is the preview.
  36. */
  37. export function PreviewModalShell({
  38. title,
  39. fullscreen,
  40. onClose,
  41. icon,
  42. titleExtra,
  43. actions,
  44. closeOnBackdropClick,
  45. children,
  46. }: PreviewModalShellProps) {
  47. const { t } = useTranslation();
  48. const { panelRef, isFullscreen, toggleFullscreen } = fullscreen;
  49. useEffect(() => {
  50. const handleKeyDown = (e: KeyboardEvent) => {
  51. if (e.key === 'Escape' && !document.fullscreenElement) onClose();
  52. };
  53. window.addEventListener('keydown', handleKeyDown);
  54. return () => window.removeEventListener('keydown', handleKeyDown);
  55. }, [onClose]);
  56. return (
  57. <div
  58. className={`fixed inset-0 bg-black/70 flex items-center justify-center z-50 ${isFullscreen ? 'p-0' : 'p-2 sm:p-4'}`}
  59. onClick={closeOnBackdropClick ? onClose : undefined}
  60. >
  61. <div
  62. ref={panelRef}
  63. className={`bg-bambu-dark-secondary border border-bambu-dark-tertiary flex flex-col ${
  64. isFullscreen
  65. ? 'w-full h-full max-w-none rounded-none'
  66. : `${PREVIEW_PANEL_SIZE_CLASS} max-w-full max-h-full rounded-lg`
  67. }`}
  68. onClick={closeOnBackdropClick ? (e) => e.stopPropagation() : undefined}
  69. >
  70. <div className="flex items-center justify-between gap-4 p-4 border-b border-bambu-dark-tertiary flex-shrink-0">
  71. <div className="flex items-center gap-2 min-w-0">
  72. {icon}
  73. <h2 className="text-lg font-semibold text-white truncate">{title}</h2>
  74. {titleExtra}
  75. </div>
  76. <div className="flex items-center gap-2 flex-shrink-0">
  77. {actions}
  78. <button
  79. onClick={toggleFullscreen}
  80. className={previewIconButtonClass}
  81. aria-label={isFullscreen ? t('fileManager.preview.exitFullscreen') : t('fileManager.preview.fullscreen')}
  82. title={isFullscreen ? t('fileManager.preview.exitFullscreen') : t('fileManager.preview.fullscreen')}
  83. >
  84. {isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
  85. </button>
  86. <button onClick={onClose} className={previewIconButtonClass} aria-label={t('common.close')}>
  87. <X className="w-5 h-5" />
  88. </button>
  89. </div>
  90. </div>
  91. {children}
  92. </div>
  93. </div>
  94. );
  95. }