FolderReadmePanel.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { useState } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { useTranslation } from 'react-i18next';
  4. import { ChevronDown, ChevronUp, FileText } from 'lucide-react';
  5. import ReactMarkdown from 'react-markdown';
  6. import remarkGfm from 'remark-gfm';
  7. import { api } from '../api/client';
  8. interface FolderReadmePanelProps {
  9. folderId: number;
  10. }
  11. /**
  12. * Side panel that renders a `.md` file from the selected folder (#1268).
  13. * Hidden when the folder has no markdown file. Disables raw HTML and links
  14. * stay text-only — same posture as the print-archive note panel.
  15. */
  16. export function FolderReadmePanel({ folderId }: FolderReadmePanelProps) {
  17. const { t } = useTranslation();
  18. const [collapsed, setCollapsed] = useState(false);
  19. const { data, isLoading, error } = useQuery({
  20. queryKey: ['folder-readme', folderId],
  21. queryFn: () => api.getLibraryFolderReadme(folderId),
  22. retry: false,
  23. staleTime: 30_000,
  24. });
  25. if (isLoading || error || !data) return null;
  26. return (
  27. <div className="mb-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg overflow-hidden">
  28. <button
  29. type="button"
  30. onClick={() => setCollapsed((v) => !v)}
  31. className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left hover:bg-bambu-dark/40 transition-colors"
  32. >
  33. <div className="flex items-center gap-2 min-w-0">
  34. <FileText className="w-4 h-4 text-bambu-green flex-shrink-0" />
  35. <span className="text-sm font-medium text-white truncate" title={data.filename}>
  36. {data.filename}
  37. </span>
  38. {data.truncated && (
  39. <span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400 flex-shrink-0">
  40. {t('fileManager.readme.truncated')}
  41. </span>
  42. )}
  43. </div>
  44. {collapsed ? (
  45. <ChevronDown className="w-4 h-4 text-bambu-gray flex-shrink-0" />
  46. ) : (
  47. <ChevronUp className="w-4 h-4 text-bambu-gray flex-shrink-0" />
  48. )}
  49. </button>
  50. {!collapsed && (
  51. <div className="px-4 py-3 border-t border-bambu-dark-tertiary max-h-96 overflow-y-auto text-sm text-bambu-gray-light leading-relaxed space-y-2">
  52. <ReactMarkdown
  53. remarkPlugins={[remarkGfm]}
  54. components={{
  55. h1: ({ children }) => <h1 className="text-lg font-semibold text-white mt-2 mb-1">{children}</h1>,
  56. h2: ({ children }) => <h2 className="text-base font-semibold text-white mt-2 mb-1">{children}</h2>,
  57. h3: ({ children }) => <h3 className="text-sm font-semibold text-white mt-2 mb-1">{children}</h3>,
  58. p: ({ children }) => <p className="my-1">{children}</p>,
  59. ul: ({ children }) => <ul className="list-disc list-inside space-y-0.5 ml-2">{children}</ul>,
  60. ol: ({ children }) => <ol className="list-decimal list-inside space-y-0.5 ml-2">{children}</ol>,
  61. li: ({ children }) => <li>{children}</li>,
  62. code: ({ children, ...props }) => {
  63. const inline = !(props as { className?: string }).className;
  64. return inline ? (
  65. <code className="px-1 py-0.5 bg-bambu-dark rounded text-xs font-mono text-bambu-green">{children}</code>
  66. ) : (
  67. <code className="block p-2 bg-bambu-dark rounded text-xs font-mono text-bambu-gray-light overflow-x-auto">{children}</code>
  68. );
  69. },
  70. pre: ({ children }) => <pre className="my-2">{children}</pre>,
  71. blockquote: ({ children }) => (
  72. <blockquote className="border-l-2 border-bambu-dark-tertiary pl-3 text-bambu-gray italic">{children}</blockquote>
  73. ),
  74. a: ({ children, href }) => (
  75. <a href={href} target="_blank" rel="noopener noreferrer" className="text-bambu-green hover:underline">
  76. {children}
  77. </a>
  78. ),
  79. table: ({ children }) => (
  80. <div className="overflow-x-auto">
  81. <table className="min-w-full text-xs border-collapse">{children}</table>
  82. </div>
  83. ),
  84. th: ({ children }) => <th className="border border-bambu-dark-tertiary px-2 py-1 text-left font-semibold text-white">{children}</th>,
  85. td: ({ children }) => <td className="border border-bambu-dark-tertiary px-2 py-1">{children}</td>,
  86. hr: () => <hr className="border-bambu-dark-tertiary my-2" />,
  87. }}
  88. >
  89. {data.content}
  90. </ReactMarkdown>
  91. </div>
  92. )}
  93. </div>
  94. );
  95. }