diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7f0f17..601753b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,23 @@ name: 🎁 Release -# Packages the PWA as a self-hostable zip and publishes a GitHub Release. -# Triggered by pushing a `v*` tag (e.g. v0.1.0) or manually from the Actions tab. +# Packages two artifact families into one GitHub Release per tag: # -# The released zip is a portable static site: extract and serve `wwwroot/` with -# any static file server (python http.server, nginx, caddy, IIS, ...). It's -# also directly installable as a PWA on Windows / macOS / Linux / Android / iOS. +# 1. PWA archive (zip + tar.gz) - Linux runner. Extract and serve wwwroot/ +# with any static file server, or install in-browser as a PWA. The +# static site itself is platform-agnostic. +# +# 2. Windows desktop bundle (Velopack alpha channel) - Windows runner. +# Portable.zip for first-time download, plus RELEASES-alpha + full.nupkg +# that the in-app UpdateManager reads to deliver auto-updates. +# Setup.exe is intentionally skipped (--noInst): we distribute the +# portable form only. See docs/releasing.md. +# +# The desktop job uses `vpk upload github --merge` so its artifacts attach +# to the same draft release that the PWA job creates - the user sees one +# release with both downloads. +# +# Triggered by pushing a `v*` tag (e.g. v0.1.0) or manually from the +# Actions tab. on: push: @@ -120,7 +132,13 @@ jobs: Linux, Android and iOS - no download required, no admin rights, no Play Store. Uninstall via your browser's app menu. - ## 📥 Self-host / offline archive + ## 🪟 Windows desktop (portable, auto-updating) + Download the Windows portable zip below, unzip anywhere, + and run `preflight.xml.exe`. The shell will check GitHub + Releases for newer alpha builds in the background and offer + a one-click restart when an update is downloaded. + + ## 📥 Self-host / offline PWA archive Download the zip below and serve `wwwroot/` with any static server: ```bash @@ -139,4 +157,101 @@ jobs: before serving. No rebuild required. ## ✅ Integrity - Verify downloads against `SHA256SUMS.txt`. + Verify PWA downloads against `SHA256SUMS.txt`. The desktop + zip is signed by Velopack's release manifest (RELEASES-alpha) + which the auto-updater verifies on each check. + + desktop: + name: 🪟 Package Windows desktop + needs: package + runs-on: windows-latest + steps: + - name: 📥 Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ⚙️ Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: 💾 Cache NuGet + uses: actions/cache@v4 + with: + path: ~\.nuget\packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'Directory.Packages.props', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- + + - name: 🏷️ Resolve version + id: ver + shell: bash + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" + VERSION="${TAG#v}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "::notice title=Desktop release::Packaging $TAG (version $VERSION)" + + - name: 🔧 Install vpk + shell: bash + run: dotnet tool install -g vpk --version 0.0.1298 + + - name: 🚀 Publish desktop (self-contained win-x64) + shell: bash + run: | + dotnet publish srcs/Preflight.Desktop/Preflight.Desktop.csproj \ + --configuration Release \ + --runtime win-x64 \ + --self-contained true \ + --nologo \ + --output artifacts/desktop/publish + + - name: 📦 vpk pack (alpha channel, portable-only) + shell: bash + run: | + vpk pack \ + --packId preflight.xml \ + --packTitle "preflight.xml" \ + --packAuthors "kYaRick" \ + --packVersion "${{ steps.ver.outputs.version }}" \ + --packDir artifacts/desktop/publish \ + --mainExe preflight.xml.exe \ + --channel alpha \ + --runtime win-x64 \ + --outputDir artifacts/desktop/releases \ + --noInst + + # Velopack labels the portable artifact as `--Portable.zip` + # (versionless, capital P). For consistency with the rest of the release + # asset family - lowercase, version-stamped, runtime-stamped - rename + # both the file and its reference in assets..json before the + # upload step reads them. + - name: 📝 Rename portable zip (lowercase + version + runtime) + shell: bash + run: | + cd artifacts/desktop/releases + cap="preflight.xml-alpha-Portable.zip" + low="preflight.xml-${{ steps.ver.outputs.version }}-alpha-win-x64-portable.zip" + if [[ -f "$cap" ]]; then + mv "$cap" "$low" + perl -pi -e "s|\\Q$cap\\E|$low|g" assets.alpha.json + echo "renamed: $cap → $low" + fi + + # --merge attaches our artifacts to the draft release the + # `package` job already created for this tag. --pre marks it + # as a pre-release (matches the alpha channel semantics). + - name: 🎁 Upload to GitHub Release (merge with PWA draft) + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + vpk upload github \ + --outputDir artifacts/desktop/releases \ + --channel alpha \ + --repoUrl https://github.com/${{ github.repository }} \ + --tag ${{ steps.ver.outputs.tag }} \ + --token "$GITHUB_TOKEN" \ + --merge \ + --pre diff --git a/.gitignore b/.gitignore index 86bdb8e..660a8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,13 @@ node_modules/ .aider* .copilot/ *.ai-cache + +# ───────────────────────────────────────────────────────────── +# Preflight.Desktop - build-time generated output only +# ───────────────────────────────────────────────────────────── +srcs/Preflight.Desktop/blazor-temp/ +srcs/Preflight.Desktop/wwwroot/ +# Portable WebView2 profile (cache, localStorage, cookies, IndexedDB). +# Created next to the executable on first run. Already inside ignored +# bin/ for dev builds, but listed here for any out-of-tree run. +**/data/WebView2/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..87f8b1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,120 @@ + + + +# Changelog + +All notable changes to **preflight.xml** are documented here. Format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## v0.1.2-alpha + +### Added + +- View Transitions API for SPA navigation - Landing → Wizard / Advanced / Docs + and wizard step changes now crossfade smoothly with no flicker. Falls back + silently to instant navigation in browsers that don't implement the spec. +- "What's new" modal - embedded CHANGELOG.md rendered inline, sharing the + same overlay chrome as the command palette and shortcuts help. +- Custom XML import modal - drag-and-drop zone, paste pad, and a "Browse" + fallback to the OS picker, all in one styled dialog instead of the + bare-browser file dialog. +- Windows desktop auto-update (Velopack, alpha channel) - the desktop + shell quietly checks GitHub Releases for newer alpha builds in the + background, downloads the delta, and offers a one-click "Restart now" + banner inside the app. No installer, no admin rights - portable zip + in, portable update out. + +### Changed + +- Loading bar no longer overflows on narrow phones - the SVG runway scales + uniformly to fit any viewport, with adequate edge margins. +- Trail under the loading-screen plane now draws ONLY behind the plane, + with a soft fade-in at the runway start. +- "Go to top" plane button rebuilt - solid accent gradient, larger glyph, + tactile press feedback, and a longer custom-eased scroll so the trip + back to the top reads as deliberate motion instead of an instant jump. +- Command palette + Shortcuts help modal now animate open AND close (the + old open-only animation made dismissal feel abrupt). +- Modals are now centred vertically in the viewport instead of pinned + to 15vh from the top - large modals like the changelog no longer feel + like they're falling off the page. +- Advanced XML preview modal now fades in/out with the same chrome family + as the other modals (previously it snapped open with no transition). +- Desktop update UI now uses a dedicated overlay window (WPF) instead of + an in-tree panel/popup, so the banner is no longer clipped or hidden by + WebView2 HWND airspace. +- Update checks now start after the first rendered app frame (post-splash) + instead of immediately at process startup, preventing update UI from + racing the compact splash phase. + +### Fixed + +- Wizard final-step action buttons no longer overlap on mobile - the + action stacks now wrap and the page bottom padding clears any floating + control. +- Ukrainian translation: "Підкрутити в Advanced" → "Доналаштувати в Advanced". +- Ukrainian "What's new" modal now actually renders the UA changelog - + MSBuild was routing CHANGELOG.uk.md to a satellite assembly because of + the .uk. infix, so the lookup always fell back to the EN file. +- Import XML modal now blurs the page header (and everything else behind + it) - the modal was rendered inside the body grid, which trapped its + backdrop-filter inside a stacking context that sat below the sticky + header. +- Desktop update banner positioning is now anchored to the main window and + continuously reflowed on move/resize, eliminating random off-window + placement. +- Desktop update banner now re-translates immediately when UI language is + changed in-app (via `preflightCulture.set` bridge to the desktop shell). + + +### Maintenance + +- Refactored ModeCard to render its content inside an `` so SPA + navigation flows through the View Transitions click interceptor instead + of programmatic Nav.NavigateTo (which bypassed it). +- Added `ChangelogService` with regex-based internal-block filter and + per-culture resource lookup. +- Embedded CHANGELOG.{en,uk}.md as compile-time resources via + `` in + Preflight.App.csproj. WithCulture="false" stops MSBuild from treating + the .uk. infix as a satellite-resource culture marker. +- Moved the changelog authoring guide out of the file's own header into + `docs/changelog.md` so the source file isn't 60 lines of HTML comment + before the actual content. +- Promoted the import XML modal to a top-level layout slot via + `ImportModalService` so its overlay sits above the FluentLayout + stacking context. +- Switched Preflight.Desktop's AssemblyName from `Preflight` to + `preflight.xml` so the executable, Velopack PackId and live PWA name + all match. The published exe is now `preflight.xml.exe`; Windows hides + the `.exe` in the UI so the user sees `preflight.xml`. +- Custom WPF entry point (`Program.Main`) so `VelopackApp.Build().Run()` + intercepts hook commands (`--veloapp-install` / `--veloapp-uninstall` + / `--veloapp-updated`) before any window is constructed. +- Added `UpdateService` with `GithubSource` + `ExplicitChannel="alpha"`, + fired from `App.OnStartup` after an 8s grace period. Failures are + swallowed - desktop runs offline-first; missing updates are not an + error condition. +- New justfile recipes `desktop-publish`, `desktop-pack`, + `desktop-release` wrap the dotnet publish + `vpk pack --noInst` + (portable-only) + `vpk upload github --merge --pre` flow. +- Extended `release.yml` with a `desktop` job on `windows-latest` that + runs after the PWA package job, producing + `preflight.xml-alpha-Portable.zip` + `RELEASES-alpha` + the matching + full/delta nupkg files, and merging them into the same draft GitHub + Release the PWA job already created. +- Split the Blazor wwwroot copy in Preflight.Desktop.csproj into + `CopyBlazorToOutDir` (post-Build) + `CopyBlazorToPublishDir` + (post-Publish) so `dotnet publish --output …` ships the PWA inside + the bundle - vpk pack used to ship a desktop folder with no wwwroot. +- Added `UpdateBannerWindow` (transparent, owner-bound overlay window) for + desktop update notifications, including localization refresh and explicit + restart/dismiss callbacks wired to `UpdateService`. + diff --git a/CHANGELOG.uk.md b/CHANGELOG.uk.md new file mode 100644 index 0000000..76d66d0 --- /dev/null +++ b/CHANGELOG.uk.md @@ -0,0 +1,123 @@ + + + +# Журнал змін + +Усі важливі зміни в **preflight.xml** документуються тут. Формат - +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), версіонування - +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Версія v0.1.2-alpha + +### Додано + +- View Transitions API для SPA-навігації - переходи Landing → Wizard / + Advanced / Docs та між кроками візарда тепер плавно кросфейдять без + миготіння. У браузерах, що не підтримують специфікацію, тихий fallback + на миттєву навігацію. +- Модалка "Що нового" - вбудований CHANGELOG.md рендериться прямо в + додатку, той самий стиль що й у командної палітри і довідки гарячих + клавіш. +- Кастомна модалка імпорту XML - drag-and-drop зона, поле для вставки + тексту, та "Вибрати файл..." як запасний шлях через системний пікер. +- Авто-оновлення для Windows-версії (Velopack, alpha-канал) - десктопна + оболонка тихо перевіряє GitHub Releases на нові alpha-білди у фоні, + довантажує дельту, і пропонує плашку "Перезапустити" просто всередині + додатку. Без інсталятора, без admin-прав - portable zip заходить, + portable update виходить. + +### Змінено + +- Лоадер на старті не вилазить за межі екрана на вузьких телефонах - + SVG runway тепер масштабується пропорційно з адекватними відступами + по краях. +- Шлейф літака на лоадері тепер малюється лише ПОЗАДУ літака з плавним + fade-in на самому початку (раніше лінія промальовувалася наперед). +- Кнопка "Догори" перебудована - суцільний акцентний градієнт, більший + гліф літака, тактильний press feedback і довший власний easing, тож + поїздка нагору читається як осмислений рух, а не різкий стрибок. +- Командна палітра і модалка довідки тепер анімують і відкриття, і + закриття (раніше анімація була тільки на open, dismissal сприймалося + різко). +- Модалки тепер центруються вертикально замість прив'язки 15vh від + верху - великі модалки типу журналу змін не виглядають "опущеними". +- Модалка перегляду XML в Advanced секції тепер плавно з'являється і + зникає (раніше клацала без анімації). +- UI оновлень у десктопі переведено на окреме WPF overlay-вікно замість + внутрішньої панелі/popup, тож банер більше не обрізається і не + перекривається HWND-поверхнею WebView2. +- Перевірка оновлень у десктопі стартує після першого відрендереного + кадру застосунку (після splash), а не одразу на старті процесу, щоб + плашка не конфліктувала з компактним splash-етапом. + +### Виправлено + +- Кнопки фінальних кроків візарда більше не накладаються на мобільному + - стек кнопок тепер обгортається, нижній padding сторінки звільняє + простір для floating-елементів. +- Український переклад: "Підкрутити в Advanced" → "Доналаштувати в + Advanced". +- Українська модалка "Що нового" тепер дійсно показує українську версію + ченджлогу - MSBuild відносив CHANGELOG.uk.md до satellite-збірки + через інфікс ".uk.", тож пошук завжди валився у EN-fallback. +- Модалка імпорту XML тепер блюрить хедер сторінки (і все інше позаду) + - раніше модалка рендерилася в середині body-сітки, що замикало + backdrop-filter у stacking-контексті нижче sticky-хедера. +- Позиціювання десктопного банера оновлень тепер прив'язане до основного + вікна і перераховується при move/resize, через що зникли випадкові + появи поза межами вікна. +- Десктопний банер оновлень тепер одразу перемальовує локалізований текст + при зміні мови в додатку (через міст `preflightCulture.set` → shell). + + +### Технічне + +- ModeCard перероблений: вміст тепер всередині `` щоб SPA + навігація йшла через перехоплювач кліків View Transitions замість + програмного Nav.NavigateTo (який його обходив). +- Доданий ChangelogService з фільтром internal-блоків через regex + і пошуком ресурсу по культурі. +- CHANGELOG.{en,uk}.md ембедяться як ресурси через + `` в + Preflight.App.csproj. WithCulture="false" відмикає авто-розпізнавання + ".uk." як культурного суфікса для satellite-збірки. +- Гайд по оформленню ченджлогу винесений з власного заголовка файлу в + `docs/changelog.md`, щоб у самому ченджлозі не було 60 рядків HTML- + коментарю до першого реального запису. +- Модалка імпорту XML піднята в layout-слот верхнього рівня через + `ImportModalService`, щоб її overlay був над stacking-контекстом + FluentLayout. +- AssemblyName Preflight.Desktop змінено з `Preflight` на + `preflight.xml`, щоб ім'я екзешника, Velopack PackId і назва живого + PWA збігалися. Опублікований файл тепер `preflight.xml.exe`; Windows + ховає `.exe` в інтерфейсі, тож юзер бачить `preflight.xml`. +- Кастомна WPF entry-point точка (`Program.Main`), щоб + `VelopackApp.Build().Run()` перехоплював hook-команди (`--veloapp-install` + / `--veloapp-uninstall` / `--veloapp-updated`) до того як WPF + сконструює перше вікно. +- Доданий `UpdateService` з `GithubSource` + `ExplicitChannel="alpha"`, + стартує з `App.OnStartup` після 8с-grace періоду. Помилки ковтаються + - десктоп працює offline-first; відсутність оновлення - не помилка. +- Нові justfile-рецепти `desktop-publish`, `desktop-pack`, + `desktop-release` обгортають dotnet publish + `vpk pack --noInst` + (тільки portable) + `vpk upload github --merge --pre`. +- `release.yml` отримав новий job `desktop` на `windows-latest`, який + виконується після PWA-package, генерує + `preflight.xml-alpha-Portable.zip` + `RELEASES-alpha` + повний/дельта + nupkg, і мерджить артефакти в той самий draft GitHub Release, що + створив PWA-job. +- Копіювання Blazor wwwroot у Preflight.Desktop.csproj розбито на + `CopyBlazorToOutDir` (post-Build) + `CopyBlazorToPublishDir` + (post-Publish), щоб `dotnet publish --output …` клав PWA всередину + бандла - раніше vpk pack пакував desktop-папку без wwwroot. +- Додане `UpdateBannerWindow` (прозоре owner-bound overlay-вікно) для + десктопних сповіщень про оновлення, з оновленням локалізації та + явними callback-обробниками "перезапустити"/"пізніше" через + `UpdateService`. + diff --git a/Directory.Build.props b/Directory.Build.props index 7ff97af..4caf123 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -27,13 +27,15 @@ - 0.1.1 + 0.1.2 alpha $(VersionPrefix)-$(VersionSuffix) $(Version) kYaRick kYaRick preflight.xml + Visual builder for Windows autounattend.xml - offline, in your browser. + $(Product) Copyright © $([System.DateTime]::UtcNow.Year) kYaRick MIT https://github.com/kYaRick/preflight.xml @@ -48,4 +50,35 @@ snupkg + + + $([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)', 'srcs', + 'Preflight.App', 'wwwroot', 'favicon.ico')) + + + + + + WinExe + net10.0-windows + true + preflight.xml + $(PreflightIconPath) + en + + diff --git a/Directory.Packages.props b/Directory.Packages.props index bfb98d6..3d8eb2f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,6 +32,16 @@ + + + + + + diff --git a/Preflight.slnx b/Preflight.slnx index 9151caf..08e8b7c 100644 --- a/Preflight.slnx +++ b/Preflight.slnx @@ -1,6 +1,7 @@ + diff --git a/README.md b/README.md index 6a7f48f..06d7dc8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

- Version + Version .NET 10 Blazor WASM Fluent UI @@ -21,7 +21,7 @@ > [!WARNING] > **Pre-alpha.** The Blazor app is being scaffolded - APIs, UI and file -> layout can change without notice until `v0.1.1`. Pin to a specific tag +> layout can change without notice until `v0.1.2`. Pin to a specific tag > if you depend on it. --- @@ -38,6 +38,22 @@ config never leaves your machine. > [Unattended Windows Setup Reference](https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/) > is the canonical source. +## 🖥️ App showcase + +

+ preflight.xml app showcase + UI overview: landing, desktop splash and in-app experience. +
+ +
+ +## 🧠 How autounattend.xml works + +
+ autounattend.xml setup flow + Concept flow: how autounattend.xml answers Windows Setup prompts end-to-end. +
+ ## 🎯 Planned features diff --git a/docs/README.md b/docs/README.md index da139e3..0f0e3b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ | Deploy a new build to the live site | [publishing.md](publishing.md) | | Ship a versioned release & self-host archive | [releasing.md](releasing.md) | | Resync vendored `Preflight.Unattend` | [upstream-sync.md](upstream-sync.md) | +| Add an entry to the changelog | [changelog.md](changelog.md) | ## 🎨 Conventions diff --git a/docs/assets/preflight-flow.gif b/docs/assets/preflight-flow.gif new file mode 100644 index 0000000..db2e49a Binary files /dev/null and b/docs/assets/preflight-flow.gif differ diff --git a/docs/assets/preflight-showcase.gif b/docs/assets/preflight-showcase.gif new file mode 100644 index 0000000..3a2cf04 Binary files /dev/null and b/docs/assets/preflight-showcase.gif differ diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..2a94339 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,83 @@ +
+ +# 📝 Changelog authoring guide + +How to edit CHANGELOG.md / CHANGELOG.uk.md so the in-app modal, the GitHub release notes, and the rendered file on GitHub all stay coherent. + +
+ +> [!IMPORTANT] +> The changelog files at the repo root are the **single source of truth** for: +> - The "What's new" modal in the running app (web + desktop) +> - GitHub Release notes (auto-published from these files by CI on tag push) +> - The repo's CHANGELOG view on GitHub + +Format follows [Keep a Changelog 1.1.0](https://keepachangelog.com). + +--- + +## ✏️ What to write + +Each released version gets a `## [X.Y.Z] - YYYY-MM-DD` heading. Inside it, +group entries under H3 sections in this order (skip ones that don't apply): + +| Section | When to use | +| :--------------- | :------------------------------------------------ | +| `### Added` | New features visible to the user | +| `### Changed` | Behaviour or visual changes to existing features | +| `### Fixed` | Bug fixes | +| `### Removed` | Features taken out | +| `### Deprecated` | Features that still work but will be removed soon | +| `### Security` | Vulnerability or hardening fixes | + +## 🙈 Hide developer-only entries + +End users don't need to read about CI tweaks, dependency bumps, refactors +with no visible effect, or maintenance chores. Wrap those in HTML-comment +markers: + +```markdown + +### Maintenance +- Bumped Foo to v2.x.y +- Refactored ChangelogService for testability + +``` + +Anything between those markers is: + +- ✅ visible on GitHub (HTML comments collapse in the renderer, but the + block content still shows in raw view and PR diffs) +- 🚫 stripped before the in-app modal renders (see + `srcs/Preflight.App/Services/ChangelogService.cs`) + +The same markers also hide the file's H1 title and intro paragraph from +the modal - the modal already renders its own localised title, so the +duplicated markdown heading would only add visual noise. Keep the title +block wrapped in markers in **every** language file. + +Use this for: chores, build/CI, internal refactors, dependency updates +(unless the bump fixed a user-visible bug - then mention the fix, not +the version bump). + +## 🗣️ Voice & tone + +- One bullet = one user-visible change. Don't bundle. +- Lead with the noun (`Loading bar no longer overflows...`, not `We fixed...`). +- Past tense for fixes, present for added/changed. +- Link to the issue/PR when it adds context: `(#123)`. +- Keep it short. The modal is a teaser; the full discussion lives on GitHub. + +## 🌐 Translations + +`CHANGELOG.uk.md` mirrors `CHANGELOG.md` in Ukrainian. When you add an entry +in EN, add the equivalent in UK - or accept that UA users will see the EN +fallback for that release. + +> [!NOTE] +> Files named `CHANGELOG..md` look like culture-specific satellite +> resources to MSBuild. The `.csproj` overrides this with +> `WithCulture="false"` so they end up in the main assembly where +> `ChangelogService` looks for them. + +← Back to the [docs index](README.md). diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..2134415 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,175 @@ +# Development Guide + +This document describes development workflows, testing modes, and local setup for working on preflight.xml. + +## Quick Start + +```bash +# Clone and restore +git clone https://github.com/kYaRick/preflight.xml.git +cd preflight.xml +dotnet restore + +# Build and run +dotnet build +dotnet run --project srcs/Preflight.Desktop + +# Run tests +dotnet test +``` + +--- + +## Testing Features + +### Update Service Testing + +The desktop app includes **optional compile-time feature flags** for testing the update flow without modifying the live GitHub release channel. These are **Debug-only** - they don't ship in Release builds. + +#### Enabling Update Test Mode + +1. **In Visual Studio / VS Code:** + - Select the launch profile **"Preflight.Desktop Update Test"** from the Run dropdown. + - Press F5 or Run > Start Debugging. + +2. **From command line:** + ```bash + dotnet run --project srcs/Preflight.Desktop \ + --configuration Debug \ + --launch-profile "Preflight.Desktop Update Test" + ``` + +#### How It Works + +When the Update Test profile is active: + +- **After ~1.2 seconds**, an "update ready" banner appears at the bottom of the window. +- The banner shows version `0.1.2-alpha-test` and localized copy about the available update. +- Clicking "Restart now" will show a message: `"Test mode: restart and update apply were skipped."` instead of actually restarting. + - This allows you to test the UI flow without closing the app. + - See [OnBannerRestartRequested](../srcs/Preflight.Desktop/MainWindow.xaml.cs) for the dry-run logic. + +#### Environment Variables + +You can customize the test behavior via environment variables (only read in Debug builds): + +| Variable | Default | Purpose | +|----------|---------|---------| +| `PREFLIGHT_DESKTOP_UPDATE_TEST_MODE` | (not set) | Set to `1`, `true`, `yes`, or `on` to enable simulated update. | +| `PREFLIGHT_DESKTOP_UPDATE_TEST_VERSION` | `9.9.9-test` | Version string to display in the update banner. | +| `PREFLIGHT_DESKTOP_UPDATE_TEST_DELAY_MS` | `1800` | Milliseconds to wait before triggering UpdateReady (default: 1.8s). | +| `PREFLIGHT_DESKTOP_UPDATE_DRY_RUN` | (not set) | Set to `1`, `true`, `yes`, or `on` to prevent actual restart/apply. | + +**Example:** To test the banner appearing instantly with a custom version: + +```bash +$env:PREFLIGHT_DESKTOP_UPDATE_TEST_MODE="1" +$env:PREFLIGHT_DESKTOP_UPDATE_TEST_VERSION="0.2.0-beta" +$env:PREFLIGHT_DESKTOP_UPDATE_TEST_DELAY_MS="0" +$env:PREFLIGHT_DESKTOP_UPDATE_DRY_RUN="1" + +dotnet run --project srcs/Preflight.Desktop --configuration Debug +``` + +### Feature Flag Compilation + +The update testing code lives in a separate partial file: [UpdateService.UpdateTest.cs](../srcs/Preflight.Desktop/UpdateService.UpdateTest.cs), wrapped in `#if UPDATE_TEST`. + +**To disable the feature:** + +Edit [Preflight.Desktop.csproj](../srcs/Preflight.Desktop/Preflight.Desktop.csproj) and comment out the UPDATE_TEST PropertyGroup: + +```xml + + + $(DefineConstants);UPDATE_TEST + +``` + +Then rebuild: +```bash +dotnet clean +dotnet build --configuration Debug +``` + +--- + +## Architecture Notes + +### Desktop Shell + +The desktop app is a pure .NET 10 WPF + WebView2 shell: +- **No Node/npm:** Pure C#/XAML only. +- **No backend:** The Blazor WASM PWA (from `srcs/Preflight.App`) is published at build time and served by WebView2 via virtual host mapping. +- **Offline-capable:** The entire PWA is bundled and runs client-side. + +### Splash & Loading + +- The startup splash is **in-window** (not a separate window), created as an overlay in [MainWindow.xaml](../srcs/Preflight.Desktop/MainWindow.xaml). +- Splash size adapts to compact mode on launch (580x320), then resizes to full dimensions (1080x900) once Blazor is ready. +- Rounded corners and styling use Fluent Design tokens from App.xaml. + +### Update Flow + +1. **UpdateService** polls GitHub Releases in the background (8s after startup). +2. If a newer version is found and downloaded, `UpdateReady` event fires. +3. **MainWindow** listens to this event and shows the update banner. +4. Clicking "Restart now" calls `UpdateService.ApplyAndRestart()`, which uses Velopack to apply the update and relaunch. + - In **dry-run mode** (test), it skips the restart and shows a debug message instead. + +--- + +## Project Structure + +``` +srcs/Preflight.Desktop/ + ├── App.xaml / App.xaml.cs # WPF app entry + ├── Program.cs # Velopack hooks + STAThread Main + ├── MainWindow.xaml / .cs # Main window, title bar, splash, update banner + ├── UpdateService.cs # Update polling service (production code) + ├── UpdateService.UpdateTest.cs # Test hooks (#if UPDATE_TEST only) + └── wwwroot/ # Blazor WASM PWA files (generated at build time) +``` + +--- + +## Building & Publishing + +### Debug +```bash +dotnet build --configuration Debug +``` + +### Release (self-contained) +```bash +dotnet publish --configuration Release \ + --runtime win-x64 \ + --self-contained +``` + +See [publishing.md](./publishing.md) for Velopack bundling and release channel setup. + +--- + +## Troubleshooting + +**Q: The update banner doesn't appear in debug mode.** +A: Ensure the "Preflight.Desktop Update Test" launch profile is selected. Check that `PREFLIGHT_DESKTOP_UPDATE_TEST_MODE=1` is set. + +**Q: Clicking "Restart now" closes the app (not dry-run).** +A: Check if `PREFLIGHT_DESKTOP_UPDATE_DRY_RUN=1` is set. Without it, the real Velopack update flow runs. + +**Q: Compile error about unused test fields in Release.** +A: This shouldn't happen - if it does, ensure `UPDATE_TEST` is only defined for Debug. Run `dotnet clean` and rebuild. + +--- + +## See Also + +- [docs/architecture.md](./architecture.md) - System design overview +- [docs/ci-cd.md](./ci-cd.md) - CI/CD pipeline & GitHub Actions +- [srcs/Preflight.Desktop/UpdateService.cs](../srcs/Preflight.Desktop/UpdateService.cs) - Update service implementation diff --git a/docs/releasing.md b/docs/releasing.md index a919d96..9a5cafb 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -2,12 +2,22 @@ # 🎁 Releasing -Tag-driven. Push vX.Y.Z → a portable PWA archive lands in a draft GitHub Release. +Tag-driven. Push vX.Y.Z → a portable PWA archive AND a Velopack desktop bundle land in a single draft GitHub Release. -Pipeline: [`release.yml`](../.github/workflows/release.yml). This page -is the operator's handbook. +Pipeline: [`release.yml`](../.github/workflows/release.yml). Two jobs: + +| Job | Runner | Output | +| :--------- | :-------------- | :--------------------------------------------------------------------------- | +| `package` | `ubuntu-latest` | `preflight.xml-pwa-X.Y.Z.zip` + `.tar.gz` + `SHA256SUMS.txt` | +| `desktop` | `windows-latest`| `preflight.xml-X.Y.Z-alpha-win-x64-portable.zip` + `RELEASES-alpha` + full/delta nupkg | + +The `desktop` job runs after `package` and uses `vpk upload github +--merge` to attach its artifacts to the same draft release - users see +one release with all downloads. + +This page is the operator's handbook. --- @@ -151,10 +161,81 @@ cd wwwroot- ## 📱 Install as a desktop app -The archive is a PWA. Served over HTTPS, browsers offer an install -button in the address bar; installing creates a standalone window with -its own icon; uninstalling is one click in the browser's app menu. -Works on Windows, macOS, Linux desktop, Android, iOS / iPadOS. +### Native Windows shell (recommended on Windows) + +The `desktop` job ships a portable bundle: + +``` +preflight.xml--alpha-win-x64-portable.zip +└── preflight.xml.exe ← double-click to run +``` + +Unzip anywhere - no admin rights, no installer, no registry writes (the +WebView2 profile and last-used folder preferences live in a `data/` +folder next to the exe). On launch the app polls GitHub Releases for +newer builds in the alpha channel and shows a one-click "Restart now" +banner inside the window when an update finishes downloading. + +> [!NOTE] +> The shell embeds the same Blazor PWA as the live site - no in-page +> functionality is lost. The native window adds OS-themed file dialogs, +> a custom title bar, and the auto-update affordance. + +### Cross-platform PWA + +The PWA archive is the canonical install for everything except a +Windows machine that wants Windows-shell ergonomics. Served over +HTTPS, browsers offer an install button in the address bar; installing +creates a standalone window with its own icon; uninstalling is one +click in the browser's app menu. Works on Windows, macOS, Linux +desktop, Android, iOS / iPadOS. + +## 🖥️ Local desktop packaging + +For a smoke test of the desktop bundle without pushing a tag: + +```bash +just desktop-pack # auto-version from Directory.Build.props +just desktop-pack 0.1.2-rc1 # explicit version override + +# Output: +# artifacts/desktop/releases/ +# ├── preflight.xml--alpha-win-x64-portable.zip +# ├── preflight.xml--alpha-full.nupkg +# ├── preflight.xml--alpha-delta.nupkg (if a previous release exists) +# ├── RELEASES-alpha +# ├── assets.alpha.json +# └── releases.alpha.json +``` + +The recipe installs `vpk` via `dotnet tool install -g vpk` if missing, +publishes the desktop project as `win-x64` self-contained, then runs +`vpk pack --noInst` (no Setup.exe; we ship the portable form only). + +To publish a local-built bundle to GitHub manually: + +```bash +export GITHUB_TOKEN=ghp_xxx # PAT with `repo` scope +just desktop-release v0.1.2 # vpk upload github --merge --pre +``` + +## 🛰️ Auto-update internals + +The desktop reads `RELEASES-alpha` and the `*-full.nupkg` / +`*-delta.nupkg` siblings from the GitHub Release matching the **alpha +channel**. The channel name is wired in three places that must agree: + +| File | Setting | +| :----------------------------------------------------- | :---------------------------- | +| `srcs/Preflight.Desktop/UpdateService.cs` | `Channel = "alpha"` | +| `justfile` | `DESKTOP_CHANNEL := "alpha"` | +| `.github/workflows/release.yml` (desktop job) | `--channel alpha` | + +Bumping the channel name (e.g. `alpha` → `stable` once 1.0 ships) is +all three at once - missing one breaks update discovery silently. +`UpdateManager.IsInstalled` returns false in dev (running from +`bin/Debug`) and for portable extractions where Velopack's `Update.exe` +is absent, so no update activity happens during local development. ## 🔢 Versioning diff --git a/justfile b/justfile index 4c05581..14f6b63 100644 --- a/justfile +++ b/justfile @@ -16,12 +16,26 @@ set shell := ["bash", "-ceuo", "pipefail"] # ─── config ────────────────────────────────────────────────── SOLUTION := "Preflight.slnx" PROJECT := "srcs/Preflight.App/Preflight.App.csproj" +DESKTOP := "srcs/Preflight.Desktop/Preflight.Desktop.csproj" CONFIG := "Release" OUT_DIR := "artifacts/app/Preflight.App/publish" SITE_DIR := OUT_DIR + "/wwwroot" README := "README.md" BUILD_PROPS := "Directory.Build.props" +# Desktop / Velopack configuration. PackId, executable name and channel +# must be in lock-step with the values UpdateService.cs reads at runtime, +# or the auto-update will not find its RELEASES feed. +DESKTOP_PUBLISH := "artifacts/desktop/publish" +DESKTOP_RELEASES := "artifacts/desktop/releases" +DESKTOP_PACK_ID := "preflight.xml" +DESKTOP_MAIN_EXE := "preflight.xml.exe" +DESKTOP_CHANNEL := "alpha" +DESKTOP_RUNTIME := "win-x64" +DESKTOP_TITLE := "preflight.xml" +DESKTOP_AUTHORS := "kYaRick" +REPO_URL := "https://github.com/kYaRick/preflight.xml" + # ─── default ───────────────────────────────────────────────── # 📋 show all available recipes @@ -126,6 +140,106 @@ serve port="8080": @echo "🌐 serving {{SITE_DIR}} at http://localhost:{{port}}/" python3 -m http.server {{port}} --directory {{SITE_DIR}} +# ─── desktop release ───────────────────────────────────────── +# +# Velopack-based packaging for Preflight.Desktop (WPF + WebView2 shell). +# Produces a portable zip + the artifacts UpdateManager needs to discover +# updates (RELEASES-, *-full.nupkg, optional *-delta.nupkg) - +# Setup.exe is intentionally skipped via --noInst because the user- +# facing distribution channel is "download portable, run, auto-update". +# +# Local flow: +# just desktop-pack → build → publish → vpk pack +# just desktop-release v0.1.2 → above + vpk upload github (needs token) +# +# CI uses the same recipes from .github/workflows/release.yml. + +# 📦 publish the desktop shell as a self-contained win-x64 single-folder +# build into {{DESKTOP_PUBLISH}}. This is what `vpk pack` consumes via +# --packDir; running this on its own is useful for local sanity checks +# of the .exe before invoking the packager. +desktop-publish: + @echo "🖥️ publishing {{DESKTOP}} ({{CONFIG}}, {{DESKTOP_RUNTIME}}) → {{DESKTOP_PUBLISH}}" + -rm -rf {{DESKTOP_PUBLISH}} + dotnet publish {{DESKTOP}} \ + --configuration {{CONFIG}} \ + --runtime {{DESKTOP_RUNTIME}} \ + --self-contained true \ + --nologo \ + --output {{DESKTOP_PUBLISH}} + @echo "✅ desktop publish ready → {{DESKTOP_PUBLISH}}" + +# 📥 install the vpk global tool if it's missing. Idempotent - safe to run +# every time. Pinned version matches the Velopack PackageReference in +# Directory.Packages.props so the SDK and the runtime library agree on +# the on-disk RELEASES format. +desktop-vpk-install: + @if ! command -v vpk >/dev/null 2>&1; then \ + echo "🔧 installing vpk global tool…"; \ + dotnet tool install -g vpk --version 0.0.1298; \ + else \ + echo "✓ vpk already installed ($(vpk --version 2>&1 | head -1))"; \ + fi + +# 📦 pack the published desktop folder into a portable Velopack release. +# --noInst skips Setup.exe (we don't ship a managed installer). +# --channel must match UpdateService.Channel in the desktop code, or +# UpdateManager will look for RELEASES- and find none. +# Output: +# {{DESKTOP_RELEASES}}/preflight.xml-alpha-Portable.zip ← user download +# {{DESKTOP_RELEASES}}/preflight.xml-{ver}-alpha-full.nupkg +# {{DESKTOP_RELEASES}}/preflight.xml-{ver}-alpha-delta.nupkg (when prev exists) +# {{DESKTOP_RELEASES}}/RELEASES-alpha ← UpdateManager index +desktop-pack version="": desktop-vpk-install desktop-publish + @ver="{{version}}"; \ + if [[ -z "$ver" ]]; then \ + version_prefix="$(grep -oPm1 '(?<=)[^<]+' {{BUILD_PROPS}})"; \ + version_suffix="$(grep -oPm1 '(?<=)[^<]+' {{BUILD_PROPS}} || true)"; \ + ver="$version_prefix"; \ + if [[ -n "$version_suffix" ]]; then ver="$ver-$version_suffix"; fi; \ + fi; \ + echo "📦 vpk pack ($ver, channel {{DESKTOP_CHANNEL}}) → {{DESKTOP_RELEASES}}"; \ + vpk pack \ + --packId {{DESKTOP_PACK_ID}} \ + --packTitle "{{DESKTOP_TITLE}}" \ + --packAuthors "{{DESKTOP_AUTHORS}}" \ + --packVersion "$ver" \ + --packDir {{DESKTOP_PUBLISH}} \ + --mainExe {{DESKTOP_MAIN_EXE}} \ + --channel {{DESKTOP_CHANNEL}} \ + --runtime {{DESKTOP_RUNTIME}} \ + --outputDir {{DESKTOP_RELEASES}} \ + --noInst; \ + cap="{{DESKTOP_PACK_ID}}-{{DESKTOP_CHANNEL}}-Portable.zip"; \ + low="{{DESKTOP_PACK_ID}}-$ver-{{DESKTOP_CHANNEL}}-{{DESKTOP_RUNTIME}}-portable.zip"; \ + if [[ -f "{{DESKTOP_RELEASES}}/$cap" ]]; then \ + mv "{{DESKTOP_RELEASES}}/$cap" "{{DESKTOP_RELEASES}}/$low"; \ + perl -pi -e "s|\\Q$cap\\E|$low|g" "{{DESKTOP_RELEASES}}/assets.{{DESKTOP_CHANNEL}}.json"; \ + echo "📝 renamed Portable.zip → $low (assets manifest patched)"; \ + fi; \ + echo "✅ desktop pack ready → {{DESKTOP_RELEASES}}" + +# 🚀 upload the packed release to a GitHub draft release matching the tag. +# --merge lets us co-exist with the PWA archive that release.yml uploads +# in parallel. Token must come from the environment; locally that's a +# PAT exported as GITHUB_TOKEN; in CI it's the workflow's auto-provided +# secrets.GITHUB_TOKEN. +desktop-release tag: (desktop-pack) + @if [[ -z "${GITHUB_TOKEN:-}" ]]; then \ + echo "❌ GITHUB_TOKEN not set in environment"; \ + exit 1; \ + fi + @echo "🎁 vpk upload github → {{REPO_URL}} ({{tag}})" + vpk upload github \ + --outputDir {{DESKTOP_RELEASES}} \ + --channel {{DESKTOP_CHANNEL}} \ + --repoUrl {{REPO_URL}} \ + --tag {{tag}} \ + --token "$GITHUB_TOKEN" \ + --merge \ + --pre + @echo "✅ desktop artifacts uploaded to release {{tag}}" + # ─── maintenance ───────────────────────────────────────────── # 🧹 remove all build artifacts diff --git a/srcs/Preflight.App/Layout/ChangelogModal.razor b/srcs/Preflight.App/Layout/ChangelogModal.razor new file mode 100644 index 0000000..a82424f --- /dev/null +++ b/srcs/Preflight.App/Layout/ChangelogModal.razor @@ -0,0 +1,46 @@ +@* + "What's new" overlay - renders the embedded CHANGELOG.md (single source of + truth shared with GitHub releases) into the same .pf-overlay chrome used + by the command palette and shortcuts help. + + The markdown is parsed once via Markdig (cached inside ChangelogService) + and emitted as a MarkupString - Markdig's output is HTML-safe by default + (no raw HTML pass-through unless explicitly enabled in the pipeline). +*@ +@inject IStringLocalizer L +@inject ChangelogService Changelog + +
+ +
+ +@code { + [Parameter] public bool Open { get; set; } + [Parameter] public EventCallback OpenChanged { get; set; } + + private async Task CloseOnBackdrop(MouseEventArgs _) => await Close(); + private async Task Close() => await OpenChanged.InvokeAsync(false); +} diff --git a/srcs/Preflight.App/Layout/CommandPalette.razor b/srcs/Preflight.App/Layout/CommandPalette.razor index 37022b6..f2e0340 100644 --- a/srcs/Preflight.App/Layout/CommandPalette.razor +++ b/srcs/Preflight.App/Layout/CommandPalette.razor @@ -113,7 +113,7 @@ new("🖥️", L["Palette.Cmd.ThemeSystem"], null, L["Palette.Group.Theme"], Action: () => ApplyThemeAsync("system")), - new("🇬🇧", L["Language.English"], null, L["Palette.Group.Language"], + new("🇺🇸", L["Language.English"], null, L["Palette.Group.Language"], Action: () => Culture.SetAsync("en")), new("🇺🇦", L["Language.Ukrainian"], null, L["Palette.Group.Language"], Action: () => Culture.SetAsync("uk")), diff --git a/srcs/Preflight.App/Layout/ImportXmlModal.razor b/srcs/Preflight.App/Layout/ImportXmlModal.razor new file mode 100644 index 0000000..ec7a53e --- /dev/null +++ b/srcs/Preflight.App/Layout/ImportXmlModal.razor @@ -0,0 +1,236 @@ +@* + In-app XML import modal - replaces Landing's previous "open native file + picker" flow. Two equally-prominent paths in the same modal: + + 1. Drop zone - drag an autounattend.xml file from Explorer. + 2. Paste pad - paste raw XML text from clipboard. + + Plus a "Browse..." button that still falls back to the native picker + for users on platforms where DnD is awkward (touch screens, some Linux + WMs). All three feed the same UnattendXmlImporter so the result handling + is identical regardless of how the XML arrived. + + Result reporting goes through OnImported(ImportResult, fileName) so the + parent owns navigation. We only own the modal state and the input UX. +*@ +@inject IStringLocalizer L +@inject IJSRuntime JS +@inject UnattendXmlImporter Importer +@implements IAsyncDisposable + +
+ +
+ +@code { + [Parameter] public bool Open { get; set; } + [Parameter] public EventCallback OpenChanged { get; set; } + [Parameter] public EventCallback OnImported { get; set; } + + private ElementReference _dropZone; + private DotNetObjectReference? _selfRef; + private bool _attached; + private bool _busy; + private string _pasted = string.Empty; + private string? _status; + private string _statusKind = "info"; // info | warn | error | success + + /// + /// Plain DTO for the parent: result of running the importer over user + /// input plus the source name (file name or "(paste)") so the parent + /// can surface a meaningful "Imported X" message. + /// + public sealed record ImportPayload(ImportResult Result, string SourceName); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Attach the drop-zone listeners exactly once. Open/close toggles only + // visibility (the modal stays in the DOM tree to keep its open/close + // transitions running), so we don't need to re-attach on every toggle. + if (firstRender || (_attached == false && Open)) + { + _selfRef ??= DotNetObjectReference.Create(this); + try + { + await JS.InvokeVoidAsync("preflightFiles.attachDropZone", _dropZone, _selfRef); + _attached = true; + } + catch + { + /* helper not yet loaded - the OnAfterRenderAsync next render will retry */ + } + } + } + + private async Task BrowseAsync() + { + try + { + var picked = await JS.InvokeAsync("preflightFiles.pickText", ".xml"); + if (picked is null) return; + await ProcessAsync(picked.Name, picked.Text); + } + catch (Exception ex) + { + SetStatus("error", ex.Message); + } + } + + [JSInvokable] + public async Task OnFileDropped(string name, string text) + { + await ProcessAsync(name, text); + await InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public Task OnFileDroppedError(string message) + { + SetStatus("error", string.Format(L["Import.ReadFailed"].Value, message)); + return InvokeAsync(StateHasChanged); + } + + private async Task OnPasteKeyDown(KeyboardEventArgs e) + { + // Ctrl+Enter (or Cmd+Enter) submits - common shortcut for textarea + // dialogs (Slack, GitHub, Discord all use it). Plain Enter still + // inserts a newline for users still composing the paste. + if (e.Key == "Enter" && (e.CtrlKey || e.MetaKey)) + { + await ApplyPasted(); + } + } + + private async Task ApplyPasted() + { + if (string.IsNullOrWhiteSpace(_pasted)) return; + await ProcessAsync(L["Import.PasteSourceName"], _pasted); + } + + private async Task ProcessAsync(string sourceName, string xml) + { + _busy = true; + _status = null; + StateHasChanged(); + try + { + var result = Importer.Import(xml); + + // Keep failed imports inside the modal so the user sees the + // validation reason immediately and can correct/retry in place. + if (result.Status == ImportStatus.Failed) + { + var details = string.Join(" ", result.Messages); + SetStatus("error", string.Format(L["Landing.Import.Failed"].Value, details)); + return; + } + + await OnImported.InvokeAsync(new ImportPayload(result, sourceName)); + // Parent will close the modal on success - leaving it open with a + // status message would be redundant if the route changed under us. + } + catch (Exception ex) + { + SetStatus("error", ex.Message); + } + finally + { + _busy = false; + StateHasChanged(); + } + } + + private void SetStatus(string kind, string message) + { + _statusKind = kind; + _status = message; + } + + private async Task CloseOnBackdrop(MouseEventArgs _) => await Close(); + private async Task Close() + { + _pasted = string.Empty; + _status = null; + await OpenChanged.InvokeAsync(false); + } + + public async ValueTask DisposeAsync() + { + try + { + await JS.InvokeVoidAsync("preflightFiles.detachDropZone", _dropZone); + } + catch + { + /* page tearing down */ + } + _selfRef?.Dispose(); + } + + private sealed record PickedFile(string Name, string Text); +} diff --git a/srcs/Preflight.App/Layout/LanguageSwitcher.razor b/srcs/Preflight.App/Layout/LanguageSwitcher.razor index 662e272..ed5c4e9 100644 --- a/srcs/Preflight.App/Layout/LanguageSwitcher.razor +++ b/srcs/Preflight.App/Layout/LanguageSwitcher.razor @@ -23,7 +23,7 @@ private static readonly Option[] _options = [ - new("en", "🇬🇧", "Language.English"), + new("en", "🇺🇸", "Language.English"), new("uk", "🇺🇦", "Language.Ukrainian"), ]; diff --git a/srcs/Preflight.App/Layout/MainLayout.razor b/srcs/Preflight.App/Layout/MainLayout.razor index 176f704..2ba5cf7 100644 --- a/srcs/Preflight.App/Layout/MainLayout.razor +++ b/srcs/Preflight.App/Layout/MainLayout.razor @@ -3,6 +3,7 @@ @inject IStringLocalizer L @inject IJSRuntime JS @inject NavigationManager Nav +@inject ImportModalService ImportModal @@ -37,6 +38,10 @@ kYaRick · + + · MIT @@ -55,15 +60,34 @@ + + +@* + Import-XML modal lives here (not on the page that triggers it) so its + .pf-overlay backdrop-filter sits in the same stacking context as the + page header. Pages open it via ImportModalService.Open(handler) and + receive results through the handler they passed in. See + Services/ImportModalService.cs for the contract. +*@ + @code { private DesignThemeModes _themeMode = DesignThemeModes.System; private bool _paletteOpen; private bool _helpOpen; + private bool _changelogOpen; private bool _showGoTop; private DotNetObjectReference? _selfRef; private bool _pendingRouteAnimation; + private void OnImportOpenChanged(bool open) + { + if (open) return; + ImportModal.Close(); + } + // Track location to know when a real navigation happened. We keep the // listener for future hooks (analytics, scroll restoration, etc.). private string _lastLocation = string.Empty; @@ -79,8 +103,11 @@ { _lastLocation = Nav.Uri; Nav.LocationChanged += OnLocationChanged; + ImportModal.StateChanged += OnImportStateChanged; } + private void OnImportStateChanged() => InvokeAsync(StateHasChanged); + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) { _pendingRouteAnimation = e.Location != _lastLocation; @@ -170,6 +197,7 @@ public async ValueTask DisposeAsync() { Nav.LocationChanged -= OnLocationChanged; + ImportModal.StateChanged -= OnImportStateChanged; try { await JS.InvokeVoidAsync("preflightKeyboard.unregister"); } catch { /* page unload */ } _selfRef?.Dispose(); } diff --git a/srcs/Preflight.App/Layout/ModeCard.razor b/srcs/Preflight.App/Layout/ModeCard.razor index b3376b2..c0af404 100644 --- a/srcs/Preflight.App/Layout/ModeCard.razor +++ b/srcs/Preflight.App/Layout/ModeCard.razor @@ -1,30 +1,41 @@ @* A clickable mode tile used on the landing page. Three of these sit side-by-side representing Wizard / Advanced / Docs. -*@ -@* - Only the outer card carries the click handler - the CTA button is decorative and - shares the same target. Stacking two handlers (card + button) meant real-browser - clicks could fire twice and race with the navigation that follows, which looked - like "first click is ignored" because the second navigation cancelled the first. + Renders the entire card as an so view-transitions.js (capture-phase + click interceptor) can wrap the navigation in document.startViewTransition(). + Programmatic Nav.NavigateTo would have skipped that interceptor - that's why + Landing → other pages used to snap with no animation. + + OnSelect still fires (for ModeService.SwitchMode bookkeeping) because the + click interceptor does NOT call stopImmediatePropagation; only preventDefault. + Blazor's @onclick handler runs in the bubble phase and updates the active + mode while the View Transition is animating the route swap. *@ - - -
@Icon
- @Title - @Description - - @CtaLabel - -
-
+ +
+ + +
@Icon
+ @Title + @Description + + @CtaLabel + +
+
+
@code { [Parameter, EditorRequired] public string Icon { get; set; } = ""; [Parameter, EditorRequired] public string Title { get; set; } = ""; [Parameter, EditorRequired] public string Description { get; set; } = ""; [Parameter, EditorRequired] public string CtaLabel { get; set; } = ""; + [Parameter, EditorRequired] public string Href { get; set; } = ""; [Parameter] public EventCallback OnSelect { get; set; } private Task HandleClick() => OnSelect.InvokeAsync(); diff --git a/srcs/Preflight.App/Pages/Advanced/AdvancedShell.razor b/srcs/Preflight.App/Pages/Advanced/AdvancedShell.razor index d7a932d..9473619 100644 --- a/srcs/Preflight.App/Pages/Advanced/AdvancedShell.razor +++ b/srcs/Preflight.App/Pages/Advanced/AdvancedShell.razor @@ -152,41 +152,51 @@ else @_xmlLineCount -@if (_expanded) -{ -