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 + + + +
+ + + 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 + + Haxiom + + / + solid-markdown-wasm + + React raw WASM + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + + setImmediateRenderMermaid(event.currentTarget.checked) + } + className="h-4 w-4 cursor-pointer accent-cyan-500" + /> +
+ + + Try Haxiom + +
+
+
+ +
+
+ } + options={editorOptions} + theme={editorTheme} + value={markdown} + onChange={(value) => setMarkdown(value ?? "")} + /> +
+ +
+
+
+ This React workspace still calls init() and{" "} + render_md() directly. React adds code block actions + and Mermaid rendering on top of the raw HTML output; wrapper-only + iframe extraction and overlay behavior remain intentionally + omitted. +
+ + {isInitializingWasm ? ( + + ) : initError ? ( +
+ {initError} +
+ ) : renderError ? ( +
+ {renderError} +
+ ) : renderedHtml ? ( +
+ ) : ( +
+ Start typing Markdown to render the raw WASM preview. +
+ )} +
+
+
+ + {fullScreenSvg ? ( + { + event.preventDefault(); + setFullScreenSvg(null); + }} + onMouseDown={(event) => { + if (event.target === event.currentTarget) { + setFullScreenSvg(null); + } + }} + > + +
event.stopPropagation()} + > +
+
+
+ ) : null} +
+ ); +} + +export default App; diff --git a/example-react/src/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/");