feat: page interactive, démos Play/Step et simulateur Carnot
Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
81
memento-note/components/interactive-demo/demo-rich-label.tsx
Normal file
81
memento-note/components/interactive-demo/demo-rich-label.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import katex from 'katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** Render node/callout label: plain lines + inline $KaTeX$. */
|
||||
export function DemoRichLabel({
|
||||
text,
|
||||
className,
|
||||
lit,
|
||||
}: {
|
||||
text: string
|
||||
className?: string
|
||||
lit?: boolean
|
||||
}) {
|
||||
const lines = useMemo(() => {
|
||||
return text.split(/\\n|\n/).map((line) => {
|
||||
const parts: Array<{ type: 'text' | 'math'; value: string }> = []
|
||||
const re = /\$([^$]+)\$/g
|
||||
let last = 0
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(line)) !== null) {
|
||||
if (m.index > last) {
|
||||
parts.push({ type: 'text', value: line.slice(last, m.index) })
|
||||
}
|
||||
parts.push({ type: 'math', value: m[1] || '' })
|
||||
last = m.index + m[0].length
|
||||
}
|
||||
if (last < line.length) parts.push({ type: 'text', value: line.slice(last) })
|
||||
if (parts.length === 0) parts.push({ type: 'text', value: line })
|
||||
return parts
|
||||
})
|
||||
}, [text])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-0.5 text-center leading-snug',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{lines.map((parts, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'flex flex-wrap items-center justify-center gap-x-1',
|
||||
i === 0
|
||||
? lit
|
||||
? 'text-[13px] font-semibold tracking-tight'
|
||||
: 'text-[12.5px] font-semibold tracking-tight'
|
||||
: 'text-[12px] font-medium opacity-90'
|
||||
)}
|
||||
>
|
||||
{parts.map((p, j) => {
|
||||
if (p.type === 'math') {
|
||||
let html = p.value
|
||||
try {
|
||||
html = katex.renderToString(p.value, {
|
||||
displayMode: false,
|
||||
throwOnError: false,
|
||||
})
|
||||
} catch {
|
||||
/* keep raw */
|
||||
}
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className="katex-node inline-block [&_.katex]:text-[0.95em]"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <span key={j}>{p.value}</span>
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user