2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00
2026-09-19 12:38:12 -03:00

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 On<X>Changed 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 ComboBoxes 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 <Image>: 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 <path> 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.
S
Description
No description provided
Readme
57 KiB
0 Stars 2 Watchers 0 Forks
Languages
C# 100%