Add project files

This commit is contained in:
alejo committed 2026-09-19 12:38:12 -03:00
commit 97cafcfa18
16 files changed
+1540

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
bin/
obj/
.vs/
.idea/
*.user
+12
View File
@@ -0,0 +1,12 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NexvaneHub.App"
RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://NexvaneHub/Styles/Theme.axaml" />
<StyleInclude Source="avares://NexvaneHub/Styles/Controls.axaml" />
</Application.Styles>
</Application>
+28
View File
@@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using NexvaneHub.ViewModels;
using NexvaneHub.Views;
namespace NexvaneHub;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1920.000000pt" height="1080.000000pt" viewBox="0 0 1920.000000 1080.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M5760 9083 l0 -828 825 825 c454 454 825 826 825 827 0 2 -371 3
-825 3 l-825 0 0 -827z"/>
<path d="M12777 8942 l-967 -967 0 -1323 c0 -1329 4 -1597 40 -2406 11 -242
18 -442 16 -444 -7 -6 199 -360 -2594 4458 -349 602 -650 1121 -670 1154 l-35
58 -1403 -1403 -1404 -1404 0 -2820 0 -2820 970 970 970 970 0 1265 c0 696 -5
1384 -10 1530 -12 315 -36 819 -54 1125 -8 121 -12 221 -11 223 4 5 -45 89
1615 -2798 865 -1504 1590 -2766 1612 -2804 l40 -69 1434 1434 1434 1434 0
2802 c0 1542 -3 2803 -8 2803 -4 0 -442 -435 -975 -968z"/>
<path d="M12855 1820 l-900 -900 903 0 902 0 0 900 c0 495 -1 900 -3 900 -1 0
-407 -405 -902 -900z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+29
View File
@@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace NexvaneHub.Converters;
/// <summary>
/// Compares a bound enum value's string representation against ConverterParameter.
/// Used for "is this nav item selected" / "is this section visible" bindings.
/// </summary>
public class EnumEqualsConverter : IValueConverter
{
public static readonly EnumEqualsConverter Instance = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null || parameter is null)
{
return false;
}
return string.Equals(value.ToString(), parameter.ToString(), StringComparison.OrdinalIgnoreCase);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
+28
View File
@@ -0,0 +1,28 @@
using System;
namespace NexvaneHub.Models;
/// <summary>
/// Plain data describing a project entry. Kept free of INotifyPropertyChanged
/// on purpose - ProjectItemViewModel wraps this for anything the UI binds to,
/// and writes edits back onto this instance.
/// </summary>
public class ProjectModel
{
public string Name { get; set; } = string.Empty;
public DateTime LastModified { get; set; }
/// <summary>Not user-editable in the UI - tied to where the project lives on disk.</summary>
public string Directory { get; set; } = string.Empty;
public string ThumbnailPath { get; set; } = string.Empty;
public string ProjectVersion { get; set; } = "1.0.0";
public string SdkVersion { get; set; } = "1.4.2";
public string DefaultCompiler { get; set; } = "MSVC";
public string DefaultArchitecture { get; set; } = "Auto";
}
+26
View File
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<RootNamespace>NexvaneHub</RootNamespace>
<AssemblyName>NexvaneHub</AssemblyName>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace NexvaneHub;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
+314
View File
@@ -0,0 +1,314 @@
# 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 `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 `<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.
+419
View File
@@ -0,0 +1,419 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Design rules for this pass:
- Corners get a modest radius (6-8px) everywhere EXCEPT the left sidebar
buttons (nav items, Account), which stay square/flush.
- Related buttons/rows still sit flush against each other where that
made sense (project rows, the Open/Delete pair) - separated by
hairlines, not gaps.
- Accent blue appears ONLY the instant something is pressed or a field
is focused - never as a resting "this is currently selected/expanded"
color. Selected/expanded states use neutral gray + bold text instead.
Note: ToggleButton has a built-in accent-colored ":checked" style from
the Fluent theme, so it's explicitly overridden below - without that
override the Project/SDK section headers would stay blue whenever
expanded, regardless of anything set on the base selector.
-->
<!-- ===================== Top bar ===================== -->
<Style Selector="Border.topBar">
<Setter Property="Height" Value="62" />
<Setter Property="Padding" Value="0,3,0,3" />
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DividerBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<!-- Flush block spanning the full height of both title-bar rows, like
the editor's logo mark - not padded/inset, sits right in the corner.
No background fill - just the mark itself on the bar. -->
<Style Selector="Border.logoBadge">
<Setter Property="Width" Value="60" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="TextBlock.appTitle">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.appTitleMuted">
<Setter Property="FontSize" Value="14" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="Margin" Value="6,0,0,0" />
</Style>
<Style Selector="Button.iconButton">
<Setter Property="Width" Value="26" />
<Setter Property="Height" Value="26" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.iconButton:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.iconButton:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- Custom window caption buttons (minimize/maximize/close), replacing
the native ones now that the title bar is fully custom-drawn. Square
on purpose - this mimics standard OS caption buttons, which are
always flush rectangles regardless of the rest of the theme. -->
<Style Selector="Button.captionButton">
<Setter Property="Width" Value="38" />
<Setter Property="Height" Value="26" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.captionButton:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.captionButton:pointerover">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Style>
<Style Selector="Button.captionButton:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- Close keeps the same red language Delete uses, not accent blue. -->
<Style Selector="Button.captionButtonClose:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
</Style>
<Style Selector="Button.captionButtonClose:pointerover">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.captionButtonClose:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerPressedBrush}" />
</Style>
<!-- Secondary menu row (File/Edit/View/Tools/Help), sitting below the
title row rather than above it. Plain text items - background only
appears on hover/press, no border, no persistent fill. -->
<Style Selector="Button.menuBarItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="6,2" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="12.5" />
</Style>
<Style Selector="Button.menuBarItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.menuBarItem:pointerover">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Style>
<Style Selector="Button.menuBarItem:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="Button.menuBarItem:pressed">
<Setter Property="Foreground" Value="White" />
</Style>
<!-- Dropdown panel opened by a menu bar item's Button.Flyout. Applies to
every Flyout in the app - fine for now since menus are the only use. -->
<Style Selector="FlyoutPresenter">
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="6" />
</Style>
<!-- One row inside a dropdown. -->
<Style Selector="Button.dropdownItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Padding" Value="10,7" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Button.dropdownItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.dropdownItem:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="Button.dropdownItem:pressed">
<Setter Property="Foreground" Value="White" />
</Style>
<!-- Divides a dropdown into sections. -->
<Style Selector="Border.dropdownSeparator">
<Setter Property="Height" Value="1" />
<Setter Property="Background" Value="{StaticResource DividerBrush}" />
<Setter Property="Margin" Value="6,4" />
</Style>
<!-- ===================== Sidebar navigation (stays square/flush) ===================== -->
<Style Selector="Border.sidebar">
<Setter Property="Background" Value="{StaticResource SidebarBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DividerBrush}" />
<Setter Property="BorderThickness" Value="0,0,1,0" />
<Setter Property="Padding" Value="0" />
</Style>
<Style Selector="Button.navItem">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,11" />
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="13.5" />
</Style>
<Style Selector="Button.navItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.navItem:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- Active tab: neutral highlight + bold text, no accent at rest. -->
<Style Selector="Button.navItem.selected /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
</Style>
<Style Selector="Button.navItem.selected">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.accountItem">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="BorderBrush" Value="{StaticResource DividerBrush}" />
<Setter Property="Padding" Value="16,11" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Button.accountItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.accountItem:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- ===================== Project list (middle column) ===================== -->
<Style Selector="Border.listContainer">
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="ListBox.projectList">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="0" />
</Style>
<!-- Rows stay flush inside the rounded container - no radius, no margin.
Padding lives on the row content (below), not here, so the hover/
selected background and the divider line both span the exact same
full-bleed width instead of one being inset from the other. -->
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource RowSelectedBrush}" />
</Style>
<Style Selector="Border.listRow">
<Setter Property="BorderBrush" Value="{StaticResource DividerBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<Style Selector="Border.thumb">
<Setter Property="Width" Value="56" />
<Setter Property="Height" Value="56" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
</Style>
<Style Selector="Border.thumbLarge">
<Setter Property="Height" Value="170" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
</Style>
<Style Selector="TextBlock.cardTitle">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Medium" />
</Style>
<Style Selector="TextBlock.cardSubtitle">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="{StaticResource TextMutedBrush}" />
<Setter Property="Margin" Value="0,3,0,0" />
</Style>
<!-- ===================== Detail panel (right column) ===================== -->
<Style Selector="Border.detailPanel">
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DividerBrush}" />
<Setter Property="BorderThickness" Value="1,0,0,0" />
</Style>
<Style Selector="TextBlock.fieldLabel">
<Setter Property="FontSize" Value="12.5" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- Read-only display (Directory only) - a plain box, not an input. -->
<Style Selector="Border.fieldBox">
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="10,7" />
</Style>
<Style Selector="TextBlock.fieldValueMuted">
<Setter Property="FontSize" Value="12.5" />
<Setter Property="Foreground" Value="{StaticResource TextMutedBrush}" />
</Style>
<!-- Editable fields (everything except Directory). -->
<Style Selector="TextBox.input">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="10,7" />
<Setter Property="FontSize" Value="12.5" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="SelectionBrush" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="TextBox.input:pointerover">
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
</Style>
<!-- Focusing an input is the field-level equivalent of "clicked". -->
<Style Selector="TextBox.input:focus">
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="TextBox.titleInput">
<Setter Property="FontSize" Value="17" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="ToggleButton.sectionHeader">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="FontSize" Value="13" />
</Style>
<!-- Override the Fluent theme's default accent-colored ":checked" look -
expanded is a state, not a click, so it stays neutral. -->
<Style Selector="ToggleButton.sectionHeader:checked /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
</Style>
<Style Selector="ToggleButton.sectionHeader:checked">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Style>
<Style Selector="ToggleButton.sectionHeader:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- ===================== Buttons ===================== -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="14,10" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource RowSelectedBrush}" />
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource DangerBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DangerBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="14,9" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#2A1416" />
</Style>
<Style Selector="Button.danger:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerPressedBrush}" />
</Style>
<Style Selector="Button.danger:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.newProject">
<Setter Property="Background" Value="{StaticResource CardBackgroundBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="14,12" />
<Setter Property="FontSize" Value="13.5" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.newProject:pointerover">
<Setter Property="Background" Value="{StaticResource CardBackgroundHoverBrush}" />
</Style>
<Style Selector="Button.newProject:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
</Styles>
+60
View File
@@ -0,0 +1,60 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Palette matched to the editor screenshot: near-black background/panels,
barely-visible dividers, and a single blue accent. The accent is used
ONLY for a selected sidebar tab and the instant a button is pressed -
everything else (hover, resting buttons, fields) stays neutral gray.
-->
<Styles.Resources>
<ResourceDictionary>
<Color x:Key="AppBackgroundColor">#1B1B1D</Color>
<Color x:Key="PanelBackgroundColor">#1F1F22</Color>
<Color x:Key="SidebarBackgroundColor">#18181A</Color>
<Color x:Key="CardBackgroundColor">#222225</Color>
<Color x:Key="CardBackgroundHoverColor">#2A2A2E</Color>
<Color x:Key="RowSelectedColor">#333338</Color>
<Color x:Key="CardBorderColor">#2C2C30</Color>
<Color x:Key="DividerColor">#2A2A2E</Color>
<Color x:Key="AccentColor">#2F6FED</Color>
<Color x:Key="DangerColor">#E5484D</Color>
<Color x:Key="DangerPressedColor">#C53A3F</Color>
<Color x:Key="TextPrimaryColor">#F2F2F3</Color>
<Color x:Key="TextSecondaryColor">#9A9AA0</Color>
<Color x:Key="TextMutedColor">#6C6C72</Color>
<SolidColorBrush x:Key="AppBackgroundBrush" Color="{StaticResource AppBackgroundColor}" />
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="{StaticResource PanelBackgroundColor}" />
<SolidColorBrush x:Key="SidebarBackgroundBrush" Color="{StaticResource SidebarBackgroundColor}" />
<SolidColorBrush x:Key="CardBackgroundBrush" Color="{StaticResource CardBackgroundColor}" />
<SolidColorBrush x:Key="CardBackgroundHoverBrush" Color="{StaticResource CardBackgroundHoverColor}" />
<SolidColorBrush x:Key="RowSelectedBrush" Color="{StaticResource RowSelectedColor}" />
<SolidColorBrush x:Key="CardBorderBrush" Color="{StaticResource CardBorderColor}" />
<SolidColorBrush x:Key="DividerBrush" Color="{StaticResource DividerColor}" />
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}" />
<SolidColorBrush x:Key="DangerPressedBrush" Color="{StaticResource DangerPressedColor}" />
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimaryColor}" />
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}" />
<SolidColorBrush x:Key="TextMutedBrush" Color="{StaticResource TextMutedColor}" />
</ResourceDictionary>
</Styles.Resources>
<Style Selector="Window">
<Setter Property="Background" Value="{StaticResource AppBackgroundBrush}" />
<Setter Property="FontFamily" Value="Inter, Segoe UI, Arial" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Style>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Style>
<Style Selector="ScrollViewer">
<Setter Property="Background" Value="Transparent" />
</Style>
</Styles>
+128
View File
@@ -0,0 +1,128 @@
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NexvaneHub.Models;
namespace NexvaneHub.ViewModels;
public enum HubSection
{
Projects,
Sdk,
Packages,
}
public partial class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
_projects = new ObservableCollection<ProjectItemViewModel>
{
new(new ProjectModel
{
Name = "Project A",
LastModified = new DateTime(2026, 9, 6),
Directory = @"C:\Dev\ProjectA",
ProjectVersion = "1.2.0",
SdkVersion = "1.4.2",
DefaultCompiler = "MSVC",
DefaultArchitecture = "Auto",
}),
new(new ProjectModel
{
Name = "Project B",
LastModified = new DateTime(2026, 8, 2),
Directory = @"C:\Dev\ProjectB",
ProjectVersion = "0.9.4",
SdkVersion = "1.3.0",
DefaultCompiler = "Clang",
DefaultArchitecture = "x64",
}),
new(new ProjectModel
{
Name = "Project C",
LastModified = new DateTime(2026, 6, 18),
Directory = @"C:\Dev\ProjectC",
ProjectVersion = "1.0.0",
SdkVersion = "1.4.2",
DefaultCompiler = "MSVC",
DefaultArchitecture = "Auto",
}),
new(new ProjectModel
{
Name = "Project D",
LastModified = new DateTime(2026, 5, 30),
Directory = @"C:\Dev\ProjectD",
ProjectVersion = "0.4.1",
SdkVersion = "1.2.7",
DefaultCompiler = "GCC",
DefaultArchitecture = "ARM64",
}),
};
_selectedProject = _projects.Count > 0 ? _projects[0] : null;
}
[ObservableProperty]
private HubSection _currentSection = HubSection.Projects;
[ObservableProperty]
private ObservableCollection<ProjectItemViewModel> _projects;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(OpenProjectCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteProjectCommand))]
private ProjectItemViewModel? _selectedProject;
[ObservableProperty]
private bool _isProjectSectionExpanded = true;
[ObservableProperty]
private bool _isSdkSectionExpanded = true;
[RelayCommand]
private void NavigateTo(HubSection section)
{
CurrentSection = section;
}
[RelayCommand(CanExecute = nameof(HasSelectedProject))]
private void OpenProject()
{
// TODO: hook this up to actually launching the editor process
// against SelectedProject.Directory.
}
[RelayCommand(CanExecute = nameof(HasSelectedProject))]
private void DeleteProject()
{
if (SelectedProject is null)
{
return;
}
var index = Projects.IndexOf(SelectedProject);
Projects.Remove(SelectedProject);
SelectedProject = Projects.Count == 0
? null
: Projects[Math.Min(index, Projects.Count - 1)];
}
private bool HasSelectedProject() => SelectedProject is not null;
[RelayCommand]
private void CreateNewProject()
{
var project = new ProjectItemViewModel(new ProjectModel
{
Name = $"New Project {Projects.Count + 1}",
LastModified = DateTime.Now,
Directory = @"C:\Dev\NewProject",
});
Projects.Insert(0, project);
SelectedProject = project;
}
}
+65
View File
@@ -0,0 +1,65 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using NexvaneHub.Models;
namespace NexvaneHub.ViewModels;
/// <summary>
/// Editable wrapper around a ProjectModel. Every property here is a live,
/// bindable field that writes straight back onto the wrapped Model when
/// changed - except Directory, which stays read-only since it's tied to the
/// project's location on disk rather than something you'd retype.
/// </summary>
public partial class ProjectItemViewModel : ViewModelBase
{
public ProjectItemViewModel(ProjectModel model)
{
Model = model;
_name = model.Name;
_lastModified = model.LastModified;
_directory = model.Directory;
_thumbnailPath = model.ThumbnailPath;
_projectVersion = model.ProjectVersion;
_sdkVersion = model.SdkVersion;
_defaultCompiler = model.DefaultCompiler;
_defaultArchitecture = model.DefaultArchitecture;
}
public ProjectModel Model { get; }
public string LastModifiedDisplay => LastModified.ToString("dd-MM-yyyy");
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(LastModifiedDisplay))]
private DateTime _lastModified;
[ObservableProperty]
private string _name;
/// <summary>Read-only in the UI - displayed via a plain field, not a TextBox.</summary>
[ObservableProperty]
private string _directory;
[ObservableProperty]
private string _thumbnailPath;
[ObservableProperty]
private string _projectVersion;
[ObservableProperty]
private string _sdkVersion;
[ObservableProperty]
private string _defaultCompiler;
[ObservableProperty]
private string _defaultArchitecture;
partial void OnNameChanged(string value) => Model.Name = value;
partial void OnThumbnailPathChanged(string value) => Model.ThumbnailPath = value;
partial void OnProjectVersionChanged(string value) => Model.ProjectVersion = value;
partial void OnSdkVersionChanged(string value) => Model.SdkVersion = value;
partial void OnDefaultCompilerChanged(string value) => Model.DefaultCompiler = value;
partial void OnDefaultArchitectureChanged(string value) => Model.DefaultArchitecture = value;
}
+7
View File
@@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace NexvaneHub.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}
+334
View File
@@ -0,0 +1,334 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:NexvaneHub.ViewModels"
xmlns:conv="using:NexvaneHub.Converters"
xmlns:avc="using:Avalonia.Data.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="NexvaneHub.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="Nexvane Hub"
Width="1180" Height="740"
MinWidth="920" MinHeight="600"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="62">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid RowDefinitions="62,*">
<!-- ===== Title bar (fully custom - see MainWindow.axaml.cs for the
drag/minimize/maximize/close handlers this wires up to) ===== -->
<Border Grid.Row="0" Classes="topBar" PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="Auto,*">
<!-- Logo spans the full height of both rows below, flush in the
top-left corner (like the editor's), not confined to the
title row alone. Vector geometry lifted straight from the
provided nexvane-logo.svg - see Assets/nexvane-logo.svg for
the source. Coordinates have the SVG's scale/translate baked
in directly (computed offline) rather than applied via
RenderTransform, so there's no Viewbox+transform reconciliation
for anything to get wrong - what's below is the tight bounding
box of the mark itself, plus a small safety margin. Sized to
fill most of its column's height (Border.topBar's padding trims
a few px off each end) rather than floating small in the
middle, so the text rows don't visually overshoot the mark. -->
<Border Grid.Column="0" Classes="logoBadge">
<Viewbox Width="36" Height="40" Stretch="Uniform" HorizontalAlignment="Right" VerticalAlignment="Center">
<Canvas Width="835.96" Height="934.96">
<Path Fill="White" Data="M 17.980,100.680 L 17.980,183.480 L 100.480,100.980 C 145.880,55.580 182.980,18.380 182.980,18.280 C 182.980,18.080 145.880,17.980 100.480,17.980 L 17.980,17.980 L 17.980,100.680" />
<Path Fill="White" Data="M 719.680,114.780 L 622.980,211.480 L 622.980,343.780 C 622.980,476.680 623.380,503.480 626.980,584.380 C 628.080,608.580 628.780,628.580 628.580,628.780 C 627.880,629.380 648.480,664.780 369.180,182.980 C 334.280,122.780 304.180,70.880 302.180,67.580 L 298.680,61.780 L 158.380,202.080 L 17.980,342.480 L 17.980,624.480 L 17.980,906.480 L 114.980,809.480 L 211.980,712.480 L 211.980,585.980 C 211.980,516.380 211.480,447.580 210.980,432.980 C 209.780,401.480 207.380,351.080 205.580,320.480 C 204.780,308.380 204.380,298.380 204.480,298.180 C 204.880,297.680 199.980,289.280 365.980,577.980 C 452.480,728.380 524.980,854.580 527.180,858.380 L 531.180,865.280 L 674.580,721.880 L 817.980,578.480 L 817.980,298.280 C 817.980,144.080 817.680,17.980 817.180,17.980 C 816.780,17.980 772.980,61.480 719.680,114.780" />
<Path Fill="White" Data="M 727.480,826.980 L 637.480,916.980 L 727.780,916.980 L 817.980,916.980 L 817.980,826.980 C 817.980,777.480 817.880,736.980 817.680,736.980 C 817.580,736.980 776.980,777.480 727.480,826.980" />
</Canvas>
</Viewbox>
</Border>
<StackPanel Grid.Column="1" Margin="20,0,20,0">
<Grid Height="34" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Classes="appTitle" Text="Nexvane" />
<TextBlock Classes="appTitleMuted" Text="Hub" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Spacing="0">
<Button Classes="iconButton" Content="&#9881;" ToolTip.Tip="Settings" Margin="0,0,8,0" />
<Button Classes="captionButton" Content="&#8212;" Click="MinimizeButton_Click" ToolTip.Tip="Minimize" />
<Button Classes="captionButton" Content="&#9633;" Click="MaximizeButton_Click" ToolTip.Tip="Maximize" />
<Button Classes="captionButton captionButtonClose" Content="&#10005;" Click="CloseButton_Click" ToolTip.Tip="Close" />
</StackPanel>
</Grid>
<!-- Menu row sits below the title, not above it. Fixed height
(34 + 22 = 56, plus Border.topBar's 3px top/bottom padding =
62 total) rather than auto-sized from padding/margin, so the
bar's total height is a known number instead of something
several nested layers of content have to happen to agree on.
Both rows hug their content tightly rather than centering
inside oversized fixed heights, and the shared 3px padding on
Border.topBar keeps everything off the bar's very top/bottom
edges. Each item opens a dropdown via Button.Flyout - Avalonia
handles positioning, click-away-to-close, and Escape for us;
the content inside is ours. Margin.Left is negative to cancel
out Button.menuBarItem's own 6px left padding - otherwise
"File"'s text sits right of where "Nexvane" starts, since the
title TextBlock has no padding of its own to match against. -->
<StackPanel Orientation="Horizontal" Height="22" VerticalAlignment="Top" Spacing="12" Margin="-6,0,0,0">
<Button Classes="menuBarItem" Content="File">
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel MinWidth="190">
<Button Classes="dropdownItem" Content="New Project" />
<Button Classes="dropdownItem" Content="Open Project" />
<Border Classes="dropdownSeparator" />
<Button Classes="dropdownItem" Content="Save" />
<Button Classes="dropdownItem" Content="Save As..." />
<Border Classes="dropdownSeparator" />
<Button Classes="dropdownItem" Content="Exit" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Classes="menuBarItem" Content="Edit">
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel MinWidth="190">
<Button Classes="dropdownItem" Content="Undo" />
<Button Classes="dropdownItem" Content="Redo" />
<Border Classes="dropdownSeparator" />
<Button Classes="dropdownItem" Content="Preferences" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Classes="menuBarItem" Content="View">
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel MinWidth="190">
<Button Classes="dropdownItem" Content="Toggle Sidebar" />
<Button Classes="dropdownItem" Content="Toggle Detail Panel" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Classes="menuBarItem" Content="Tools">
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel MinWidth="190">
<Button Classes="dropdownItem" Content="SDK Manager" />
<Button Classes="dropdownItem" Content="Package Manager" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Classes="menuBarItem" Content="Help">
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel MinWidth="190">
<Button Classes="dropdownItem" Content="Documentation" />
<Border Classes="dropdownSeparator" />
<Button Classes="dropdownItem" Content="About Nexvane Hub" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</Border>
<!-- ===== Body ===== -->
<Grid Grid.Row="1" ColumnDefinitions="210,*,360">
<!-- Sidebar navigation -->
<Border Grid.Column="0" Classes="sidebar">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0">
<Button Classes="navItem"
Classes.selected="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Projects}"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:HubSection.Projects}"
Content="Projects" />
<Button Classes="navItem"
Classes.selected="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Sdk}"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:HubSection.Sdk}"
Content="SDK" />
<Button Classes="navItem"
Classes.selected="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Packages}"
Command="{Binding NavigateToCommand}"
CommandParameter="{x:Static vm:HubSection.Packages}"
Content="Packages" />
</StackPanel>
<Button Grid.Row="1" Classes="accountItem" Content="Account" />
</Grid>
</Border>
<!-- Middle column: project list (with New folded in as its footer) / other sections -->
<Grid Grid.Column="1">
<!-- Everything for the Projects section - list + New - lives inside
ONE rounded, clipped container, so New reads as part of the
card rather than a separate floating button below it. -->
<Border Classes="listContainer"
Margin="16"
IsVisible="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Projects}">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Padding="0,0,14,0">
<ListBox Classes="projectList"
ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ProjectItemViewModel">
<Border Classes="listRow">
<Grid ColumnDefinitions="56,*" Margin="14,10">
<Border Grid.Column="0" Classes="thumb" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<TextBlock Classes="cardTitle" Text="{Binding Name}" />
<TextBlock Classes="cardSubtitle" Text="{Binding LastModifiedDisplay}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Border Grid.Row="1" BorderBrush="{StaticResource DividerBrush}" BorderThickness="0,1,0,0">
<Button Classes="newProject"
Content="+ New"
Command="{Binding CreateNewProjectCommand}"
CornerRadius="0"
BorderThickness="0"
Margin="0" />
</Border>
</Grid>
</Border>
<Border Padding="20"
IsVisible="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Sdk}">
<TextBlock Classes="fieldLabel" Text="SDK management goes here." />
</Border>
<Border Padding="20"
IsVisible="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Packages}">
<TextBlock Classes="fieldLabel" Text="Installed packages go here." />
</Border>
</Grid>
<!-- Right column: selected project detail - only part of the Projects
tab, not shown while viewing SDK/Packages. -->
<Border Grid.Column="2" Classes="detailPanel"
IsVisible="{Binding CurrentSection, Converter={x:Static conv:EnumEqualsConverter.Instance}, ConverterParameter=Projects}">
<Grid IsVisible="{Binding SelectedProject, Converter={x:Static avc:ObjectConverters.IsNotNull}}"
RowDefinitions="Auto,*,Auto"
Margin="18">
<TextBox Grid.Row="0"
Classes="input titleInput"
Text="{Binding SelectedProject.Name, Mode=TwoWay}" />
<ScrollViewer Grid.Row="1" Margin="0,14,0,0" Padding="0,0,14,0">
<StackPanel>
<Border Classes="thumbLarge" />
<ToggleButton Classes="sectionHeader"
Content="&#9662; Project"
IsChecked="{Binding IsProjectSectionExpanded}"
Margin="0,16,0,0" />
<StackPanel IsVisible="{Binding IsProjectSectionExpanded}" Margin="0,10,0,0" Spacing="10">
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Directory" />
<Border Grid.Column="1" Classes="fieldBox">
<TextBlock Classes="fieldValueMuted"
Text="{Binding SelectedProject.Directory}"
TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Thumbnail" />
<TextBox Grid.Column="1"
Classes="input"
Text="{Binding SelectedProject.ThumbnailPath, Mode=TwoWay}"
Watermark="No thumbnail set" />
</Grid>
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Version" />
<TextBox Grid.Column="1"
Classes="input"
Text="{Binding SelectedProject.ProjectVersion, Mode=TwoWay}" />
</Grid>
</StackPanel>
<ToggleButton Classes="sectionHeader"
Content="&#9662; SDK"
IsChecked="{Binding IsSdkSectionExpanded}"
Margin="0,14,0,0" />
<StackPanel IsVisible="{Binding IsSdkSectionExpanded}" Margin="0,10,0,0" Spacing="10">
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Version" />
<TextBox Grid.Column="1"
Classes="input"
Text="{Binding SelectedProject.SdkVersion, Mode=TwoWay}" />
</Grid>
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Def. Compiler" />
<TextBox Grid.Column="1"
Classes="input"
Text="{Binding SelectedProject.DefaultCompiler, Mode=TwoWay}" />
</Grid>
<Grid ColumnDefinitions="100,*">
<TextBlock Classes="fieldLabel" Text="Def. Arch" />
<TextBox Grid.Column="1"
Classes="input"
Text="{Binding SelectedProject.DefaultArchitecture, Mode=TwoWay}" />
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>
<!-- Open/Delete as one rounded, clipped block with a hairline
divider between them - a visible seam so the pair doesn't
read as one smeared-together shape, but still no gap/margin
between the two buttons themselves. -->
<Border Grid.Row="2"
Margin="0,16,0,0"
CornerRadius="8"
ClipToBounds="True"
BorderBrush="{StaticResource CardBorderBrush}"
BorderThickness="1">
<StackPanel Spacing="0">
<Button Classes="primary"
Content="Open"
Command="{Binding OpenProjectCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="0"
BorderThickness="0" />
<Border Height="1" Background="{StaticResource CardBorderBrush}" />
<Button Classes="danger"
Content="Delete"
Command="{Binding DeleteProjectCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
CornerRadius="0"
BorderThickness="0" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Window>
+43
View File
@@ -0,0 +1,43 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace NexvaneHub.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
// The title bar has no native chrome anymore (ExtendClientAreaChromeHints
// = NoChrome), so dragging the window has to be done by hand. Buttons in
// the title bar (settings gear, minimize/maximize/close) consume their
// own PointerPressed before it reaches here, so this only fires for
// clicks on the blank part of the bar.
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
BeginMoveDrag(e);
}
}
private void MinimizeButton_Click(object? sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void MaximizeButton_Click(object? sender, RoutedEventArgs e)
{
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
}
private void CloseButton_Click(object? sender, RoutedEventArgs e)
{
Close();
}
}