# Nexvane Hub An Avalonia UI (MVVM) scaffold for a project-launcher "hub" app, styled to match the Nexvane editor's dark UI and the cards/buttons from the marketing site. ## Running it Requires the .NET 8 SDK. ``` dotnet restore dotnet run ``` ## Structure ``` NexvaneHub/ ├─ Models/ │ └─ ProjectModel.cs Plain data: name, directory, SDK version, etc. ├─ ViewModels/ │ ├─ ViewModelBase.cs │ ├─ MainWindowViewModel.cs Nav state, project list, Open/Delete/New commands │ └─ ProjectItemViewModel.cs ├─ Views/ │ └─ MainWindow.axaml(.cs) Top bar + sidebar nav + card list + detail panel ├─ Converters/ │ └─ EnumEqualsConverter.cs Drives nav selection & section switching ├─ Styles/ │ ├─ Theme.axaml Color palette (brushes) — edit this to re-theme │ └─ Controls.axaml Card / button / nav-item / field styles └─ App.axaml(.cs) ``` ## What's wired up vs. stubbed - The **Projects** section is fully built: card list, selection, detail panel with collapsible "Project" / "SDK" groups, New / Open / Delete. - Every field in the detail panel is a live, two-way-bound `TextBox` — **Name** (the title), **Thumbnail**, **Version**, **SDK Version**, **Default Compiler**, **Default Arch** — and edits write straight back onto the underlying `ProjectModel` via the `OnChanged` partial methods in `ProjectItemViewModel.cs`. **Directory** is the one exception: it's a plain read-only field (not a `TextBox`), since it's tied to where the project actually lives on disk rather than something you'd retype by hand. - Default Compiler / Default Arch are plain text inputs here rather than dropdowns of preset values (MSVC/Clang/GCC, x64/ARM64/Auto) — say the word and I'll swap them for `ComboBox`es with those options. - **SDK** and **Packages** nav items switch the middle pane but are placeholder content — swap the placeholder `Border` in `MainWindow.axaml` for real views when you're ready (a `SdkView.axaml` / `PackagesView.axaml` user control each, same pattern as the project list). - `OpenProjectCommand` and `DeleteProjectCommand` are stubbed in `MainWindowViewModel` — `DeleteProject` already removes it from the in-memory list, but neither touches disk. Hook `OpenProject` up to launching your editor process against `SelectedProject.Directory`, and add a confirmation dialog to `DeleteProject` before it goes near real files. - Project thumbnails are plain placeholder tiles (no image loading yet) since there's no asset pipeline in this scaffold — swap `Border.thumb` / `Border.thumbLarge` for an `Image` bound to `ThumbnailPath` once you have real screenshots to show. ## Custom title bar The window uses a fully custom title bar (`ExtendClientAreaToDecorationsHint` + `ExtendClientAreaChromeHints="NoChrome"` in `MainWindow.axaml`), so the OS's native title/caption buttons are gone and replaced with themed ones. Two things make this work, both in `MainWindow.axaml.cs`: - Dragging: with no native chrome, the OS no longer treats the top bar as draggable, so `TitleBar_PointerPressed` calls `BeginMoveDrag` by hand. The settings gear and the minimize/maximize/close buttons sit inside that same bar but consume their own `PointerPressed` before it bubbles up, so clicking them doesn't also start a drag - you don't need to carve out separate "drag zones" around them. - Minimize/maximize/close: plain `Click` handlers that flip `WindowState` or call `Close()`. This is the standard cross-platform-friendly approach (as opposed to `SystemDecorations="None"`, which also removes OS-level resize/snap behavior) but it's only been checked against the Windows screenshots you sent — Linux/macOS may need small adjustments (e.g. macOS traffic-light buttons behave differently under this hint). ## Design system - **Rounded corners (6-8px) almost everywhere** — buttons, fields, cards, the project list frame, the logo mark. The one exception is the **left sidebar buttons** (Projects/SDK/Packages/Account), which stay square and flush against each other, no radius, no margin — everything else softened. - **Flush pairs where it matters:** sidebar nav items sit directly against each other, and Open/Delete are one visual pair with no gap between them — Open's own bottom border doubles as the divider, and only the outer corners of the pair are rounded (`CornerRadius="6,6,0,0"` / `"0,0,6,6"`). - **Accent blue appears only at the instant of interaction** — a button being pressed, or a field gaining focus. Nothing stays accent-colored at rest: the active sidebar tab is a neutral highlight + bold text, and the Project/SDK section headers are neutral even while expanded. That second one needed an explicit override — Avalonia's `ToggleButton` has a built-in accent-colored `:checked` style from the Fluent theme, so `Controls.axaml` overrides `ToggleButton.sectionHeader:checked` directly; without that override the section headers would stay blue whenever expanded regardless of anything else in the stylesheet. Delete keeps its own red language on press rather than switching to blue, since a destructive action turning "accent blue" would read as reassuring rather than a warning. - Palette pulled tighter to the editor screenshot: `#1B1B1D` background, `#1F1F22` panels, `#2F6FED` accent. All of it lives in `Styles/Theme.axaml` as named brushes — change values there and it propagates everywhere. Shapes (radius, padding, borders) live in `Styles/Controls.axaml`. ## Layout fixes from the last round - **Scrollbar gutter:** both scrollable areas (the project list, the detail panel's fields) now have `Padding="0,0,14,0"` on their `ScrollViewer`, so the scrollbar has its own space instead of drawing over the last ~14px of content. - **Divider vs. hover width:** `ListBoxItem` padding was moved off the item itself and onto the content inside each row's `DataTemplate`. Previously the item's own padding shrank the divider line but not the hover/selected background, so the two didn't line up — now both span the row's full width. - **New folded into the Projects card:** the project list and the "New" button now live inside one `Border` (rounded + `ClipToBounds="True"`), with New as a bottom row separated only by a hairline — not a second, separately-bordered element below the card. - **Open/Delete got their divider back:** the fully-seamless version from last round read as one blurred shape rather than two buttons. They're still a single rounded, clipped block with zero margin between them, just with a 1px hairline down the middle so the pairing is unambiguous. - **Detail panel now belongs to the Projects tab:** the right column (`Border.detailPanel`) is `IsVisible`-bound to `CurrentSection == Projects` the same way the project list already was — it no longer stays on screen showing stale project data while browsing SDK/Packages. Note the column itself is a fixed `360` width in the outer `Grid.ColumnDefinitions`, so when it's hidden that space goes blank rather than being reclaimed by the middle column — say the word if you'd rather it collapse to `0` and let the list expand to fill it. - **Title bar menu row:** a second row (`File / Edit / View / Tools / Help`, plain text buttons, no persistent fill) now sits below the title, per your correction on the reference screenshot. Both rows live inside the same draggable `Border.topBar`, so dragging still works from either row's blank space, and the window's outer `Grid` row switched from a fixed `56` to `Auto` to fit the taller two-row bar. ## Menu dropdowns Each menu item opens a dropdown via `Button.Flyout` — Avalonia's built-in mechanism handles positioning, click-away-to-close, and Escape-to-dismiss; everything visible inside is our own styled content (`Button.dropdownItem` rows + `Border.dropdownSeparator` dividers), not the default theme's `Menu`/ `MenuItem` look. This was a deliberate choice: `MenuItem`'s built-in submenu popup pulls its background/border from deeper theme resources than a plain `Button`, so styling `Button` + `Flyout` (only two things to override: `FlyoutPresenter` for the panel chrome, `Button.dropdownItem` for the rows) was the more reliable path without being able to compile-check the result. - **Sections** are just `Border.dropdownSeparator` — a 1px divider — dropped between groups of `Button.dropdownItem` rows in the `StackPanel`. Add or remove them freely; there's no hidden logic tying them to anything. - Every dropdown item is a placeholder with no `Click` handler, same as the menu bar buttons themselves last round — clicking one closes the dropdown (that part's free, from the Flyout) but doesn't do anything yet. Wiring one up is a `Click="SomeHandler"` in the XAML plus a method in `MainWindow.axaml.cs` (or a `Command` binding if it belongs on `MainWindowViewModel` instead — e.g. "Toggle Sidebar" is a real candidate for that once the sidebar is collapsible). - The item lists (New Project, Undo, Toggle Sidebar, etc.) are my best guess at what a project-hub menu would plausibly contain, not something you asked for by name — trim, rename, or reorganize the sections however actually fits. ## Logo mark The title bar's logo badge now renders your actual `nexvane-logo.svg` (kept in `Assets/nexvane-logo.svg` for reference) instead of a placeholder "N" glyph, and spans the full height of both title-bar rows rather than being confined to the top one — like the editor's title bar. Both were part of the same request. - **Why it's inline vector, not an ``:** Avalonia has no built-in SVG support, and pulling in an SVG-rendering package would mean a new dependency I couldn't verify actually resolves/compiles without a working `dotnet restore` here. Instead, the exact path data from your SVG (all three `` elements, copied verbatim — the geometry itself isn't something I redrew or approximated) is embedded directly as Avalonia `Path` elements inside `MainWindow.axaml`, wrapped in a `Canvas` with the same `scale(0.1,-0.1)` + `translate` your SVG applies, further shifted so the mark's tight bounding box starts at `(0,0)` and a `Viewbox` scales it cleanly to size. Net effect: what renders is your logo's real geometry, using only built-in Avalonia controls — no new package, so nothing new that can fail to restore. - **The tall badge:** `Border.logoBadge` changed from a fixed `30x30` square to a `64`-wide column that stretches to fill its `Grid` cell's height — paired with moving it into its own `Grid.Column="0"` next to a `Grid.Column="1"` holding both the title row and the menu row stacked on top of each other, so the badge naturally spans both. `Border.topBar`'s own padding moved from the whole bar onto just that second column (`Margin="16,0,20,0"`), since the badge needs to sit flush in the corner, not inset. - If you'd rather use the actual `.svg` file directly (say, for reuse elsewhere as an app icon) rather than this inlined copy, that's where a package like `Avalonia.Svg.Skia` would come in — just flag it and I'll wire it up, now that there's a known-working fallback already in place if that package route hits a snag. ## Title bar height fix The oversized bar (and the stray unstyled "Close" button floating below the real controls) both trace back to the same thing: the bar's total height was being **auto-measured** through several nested layers — `Viewbox` inside `Border` inside `Grid` inside `StackPanel` inside `Border.topBar` — while `ExtendClientAreaTitleBarHeightHint="-1"` told the OS to *auto-detect* that same height from the content. Two auto-measurements that both depend on several nested layers agreeing exactly are exactly the kind of thing that's fragile to get wrong, and apparently did here. Fixed by pinning the number everywhere instead of deriving it: - `ExtendClientAreaTitleBarHeightHint="88"` — explicit, not `-1`. - The window's outer `Grid.RowDefinitions` is `"88,*"`, not `"Auto,*"`. - `Border.topBar` has an explicit `Height="88"` in `Controls.axaml`. - The menu row is `Height="32"` directly, not implied by margin/padding. 56 (title row) + 32 (menu row) = 88 everywhere, top to bottom, instead of being something four different elements had to happen to agree on through measurement. I couldn't run the app to confirm this eliminates the stray "Close" button specifically — that part reads as Windows/Avalonia injecting a fallback once the declared and auto-detected heights disagreed, which is a reasonable explanation but not one I can verify without a build. If it's still there after this, the next thing to try is `SystemDecorations="None"` instead of the `ExtendClientArea*` hints — a blunter tool that drops native resize/snap behavior, but leaves nothing for the OS to guess about. ## Logo crop + menu alignment fix Two more from the same area, addressed separately since the title bar height/Close-button issue above is parked for now: - **Logo was cropping.** The previous version scaled/positioned the mark via `Canvas.RenderTransform` (a `ScaleTransform` + `TranslateTransform`) while leaving the raw SVG coordinates untouched, relying on `Viewbox` to correctly reconcile the `Canvas`'s *declared* size against a *separately transformed* child. I re-verified the bounding-box math itself was correct (recomputed it a second way, using a full path parser rather than just tracking points, and got the same 800×899 result) — so the transform numbers weren't wrong, but I couldn't rule out the reconciliation itself as the failure point without being able to render it. Removed that uncertainty entirely: the SVG's `scale(0.1,-0.1)` + `translate` is now baked directly into every coordinate (computed once, offline), so each `Path.Data` is already in its final coordinate space — no `RenderTransform` left for `Viewbox` to reconcile against. There's also a 2% safety margin baked into the `Canvas` size now, so even a small rounding difference can't clip anything. - **Menu row wasn't aligned with the title.** `Button.menuBarItem` has `Padding="7,3"` for a comfortable hit target, but the title `TextBlock` above it has no padding — so "File" rendered 7px right of where "Nexvane" starts. Fixed with `Margin="-7,0,0,0"` on the menu row's `StackPanel`, canceling that offset out so the two rows' text lines up. ## Compacting the title bar The 88px bar had both rows *centered inside* fixed heights bigger than their actual content needed, which read as loose/padded rather than tight like the editor reference. Shrunk everything to hug its content instead of floating inside oversized rows, and brought the bar's total height down to match: - Title row: `56px → 38px`. Menu row: `32px → 24px`. Total: `88px → 62px` (same pattern as before — pinned as an explicit number in four places: `ExtendClientAreaTitleBarHeightHint`, the window's outer `Grid` row, `Border.topBar`'s `Height`, and the two rows' own heights summing to it). - Caption buttons (`38×26`, down from `44×32`), the settings gear (`26×26`, down from `34×34`), and the logo `Viewbox` (`22×24.6`, down from `26×29`) all shrank to match, so nothing looks oversized next to the tighter text. - `Button.menuBarItem`'s padding went from `7,3` to `6,2`, and the alignment-compensating margin on the menu row updated from `-7` to `-6` to match. - `Border.logoBadge` narrowed from `64px` to `56px` wide, in proportion with the shorter bar. All of this is proportional scaling from the same values as before, not a redesign — if 62px is still taller (or shorter) than you'd like, every number above is a single line in `Controls.axaml` or `MainWindow.axaml`, not something that requires touching the structure. ## Fixing the logo/text vertical balance Two related issues after compacting: the text rows visually overshooting the logo (top and bottom), and everything sitting too close to the bar's edges. Same root cause for both — the logo's `Viewbox` (`22×24.6`) was small relative to its `62px`-tall column, so it rendered as a small mark centered in a lot of invisible padding, while the title/menu rows spanned the *full* column height with only their own font-metric spacing. The logo's visible ink and the text's visible extent weren't bounded by the same space, even though they shared the same container. Fixed by giving both the same physical space to live in: - `Border.topBar` now has `Padding="0,3,0,3"` — a shared 3px inset from the bar's top and bottom that both the logo *and* the text column sit inside (it's on the shared parent, not applied separately to each side, so there's no way for them to drift out of sync again). Total height is still `62px`; the title/menu rows shrank from `38/24` to `34/22` to fit inside the now-smaller `56px` content area (`34+22=56`, `+3+3` padding `=62`). - The logo `Viewbox` grew from `22×24.6` to `43×48` — closer to filling that `56px` content height instead of floating small in the middle, so its visible bounds now roughly bracket the text block instead of being dwarfed by it. `Border.logoBadge` widened from `56px` to `60px` to give the bigger mark reasonable side clearance. - The text column's margins grew from `14,0,16,0` to `20,0,20,0` for more breathing room from both the logo and the window's right edge.