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

315 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.