webui: implement Mermaid code splitting with lazy loading

Split Mermaid library into separate bundle with content-based hashing for
optimal caching and reduced bundle size in main application components.

Implementation Changes:

1. Lazy Loading Infrastructure (sketch-timeline-message.ts):
   - Replace direct mermaid import with type-only import
   - Add loadMermaid() function with Promise-based dynamic loading
   - Implement __MERMAID_HASH__ constant injection pattern
   - Add global window.mermaid type declarations
   - Create mermaid loading promise with singleton pattern

2. Bundle Splitting (esbuild.go):
   - Add createStandaloneMermaidBundle() function
   - Generate content-based hash from mermaid package.json
   - Create mermaid-standalone-{hash}.js as IIFE format bundle
   - Implement esbuildBundleWithExternals() replacing esbuildBundleWithExternal()
   - Add --external:mermaid to all TypeScript bundle builds
   - Inject __MERMAID_HASH__ constant at build time

3. Async Rendering (sketch-timeline-message.ts):
   - Update renderMermaidDiagrams() to async/await pattern
   - Load mermaid library only when diagrams are present
   - Initialize mermaid configuration after dynamic loading
   - Maintain fallback to code blocks on loading errors
   - Preserve all existing mermaid functionality and configuration

Technical Details:
- Uses content-based hashing for optimal browser caching
- Mermaid loaded on-demand only when diagrams are present
- Singleton loading pattern prevents duplicate network requests
- Maintains existing mermaid initialization options
- Preserves error handling with code block fallbacks
- IIFE format enables direct window.mermaid assignment

Benefits:
- Reduces bundle size for components not using mermaid diagrams
- Enables browser caching of mermaid library across sessions
- Maintains existing functionality with lazy loading
- Improves initial page load performance
- Provides same user experience with deferred mermaid loading

This follows the same pattern established for Monaco editor bundling,
providing consistent lazy loading architecture for large dependencies.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s0f8a82fcd28add05k
diff --git a/webui/esbuild.go b/webui/esbuild.go
index f8f010d..1c356ce 100644
--- a/webui/esbuild.go
+++ b/webui/esbuild.go
@@ -215,22 +215,26 @@
 		"node_modules/monaco-editor/esm/vs/language/json/json.worker.js",
 	}
 
-	// Additionally create a standalone Monaco bundle for caching
+	// Additionally create standalone bundles for caching
 	monacoHash, err := createStandaloneMonacoBundle(tmpHashDir, buildDir)
 	if err != nil {
 		return nil, fmt.Errorf("create monaco bundle: %w", err)
 	}
-	_ = monacoHash // We created it for caching benefits, but don't use it yet
 
-	// Bundle all files with Monaco as external (since they may transitively import Monaco)
+	mermaidHash, err := createStandaloneMermaidBundle(tmpHashDir, buildDir)
+	if err != nil {
+		return nil, fmt.Errorf("create mermaid bundle: %w", err)
+	}
+
+	// Bundle all files with Monaco and Mermaid as external (since they may transitively import them)
 	for _, tsName := range bundleTs {
-		// Use external Monaco for all TypeScript files to ensure consistency
+		// Use external Monaco and Mermaid for all TypeScript files to ensure consistency
 		if strings.HasSuffix(tsName, ".ts") {
-			if err := esbuildBundleWithExternal(tmpHashDir, filepath.Join(buildDir, tsName), monacoHash); err != nil {
+			if err := esbuildBundleWithExternals(tmpHashDir, filepath.Join(buildDir, tsName), monacoHash, mermaidHash); err != nil {
 				return nil, fmt.Errorf("esbuild: %s: %w", tsName, err)
 			}
 		} else {
-			// Bundle worker files normally (they don't use Monaco)
+			// Bundle worker files normally (they don't use Monaco or Mermaid)
 			if err := esbuildBundle(tmpHashDir, filepath.Join(buildDir, tsName), ""); err != nil {
 				return nil, fmt.Errorf("esbuild: %s: %w", tsName, err)
 			}
@@ -517,8 +521,60 @@
 	return monacoHash, nil
 }
 
-// esbuildBundleWithExternal bundles a file with Monaco as external dependency
-func esbuildBundleWithExternal(outDir, src, monacoHash string) error {
+// createStandaloneMermaidBundle creates a separate Mermaid bundle with content-based hash
+// This is useful for caching Mermaid separately from the main application bundles
+func createStandaloneMermaidBundle(outDir, buildDir string) (string, error) {
+	// Create a temporary entry file that imports Mermaid and exposes it globally
+	mermaidEntryContent := `import mermaid from 'mermaid';
+window.mermaid = mermaid;
+export default mermaid;
+`
+	mermaidEntryPath := filepath.Join(buildDir, "mermaid-standalone-entry.js")
+	if err := os.WriteFile(mermaidEntryPath, []byte(mermaidEntryContent), 0o666); err != nil {
+		return "", fmt.Errorf("write mermaid entry: %w", err)
+	}
+
+	// Calculate hash of mermaid package for content-based naming
+	mermaidPackageJson := filepath.Join(buildDir, "node_modules", "mermaid", "package.json")
+	mermaidContent, err := os.ReadFile(mermaidPackageJson)
+	if err != nil {
+		return "", fmt.Errorf("read mermaid package.json: %w", err)
+	}
+
+	h := sha256.New()
+	h.Write(mermaidContent)
+	mermaidHash := hex.EncodeToString(h.Sum(nil))[:16]
+
+	// Bundle Mermaid with content-based filename
+	mermaidOutputName := fmt.Sprintf("mermaid-standalone-%s.js", mermaidHash)
+	mermaidOutputPath := filepath.Join(outDir, mermaidOutputName)
+
+	args := []string{
+		mermaidEntryPath,
+		"--bundle",
+		"--sourcemap",
+		"--minify",
+		"--log-level=error",
+		"--outfile=" + mermaidOutputPath,
+		"--format=iife",
+		"--global-name=__MermaidLoader__",
+		"--loader:.ttf=file",
+		"--loader:.eot=file",
+		"--loader:.woff=file",
+		"--loader:.woff2=file",
+		"--public-path=.",
+	}
+
+	ret := esbuildcli.Run(args)
+	if ret != 0 {
+		return "", fmt.Errorf("esbuild mermaid bundle failed: %d", ret)
+	}
+
+	return mermaidHash, nil
+}
+
+// esbuildBundleWithExternals bundles a file with Monaco and Mermaid as external dependencies
+func esbuildBundleWithExternals(outDir, src, monacoHash, mermaidHash string) error {
 	args := []string{
 		src,
 		"--bundle",
@@ -527,12 +583,14 @@
 		"--log-level=error",
 		"--outdir=" + outDir,
 		"--external:monaco-editor",
+		"--external:mermaid",
 		"--loader:.ttf=file",
 		"--loader:.eot=file",
 		"--loader:.woff=file",
 		"--loader:.woff2=file",
 		"--public-path=.",
 		"--define:__MONACO_HASH__=\"" + monacoHash + "\"",
+		"--define:__MERMAID_HASH__=\"" + mermaidHash + "\"",
 	}
 
 	ret := esbuildcli.Run(args)