From 885274749b34bff8dbcd3867a090ca7b846a72c2 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Thu, 2 Apr 2026 17:06:58 +0800 Subject: [PATCH 1/6] chore: add reactjs example --- .github/workflows/biome-check.yaml | 16 +- .github/workflows/build-release.yml | 2 + bun.lockb | Bin 205976 -> 219896 bytes example-react/index.html | 15 + example-react/package.json | 31 + example-react/src/App.tsx | 493 +++++++++++++++ example-react/src/assets/haxiom.svg | 1 + example-react/src/assets/markdown_preview.md | 168 +++++ example-react/src/index.css | 543 +++++++++++++++++ example-react/src/main.tsx | 12 + example-react/src/monaco.ts | 37 ++ example-react/src/previewEnhancements.ts | 610 +++++++++++++++++++ example-react/tsconfig.json | 26 + example-react/vite.config.ts | 30 + package.json | 4 +- verify-actions.ts | 20 +- 16 files changed, 1999 insertions(+), 9 deletions(-) create mode 100644 example-react/index.html create mode 100644 example-react/package.json create mode 100644 example-react/src/App.tsx create mode 100644 example-react/src/assets/haxiom.svg create mode 100644 example-react/src/assets/markdown_preview.md create mode 100644 example-react/src/index.css create mode 100644 example-react/src/main.tsx create mode 100644 example-react/src/monaco.ts create mode 100644 example-react/src/previewEnhancements.ts create mode 100644 example-react/tsconfig.json create mode 100644 example-react/vite.config.ts diff --git a/.github/workflows/biome-check.yaml b/.github/workflows/biome-check.yaml index c85b4e9..ef6d0fd 100644 --- a/.github/workflows/biome-check.yaml +++ b/.github/workflows/biome-check.yaml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Rust toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 with: toolchain: stable - name: Check Rust code @@ -27,14 +27,14 @@ jobs: needs: check steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Rust toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 with: toolchain: stable - name: Cache wasm-pack id: cache-wasm-pack - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.cargo/bin/wasm-pack key: wasm-pack-${{ runner.os }} @@ -44,10 +44,14 @@ jobs: - name: Build Rust project run: cd markdown-renderer && wasm-pack build --target web --release --features sanitize - name: Set up Bun.js - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: bun-version: latest + - name: Post-process wasm package + run: bun process_wasm_pkg.js - name: Install Dependencies run: bun install + - name: Build React example + run: bun run example-react:build - name: Run Biome Check run: bun run check diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index cedd4a9..7912b54 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -43,6 +43,8 @@ jobs: run: bun install - name: Build library run: bun run build + - name: Build React example + run: bun run example-react:build - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/bun.lockb b/bun.lockb index f83269331b519856e904156cc1986a756db7d0c2..4e3f1d54ee0ecb47cefaadd82e1198f1aa71fbcc 100755 GIT binary patch delta 35702 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 + + + + + + + solid-markdown-wasm raw WASM React example + + + +
+ + + diff --git a/example-react/package.json b/example-react/package.json new file mode 100644 index 0000000..d74ce64 --- /dev/null +++ b/example-react/package.json @@ -0,0 +1,31 @@ +{ + "name": "vite-template-react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "license": "MIT", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "markdown-renderer": "file:../markdown-renderer/pkg/", + "mermaid": "^11.12.2", + "monaco-editor": "^0.48.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss": "^4.1.7" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.7", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.4.1", + "typescript": "^5.8.3", + "vite": "^6.0.0", + "vite-plugin-wasm": "^3.5.0" + } +} diff --git a/example-react/src/App.tsx b/example-react/src/App.tsx new file mode 100644 index 0000000..3b531ee --- /dev/null +++ b/example-react/src/App.tsx @@ -0,0 +1,493 @@ +import Editor from "@monaco-editor/react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import init, { render_md, type Themes } from "markdown-renderer"; +import haxiomLogo from "./assets/haxiom.svg"; +import initialMarkdown from "./assets/markdown_preview.md?raw"; +import { + applyPreviewEnhancements, + DEFAULT_MERMAID_CONFIG, + handlePreviewInteraction, +} from "./previewEnhancements"; + +const MEDIA_QUERY = "(prefers-color-scheme: dark)"; + +const CODE_THEMES: Themes[] = [ + "1337", + "OneHalfDark", + "OneHalfLight", + "Tomorrow", + "agola-dark", + "ascetic-white", + "axar", + "ayu-dark", + "ayu-light", + "ayu-mirage", + "base16-atelierdune-light", + "base16-ocean-dark", + "base16-ocean-light", + "bbedit", + "boron", + "charcoal", + "cheerfully-light", + "classic-modified", + "demain", + "dimmed-fluid", + "dracula", + "gray-matter-dark", + "green", + "gruvbox-dark", + "gruvbox-light", + "idle", + "inspired-github", + "ir-white", + "kronuz", + "material-dark", + "material-light", + "monokai", + "nord", + "nyx-bold", + "one-dark", + "railsbase16-green-screen-dark", + "solarized-dark", + "solarized-light", + "subway-madrid", + "subway-moscow", + "two-dark", + "visual-studio-dark", + "zenburn", +]; + +const EDITOR_THEMES = [ + { value: "vs", label: "Light" }, + { value: "vs-dark", label: "Dark" }, + { value: "hc-black", label: "High Contrast" }, +] as const; + +type EditorTheme = (typeof EDITOR_THEMES)[number]["value"]; + +const getPrefersDark = () => window.matchMedia(MEDIA_QUERY).matches; +const getDefaultCodeTheme = (isDark: boolean): Themes => + isDark ? "ayu-dark" : "ayu-light"; +const getDefaultEditorTheme = (isDark: boolean): EditorTheme => + isDark ? "vs-dark" : "vs"; + +function LoadingFallback() { + return ( +
+
+
+ ); +} + +function App() { + const [markdown, setMarkdown] = useState(""); + const [debouncedMarkdown, setDebouncedMarkdown] = useState(""); + const [isDarkMode, setIsDarkMode] = useState(() => getPrefersDark()); + const [codeTheme, setCodeTheme] = useState(() => + getDefaultCodeTheme(getPrefersDark()), + ); + const [editorTheme, setEditorTheme] = useState(() => + getDefaultEditorTheme(getPrefersDark()), + ); + const [immediateRenderMermaid, setImmediateRenderMermaid] = useState(false); + const [isInitializingWasm, setIsInitializingWasm] = useState(true); + const [isWasmReady, setIsWasmReady] = useState(false); + const [initError, setInitError] = useState(null); + const [renderError, setRenderError] = useState(null); + const [renderedHtml, setRenderedHtml] = useState(""); + const [fullScreenSvg, setFullScreenSvg] = useState(null); + const previewRef = useRef(null); + const overlayContentRef = useRef(null); + + useEffect(() => { + setMarkdown(initialMarkdown); + setDebouncedMarkdown(initialMarkdown); + }, []); + + useEffect(() => { + let cancelled = false; + + const initializeWasm = async () => { + setIsInitializingWasm(true); + setInitError(null); + + try { + await init(); + if (cancelled) { + return; + } + setIsWasmReady(true); + } catch (error) { + if (cancelled) { + return; + } + const message = + error instanceof Error + ? error.message + : String(error ?? "Unknown error"); + setIsWasmReady(false); + setInitError(`Failed to initialize WASM: ${message}`); + } finally { + if (!cancelled) { + setIsInitializingWasm(false); + } + } + }; + + void initializeWasm(); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + const mediaQuery = window.matchMedia(MEDIA_QUERY); + + const handleChange = (event: MediaQueryListEvent) => { + setIsDarkMode(event.matches); + setCodeTheme(getDefaultCodeTheme(event.matches)); + setEditorTheme(getDefaultEditorTheme(event.matches)); + }; + + mediaQuery.addEventListener("change", handleChange); + + return () => { + mediaQuery.removeEventListener("change", handleChange); + }; + }, []); + + useEffect(() => { + const debounceAmount = markdown.length > 100_000 ? 50 : 0; + const timeoutId = window.setTimeout(() => { + setDebouncedMarkdown(markdown); + }, debounceAmount); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [markdown]); + + useEffect(() => { + if (!isWasmReady || initError) { + setRenderedHtml(""); + return; + } + + try { + const html = render_md(debouncedMarkdown, codeTheme); + setRenderedHtml(html); + setRenderError(null); + } catch (error) { + const message = + error instanceof Error + ? error.message + : String(error ?? "Unknown error"); + setRenderedHtml(""); + setRenderError(`Failed to render markdown: ${message}`); + } + }, [codeTheme, debouncedMarkdown, initError, isWasmReady]); + + useEffect(() => { + const preview = previewRef.current; + if (!preview) { + return; + } + + preview.innerHTML = renderedHtml; + + if (!renderedHtml) { + return; + } + + let disposed = false; + + const enhancePreview = async () => { + try { + await applyPreviewEnhancements(preview, { + immediateRenderMermaid, + mermaidConfig: DEFAULT_MERMAID_CONFIG, + }); + } catch (error) { + if (!disposed) { + console.error("Failed to enhance preview:", error); + } + } + }; + + void enhancePreview(); + + return () => { + disposed = true; + }; + }, [immediateRenderMermaid, renderedHtml]); + + useEffect(() => { + const preview = previewRef.current; + if (!preview || !renderedHtml) { + return; + } + + const handleClick = (event: MouseEvent) => { + void handlePreviewInteraction(preview, event, { + mermaidConfig: DEFAULT_MERMAID_CONFIG, + onOpenMermaidPreview: setFullScreenSvg, + }); + }; + + preview.addEventListener("click", handleClick); + return () => { + preview.removeEventListener("click", handleClick); + }; + }, [renderedHtml]); + + useEffect(() => { + const overlayContent = overlayContentRef.current; + if (!overlayContent) { + return; + } + + overlayContent.replaceChildren(); + + if (!fullScreenSvg) { + return; + } + + const range = document.createRange(); + overlayContent.append(range.createContextualFragment(fullScreenSvg)); + }, [fullScreenSvg]); + + useEffect(() => { + if (!fullScreenSvg) { + return; + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setFullScreenSvg(null); + } + }; + + window.addEventListener("keydown", handleEscape); + return () => { + window.removeEventListener("keydown", handleEscape); + }; + }, [fullScreenSvg]); + + const editorOptions = useMemo( + () => ({ + automaticLayout: true, + fontFamily: "'Iosevka', ui-monospace, monospace", + fontSize: 22, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: "on" as const, + }), + [], + ); + + const surfaceClass = isDarkMode + ? "border-gray-700 bg-[#0d1117] text-gray-100" + : "border-gray-200 bg-white text-gray-900"; + const toolbarClass = isDarkMode + ? "border-gray-700 bg-black" + : "border-gray-200 bg-gray-100"; + const mutedTextClass = isDarkMode ? "text-gray-400" : "text-gray-500"; + const labelClass = isDarkMode + ? "text-sm font-medium text-gray-300" + : "text-sm font-medium text-gray-700"; + const selectClass = isDarkMode + ? "rounded border border-gray-600 bg-gray-700 px-3 py-1.5 text-sm font-medium text-gray-200 hover:bg-gray-600" + : "rounded border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"; + const bannerClass = isDarkMode + ? "border-cyan-500/30 bg-cyan-500/10 text-cyan-100" + : "border-indigo-200 bg-indigo-50 text-indigo-900"; + const errorClass = isDarkMode + ? "border-red-500/40 bg-red-500/10 text-red-100" + : "border-red-200 bg-red-50 text-red-800"; + const emptyStateClass = isDarkMode + ? "border-dashed border-gray-700 bg-[#11161d] text-gray-300" + : "border-dashed border-gray-200 bg-gray-50 text-gray-600"; + + return ( +
+
+
+
+ Haxiom + + Haxiom + + / + solid-markdown-wasm + + React raw WASM + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + setImmediateRenderMermaid(event.currentTarget.checked) + } + className="h-4 w-4 cursor-pointer accent-cyan-500" + /> +
+ + + Try Haxiom + +
+
+
+ +
+
+ } + options={editorOptions} + theme={editorTheme} + value={markdown} + onChange={(value) => setMarkdown(value ?? "")} + /> +
+ +
+
+
+ This React workspace still calls init() and{" "} + render_md() directly. React adds code block actions + and Mermaid rendering on top of the raw HTML output; wrapper-only + iframe extraction and overlay behavior remain intentionally + omitted. +
+ + {isInitializingWasm ? ( + + ) : initError ? ( +
+ {initError} +
+ ) : renderError ? ( +
+ {renderError} +
+ ) : renderedHtml ? ( +
+ ) : ( +
+ Start typing Markdown to render the raw WASM preview. +
+ )} +
+
+
+ + {fullScreenSvg ? ( + { + event.preventDefault(); + setFullScreenSvg(null); + }} + onMouseDown={(event) => { + if (event.target === event.currentTarget) { + setFullScreenSvg(null); + } + }} + > + +
event.stopPropagation()} + > +
+
+
+ ) : null} +
+ ); +} + +export default App; diff --git a/example-react/src/assets/haxiom.svg b/example-react/src/assets/haxiom.svg new file mode 100644 index 0000000..fd67307 --- /dev/null +++ b/example-react/src/assets/haxiom.svg @@ -0,0 +1 @@ +Brand Logo diff --git a/example-react/src/assets/markdown_preview.md b/example-react/src/assets/markdown_preview.md new file mode 100644 index 0000000..09d1984 --- /dev/null +++ b/example-react/src/assets/markdown_preview.md @@ -0,0 +1,168 @@ +# Markdown syntax guide 🔥🔥🔥🔥 + +> You are using [solid-markdown-wasm (github)](https://github.com/zeon256/solid-markdown-wasm). + +## Blockquotes + +> [!NOTE] +> This is an informational blockquote. $x+3$ + +> [!TIP] +> This is a tip blockquote. + +> [!IMPORTANT] +> This is an important blockquote. + +> [!WARNING] +> This is a warning blockquote. + +> [!CAUTION] +> This is a caution blockquote. + + +> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz. +> +>> Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. + +## Mermaid Diagrams + +### Flow Chart +```mermaid +graph TD + A[Start] --> B{Is it working?} + B -->|Yes| C[Great!] + B -->|No| D[Debug] + D --> B + C --> E[End] +``` + +### Sequence Diagram +```mermaid +sequenceDiagram + participant Client + participant Server + participant Database + Client->>Server: Request data + Server->>Database: Query + Database-->>Server: Results + Server-->>Client: Response +``` + +### Gantt Chart +```mermaid +gantt + title Project Timeline + dateFormat YYYY-MM-DD + section Planning + Requirements :2024-01-01, 7d + Design :2024-01-08, 5d + section Development + Backend :2024-01-13, 14d + Frontend :2024-01-20, 14d +``` + +## Blocks of code + +```javascript +let message = 'Hello world'; +alert(message); +``` + +```cpp +#include +#include + +auto main() -> int { + std::vector v = {8, 4, 5, 9}; + + v.emplace_back(6); + v.emplace_back(9); + + v[2] = -1; + + for (int n : v) + std::cout << n << ' '; + std::cout << '\n'; +} +``` + +## Headers + +# This is a Heading h1 +## This is a Heading h2 +###### This is a Heading h6 + +## Emphasis + +*This text will be italic* +_This will also be italic_ + +**This text will be bold** +__This will also be bold__ + +_You **can** combine them_ + +## Lists + +### Unordered + +* Item 1 +* Item 2 +* Item 2a +* Item 2b + * Item 3a + * Item 3b + +### Ordered + +1. Item 1 +2. Item 2 +3. Item 3 + 1. Item 3a + 2. Item 3b + +### Tasklist +- [x] Wake up +- [ ] Drink water +- [ ] Make lunch + +## Images + +![This is an alt text.](/images/tung.jpeg "This is a sample image.") + +## Links + +You are using [solid-markdown-wasm](https://github.com/zeon256/solid-markdown-wasm). + +## Tables + +| Left columns | Right columns | +| ------------- |:-------------:| +| left foo | right foo | +| left bar | right bar | +| left baz | right baz | + +## Iframes + + + +## Inline code + +This web site is using `solid-markdown-wasm`. + +# 🔬 Typst Math in Markdown! + +## 1. Complex Multi-line Equations + +### Maxwell's Equations in Differential Form +```math +nabla dot bold(E) &= rho / epsilon_0 \ +nabla dot bold(B) &= 0 \ +nabla times bold(E) &= -(diff bold(B)) / (diff t) \ +nabla times bold(B) &= mu_0 bold(J) + mu_0 epsilon_0 (diff bold(E)) / (diff t) +``` + +### Schrödinger Equation with Hamiltonian +```math +i planck.reduce (diff Psi(bold(r), t)) / (diff t) = hat(H) Psi(bold(r), t) = [-(planck.reduce^2) / (2m) nabla^2 + V(bold(r), t)] Psi(bold(r), t) +``` diff --git a/example-react/src/index.css b/example-react/src/index.css new file mode 100644 index 0000000..a4aea67 --- /dev/null +++ b/example-react/src/index.css @@ -0,0 +1,543 @@ +@import "tailwindcss"; + +:root { + color: #111827; + background: #ffffff; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; +} + +code, +pre, +kbd, +samp { + font-family: ui-monospace, Iosevka, SFMono-Regular, SFMono-Regular, Consolas, + "Liberation Mono", Menlo, monospace; +} + +.spinner { + width: 40px; + height: 40px; + margin: 20px auto; + border: 4px solid rgba(148, 163, 184, 0.25); + border-top-color: rgb(14, 165, 233); + border-radius: 9999px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +[data-theme="dark"] { + color-scheme: dark; + --haxiom-accent-color: rgb(111, 255, 233); + --haxiom-fg-color: black; + --fgColor-default: #f0f6fc; + --fgColor-muted: #9198a1; + --fgColor-accent: #58a6ff; + --fgColor-success: #3fb950; + --fgColor-danger: #ff7b72; + --bgColor-default: #0d1117; + --bgColor-muted: #161b22; + --bgColor-subtle: #11161d; + --bgColor-neutral-muted: #656c7633; + --borderColor-default: #30363d; + --borderColor-muted: #3d444db3; + --borderColor-success: #238636; + --borderColor-danger: #f85149; + --blockquote-border: #3d444d; + --table-stripe: rgba(240, 246, 252, 0.02); + --inline-code-bg: rgba(110, 118, 129, 0.4); + --shadow-elevated: 0 16px 40px rgba(0, 0, 0, 0.25); +} + +[data-theme="light"] { + color-scheme: light; + --haxiom-accent-color: #4f46e5; + --haxiom-fg-color: white; + --fgColor-default: #1f2328; + --fgColor-muted: #59636e; + --fgColor-accent: #0969da; + --fgColor-success: #1a7f37; + --fgColor-danger: #cf222e; + --bgColor-default: #ffffff; + --bgColor-muted: #f6f8fa; + --bgColor-subtle: #f8fafc; + --bgColor-neutral-muted: #818b981f; + --borderColor-default: #d1d9e0; + --borderColor-muted: #d1d9e0b3; + --borderColor-success: #1f883d; + --borderColor-danger: #cf222e; + --blockquote-border: #d0d7de; + --table-stripe: rgba(31, 35, 40, 0.02); + --inline-code-bg: rgba(175, 184, 193, 0.2); + --shadow-elevated: 0 16px 40px rgba(15, 23, 42, 0.08); +} + +.markdown-body { + margin: 0; + color: var(--fgColor-default); + background: transparent; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 16px; + line-height: 1.65; + word-wrap: break-word; +} + +.markdown-body > :first-child { + margin-top: 0; +} + +.markdown-body > :last-child { + margin-bottom: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin: 1.6em 0 0.8em; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1, +.markdown-body h2 { + padding-bottom: 0.3em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body h1 { + font-size: 2rem; +} + +.markdown-body h2 { + font-size: 1.5rem; +} + +.markdown-body h3 { + font-size: 1.25rem; +} + +.markdown-body h4 { + font-size: 1rem; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin: 0 0 1rem; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 1.5rem; +} + +.markdown-body ul { + list-style: disc; +} + +.markdown-body ol { + list-style: decimal; +} + +.markdown-body li + li { + margin-top: 0.25rem; +} + +.markdown-body li > p { + margin-top: 0.75rem; +} + +.markdown-body blockquote { + padding: 0 1rem; + color: var(--fgColor-muted); + border-left: 0.25rem solid var(--blockquote-border); +} + +.markdown-body hr { + height: 0.25rem; + margin: 1.5rem 0; + background: var(--borderColor-default); + border: 0; +} + +.markdown-body a { + color: var(--fgColor-accent); + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body code { + padding: 0.15rem 0.35rem; + font-size: 0.875em; + background: var(--inline-code-bg); + border-radius: 0.375rem; +} + +.markdown-body pre { + overflow-x: auto; + padding: 0; + border: 1px solid var(--borderColor-default); + border-radius: 0 0 0.75rem 0.75rem; + background: var(--bgColor-subtle); + box-shadow: var(--shadow-elevated); +} + +.markdown-body pre code { + display: block; + padding: 1rem 1.25rem; + background: transparent; + border-radius: 0; +} + +.markdown-body .code-block-wrapper { + overflow: hidden; + margin-bottom: 1rem; + border: 1px solid var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-subtle); + box-shadow: var(--shadow-elevated); +} + +.markdown-body .code-block-wrapper pre { + margin: 0; + border: 0; + border-radius: 0; + box-shadow: none; +} + +.markdown-body .code-block-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--borderColor-default); + background: var(--bgColor-muted); +} + +.markdown-body .code-block-language { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fgColor-muted); +} + +.markdown-body .code-block-buttons { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.markdown-body .code-block-copy, +.markdown-body .code-block-collapse, +.markdown-body .code-block-maximize, +.markdown-body .code-block-play { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.5rem; + color: var(--fgColor-muted); + background: transparent; + border: 1px solid var(--borderColor-muted); + border-radius: 0.35rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.markdown-body .code-block-copy:hover, +.markdown-body .code-block-collapse:hover, +.markdown-body .code-block-maximize:hover, +.markdown-body .code-block-play:hover { + color: var(--fgColor-default); + background: var(--bgColor-neutral-muted); + border-color: var(--borderColor-default); +} + +.markdown-body .code-block-copy:active, +.markdown-body .code-block-collapse:active, +.markdown-body .code-block-maximize:active, +.markdown-body .code-block-play:active { + transform: scale(0.95); +} + +.markdown-body .code-block-copy:disabled, +.markdown-body .code-block-collapse:disabled, +.markdown-body .code-block-maximize:disabled, +.markdown-body .code-block-play:disabled { + opacity: 0.6; + cursor: progress; +} + +.markdown-body .code-block-copy svg, +.markdown-body .code-block-collapse svg, +.markdown-body .code-block-maximize svg, +.markdown-body .code-block-play svg { + width: 1rem; + height: 1rem; +} + +.markdown-body .code-lang-data { + display: none; +} + +.markdown-body .code-block-wrapper.collapsed pre { + display: none; +} + +.markdown-body .code-block-copy.copied, +.markdown-body .code-block-copy.copied:hover { + color: var(--fgColor-success); + border-color: var(--borderColor-success); +} + +.markdown-body + .code-block-wrapper[data-mermaid-status="unrendered"] + .code-block-maximize, +.markdown-body + .code-block-wrapper[data-mermaid-status="code"] + .code-block-maximize, +.markdown-body .code-block-wrapper:not([data-mermaid-status]) .code-block-play, +.markdown-body + .code-block-wrapper:not([data-mermaid-status]) + .code-block-maximize { + display: none; +} + +.markdown-body table { + display: block; + width: max-content; + max-width: 100%; + overflow-x: auto; + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body table tr { + background: transparent; + border-top: 1px solid var(--borderColor-default); +} + +.markdown-body table tr:nth-child(2n) { + background: var(--table-stripe); +} + +.markdown-body table th, +.markdown-body table td { + padding: 0.5rem 0.85rem; + border: 1px solid var(--borderColor-default); +} + +.markdown-body img, +.markdown-body svg, +.markdown-body video { + max-width: 100%; + box-sizing: border-box; +} + +.markdown-body iframe { + width: 100%; + max-width: 100%; + min-height: 315px; + border: 0; + border-radius: 0.75rem; +} + +.markdown-body .iframe-placeholder { + width: 100%; + min-height: 300px; + border: 1px dashed var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-muted); +} + +.markdown-body details { + padding: 0.75rem 1rem; + border: 1px solid var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-muted); +} + +.markdown-body summary { + cursor: pointer; + font-weight: 600; +} + +.markdown-body input[type="checkbox"] { + margin-right: 0.45rem; +} + +.markdown-body .math-code-block, +.markdown-body .math-display, +.markdown-body .math-inline { + overflow-x: auto; + scrollbar-width: thin; +} + +.markdown-body .math-code-block svg, +.markdown-body .math-display svg, +.markdown-body .math-inline svg { + max-width: none !important; +} + +.markdown-body .math-display { + margin: 1.25rem 0; +} + +.markdown-body .math-inline { + display: inline-block; + max-width: 100%; + vertical-align: middle; +} + +.markdown-body .task-list-item { + list-style: none; +} + +.markdown-body .task-list-item input { + vertical-align: middle; +} + +.markdown-body .contains-task-list { + padding-left: 0; +} + +.markdown-body pre[data-mermaid-status="rendered"] { + display: flex; + justify-content: center; + align-items: center; + min-width: 600px; + padding: 1rem 0; + background: transparent; + border: 0; + overflow-x: auto; +} + +.markdown-body pre[data-mermaid-status="rendered"] svg { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; +} + +.mermaid-loading { + padding: 1rem; + color: var(--fgColor-muted); + text-align: center; +} + +.mermaid-error { + padding: 1rem; + color: var(--fgColor-danger); + background: transparent; + border: 1px solid var(--borderColor-danger); + border-radius: 0.5rem; +} + +.mermaid-clickable pre[data-mermaid-processed="true"] svg { + cursor: zoom-in; +} + +dialog.mermaid-preview-overlay { + position: fixed; + inset: 0; + display: none; + width: 100vw; + height: 100vh; + max-width: none; + max-height: none; + margin: 0; + padding: 40px; + border: none; + background: rgba(0, 0, 0, 0.85); + box-sizing: border-box; + backdrop-filter: blur(8px); +} + +dialog.mermaid-preview-overlay[open] { + display: flex; + align-items: center; + justify-content: center; +} + +dialog.mermaid-preview-overlay::backdrop { + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(8px); +} + +.mermaid-preview-content { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + overflow: auto; +} + +.mermaid-preview-content > div { + display: flex; + align-items: center; + justify-content: center; + min-width: 100%; + min-height: 100%; +} + +.mermaid-preview-content svg { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + background: var(--bgColor-default); +} + +.mermaid-preview-close { + position: absolute; + top: 20px; + right: 20px; + padding: 0.5rem 0.75rem; + color: white; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + cursor: pointer; +} + +.mermaid-preview-close:hover { + background: rgba(255, 255, 255, 0.14); +} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx new file mode 100644 index 0000000..16a1463 --- /dev/null +++ b/example-react/src/main.tsx @@ -0,0 +1,12 @@ +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./index.css"; +import "./monaco"; + +const rootElement = document.getElementById("root"); + +if (!rootElement) { + throw new Error("Missing #root element for React example"); +} + +createRoot(rootElement).render(); diff --git a/example-react/src/monaco.ts b/example-react/src/monaco.ts new file mode 100644 index 0000000..f85d9d3 --- /dev/null +++ b/example-react/src/monaco.ts @@ -0,0 +1,37 @@ +import { loader } from "@monaco-editor/react"; +import * as monaco from "monaco-editor"; +import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; +import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; +import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; +import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; +import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; + +( + self as typeof self & { + MonacoEnvironment: { + getWorker(_: string, label: string): Worker; + }; + } +).MonacoEnvironment = { + getWorker(_, label) { + switch (label) { + case "json": + return new jsonWorker(); + case "css": + case "scss": + case "less": + return new cssWorker(); + case "html": + case "handlebars": + case "razor": + return new htmlWorker(); + case "typescript": + case "javascript": + return new tsWorker(); + default: + return new editorWorker(); + } + }, +}; + +loader.config({ monaco }); diff --git a/example-react/src/previewEnhancements.ts b/example-react/src/previewEnhancements.ts new file mode 100644 index 0000000..2a32a18 --- /dev/null +++ b/example-react/src/previewEnhancements.ts @@ -0,0 +1,610 @@ +import mermaid, { type MermaidConfig } from "mermaid"; + +const CACHE_KEY = "example-react-mermaid-cache-v1"; +const MAX_CACHE_SIZE = 50; +const STYLE_VERSION = "v1"; + +type MermaidTheme = "dark" | "default"; + +type IconName = + | "check" + | "code" + | "collapse" + | "copy" + | "expand" + | "maximize" + | "play"; + +const ICON_PATHS: Record = { + check: ["M20 6 9 17l-5-5"], + code: ["m18 16 4-4-4-4", "m6 8-4 4 4 4", "m14.5 4-5 16"], + collapse: ["m7 20 5-5 5 5", "m7 4 5 5 5-5", "m19 9-7 7-7-7"], + copy: [ + "M16 4H8a2 2 0 0 0-2 2v12", + "M20 8H12a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2Z", + ], + expand: ["m7 15 5 5 5-5", "m7 9 5-5 5 5", "m5 15 7-7 7 7"], + maximize: [ + "M8 3H5a2 2 0 0 0-2 2v3", + "M21 8V5a2 2 0 0 0-2-2h-3", + "M3 16v3a2 2 0 0 0 2 2h3", + "M16 21h3a2 2 0 0 0 2-2v-3", + ], + play: ["M6 4v16l13-8z"], +}; + +const copyResetTimers = new WeakMap(); +const expandedMermaidSources = new Set(); + +export type MermaidConfigFn = (theme: MermaidTheme) => MermaidConfig; + +export interface PreviewEnhancementOptions { + immediateRenderMermaid: boolean; + mermaidConfig?: MermaidConfigFn; +} + +export interface PreviewInteractionOptions { + mermaidConfig?: MermaidConfigFn; + onOpenMermaidPreview: (svgMarkup: string) => void; +} + +export const DEFAULT_MERMAID_CONFIG: MermaidConfigFn = (theme) => { + const isDark = theme === "dark"; + const haxiomAccent = isDark ? "rgb(111, 255, 233)" : "#4f46e5"; + const haxiomFg = isDark ? "#000000" : "#ffffff"; + const textColor = isDark ? "#c9d1d9" : "#24292f"; + const bkgColor = isDark ? "#0d1117" : "#ffffff"; + + return { + startOnLoad: false, + theme: "base", + securityLevel: "loose", + fontFamily: "arial", + themeVariables: { + primaryColor: haxiomAccent, + primaryTextColor: haxiomFg, + primaryBorderColor: haxiomAccent, + lineColor: isDark ? haxiomAccent : "#444444", + secondaryColor: haxiomAccent, + tertiaryColor: isDark ? "#222222" : "#eeeeee", + mainBkg: bkgColor, + nodeBkg: haxiomAccent, + textColor, + nodeBorder: haxiomAccent, + clusterBkg: isDark ? "#161b22" : "#f6f8fa", + clusterBorder: isDark ? "#30363d" : "#d0d7de", + defaultLinkColor: isDark ? "#8b949e" : "#57606a", + titleColor: haxiomAccent, + edgeLabelBackground: isDark ? "#161b22" : "#ffffff", + fontFamily: "arial", + fontSize: "14px", + taskBkgColor: haxiomAccent, + taskTextColor: haxiomFg, + taskBorderColor: haxiomAccent, + activeTaskBkgColor: haxiomAccent, + activeTaskTextColor: haxiomFg, + doneTaskBkgColor: isDark ? "#333333" : "#d1d5db", + doneTaskTextColor: isDark ? "#888888" : "#4b5563", + critBkgColor: "#f87171", + critTextColor: "#ffffff", + todayLineColor: "#f87171", + gridColor: isDark ? "#30363d" : "#d0d7de", + sectionBkgColor: isDark ? "#161b22" : "#f6f8fa", + sectionBkgColor2: isDark ? "#0d1117" : "#ffffff", + }, + gantt: { + useMaxWidth: true, + htmlLabels: false, + }, + }; +}; + +function getPreviewTheme(): MermaidTheme { + const attr = document + .querySelector("[data-theme]") + ?.getAttribute("data-theme"); + if (attr === "dark") { + return "dark"; + } + if (attr === "light") { + return "default"; + } + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "default"; +} + +function simpleHash(value: string): string { + let hash = 0; + for (let index = 0; index < value.length; index += 1) { + hash = (hash << 5) - hash + value.charCodeAt(index); + hash |= 0; + } + return Math.abs(hash).toString(36); +} + +function loadCache(): Map { + try { + const cached = localStorage.getItem(CACHE_KEY); + if (!cached) { + return new Map(); + } + return new Map(JSON.parse(cached) as [string, string][]); + } catch { + return new Map(); + } +} + +function saveCache(cache: Map): void { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify([...cache.entries()])); + } catch { + // Ignore cache persistence failures; rendering can continue without persistence. + } +} + +const mermaidCache = loadCache(); + +function rememberMermaidSvg(key: string, svg: string): void { + mermaidCache.set(key, svg); + while (mermaidCache.size > MAX_CACHE_SIZE) { + const oldestKey = mermaidCache.keys().next().value; + if (!oldestKey) { + break; + } + mermaidCache.delete(oldestKey); + } + saveCache(mermaidCache); +} + +function createIcon(name: IconName): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + svg.setAttribute("aria-hidden", "true"); + + for (const pathValue of ICON_PATHS[name]) { + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathValue); + svg.appendChild(path); + } + + return svg; +} + +function setButtonIcon(button: HTMLButtonElement, name: IconName): void { + button.replaceChildren(createIcon(name)); +} + +function ensureButton( + container: HTMLElement, + className: string, +): HTMLButtonElement { + const existing = container.querySelector(`.${className}`); + if (existing instanceof HTMLButtonElement) { + return existing; + } + + const button = document.createElement("button"); + button.type = "button"; + button.className = className; + container.appendChild(button); + return button; +} + +function getCodeBlockWrapper(target: Element | null): HTMLElement | null { + return target?.closest(".code-block-wrapper") as HTMLElement | null; +} + +function getWrapperLanguage(wrapper: HTMLElement): string { + const storedLanguage = wrapper.dataset.codeLanguage?.trim(); + if (storedLanguage) { + return storedLanguage; + } + + const languageFromData = wrapper + .querySelector(".code-lang-data") + ?.getAttribute("data-lang") + ?.trim(); + if (languageFromData) { + wrapper.dataset.codeLanguage = languageFromData; + return languageFromData; + } + + const codeClass = wrapper.querySelector("code")?.className ?? ""; + const languageClass = codeClass + .split(/\s+/) + .find((entry) => entry.startsWith("language-")); + const resolvedLanguage = languageClass?.replace("language-", "") ?? "code"; + wrapper.dataset.codeLanguage = resolvedLanguage; + return resolvedLanguage; +} + +function getMermaidSource(wrapper: HTMLElement): string { + const pre = wrapper.querySelector("pre"); + const datasetSource = pre?.getAttribute("data-mermaid-source")?.trim(); + if (datasetSource) { + return datasetSource; + } + + return ( + wrapper.querySelector("code.language-mermaid")?.textContent?.trim() ?? "" + ); +} + +function updateWrapperButtons( + wrapper: HTMLElement, + immediateRenderMermaid: boolean, +): void { + const header = wrapper.querySelector(".code-block-header"); + if (!(header instanceof HTMLElement)) { + return; + } + + const languageLabel = header.querySelector(".code-block-language"); + if (languageLabel) { + languageLabel.textContent = getWrapperLanguage(wrapper); + } + + let buttonContainer = header.querySelector( + ".code-block-buttons", + ); + if (!buttonContainer) { + buttonContainer = document.createElement("div"); + buttonContainer.className = "code-block-buttons"; + header.appendChild(buttonContainer); + } + const controls = buttonContainer; + + const collapseButton = ensureButton(controls, "code-block-collapse"); + collapseButton.setAttribute( + "aria-label", + wrapper.classList.contains("collapsed") ? "Expand code" : "Collapse code", + ); + setButtonIcon( + collapseButton, + wrapper.classList.contains("collapsed") ? "expand" : "collapse", + ); + + const copyButton = ensureButton(controls, "code-block-copy"); + copyButton.setAttribute("aria-label", "Copy code"); + setButtonIcon( + copyButton, + copyButton.classList.contains("copied") ? "check" : "copy", + ); + + const isMermaid = getWrapperLanguage(wrapper).toLowerCase() === "mermaid"; + wrapper.classList.toggle("mermaid-clickable", isMermaid); + + const existingPlay = controls.querySelector(".code-block-play"); + const existingMaximize = controls.querySelector(".code-block-maximize"); + + if (!isMermaid) { + existingPlay?.remove(); + existingMaximize?.remove(); + delete wrapper.dataset.mermaidStatus; + return; + } + + if (!wrapper.dataset.mermaidStatus) { + wrapper.dataset.mermaidStatus = immediateRenderMermaid + ? "rendered" + : "unrendered"; + } + + const playButton = ensureButton(controls, "code-block-play"); + const maximizeButton = ensureButton(controls, "code-block-maximize"); + const rendered = wrapper.dataset.mermaidStatus === "rendered"; + + playButton.setAttribute( + "aria-label", + rendered ? "Show code" : "Render diagram", + ); + setButtonIcon(playButton, rendered ? "code" : "play"); + + maximizeButton.setAttribute("aria-label", "Open Mermaid preview"); + setButtonIcon(maximizeButton, "maximize"); +} + +function renderSvgMarkup(markup: string): DocumentFragment { + const range = document.createRange(); + return range.createContextualFragment(markup); +} + +function setMermaidLoading(pre: HTMLElement): void { + const loading = document.createElement("div"); + loading.className = "mermaid-loading"; + loading.textContent = "⏳ Rendering diagram..."; + pre.replaceChildren(loading); +} + +function setMermaidError(pre: HTMLElement, message: string): void { + const error = document.createElement("div"); + error.className = "mermaid-error"; + const strong = document.createElement("strong"); + strong.textContent = "Mermaid Error:"; + error.append(strong, document.createElement("br"), message); + pre.replaceChildren(error); +} + +async function renderMermaidSvg( + source: string, + mermaidConfig: MermaidConfigFn = DEFAULT_MERMAID_CONFIG, +): Promise { + const theme = getPreviewTheme(); + mermaid.initialize(mermaidConfig(theme)); + + const configHash = simpleHash(mermaidConfig.toString()); + const cacheKey = simpleHash( + `${source}_${theme}_${STYLE_VERSION}_${configHash}`, + ); + const cached = mermaidCache.get(cacheKey); + if (cached) { + return cached; + } + + const { svg } = await mermaid.render( + `example-react-mermaid-${cacheKey}`, + source, + ); + rememberMermaidSvg(cacheKey, svg); + return svg; +} + +async function renderMermaidIntoWrapper( + root: HTMLElement, + wrapper: HTMLElement, + mermaidConfig: MermaidConfigFn, +): Promise { + const pre = wrapper.querySelector("pre"); + if (!(pre instanceof HTMLElement)) { + return; + } + + const source = getMermaidSource(wrapper); + if (!source) { + return; + } + + const sourceKey = simpleHash(source); + pre.dataset.mermaidSource = source; + setMermaidLoading(pre); + + try { + const svg = await renderMermaidSvg(source, mermaidConfig); + if (!pre.isConnected || !root.contains(pre)) { + return; + } + pre.replaceChildren(renderSvgMarkup(svg)); + pre.dataset.mermaidSource = source; + pre.dataset.mermaidProcessed = "true"; + wrapper.dataset.mermaidStatus = "rendered"; + expandedMermaidSources.add(sourceKey); + } catch (error) { + if (!pre.isConnected || !root.contains(pre)) { + return; + } + wrapper.dataset.mermaidStatus = "unrendered"; + setMermaidError( + pre, + error instanceof Error ? error.message : "Unknown Mermaid error", + ); + } + + updateWrapperButtons(wrapper, false); +} + +function showMermaidSource(wrapper: HTMLElement): void { + const pre = wrapper.querySelector("pre"); + if (!(pre instanceof HTMLElement)) { + return; + } + + const source = getMermaidSource(wrapper); + if (!source) { + return; + } + + const code = document.createElement("code"); + code.className = "language-mermaid"; + code.textContent = source; + pre.replaceChildren(code); + pre.dataset.mermaidSource = source; + pre.dataset.mermaidProcessed = "false"; + wrapper.dataset.mermaidStatus = "code"; + expandedMermaidSources.delete(simpleHash(source)); + updateWrapperButtons(wrapper, false); +} + +async function copyText(text: string): Promise { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + } catch { + // Fall through to the textarea fallback below. + } + + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", "true"); + textarea.style.position = "fixed"; + textarea.style.top = "0"; + textarea.style.left = "0"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + textarea.remove(); +} + +function flashCopySuccess(button: HTMLButtonElement): void { + const existingTimer = copyResetTimers.get(button); + if (existingTimer) { + window.clearTimeout(existingTimer); + } + + button.classList.add("copied"); + setButtonIcon(button, "check"); + + const timeoutId = window.setTimeout(() => { + button.classList.remove("copied"); + setButtonIcon(button, "copy"); + copyResetTimers.delete(button); + }, 2000); + + copyResetTimers.set(button, timeoutId); +} + +export async function applyPreviewEnhancements( + root: HTMLElement, + options: PreviewEnhancementOptions, +): Promise { + const mermaidConfig = options.mermaidConfig ?? DEFAULT_MERMAID_CONFIG; + const wrappers = root.querySelectorAll(".code-block-wrapper"); + + for (const wrapper of wrappers) { + if (!(wrapper instanceof HTMLElement)) { + continue; + } + updateWrapperButtons(wrapper, options.immediateRenderMermaid); + } + + const mermaidWrappers = root.querySelectorAll(".code-block-wrapper"); + for (const wrapper of mermaidWrappers) { + if (!(wrapper instanceof HTMLElement)) { + continue; + } + if (getWrapperLanguage(wrapper).toLowerCase() !== "mermaid") { + continue; + } + + const source = getMermaidSource(wrapper); + if (!source) { + continue; + } + + const sourceKey = simpleHash(source); + const shouldRender = + options.immediateRenderMermaid || expandedMermaidSources.has(sourceKey); + + if (shouldRender) { + await renderMermaidIntoWrapper(root, wrapper, mermaidConfig); + continue; + } + + if (!wrapper.dataset.mermaidStatus) { + wrapper.dataset.mermaidStatus = "unrendered"; + } + updateWrapperButtons(wrapper, options.immediateRenderMermaid); + } +} + +export async function handlePreviewInteraction( + root: HTMLElement, + event: MouseEvent, + options: PreviewInteractionOptions, +): Promise { + const target = event.target; + if (!(target instanceof Element) || !root.contains(target)) { + return; + } + + const collapseButton = target.closest(".code-block-collapse"); + if (collapseButton instanceof HTMLButtonElement) { + event.stopPropagation(); + const wrapper = getCodeBlockWrapper(collapseButton); + if (!wrapper) { + return; + } + wrapper.classList.toggle("collapsed"); + updateWrapperButtons(wrapper, false); + return; + } + + const copyButton = target.closest(".code-block-copy"); + if (copyButton instanceof HTMLButtonElement) { + event.stopPropagation(); + const wrapper = getCodeBlockWrapper(copyButton); + if (!wrapper) { + return; + } + const pre = wrapper.querySelector("pre"); + const rawCode = + wrapper.querySelector("code")?.textContent ?? + pre?.getAttribute("data-mermaid-source") ?? + ""; + if (!rawCode) { + return; + } + await copyText(rawCode); + flashCopySuccess(copyButton); + return; + } + + const maximizeButton = target.closest(".code-block-maximize"); + if (maximizeButton instanceof HTMLButtonElement) { + event.stopPropagation(); + const wrapper = getCodeBlockWrapper(maximizeButton); + if (!wrapper) { + return; + } + + const svg = wrapper.querySelector("pre svg"); + if (svg instanceof SVGElement) { + options.onOpenMermaidPreview(svg.outerHTML); + return; + } + + const source = getMermaidSource(wrapper); + if (!source) { + return; + } + + const markup = await renderMermaidSvg( + source, + options.mermaidConfig ?? DEFAULT_MERMAID_CONFIG, + ); + options.onOpenMermaidPreview(markup); + return; + } + + const playButton = target.closest(".code-block-play"); + if (playButton instanceof HTMLButtonElement) { + event.stopPropagation(); + const wrapper = getCodeBlockWrapper(playButton); + if (!wrapper) { + return; + } + + if (wrapper.dataset.mermaidStatus === "rendered") { + showMermaidSource(wrapper); + return; + } + + playButton.disabled = true; + try { + await renderMermaidIntoWrapper( + root, + wrapper, + options.mermaidConfig ?? DEFAULT_MERMAID_CONFIG, + ); + } finally { + playButton.disabled = false; + } + return; + } + + const renderedMermaid = target.closest( + 'pre[data-mermaid-processed="true"] svg', + ); + if (renderedMermaid instanceof SVGElement) { + event.stopPropagation(); + options.onOpenMermaidPreview(renderedMermaid.outerHTML); + } +} diff --git a/example-react/tsconfig.json b/example-react/tsconfig.json new file mode 100644 index 0000000..bdac53c --- /dev/null +++ b/example-react/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "markdown-renderer": ["../markdown-renderer/pkg"] + } + }, + "include": ["src"] +} diff --git a/example-react/vite.config.ts b/example-react/vite.config.ts new file mode 100644 index 0000000..67f637f --- /dev/null +++ b/example-react/vite.config.ts @@ -0,0 +1,30 @@ +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import wasm from "vite-plugin-wasm"; + +export default defineConfig({ + plugins: [wasm(), react(), tailwindcss()], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes("node_modules/monaco-editor")) { + return "monaco-editor"; + } + }, + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith(".wasm")) { + return "assets/[name][extname]"; + } + return "assets/[name]-[hash][extname]"; + }, + }, + }, + assetsInlineLimit: 0, + }, + assetsInclude: ["**/*.wasm"], + optimizeDeps: { + exclude: ["markdown-renderer"], + }, +}); diff --git a/package.json b/package.json index ca65765..8dc4c48 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,15 @@ "type": "module", "module": "./dist/solid-markdown-wasm.es.js", "types": "./dist/solid-markdown-wasm.es.d.ts", - "workspaces": ["example"], + "workspaces": ["example", "example-react"], "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", "example:dev": "cd example && bun run dev", "example:build": "cd example && bun run build", + "example-react:dev": "cd example-react && bun run dev", + "example-react:build": "cd example-react && bun run build", "fmt": "./node_modules/@biomejs/biome/bin/biome format --write .", "check": "./node_modules/@biomejs/biome/bin/biome check" }, diff --git a/verify-actions.ts b/verify-actions.ts index 148d63f..66cced9 100644 --- a/verify-actions.ts +++ b/verify-actions.ts @@ -45,11 +45,19 @@ const ACTION_REGEX = const ACTION_WITH_COMMENT_REGEX = /uses:\s*([^/]+)\/([^@\s]+)@([a-f0-9]{40})\s+#\s*(v?\d+(?:\.\d+)*(?:-[\w.]+)?)/gi; +const actionShaCache = new Map(); +const fetchedActionKeys = new Set(); + async function fetchLatestSha( owner: string, repo: string, version: string, ): Promise { + const cacheKey = `${owner}/${repo}@${version}`.toLowerCase(); + if (fetchedActionKeys.has(cacheKey)) { + return actionShaCache.get(cacheKey) ?? null; + } + try { // Remove 'v' prefix if present for API call const tagVersion = version.startsWith("v") ? version : `v${version}`; @@ -74,6 +82,8 @@ async function fetchLatestSha( // Handle both direct commits and annotated tags if (data.object.type === "commit") { + fetchedActionKeys.add(cacheKey); + actionShaCache.set(cacheKey, data.object.sha); return data.object.sha; } @@ -91,15 +101,21 @@ async function fetchLatestSha( } const tagData = await tagResponse.json(); + fetchedActionKeys.add(cacheKey); + actionShaCache.set(cacheKey, tagData.object.sha); return tagData.object.sha; } + fetchedActionKeys.add(cacheKey); + actionShaCache.set(cacheKey, null); return null; } catch (error) { console.error( ` ❌ Error fetching SHA for ${owner}/${repo}@${version}:`, error, ); + fetchedActionKeys.add(cacheKey); + actionShaCache.set(cacheKey, null); return null; } } @@ -267,7 +283,7 @@ async function updateWorkflowFile(filePath: string): Promise { // Try alternative pattern (version tag -> SHA) const newLine2 = oldLine.replace( new RegExp( - `uses:\s*${action.owner}/${action.repo}@${action.version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?!\\s*#)`, + `uses:\\s*${action.owner}/${action.repo}@${action.version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?!\\s*#)`, "i", ), `uses: ${action.owner}/${action.repo}@${expectedSha} # ${action.version}`, @@ -309,7 +325,7 @@ async function main() { } // Find all workflow files - const workflowFiles = await glob(".github/workflows/*.yml"); + const workflowFiles = await glob(".github/workflows/**/*.{yml,yaml}"); if (workflowFiles.length === 0) { console.log("❌ No workflow files found in .github/workflows/"); From c6f4bcaee565befcad750cfad6ec3dbdc7de4920 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Thu, 2 Apr 2026 17:08:38 +0800 Subject: [PATCH 2/6] docs: update readme with examples --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69f85df..0f2f321 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,17 @@ - [Haxiom](https://haxiom.io) - Using this project and want your company/project here? Feel free to open a PR! -## Example +## Examples -You can visit [live-preview.inve.rs](https://live-preview.inve.rs "live-preview") to see the [example](./example) folder deployed. It uses [solid-monaco](https://github.com/alxnddr/solid-monaco "solid-monaco") + [tailwind](https://tailwindcss.com/ "tailwindcss") + [solidjs](https://www.solidjs.com/ "solidjs") and this library to showcase what is possible. +- **SolidJS example (`./example`)**: Visit [live-preview.inve.rs](https://live-preview.inve.rs "live-preview") to see the deployed SolidJS demo. It uses [solid-monaco](https://github.com/alxnddr/solid-monaco "solid-monaco") + [tailwind](https://tailwindcss.com/ "tailwindcss") + [solidjs](https://www.solidjs.com/ "solidjs"). +- **React example (`./example-react`)**: A React + Monaco demo showcasing the same WASM markdown renderer APIs in a React app. + +Run them locally from the repository root: + +```bash +bun run example:dev +bun run example-react:dev +``` ## Installation From 34ca6c349b230f0137e018ac4883a1e2ee3a87d4 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Sat, 4 Apr 2026 15:59:02 +0800 Subject: [PATCH 3/6] refator: make things DRY --- bun.lockb | Bin 219896 -> 220576 bytes example-react/package.json | 1 + example-react/src/App.tsx | 77 +- example-react/src/index.css | 550 +----- example-react/src/monaco.ts | 3 +- example-react/vite.config.ts | 26 +- example/package.json | 1 + example/src/App.tsx | 69 +- example/src/assets/haxiom.svg | 1 - example/src/assets/markdown_preview.md | 168 -- example/src/index.css | 1714 +---------------- example/tsconfig.json | 2 +- example/vite.config.ts | 29 +- package.json | 2 +- packages/example-shared/README.md | 108 ++ packages/example-shared/package.json | 22 + .../example-shared}/src/assets/haxiom.svg | 0 .../src/assets/markdown_preview.md | 0 .../example-shared/src/constants/index.ts | 191 ++ packages/example-shared/src/styles/base.css | 101 + .../example-shared/src/styles/code-blocks.css | 128 ++ .../example-shared/src/styles/markdown.css | 248 +++ .../example-shared/src/styles/mermaid.css | 113 ++ packages/example-shared/tsconfig.json | 20 + .../example-shared/vite/shared-config.d.ts | 17 + packages/example-shared/vite/shared-config.js | 44 + 26 files changed, 1072 insertions(+), 2563 deletions(-) delete mode 100644 example/src/assets/haxiom.svg delete mode 100644 example/src/assets/markdown_preview.md create mode 100644 packages/example-shared/README.md create mode 100644 packages/example-shared/package.json rename {example-react => packages/example-shared}/src/assets/haxiom.svg (100%) rename {example-react => packages/example-shared}/src/assets/markdown_preview.md (100%) create mode 100644 packages/example-shared/src/constants/index.ts create mode 100644 packages/example-shared/src/styles/base.css create mode 100644 packages/example-shared/src/styles/code-blocks.css create mode 100644 packages/example-shared/src/styles/markdown.css create mode 100644 packages/example-shared/src/styles/mermaid.css create mode 100644 packages/example-shared/tsconfig.json create mode 100644 packages/example-shared/vite/shared-config.d.ts create mode 100644 packages/example-shared/vite/shared-config.js diff --git a/bun.lockb b/bun.lockb index 4e3f1d54ee0ecb47cefaadd82e1198f1aa71fbcc..2fae3386120c68c0c2f2b316864f87bfdc0ebc5e 100755 GIT binary patch delta 31332 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�$JJDO1Ih_gD#|GJcf?mumV=&DO`H>r`pfS13(Tb;Zog+DEdRpwp2NrK`KRH4v3E4rL7~qK*_UgHX9N z;YVbe@EQs-UyOjy(>N-&Zi9h!FW&oft;}earK?ErLFj<<#WzD_cX4}=954IV6oHAT zTz8Q%RIV>_6J<9e{6M1YfgIjQM4HACv8B&GH*0d&iN(wE7U{& zT9sg=M%6LSw7u5sy<-7gKM}VE%hgza@#SDS&hxKL*@jArD@P;@kwa=5C)fVs)a0;h zRN;3r`$~u*vX`a>LqU&ex4XDJ*rsw-UzH?JQUgpA*N4h}?3}P0Cif3Bj^-^-^SSX{ z_nZspQQ82KjMHzA+NCG^d)#h;kYH_Gb`#5oLEXl2xzlI9?zwZ@2i7&FN>PPVE~NNWovJmIOv06Z+#)NmYOCcA*tfIygW=I_9jnG2_eJN2;%7^ease&xZdp*!P`+d{61hm^K) ztnj*lpBCJI|3Y`0zxhFd# z@;Aw@rVvb)mqc_jih5fN0jaMZ9SH+4%sHpL?4uZTli^{o$~0PAh*imQu!nJ4=BB*% zHz$XD`>8W?!d}J$aYW`U*;P~|!(K%RbBf%KZ4+HmP>`L1r(m)@D1J>r>0TDCN6VY) z8|NMuv>v>?+2Iato!J{mryY=_=68lZP^oivaLM} z3)V6onuizW!rgqnX?Lb6d#U;v}VQoKPL!^UarvRv(&f6NnKNsaQw;brK*O zOExr7SjHi9?FeWoZ{DNBTYs(RI>Bb1&tQU~x$Kj#k9oY(_+c4B0t3Ry`d$#$LbVFBdiKCQ6YO8!28WM4_zcxvq#<{`0EOoPoaPM3DCRV%HZcg?r-xF~<~NO1rXp2m68y=SMLc)$3? zy^v^wG_d^mM0}ivK{Q?jrK7yBh#u*3myY&$R6}FVIFt0dS7&8zzW(MlL<_>WfM$%N zs1r}!HEsUj8*N5Y*U!Ek#iev)X&gqqb@%G8>bhQ?kGK)Y5*0cv*aYMiFFZj!vn<%w zA+Mhw%ouxm(35Xwu0D0Pd$Z1a1uKW!t$hd~srtID{CeW<7Pb@Jf;x7xqbS+63Z-a!aeVQ)sz zp|rb6TM8M|T44RZ zwfQ`sP?WZ4C}?}I=#VLg)HjZ%ZhX6WUzcqSd{H8_BbBIC7qKi84Q3o>y>93Wj;%vq zr!5ioStQX}6lY`ocq3Ex@HCDg?s6t6>0*Wy-Ppkm<5m!$fHM4>yjS_j~)uxs`Di11-dQ}N=JEM90pp#FWg zICXdbt9^GK+jPt(7c^8vO+_J$1K4MkJ-RkE>+)EegmLO_%GjHC7Ju@AI)1d`?H9|D zI6E$$p9<4|QMfF_90yZzL)4rmkMVqbI<#lA_31E26iky3dp<#z)SHmPHet;{%2&mV z9GHR+#S8F-8z-Y5e)RSCjvg0$>{)0)sA{H-v(pnc-n|=DSoXUuBrHYzo`Xiwk6X{H zGU;k^+NB?`bTZ;*h@k1nYK7P!V|>PICI`o zHDUC`Mdy=ka#utz2!tDl)X(X$ZTwfe9DCXXjAQI;epD9J>bEz3vxRt%5nE?qk~fZf zKhUyE$I}VQa&-tp?U<=E#MK!X1n!%~)WL z4Bv)$Z32Qru#8wMI?RF|ibcg-EYj0v$qm_0Vkuaj#)-+F^`34Q*MCEa76(t^q@Lnb zr8t?h(YBjJ!8U9hD0IGXnvE3qiV?KzIx3>cr>76uh|036kegkwWDB>iBy0a43h}n^ zL;A+r%*d{CI*%;@<6sDXr6QdXrCUJ`iPm`_e=YfWF$)1<#>vr%)3=|z@A|=hTm6kQ zpOuf&y0k1UIcf{}bEW^PC)sV92+fx}%f8dZ^n5w;uj;W6Gft=;e)GG4_(G4xw$%T& zMf93=6bW-sv)*DnNPXkur;PQNYtCNOQy{(2fMDu>4LgX!Idac1m6=oda|5ZDy^?Y!=8FAH)8QOM`@S2M}QbfDCwiUAH<9Ru=%Sefy993=%tgJ({ zR(8i)y4pN+Hsgr@-`|?_a^Aybp=w6zdfX~3B=A?Qpsz*HIvACnC)cTh4MR|{+Wd8@ zssTQ^;!+WYf*SruetSjD`54ddZ`l-bD4Le(iXi!59#olLyNu+=MLL8?^W}~ z@cBqk%S4=-FPn^7RA%iJXXoS5R7LO9;?%5%bz4VTAx5?#&=M0XaNm%R6(u^Vj*G&u9uc+InNbxm&!j0Q)C;g+i;{ zZ9aLe{N|^Oa{l9S!xZt55?U^PU4~U);6m9$wu=-!7oyD{pDM77rKtkBKb{;OAO_%B zG&0#DE-plYjGq^n)FJo9qMPOK%h+*;;fVoWWD2$j`F$fiK|GD0SGX~>efaKS@ncCY zNEM6|y+}?M|5BtcM)5W;!b8kMqGXYrA9iYW#e7cRf zEk4FO1@##`%B?07!eT_xVsx)QB5Mh<7%jeCjG0np*0qO}%5c|$b-oA5=qz^`gQCV# zJY`rQqL+fZMhqdcRZLzARU1FquxY~ukF4|~)zn5+_j(samXPa;n@i;e>`mbZeQVRR zyWuMmU6x_DX#9A?@aiv5yLju14yxYO)y&h?6}(LBUnU288o%ezYhT&aZ?3zCsdZO- zjMl#s1d)JA|N-V(*jVL**tpB>(sQZG8cd}a+d z`R!>Ws4j`5tKty^J&j+1IGr0`p7G)F;ix<9L)5iH$O_rddoxmKgcP2#?L`OaEb>=C zF}=hl5KrS*AI1$hu>E7_xxXMmY>uf5ri#lD2s3^@qGx33xJ{v3;~)^C?Q4u5oM@KU z>`PfX|GQci7=&JkfVN}}B;#i+_P$#; ztn|ltb?vH!>=VliP^DMbVb_2vS^Rf7jC;4?x3;M7i&6-%d*VHi`o`~O9NU;uPgyhW zj7_eE5YDS%0v8D1)hLqoo{yas`!*o?CMZMYca5_B+Z(hVaZeOLyuJxv(U2u$X)kvf zAGflpY)?OH5*ZL6)!Qg;Bi*p3kigrWyzW1rPcwb{Y(KOLlthmijo;k37WZCHX~f4b zs_msNqRt3^j>L^03yJSPU9JhLPs`gW0dyZd+6Ifp_Awl2ZkD6>PbHM zpo$(CMY-16^-(XCbQpW!tdU1nZGHjUtX7`7o z|CHSyLbl^y2~h2)?EWC|zKDhZc7H=ap0@jg9Cm-J)<1ps2Z^Wb{t)t(xJ7x~6D~B| zj33+VP~(kd-w*k59A3ZUwQdj|9W4+!8!$TcVQKuJU<>8gT7L-J&a#s*&WyX)T3q2^x`Wt0FNfs z_uNmh1Al$(e+!v56i0hu);uFzH)4V`egS4iy><^$7xo)X?MKB3!Z?1Kaj@(Wvp3=a zZPz{PHp%tGfsI(`Xzu#PZ()70b4ks2-l>8|f#>#U_0$B|e*9;L?AM|1UiI6nyvMV5 zW=4qd>pw*=@4XWJVajS}HUn|!S*7~c#GPFcn>ImB>WdS%h%m|cC78V5Uv>+8Zb4m{ z-O?lA$71MXue>*2Y}DN{uG?JH5!*4^Jo)Ss_M7FFj&b|6jhk%|o3L5VG5b!_a@4jt zCVCvF&PJ|{$ob@b2`!S33a>3Fnz|uz6}`8}_MXNM-TZk=NXe)>BKI)k=fJ-DV&lfX ztmFRpLFu0Y#g)ceb>K|_6;BvoIUD*m?mvd;mrcN?aTFvr^;8ZwblnzrRm$*N{y}w zR-u-)^(JhY=-~%@MhV(WaH^O|kPQ@CBmoQ?;HJH()83?N)Qoxi{YG_KAeQ2qT*TE;&B>#v+Ahuyoj(j*|ZwJXYSAxR%eLlzFiKA%(Nzrw~k54ZmoYo<)1y) zik}mV(tb!e-8yl6GJXoqf2uV*{V|b}8QbMm8ye{gq}ESKsH9p)yH`9Mdk?`UK)A*Z z5f>8enlv%K_A}StAE*h#uFC0qI!)qDQztH*8FyT1Z bF1DWWff!b3>eKtA_|eLgKH2e<%KHBUpgSU6 delta 30884 zcmeHwcUToi+y2hVQ4WfNim0G~T~VY+vjL*43Kr~LkAR9I3fS8LJ9Zp(?25f6qA?gd zG37PUC^0db#28XFiAEEX*A(@4KfALzB+=x3zw7(1-#^)ld+(iRp6N5s%)SbKE3bnv#^2HDT)b6lH8m=2%;5vXX{?mEoUe zOCOm!ajeoDvDSgRCfEbq(MeHigRg_Vz;A*pf}P<{;T|BoO9CEJ%PL9@a1XF2I1ua( z_656v2SCvZj*)WH{g@(veBwmp23Hb~VV*TMLY0zP3a0GO1XC;-JcIezN1%ihSE7vb zQ@E?boot&rIty{cK~9yB96~WF%1Bouv+-bxH94eN(zvv7nf)E^38`aK6v%CvnYO9l zLy6*V3xE7&2jf=32uFInl4|Yj1{tN%K>B$}w?(oq3Qy%40j7HR1rd<@zT{hAO5+HaMp(3R(X281`;YRnQolDE zj+!PKOl|5f*{7xvuP5A8eNNyi;QNph-vpzL?b#QNhwKxQ_kpQQ>m+}N*eQeO&{L>> zb0MeJnksoTnEVGy{ulH~FG0GmN_QLS4v;(-a#v-blI0#t|vu+~(B0^8xr$U!IVfJ~d@ZO45V+ih`=mEUH^f|?k$kdifgVrp8l@?29RnGIm7-UVQ) z_K8w%lkRvhHD?5v^y-4C+1$aD%}4%5xG%w^cSX8Sf{|T&_Acqc!4!dADvSkF1pTEu z7EBQYf=SN{ObL~f{`XM;^1lJ5jJ^rh>nSA@=0s6^S{UwJunYWWfy*mn(0R)n9n=Z| zMX)>6=ztBv3?otrOclEVa^i;JM#5PWN7^#8vXt@TvL?V#DGwp1X1oig1k!Ek@=za5fepw&;+t5PxvOjBW_Eie`LAlMJQ98CV>rGFnVWxo{|#kFVG z!2^|}EST!+8ze*uAAzZ7yaT3;rJ%woqr)?8Nh!$E&3=Zwet$!6GMM~Rvzm=e8#i3J z2RDT~kMPtpl17ailbVc)C&iYWoSHd)3gWAv^ir~$4>BsYDwrDJ-vf=3y$7a}oB&hC zwt*?5^Q8Z1uov9j!Ia@hFg4`)MEK9ck-j;)yPv z_S++j1TTUqp(9|5aE+AD1XIF8!Bmy4z|_Ds!BoXRrx+Q%3MTn6FzKxZ!{%a9Vk|a# zfvJf?C3}LQXV3m6*+}?9Fe#js?ww#N;aV{D^*arXjAW)Hr%;dT4D(Kda1EG>(*{g> zY2%V@S)&x?LAqgZz5^;0=hA`bIp$k?k6tm6DZ&j2uHc6{|vk zPBVEFw0A*J8SY&EQ+Xfzjmd`V5}3N;Au!d?axhiAY22pT($Z41CMYeY8QJxpZkS3W zVoS3nr__dwtZ!Va#$ioMm*;ufa8;P#H{$7BEFT z9`-yXR8i0>sS{FDvR27rT+BAQWFeSlh) zjlp9ZJqfCc(sZ7oP#sJq8DYzs&}?F6sugZ(hm@S<>*PsT_$@T*ZvdE* zO#oB9jp>C%7k=`h{5^f1IVxFEipBjC!g9 zz23`>jK;%F4QE?nRL2>xI~o6Ocp#5Kc}B+@m7*6&X{#$>k+IV7cY(V+^vs594mV}E z4wxF&Lvl+bL><{&#y5f64er`t8i5{Q>fjj@ZRuG~Q`3{e`A-#n>}`=MRq)+4hK0ET zrtxzeOcrGunC6s4lC!{6AA`Zwz?x)##6#tNbDfdhGhj+~^m-$ky}{HUAAzYj8{m)Z z+Ot=0Fv>Y_lcF?$M+CSo*hBL78;ybeHkd|Kf#f`}FWecDdw^-=wE$DYW#$_Rb_G*C zM1iT^Lco-gXi5Y=RWpOBGS@R$pxioCGhh~OlcBA8$sZyTPLqL>wGuAz)sLdl-K z)97md)ns8u#D6zijK+YazOfipMj1hK=vBRKpV6Y2u99HIlyBjtl6@iNdOhpDU~o9OeHyUU3x95VzIU)PE~{U)?zD$L`nF zSQd}0r?I^}2lrY0JnpBtdwq>1^8WQT>yKp=r3YfMa`(U}HiF--uUQ^ADaufOr+zH! z$)`5ZnBYYXH1$&pcWD^MX7j{`nx(j`qQrC8hOx|>mo(JWf#r11wLGJd#@-dET;F9E;{T%``TF zpU3?OcMs6mRNf!=S9yMbX0c*GNASS`u`GrMHPCwNhFP5reZcL|JRVLUNV zvrfUFh(?NKM8*yl&bQWeX2*C5Jnq3Gj6C$}Yrr#tG?vU?!hHultDm^>&cSi43C|1G z*kpbk_ag2WqFEnd^t47mCtea9#Ugn@h{g|vxbaZ|5$qt3X`xv@hv73aY8&1ty9(|Xp!brapzaOeuCPLPlU#u5vy@)5uw#tiwqO7jyMO6Le^y<~P zd$`8zxR;8wkmo~qN59wQK@l43&!^(Ph8IO>YH>C05*f#=JTX#ZQG6ZlQ+P?FroQgU zo3xB$D$i)CSv3qj7)&HeJ!&3jtu*U($S7&X-5W-sXXLe_{&yYsbne$$vlhXc#>3x9 z_piVUT5FcR-ii{%+tiCi>M?CJ^`kD{dAqcye?cTYYnIM8whHZh9dKjZw@u@NBw7!bcMh~BehH~N7b{gx<6WeLlT$qJcx=&2ADE103X{TAr)S;MawT)%% zct$MMZ$_*qzZV?HPIA^>V?XI`%6AyggFA!n&KF?IDJF5Bvk1L)%kk6>8hUU+2hEaR z50%Fc){SL*cubsT{Rr-0XrqErnQpu|PGg;T4UGniP1CH;*GFBzpK%NiYXumQ2;D^; ztv!$INW(Fwqh|RMJ|4oa0AmmxTK%;FuiPn)wdMUgX)KiI<9?jq#$Dk-oi%GrV?_zn zQ_biQ#b)xN&KkymHC}VS2cH=Dz-s813upDhonx9PN=HaV@myJ^RlFo#V`q8e1dTcI zj0DZnxfyhLvxHc*1?!?&uKwn(&GX-73vmwu`7tVtC)SepSyr`#Uy@ZiSJ)hBVW3D{0 zm&Q8sb-179CAi1%#=S{5qqnBs2;;|l$FbI&_0g=e!woB9DQ#Zz`e@9;uj8(9zrGsF zE&iGwxk`%>nt zo5an$1hP*tbjeJX)s0DGo-ssYDg31&n)UT)Md?E^{FY^FE%ZVvne%TVHjw8H)vUYV zLB%t2@*P|hC*v7gqO1Y%pqx0@G#;$JD11{$RUT6>iY4&-!!@>vML~opDrnKilDVpU2xD4Ocya;aInWk-u;X6DT zamB-`>T$7MykvyN9`Qun`|ymBn&n7>=~t1nQJN*Ai=qtH!@q?mBc^6uO`V0@FIBUi zgd4$(R%A7J0c1(tj2dy~IT2B8H;)+&W8IA}8Xdy8)+>-r=e9ALwGcAu z4tg~@{{k0kIh$W@9>_1(cP4{mX_KfZL3-&I^PDt|CGqpPzr@|gq9%GN${>C*Ef&*a zKHL}K9;UmS@t}0gvb?wSw|tGKA>?D}gznNy*V)7q$7z=D;qIZkJMa>?H}#eN*86y( z%weXCh_Z(EGkSv)Pi!0oi_J2$l^ISJ40f7!8%9}gO9ewcq`wi^qIcPB?w6@?-%KY9 zEfe(l&H6JmsC!`@gy=gDFs%le^MyPnOS7JZjE0jFbvesJxMFy%39+yQH700mE4NM1 zEKdeeXCUctejn~P2UB+-cT=7-QL`))~PMk^(lv@&#OhyiLbgYc-pZCsiLP{dnRO&AJ3K7+NYMG4@pfi#7&X`1x`+|=HxZi6rtPSY@@T*v(&_nWR+ze+NCjIlbZ z#|x%Y^Z$fW)R z-U$yj@~~*56(vz{*Wn(jyCZl|wq_A&6y?$x?Va$Tr)}vn z)>Jvbi=bjjSCqbbkU_j8N3#mJF-Bn)iZSG^JY%+I?KAFAE6J^#&C#qM!h@>B$zi-O z3C__hy~gX^6ziZ}+;6UC{RD0@FVgV&ZnE-%xf<)of5LqgkC~@g-_0Fkh=EdC?Vl6fi zeOmY2!SfeteCt9dEO01rV;+>NSqDxs`~3arD7J+co9MlpQX;$kLBS&TR ziAdumi!|q{kYQ5LR|wU)+o`g=>*6}>MV_%(v#3+iG4%wZIa{Jxw!)pvFD{71gq62M z<13apVFg9A6IKRGVPd9Z>>@6vTi4CJU@0;%o&UTv4n`_wna0NOT-*=y;$@n}Z6>V^ zD6&4>ww#tjdvTBA_m^vyb+f3psbm(OvqHlJavt}C+&xdTerq=hQH~chjbe3qejfSX zhRfPCVJ&$w$c#ctPio7au zTf=Zxk2hXLdCFL&sqfF_$5+K6YizaVymX$C$Zsnu%MC~(Xtinam`_Eb#qvPzw?<{S8RTOgYH}jG5>$q-0!Fy)D9cjL@tp3(TUq^C-5C7YmJF7Sd=LJ(4<_ZLQ`! zXCb-}g8!Bc>upGA+@tcb5U$T7*J&7sIqNj*{#>I|>nkGb9k>E?z2fG-)sod~kzs>M zb^QK3f4yei43B6%pJZGdgaSX9AIo;~oNb!r2HbJF z0jF)iu35f;thK&_ZN!6iXly*6iu+++v_s=(cR2A1JKLji z5_f96`%Wjm5w6v|WG5z$)zpi|c8Fyac*ZV`mFF+vp3K>9&GM2!J@Zq$VzKkd+fCa+ zy6@qBdo;@rYv~;T`C*^3XSwC-I@#a&R+E?BsUk`3Gnm<8C>>g{F6s*5^S7 zHA~J`MH!%n{{T<@=*hYu->|-VuXMi*7nxVHdG$>SwtMTI(^z+2@|)R&F zWw`WsERD8fp~>$YXwTUpO)c5ZJ0GH5Xx<^sI(~<-ZpTQ1L4bKK(9~Ny_~?Q-){++# zXsF<_*+m2|K_u$4n26r&O5BKUl`V3PL zt3KGne=dl#B<|Ixn1WdA9z4<1iz#$pl=T{19pNg&gL;Sa?nf&y*WY$_*8Tg8rFvx= z@YY}8!d50*cd-Co^db!qYoTU+X}{da@%-natj!M?Q96-}Rp%vz8e7C0AJZ%^A0%U3 z>tw8@!E@9tn;nZ~X`H>JsTZE(onMMW=JQ_CEK8oJ=7pzw37!&=WI0*;-n414G&@9- z3I)#K1;;h@$RYmoae9job3$WXcrNa_y!eEso-5!rPsU+OV>_u?Vh$@xIvHr^=kY{F zmkbfjAC1%}hk46WH09)+(%4*n{uGAY5nlOp95zV(Psj4{^_}b$kcTorC4d2L09~cI zD%|w|@~;nAfF=N4#3c6zRG=9^S7|1BbD%5`3eZK2bnRrs^oy7jTN=*)5j!C)jFuix zw2bGk8BIV6jNiEI6GS1}0hEpQKy6^SbSHzUoGAcZ#EjoLD+)h&*3(X*<^fca1pv8W?)8h9 zv|-?N|K-xX0!$Y%`NJmb{xGBZMNINlhEr#Xe~pyGH0l?)re1YNrJyu3e(-G{y~+xu z?_V)R|B}@EPZ-Tuao45v0frS4R>;2<@67auG z=o$L6`0a( z3ns59x=AK^JLxXXB#LE9XFWiBsX$Bt@K#foca(BslE+K;)0pw4uX@_Op*2cs{eR$K zB>&%KfbqevHP=VN7@6&-F?Gjr@OKAiOTGU^HnRO!5mK|ymI)K1wy$~O-Sehvo;>;W z>h|*RUn%|nFW3d~Z$$i*kIgb2VsdX~^tB1&;hD_1>%TntjO*3)9=}6IK}?T3ajOPC zA?5#vY*@YjjRa`D_A)X|b?^$9^8TtUz_VFeT~eTcugie{k^xIIiO%Cjz3(EJ^e;)h z(oFJV$SFhDq(3nY*$==Z`cS%WnpXEF1Z3nsBVWn%__=fwQ^a3_D}jHM^1ouzr>|*J zhJOW9{0u@0s*)!zJ3WvBeWR6fXq6uS4O5WvxKW8KgDHV3V3K>#?HLR`yHZUm5K|7l zz!ZVE7^FcqvLn69TY+MhfqKqncXi*$FD z0f{N19#Y;@axdvmOd04a-KCl0?I-p7OaI?7^*<6&1cRgkF+C2!jS?O%<-`3h90tJHvmi^nW(X@&9ZEqXzP2M8uTv4(TSQ z4DOU}VyeNt;0oZQQvOUftMHi$r6Qok#R<461E;~1@EOTx!T3{NmhN-pz*U-Qc)lU^ zE=ab&B^8M&!b{RkOeOe0y8n;Zt|veVe~5%Cg1-V&3GaYOepm9hlJ9}3is|1Ys6@Yj z@uxh(jVxmY1g6gCPLW6^eGlm-CVfwEIog%gmLA0P=!Y9c&;U$|jU+b#Q-=JdyBQdN zN}zNHOKw3zT&0=9g-ZW$>94cwe|l9@b(0^Jf&#b!bd~1+?cKhfzTEEr|Jd!*UQ1D; z^}E6U?DmaaK2^yIfO^6|yM3w+efLj8x%4idO z_@CWAItulbe|Gz*g#Tz0Z)}_Yo7W8g?Dqeow*>#}_Vq6KyVnu4^807EUwZdX6W~9) z{ipBp&D}p%yZ`L=X_rqIF-`UV?Dk9V{^@ZcKiP)yZs~eOWr#ydaYs!qT?!7 zLHx>D0&@{Rtzvz}!&Uei#NgE|9`e1bA=tAT0@pPVR22Q!K+tau1gA*gCaeMiX92-j z0YPQ)5($oxpw?Ols*2RL5R6(2!9@~O6E)UBP<_li(T&+H8ctSLAMlVBtmx?vS94h};B0#3l$fZGxbl zxJ`mvB)RwF~o3v+_*1+3J+vdOO5lE!7y=ljvZwwG;TO$6J=y0HwA zNR(TczmJtuEpLS2U;7Gkjxrx6E*xaeHRj;^4S4sY$6)>k{A-Z8Q1bqI^4`xeXVo4l zY8T-9|47@#72hI5Zn8aF7edxqC85>)&*`OwwF+2YC#(67$YF&uk1~Je(Q23@tLZ5z z$@uRxWsf-ZB5P&oYLn^dMf>eviBAf@LbjNV6$cAZf?kD>3R%xGtgJ{p!TMT!@l{4E z<*}!p$9pd_XOCYe8~=JmwL$+hNiTQW2)~m|c$ojInv|9rK&N?-ihtq7ldO=rT%K(N zyD__P*=bgdS@d{*E5FtJe@<&*$r<)#8A};_v%p%&j-+k z5?J*AU@I&>iH64)&zy{W$5Vilg=g!lY0iasJnQtw`_JlM`Tkk|?$!KET zEM;Fv8NF1z1<*yGM0GVpCFqu1GZZ=qjbNg2&m*QM-hDWlB- zeJ)DZH&R9)n|SCFyjGPmdfDhHWp^P{=~XCwp5i5i_oO1dliejVb00GE4JEyQrRN8b zQKIygaxI?edMv|IM$FGYpGX?VpRUuJIjY_YQdSGk?E$)6rHsC*v<#psuP9|cczzYnRJ)bHNXxGH!tt6E){u&R zknP7aT{XefVd?HBEEK$h$c}rJ*ZG>ILGt9!ggi94uu)c&;yHG~y`@ zdRO5J88s>OF_PIsa61oBVxdyl0?#C;D@@ABqEbS1(Wj3TIE+9KxFR6KpAt^bQbwOn z(jz7nB~r>-Nm(RhEt%32gfGR)z%B6{1x1RutyFA9{*Y0CC@I4%q_mN;XepaQIlwg| zMhuBmyV~fZ6*5{M0c5B?0X_x30KNon0k;7%SX+SAz)FAv0-%rA)&Q%3Jb*efnF{K> zp8y{U*OscM#&sFzXi^8tJT>Ya6fD^Pz2co(<|ya!wZ$fA)k zA>%>qSXpdpsm3-T8?+k;L$f^u{{;LS_yM5Lgy{1k`t0a?MYyz5hui6UMP7h6a1hu7 z>;_&0_5sI$=YZ#d{lEdB0C)j73>*QD0xto_fxW;Xpb*#z+(p(_gVz88AltVNSPyJq z_-8UQd>mK>ECCh*dB8GYDX<*K1y%ryfknUqfQ%!3>m?27C4yS34ei75Xaj}=NkB3% z1Q-gW03(2rKmyPO=n8ZP5`i8-Pk_EwjA$OXP+qD~vNiT!){(ZoR( ztvW!~j%+i1{oxJZBtUlgG;jte0$xTYUjb;$ZUNQ-TY-FF8?X;J02~C?0|M9pYzKA# zdw{jT^JHod<6$GPA9xPf3+x1501g4W0DQY!IRfkk3V_YPDj)%f1G)fRfo?!wpg%AG z(17+pN1!{92=oRz0E2*`G}U&(LqA|3&0J00Z9RRlh z&ZFl31sntp0DFObzzaYDa2R+Fcplgf903m5aifnb>Er9Wz*bZ*eSc^pFbkLfWC3I0PldllT=fbUR(QOH0l zK-P;U4KkHv8k2z`z))ZqkOuSudINoc9sqqaAsd(l(6 zEz~G|+jkzqX9JCZ#y|@o1gHj71v;x}{{#ex2RZ=*fPuhpU@VXh(6>l>0mk0|8&U@P?fAfu@`3-|%gCxCO1=K)KBWk57w zKL>?Zf!6?9$ObT1B!ubz!l(a;4R=qpb&ToI1U^Gjsm{O5G^uj zQBet?g+yhb3eXhDK?W#efj}(u-$cR;>>;1elLcuGA$_x;AwZrN;HHHIEfAs+FbZf3 zv(dS7&UmKY zJ|E8(;3o8F1h@b<@Ju730zfii8q35Vm(rs!?vT;6PSX}m=Y9ZHKhn1ALx2=YkN_D5 zG7w}OYC~2F@CAGTPoO41_MZ&Sk3dyCR|Y5y85lA)Rp7RRNxvFERY~zvd?YJDI%eEH zG`!46X>y?wyaWXbL=&s2=nc0QKsNqo$O+^o|KWHheN&e*NEs$Kjc-bvYS&!o9P0`P zuCCj_qNtnN(5pI}RbUAog2oW_5nCYC zQlR4won<$M&8yV+({s*jNN_+NHzC>8_4bZz*DYq8jX7f*gB7;lYmB#P=>N-iDyz0YL$<84sB_ov2p# zqLGggoIN{m%h`w9Uc0Mnv8;?+i~7u4w8ze?GlSUoR6 z!O)KrSx{h!Vs{U<3mYJc`=~WU#hz+cmLvxEgio5t19_9la6y9YN=`-v`yU*lvRNo) zAYyo|ijRA$)kDp5P`fve_bTaeb2v2M9RP!8o}hY5seHWr-G*PnClWzXoaUlgFSSOP zd9>=i@al{FY9Lt_5)c#~fbnG>vid=+*Q1JUo9C!3A^dS#FJP6!%dw7{q3#pXV0FdI;a z_PxTqZz3mDOV#FuIp^0aGa40+Dy8dI1#z|?TGKq7)%x(#t$}B2_@f>T1;sqRRjW7q z*ShUH&vt~h2-p57fq5S6l?ERi59s(=SBHX^h=Bs$DGnqG7KwvFZN+l9Ld_Fto1HmY zYovd6h$EDFHf^<9eXCu3@cC*-Rz`{&{n16^h5GL?}=_U>i(4{3HFZ0CO zCEHv!W~|&DS>hc5A|clDl1J-<9F(RDyaHfLE8$yA*w$*SeGGcZ!hym-;%WY*LHTE`UZLl4P=a| z)8f<+v>(krRGf3KhaDcaCi+b`HYCL8VXeh|D6rF_{LsJAi-H21D_9cr%p+*4Y{=~Q z(cpJ(sf-pqbZr((DI)XC+JcJvB3jq}Wx7M*xF~{x`mY+|t>J2QZ}W8AWf?xU*{*9R zIut*vA>4+c+nVR^286E&jo;fV*`dK|!ZxB*FD5FzTQ)cy=J0V7gNGp(<{7{hhIzbi zefRcv4ut@b2L;wf>?b|*sNS82MqZh(e$mdMH&EP!La2Gh?~!*RAJ+NdZiGX@JO#M# z^6?8Mhq?A}_~eRaHpFcnBYZR^Gy3(LtF|~4_6Zvl*aU*^!e zSyLRc!48@y7r$5a_KB1Kj*WI`I0^f3#N;U+QH1`Y8`_9Piy#vA7V)6Js0tbQoY;g= z-p{UW*O%Jlh5?-9uWX*c1)asg8EuR@qUULT-7`|b3=CP z>k~u1{oIXRX()V0z@U^BJx8dmSc=G-hRJcy2(<-E7dJ+zBiR(uVI=Z2S4he9uOIxuRed&?;fy?C(&3Zb^9MpC@hH+4rZU5{fW z%(^tsnJ)S~Z%-9Z?cWZc1>z3HZ5~YR?B3S@7+ZD0q41pW8-qMpMXNDr_z_|pT%qRi z&uiSWV?qW$*Fj(Uga!l$D#k(3IMeXS(apb9_egiNbgno>5v>sUYeBokeR5S2<fLa$B3rGlJ`5KhrePUm5kI7%L*|OXW09}3B5y3Z z_W4M0U@YvWdD8Pdw?S!XbJ<2c7g+BEDK{d;$Iu8hPmbPK;p*cjvG2BUFLPqe6c!R?NIj(yx@eT%{I>$z4yw@@f+W}aD^f? z)+y`7Bj|gXr%Lxte5cFc@6MMvQk*V=#v#ImA|4cKo+AAyjU5_ar~UiT3&mO(>!$E1 z{l#PU!=I;*db!`KohtK%gx1ZP@X0_DBjBtEXLA2@FK@22)uOUQNIW2!ATCned2rGK zKgi|&*G=N8I;m_XB-B0S8P(Zco4>rR#vA9VBGnegBw1B>jYr((Vb`_ll+-{Jv_9J&K_iTyhr}iX4O>>}xE3Y88IRHP)aug1%1ZG;>J-HQw(*0we{UHt zGmd;23T&qsi^^lZVnK!)qJG?7oXkMH=9$yqwLe(p@y0SABAB`C6yK7*i}+;%tYe)_ zwI-_~+GIlHC;DchGlq!8aCzy6UL)bKTgPhsn&BJd#DW4Mun)k?8F2;*q2?*swX5&F za%NT0D2IZ1j`sNm^?zPF%h%cAQz%@s5Y#+)J7H^K+AJ12*P&n@&D~~(cy;_c_t!go z6fqD%z09+`k3a5M)xPW-m`gKTL1H<@Z65x8<+<@gfAzF&bSNa@t)e$dbpB+ulg*}{ zvpIb1;s(WSo+rLDnolH?kSID& zRD;!b62+{E82IK9-qmkC4v1eq@inw74R5rHdC0eam)oDrI`qzYCl(xlk%whlS@9;K z@~YEA|KI6s-&gLHxfawR(V@^vJb^-}d5-w(ir??K@yXlA9SY`2<2PG&4eND3cAUd! zx#%(pL%=+mym_x1k;Nahi*YEJhnM$1{8jGIN+;$yeD3xT8z-SMU3(gnu9w%zIr)ty z9CK*+i%W={briQJp^N{j3-=8gb!!2}_UIY4OyS_+-0-G$xfYf{a#r(-? zaH#nmfl)ur$aac4szIMdCw3I(>Ehx2Q%Bs-4+x+}3em@2cX5Ye8Z9!`snwiNhx3Hr z6bQD7RzxSn8OSY=zc)~fpMqvF4>)f&s-dO-QpLv>7*5<`0qQi2GAuu(^nuibOYy{!{fCqljEl;)AK`QMtwe?Z;Aw ztrf3M!*azuWBf$bYLy2sUpLzcd+HY0GboCA3i;6|-+bp%^NsorpAVBoz;rZ*d2adD z;1f@le%F^dG(3fEI?@Og(?MS5!RHb8rgy98wxHOdmnEKuLYR3{`okgXX0_P(#uJBv zd6xPYG4-8)h;9Fh!{=&>cubivPhSsjcrYUPm&GdDE6 zv9Ob?L&GB2Omwi1(}nj;49&07MfaJQ+8?Bg(KFQqFZ0Ou!e(W5R=RW!8xwOb>MV*8 zkgXA4gS?(PXX(vUMfl88J6OwNHHuGTvg5_rSaLy*y+&htgl!1Mn<(X4rnol?Wik&YuK956oxSmO7CAH~in)QN>t#GM)8U^z*Pn>TtW9i!OW*q*=Q5T0q2}-VBnk;_J!BXwdHSo9f zsNR??X3bXHtKUu*ugq3kKBfA+pWdARS!)@^#bzvOD;nj{9M#FoJox_W{V6*)zrE!} zM{!z+OQ;+5v(}=*R<*iN=b|Rfr0d&r`k8P0%YnME7~foq4K$m6HJn4%#+; zjF%mF(*+xhkDtfQ!`x#Y$N%qy<=+N|i~ys^xT# zuV;z3wxi*4MdD^Gaq7*-5~oyVj0W}a2@D8V^0STIS%sAydb!WNhRMNv*HI{z&PR{B z2nAZA*2?Rgp0t%ub|_31MNs%%#8RYpn5v@g0yUC(h`|dGP3_snd!#il#&35Hf92ms z&qc<=n}|Fpu$E#!>BU2@GLoA&V|dYsRb#JFJsIy$2aB7eH(va-0CQo2$XNkbYcX^g zsHd|7WW^JgbIoJ&f&*zo&I zBo$%2`8J|p&x?z>$nP}47O6R2^X3}W;5_+<;&}^()5c4_%gPg*79n9>y@t5F2u)@c zUoS$?>cVF+e42`Ukm#`(z1RF{gc|?Kv)-6@Xg_KbFQBP_hL#K?-hr|ajTv70&|v7d z1RMMWv3Ln8#QgPyIfqg%HHqv-y93HsI6fs6XO`ftr1>)n8`iy1Gh^(r^16b)tKq_R zDRQ||#4JTF^|`v56KYZlMvCR|_BMY`VMvA3Q!d`P)kbd;n}?77_VY!v*z*S zOkBaJYh;A+i3;W?rGdDUha86szm*sV+8V>`eEw6&n9u5#!JHZt5J}rYWw1E45;b7{ zYDA^)#TuWsm$o_-7K!^%z)G|{N9&uvI&tbi$0+Z<5nCL3SJsGVj@CDSr=s`r=-JP8 z{-mx$BSB+gRG^4r973uctcE?}fAs2vE$Qt{A!? zFtv^QyDRjrpnsC|<~lKC6*6J|n8lSYA0#M4KRu%N1uM%rpoWwq*GclQRvbeca)5!yPmdhD$g z+iBef+W^IC!nPXaX(6VAyv*OfXzg2nNZhS$p$@$a@jMj5%pc5H_;s&cFYHQ)b|{!X zsqt>_O>d0q@)fpPW)g326pvRUx8{#<)C)=sh#hczxkJN41g$|PB1AmVK#{UW4G9g& zH|*j^Px5YztbeIVJF-9eh=_s@y#`O+zOCZ1%9T4fe7cH@h{0SY!diXM%Fi8mHOaCm-jO#u1NVNOFg+= zq_0Jh&7c2>_Nct~lgL%OkO0l`=+wVk8R0-NBDxE!B7 z&lTTqzz0yHg~vt=9P>9#rdMzEbLxU#Bd8MTL&P9Ojy?5@ePY%|Y)`)3w`LQTe}^`z zHRbbDmgMRCjdyg0GAv__O{#yJpCQFwB>RtvxiQnn{MDNL(+4ic+)P>N#_Aj}){|JO zZbIoNiw&Ue=C3Ht@6w1@HxAJ-!{=-^<_Uk?HRapel?ZNl`)vVe)8rK zpUzREJIyh|&_emb0pYY+ZQ}9(nzT{+-NJ9G_1not7L2u@h2CB1X5Yhy9?c(D(LPD* z(4?r4=6qy@7MA*6rw0D%qtzBPnE9hEUyq(X-`X+^pYf0$#h~vgYRI+zGY0_rv%aD5 zuu$B~#d}+@RFjKw*dkh+8w>dwq0e5!KYin-FOS92tytX~yR526BUEmN%$4cAt(aaa ziyyY4Z~dtz^hzBPwoFYd<5=6~adE%#C;CWg_rBh|;gp z_383A-xk%$eQ4aW9coCuGe?XO zcxuUmxT@by#p=S`B5b8Equ`3(A^Po5``K?+F>H083X#ng<$rW0DQi;jgse%*yVEPh zzWbq1)w}Sg_uDBg_m9tYO0Jy@e?WO*_^mIUub#g2G=KURlcp`(*t*YO8TH5b$uAg* z4BegjTFsXi+jssdGtjmm`Z4=DZLh9?6yD3|_|t|lB@3SFj~s?g`#AZ%#235#(H~wE zzjFA*{JIwP%btJCAo)`oKkRAsdGu1J!!vGH5r=oGZSh&a{hex5&4qH;GdCji0-I9t z@%SA^g`MapI`2}03XkqmSF9^cjxKYyrpv>A_@)*v=l&vKV3|5ziut{s!>w;#`|;ts z!@Ku46wdY+nFGtTrGu!)yWEpPWq=+^%pFvws!K=t1H=6Wh<$^~#98q29{<(!Xi%B$ F{|~H8k&plY diff --git a/example-react/package.json b/example-react/package.json index d74ce64..0015a2e 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -12,6 +12,7 @@ "license": "MIT", "dependencies": { "@monaco-editor/react": "^4.7.0", + "@solid-markdown-wasm/example-shared": "file:../packages/example-shared", "markdown-renderer": "file:../markdown-renderer/pkg/", "mermaid": "^11.12.2", "monaco-editor": "^0.48.0", diff --git a/example-react/src/App.tsx b/example-react/src/App.tsx index 3b531ee..2f8528e 100644 --- a/example-react/src/App.tsx +++ b/example-react/src/App.tsx @@ -1,76 +1,25 @@ import Editor from "@monaco-editor/react"; +import haxiomLogo from "@solid-markdown-wasm/example-shared/assets/haxiom.svg"; +import initialMarkdown from "@solid-markdown-wasm/example-shared/assets/markdown_preview.md?raw"; +import { + CODE_THEMES, + DEFAULT_MERMAID_CONFIG, + EDITOR_THEMES, + type EditorTheme, + type Themes, + getDefaultCodeTheme, + getDefaultEditorTheme, + getPrefersDark, +} from "@solid-markdown-wasm/example-shared/constants"; +import init, { render_md } from "markdown-renderer"; import { useEffect, useMemo, useRef, useState } from "react"; -import init, { render_md, type Themes } from "markdown-renderer"; -import haxiomLogo from "./assets/haxiom.svg"; -import initialMarkdown from "./assets/markdown_preview.md?raw"; import { applyPreviewEnhancements, - DEFAULT_MERMAID_CONFIG, handlePreviewInteraction, } from "./previewEnhancements"; const MEDIA_QUERY = "(prefers-color-scheme: dark)"; -const CODE_THEMES: Themes[] = [ - "1337", - "OneHalfDark", - "OneHalfLight", - "Tomorrow", - "agola-dark", - "ascetic-white", - "axar", - "ayu-dark", - "ayu-light", - "ayu-mirage", - "base16-atelierdune-light", - "base16-ocean-dark", - "base16-ocean-light", - "bbedit", - "boron", - "charcoal", - "cheerfully-light", - "classic-modified", - "demain", - "dimmed-fluid", - "dracula", - "gray-matter-dark", - "green", - "gruvbox-dark", - "gruvbox-light", - "idle", - "inspired-github", - "ir-white", - "kronuz", - "material-dark", - "material-light", - "monokai", - "nord", - "nyx-bold", - "one-dark", - "railsbase16-green-screen-dark", - "solarized-dark", - "solarized-light", - "subway-madrid", - "subway-moscow", - "two-dark", - "visual-studio-dark", - "zenburn", -]; - -const EDITOR_THEMES = [ - { value: "vs", label: "Light" }, - { value: "vs-dark", label: "Dark" }, - { value: "hc-black", label: "High Contrast" }, -] as const; - -type EditorTheme = (typeof EDITOR_THEMES)[number]["value"]; - -const getPrefersDark = () => window.matchMedia(MEDIA_QUERY).matches; -const getDefaultCodeTheme = (isDark: boolean): Themes => - isDark ? "ayu-dark" : "ayu-light"; -const getDefaultEditorTheme = (isDark: boolean): EditorTheme => - isDark ? "vs-dark" : "vs"; - function LoadingFallback() { return (
diff --git a/example-react/src/index.css b/example-react/src/index.css index a4aea67..c9c51ea 100644 --- a/example-react/src/index.css +++ b/example-react/src/index.css @@ -1,543 +1,17 @@ -@import "tailwindcss"; +/** + * React example styles - imports from shared package + */ -:root { - color: #111827; - background: #ffffff; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} +/* Shared base styles (includes Tailwind, spinner, theme variables) */ +@import "@solid-markdown-wasm/example-shared/styles/base.css"; -html, -body, -#root { - height: 100%; -} +/* Markdown rendering styles */ +@import "@solid-markdown-wasm/example-shared/styles/markdown.css"; -body { - margin: 0; -} +/* Code block enhancements */ +@import "@solid-markdown-wasm/example-shared/styles/code-blocks.css"; -code, -pre, -kbd, -samp { - font-family: ui-monospace, Iosevka, SFMono-Regular, SFMono-Regular, Consolas, - "Liberation Mono", Menlo, monospace; -} +/* Mermaid diagram styles */ +@import "@solid-markdown-wasm/example-shared/styles/mermaid.css"; -.spinner { - width: 40px; - height: 40px; - margin: 20px auto; - border: 4px solid rgba(148, 163, 184, 0.25); - border-top-color: rgb(14, 165, 233); - border-radius: 9999px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -[data-theme="dark"] { - color-scheme: dark; - --haxiom-accent-color: rgb(111, 255, 233); - --haxiom-fg-color: black; - --fgColor-default: #f0f6fc; - --fgColor-muted: #9198a1; - --fgColor-accent: #58a6ff; - --fgColor-success: #3fb950; - --fgColor-danger: #ff7b72; - --bgColor-default: #0d1117; - --bgColor-muted: #161b22; - --bgColor-subtle: #11161d; - --bgColor-neutral-muted: #656c7633; - --borderColor-default: #30363d; - --borderColor-muted: #3d444db3; - --borderColor-success: #238636; - --borderColor-danger: #f85149; - --blockquote-border: #3d444d; - --table-stripe: rgba(240, 246, 252, 0.02); - --inline-code-bg: rgba(110, 118, 129, 0.4); - --shadow-elevated: 0 16px 40px rgba(0, 0, 0, 0.25); -} - -[data-theme="light"] { - color-scheme: light; - --haxiom-accent-color: #4f46e5; - --haxiom-fg-color: white; - --fgColor-default: #1f2328; - --fgColor-muted: #59636e; - --fgColor-accent: #0969da; - --fgColor-success: #1a7f37; - --fgColor-danger: #cf222e; - --bgColor-default: #ffffff; - --bgColor-muted: #f6f8fa; - --bgColor-subtle: #f8fafc; - --bgColor-neutral-muted: #818b981f; - --borderColor-default: #d1d9e0; - --borderColor-muted: #d1d9e0b3; - --borderColor-success: #1f883d; - --borderColor-danger: #cf222e; - --blockquote-border: #d0d7de; - --table-stripe: rgba(31, 35, 40, 0.02); - --inline-code-bg: rgba(175, 184, 193, 0.2); - --shadow-elevated: 0 16px 40px rgba(15, 23, 42, 0.08); -} - -.markdown-body { - margin: 0; - color: var(--fgColor-default); - background: transparent; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font-size: 16px; - line-height: 1.65; - word-wrap: break-word; -} - -.markdown-body > :first-child { - margin-top: 0; -} - -.markdown-body > :last-child { - margin-bottom: 0; -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin: 1.6em 0 0.8em; - font-weight: 600; - line-height: 1.25; -} - -.markdown-body h1, -.markdown-body h2 { - padding-bottom: 0.3em; - border-bottom: 1px solid var(--borderColor-muted); -} - -.markdown-body h1 { - font-size: 2rem; -} - -.markdown-body h2 { - font-size: 1.5rem; -} - -.markdown-body h3 { - font-size: 1.25rem; -} - -.markdown-body h4 { - font-size: 1rem; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin: 0 0 1rem; -} - -.markdown-body ul, -.markdown-body ol { - padding-left: 1.5rem; -} - -.markdown-body ul { - list-style: disc; -} - -.markdown-body ol { - list-style: decimal; -} - -.markdown-body li + li { - margin-top: 0.25rem; -} - -.markdown-body li > p { - margin-top: 0.75rem; -} - -.markdown-body blockquote { - padding: 0 1rem; - color: var(--fgColor-muted); - border-left: 0.25rem solid var(--blockquote-border); -} - -.markdown-body hr { - height: 0.25rem; - margin: 1.5rem 0; - background: var(--borderColor-default); - border: 0; -} - -.markdown-body a { - color: var(--fgColor-accent); - text-decoration: none; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body strong { - font-weight: 600; -} - -.markdown-body code { - padding: 0.15rem 0.35rem; - font-size: 0.875em; - background: var(--inline-code-bg); - border-radius: 0.375rem; -} - -.markdown-body pre { - overflow-x: auto; - padding: 0; - border: 1px solid var(--borderColor-default); - border-radius: 0 0 0.75rem 0.75rem; - background: var(--bgColor-subtle); - box-shadow: var(--shadow-elevated); -} - -.markdown-body pre code { - display: block; - padding: 1rem 1.25rem; - background: transparent; - border-radius: 0; -} - -.markdown-body .code-block-wrapper { - overflow: hidden; - margin-bottom: 1rem; - border: 1px solid var(--borderColor-default); - border-radius: 0.75rem; - background: var(--bgColor-subtle); - box-shadow: var(--shadow-elevated); -} - -.markdown-body .code-block-wrapper pre { - margin: 0; - border: 0; - border-radius: 0; - box-shadow: none; -} - -.markdown-body .code-block-header { - display: flex; - justify-content: space-between; - gap: 1rem; - align-items: center; - padding: 0.65rem 1rem; - border-bottom: 1px solid var(--borderColor-default); - background: var(--bgColor-muted); -} - -.markdown-body .code-block-language { - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--fgColor-muted); -} - -.markdown-body .code-block-buttons { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.markdown-body .code-block-copy, -.markdown-body .code-block-collapse, -.markdown-body .code-block-maximize, -.markdown-body .code-block-play { - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem 0.5rem; - color: var(--fgColor-muted); - background: transparent; - border: 1px solid var(--borderColor-muted); - border-radius: 0.35rem; - cursor: pointer; - transition: all 0.15s ease; -} - -.markdown-body .code-block-copy:hover, -.markdown-body .code-block-collapse:hover, -.markdown-body .code-block-maximize:hover, -.markdown-body .code-block-play:hover { - color: var(--fgColor-default); - background: var(--bgColor-neutral-muted); - border-color: var(--borderColor-default); -} - -.markdown-body .code-block-copy:active, -.markdown-body .code-block-collapse:active, -.markdown-body .code-block-maximize:active, -.markdown-body .code-block-play:active { - transform: scale(0.95); -} - -.markdown-body .code-block-copy:disabled, -.markdown-body .code-block-collapse:disabled, -.markdown-body .code-block-maximize:disabled, -.markdown-body .code-block-play:disabled { - opacity: 0.6; - cursor: progress; -} - -.markdown-body .code-block-copy svg, -.markdown-body .code-block-collapse svg, -.markdown-body .code-block-maximize svg, -.markdown-body .code-block-play svg { - width: 1rem; - height: 1rem; -} - -.markdown-body .code-lang-data { - display: none; -} - -.markdown-body .code-block-wrapper.collapsed pre { - display: none; -} - -.markdown-body .code-block-copy.copied, -.markdown-body .code-block-copy.copied:hover { - color: var(--fgColor-success); - border-color: var(--borderColor-success); -} - -.markdown-body - .code-block-wrapper[data-mermaid-status="unrendered"] - .code-block-maximize, -.markdown-body - .code-block-wrapper[data-mermaid-status="code"] - .code-block-maximize, -.markdown-body .code-block-wrapper:not([data-mermaid-status]) .code-block-play, -.markdown-body - .code-block-wrapper:not([data-mermaid-status]) - .code-block-maximize { - display: none; -} - -.markdown-body table { - display: block; - width: max-content; - max-width: 100%; - overflow-x: auto; - border-spacing: 0; - border-collapse: collapse; -} - -.markdown-body table tr { - background: transparent; - border-top: 1px solid var(--borderColor-default); -} - -.markdown-body table tr:nth-child(2n) { - background: var(--table-stripe); -} - -.markdown-body table th, -.markdown-body table td { - padding: 0.5rem 0.85rem; - border: 1px solid var(--borderColor-default); -} - -.markdown-body img, -.markdown-body svg, -.markdown-body video { - max-width: 100%; - box-sizing: border-box; -} - -.markdown-body iframe { - width: 100%; - max-width: 100%; - min-height: 315px; - border: 0; - border-radius: 0.75rem; -} - -.markdown-body .iframe-placeholder { - width: 100%; - min-height: 300px; - border: 1px dashed var(--borderColor-default); - border-radius: 0.75rem; - background: var(--bgColor-muted); -} - -.markdown-body details { - padding: 0.75rem 1rem; - border: 1px solid var(--borderColor-default); - border-radius: 0.75rem; - background: var(--bgColor-muted); -} - -.markdown-body summary { - cursor: pointer; - font-weight: 600; -} - -.markdown-body input[type="checkbox"] { - margin-right: 0.45rem; -} - -.markdown-body .math-code-block, -.markdown-body .math-display, -.markdown-body .math-inline { - overflow-x: auto; - scrollbar-width: thin; -} - -.markdown-body .math-code-block svg, -.markdown-body .math-display svg, -.markdown-body .math-inline svg { - max-width: none !important; -} - -.markdown-body .math-display { - margin: 1.25rem 0; -} - -.markdown-body .math-inline { - display: inline-block; - max-width: 100%; - vertical-align: middle; -} - -.markdown-body .task-list-item { - list-style: none; -} - -.markdown-body .task-list-item input { - vertical-align: middle; -} - -.markdown-body .contains-task-list { - padding-left: 0; -} - -.markdown-body pre[data-mermaid-status="rendered"] { - display: flex; - justify-content: center; - align-items: center; - min-width: 600px; - padding: 1rem 0; - background: transparent; - border: 0; - overflow-x: auto; -} - -.markdown-body pre[data-mermaid-status="rendered"] svg { - display: block; - margin: 0 auto; - max-width: 100%; - height: auto; -} - -.mermaid-loading { - padding: 1rem; - color: var(--fgColor-muted); - text-align: center; -} - -.mermaid-error { - padding: 1rem; - color: var(--fgColor-danger); - background: transparent; - border: 1px solid var(--borderColor-danger); - border-radius: 0.5rem; -} - -.mermaid-clickable pre[data-mermaid-processed="true"] svg { - cursor: zoom-in; -} - -dialog.mermaid-preview-overlay { - position: fixed; - inset: 0; - display: none; - width: 100vw; - height: 100vh; - max-width: none; - max-height: none; - margin: 0; - padding: 40px; - border: none; - background: rgba(0, 0, 0, 0.85); - box-sizing: border-box; - backdrop-filter: blur(8px); -} - -dialog.mermaid-preview-overlay[open] { - display: flex; - align-items: center; - justify-content: center; -} - -dialog.mermaid-preview-overlay::backdrop { - background: rgba(0, 0, 0, 0.85); - backdrop-filter: blur(8px); -} - -.mermaid-preview-content { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - overflow: auto; -} - -.mermaid-preview-content > div { - display: flex; - align-items: center; - justify-content: center; - min-width: 100%; - min-height: 100%; -} - -.mermaid-preview-content svg { - width: auto; - height: auto; - max-width: 100%; - max-height: 100%; - background: var(--bgColor-default); -} - -.mermaid-preview-close { - position: absolute; - top: 20px; - right: 20px; - padding: 0.5rem 0.75rem; - color: white; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 0.5rem; - cursor: pointer; -} - -.mermaid-preview-close:hover { - background: rgba(255, 255, 255, 0.14); -} +/* React-specific overrides (if any) can go here */ diff --git a/example-react/src/monaco.ts b/example-react/src/monaco.ts index f85d9d3..40272a9 100644 --- a/example-react/src/monaco.ts +++ b/example-react/src/monaco.ts @@ -1,11 +1,10 @@ import { loader } from "@monaco-editor/react"; import * as monaco from "monaco-editor"; +import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; -import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; - ( self as typeof self & { MonacoEnvironment: { diff --git a/example-react/vite.config.ts b/example-react/vite.config.ts index 67f637f..68ea4ef 100644 --- a/example-react/vite.config.ts +++ b/example-react/vite.config.ts @@ -1,3 +1,10 @@ +import { + OPTIMIZE_DEPS_EXCLUDE, + SHARED_ASSETS_INCLUDE, + SHARED_ASSETS_INLINE_LIMIT, + getAssetFileNames, + getManualChunks, +} from "@solid-markdown-wasm/example-shared/vite-config"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; @@ -8,23 +15,14 @@ export default defineConfig({ build: { rollupOptions: { output: { - manualChunks(id) { - if (id.includes("node_modules/monaco-editor")) { - return "monaco-editor"; - } - }, - assetFileNames: (assetInfo) => { - if (assetInfo.name?.endsWith(".wasm")) { - return "assets/[name][extname]"; - } - return "assets/[name]-[hash][extname]"; - }, + manualChunks: getManualChunks("monaco-editor"), + assetFileNames: getAssetFileNames(), }, }, - assetsInlineLimit: 0, + assetsInlineLimit: SHARED_ASSETS_INLINE_LIMIT, }, - assetsInclude: ["**/*.wasm"], + assetsInclude: SHARED_ASSETS_INCLUDE, optimizeDeps: { - exclude: ["markdown-renderer"], + exclude: OPTIMIZE_DEPS_EXCLUDE, }, }); diff --git a/example/package.json b/example/package.json index 0699f4f..012c9cf 100644 --- a/example/package.json +++ b/example/package.json @@ -17,6 +17,7 @@ "vite-plugin-wasm": "^3.5.0" }, "dependencies": { + "@solid-markdown-wasm/example-shared": "file:../packages/example-shared", "@tailwindcss/vite": "^4.1.7", "solid-js": "^1.9.5", "solid-markdown-wasm": "file:..", diff --git a/example/src/App.tsx b/example/src/App.tsx index c4a0027..f84ab29 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,3 +1,11 @@ +import haxiomLogo from "@solid-markdown-wasm/example-shared/assets/haxiom.svg"; +import initialMarkdown from "@solid-markdown-wasm/example-shared/assets/markdown_preview.md?raw"; +import { + CODE_THEMES, + DEFAULT_MERMAID_CONFIG, + EDITOR_THEMES, + type Themes, +} from "@solid-markdown-wasm/example-shared/constants"; import { type Component, For, @@ -5,67 +13,8 @@ import { onCleanup, onMount, } from "solid-js"; -import { - DEFAULT_MERMAID_CONFIG, - MarkdownRenderer, - type Themes, -} from "solid-markdown-wasm"; +import { MarkdownRenderer } from "solid-markdown-wasm"; import { MonacoEditor } from "solid-monaco"; -import haxiomLogo from "../src/assets/haxiom.svg"; -import initialMarkdown from "../src/assets/markdown_preview.md?raw"; - -// All available themes from the Rust lib.rs (matches the Themes type) -const CODE_THEMES: Themes[] = [ - "1337", - "OneHalfDark", - "OneHalfLight", - "Tomorrow", - "agola-dark", - "ascetic-white", - "axar", - "ayu-dark", - "ayu-light", - "ayu-mirage", - "base16-atelierdune-light", - "base16-ocean-dark", - "base16-ocean-light", - "bbedit", - "boron", - "charcoal", - "cheerfully-light", - "classic-modified", - "demain", - "dimmed-fluid", - "dracula", - "gray-matter-dark", - "green", - "gruvbox-dark", - "gruvbox-light", - "idle", - "inspired-github", - "ir-white", - "kronuz", - "material-dark", - "material-light", - "monokai", - "nord", - "nyx-bold", - "one-dark", - "railsbase16-green-screen-dark", - "solarized-dark", - "solarized-light", - "subway-madrid", - "subway-moscow", - "two-dark", - "visual-studio-dark", - "zenburn", -]; - -const EDITOR_THEMES = [ - { value: "vs", label: "Light" }, - { value: "vs-dark", label: "Dark" }, - { value: "hc-black", label: "High Contrast" }, -] as const; const LoadingFallback = () => (
diff --git a/example/src/assets/haxiom.svg b/example/src/assets/haxiom.svg deleted file mode 100644 index fd67307..0000000 --- a/example/src/assets/haxiom.svg +++ /dev/null @@ -1 +0,0 @@ -Brand Logo diff --git a/example/src/assets/markdown_preview.md b/example/src/assets/markdown_preview.md deleted file mode 100644 index 09d1984..0000000 --- a/example/src/assets/markdown_preview.md +++ /dev/null @@ -1,168 +0,0 @@ -# Markdown syntax guide 🔥🔥🔥🔥 - -> You are using [solid-markdown-wasm (github)](https://github.com/zeon256/solid-markdown-wasm). - -## Blockquotes - -> [!NOTE] -> This is an informational blockquote. $x+3$ - -> [!TIP] -> This is a tip blockquote. - -> [!IMPORTANT] -> This is an important blockquote. - -> [!WARNING] -> This is a warning blockquote. - -> [!CAUTION] -> This is a caution blockquote. - - -> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz. -> ->> Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor. - -## Mermaid Diagrams - -### Flow Chart -```mermaid -graph TD - A[Start] --> B{Is it working?} - B -->|Yes| C[Great!] - B -->|No| D[Debug] - D --> B - C --> E[End] -``` - -### Sequence Diagram -```mermaid -sequenceDiagram - participant Client - participant Server - participant Database - Client->>Server: Request data - Server->>Database: Query - Database-->>Server: Results - Server-->>Client: Response -``` - -### Gantt Chart -```mermaid -gantt - title Project Timeline - dateFormat YYYY-MM-DD - section Planning - Requirements :2024-01-01, 7d - Design :2024-01-08, 5d - section Development - Backend :2024-01-13, 14d - Frontend :2024-01-20, 14d -``` - -## Blocks of code - -```javascript -let message = 'Hello world'; -alert(message); -``` - -```cpp -#include -#include - -auto main() -> int { - std::vector v = {8, 4, 5, 9}; - - v.emplace_back(6); - v.emplace_back(9); - - v[2] = -1; - - for (int n : v) - std::cout << n << ' '; - std::cout << '\n'; -} -``` - -## Headers - -# This is a Heading h1 -## This is a Heading h2 -###### This is a Heading h6 - -## Emphasis - -*This text will be italic* -_This will also be italic_ - -**This text will be bold** -__This will also be bold__ - -_You **can** combine them_ - -## Lists - -### Unordered - -* Item 1 -* Item 2 -* Item 2a -* Item 2b - * Item 3a - * Item 3b - -### Ordered - -1. Item 1 -2. Item 2 -3. Item 3 - 1. Item 3a - 2. Item 3b - -### Tasklist -- [x] Wake up -- [ ] Drink water -- [ ] Make lunch - -## Images - -![This is an alt text.](/images/tung.jpeg "This is a sample image.") - -## Links - -You are using [solid-markdown-wasm](https://github.com/zeon256/solid-markdown-wasm). - -## Tables - -| Left columns | Right columns | -| ------------- |:-------------:| -| left foo | right foo | -| left bar | right bar | -| left baz | right baz | - -## Iframes - - - -## Inline code - -This web site is using `solid-markdown-wasm`. - -# 🔬 Typst Math in Markdown! - -## 1. Complex Multi-line Equations - -### Maxwell's Equations in Differential Form -```math -nabla dot bold(E) &= rho / epsilon_0 \ -nabla dot bold(B) &= 0 \ -nabla times bold(E) &= -(diff bold(B)) / (diff t) \ -nabla times bold(B) &= mu_0 bold(J) + mu_0 epsilon_0 (diff bold(E)) / (diff t) -``` - -### Schrödinger Equation with Hamiltonian -```math -i planck.reduce (diff Psi(bold(r), t)) / (diff t) = hat(H) Psi(bold(r), t) = [-(planck.reduce^2) / (2m) nabla^2 + V(bold(r), t)] Psi(bold(r), t) -``` diff --git a/example/src/index.css b/example/src/index.css index 4e99462..c1640da 100644 --- a/example/src/index.css +++ b/example/src/index.css @@ -1,1703 +1,23 @@ -@import "tailwindcss"; -@source inline('inline-block'); -@source inline('align-middle'); -@source inline('text-center'); -@source inline('block'); - -.spinner { - width: 40px; - height: 40px; - border-radius: 50%; - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - animation: spin 1s linear infinite; - margin: 20px auto; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -.markdown-body { - --base-size-4: 0.25rem; - --base-size-8: 0.5rem; - --base-size-16: 1rem; - --base-size-24: 1.5rem; - --base-size-40: 2.5rem; - --base-text-weight-normal: 400; - --base-text-weight-medium: 500; - --base-text-weight-semibold: 600; - --fontStack-monospace: ui-monospace, Iosevka, monospace; - --fgColor-accent: Highlight; -} - -.markdown-body ul { - list-style: disc; -} - -.markdown-body li { - display: list-item; -} - -.markdown-body ol { - list-style: decimal; -} - -@media (prefers-color-scheme: dark) { - :root, - .markdown-body, - [data-theme="dark"] { - /* dark */ - color-scheme: dark; - --haxiom-accent-color: rgb(111, 255, 233); - --haxiom-accent-color-hover: rgb(111, 255, 233); - --haxiom-accent-color-disabled: rgb(111, 255, 233); - --haxiom-fg-color: black; - --focus-outlineColor: #1f6feb; - --fgColor-default: #f0f6fc; - --fgColor-muted: #9198a1; - --fgColor-accent: #4493f8; - --fgColor-success: #14b8a6; - --fgColor-attention: #d29922; - --fgColor-danger: #f85149; - --fgColor-done: #ab7df8; - --bgColor-default: #0d1117; - --bgColor-muted: #151b23; - --bgColor-neutral-muted: #656c7633; - --bgColor-attention-muted: #bb800926; - --borderColor-default: #3d444d; - --borderColor-muted: #3d444db3; - --borderColor-neutral-muted: #3d444db3; - --borderColor-accent-emphasis: #1f6feb; - --borderColor-success-emphasis: #0d9488; - --borderColor-attention-emphasis: #9e6a03; - --borderColor-danger-emphasis: #da3633; - --borderColor-done-emphasis: #8957e5; - --color-prettylights-syntax-comment: #9198a1; - --color-prettylights-syntax-constant: #79c0ff; - --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; - --color-prettylights-syntax-entity: #d2a8ff; - --color-prettylights-syntax-storage-modifier-import: #f0f6fc; - --color-prettylights-syntax-entity-tag: #7ee787; - --color-prettylights-syntax-keyword: #ff7b72; - --color-prettylights-syntax-string: #a5d6ff; - --color-prettylights-syntax-variable: #ffa657; - --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; - --color-prettylights-syntax-brackethighlighter-angle: #9198a1; - --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; - --color-prettylights-syntax-invalid-illegal-bg: #8e1519; - --color-prettylights-syntax-carriage-return-text: #f0f6fc; - --color-prettylights-syntax-carriage-return-bg: #b62324; - --color-prettylights-syntax-string-regexp: #7ee787; - --color-prettylights-syntax-markup-list: #f2cc60; - --color-prettylights-syntax-markup-heading: #1f6feb; - --color-prettylights-syntax-markup-italic: #f0f6fc; - --color-prettylights-syntax-markup-bold: #f0f6fc; - --color-prettylights-syntax-markup-deleted-text: #ffdcd7; - --color-prettylights-syntax-markup-deleted-bg: #67060c; - --color-prettylights-syntax-markup-inserted-text: #aff5b4; - --color-prettylights-syntax-markup-inserted-bg: #033a16; - --color-prettylights-syntax-markup-changed-text: #ffdfb6; - --color-prettylights-syntax-markup-changed-bg: #5a1e02; - --color-prettylights-syntax-markup-ignored-text: #f0f6fc; - --color-prettylights-syntax-markup-ignored-bg: #1158c7; - --color-prettylights-syntax-meta-diff-range: #d2a8ff; - --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; - } -} - -@media (prefers-color-scheme: light) { - :root, - .markdown-body, - [data-theme="light"] { - /* light */ - color-scheme: light; - --haxiom-accent-color: #4f46e5; - --haxiom-accent-color-hover: #4f46e5; - --haxiom-accent-color-disabled: #4f46e5; - --haxiom-fg-color: white; - --focus-outlineColor: #0969da; - --fgColor-default: #1f2328; - --fgColor-muted: #59636e; - --fgColor-accent: #0969da; - --fgColor-success: #0d9488; - --fgColor-attention: #9a6700; - --fgColor-danger: #d1242f; - --fgColor-done: #8250df; - --bgColor-default: #ffffff; - --bgColor-muted: #f6f8fa; - --bgColor-neutral-muted: #818b981f; - --bgColor-attention-muted: #fff8c5; - --borderColor-default: #d1d9e0; - --borderColor-muted: #d1d9e0b3; - --borderColor-neutral-muted: #d1d9e0b3; - --borderColor-accent-emphasis: #0969da; - --borderColor-success-emphasis: #0d9488; - --borderColor-attention-emphasis: #9a6700; - --borderColor-danger-emphasis: #cf222e; - --borderColor-done-emphasis: #8250df; - --color-prettylights-syntax-comment: #59636e; - --color-prettylights-syntax-constant: #0550ae; - --color-prettylights-syntax-constant-other-reference-link: #0a3069; - --color-prettylights-syntax-entity: #6639ba; - --color-prettylights-syntax-storage-modifier-import: #1f2328; - --color-prettylights-syntax-entity-tag: #0550ae; - --color-prettylights-syntax-keyword: #cf222e; - --color-prettylights-syntax-string: #0a3069; - --color-prettylights-syntax-variable: #953800; - --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; - --color-prettylights-syntax-brackethighlighter-angle: #59636e; - --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; - --color-prettylights-syntax-invalid-illegal-bg: #82071e; - --color-prettylights-syntax-carriage-return-text: #f6f8fa; - --color-prettylights-syntax-carriage-return-bg: #cf222e; - --color-prettylights-syntax-string-regexp: #116329; - --color-prettylights-syntax-markup-list: #3b2300; - --color-prettylights-syntax-markup-heading: #0550ae; - --color-prettylights-syntax-markup-italic: #1f2328; - --color-prettylights-syntax-markup-bold: #1f2328; - --color-prettylights-syntax-markup-deleted-text: #82071e; - --color-prettylights-syntax-markup-deleted-bg: #ffebe9; - --color-prettylights-syntax-markup-inserted-text: #116329; - --color-prettylights-syntax-markup-inserted-bg: #dafbe1; - --color-prettylights-syntax-markup-changed-text: #953800; - --color-prettylights-syntax-markup-changed-bg: #ffd8b5; - --color-prettylights-syntax-markup-ignored-text: #d1d9e0; - --color-prettylights-syntax-markup-ignored-bg: #0550ae; - --color-prettylights-syntax-meta-diff-range: #8250df; - --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; - } -} - -.markdown-body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - color: var(--fgColor-default); - background-color: var(--bgColor-default); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .octicon { - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; -} - -.markdown-body h1:hover .anchor .octicon-link:before, -.markdown-body h2:hover .anchor .octicon-link:before, -.markdown-body h3:hover .anchor .octicon-link:before, -.markdown-body h4:hover .anchor .octicon-link:before, -.markdown-body h5:hover .anchor .octicon-link:before, -.markdown-body h6:hover .anchor .octicon-link:before { - width: 16px; - height: 16px; - content: " "; - display: inline-block; - background-color: currentColor; - -webkit-mask-image: url("data:image/svg+xml,"); - mask-image: url("data:image/svg+xml,"); -} - -.markdown-body details, -.markdown-body figcaption, -.markdown-body figure { - display: block; -} - -.markdown-body summary { - display: list-item; -} - -.markdown-body [hidden] { - display: none !important; -} - -.markdown-body a { - background-color: transparent; - color: var(--fgColor-accent); - text-decoration: none; -} - -.markdown-body abbr[title] { - border-bottom: none; - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -.markdown-body b, -.markdown-body strong { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dfn { - font-style: italic; -} - -.markdown-body h1 { - margin: .67em 0; - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 2em; - border-bottom: 1px solid var(--borderColor-muted); -} - -.markdown-body mark { - background-color: var(--bgColor-attention-muted); - color: var(--fgColor-default); -} - -.markdown-body small { - font-size: 90%; -} - -.markdown-body sub, -.markdown-body sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -.markdown-body sub { - bottom: -0.25em; -} - -.markdown-body sup { - top: -0.5em; -} - -.markdown-body img { - border-style: none; - max-width: 100%; - box-sizing: content-box; -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre, -.markdown-body samp { - font-family: monospace; - font-size: 1em; -} - -.markdown-body figure { - margin: 1em var(--base-size-40); -} - -.markdown-body hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - border-bottom: 1px solid var(--borderColor-muted); - height: .25em; - padding: 0; - margin: var(--base-size-24) 0; - background-color: var(--borderColor-default); - border: 0; -} - -.markdown-body input { - font: inherit; - margin: 0; - overflow: visible; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body [type="button"], -.markdown-body [type="reset"], -.markdown-body [type="submit"] { - -webkit-appearance: button; - appearance: button; -} - -.markdown-body [type="checkbox"], -.markdown-body [type="radio"] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body [type="number"]::-webkit-inner-spin-button, -.markdown-body [type="number"]::-webkit-outer-spin-button { - height: auto; -} - -.markdown-body [type="search"]::-webkit-search-cancel-button, -.markdown-body [type="search"]::-webkit-search-decoration { - -webkit-appearance: none; - appearance: none; -} - -.markdown-body ::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -.markdown-body ::-webkit-file-upload-button { - -webkit-appearance: button; - appearance: button; - font: inherit; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body ::placeholder { - color: var(--fgColor-muted); - opacity: 1; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; - display: block; - width: max-content; - max-width: 100%; - overflow: auto; - font-variant: tabular-nums; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body details summary { - cursor: pointer; -} - -.markdown-body a:focus, -.markdown-body [role="button"]:focus, -.markdown-body input[type="radio"]:focus, -.markdown-body input[type="checkbox"]:focus { - outline: 2px solid var(--focus-outlineColor); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:focus:not(:focus-visible), -.markdown-body [role="button"]:focus:not(:focus-visible), -.markdown-body input[type="radio"]:focus:not(:focus-visible), -.markdown-body input[type="checkbox"]:focus:not(:focus-visible) { - outline: solid 1px transparent; -} - -.markdown-body a:focus-visible, -.markdown-body [role="button"]:focus-visible, -.markdown-body input[type="radio"]:focus-visible, -.markdown-body input[type="checkbox"]:focus-visible { - outline: 2px solid var(--focus-outlineColor); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:not([class]):focus, -.markdown-body a:not([class]):focus-visible, -.markdown-body input[type="radio"]:focus, -.markdown-body input[type="radio"]:focus-visible, -.markdown-body input[type="checkbox"]:focus, -.markdown-body input[type="checkbox"]:focus-visible { - outline-offset: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: var(--base-size-4); - font: 11px - var( - --fontStack-monospace, - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace - ); - line-height: 10px; - color: var(--fgColor-default); - vertical-align: middle; - background-color: var(--bgColor-muted); - border: solid 1px var(--borderColor-neutral-muted); - border-bottom-color: var(--borderColor-neutral-muted); - border-radius: 6px; - box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: var(--base-size-24); - margin-bottom: var(--base-size-16); - font-weight: var(--base-text-weight-semibold, 600); - line-height: 1.25; -} - -.markdown-body h2 { - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 1.5em; - border-bottom: 1px solid var(--borderColor-muted); -} - -.markdown-body h3 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1.25em; -} - -.markdown-body h4 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1em; -} - -.markdown-body h5 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .875em; -} - -.markdown-body h6 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .85em; - color: var(--fgColor-muted); -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; - padding: 0 1em; - color: var(--fgColor-muted); - border-left: .25em solid var(--borderColor-default); -} - -.markdown-body ul, -.markdown-body ol { - margin-top: 0; - margin-bottom: 0; - padding-left: 2em; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body tt, -.markdown-body code, -.markdown-body samp { - font-family: var( - --fontStack-monospace, - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace - ); - font-size: 16px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font-family: var( - --fontStack-monospace, - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace - ); - font-size: 12px; - word-wrap: normal; -} - -.markdown-body .octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.markdown-body input::-webkit-outer-spin-button, -.markdown-body input::-webkit-inner-spin-button { - margin: 0; - appearance: none; -} - -.markdown-body .mr-2 { - margin-right: var(--base-size-8, 8px) !important; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body > *:first-child { - margin-top: 0 !important; -} - -.markdown-body > *:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: var(--fgColor-danger); -} - -.markdown-body .anchor { - float: left; - padding-right: var(--base-size-4); - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin-top: 0; - margin-bottom: var(--base-size-16); -} - -.markdown-body blockquote > :first-child { - margin-top: 0; -} - -.markdown-body blockquote > :last-child { - margin-bottom: 0; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: var(--fgColor-default); - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - padding: 0 .2em; - font-size: inherit; -} - -.markdown-body summary h1, -.markdown-body summary h2, -.markdown-body summary h3, -.markdown-body summary h4, -.markdown-body summary h5, -.markdown-body summary h6 { - display: inline-block; -} - -.markdown-body summary h1 .anchor, -.markdown-body summary h2 .anchor, -.markdown-body summary h3 .anchor, -.markdown-body summary h4 .anchor, -.markdown-body summary h5 .anchor, -.markdown-body summary h6 .anchor { - margin-left: -40px; -} - -.markdown-body summary h1, -.markdown-body summary h2 { - padding-bottom: 0; - border-bottom: 0; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ol[type="a s"] { - list-style-type: lower-alpha; -} - -.markdown-body ol[type="A s"] { - list-style-type: upper-alpha; -} - -.markdown-body ol[type="i s"] { - list-style-type: lower-roman; -} - -.markdown-body ol[type="I s"] { - list-style-type: upper-roman; -} - -.markdown-body ol[type="1"] { - list-style-type: decimal; -} - -.markdown-body div > ol:not([type]) { - list-style-type: decimal; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li > p { - margin-top: var(--base-size-16); -} - -.markdown-body li + li { - margin-top: .25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: var(--base-size-16); - font-size: 1em; - font-style: italic; - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dl dd { - padding: 0 var(--base-size-16); - margin-bottom: var(--base-size-16); -} - -.markdown-body table th { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid var(--borderColor-default); -} - -.markdown-body table td > :last-child { - margin-bottom: 0; -} - -.markdown-body table tr { - background-color: var(--bgColor-default); - border-top: 1px solid var(--borderColor-muted); -} - -.markdown-body table tr:nth-child(2n) { - background-color: var(--bgColor-muted); -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img[align="right"] { - padding-left: 20px; -} - -.markdown-body img[align="left"] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame > span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid var(--borderColor-default); -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: var(--fgColor-default); -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right > span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: .2em .4em; - margin: 0; - font-size: 120%; - white-space: break-spaces; - background-color: var(--bgColor-neutral-muted); - border-radius: 6px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} +/** + * Solid example styles - imports from shared package + */ -.markdown-body del code { - text-decoration: inherit; -} +/* Shared base styles (includes Tailwind, spinner, theme variables) */ +@import "@solid-markdown-wasm/example-shared/styles/base.css"; -.markdown-body samp { - font-size: 85%; -} +/* Markdown rendering styles */ +@import "@solid-markdown-wasm/example-shared/styles/markdown.css"; -.markdown-body pre code { - font-size: 100%; -} +/* Code block enhancements */ +@import "@solid-markdown-wasm/example-shared/styles/code-blocks.css"; -.markdown-body pre > code { - padding: 0; - margin: 0; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} +/* Mermaid diagram styles */ +@import "@solid-markdown-wasm/example-shared/styles/mermaid.css"; -.markdown-body .highlight { - margin-bottom: var(--base-size-16); -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: var(--base-size-16); - overflow: auto; - font-size: 120%; - line-height: 1.45; - color: var(--fgColor-default); - background-color: var(--bgColor-muted); - border-radius: 6px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px var(--base-size-8) 9px; - text-align: right; - background: var(--bgColor-default); - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: var(--base-text-weight-semibold, 600); - background: var(--bgColor-muted); - border-top: 0; -} - -.markdown-body [data-footnote-ref]::before { - content: "["; -} - -.markdown-body [data-footnote-ref]::after { - content: "]"; -} - -.markdown-body .footnotes { - font-size: 12px; - color: var(--fgColor-muted); - border-top: 1px solid var(--borderColor-default); -} - -.markdown-body .footnotes ol { - padding-left: var(--base-size-16); -} - -.markdown-body .footnotes ol ul { - display: inline-block; - padding-left: var(--base-size-16); - margin-top: var(--base-size-16); -} - -.markdown-body .footnotes li { - position: relative; -} - -.markdown-body .footnotes li:target::before { - position: absolute; - top: calc(var(--base-size-8) * -1); - right: calc(var(--base-size-8) * -1); - bottom: calc(var(--base-size-8) * -1); - left: calc(var(--base-size-24) * -1); - pointer-events: none; - content: ""; - border: 2px solid var(--borderColor-accent-emphasis); - border-radius: 6px; -} - -.markdown-body .footnotes li:target { - color: var(--fgColor-default); -} - -.markdown-body .footnotes .data-footnote-backref g-emoji { - font-family: monospace; -} - -.markdown-body body:has(:modal) { - padding-right: var(--dialog-scrollgutter) !important; -} - -.markdown-body .pl-c { - color: var(--color-prettylights-syntax-comment); -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: var(--color-prettylights-syntax-constant); -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: var(--color-prettylights-syntax-entity); -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: var(--color-prettylights-syntax-storage-modifier-import); -} - -.markdown-body .pl-ent { - color: var(--color-prettylights-syntax-entity-tag); -} - -.markdown-body .pl-k { - color: var(--color-prettylights-syntax-keyword); -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: var(--color-prettylights-syntax-string); -} - -.markdown-body .pl-v, -.markdown-body .pl-smw { - color: var(--color-prettylights-syntax-variable); -} - -.markdown-body .pl-bu { - color: var(--color-prettylights-syntax-brackethighlighter-unmatched); -} - -.markdown-body .pl-ii { - color: var(--color-prettylights-syntax-invalid-illegal-text); - background-color: var(--color-prettylights-syntax-invalid-illegal-bg); -} - -.markdown-body .pl-c2 { - color: var(--color-prettylights-syntax-carriage-return-text); - background-color: var(--color-prettylights-syntax-carriage-return-bg); -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: var(--color-prettylights-syntax-string-regexp); -} - -.markdown-body .pl-ml { - color: var(--color-prettylights-syntax-markup-list); -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-heading); -} - -.markdown-body .pl-mi { - font-style: italic; - color: var(--color-prettylights-syntax-markup-italic); -} - -.markdown-body .pl-mb { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-bold); -} - -.markdown-body .pl-md { - color: var(--color-prettylights-syntax-markup-deleted-text); - background-color: var(--color-prettylights-syntax-markup-deleted-bg); -} - -.markdown-body .pl-mi1 { - color: var(--color-prettylights-syntax-markup-inserted-text); - background-color: var(--color-prettylights-syntax-markup-inserted-bg); -} - -.markdown-body .pl-mc { - color: var(--color-prettylights-syntax-markup-changed-text); - background-color: var(--color-prettylights-syntax-markup-changed-bg); -} - -.markdown-body .pl-mi2 { - color: var(--color-prettylights-syntax-markup-ignored-text); - background-color: var(--color-prettylights-syntax-markup-ignored-bg); -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: var(--color-prettylights-syntax-meta-diff-range); -} - -.markdown-body .pl-ba { - color: var(--color-prettylights-syntax-brackethighlighter-angle); -} - -.markdown-body .pl-sg { - color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); -} - -.markdown-body .pl-corl { - text-decoration: underline; - color: var(--color-prettylights-syntax-constant-other-reference-link); -} - -.markdown-body [role="button"]:focus:not(:focus-visible), -.markdown-body [role="tabpanel"][tabindex="0"]:focus:not(:focus-visible), -.markdown-body button:focus:not(:focus-visible), -.markdown-body summary:focus:not(:focus-visible), -.markdown-body a:focus:not(:focus-visible) { - outline: none; - box-shadow: none; -} - -.markdown-body [tabindex="0"]:focus:not(:focus-visible), -.markdown-body details-dialog:focus:not(:focus-visible) { - outline: none; -} - -.markdown-body g-emoji { - display: inline-block; - min-width: 1ch; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", - sans-serif; - font-size: 1em; - font-style: normal !important; - font-weight: var(--base-text-weight-normal, 400); - line-height: 1; - vertical-align: -0.075em; -} - -.markdown-body g-emoji img { - width: 1em; - height: 1em; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item label { - font-weight: var(--base-text-weight-normal, 400); -} - -.markdown-body .task-list-item.enabled label { - cursor: pointer; -} - -.markdown-body .task-list-item + .task-list-item { - margin-top: var(--base-size-4); -} - -.markdown-body .task-list-item .handle { - display: none; -} - -.markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; - vertical-align: middle; -} - -.markdown-body ul:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body ol:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body - .contains-task-list:focus-within - .task-list-item-convert-container { - display: block; - width: auto; - height: 24px; - overflow: visible; - clip: auto; -} - -.markdown-body ::-webkit-calendar-picker-indicator { - filter: invert(50%); -} - -.markdown-body .markdown-alert { - padding: var(--base-size-8) var(--base-size-16); - margin-bottom: var(--base-size-16); - color: inherit; - border-left: .25em solid var(--borderColor-default); -} - -.markdown-body .markdown-alert > :first-child { - margin-top: 0; -} - -.markdown-body .markdown-alert > :last-child { - margin-bottom: 0; -} - -.markdown-body .markdown-alert .markdown-alert-title { - display: flex; - font-weight: var(--base-text-weight-medium, 500); - align-items: center; - line-height: 1; -} - -.markdown-body .markdown-alert.markdown-alert-note { - border-left-color: var(--borderColor-accent-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { - color: var(--fgColor-accent); -} - -.markdown-body .markdown-alert.markdown-alert-important { - border-left-color: var(--borderColor-done-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { - color: var(--fgColor-done); -} - -.markdown-body .markdown-alert.markdown-alert-warning { - border-left-color: var(--borderColor-attention-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { - color: var(--fgColor-attention); -} - -.markdown-body .markdown-alert.markdown-alert-tip { - border-left-color: var(--borderColor-success-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { - color: var(--fgColor-success); -} - -.markdown-body .markdown-alert.markdown-alert-caution { - border-left-color: var(--borderColor-danger-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { - color: var(--fgColor-danger); -} - -.markdown-body > *:first-child > .heading-element:first-child { - margin-top: 0 !important; -} - -.markdown-body .highlight pre:has(+ .zeroclipboard-container) { - min-height: 52px; -} - -/* Code block wrapper styling */ -.markdown-body .code-block-wrapper { - position: relative; - margin-bottom: 1rem; - border-radius: 0.5rem; - overflow: hidden; -} - -.markdown-body .code-block-wrapper pre { - margin: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.markdown-body .code-block-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 1rem; - background-color: var(--bgColor-muted, #151b23); - border-bottom: 1px solid var(--borderColor-muted, #3d444db3); -} - -.markdown-body .code-block-language { - font-size: 0.75rem; - font-weight: 500; - color: var(--fgColor-muted, #9198a1); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.markdown-body .code-block-buttons { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.markdown-body .code-block-copy, -.markdown-body .code-block-collapse, -.markdown-body .code-block-maximize, -.markdown-body .code-block-play { - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - color: var(--fgColor-muted, #9198a1); - background: transparent; - border: 1px solid var(--borderColor-muted, #3d444db3); - border-radius: 0.25rem; - cursor: pointer; - transition: all 0.15s ease; -} - -.markdown-body .code-block-copy:hover, -.markdown-body .code-block-collapse:hover, -.markdown-body .code-block-maximize:hover, -.markdown-body .code-block-play:hover { - color: var(--fgColor-default, #f0f6fc); - background-color: var(--bgColor-neutral-muted, #656c7633); - border-color: var(--borderColor-default, #3d444d); -} - -.markdown-body .code-block-copy:active, -.markdown-body .code-block-collapse:active, -.markdown-body .code-block-play:active { - transform: scale(0.95); -} - -.markdown-body .code-block-copy svg, -.markdown-body .code-block-collapse svg, -.markdown-body .code-block-maximize svg, -.markdown-body .code-block-play svg { - width: 1rem; - height: 1rem; -} - -/* Visibility logic for Mermaid buttons */ -.markdown-body - .code-block-wrapper[data-mermaid-status="unrendered"] - .code-block-maximize, -.markdown-body - .code-block-wrapper[data-mermaid-status="code"] - .code-block-maximize { - display: none; -} - -/* Hide play button for non-mermaid blocks (which don't have data-mermaid-status) */ -.markdown-body .code-block-wrapper:not([data-mermaid-status]) .code-block-play { - display: none; -} - -/* Hide the language data span (it's only for JS to read) */ -.markdown-body .code-lang-data { - display: none; -} - -/* Collapsed state for code blocks */ -.markdown-body .code-block-wrapper.collapsed pre { - display: none; -} - -/* Copy button success state */ -.markdown-body .code-block-copy.copied { - color: var(--fgColor-success, #14b8a6); - border-color: var(--fgColor-success, #14b8a6); -} - -.markdown-body .code-block-copy.copied:hover { - color: var(--fgColor-success, #14b8a6); - border-color: var(--fgColor-success, #14b8a6); -} - -/* Light mode adjustments */ -@media (prefers-color-scheme: light) { - .markdown-body .code-block-header { - background-color: var(--bgColor-muted, #f6f8fa); - border-bottom-color: var(--borderColor-muted, #d1d9e0b3); - } - - .markdown-body .code-block-language { - color: var(--fgColor-muted, #59636e); - } - - .markdown-body .code-block-copy, - .markdown-body .code-block-collapse, - .markdown-body .code-block-maximize, - .markdown-body .code-block-play { - color: var(--fgColor-muted, #59636e); - border-color: var(--borderColor-muted, #d1d9e0b3); - } - - .markdown-body .code-block-copy:hover, - .markdown-body .code-block-collapse:hover, - .markdown-body .code-block-maximize:hover, - .markdown-body .code-block-play:hover { - color: var(--fgColor-default, #1f2328); - background-color: var(--bgColor-neutral-muted, #818b981f); - border-color: var(--borderColor-default, #d1d9e0); - } - - .markdown-body .code-block-copy.copied { - color: var(--fgColor-success, #14b8a6); - border-color: var(--fgColor-success, #14b8a6); - } - - .markdown-body .code-block-copy.copied:hover { - color: var(--fgColor-success, #14b8a6); - border-color: var(--fgColor-success, #14b8a6); - } -} - -/* Iframe placeholder styles - these reserve space for the overlay iframes */ -.markdown-body .iframe-placeholder { - /* Default dimensions if not specified inline */ - width: 100%; - min-height: 300px; - /* Allow inline styles to override */ - box-sizing: border-box; - background: transparent; - border-radius: 6px; - position: relative; - /* Margins for proper spacing */ - margin-top: 16px; - margin-bottom: 16px; -} - -/* When iframe has explicit dimensions via inline style, don't force min-height */ -.markdown-body .iframe-placeholder[style*="height"] { - min-height: unset; -} - -.markdown-body .iframe-placeholder[style*="width"] { - width: unset; -} - -/* Iframe overlay wrapper styles */ -.iframe-overlay-wrapper { - overflow: hidden; - border-radius: 6px; -} - -.iframe-overlay-wrapper iframe { - display: block; - width: 100%; - height: 100%; - border: none; -} - -@media (prefers-color-scheme: dark) { - .markdown-body .iframe-placeholder { - background: var(--bgColor-muted, #161b22); - border-color: var(--borderColor-default, #30363d); - } -} - -/* Responsive iframe and math styles for markdown content */ -.markdown-body iframe { - max-width: 100%; - border: none; -} - -.markdown-body .math-code-block, -.markdown-body .math-display, -.markdown-body .math-inline { - overflow-x: auto; - max-width: 100%; - -webkit-overflow-scrolling: touch; - padding: 1rem 0; - /* Increased padding for all math blocks */ - line-height: normal !important; -} - -.markdown-body .math-code-block { - margin: 1.25rem 0; -} - -.markdown-body .math-inline { - display: inline-block; - vertical-align: middle; - /* Middle is safer for multiline equations */ - padding: 0.75rem 0 0.75rem 0; - /* Extra bottom padding for scrollbar */ - max-width: 100%; - /* Maintain sharpness without forcing sub-pixel clipping */ - backface-visibility: hidden; - -webkit-font-smoothing: subpixel-antialiased; -} - -.markdown-body .math-code-block svg, -.markdown-body .math-display svg, -.markdown-body .math-inline svg { - max-width: none !important; - overflow: visible !important; - /* Anti-blur fixes */ - shape-rendering: geometricPrecision !important; - image-rendering: -webkit-optimize-contrast !important; - transform: translateZ(0); -} - -.markdown-body .math-code-block::-webkit-scrollbar, -.markdown-body .math-display::-webkit-scrollbar, -.markdown-body .math-inline::-webkit-scrollbar { - height: 4px; -} - -.markdown-body .math-code-block::-webkit-scrollbar-track, -.markdown-body .math-display::-webkit-scrollbar-track, -.markdown-body .math-inline::-webkit-scrollbar-track { - background: transparent; -} - -/* Show scrollbar thumb by default for visibility on mobile */ -.markdown-body .math-code-block::-webkit-scrollbar-thumb, -.markdown-body .math-display::-webkit-scrollbar-thumb, -.markdown-body .math-inline::-webkit-scrollbar-thumb { - background-color: rgba(var(--text-secondary-rgb), 0.3); - border-radius: 10px; -} - -/* Increase visibility on hover */ -.markdown-body .math-code-block:hover::-webkit-scrollbar-thumb, -.markdown-body .math-display:hover::-webkit-scrollbar-thumb, -.markdown-body .math-inline:hover::-webkit-scrollbar-thumb, -.markdown-body .math-code-block:active::-webkit-scrollbar-thumb, -.markdown-body .math-display:active::-webkit-scrollbar-thumb, -.markdown-body .math-inline:active::-webkit-scrollbar-thumb { - background-color: rgba(var(--text-secondary-rgb), 0.8); -} - -.markdown-body .math-code-block::-webkit-scrollbar-thumb:hover, -.markdown-body .math-display::-webkit-scrollbar-thumb:hover, -.markdown-body .math-inline::-webkit-scrollbar-thumb:hover { - background-color: rgba(var(--text-secondary-rgb), 1); -} - -/* Firefox support */ -.markdown-body .math-code-block, -.markdown-body .math-display, -.markdown-body .math-inline { - scrollbar-width: thin; - scrollbar-color: rgba(var(--text-secondary-rgb), 0.3) transparent; -} - -.markdown-body .math-code-block:hover, -.markdown-body .math-display:hover, -.markdown-body .math-inline:hover, -.markdown-body .math-code-block:active, -.markdown-body .math-display:active, -.markdown-body .math-inline:active { - scrollbar-width: thin; -} - -/* Mermaid Diagrams */ -.markdown-body pre[data-mermaid-status="rendered"] { - display: flex; - justify-content: center; - align-items: center; - background: transparent; - border: none; - padding: 1rem 0; - min-width: 600px; - overflow-x: auto; -} - -.markdown-body pre[data-mermaid-status="rendered"] svg { - display: block; - margin: 0 auto; - max-width: 100%; - height: auto; -} - -.mermaid-loading { - padding: 1rem; - color: var(--fgColor-muted, #6b7280); - text-align: center; -} - -.mermaid-error { - color: var(--fgColor-danger, #f85149); - padding: 1rem; - border: 1px solid var(--borderColor-danger-emphasis, #da3633); - border-radius: 4px; - background-color: transparent; -} - -@media (prefers-color-scheme: light) { - .mermaid-error { - color: var(--fgColor-danger, #d1242f); - border-color: var(--borderColor-danger-emphasis, #cf222e); - } -} - -.mermaid-clickable .language-mermaid { - cursor: zoom-in; - transition: opacity 0.2s; -} - -.mermaid-clickable .language-mermaid:hover { - opacity: 0.9; -} - -.mermaid-preview-content svg { - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; -} - -.mermaid-preview-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.85); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(8px); - cursor: zoom-out; - padding: 40px; - box-sizing: border-box; -} - -.mermaid-preview-content { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-color: transparent; - overflow: auto; - cursor: default; -} - -.mermaid-preview-close { - position: absolute; - top: 20px; - right: 20px; - background: none; - border: none; - color: white; - cursor: pointer; - padding: 10px; -} +/* Tailwind source directives for Solid's JIT compiler */ +@source inline('inline-block'); +@source inline('align-middle'); +@source inline('text-center'); +@source inline('block'); -.mermaid-preview-overlay .mermaid-preview-content svg { - background-color: var(--bgColor-default); -} +/* Solid-specific overrides (if any) can go here */ diff --git a/example/tsconfig.json b/example/tsconfig.json index 249b273..7fa3b75 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -3,7 +3,7 @@ "strict": true, "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "jsx": "preserve", diff --git a/example/vite.config.ts b/example/vite.config.ts index 2dad54f..71c68e4 100644 --- a/example/vite.config.ts +++ b/example/vite.config.ts @@ -1,3 +1,10 @@ +import { + OPTIMIZE_DEPS_EXCLUDE, + SHARED_ASSETS_INCLUDE, + SHARED_ASSETS_INLINE_LIMIT, + getAssetFileNames, + getManualChunks, +} from "@solid-markdown-wasm/example-shared/vite-config"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import solid from "vite-plugin-solid"; @@ -11,26 +18,14 @@ export default defineConfig({ build: { rollupOptions: { output: { - manualChunks(id) { - if (id.includes("node_modules/solid-monaco")) { - return "solid-monaco"; - } - }, - assetFileNames: (assetInfo) => { - // Keep .wasm files with their original names - if (assetInfo.name?.endsWith(".wasm")) { - return "assets/[name][extname]"; - } - return "assets/[name]-[hash][extname]"; - }, + manualChunks: getManualChunks("solid-monaco"), + assetFileNames: getAssetFileNames(), }, }, - assetsInlineLimit: 0, // Disable inlining of assets (prevents base64 encoding) + assetsInlineLimit: SHARED_ASSETS_INLINE_LIMIT, }, - // Ensure .wasm files are treated as assets, not modules - assetsInclude: ["**/*.wasm"], - // Tell Vite to exclude .wasm from being processed as JavaScript + assetsInclude: SHARED_ASSETS_INCLUDE, optimizeDeps: { - exclude: ["markdown-renderer"], + exclude: OPTIMIZE_DEPS_EXCLUDE, }, }); diff --git a/package.json b/package.json index 8dc4c48..f9c81e3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "module": "./dist/solid-markdown-wasm.es.js", "types": "./dist/solid-markdown-wasm.es.d.ts", - "workspaces": ["example", "example-react"], + "workspaces": ["example", "example-react", "packages/*"], "scripts": { "dev": "vite", "build": "tsc -b && vite build", diff --git a/packages/example-shared/README.md b/packages/example-shared/README.md new file mode 100644 index 0000000..18f82cc --- /dev/null +++ b/packages/example-shared/README.md @@ -0,0 +1,108 @@ +# @solid-markdown-wasm/example-shared + +Shared resources for the solid-markdown-wasm example applications. This package eliminates duplication between the React and Solid examples by providing common assets, styles, constants, and configuration utilities. + +## What's Included + +### Assets (`./assets/*`) +- `haxiom.svg` - Haxiom logo used in both examples +- `markdown_preview.md` - Default markdown content for the editors + +### Styles (`./styles/*`) + +- **`base.css`** - Tailwind imports, spinner animation, CSS variables for theming +- **`markdown.css`** - Base `.markdown-body` styles for rendered markdown content +- **`code-blocks.css`** - Code block wrapper, header, and button styles +- **`mermaid.css`** - Mermaid diagram rendering styles and preview overlay + +### Constants (`./constants`) + +TypeScript exports: +- `CODE_THEMES` - Array of 40+ available syntax highlighting themes +- `EDITOR_THEMES` - Array of Monaco editor themes (Light, Dark, High Contrast) +- `DEFAULT_MERMAID_CONFIG` - Mermaid configuration function for consistent diagram theming +- `DARK_MODE_MEDIA_QUERY` - Media query string for dark mode detection +- Helper functions: `getDefaultCodeTheme()`, `getDefaultEditorTheme()`, `getPrefersDark()` +- Types: `Themes`, `EditorTheme`, `MermaidTheme`, `MermaidConfigFn` + +### Vite Config (`./vite-config`) + +JavaScript exports: +- `getManualChunks(chunkName)` - Function for vendor code splitting +- `getAssetFileNames()` - Asset file naming function with WASM support +- `OPTIMIZE_DEPS_EXCLUDE` - Dependencies to exclude from optimization +- `SHARED_ASSETS_INCLUDE` - Asset include patterns +- `SHARED_ASSETS_INLINE_LIMIT` - Build config for WASM handling + +## Usage + +### Importing Assets + +```typescript +import haxiomLogo from "@solid-markdown-wasm/example-shared/assets/haxiom.svg"; +import initialMarkdown from "@solid-markdown-wasm/example-shared/assets/markdown_preview.md?raw"; +``` + +### Importing Styles + +```css +@import "@solid-markdown-wasm/example-shared/styles/base.css"; +@import "@solid-markdown-wasm/example-shared/styles/markdown.css"; +@import "@solid-markdown-wasm/example-shared/styles/code-blocks.css"; +@import "@solid-markdown-wasm/example-shared/styles/mermaid.css"; +``` + +### Importing Constants + +```typescript +import { + CODE_THEMES, + EDITOR_THEMES, + DEFAULT_MERMAID_CONFIG, + type Themes, + type EditorTheme, +} from "@solid-markdown-wasm/example-shared/constants"; +``` + +### Using Shared Vite Config + +```typescript +import { + getManualChunks, + getAssetFileNames, + OPTIMIZE_DEPS_EXCLUDE, + SHARED_ASSETS_INCLUDE, + SHARED_ASSETS_INLINE_LIMIT, +} from "@solid-markdown-wasm/example-shared/vite-config"; + +export default defineConfig({ + build: { + rollupOptions: { + output: { + manualChunks: getManualChunks("monaco-editor"), + assetFileNames: getAssetFileNames(), + }, + }, + assetsInlineLimit: SHARED_ASSETS_INLINE_LIMIT, + }, + assetsInclude: SHARED_ASSETS_INCLUDE, + optimizeDeps: { + exclude: OPTIMIZE_DEPS_EXCLUDE, + }, +}); +``` + +## Benefits + +- **DRY Principle**: ~500+ lines of duplicated CSS eliminated +- **Single Source of Truth**: Themes and configuration defined once +- **Consistent Styling**: Both examples share identical visual styles +- **Easier Maintenance**: Updates apply to both examples automatically +- **Reduced Bundle Size**: Shared assets only exist once in the monorepo + +## Notes + +- This package is marked as `private` and is only intended for use within the solid-markdown-wasm monorepo +- The package uses TypeScript files directly (no build step required) since it's consumed within the monorepo +- CSS files use Tailwind v4 `@import` syntax +- The vite-config is provided as JavaScript (not TypeScript) to avoid Node.js loader issues diff --git a/packages/example-shared/package.json b/packages/example-shared/package.json new file mode 100644 index 0000000..8cde7a6 --- /dev/null +++ b/packages/example-shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "@solid-markdown-wasm/example-shared", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + "./assets/*": "./src/assets/*", + "./styles/*": "./src/styles/*", + "./constants": "./src/constants/index.ts", + "./vite-config": { + "import": "./vite/shared-config.js", + "types": "./vite/shared-config.d.ts" + } + }, + "devDependencies": { + "mermaid": "^11.12.2", + "typescript": "^5.8.3", + "vite": "^6.0.0", + "vite-plugin-wasm": "^3.5.0", + "@tailwindcss/vite": "^4.1.7" + } +} diff --git a/example-react/src/assets/haxiom.svg b/packages/example-shared/src/assets/haxiom.svg similarity index 100% rename from example-react/src/assets/haxiom.svg rename to packages/example-shared/src/assets/haxiom.svg diff --git a/example-react/src/assets/markdown_preview.md b/packages/example-shared/src/assets/markdown_preview.md similarity index 100% rename from example-react/src/assets/markdown_preview.md rename to packages/example-shared/src/assets/markdown_preview.md diff --git a/packages/example-shared/src/constants/index.ts b/packages/example-shared/src/constants/index.ts new file mode 100644 index 0000000..cf6767a --- /dev/null +++ b/packages/example-shared/src/constants/index.ts @@ -0,0 +1,191 @@ +/** + * Shared constants for solid-markdown-wasm examples + * Used by both React and Solid example applications + */ + +import type { MermaidConfig } from "mermaid"; + +export type MermaidTheme = "dark" | "default"; + +export type Themes = + | "1337" + | "OneHalfDark" + | "OneHalfLight" + | "Tomorrow" + | "agola-dark" + | "ascetic-white" + | "axar" + | "ayu-dark" + | "ayu-light" + | "ayu-mirage" + | "base16-atelierdune-light" + | "base16-ocean-dark" + | "base16-ocean-light" + | "bbedit" + | "boron" + | "charcoal" + | "cheerfully-light" + | "classic-modified" + | "demain" + | "dimmed-fluid" + | "dracula" + | "gray-matter-dark" + | "green" + | "gruvbox-dark" + | "gruvbox-light" + | "idle" + | "inspired-github" + | "ir-white" + | "kronuz" + | "material-dark" + | "material-light" + | "monokai" + | "nord" + | "nyx-bold" + | "one-dark" + | "railsbase16-green-screen-dark" + | "solarized-dark" + | "solarized-light" + | "subway-madrid" + | "subway-moscow" + | "two-dark" + | "visual-studio-dark" + | "zenburn"; + +/** All available code highlighting themes from the Rust markdown-renderer */ +export const CODE_THEMES: Themes[] = [ + "1337", + "OneHalfDark", + "OneHalfLight", + "Tomorrow", + "agola-dark", + "ascetic-white", + "axar", + "ayu-dark", + "ayu-light", + "ayu-mirage", + "base16-atelierdune-light", + "base16-ocean-dark", + "base16-ocean-light", + "bbedit", + "boron", + "charcoal", + "cheerfully-light", + "classic-modified", + "demain", + "dimmed-fluid", + "dracula", + "gray-matter-dark", + "green", + "gruvbox-dark", + "gruvbox-light", + "idle", + "inspired-github", + "ir-white", + "kronuz", + "material-dark", + "material-light", + "monokai", + "nord", + "nyx-bold", + "one-dark", + "railsbase16-green-screen-dark", + "solarized-dark", + "solarized-light", + "subway-madrid", + "subway-moscow", + "two-dark", + "visual-studio-dark", + "zenburn", +]; + +/** Available Monaco editor themes */ +export const EDITOR_THEMES = [ + { value: "vs", label: "Light" }, + { value: "vs-dark", label: "Dark" }, + { value: "hc-black", label: "High Contrast" }, +] as const; + +export type EditorTheme = (typeof EDITOR_THEMES)[number]["value"]; + +/** Function type for generating Mermaid configuration based on theme */ +export type MermaidConfigFn = (theme: MermaidTheme) => MermaidConfig; + +/** + * Default Mermaid configuration generator + * Provides consistent theming across both React and Solid examples + */ +export const DEFAULT_MERMAID_CONFIG: MermaidConfigFn = (theme) => { + const isDark = theme === "dark"; + const haxiomAccent = isDark ? "rgb(111, 255, 233)" : "#4f46e5"; + const haxiomFg = isDark ? "#000000" : "#ffffff"; + const textColor = isDark ? "#c9d1d9" : "#24292f"; + const bkgColor = isDark ? "#0d1117" : "#ffffff"; + + return { + startOnLoad: false, + theme: "base", + securityLevel: "loose", + fontFamily: "arial", + themeVariables: { + primaryColor: haxiomAccent, + primaryTextColor: haxiomFg, + primaryBorderColor: haxiomAccent, + lineColor: isDark ? haxiomAccent : "#444444", + secondaryColor: haxiomAccent, + tertiaryColor: isDark ? "#222222" : "#eeeeee", + mainBkg: bkgColor, + nodeBkg: haxiomAccent, + textColor, + nodeBorder: haxiomAccent, + clusterBkg: isDark ? "#161b22" : "#f6f8fa", + clusterBorder: isDark ? "#30363d" : "#d0d7de", + defaultLinkColor: isDark ? "#8b949e" : "#57606a", + titleColor: haxiomAccent, + edgeLabelBackground: isDark ? "#161b22" : "#ffffff", + fontFamily: "arial", + fontSize: "14px", + taskBkgColor: haxiomAccent, + taskTextColor: haxiomFg, + taskBorderColor: haxiomAccent, + activeTaskBkgColor: haxiomAccent, + activeTaskTextColor: haxiomFg, + doneTaskBkgColor: isDark ? "#333333" : "#d1d5db", + doneTaskTextColor: isDark ? "#888888" : "#4b5563", + critBkgColor: "#f87171", + critTextColor: "#ffffff", + todayLineColor: "#f87171", + gridColor: isDark ? "#30363d" : "#d0d7de", + sectionBkgColor: isDark ? "#161b22" : "#f6f8fa", + sectionBkgColor2: isDark ? "#0d1117" : "#ffffff", + }, + gantt: { + useMaxWidth: true, + htmlLabels: false, + }, + }; +}; + +/** Media query for detecting dark mode preference */ +export const DARK_MODE_MEDIA_QUERY = "(prefers-color-scheme: dark)"; + +/** + * Get the default code theme based on dark mode preference + */ +export function getDefaultCodeTheme(isDark: boolean): Themes { + return isDark ? "ayu-dark" : "ayu-light"; +} + +/** + * Get the default editor theme based on dark mode preference + */ +export function getDefaultEditorTheme(isDark: boolean): EditorTheme { + return isDark ? "vs-dark" : "vs"; +} + +/** + * Check if the user prefers dark mode + */ +export function getPrefersDark(): boolean { + return window.matchMedia(DARK_MODE_MEDIA_QUERY).matches; +} diff --git a/packages/example-shared/src/styles/base.css b/packages/example-shared/src/styles/base.css new file mode 100644 index 0000000..786f445 --- /dev/null +++ b/packages/example-shared/src/styles/base.css @@ -0,0 +1,101 @@ +/** + * Shared base styles for solid-markdown-wasm examples + * Includes spinner animation, root variables, and base typography + */ + +@import "tailwindcss"; + +:root { + color: #111827; + background: #ffffff; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +html, +body, +#root { + height: 100%; +} + +body { + margin: 0; +} + +code, +pre, +kbd, +samp { + font-family: ui-monospace, Iosevka, SFMono-Regular, Consolas, + "Liberation Mono", Menlo, monospace; +} + +/* Spinner Animation */ +.spinner { + width: 40px; + height: 40px; + margin: 20px auto; + border: 4px solid rgba(148, 163, 184, 0.25); + border-top-color: rgb(14, 165, 233); + border-radius: 9999px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* Theme Variables */ +[data-theme="dark"] { + color-scheme: dark; + --haxiom-accent-color: rgb(111, 255, 233); + --haxiom-fg-color: black; + --fgColor-default: #f0f6fc; + --fgColor-muted: #9198a1; + --fgColor-accent: #58a6ff; + --fgColor-success: #3fb950; + --fgColor-danger: #ff7b72; + --bgColor-default: #0d1117; + --bgColor-muted: #161b22; + --bgColor-subtle: #11161d; + --bgColor-neutral-muted: #656c7633; + --borderColor-default: #30363d; + --borderColor-muted: #3d444db3; + --borderColor-success: #238636; + --borderColor-danger: #f85149; + --blockquote-border: #3d444d; + --table-stripe: rgba(240, 246, 252, 0.02); + --inline-code-bg: rgba(110, 118, 129, 0.4); + --shadow-elevated: 0 16px 40px rgba(0, 0, 0, 0.25); +} + +[data-theme="light"] { + color-scheme: light; + --haxiom-accent-color: #4f46e5; + --haxiom-fg-color: white; + --fgColor-default: #1f2328; + --fgColor-muted: #59636e; + --fgColor-accent: #0969da; + --fgColor-success: #1a7f37; + --fgColor-danger: #cf222e; + --bgColor-default: #ffffff; + --bgColor-muted: #f6f8fa; + --bgColor-subtle: #f8fafc; + --bgColor-neutral-muted: #818b981f; + --borderColor-default: #d1d9e0; + --borderColor-muted: #d1d9e0b3; + --borderColor-success: #1f883d; + --borderColor-danger: #cf222e; + --blockquote-border: #d0d7de; + --table-stripe: rgba(31, 35, 40, 0.02); + --inline-code-bg: rgba(175, 184, 193, 0.2); + --shadow-elevated: 0 16px 40px rgba(15, 23, 42, 0.08); +} diff --git a/packages/example-shared/src/styles/code-blocks.css b/packages/example-shared/src/styles/code-blocks.css new file mode 100644 index 0000000..6fc1705 --- /dev/null +++ b/packages/example-shared/src/styles/code-blocks.css @@ -0,0 +1,128 @@ +/** + * Code block wrapper and header styles + * Enhanced code blocks with language labels and action buttons + */ + +/* Code block wrapper styling */ +.markdown-body .code-block-wrapper { + overflow: hidden; + margin-bottom: 1rem; + border: 1px solid var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-subtle); + box-shadow: var(--shadow-elevated); +} + +.markdown-body .code-block-wrapper pre { + margin: 0; + border: 0; + border-radius: 0; + box-shadow: none; +} + +/* Code block header */ +.markdown-body .code-block-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--borderColor-default); + background: var(--bgColor-muted); +} + +.markdown-body .code-block-language { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fgColor-muted); +} + +/* Button container */ +.markdown-body .code-block-buttons { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Action buttons */ +.markdown-body .code-block-copy, +.markdown-body .code-block-collapse, +.markdown-body .code-block-maximize, +.markdown-body .code-block-play { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.5rem; + color: var(--fgColor-muted); + background: transparent; + border: 1px solid var(--borderColor-muted); + border-radius: 0.35rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.markdown-body .code-block-copy:hover, +.markdown-body .code-block-collapse:hover, +.markdown-body .code-block-maximize:hover, +.markdown-body .code-block-play:hover { + color: var(--fgColor-default); + background: var(--bgColor-neutral-muted); + border-color: var(--borderColor-default); +} + +.markdown-body .code-block-copy:active, +.markdown-body .code-block-collapse:active, +.markdown-body .code-block-maximize:active, +.markdown-body .code-block-play:active { + transform: scale(0.95); +} + +.markdown-body .code-block-copy:disabled, +.markdown-body .code-block-collapse:disabled, +.markdown-body .code-block-maximize:disabled, +.markdown-body .code-block-play:disabled { + opacity: 0.6; + cursor: progress; +} + +/* Button icons */ +.markdown-body .code-block-copy svg, +.markdown-body .code-block-collapse svg, +.markdown-body .code-block-maximize svg, +.markdown-body .code-block-play svg { + width: 1rem; + height: 1rem; +} + +/* Hidden language data element */ +.markdown-body .code-lang-data { + display: none; +} + +/* Collapsed state */ +.markdown-body .code-block-wrapper.collapsed pre { + display: none; +} + +/* Copy success state */ +.markdown-body .code-block-copy.copied, +.markdown-body .code-block-copy.copied:hover { + color: var(--fgColor-success); + border-color: var(--borderColor-success); +} + +/* Mermaid button visibility logic */ +.markdown-body + .code-block-wrapper[data-mermaid-status="unrendered"] + .code-block-maximize, +.markdown-body + .code-block-wrapper[data-mermaid-status="code"] + .code-block-maximize, +.markdown-body .code-block-wrapper:not([data-mermaid-status]) .code-block-play, +.markdown-body + .code-block-wrapper:not([data-mermaid-status]) + .code-block-maximize { + display: none; +} diff --git a/packages/example-shared/src/styles/markdown.css b/packages/example-shared/src/styles/markdown.css new file mode 100644 index 0000000..086e201 --- /dev/null +++ b/packages/example-shared/src/styles/markdown.css @@ -0,0 +1,248 @@ +/** + * Markdown body styles for rendered content + * Applies to the .markdown-body class + */ + +.markdown-body { + margin: 0; + color: var(--fgColor-default); + background: transparent; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 16px; + line-height: 1.65; + word-wrap: break-word; +} + +.markdown-body > :first-child { + margin-top: 0; +} + +.markdown-body > :last-child { + margin-bottom: 0; +} + +/* Headings */ +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin: 1.6em 0 0.8em; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1, +.markdown-body h2 { + padding-bottom: 0.3em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body h1 { + font-size: 2rem; +} + +.markdown-body h2 { + font-size: 1.5rem; +} + +.markdown-body h3 { + font-size: 1.25rem; +} + +.markdown-body h4 { + font-size: 1rem; +} + +/* Paragraphs and block elements */ +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin: 0 0 1rem; +} + +/* Lists */ +.markdown-body ul, +.markdown-body ol { + padding-left: 1.5rem; +} + +.markdown-body ul { + list-style: disc; +} + +.markdown-body ol { + list-style: decimal; +} + +.markdown-body li + li { + margin-top: 0.25rem; +} + +.markdown-body li > p { + margin-top: 0.75rem; +} + +/* Blockquotes */ +.markdown-body blockquote { + padding: 0 1rem; + color: var(--fgColor-muted); + border-left: 0.25rem solid var(--blockquote-border); +} + +/* Horizontal rule */ +.markdown-body hr { + height: 0.25rem; + margin: 1.5rem 0; + background: var(--borderColor-default); + border: 0; +} + +/* Links */ +.markdown-body a { + color: var(--fgColor-accent); + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +/* Strong/Bold */ +.markdown-body strong { + font-weight: 600; +} + +/* Inline code */ +.markdown-body code { + padding: 0.15rem 0.35rem; + font-size: 0.875em; + background: var(--inline-code-bg); + border-radius: 0.375rem; +} + +/* Code blocks */ +.markdown-body pre { + overflow-x: auto; + padding: 0; + border: 1px solid var(--borderColor-default); + border-radius: 0 0 0.75rem 0.75rem; + background: var(--bgColor-subtle); + box-shadow: var(--shadow-elevated); +} + +.markdown-body pre code { + display: block; + padding: 1rem 1.25rem; + background: transparent; + border-radius: 0; +} + +/* Tables */ +.markdown-body table { + display: block; + width: max-content; + max-width: 100%; + overflow-x: auto; + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body table tr { + background: transparent; + border-top: 1px solid var(--borderColor-default); +} + +.markdown-body table tr:nth-child(2n) { + background: var(--table-stripe); +} + +.markdown-body table th, +.markdown-body table td { + padding: 0.5rem 0.85rem; + border: 1px solid var(--borderColor-default); +} + +/* Images and media */ +.markdown-body img, +.markdown-body svg, +.markdown-body video { + max-width: 100%; + box-sizing: border-box; +} + +.markdown-body iframe { + width: 100%; + max-width: 100%; + min-height: 315px; + border: 0; + border-radius: 0.75rem; +} + +.markdown-body .iframe-placeholder { + width: 100%; + min-height: 300px; + border: 1px dashed var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-muted); +} + +/* Details/Summary */ +.markdown-body details { + padding: 0.75rem 1rem; + border: 1px solid var(--borderColor-default); + border-radius: 0.75rem; + background: var(--bgColor-muted); +} + +.markdown-body summary { + cursor: pointer; + font-weight: 600; +} + +.markdown-body input[type="checkbox"] { + margin-right: 0.45rem; +} + +/* Math blocks */ +.markdown-body .math-code-block, +.markdown-body .math-display, +.markdown-body .math-inline { + overflow-x: auto; + scrollbar-width: thin; +} + +.markdown-body .math-code-block svg, +.markdown-body .math-display svg, +.markdown-body .math-inline svg { + max-width: none !important; +} + +.markdown-body .math-display { + margin: 1.25rem 0; +} + +.markdown-body .math-inline { + display: inline-block; + max-width: 100%; + vertical-align: middle; +} + +/* Task lists */ +.markdown-body .task-list-item { + list-style: none; +} + +.markdown-body .task-list-item input { + vertical-align: middle; +} + +.markdown-body .contains-task-list { + padding-left: 0; +} diff --git a/packages/example-shared/src/styles/mermaid.css b/packages/example-shared/src/styles/mermaid.css new file mode 100644 index 0000000..3e48ca8 --- /dev/null +++ b/packages/example-shared/src/styles/mermaid.css @@ -0,0 +1,113 @@ +/** + * Mermaid diagram styles and preview overlay + * Styles for rendered Mermaid diagrams and the fullscreen preview + */ + +/* Rendered mermaid container */ +.markdown-body pre[data-mermaid-status="rendered"] { + display: flex; + justify-content: center; + align-items: center; + min-width: 600px; + padding: 1rem 0; + background: transparent; + border: 0; + overflow-x: auto; +} + +.markdown-body pre[data-mermaid-status="rendered"] svg { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; +} + +/* Loading and error states */ +.mermaid-loading { + padding: 1rem; + color: var(--fgColor-muted); + text-align: center; +} + +.mermaid-error { + padding: 1rem; + color: var(--fgColor-danger); + background: transparent; + border: 1px solid var(--borderColor-danger); + border-radius: 0.5rem; +} + +/* Clickable mermaid diagrams */ +.mermaid-clickable pre[data-mermaid-processed="true"] svg { + cursor: zoom-in; +} + +/* Fullscreen preview overlay */ +dialog.mermaid-preview-overlay { + position: fixed; + inset: 0; + display: none; + width: 100vw; + height: 100vh; + max-width: none; + max-height: none; + margin: 0; + padding: 40px; + border: none; + background: rgba(0, 0, 0, 0.85); + box-sizing: border-box; + backdrop-filter: blur(8px); +} + +dialog.mermaid-preview-overlay[open] { + display: flex; + align-items: center; + justify-content: center; +} + +dialog.mermaid-preview-overlay::backdrop { + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(8px); +} + +.mermaid-preview-content { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + overflow: auto; +} + +.mermaid-preview-content > div { + display: flex; + align-items: center; + justify-content: center; + min-width: 100%; + min-height: 100%; +} + +.mermaid-preview-content svg { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + background: var(--bgColor-default); +} + +/* Close button for overlay */ +.mermaid-preview-close { + position: absolute; + top: 20px; + right: 20px; + padding: 0.5rem 0.75rem; + color: white; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + cursor: pointer; +} + +.mermaid-preview-close:hover { + background: rgba(255, 255, 255, 0.14); +} diff --git a/packages/example-shared/tsconfig.json b/packages/example-shared/tsconfig.json new file mode 100644 index 0000000..ea9a8b0 --- /dev/null +++ b/packages/example-shared/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/example-shared/vite/shared-config.d.ts b/packages/example-shared/vite/shared-config.d.ts new file mode 100644 index 0000000..09fae14 --- /dev/null +++ b/packages/example-shared/vite/shared-config.d.ts @@ -0,0 +1,17 @@ +/** + * Type declarations for @solid-markdown-wasm/example-shared/vite-config + */ + +declare module "@solid-markdown-wasm/example-shared/vite-config" { + export function getManualChunks( + chunkName: string, + ): (id: string) => string | undefined; + + export function getAssetFileNames(): (assetInfo: { name?: string }) => string; + + export const OPTIMIZE_DEPS_EXCLUDE: string[]; + + export const SHARED_ASSETS_INCLUDE: string[]; + + export const SHARED_ASSETS_INLINE_LIMIT: number; +} diff --git a/packages/example-shared/vite/shared-config.js b/packages/example-shared/vite/shared-config.js new file mode 100644 index 0000000..9faac95 --- /dev/null +++ b/packages/example-shared/vite/shared-config.js @@ -0,0 +1,44 @@ +/** + * Shared Vite configuration utilities for examples + */ + +/** + * Get manual chunks function for vendor code splitting + * @param {string} chunkName - The name of the chunk to split (e.g., "monaco-editor", "solid-monaco") + * @returns {Function} Manual chunks function for rollup + */ +export function getManualChunks(chunkName) { + return (id) => { + if (id.includes(`node_modules/${chunkName}`)) { + return chunkName; + } + }; +} + +/** + * Get asset file names function for proper WASM handling + * @returns {Function} Asset file names function for rollup + */ +export function getAssetFileNames() { + return (assetInfo) => { + if (assetInfo.name?.endsWith(".wasm")) { + return "assets/[name][extname]"; + } + return "assets/[name]-[hash][extname]"; + }; +} + +/** + * Dependencies to exclude from optimization (WASM packages) + */ +export const OPTIMIZE_DEPS_EXCLUDE = ["markdown-renderer"]; + +/** + * Assets include patterns + */ +export const SHARED_ASSETS_INCLUDE = ["**/*.wasm"]; + +/** + * Default build assets inline limit (0 = disable inlining) + */ +export const SHARED_ASSETS_INLINE_LIMIT = 0; From 6a929d5b6080013ce89dbae62c290465bed67e10 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Sat, 4 Apr 2026 16:18:42 +0800 Subject: [PATCH 4/6] refator: make sure that react example also uses lucide --- bun.lockb | Bin 220576 -> 221432 bytes example-react/package.json | 1 + example-react/src/previewEnhancements.ts | 144 +++++++++++------------ 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/bun.lockb b/bun.lockb index 2fae3386120c68c0c2f2b316864f87bfdc0ebc5e..dc6e56ccf6805361d0ffbc77f1c0edda65cb45ba 100755 GIT binary patch delta 3592 zcmd^?i&IqB9mmf(mlbwZ+~xh2YghL${Myl+ZOornZXa&&zK$FwR%y<#|N&Q zx%$NE3!N+KHAW?~p=P3}DLA~L-s}o&XUxdhmbJ^4KD7?9`KXUWeU{>Xd&^f(LM$2l z9O_}PN0n{Xjh0g0tk1BxGuC%7mWEh8Y&dKc?0DGy$Th<@slIk~9=AMiHowho&ine; z%QM4w-b9+0)gY)-{)$8#+6*P(CZUaBW*b`JY)WR#H9DYc*o^TufiFzf~EoFy&V+d4Zh}U^a zA%9lCf5OfCb=OI^h&!V1JXvK}c!aTHZx=7?(LHYdzP_}_Z7_E-_Ou?qt%Cnt9|FCg zJ5ITcmM&kXA-cs;D&}?R=TBAfJ-VgW&98MX>ebCgtRcHC7}NA`FGOzUsNF}6>x7Y9)1;|n?Se(2$M@Cz=}qIGXx@}r4b;i z31EFAK$PqwaF#%76F`hy(*&?#H^4;#aniaQAiWvjx!nK>vY)`83FJ2eB+6ej18i>r z_>@4B%xwWEY6Wxnr5^6`^=w%oFh_jpdHiq%0LxTgdD^LIr2ujqg0|39MtDb++h-Z9eeKLSjR z1+*g=TkMoIQ-t$JqYr$A*r(p>x$h_j5h0QirrLb7n?0AG!f`s_`$Ftu(TG5&{@UeRGt;A13r~_ z*fFq4Py`l{)-zemH|Ia<=JF^A(qLmLYWA?2hnYltymU(8VFs59K~$#X=$nCN^238DGya5 z*$|D6$`qBJ1Aj6`n+!|Wolc%ham8rc5zB+(z-So?6~ng#I}h2RBE_b_CsA?uSTM^+ zg*v5DEc-B~5+`CfSRv$4GcHkm6j7RDQx$W7ITb6F7KaEecBzmPp%R2BrChN`;ZIep zLb2_1PL#$rIV3ls{4L3FPK%Z)Da zLVPw{x)`+h2@ox~6-t7f(poMu4fJhLAgjwoq3LWT&V+Kgn&y+{=`I&9jkVHk_y|P% z;6<#!4rCbN(_S${6Xde*iOHMk7P*2jJ?9=yy9`FLvCt>*!(i!l-w8hzmbODEMBP58 z;sfy4!2dBscexeuM2NOi5)=c`cB5_h4B~e=+sql>ta9%W5hwS!v|!JH zx!Q&x)9>oAOHfYNNqeo9#F?B^t3{iFmiX|YC30=8cHMMY)rXgO)8y90TAZn@-j}qn KUcR+hyZbNe=8wGq delta 3141 zcmds(iEq?J6vt;~yVzZN>~34y(w5R=3rlICExlQ{w6(t~kwcII1>^_>)F^lak;K** zfr^RXFFKT?-6l}wQcHoY0yRxQuBeb`yh5~c2&g~_R6GE`Z}&CvZ*a1&@4e4^^L{ha zncr{j^hEj(MQ+ITEV_N-)wB1Wc&u{ntZ9}>GfEbg98+JJPxTc}oLzIZ>x*TMxfVgq zv#~B!Jvy%ArTLD$zjp~?6Qc3C#S0d_fLQHrAtK=WpoyPEJQn%@Yqy!`^hvil%V~J- z?-3#kv8&K=&}X0{p?4tH0o@EuwGui{I@dZn=cnH=WtchjpkbSi>II0n%(EFjxz_AG zXvl9&Pr#5aQ+63|cZFGN5e_qZKJdvE=Eg2Vo!@JI-&G@%%(RaUYr_XZ)R^bGs;tLg z%{MVi{rX zJY>jvv+t0h+B?j$!!_2QJA^0;mPj^F9ya70(|N?OZr&%vGp2V*LdF6;< z+tL{->N4|6ed=7N`EqxS^q4)}hJ2>e_NnP`M&ofel@Rn#xD74MyFbM@gu(j<9u3BV z5KLi(rtDZ~D|8}tVNLld?31SaRA}m-#hf&m9{i#HxzLn1mUTX~gkNA0HNik30Dhu` z6UtbRCy4f6O&z@)_i^0U*?gw{e?G&$-s%JTqLve-L+5Rhb^2Q^BlSC*09?A4z`9KU z4>beC>W$3+CCvc02@KH_HUnHHuwyeog6<=*X*0m%TL6aYwk-g)TL7$E0h0C9tpIlj z93U`E%WVMfZv|Mq4PdzLATV^{TnKd(7deM^$|Yu(bZ?Q0@&3Xg+M-t=)Gjj{1bZZ>Utt#9;>@n{(JIn>ALQSjgAC*B03NzlQh155U8 zeU?hx=X58K2|L)3LMDyiS!R-14A^jHN~e{egoQ(RkBS#y(_F)t(SX;(re82q;=6%x%X z74~u1v`!A~ELAZLLphWNv==tDBxw4FrbD8!ZnT7HoRUMgTs?)A#5OjLADH6cZkVvN)8@Y!p}}Y-*#KWyAgwLJOaxWf`WG z%b^^g2u>fvEEntqgcf-$7=_0`Xn54}Id3fN(HL$9bOEzG*g4GTtkLN5A#q@|422;U zMW0J?3PKMn(rp-%x0v$_fv66(5@toPi+QT0VEBn*NGY>&-A5(Hb9yO4Gp|u?fNrH^idDSZ0N?*n?dO@`+ws-Mv*gu)( zkrg;mtu_o!p)KczI3RCf0h%xoI$U%T!u6^tsz4u|q8tq___Sr;Ku!iUZD}Wj@?Qs| zcrWY~u$Mz24pgV2!}4oQH}k)fmXJmUS3IM^`|>a!BE3K9#y0h;pTAw!gC2tI%c z670o;28vVRfuf19m!adwh@+Eu3id!=3jAaUt~25o2au|mCJSy970W6cmH&+%<` z;Ff#3GxZVQlMlb9!SBmZJ_Zz<(Ky5wA;~Q>jR^9D$DLcOO`#oxGYyYcd3Lx-E>R7 z)UQ-qq62D%<)v`@lzMDcw59d>lP4_c>2YuTk@4_;@5WnAu@$>lzFW6%{Ey%E-}!ps iN1Qivjy^Tda>>4KPN<)M4)uG`ewlqSSF8D!{(k{gmJC7w diff --git a/example-react/package.json b/example-react/package.json index 0015a2e..c6a8ef8 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -13,6 +13,7 @@ "dependencies": { "@monaco-editor/react": "^4.7.0", "@solid-markdown-wasm/example-shared": "file:../packages/example-shared", + "lucide-react": "^0.479.0", "markdown-renderer": "file:../markdown-renderer/pkg/", "mermaid": "^11.12.2", "monaco-editor": "^0.48.0", diff --git a/example-react/src/previewEnhancements.ts b/example-react/src/previewEnhancements.ts index 2a32a18..19c0c35 100644 --- a/example-react/src/previewEnhancements.ts +++ b/example-react/src/previewEnhancements.ts @@ -1,37 +1,23 @@ +import { + Check, + ChevronsDownUp, + ChevronsUpDown, + Code, + Copy, + Maximize, + Play, +} from "lucide-react"; import mermaid, { type MermaidConfig } from "mermaid"; +import { createElement } from "react"; +import { createRoot } from "react-dom/client"; const CACHE_KEY = "example-react-mermaid-cache-v1"; const MAX_CACHE_SIZE = 50; const STYLE_VERSION = "v1"; -type MermaidTheme = "dark" | "default"; +const ICON_SIZE = 16; -type IconName = - | "check" - | "code" - | "collapse" - | "copy" - | "expand" - | "maximize" - | "play"; - -const ICON_PATHS: Record = { - check: ["M20 6 9 17l-5-5"], - code: ["m18 16 4-4-4-4", "m6 8-4 4 4 4", "m14.5 4-5 16"], - collapse: ["m7 20 5-5 5 5", "m7 4 5 5 5-5", "m19 9-7 7-7-7"], - copy: [ - "M16 4H8a2 2 0 0 0-2 2v12", - "M20 8H12a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2Z", - ], - expand: ["m7 15 5 5 5-5", "m7 9 5-5 5 5", "m5 15 7-7 7 7"], - maximize: [ - "M8 3H5a2 2 0 0 0-2 2v3", - "M21 8V5a2 2 0 0 0-2-2h-3", - "M3 16v3a2 2 0 0 0 2 2h3", - "M16 21h3a2 2 0 0 0 2-2v-3", - ], - play: ["M6 4v16l13-8z"], -}; +type MermaidTheme = "dark" | "default"; const copyResetTimers = new WeakMap(); const expandedMermaidSources = new Set(); @@ -157,27 +143,22 @@ function rememberMermaidSvg(key: string, svg: string): void { saveCache(mermaidCache); } -function createIcon(name: IconName): SVGSVGElement { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("fill", "none"); - svg.setAttribute("stroke", "currentColor"); - svg.setAttribute("stroke-width", "2"); - svg.setAttribute("stroke-linecap", "round"); - svg.setAttribute("stroke-linejoin", "round"); - svg.setAttribute("aria-hidden", "true"); - - for (const pathValue of ICON_PATHS[name]) { - const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - path.setAttribute("d", pathValue); - svg.appendChild(path); - } - - return svg; -} - -function setButtonIcon(button: HTMLButtonElement, name: IconName): void { - button.replaceChildren(createIcon(name)); +type IconComponent = + | typeof Check + | typeof ChevronsDownUp + | typeof ChevronsUpDown + | typeof Code + | typeof Copy + | typeof Maximize + | typeof Play; + +function setButtonIcon(button: HTMLButtonElement, Icon: IconComponent): void { + const root = createRoot(button); + root.render( + createElement(Icon, { + size: ICON_SIZE, + }), + ); } function ensureButton( @@ -260,23 +241,7 @@ function updateWrapperButtons( } const controls = buttonContainer; - const collapseButton = ensureButton(controls, "code-block-collapse"); - collapseButton.setAttribute( - "aria-label", - wrapper.classList.contains("collapsed") ? "Expand code" : "Collapse code", - ); - setButtonIcon( - collapseButton, - wrapper.classList.contains("collapsed") ? "expand" : "collapse", - ); - - const copyButton = ensureButton(controls, "code-block-copy"); - copyButton.setAttribute("aria-label", "Copy code"); - setButtonIcon( - copyButton, - copyButton.classList.contains("copied") ? "check" : "copy", - ); - + // Check if this is a mermaid block first to determine button order const isMermaid = getWrapperLanguage(wrapper).toLowerCase() === "mermaid"; wrapper.classList.toggle("mermaid-clickable", isMermaid); @@ -284,30 +249,65 @@ function updateWrapperButtons( const existingMaximize = controls.querySelector(".code-block-maximize"); if (!isMermaid) { + // Non-mermaid blocks: remove mermaid buttons, keep collapse + copy existingPlay?.remove(); existingMaximize?.remove(); delete wrapper.dataset.mermaidStatus; + + // Create collapse and copy buttons in order + const collapseButton = ensureButton(controls, "code-block-collapse"); + collapseButton.setAttribute( + "aria-label", + wrapper.classList.contains("collapsed") ? "Expand code" : "Collapse code", + ); + setButtonIcon( + collapseButton, + wrapper.classList.contains("collapsed") ? ChevronsUpDown : ChevronsDownUp, + ); + + const copyButton = ensureButton(controls, "code-block-copy"); + copyButton.setAttribute("aria-label", "Copy code"); + setButtonIcon(copyButton, Copy); return; } + // For mermaid blocks, buttons should be in order: maximize, play, collapse, copy + // This way play appears first when maximize is hidden by CSS + if (!wrapper.dataset.mermaidStatus) { wrapper.dataset.mermaidStatus = immediateRenderMermaid ? "rendered" : "unrendered"; } - const playButton = ensureButton(controls, "code-block-play"); - const maximizeButton = ensureButton(controls, "code-block-maximize"); const rendered = wrapper.dataset.mermaidStatus === "rendered"; + // Create mermaid buttons first (maximize, then play) + const maximizeButton = ensureButton(controls, "code-block-maximize"); + maximizeButton.setAttribute("aria-label", "Open Mermaid preview"); + setButtonIcon(maximizeButton, Maximize); + + const playButton = ensureButton(controls, "code-block-play"); playButton.setAttribute( "aria-label", rendered ? "Show code" : "Render diagram", ); - setButtonIcon(playButton, rendered ? "code" : "play"); + setButtonIcon(playButton, rendered ? Code : Play); - maximizeButton.setAttribute("aria-label", "Open Mermaid preview"); - setButtonIcon(maximizeButton, "maximize"); + // Then create the standard buttons (collapse, copy) + const collapseButton = ensureButton(controls, "code-block-collapse"); + collapseButton.setAttribute( + "aria-label", + wrapper.classList.contains("collapsed") ? "Expand code" : "Collapse code", + ); + setButtonIcon( + collapseButton, + wrapper.classList.contains("collapsed") ? ChevronsUpDown : ChevronsDownUp, + ); + + const copyButton = ensureButton(controls, "code-block-copy"); + copyButton.setAttribute("aria-label", "Copy code"); + setButtonIcon(copyButton, Copy); } function renderSvgMarkup(markup: string): DocumentFragment { @@ -450,11 +450,11 @@ function flashCopySuccess(button: HTMLButtonElement): void { } button.classList.add("copied"); - setButtonIcon(button, "check"); + setButtonIcon(button, Check); const timeoutId = window.setTimeout(() => { button.classList.remove("copied"); - setButtonIcon(button, "copy"); + setButtonIcon(button, Copy); copyResetTimers.delete(button); }, 2000); From 837e4fe69fc17eec25f809fc8e728b60fd763a16 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Sat, 4 Apr 2026 16:33:10 +0800 Subject: [PATCH 5/6] refator: ensure that the colors are the same for mermaid --- example-react/src/App.tsx | 6 ++--- example-react/src/previewEnhancements.ts | 11 +++++--- example/src/App.tsx | 24 ++--------------- .../example-shared/src/constants/index.ts | 27 +++++++++++++++++++ 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/example-react/src/App.tsx b/example-react/src/App.tsx index 2f8528e..3674907 100644 --- a/example-react/src/App.tsx +++ b/example-react/src/App.tsx @@ -3,8 +3,8 @@ import haxiomLogo from "@solid-markdown-wasm/example-shared/assets/haxiom.svg"; import initialMarkdown from "@solid-markdown-wasm/example-shared/assets/markdown_preview.md?raw"; import { CODE_THEMES, - DEFAULT_MERMAID_CONFIG, EDITOR_THEMES, + EXAMPLE_MERMAID_CONFIG, type EditorTheme, type Themes, getDefaultCodeTheme, @@ -155,7 +155,7 @@ function App() { try { await applyPreviewEnhancements(preview, { immediateRenderMermaid, - mermaidConfig: DEFAULT_MERMAID_CONFIG, + mermaidConfig: EXAMPLE_MERMAID_CONFIG, }); } catch (error) { if (!disposed) { @@ -179,7 +179,7 @@ function App() { const handleClick = (event: MouseEvent) => { void handlePreviewInteraction(preview, event, { - mermaidConfig: DEFAULT_MERMAID_CONFIG, + mermaidConfig: EXAMPLE_MERMAID_CONFIG, onOpenMermaidPreview: setFullScreenSvg, }); }; diff --git a/example-react/src/previewEnhancements.ts b/example-react/src/previewEnhancements.ts index 19c0c35..976f80f 100644 --- a/example-react/src/previewEnhancements.ts +++ b/example-react/src/previewEnhancements.ts @@ -9,17 +9,18 @@ import { } from "lucide-react"; import mermaid, { type MermaidConfig } from "mermaid"; import { createElement } from "react"; -import { createRoot } from "react-dom/client"; +import { type Root, createRoot } from "react-dom/client"; const CACHE_KEY = "example-react-mermaid-cache-v1"; const MAX_CACHE_SIZE = 50; -const STYLE_VERSION = "v1"; +const STYLE_VERSION = "v2"; const ICON_SIZE = 16; type MermaidTheme = "dark" | "default"; const copyResetTimers = new WeakMap(); +const iconRoots = new WeakMap(); const expandedMermaidSources = new Set(); export type MermaidConfigFn = (theme: MermaidTheme) => MermaidConfig; @@ -153,7 +154,11 @@ type IconComponent = | typeof Play; function setButtonIcon(button: HTMLButtonElement, Icon: IconComponent): void { - const root = createRoot(button); + const existingRoot = iconRoots.get(button); + const root = existingRoot ?? createRoot(button); + if (!existingRoot) { + iconRoots.set(button, root); + } root.render( createElement(Icon, { size: ICON_SIZE, diff --git a/example/src/App.tsx b/example/src/App.tsx index f84ab29..365e1e1 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,8 +2,8 @@ import haxiomLogo from "@solid-markdown-wasm/example-shared/assets/haxiom.svg"; import initialMarkdown from "@solid-markdown-wasm/example-shared/assets/markdown_preview.md?raw"; import { CODE_THEMES, - DEFAULT_MERMAID_CONFIG, EDITOR_THEMES, + EXAMPLE_MERMAID_CONFIG, type Themes, } from "@solid-markdown-wasm/example-shared/constants"; import { @@ -255,27 +255,7 @@ const App: Component = () => { fallback={} onLoaded={() => console.log("WASM Loaded")} immediateRenderMermaid={immediateMermaid()} - mermaidConfig={(theme) => { - const isDark = theme === "dark"; - const textColor = isDark ? "#c9d1d9" : "#24292f"; - const nodeBkg = isDark ? "#BB2528" : "#fee2e2"; // Dark red vs light red - const nodeText = isDark ? "#ffffff" : "#991b1b"; // White vs dark red - - return { - ...DEFAULT_MERMAID_CONFIG(theme), - themeVariables: { - ...DEFAULT_MERMAID_CONFIG(theme).themeVariables, - primaryColor: nodeBkg, - nodeBkg: nodeBkg, - primaryTextColor: nodeText, - nodeTextColor: nodeText, - textColor: textColor, - lineColor: "#FF0000", - secondaryColor: "#006100", - tertiaryColor: isDark ? "#222222" : "#eeeeee", - }, - }; - }} + mermaidConfig={EXAMPLE_MERMAID_CONFIG} />
diff --git a/packages/example-shared/src/constants/index.ts b/packages/example-shared/src/constants/index.ts index cf6767a..b90b6ea 100644 --- a/packages/example-shared/src/constants/index.ts +++ b/packages/example-shared/src/constants/index.ts @@ -166,6 +166,33 @@ export const DEFAULT_MERMAID_CONFIG: MermaidConfigFn = (theme) => { }; }; +/** + * Mermaid configuration used by both example apps. + * Matches the previous Solid example styling (red links, stronger contrast). + */ +export const EXAMPLE_MERMAID_CONFIG: MermaidConfigFn = (theme) => { + const isDark = theme === "dark"; + const baseConfig = DEFAULT_MERMAID_CONFIG(theme); + const nodeBkg = isDark ? "#BB2528" : "#fee2e2"; + const nodeText = isDark ? "#ffffff" : "#991b1b"; + const textColor = isDark ? "#c9d1d9" : "#24292f"; + + return { + ...baseConfig, + themeVariables: { + ...baseConfig.themeVariables, + primaryColor: nodeBkg, + nodeBkg, + primaryTextColor: nodeText, + nodeTextColor: nodeText, + textColor, + lineColor: "#FF0000", + secondaryColor: "#006100", + tertiaryColor: isDark ? "#222222" : "#eeeeee", + }, + }; +}; + /** Media query for detecting dark mode preference */ export const DARK_MODE_MEDIA_QUERY = "(prefers-color-scheme: dark)"; From 07bb19620a9c28fb48bd5a59e54df77929ca17e4 Mon Sep 17 00:00:00 2001 From: Budi Syahiddin Date: Sat, 4 Apr 2026 17:42:19 +0800 Subject: [PATCH 6/6] refactor: make sure that the blockquotes for info all these are correct --- example-react/src/previewEnhancements.ts | 15 ++++ packages/example-shared/src/styles/base.css | 20 ++++++ .../example-shared/src/styles/markdown.css | 69 +++++++++++++++++++ .../example-shared/src/styles/mermaid.css | 18 +++++ 4 files changed, 122 insertions(+) diff --git a/example-react/src/previewEnhancements.ts b/example-react/src/previewEnhancements.ts index 976f80f..69c3923 100644 --- a/example-react/src/previewEnhancements.ts +++ b/example-react/src/previewEnhancements.ts @@ -258,6 +258,10 @@ function updateWrapperButtons( existingPlay?.remove(); existingMaximize?.remove(); delete wrapper.dataset.mermaidStatus; + const pre = wrapper.querySelector("pre"); + if (pre instanceof HTMLElement) { + delete pre.dataset.mermaidStatus; + } // Create collapse and copy buttons in order const collapseButton = ensureButton(controls, "code-block-collapse"); @@ -284,6 +288,10 @@ function updateWrapperButtons( ? "rendered" : "unrendered"; } + const pre = wrapper.querySelector("pre"); + if (pre instanceof HTMLElement && !pre.dataset.mermaidStatus) { + pre.dataset.mermaidStatus = wrapper.dataset.mermaidStatus; + } const rendered = wrapper.dataset.mermaidStatus === "rendered"; @@ -387,12 +395,14 @@ async function renderMermaidIntoWrapper( pre.replaceChildren(renderSvgMarkup(svg)); pre.dataset.mermaidSource = source; pre.dataset.mermaidProcessed = "true"; + pre.dataset.mermaidStatus = "rendered"; wrapper.dataset.mermaidStatus = "rendered"; expandedMermaidSources.add(sourceKey); } catch (error) { if (!pre.isConnected || !root.contains(pre)) { return; } + pre.dataset.mermaidStatus = "unrendered"; wrapper.dataset.mermaidStatus = "unrendered"; setMermaidError( pre, @@ -420,6 +430,7 @@ function showMermaidSource(wrapper: HTMLElement): void { pre.replaceChildren(code); pre.dataset.mermaidSource = source; pre.dataset.mermaidProcessed = "false"; + pre.dataset.mermaidStatus = "code"; wrapper.dataset.mermaidStatus = "code"; expandedMermaidSources.delete(simpleHash(source)); updateWrapperButtons(wrapper, false); @@ -506,6 +517,10 @@ export async function applyPreviewEnhancements( if (!wrapper.dataset.mermaidStatus) { wrapper.dataset.mermaidStatus = "unrendered"; } + const pre = wrapper.querySelector("pre"); + if (pre instanceof HTMLElement) { + pre.dataset.mermaidStatus = "unrendered"; + } updateWrapperButtons(wrapper, options.immediateRenderMermaid); } } diff --git a/packages/example-shared/src/styles/base.css b/packages/example-shared/src/styles/base.css index 786f445..1194a0d 100644 --- a/packages/example-shared/src/styles/base.css +++ b/packages/example-shared/src/styles/base.css @@ -56,11 +56,16 @@ samp { /* Theme Variables */ [data-theme="dark"] { color-scheme: dark; + --base-size-8: 0.5rem; + --base-size-16: 1rem; + --base-text-weight-medium: 500; --haxiom-accent-color: rgb(111, 255, 233); --haxiom-fg-color: black; --fgColor-default: #f0f6fc; --fgColor-muted: #9198a1; --fgColor-accent: #58a6ff; + --fgColor-done: #a371f7; + --fgColor-attention: #d29922; --fgColor-success: #3fb950; --fgColor-danger: #ff7b72; --bgColor-default: #0d1117; @@ -69,8 +74,13 @@ samp { --bgColor-neutral-muted: #656c7633; --borderColor-default: #30363d; --borderColor-muted: #3d444db3; + --borderColor-accent-emphasis: #1f6feb; + --borderColor-done-emphasis: #8957e5; + --borderColor-attention-emphasis: #9e6a03; --borderColor-success: #238636; + --borderColor-success-emphasis: #238636; --borderColor-danger: #f85149; + --borderColor-danger-emphasis: #da3633; --blockquote-border: #3d444d; --table-stripe: rgba(240, 246, 252, 0.02); --inline-code-bg: rgba(110, 118, 129, 0.4); @@ -79,11 +89,16 @@ samp { [data-theme="light"] { color-scheme: light; + --base-size-8: 0.5rem; + --base-size-16: 1rem; + --base-text-weight-medium: 500; --haxiom-accent-color: #4f46e5; --haxiom-fg-color: white; --fgColor-default: #1f2328; --fgColor-muted: #59636e; --fgColor-accent: #0969da; + --fgColor-done: #8250df; + --fgColor-attention: #9a6700; --fgColor-success: #1a7f37; --fgColor-danger: #cf222e; --bgColor-default: #ffffff; @@ -92,8 +107,13 @@ samp { --bgColor-neutral-muted: #818b981f; --borderColor-default: #d1d9e0; --borderColor-muted: #d1d9e0b3; + --borderColor-accent-emphasis: #0969da; + --borderColor-done-emphasis: #8250df; + --borderColor-attention-emphasis: #bf8700; --borderColor-success: #1f883d; + --borderColor-success-emphasis: #1f883d; --borderColor-danger: #cf222e; + --borderColor-danger-emphasis: #cf222e; --blockquote-border: #d0d7de; --table-stripe: rgba(31, 35, 40, 0.02); --inline-code-bg: rgba(175, 184, 193, 0.2); diff --git a/packages/example-shared/src/styles/markdown.css b/packages/example-shared/src/styles/markdown.css index 086e201..36bfb52 100644 --- a/packages/example-shared/src/styles/markdown.css +++ b/packages/example-shared/src/styles/markdown.css @@ -246,3 +246,72 @@ .markdown-body .contains-task-list { padding-left: 0; } + +.markdown-body .markdown-alert { + padding: var(--base-size-8) var(--base-size-16); + margin-bottom: var(--base-size-16); + color: inherit; + border-left: .25em solid var(--borderColor-default); +} + +.markdown-body .markdown-alert > :first-child { + margin-top: 0; +} + +.markdown-body .markdown-alert > :last-child { + margin-bottom: 0; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: var(--base-text-weight-medium, 500); + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: var(--base-text-weight-medium, 500); + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert.markdown-alert-note { + border-left-color: var(--borderColor-accent-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { + color: var(--fgColor-accent); +} + +.markdown-body .markdown-alert.markdown-alert-important { + border-left-color: var(--borderColor-done-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { + color: var(--fgColor-done); +} + +.markdown-body .markdown-alert.markdown-alert-warning { + border-left-color: var(--borderColor-attention-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { + color: var(--fgColor-attention); +} + +.markdown-body .markdown-alert.markdown-alert-tip { + border-left-color: var(--borderColor-success-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { + color: var(--fgColor-success); +} + +.markdown-body .markdown-alert.markdown-alert-caution { + border-left-color: var(--borderColor-danger-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { + color: var(--fgColor-danger); +} diff --git a/packages/example-shared/src/styles/mermaid.css b/packages/example-shared/src/styles/mermaid.css index 3e48ca8..ebb2261 100644 --- a/packages/example-shared/src/styles/mermaid.css +++ b/packages/example-shared/src/styles/mermaid.css @@ -15,6 +15,17 @@ overflow-x: auto; } +.markdown-body .code-block-wrapper[data-mermaid-status="rendered"] pre { + display: flex; + justify-content: center; + align-items: center; + min-width: 600px; + padding: 1rem 0; + background: transparent; + border: 0; + overflow-x: auto; +} + .markdown-body pre[data-mermaid-status="rendered"] svg { display: block; margin: 0 auto; @@ -22,6 +33,13 @@ height: auto; } +.markdown-body .code-block-wrapper[data-mermaid-status="rendered"] pre svg { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; +} + /* Loading and error states */ .mermaid-loading { padding: 1rem;