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/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
diff --git a/bun.lockb b/bun.lockb
index f832693..dc6e56c 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/example-react/index.html b/example-react/index.html
new file mode 100644
index 0000000..df2395e
--- /dev/null
+++ b/example-react/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ solid-markdown-wasm raw WASM React example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/example-react/package.json b/example-react/package.json
new file mode 100644
index 0000000..c6a8ef8
--- /dev/null
+++ b/example-react/package.json
@@ -0,0 +1,33 @@
+{
+ "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",
+ "@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",
+ "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..3674907
--- /dev/null
+++ b/example-react/src/App.tsx
@@ -0,0 +1,442 @@
+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,
+ EDITOR_THEMES,
+ EXAMPLE_MERMAID_CONFIG,
+ 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 {
+ applyPreviewEnhancements,
+ handlePreviewInteraction,
+} from "./previewEnhancements";
+
+const MEDIA_QUERY = "(prefers-color-scheme: dark)";
+
+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: EXAMPLE_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: EXAMPLE_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
+
+
/
+
solid-markdown-wasm
+
+ React raw WASM
+
+
+
+
+
+
+ Editor Theme:
+
+
+ setEditorTheme(event.currentTarget.value as EditorTheme)
+ }
+ >
+ {EDITOR_THEMES.map((theme) => (
+
+ {theme.label}
+
+ ))}
+
+
+
+
+
+ Code Block Theme:
+
+
+ setCodeTheme(event.currentTarget.value as Themes)
+ }
+ >
+ {CODE_THEMES.map((theme) => (
+
+ {theme}
+
+ ))}
+
+
+
+
+
+ Auto-render Mermaid:
+
+
+ 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);
+ }
+ }}
+ >
+ setFullScreenSvg(null)}
+ >
+ Close
+
+ event.stopPropagation()}
+ >
+
+
+
+ ) : null}
+
+ );
+}
+
+export default App;
diff --git a/example-react/src/index.css b/example-react/src/index.css
new file mode 100644
index 0000000..c9c51ea
--- /dev/null
+++ b/example-react/src/index.css
@@ -0,0 +1,17 @@
+/**
+ * React example styles - imports from shared package
+ */
+
+/* Shared base styles (includes Tailwind, spinner, theme variables) */
+@import "@solid-markdown-wasm/example-shared/styles/base.css";
+
+/* Markdown rendering styles */
+@import "@solid-markdown-wasm/example-shared/styles/markdown.css";
+
+/* Code block enhancements */
+@import "@solid-markdown-wasm/example-shared/styles/code-blocks.css";
+
+/* Mermaid diagram styles */
+@import "@solid-markdown-wasm/example-shared/styles/mermaid.css";
+
+/* React-specific overrides (if any) can go here */
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..40272a9
--- /dev/null
+++ b/example-react/src/monaco.ts
@@ -0,0 +1,36 @@
+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";
+(
+ 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..69c3923
--- /dev/null
+++ b/example-react/src/previewEnhancements.ts
@@ -0,0 +1,630 @@
+import {
+ Check,
+ ChevronsDownUp,
+ ChevronsUpDown,
+ Code,
+ Copy,
+ Maximize,
+ Play,
+} from "lucide-react";
+import mermaid, { type MermaidConfig } from "mermaid";
+import { createElement } from "react";
+import { type Root, createRoot } from "react-dom/client";
+
+const CACHE_KEY = "example-react-mermaid-cache-v1";
+const MAX_CACHE_SIZE = 50;
+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;
+
+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);
+}
+
+type IconComponent =
+ | typeof Check
+ | typeof ChevronsDownUp
+ | typeof ChevronsUpDown
+ | typeof Code
+ | typeof Copy
+ | typeof Maximize
+ | typeof Play;
+
+function setButtonIcon(button: HTMLButtonElement, Icon: IconComponent): void {
+ const existingRoot = iconRoots.get(button);
+ const root = existingRoot ?? createRoot(button);
+ if (!existingRoot) {
+ iconRoots.set(button, root);
+ }
+ root.render(
+ createElement(Icon, {
+ size: ICON_SIZE,
+ }),
+ );
+}
+
+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;
+
+ // Check if this is a mermaid block first to determine button order
+ 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) {
+ // Non-mermaid blocks: remove mermaid buttons, keep collapse + copy
+ 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");
+ 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 pre = wrapper.querySelector("pre");
+ if (pre instanceof HTMLElement && !pre.dataset.mermaidStatus) {
+ pre.dataset.mermaidStatus = wrapper.dataset.mermaidStatus;
+ }
+
+ 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);
+
+ // 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 {
+ 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";
+ 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,
+ 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";
+ pre.dataset.mermaidStatus = "code";
+ 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";
+ }
+ const pre = wrapper.querySelector("pre");
+ if (pre instanceof HTMLElement) {
+ pre.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..68ea4ef
--- /dev/null
+++ b/example-react/vite.config.ts
@@ -0,0 +1,28 @@
+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";
+import wasm from "vite-plugin-wasm";
+
+export default defineConfig({
+ plugins: [wasm(), react(), tailwindcss()],
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks: getManualChunks("monaco-editor"),
+ assetFileNames: getAssetFileNames(),
+ },
+ },
+ assetsInlineLimit: SHARED_ASSETS_INLINE_LIMIT,
+ },
+ assetsInclude: SHARED_ASSETS_INCLUDE,
+ optimizeDeps: {
+ 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..365e1e1 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,
+ EDITOR_THEMES,
+ EXAMPLE_MERMAID_CONFIG,
+ 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 = () => (
@@ -306,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/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 ca65765..f9c81e3 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", "packages/*"],
"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/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/src/assets/haxiom.svg b/packages/example-shared/src/assets/haxiom.svg
similarity index 100%
rename from example/src/assets/haxiom.svg
rename to packages/example-shared/src/assets/haxiom.svg
diff --git a/example/src/assets/markdown_preview.md b/packages/example-shared/src/assets/markdown_preview.md
similarity index 100%
rename from example/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..b90b6ea
--- /dev/null
+++ b/packages/example-shared/src/constants/index.ts
@@ -0,0 +1,218 @@
+/**
+ * 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,
+ },
+ };
+};
+
+/**
+ * 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)";
+
+/**
+ * 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..1194a0d
--- /dev/null
+++ b/packages/example-shared/src/styles/base.css
@@ -0,0 +1,121 @@
+/**
+ * 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;
+ --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;
+ --bgColor-muted: #161b22;
+ --bgColor-subtle: #11161d;
+ --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);
+ --shadow-elevated: 0 16px 40px rgba(0, 0, 0, 0.25);
+}
+
+[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;
+ --bgColor-muted: #f6f8fa;
+ --bgColor-subtle: #f8fafc;
+ --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);
+ --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..36bfb52
--- /dev/null
+++ b/packages/example-shared/src/styles/markdown.css
@@ -0,0 +1,317 @@
+/**
+ * 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;
+}
+
+.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
new file mode 100644
index 0000000..ebb2261
--- /dev/null
+++ b/packages/example-shared/src/styles/mermaid.css
@@ -0,0 +1,131 @@
+/**
+ * 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 .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;
+ max-width: 100%;
+ 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;
+ 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;
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/");