diff --git a/apps/web/package.json b/apps/web/package.json
index 72ec1b3..cb5c089 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -17,11 +17,13 @@
"@tpmjs/types": "workspace:*",
"@tpmjs/ui": "workspace:*",
"@tpmjs/utils": "workspace:*",
+ "@types/react-syntax-highlighter": "^15.5.13",
"next": "^16.0.4",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
+ "react-syntax-highlighter": "^16.1.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
diff --git a/apps/web/src/components/Markdown.tsx b/apps/web/src/components/Markdown.tsx
index 8caef0a..a6bc1cc 100644
--- a/apps/web/src/components/Markdown.tsx
+++ b/apps/web/src/components/Markdown.tsx
@@ -1,6 +1,9 @@
'use client';
import ReactMarkdown from 'react-markdown';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+// eslint-disable-next-line import/no-internal-modules
+import { solarizedlight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import remarkGfm from 'remark-gfm';
@@ -18,63 +21,71 @@ interface MarkdownProps {
export function Markdown({ content, className = '' }: MarkdownProps): React.ReactElement {
return (
{
+ const match = /language-(\w+)/.exec(className || '');
+ const language = match ? match[1] : '';
const isInline = !className;
+
if (isInline) {
return (
{children}
);
}
+
return (
-
- {children}
-
- );
- },
- // Better styled pre blocks
- pre: ({ children, ...props }) => {
- return (
-
- {children}
-
+ {String(children).replace(/\n$/, '')}
+
+ );
+ },
+ // Pre wrapper for code blocks (SyntaxHighlighter handles its own styling)
+ pre: ({ children, ...props }) => {
+ return (
+
);
},
// Make links open in new tab with better styling
@@ -92,12 +103,12 @@ export function Markdown({ content, className = '' }: MarkdownProps): React.Reac
);
},
- // Better table styling
+ // Better table styling with improved readability
table: ({ children, ...props }) => {
return (
-
+
{children}
@@ -105,6 +116,44 @@ export function Markdown({ content, className = '' }: MarkdownProps): React.Reac
);
},
+ // Table header with better styling
+ thead: ({ children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+ },
+ // Table header cells with better spacing
+ th: ({ children, ...props }) => {
+ return (
+ |
+ {children}
+ |
+ );
+ },
+ // Table data cells with better spacing
+ td: ({ children, ...props }) => {
+ return (
+
+ {children}
+ |
+ );
+ },
+ // Table rows with hover effect
+ tr: ({ children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+ },
}}
>
{content}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0b7e802..f3ca6fa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
'@tpmjs/utils':
specifier: workspace:*
version: link:../../packages/utils
+ '@types/react-syntax-highlighter':
+ specifier: ^15.5.13
+ version: 15.5.13
next:
specifier: ^16.0.4
version: 16.0.4(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -74,6 +77,9 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.7)(react@19.2.0)
+ react-syntax-highlighter:
+ specifier: ^16.1.0
+ version: 16.1.0(react@19.2.0)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
@@ -1950,11 +1956,17 @@ packages:
'@types/node@22.19.1':
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
+ '@types/prismjs@1.26.5':
+ resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
+
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
+ '@types/react-syntax-highlighter@15.5.13':
+ resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==}
+
'@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
@@ -2907,6 +2919,9 @@ packages:
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+ fault@1.0.4:
+ resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
+
fd-package-json@2.0.0:
resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==}
@@ -2953,6 +2968,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
+ format@0.2.2:
+ resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
+ engines: {node: '>=0.4.x'}
+
formatly@0.3.0:
resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==}
engines: {node: '>=18.3.0'}
@@ -3129,6 +3148,12 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ highlight.js@10.7.3:
+ resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
+
+ highlightjs-vue@1.0.0:
+ resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -3531,6 +3556,9 @@ packages:
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+ lowlight@1.20.0:
+ resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
+
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -4072,6 +4100,10 @@ packages:
typescript:
optional: true
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@@ -4131,6 +4163,12 @@ packages:
'@types/react': '>=18'
react: '>=18'
+ react-syntax-highlighter@16.1.0:
+ resolution: {integrity: sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==}
+ engines: {node: '>= 16.20.2'}
+ peerDependencies:
+ react: '>= 0.14.0'
+
react@19.2.0:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'}
@@ -4166,6 +4204,9 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
+ refractor@5.0.0:
+ resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
+
regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
@@ -6348,10 +6389,16 @@ snapshots:
dependencies:
undici-types: 6.21.0
+ '@types/prismjs@1.26.5': {}
+
'@types/react-dom@19.2.3(@types/react@19.2.7)':
dependencies:
'@types/react': 19.2.7
+ '@types/react-syntax-highlighter@15.5.13':
+ dependencies:
+ '@types/react': 19.2.7
+
'@types/react@19.2.7':
dependencies:
csstype: 3.2.3
@@ -7704,6 +7751,10 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fault@1.0.4:
+ dependencies:
+ format: 0.2.2
+
fd-package-json@2.0.0:
dependencies:
walk-up-path: 4.0.0
@@ -7752,6 +7803,8 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ format@0.2.2: {}
+
formatly@0.3.0:
dependencies:
fd-package-json: 2.0.0
@@ -7997,6 +8050,10 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ highlight.js@10.7.3: {}
+
+ highlightjs-vue@1.0.0: {}
+
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
@@ -8361,6 +8418,11 @@ snapshots:
loupe@3.2.1: {}
+ lowlight@1.20.0:
+ dependencies:
+ fault: 1.0.4
+ highlight.js: 10.7.3
+
lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
@@ -9128,6 +9190,8 @@ snapshots:
transitivePeerDependencies:
- magicast
+ prismjs@1.30.0: {}
+
process@0.11.10: {}
prompts@2.4.2:
@@ -9204,6 +9268,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-syntax-highlighter@16.1.0(react@19.2.0):
+ dependencies:
+ '@babel/runtime': 7.28.4
+ highlight.js: 10.7.3
+ highlightjs-vue: 1.0.0
+ lowlight: 1.20.0
+ prismjs: 1.30.0
+ react: 19.2.0
+ refractor: 5.0.0
+
react@19.2.0: {}
read-cache@1.0.0:
@@ -9251,6 +9325,13 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
+ refractor@5.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/prismjs': 1.26.5
+ hastscript: 9.0.1
+ parse-entities: 4.0.2
+
regexp-tree@0.1.27: {}
regexp.prototype.flags@1.5.4: