17 KiB
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 underlyingProjectModelvia theOn<X>Changedpartial methods inProjectItemViewModel.cs. Directory is the one exception: it's a plain read-only field (not aTextBox), 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
BorderinMainWindow.axamlfor real views when you're ready (aSdkView.axaml/PackagesView.axamluser control each, same pattern as the project list). OpenProjectCommandandDeleteProjectCommandare stubbed inMainWindowViewModel—DeleteProjectalready removes it from the in-memory list, but neither touches disk. HookOpenProjectup to launching your editor process againstSelectedProject.Directory, and add a confirmation dialog toDeleteProjectbefore 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.thumbLargefor anImagebound toThumbnailPathonce you have real screenshots to show.
Custom title bar
The window uses a fully custom title bar (ExtendClientAreaToDecorationsHint
ExtendClientAreaChromeHints="NoChrome"inMainWindow.axaml), so the OS's native title/caption buttons are gone and replaced with themed ones. Two things make this work, both inMainWindow.axaml.cs:
- Dragging: with no native chrome, the OS no longer treats the top bar as
draggable, so
TitleBar_PointerPressedcallsBeginMoveDragby hand. The settings gear and the minimize/maximize/close buttons sit inside that same bar but consume their ownPointerPressedbefore 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
Clickhandlers that flipWindowStateor callClose().
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
ToggleButtonhas a built-in accent-colored:checkedstyle from the Fluent theme, soControls.axamloverridesToggleButton.sectionHeader:checkeddirectly; 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:
#1B1B1Dbackground,#1F1F22panels,#2F6FEDaccent. All of it lives inStyles/Theme.axamlas named brushes — change values there and it propagates everywhere. Shapes (radius, padding, borders) live inStyles/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 theirScrollViewer, so the scrollbar has its own space instead of drawing over the last ~14px of content. - Divider vs. hover width:
ListBoxItempadding was moved off the item itself and onto the content inside each row'sDataTemplate. 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) isIsVisible-bound toCurrentSection == Projectsthe 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 fixed360width in the outerGrid.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 to0and 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 draggableBorder.topBar, so dragging still works from either row's blank space, and the window's outerGridrow switched from a fixed56toAutoto 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 ofButton.dropdownItemrows in theStackPanel. Add or remove them freely; there's no hidden logic tying them to anything. - Every dropdown item is a placeholder with no
Clickhandler, 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 aClick="SomeHandler"in the XAML plus a method inMainWindow.axaml.cs(or aCommandbinding if it belongs onMainWindowViewModelinstead — 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 workingdotnet restorehere. 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 AvaloniaPathelements insideMainWindow.axaml, wrapped in aCanvaswith the samescale(0.1,-0.1)+translateyour SVG applies, further shifted so the mark's tight bounding box starts at(0,0)and aViewboxscales 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.logoBadgechanged from a fixed30x30square to a64-wide column that stretches to fill itsGridcell's height — paired with moving it into its ownGrid.Column="0"next to aGrid.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
.svgfile directly (say, for reuse elsewhere as an app icon) rather than this inlined copy, that's where a package likeAvalonia.Svg.Skiawould 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.RowDefinitionsis"88,*", not"Auto,*". Border.topBarhas an explicitHeight="88"inControls.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(aScaleTransform+TranslateTransform) while leaving the raw SVG coordinates untouched, relying onViewboxto correctly reconcile theCanvas'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'sscale(0.1,-0.1)+translateis now baked directly into every coordinate (computed once, offline), so eachPath.Datais already in its final coordinate space — noRenderTransformleft forViewboxto reconcile against. There's also a 2% safety margin baked into theCanvassize now, so even a small rounding difference can't clip anything. - Menu row wasn't aligned with the title.
Button.menuBarItemhasPadding="7,3"for a comfortable hit target, but the titleTextBlockabove it has no padding — so "File" rendered 7px right of where "Nexvane" starts. Fixed withMargin="-7,0,0,0"on the menu row'sStackPanel, 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 outerGridrow,Border.topBar'sHeight, and the two rows' own heights summing to it). - Caption buttons (
38×26, down from44×32), the settings gear (26×26, down from34×34), and the logoViewbox(22×24.6, down from26×29) all shrank to match, so nothing looks oversized next to the tighter text. Button.menuBarItem's padding went from7,3to6,2, and the alignment-compensating margin on the menu row updated from-7to-6to match.Border.logoBadgenarrowed from64pxto56pxwide, 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.topBarnow hasPadding="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 still62px; the title/menu rows shrank from38/24to34/22to fit inside the now-smaller56pxcontent area (34+22=56,+3+3padding=62).- The logo
Viewboxgrew from22×24.6to43×48— closer to filling that56pxcontent 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.logoBadgewidened from56pxto60pxto give the bigger mark reasonable side clearance. - The text column's margins grew from
14,0,16,0to20,0,20,0for more breathing room from both the logo and the window's right edge.