feat: add syntax highlighting and improve README readability

**Syntax Highlighting:**
- Add react-syntax-highlighter with Solarized Light theme
- Proper language detection from markdown code blocks
- Beautiful syntax highlighting for all code examples
- Improved inline code styling with subtle borders

**Enhanced Readability:**
- Larger base typography with prose-lg
- Better contrast for text colors (zinc-700/zinc-300)
- Improved heading spacing and hierarchy
- Enhanced table styling with hover effects and better spacing
- Table headers with uppercase, bold styling
- Table cells with generous padding (px-6 py-4/py-3)
- Row hover effects for better interaction
- Better blockquote styling with blue accents
- Improved list spacing with leading-relaxed
- Enhanced image borders and shadows

**Table Improvements:**
- Professional header styling with background colors
- Better cell padding and spacing
- Hover effects on rows
- Improved borders and shadows
- Responsive overflow handling

All code blocks now have beautiful Solarized Light syntax highlighting, and the overall typography is more readable and professional.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-11-29 23:21:40 +10:00
parent 48c8775acf
commit 91f950d453
3 changed files with 172 additions and 40 deletions

View file

@ -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",

View file

@ -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 (
<div
className={`prose prose-slate dark:prose-invert max-w-none
prose-headings:font-semibold prose-headings:tracking-tight
prose-h1:text-4xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-border
prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-border
prose-h3:text-xl prose-h3:mb-3 prose-h3:mt-6
prose-h4:text-lg prose-h4:mb-2 prose-h4:mt-5
prose-p:text-base prose-p:leading-7 prose-p:mb-4
prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-a:font-medium
prose-code:text-sm prose-code:font-mono prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:bg-zinc-100 dark:prose-code:bg-zinc-800 prose-code:text-pink-600 dark:prose-code:text-pink-400 prose-code:before:content-none prose-code:after:content-none prose-code:font-semibold
prose-pre:bg-zinc-50 dark:prose-pre:bg-zinc-900 prose-pre:border prose-pre:border-zinc-200 dark:prose-pre:border-zinc-800 prose-pre:rounded-lg prose-pre:p-4 prose-pre:overflow-x-auto prose-pre:shadow-sm
prose-pre:my-5
prose-blockquote:border-l-4 prose-blockquote:border-zinc-300 dark:prose-blockquote:border-zinc-700 prose-blockquote:pl-4 prose-blockquote:py-1 prose-blockquote:italic prose-blockquote:text-zinc-600 dark:prose-blockquote:text-zinc-400 prose-blockquote:bg-zinc-50 dark:prose-blockquote:bg-zinc-900/50 prose-blockquote:my-4
prose-ul:list-disc prose-ul:pl-6 prose-ul:my-4 prose-ul:space-y-2
prose-ol:list-decimal prose-ol:pl-6 prose-ol:my-4 prose-ol:space-y-2
prose-li:text-base prose-li:leading-7
prose-table:border-collapse prose-table:w-full prose-table:my-6 prose-table:text-sm
prose-thead:bg-zinc-50 dark:prose-thead:bg-zinc-800/50
prose-th:border prose-th:border-zinc-300 dark:prose-th:border-zinc-700 prose-th:px-4 prose-th:py-3 prose-th:text-left prose-th:font-semibold
prose-td:border prose-td:border-zinc-300 dark:prose-td:border-zinc-700 prose-td:px-4 prose-td:py-3
prose-tr:border-b prose-tr:border-zinc-200 dark:prose-tr:border-zinc-800
prose-img:rounded-lg prose-img:shadow-lg prose-img:my-6
prose-hr:border-zinc-300 dark:prose-hr:border-zinc-700 prose-hr:my-8
prose-strong:font-semibold
className={`prose prose-slate dark:prose-invert max-w-none prose-lg
prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-zinc-900 dark:prose-headings:text-zinc-100
prose-h1:text-4xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-zinc-200 dark:prose-h1:border-zinc-700
prose-h2:text-3xl prose-h2:mb-5 prose-h2:mt-10 prose-h2:pb-2 prose-h2:border-b prose-h2:border-zinc-200 dark:prose-h2:border-zinc-700
prose-h3:text-2xl prose-h3:mb-4 prose-h3:mt-8
prose-h4:text-xl prose-h4:mb-3 prose-h4:mt-6
prose-p:text-base prose-p:leading-relaxed prose-p:mb-4 prose-p:text-zinc-700 dark:prose-p:text-zinc-300
prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-a:font-medium prose-a:transition-colors
prose-code:text-sm prose-code:font-mono prose-code:before:content-none prose-code:after:content-none
prose-pre:p-0 prose-pre:m-0 prose-pre:bg-transparent prose-pre:border-0
prose-blockquote:border-l-4 prose-blockquote:border-blue-500 dark:prose-blockquote:border-blue-400 prose-blockquote:pl-5 prose-blockquote:py-2 prose-blockquote:italic prose-blockquote:text-zinc-700 dark:prose-blockquote:text-zinc-300 prose-blockquote:bg-blue-50 dark:prose-blockquote:bg-blue-950/20 prose-blockquote:my-6 prose-blockquote:rounded-r
prose-ul:list-disc prose-ul:pl-6 prose-ul:my-5 prose-ul:space-y-2
prose-ol:list-decimal prose-ol:pl-6 prose-ol:my-5 prose-ol:space-y-2
prose-li:text-base prose-li:leading-relaxed prose-li:text-zinc-700 dark:prose-li:text-zinc-300
prose-img:rounded-lg prose-img:shadow-lg prose-img:my-8 prose-img:border prose-img:border-zinc-200 dark:prose-img:border-zinc-700
prose-hr:border-zinc-300 dark:prose-hr:border-zinc-700 prose-hr:my-10
prose-strong:font-semibold prose-strong:text-zinc-900 dark:prose-strong:text-zinc-100
${className}`}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{
// Custom component for code blocks with better styling
// Custom component for code blocks with syntax highlighting
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const isInline = !className;
if (isInline) {
return (
<code
className="bg-zinc-100 dark:bg-zinc-800 text-pink-600 dark:text-pink-400 px-1.5 py-0.5 rounded text-sm font-mono font-semibold"
className="bg-pink-50 dark:bg-pink-950/30 text-pink-700 dark:text-pink-400 px-1.5 py-0.5 rounded text-sm font-mono font-semibold border border-pink-200 dark:border-pink-900"
{...props}
>
{children}
</code>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
// Better styled pre blocks
pre: ({ children, ...props }) => {
return (
<pre
className="bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-4 overflow-x-auto my-5 shadow-sm"
<SyntaxHighlighter
language={language || 'text'}
// @ts-expect-error - Type conflict between solarizedlight theme and SyntaxHighlighter props
style={solarizedlight}
customStyle={{
margin: '0',
borderRadius: '0.5rem',
fontSize: '0.875rem',
lineHeight: '1.5',
padding: '1rem',
}}
wrapLongLines={true}
{...props}
>
{children}
</pre>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
);
},
// Pre wrapper for code blocks (SyntaxHighlighter handles its own styling)
pre: ({ children, ...props }) => {
return (
<div className="my-6 overflow-hidden rounded-lg shadow-md border border-zinc-200 dark:border-zinc-700">
<pre {...props}>{children}</pre>
</div>
);
},
// Make links open in new tab with better styling
@ -92,12 +103,12 @@ export function Markdown({ content, className = '' }: MarkdownProps): React.Reac
</a>
);
},
// Better table styling
// Better table styling with improved readability
table: ({ children, ...props }) => {
return (
<div className="overflow-x-auto my-6">
<div className="overflow-x-auto my-8 rounded-lg border border-zinc-300 dark:border-zinc-700 shadow-sm">
<table
className="min-w-full divide-y divide-zinc-300 dark:divide-zinc-700 border border-zinc-300 dark:border-zinc-700 rounded-lg"
className="min-w-full divide-y divide-zinc-300 dark:divide-zinc-700"
{...props}
>
{children}
@ -105,6 +116,44 @@ export function Markdown({ content, className = '' }: MarkdownProps): React.Reac
</div>
);
},
// Table header with better styling
thead: ({ children, ...props }) => {
return (
<thead className="bg-zinc-100 dark:bg-zinc-800" {...props}>
{children}
</thead>
);
},
// Table header cells with better spacing
th: ({ children, ...props }) => {
return (
<th
className="px-6 py-3 text-left text-xs font-semibold text-zinc-700 dark:text-zinc-300 uppercase tracking-wider"
{...props}
>
{children}
</th>
);
},
// Table data cells with better spacing
td: ({ children, ...props }) => {
return (
<td className="px-6 py-4 text-sm text-zinc-900 dark:text-zinc-100" {...props}>
{children}
</td>
);
},
// Table rows with hover effect
tr: ({ children, ...props }) => {
return (
<tr
className="border-b border-zinc-200 dark:border-zinc-800 last:border-b-0 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors"
{...props}
>
{children}
</tr>
);
},
}}
>
{content}

81
pnpm-lock.yaml generated
View file

@ -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: