|
1 | 1 | using ShellUI.Core.Models; |
2 | 2 | using ShellUI.Templates; |
3 | 3 | using System.Text.Json; |
| 4 | +using System.Text.RegularExpressions; |
4 | 5 | using Spectre.Console; |
5 | 6 |
|
6 | 7 | namespace ShellUI.CLI.Services; |
@@ -147,6 +148,10 @@ await AnsiConsole.Status() |
147 | 148 | await SetupTailwindStandaloneAsync(); |
148 | 149 | } |
149 | 150 |
|
| 151 | + // Step 6.5: Patch App.razor / index.html — render mode + theme bootstrap + shellui.js |
| 152 | + ctx.Status("Wiring up theme and render mode..."); |
| 153 | + await BootstrapHostAsync(projectInfo); |
| 154 | + |
150 | 155 | // Step 7: Create MSBuild targets file |
151 | 156 | ctx.Status("Setting up MSBuild integration..."); |
152 | 157 | var buildPath = Path.Combine(Directory.GetCurrentDirectory(), "Build"); |
@@ -181,7 +186,6 @@ await AnsiConsole.Status() |
181 | 186 | AnsiConsole.MarkupLine("\n[blue]Next steps:[/]"); |
182 | 187 | AnsiConsole.MarkupLine(" [dim]1. Add components:[/] dotnet shellui add button"); |
183 | 188 | AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] dotnet shellui list"); |
184 | | - AnsiConsole.MarkupLine(" [dim]3. CopyButton/FileUpload/Command:[/] Add [yellow]<script src=\"shellui.js\"></script>[/] before Blazor script in App.razor or index.html"); |
185 | 189 | } |
186 | 190 |
|
187 | 191 | private static async Task SetupTailwindNpmAsync() |
@@ -435,6 +439,107 @@ private static async Task UpdateProjectFileAsync(string projectFilePath, string |
435 | 439 | } |
436 | 440 | } |
437 | 441 |
|
| 442 | + private static async Task BootstrapHostAsync(ProjectInfo projectInfo) |
| 443 | + { |
| 444 | + var cwd = Directory.GetCurrentDirectory(); |
| 445 | + |
| 446 | + // Blazor Web App / Server / SSR: patch Components/App.razor |
| 447 | + var appRazor = Path.Combine(cwd, "Components", "App.razor"); |
| 448 | + if (File.Exists(appRazor)) |
| 449 | + { |
| 450 | + var original = await File.ReadAllTextAsync(appRazor); |
| 451 | + var patched = RewriteAppRazor(original); |
| 452 | + if (patched != original) |
| 453 | + { |
| 454 | + await File.WriteAllTextAsync(appRazor, patched); |
| 455 | + AnsiConsole.MarkupLine("[green]Patched:[/] Components/App.razor"); |
| 456 | + } |
| 457 | + return; |
| 458 | + } |
| 459 | + |
| 460 | + // Blazor WASM (standalone): patch wwwroot/index.html instead — no Routes/HeadOutlet |
| 461 | + // render-mode pattern there; just inject theme bootstrap + shellui.js script tag. |
| 462 | + var indexHtml = Path.Combine(cwd, "wwwroot", "index.html"); |
| 463 | + if (File.Exists(indexHtml)) |
| 464 | + { |
| 465 | + var original = await File.ReadAllTextAsync(indexHtml); |
| 466 | + var patched = RewriteWasmIndexHtml(original); |
| 467 | + if (patched != original) |
| 468 | + { |
| 469 | + await File.WriteAllTextAsync(indexHtml, patched); |
| 470 | + AnsiConsole.MarkupLine("[green]Patched:[/] wwwroot/index.html"); |
| 471 | + } |
| 472 | + return; |
| 473 | + } |
| 474 | + |
| 475 | + AnsiConsole.MarkupLine("[yellow]No Components/App.razor or wwwroot/index.html found — skipped host bootstrap.[/]"); |
| 476 | + } |
| 477 | + |
| 478 | + // Idempotent: a second `shellui init` won't double-inject. Tags that already |
| 479 | + // carry @rendermode or any other attribute aren't matched, so user customizations |
| 480 | + // are preserved (they'll need to set @rendermode manually). |
| 481 | + internal static string RewriteAppRazor(string content) |
| 482 | + { |
| 483 | + content = Regex.Replace(content, @"<HeadOutlet\s*/>", @"<HeadOutlet @rendermode=""InteractiveServer"" />"); |
| 484 | + content = Regex.Replace(content, @"<Routes\s*/>", @"<Routes @rendermode=""InteractiveServer"" />"); |
| 485 | + |
| 486 | + // 3. Theme bootstrap in <head> — sets `dark` class before paint to avoid FOUC. |
| 487 | + if (!content.Contains("ShellUI theme bootstrap")) |
| 488 | + { |
| 489 | + const string themeScript = |
| 490 | +@" <script> |
| 491 | + // ShellUI theme bootstrap — runs before Blazor mounts to avoid a light-flash on dark pages. |
| 492 | + if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'light'); } |
| 493 | + if (localStorage.getItem('theme') === 'dark') { |
| 494 | + document.documentElement.classList.add('dark'); |
| 495 | + } |
| 496 | + </script> |
| 497 | +"; |
| 498 | + content = Regex.Replace(content, @"</head>", themeScript + "</head>", RegexOptions.IgnoreCase); |
| 499 | + } |
| 500 | + |
| 501 | + // 4. <script src="shellui.js"></script> immediately before blazor.web.js — provides |
| 502 | + // window.ShellUI.* (addClassToDocument, focusElement, copyToClipboard, …) for |
| 503 | + // ThemeToggle, CopyButton, InputOTP, FileUpload, Command. The pattern matches |
| 504 | + // both the modern `@Assets["_framework/blazor.web.js"]` and the bare form. |
| 505 | + if (!content.Contains("shellui.js")) |
| 506 | + { |
| 507 | + content = Regex.Replace( |
| 508 | + content, |
| 509 | + @"(<script\s+src=[^>]*_framework/blazor\.web\.js[^>]*>)", |
| 510 | + "<script src=\"shellui.js\"></script>\n $1"); |
| 511 | + } |
| 512 | + |
| 513 | + return content; |
| 514 | + } |
| 515 | + |
| 516 | + internal static string RewriteWasmIndexHtml(string content) |
| 517 | + { |
| 518 | + if (!content.Contains("ShellUI theme bootstrap")) |
| 519 | + { |
| 520 | + const string themeScript = |
| 521 | +@" <script> |
| 522 | + // ShellUI theme bootstrap — runs before Blazor mounts to avoid a light-flash on dark pages. |
| 523 | + if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'light'); } |
| 524 | + if (localStorage.getItem('theme') === 'dark') { |
| 525 | + document.documentElement.classList.add('dark'); |
| 526 | + } |
| 527 | + </script> |
| 528 | +"; |
| 529 | + content = Regex.Replace(content, @"</head>", themeScript + "</head>", RegexOptions.IgnoreCase); |
| 530 | + } |
| 531 | + |
| 532 | + if (!content.Contains("shellui.js")) |
| 533 | + { |
| 534 | + content = Regex.Replace( |
| 535 | + content, |
| 536 | + @"(<script\s+src=[^>]*_framework/blazor\.webassembly\.js[^>]*>)", |
| 537 | + "<script src=\"shellui.js\"></script>\n $1"); |
| 538 | + } |
| 539 | + |
| 540 | + return content; |
| 541 | + } |
| 542 | + |
438 | 543 | private static void RemoveBootstrapFiles() |
439 | 544 | { |
440 | 545 | try |
|
0 commit comments