Added multi-currency support, account/budget management, and settings
All checks were successful
Build Linux / build (push) Successful in 1m8s

- Primary account determines app-wide reference currency; all totals, charts, and summaries convert to it automatically using live rates

- Transactions show both converted and original amounts for cross-currency accounts; IsMultiCurrency recalculates on primary currency change

- Exchange rates fetched live on account save and broadcast via RatesRefreshed so all views update without a restart

- Account create/edit/delete with currency, icon, color, and primary toggle

- Budget create/edit/delete; savings goal dialog

- Settings view: display name, avatar upload, theme, language

- Removed currency selector from Settings (follows primary account)

- Fixed account list sort: primary first, then oldest CreatedAt, per group

- Fixed total balance overlap in dashboard accounts card
This commit is contained in:
2026-04-03 02:39:51 +03:00
parent 1f99e49dec
commit d8dea1913a
103 changed files with 3160 additions and 698 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")",
"Bash(dotnet build:*)"
]
},
"spinnerTipsEnabled": true
}

View File

@@ -0,0 +1,258 @@
---
name: avalonia
description: >
Use when working on any Avalonia UI code — AXAML, control styling, bindings,
control templates, animations, custom controls, platform differences, or
LiveCharts2/Svg.Skia integration. Triggers on questions about Avalonia
controls, properties, ControlThemes, styles, pseudo-classes, DataTemplates,
ViewLocator, or any "how do I do X in Avalonia" question.
---
# Avalonia UI Skill
You are working in an Avalonia UI project. This skill gives you accurate,
verified knowledge about Avalonia and prevents hallucinating WPF-style patterns
that do not work in Avalonia.
---
## Step 1 — Check before you answer
**NEVER** answer from memory alone for:
- Specific control properties or template part names
- Pseudo-class selectors (`:pointerover`, `:pressed`, `:focus`, etc.)
- Animation API (`Animation`, `KeyFrame`, `Cue`, `Easing` classes)
- `ControlTheme` vs `Style` syntax differences
- Platform-specific behaviors (mobile vs desktop)
- LiveCharts2 or Svg.Skia properties
**Always verify** using one of these sources in order:
1. **Official docs**: `https://docs.avaloniaui.net/docs/reference/controls/{control-name}`
2. **GitHub source** (most reliable for exact property names):
`https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/{ControlName}.cs`
3. **Avalonia samples**: `https://github.com/AvaloniaUI/Avalonia.Samples`
For styling/theming questions also check:
- `https://github.com/AvaloniaUI/Avalonia/tree/master/src/Avalonia.Themes.Fluent/Controls`
---
## Step 2 — Core Avalonia vs WPF differences
These are frequent sources of errors. Apply automatically:
### Styling
```xml
<!-- Avalonia: CSS-like selectors -->
<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Blue"/>
</Style>
<!-- NOT WPF DataTriggers — those do not exist in Avalonia -->
<!-- NOT WPF Triggers — use pseudo-classes instead -->
```
### ControlTheme (Avalonia 11+)
```xml
<!-- For re-theming built-in controls use ControlTheme, not Style -->
<ControlTheme x:Key="{x:Type Button}" TargetType="Button">
<Setter Property="Template">
<ControlTemplate>...</ControlTemplate>
</Setter>
</ControlTheme>
```
### Bindings
```xml
<!-- x:CompileBindings="True" (default) requires x:DataType -->
<!-- Use x:CompileBindings="False" on shell/dynamic views -->
<!-- DynamicResource NOT StaticResource for theme colors -->
<!-- No ElementName binding across UserControl boundaries — use RelativeSource or pass via property -->
```
### No DataTriggers
Avalonia has no DataTriggers. Use instead:
- `Classes.myClass="{Binding SomeBool}"` + style on `.myClass`
- `IsVisible="{Binding SomeBool}"`
- `MultiBinding` with converter
### x:Name in code-behind
`x:Name` does NOT create direct fields in Avalonia. Access named controls via:
```csharp
var btn = this.Get<Button>("PART_Button"); // throws if not found
var btn = this.FindControl<Button>("PART_Button"); // returns null if not found
// TranslateTransform cannot have x:Name — access via RenderTransform:
var tf = (TranslateTransform)someControl.RenderTransform!;
```
### Animations in code-behind
```csharp
var animation = new Animation
{
Duration = TimeSpan.FromMilliseconds(320),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(TranslateTransform.YProperty, 0d) } },
new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(TranslateTransform.YProperty, 300d) } }
}
};
await animation.RunAsync(targetControl);
```
### Platform detection
```csharp
bool isMobile = ApplicationLifetime is ISingleViewApplicationLifetime;
// App.IsMobile is the project's cached version
```
---
## Step 3 — Known Avalonia gotchas from this project
### ViewLocator (no DataTemplates in AXAML)
```csharp
// ViewLocator auto-resolves: {Name}ViewModel → {Name}View (desktop) or {Name}ViewMobile (mobile)
// Do NOT register DataTemplates in AXAML
// Register FuncDataTemplate in App.axaml.cs code-behind if needed
```
### Observable property initialization order
Object initializers set properties one by one — `partial void On{Property}Changed` fires
immediately, before other properties are set. **Never** trigger initialization logic from
property changed handlers when the VM needs multiple properties. Always use an explicit
`Initialize()` method called after the object initializer.
```csharp
// WRONG
partial void OnTransactionsChanged(List<Transaction> value) => ProcessData(); // Categories may be null
// RIGHT
var vm = new MyViewModel { Transactions = t, Categories = c, Accounts = a };
vm.Initialize(); // all props guaranteed set
```
### ObservableCollection mutations
Mutating a `List<T>` never triggers binding updates. Replace the entire collection:
```csharp
MyList = new List<T>(newItems); // triggers OnPropertyChanged
// NOT: MyList.Add(item); // binding won't update for List<T>
```
For `ObservableCollection<T>`, `.Add()` and `.Remove()` do trigger updates but `.Clear()` +
re-add causes a full re-render. Prefer replacing the collection for large updates.
### ScrollViewer + LiveCharts2
LiveCharts2 CartesianChart intercepts scroll events. Forward them manually:
```csharp
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var charts = this.GetVisualDescendants().OfType<CartesianChart>();
foreach (var chart in charts)
chart.AddHandler(PointerWheelChangedEvent, OnChartScroll, RoutingStrategies.Tunnel);
}
private void OnChartScroll(object? sender, PointerWheelEventArgs e)
{
var sv = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();
if (sv is null) return;
sv.Offset = new Vector(sv.Offset.X, sv.Offset.Y - e.Delta.Y * sv.SmallChange.Height * 3);
e.Handled = true;
}
```
### Half-donut chart
```xml
<Border Height="150" ClipToBounds="True">
<lvc:PieChart Series="{Binding ...}" Height="300" Margin="0,0,0,-150"
InitialRotation="-180" MaxAngle="180" LegendPosition="Hidden"
ZoomMode="None"/>
</Border>
```
### Svg.Skia CSS
```xml
<!-- stroke-based (Lucide icons) -->
<Svg Path="../Assets/Icons/name.svg" Css="{DynamicResource SvgBlue}"/>
<!-- SvgBlue resource = "path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #7B9CFF; }" -->
<!-- Fill-based icons use SvgFillBlue etc. -->
```
### Mobile-specific AXAML rules
- No `BoxShadow` — GPU expensive, causes jitter
- No `MinWidth`/`MinHeight` on UserControl root
- Add `Classes="mobile"` to root element for mobile-specific style overrides
- Use `VirtualizingStackPanel` in ItemsControl for long lists
- Page size 10 on mobile vs 25 on desktop
### CalendarDayButton / Calendar
Avalonia's Calendar uses `CalendarDayButton` not `CalendarDayItem`.
Template parts: `PART_MonthView`, `PART_YearView`, `PART_HeaderButton`, `PART_PreviousButton`, `PART_NextButton`.
### FlyoutPresenter
```xml
<!-- Custom transparent flyout presenter must be a ControlTheme in Resources, not Styles -->
<ControlTheme x:Key="TransparentFlyoutPresenter" TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</ControlTheme>
```
### TextBox ghost class
```xml
<!-- Transparent textbox that works in all states -->
<Style Selector="TextBox.ghost">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="FocusAdorner" Value="{x:Null}"/>
</Style>
<Style Selector="TextBox.ghost:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<!-- Also add :focus and :disabled variants -->
```
---
## Step 4 — How to look up unfamiliar Avalonia APIs
### For a control's properties:
```
Fetch: https://docs.avaloniaui.net/docs/reference/controls/{control-name-lowercase}
```
### For template part names (e.g. what's inside a ComboBox):
```
Search GitHub: https://github.com/search?q=repo:AvaloniaUI/Avalonia+PART_+{ControlName}&type=code
Or fetch: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/{ControlName}.axaml
```
### For pseudo-class selectors:
```
Fetch: https://docs.avaloniaui.net/docs/reference/styles/pseudo-classes
```
### For animation classes (Easing, FillMode, etc.):
```
Fetch: https://docs.avaloniaui.net/docs/guides/graphics-and-animations/animation
```
### For ColorPicker internals:
```
Fetch: https://raw.githubusercontent.com/AvaloniaUI/Avalonia/refs/heads/master/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
```
---
## Step 5 — Response format
1. State what you verified and where
2. Provide the correct AXAML or C# with no WPF-isms
3. Flag any Avalonia version caveat if relevant (project uses 11.x)
4. If something cannot be done via AXAML, explain the code-behind approach
5. Never guess at property names — fetch source if uncertain

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ obj/
./Clario/CLAUDE_CONTEXT.md
publish/
*.tar.gz
Clario/devsettings.json

196
CLAUDE.md Normal file
View File

@@ -0,0 +1,196 @@
# Clario — Claude Code Instructions
Clario is a cross-platform personal finance tracking app.
See @NEW_CHAT_CONTEXT.md for full project context before starting any task.
---
## Tech Stack
- **UI**: Avalonia UI XPlat (.NET 9), CommunityToolkit.MVVM
- **Backend**: Supabase (PostgreSQL, Auth, RLS, Realtime)
- **Charts**: LiveCharts2 (SkiaSharp)
- **IDE**: JetBrains Rider, Windows dev machine (Arabic region — always use `en-US` CultureInfo)
## Project Structure
```
Clario/ ← shared (ViewModels, Models, Services, Data, CustomControls, Behaviors, Converters)
Clario.Desktop/ ← Windows/macOS/Linux entry point
Clario.Android/ ← Android entry point
Views/ ← desktop AXAML views only
MobileViews/ ← mobile AXAML views only
```
## Build & Run
```bash
# Desktop
dotnet run --project Clario.Desktop
# Android (requires connected device or emulator)
dotnet build Clario.Android -c Release
# Verify build
dotnet build Clario.sln
```
## Platform Detection
```csharp
// Always check this before any platform-specific logic
App.IsMobile // true on Android/iOS, false on desktop
```
---
## CRITICAL RULES — Read before every task
### AXAML Rules
- **ALWAYS** use `{DynamicResource}` for theme colors, never hardcode hex
- **NEVER** put DataTemplates in AXAML — ViewLocator handles all view resolution
- **NEVER** add `MinWidth`/`MinHeight` to UserControl in mobile views
- **NEVER** use `BoxShadow` in mobile views
- Use `x:CompileBindings="False"` on shell views with dynamic DataContext
- Desktop views go in `Views/`, mobile views go in `MobileViews/` named `{Name}ViewMobile.axaml`
- Icon background opacity always: `<SolidColorBrush Color="..." Opacity="0.15"/>`
- Separator between list items: `Spacing="1"` on StackPanel + `BorderSubtle` background on container
### ViewModel Rules
- **NEVER** fetch data in child ViewModel constructors
- **NEVER** trigger initialization from `partial void On{Property}Changed` when VM depends on multiple properties
- Call `Initialize()` explicitly after object initializer sets all required fields
- Child VMs have `public required ViewModelBase parentViewModel`
- Replace lists entirely to trigger bindings — never mutate and expect updates
### C# Rules
- Always `en-US` CultureInfo for dates/numbers (Windows has Arabic region)
- Use `Task.WhenAll` for parallel async fetches in `InitializeApp`
- Use `_ = SomeAsyncMethod()` for fire-and-forget with try/catch inside the method
- Wrap fire-and-forget in try/catch — exceptions are silently swallowed
### Style Classes
```
accented → primary action button (AccentBlue bg)
base → secondary action button
nav → transparent navigation/toggle button
danger → destructive action (DangerButtonBackground + AccentRed text)
ghost → transparent TextBox (no border, any state)
label → uppercase muted TextBlock label
muted → TextMuted foreground
mobile → root class on mobile views (enables mobile overrides)
```
---
## Design Tokens (quick reference)
```
BgBase/BgSurface/BgSidebar/BgHover
BorderSubtle/BorderAccent
TextPrimary/TextSecondary/TextMuted/TextDisabled
AccentBlue/AccentGreen/AccentYellow/AccentRed/AccentPurple/AccentOrange/AccentPink
IconBgBlue/IconBgGreen/IconBgRed/IconBgOrange/IconBgPurple/IconBgPink
BadgeBgRed/BadgeBgYellow/BadgeBgGreen/BadgeBgBlue
DangerButtonBackground/DangerButtonBorder
SvgPrimary/SvgSecondary/SvgMuted/SvgDisabled/SvgBlue/SvgGreen/SvgYellow/SvgRed
```
## SVG Pattern
```xml
<Svg Path="../Assets/Icons/icon-name.svg" Width="16" Height="16" Css="{DynamicResource SvgBlue}"/>
```
---
## Converters (quick reference)
| Key | Usage |
|-----|-------|
| `HexToColorConverter` | `ConverterParameter=color/css/brush` |
| `AmountColorConverter` | type string → AccentRed/Green brush |
| `AmountSignConverter` | MultiBinding(amount, type) → `+$x.xx` |
| `BoolToColorConverter` | `ConverterParameter='#hex1\|#hex2'` |
| `BoolToCssConverter` | `ConverterParameter='#hex1\|#hex2'` → SVG CSS |
| `SvgPathFromName` | `"icon-name"``"../Assets/Icons/icon-name.svg"` |
| `DateFormatConverter` | DateTime → string (always en-US) |
| `EqualValueConverter` | MultiBinding equality → bool |
| `NetworthSumConverter` | MultiBinding(income, expenses) → net |
| `PercentageConverter` | MultiBinding(value, total) → % |
---
## Flyout Pattern
```xml
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight"
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
<views:SomeView/>
</Flyout>
</Button.Flyout>
```
## Modal Overlay Pattern
```xml
<!-- In MainView content area, on top of ContentControl -->
<views:SomeFormView
DataContext="{Binding SomeFormVM}"
IsVisible="{Binding IsFormVisible}"/>
```
The view's root Grid must have `<Border Background="#70000000"/>` as the dim layer.
## Bottom Sheet Pattern (mobile)
- Controlled via `ShowSheet()` / `HideSheet()` public methods in code-behind
- `TranslateTransform` animation: CubicEaseOut 320ms up, CubicEaseIn 260ms down
- `OverlayGrid.IsVisible = false` by default in AXAML
- Set `BottomSheet.MaxHeight = Bounds.Height * 0.82` in `OnAttachedToVisualTree`
---
## Supabase
```csharp
// All queries via DataRepo
DataRepo.General.FetchTransactions()
DataRepo.General.FetchCategories()
DataRepo.General.FetchAccounts()
DataRepo.General.FetchBudgets()
DataRepo.General.FetchProfileInfo()
// etc.
// Auth
SupabaseService.Client.Auth.CurrentUser
SupabaseService.Client.Auth.SignIn(email, password)
SupabaseService.Client.Auth.SignOut()
SupabaseService.Client.Auth.Update(new UserAttributes { ... })
```
RLS: all tables enabled. INSERT uses `WITH CHECK (auth.uid() = user_id)`.
---
## Verification
After any code change, verify by:
1. `dotnet build Clario.sln` — must have zero errors
2. Check AXAML for `{DynamicResource}` on all color bindings
3. Check that no ViewModel constructor fetches data
4. On mobile views: no `BoxShadow`, no `MinWidth`/`MinHeight`, has `Classes="mobile"` on root
---
## What's Not Yet Built
- `AuthViewMobile` — needs creating
- Settings view mobile version — needs creating
- Analytics view — not designed yet
- Light theme — token file incomplete, do not assume it's complete
- Real-time Supabase subscriptions — not wired to UI

View File

@@ -42,7 +42,7 @@ public partial class App : Application
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatformLoading)
{
Console.WriteLine("ANDROID PATH HIT");
DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatformLoading.MainView = new MainAppMobile()
{
DataContext = new LoadingViewModel()
@@ -62,7 +62,7 @@ public partial class App : Application
}
catch (Exception e)
{
Console.WriteLine($"[Auth] RetrieveSession failed: {e.Message}");
DebugLogger.Log($"[Auth] RetrieveSession failed: {e.Message}");
}
var user = SupabaseService.Client.Auth.CurrentUser;
@@ -84,7 +84,7 @@ public partial class App : Application
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
Console.WriteLine("ANDROID PATH HIT");
DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -1,25 +0,0 @@
<svg width="261" height="293" viewBox="0 0 261 293" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M158.179 13.01C158.66 7.50821 154.588 2.62135 149.066 2.52288C122.238 2.04452 95.7365 9.07331 72.5947 22.9121C49.4528 36.7509 30.7204 56.7719 18.4475 80.6321C15.9214 85.5434 18.2994 91.4436 23.3741 93.6229L52.2522 106.025C57.3268 108.204 63.1612 105.823 65.875 101.013C73.7479 87.058 85.1404 75.3323 98.9894 67.0506C112.838 58.7689 128.559 54.281 144.578 53.9485C150.1 53.8339 154.958 49.8208 155.44 44.319L158.179 13.01Z" fill="url(#paint0_linear_104_135)"/>
<path d="M23.3741 93.6229C18.2994 91.4436 12.3833 93.7818 10.5611 98.9954C-0.408524 130.383 -0.208299 164.717 11.32 196.122C22.8483 227.527 44.9093 253.837 73.5808 270.673C78.3433 273.469 84.3671 271.424 86.8266 266.479L100.822 238.339C103.282 233.394 101.23 227.436 96.5789 224.458C79.6305 213.605 66.5961 197.463 59.5985 178.4C52.6009 159.337 52.0958 138.595 57.9976 119.355C59.6172 114.075 57.3268 108.204 52.2522 106.025L23.3741 93.6229Z" fill="url(#paint1_linear_104_135)"/>
<path d="M86.8266 266.479C84.3671 271.424 86.3711 277.462 91.4748 279.572C119.302 291.079 150.128 293.604 179.597 286.645C209.066 279.685 235.506 263.636 255.245 240.896C258.865 236.725 257.956 230.429 253.544 227.106L228.438 208.201C224.026 204.879 217.793 205.806 214.015 209.834C201.617 223.05 185.555 232.394 167.777 236.593C149.999 240.791 131.453 239.621 114.452 233.347C109.271 231.435 103.282 233.394 100.822 238.339L86.8266 266.479Z" fill="url(#paint2_linear_104_135)"/>
<path d="M254.158 66.7147C258.595 63.4264 259.552 57.1377 255.964 52.9394C244.93 40.0298 231.681 29.153 216.815 20.8348C201.949 12.5165 185.749 6.91511 168.974 4.26464C163.519 3.40269 158.66 7.50821 158.179 13.01L155.44 44.319C154.958 49.8208 159.046 54.6165 164.464 55.6882C173.994 57.5734 183.189 60.9512 191.703 65.7152C200.217 70.4792 207.905 76.5485 214.497 83.6844C218.245 87.7411 224.471 88.716 228.908 85.4277L254.158 66.7147Z" fill="url(#paint3_linear_104_135)"/>
<path d="M52.2522 106.025L23.3741 93.6229M52.2522 106.025C57.3268 108.204 63.1612 105.823 65.875 101.013C73.7479 87.058 85.1404 75.3323 98.9894 67.0506C112.838 58.7689 128.559 54.281 144.578 53.9485C150.1 53.8339 154.958 49.8208 155.44 44.319M52.2522 106.025C57.3268 108.204 59.6172 114.075 57.9976 119.355C52.0958 138.595 52.6009 159.337 59.5985 178.4C66.5961 197.463 79.6305 213.605 96.5789 224.458C101.23 227.436 103.282 233.394 100.822 238.339M23.3741 93.6229C18.2994 91.4436 15.9214 85.5434 18.4475 80.6321C30.7204 56.7719 49.4528 36.7509 72.5947 22.9121C95.7365 9.07331 122.238 2.04452 149.066 2.52288C154.588 2.62135 158.66 7.50821 158.179 13.01M23.3741 93.6229C18.2994 91.4436 12.3833 93.7818 10.5611 98.9954C-0.408524 130.383 -0.208299 164.717 11.32 196.122C22.8483 227.527 44.9093 253.837 73.5808 270.673C78.3433 273.469 84.3671 271.424 86.8266 266.479M155.44 44.319L158.179 13.01M155.44 44.319C154.958 49.8208 159.046 54.6165 164.464 55.6882C173.994 57.5734 183.189 60.9512 191.703 65.7152C200.217 70.4792 207.905 76.5485 214.497 83.6844C218.245 87.7411 224.471 88.716 228.908 85.4277L254.158 66.7147C258.595 63.4264 259.552 57.1377 255.964 52.9394C244.93 40.0298 231.681 29.153 216.815 20.8348C201.949 12.5165 185.749 6.91511 168.974 4.26464C163.519 3.40269 158.66 7.50821 158.179 13.01M100.822 238.339L86.8266 266.479M100.822 238.339C103.282 233.394 109.271 231.435 114.452 233.347C131.453 239.621 149.999 240.791 167.777 236.593C185.555 232.394 201.617 223.05 214.015 209.834C217.793 205.806 224.026 204.879 228.438 208.201L253.544 227.106C257.956 230.429 258.865 236.725 255.245 240.896C235.506 263.636 209.066 279.685 179.597 286.645C150.128 293.604 119.302 291.079 91.4748 279.572C86.3711 277.462 84.3671 271.424 86.8266 266.479" stroke="#13161E" stroke-width="5"/>
<defs>
<linearGradient id="paint0_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint1_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint2_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint3_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="20" height="5" x="2" y="3" rx="1" />
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
<path d="M10 12h4" />
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m10 17 5-5-5-5" />
<path d="M15 12H3" />
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<line x1="19" x2="19" y1="8" y2="14" />
<line x1="22" x2="16" y1="11" y2="11" />
</svg>

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,12 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="64" fill="#13161E"/>
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9867 75.7593 45.872C75.7593 42.7574 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0854 96.372 30.896C98.0844 31.7067 99.4757 32.8374 100.546 34.288C101.616 35.696 102.215 37.3174 102.344 39.152V52.592C102.215 54.4267 101.616 56.0694 100.546 57.52C99.5185 58.928 98.1486 60.0374 96.4362 60.848C94.7238 61.6587 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5494 97.7847 51.184C99.069 49.776 99.7111 48.0054 99.7111 45.872C99.7111 44.4214 99.4114 43.1414 98.8121 42.032C98.2556 40.9227 97.4422 40.0694 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0694 87.2536 40.9227 86.6114 42.032C86.0121 43.1414 85.7125 44.4214 85.7125 45.872C85.7125 47.28 86.0121 48.5387 86.6114 49.648C87.2108 50.7574 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5094 123.774 34.6934 125.7 32.688C127.627 30.6827 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9147 137.644 30.384C138.8 30.8107 139.806 31.4934 140.662 32.432L134.819 39.792C134.391 39.3227 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8374 125.7 42.3307 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4294 142.894 22.1494 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1494 152.997 23.4294 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5654 161.714 56.6027 160.215 54.128C158.717 51.6534 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3414 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0774 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6534 189.882 54.128C188.384 56.6027 186.35 58.5654 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9334 178.773 52.336C179.843 51.696 180.657 50.8214 181.213 49.712C181.813 48.56 182.112 47.2587 182.112 45.808C182.112 44.3574 181.813 43.0987 181.213 42.032C180.614 40.9227 179.779 40.0694 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0694 169.484 40.9227 168.884 42.032C168.285 43.1414 167.985 44.4214 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8214 170.318 51.696 171.389 52.336C172.459 52.9334 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.5629C3.26812 20.1135 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1431 -0.560215 35.2232 1.82443 41.6993C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.649 16.9343 57.2274 17.443 56.2077L20.3381 50.4049C20.8468 49.3852 20.4224 48.1565 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0448C10.3634 34.1138 10.2589 29.8367 11.4797 25.8691C11.8148 24.7803 11.341 23.5697 10.2913 23.1203L4.31783 20.5629Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1136 54.1264 10.2527C51.8524 7.60548 49.1219 5.3751 46.0581 3.66936C42.9943 1.96362 39.6554 0.814996 36.1983 0.27149C35.0741 0.0947387 34.0727 0.936616 33.9735 2.06482L33.4089 8.48501C33.3097 9.61322 34.1522 10.5966 35.2688 10.8164C37.2328 11.203 39.1278 11.8956 40.8825 12.8725C42.6372 13.8494 44.2218 15.094 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9147L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,12 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="64" fill="white"/>
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9866 75.7593 45.872C75.7593 42.7573 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0853 96.372 30.896C98.0844 31.7066 99.4757 32.8373 100.546 34.288C101.616 35.696 102.215 37.3173 102.344 39.152V52.592C102.215 54.4266 101.616 56.0693 100.546 57.52C99.5185 58.928 98.1486 60.0373 96.4362 60.848C94.7238 61.6586 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5493 97.7847 51.184C99.069 49.776 99.7111 48.0053 99.7111 45.872C99.7111 44.4213 99.4114 43.1413 98.8121 42.032C98.2556 40.9226 97.4422 40.0693 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0693 87.2536 40.9226 86.6114 42.032C86.0121 43.1413 85.7125 44.4213 85.7125 45.872C85.7125 47.28 86.0121 48.5386 86.6114 49.648C87.2108 50.7573 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5093 123.774 34.6933 125.7 32.688C127.627 30.6826 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9146 137.644 30.384C138.8 30.8106 139.806 31.4933 140.662 32.432L134.819 39.792C134.391 39.3226 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8373 125.7 42.3306 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4293 142.894 22.1493 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1493 152.997 23.4293 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5653 161.714 56.6026 160.215 54.128C158.717 51.6533 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3413 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0773 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6533 189.882 54.128C188.384 56.6026 186.35 58.5653 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9333 178.773 52.336C179.843 51.696 180.657 50.8213 181.213 49.712C181.813 48.56 182.112 47.2586 182.112 45.808C182.112 44.3573 181.813 43.0986 181.213 42.032C180.614 40.9226 179.779 40.0693 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0693 169.484 40.9226 168.884 42.032C168.285 43.1413 167.985 44.4213 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8213 170.318 51.696 171.389 52.336C172.459 52.9333 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.563C3.26812 20.1136 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1432 -0.560215 35.2233 1.82443 41.6994C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.6491 16.9343 57.2275 17.443 56.2077L20.3381 50.405C20.8468 49.3853 20.4224 48.1566 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0449C10.3634 34.1139 10.2589 29.8368 11.4797 25.8692C11.8148 24.7804 11.341 23.5697 10.2913 23.1203L4.31783 20.563Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1137 54.1264 10.2528C51.8524 7.60554 49.1219 5.37516 46.0581 3.66942C42.9943 1.96368 39.6554 0.815057 36.1983 0.271551C35.0741 0.0947997 34.0727 0.936677 33.9735 2.06488L33.4089 8.48507C33.3097 9.61328 34.1522 10.5967 35.2688 10.8165C37.2328 11.203 39.1278 11.8957 40.8825 12.8726C42.6372 13.8495 44.2218 15.0941 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9148L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,11 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9866 75.7593 45.872C75.7593 42.7573 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0853 96.372 30.896C98.0844 31.7066 99.4757 32.8373 100.546 34.288C101.616 35.696 102.215 37.3173 102.344 39.152V52.592C102.215 54.4266 101.616 56.0693 100.546 57.52C99.5185 58.928 98.1486 60.0373 96.4362 60.848C94.7238 61.6586 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5493 97.7847 51.184C99.069 49.776 99.7111 48.0053 99.7111 45.872C99.7111 44.4213 99.4114 43.1413 98.8121 42.032C98.2556 40.9226 97.4422 40.0693 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0693 87.2536 40.9226 86.6114 42.032C86.0121 43.1413 85.7125 44.4213 85.7125 45.872C85.7125 47.28 86.0121 48.5386 86.6114 49.648C87.2108 50.7573 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5093 123.774 34.6933 125.7 32.688C127.627 30.6826 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9146 137.644 30.384C138.8 30.8106 139.806 31.4933 140.662 32.432L134.819 39.792C134.391 39.3226 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8373 125.7 42.3306 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4293 142.894 22.1493 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1493 152.997 23.4293 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5653 161.714 56.6026 160.215 54.128C158.717 51.6533 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3413 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0773 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6533 189.882 54.128C188.384 56.6026 186.35 58.5653 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9333 178.773 52.336C179.843 51.696 180.657 50.8213 181.213 49.712C181.813 48.56 182.112 47.2586 182.112 45.808C182.112 44.3573 181.813 43.0986 181.213 42.032C180.614 40.9226 179.779 40.0693 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0693 169.484 40.9226 168.884 42.032C168.285 43.1413 167.985 44.4213 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8213 170.318 51.696 171.389 52.336C172.459 52.9333 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.563C3.26812 20.1136 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1432 -0.560215 35.2233 1.82443 41.6994C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.6491 16.9343 57.2275 17.443 56.2077L20.3381 50.405C20.8468 49.3853 20.4224 48.1566 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0449C10.3634 34.1139 10.2589 29.8368 11.4797 25.8692C11.8148 24.7804 11.341 23.5697 10.2913 23.1203L4.31783 20.563Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1137 54.1264 10.2528C51.8524 7.60554 49.1219 5.37516 46.0581 3.66942C42.9943 1.96368 39.6554 0.815057 36.1983 0.271551C35.0741 0.0947997 34.0727 0.936677 33.9735 2.06488L33.4089 8.48507C33.3097 9.61328 34.1522 10.5967 35.2688 10.8165C37.2328 11.203 39.1278 11.8957 40.8825 12.8726C42.6372 13.8495 44.2218 15.0941 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9148L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#0F1117"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#0F1117"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#0F1117"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#0F1117"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#F0F2F8"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#F0F2F8"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#F0F2F8"/>
<path d="M112.475 31.8413C114.197 30.5655 114.568 28.1254 113.176 26.4965C108.896 21.4876 103.757 17.2674 97.9898 14.0399C92.2231 10.8124 85.9388 8.63908 79.4318 7.6107C77.3157 7.27626 75.4308 8.8692 75.2441 11.0039L74.1816 23.1518C73.9948 25.2865 75.5805 27.1472 77.6821 27.5631C81.3789 28.2945 84.9457 29.6051 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4256C98.5445 39.9996 100.959 40.3778 102.681 39.102L112.475 31.8413Z" fill="#F0F2F8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" fill="#13161E"/>
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5655 114.568 28.1254 113.176 26.4965C108.896 21.4876 103.757 17.2674 97.9898 14.0399C92.2231 10.8124 85.9388 8.63908 79.4318 7.6107C77.3157 7.27626 75.4308 8.8692 75.2441 11.0039L74.1816 23.1518C73.9948 25.2865 75.5805 27.1472 77.6821 27.5631C81.3789 28.2945 84.9457 29.6051 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4256C98.5445 39.9996 100.959 40.3778 102.681 39.102L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" fill="white"/>
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,6 +0,0 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M126.626 365.882C123.313 372.544 126.012 380.678 132.887 383.521C170.367 399.024 211.887 402.425 251.579 393.049C291.272 383.673 326.884 362.052 353.47 331.415C358.346 325.796 357.122 317.314 351.18 312.838L317.364 287.368C311.422 282.892 303.026 284.142 297.937 289.568C281.239 307.373 259.604 319.962 235.658 325.619C211.713 331.275 186.733 329.698 163.835 321.246C156.857 318.67 148.79 321.309 145.477 327.971L126.626 365.882Z" fill="#D9D9D9"/>
<path d="M36.1852 130.258C29.3501 127.322 21.3816 130.472 18.9273 137.495C4.15218 179.781 4.42187 226.037 19.9494 268.347C35.477 310.657 65.1913 346.101 103.809 368.783C110.224 372.55 118.337 369.796 121.65 363.134L140.501 325.223C143.814 318.561 141.05 310.533 134.786 306.521C111.958 291.9 94.4015 270.153 84.9763 244.471C75.5511 218.789 74.8708 190.846 82.82 164.924C85.0015 157.811 81.9166 149.902 75.0814 146.966L36.1852 130.258Z" fill="#D9D9D9"/>
<path d="M219.24 16.3331C219.888 8.92088 214.403 2.33719 206.965 2.20454C170.831 1.56007 135.136 11.0294 103.966 29.6733C72.7955 48.3173 47.5647 75.2901 31.0342 107.435C27.6317 114.052 30.8347 122.001 37.6698 124.937L76.5661 141.645C83.4012 144.581 91.2596 141.373 94.9148 134.892C105.519 116.092 120.864 100.295 139.517 89.1377C158.17 77.9805 179.345 71.9343 200.921 71.4863C208.358 71.3318 214.902 65.9253 215.55 58.5131L219.24 16.3331Z" fill="#D9D9D9"/>
<path d="M353.471 88.3147C359.447 83.8846 360.736 75.4123 355.903 69.7563C341.041 52.3641 323.197 37.7107 303.173 26.5042C283.15 15.2977 261.329 7.75132 238.736 4.18055C231.388 3.0193 224.843 8.55036 224.195 15.9625L220.506 58.1426C219.857 65.5548 225.363 72.0157 232.66 73.4596C245.497 75.9993 257.881 80.55 269.349 86.9681C280.816 93.3862 291.173 101.563 300.051 111.177C305.099 116.642 313.484 117.955 319.461 113.525L353.471 88.3147Z" fill="#D9D9D9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,11 +0,0 @@
<svg width="599" height="200" viewBox="0 0 599 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M189.81 191.95V46.75H220.41V191.95H189.81Z" fill="white"/>
<path d="M282.558 193.95C273.625 193.95 265.625 191.75 258.558 187.35C251.625 182.95 246.092 176.95 241.958 169.35C237.958 161.75 235.958 153.083 235.958 143.35C235.958 133.617 237.958 124.95 241.958 117.35C246.092 109.75 251.625 103.75 258.558 99.35C265.625 94.95 273.625 92.75 282.558 92.75C289.092 92.75 294.958 94.0167 300.159 96.55C305.492 99.0833 309.825 102.617 313.159 107.15C316.492 111.55 318.359 116.617 318.758 122.35V164.35C318.359 170.083 316.492 175.217 313.159 179.75C309.958 184.15 305.692 187.617 300.358 190.15C295.025 192.683 289.092 193.95 282.558 193.95ZM288.758 166.35C295.292 166.35 300.559 164.217 304.559 159.95C308.559 155.55 310.559 150.017 310.559 143.35C310.559 138.817 309.625 134.817 307.758 131.35C306.025 127.883 303.492 125.217 300.159 123.35C296.958 121.35 293.225 120.35 288.958 120.35C284.692 120.35 280.892 121.35 277.558 123.35C274.358 125.217 271.758 127.883 269.758 131.35C267.892 134.817 266.958 138.817 266.958 143.35C266.958 147.75 267.892 151.683 269.758 155.15C271.625 158.617 274.225 161.35 277.558 163.35C280.892 165.35 284.625 166.35 288.758 166.35ZM309.359 191.95V165.75L313.959 142.15L309.359 118.55V94.75H339.358V191.95H309.359Z" fill="white"/>
<path d="M360.904 191.95V94.75H391.504V191.95H360.904ZM391.504 138.55L378.704 128.55C381.237 117.217 385.504 108.417 391.504 102.15C397.504 95.8833 405.837 92.75 416.504 92.75C421.17 92.75 425.237 93.4833 428.704 94.95C432.304 96.2833 435.437 98.4167 438.104 101.35L419.904 124.35C418.57 122.883 416.904 121.75 414.904 120.95C412.904 120.15 410.637 119.75 408.104 119.75C403.037 119.75 398.97 121.35 395.904 124.55C392.97 127.617 391.504 132.283 391.504 138.55Z" fill="white"/>
<path d="M446.255 191.95V94.75H476.855V191.95H446.255ZM461.655 81.35C456.855 81.35 452.855 79.75 449.655 76.55C446.589 73.2167 445.055 69.2167 445.055 64.55C445.055 59.75 446.589 55.75 449.655 52.55C452.855 49.35 456.855 47.75 461.655 47.75C466.455 47.75 470.389 49.35 473.455 52.55C476.522 55.75 478.055 59.75 478.055 64.55C478.055 69.2167 476.522 73.2167 473.455 76.55C470.389 79.75 466.455 81.35 461.655 81.35Z" fill="white"/>
<path d="M545.204 194.15C535.204 194.15 526.137 191.95 518.004 187.55C510.004 183.017 503.67 176.883 499.004 169.15C494.337 161.417 492.004 152.75 492.004 143.15C492.004 133.55 494.337 124.95 499.004 117.35C503.67 109.75 510.004 103.75 518.004 99.35C526.004 94.8167 535.07 92.55 545.204 92.55C555.337 92.55 564.404 94.75 572.404 99.15C580.404 103.55 586.737 109.617 591.404 117.35C596.07 124.95 598.404 133.55 598.404 143.15C598.404 152.75 596.07 161.417 591.404 169.15C586.737 176.883 580.404 183.017 572.404 187.55C564.404 191.95 555.337 194.15 545.204 194.15ZM545.204 166.35C549.604 166.35 553.47 165.417 556.804 163.55C560.137 161.55 562.67 158.817 564.404 155.35C566.27 151.75 567.204 147.683 567.204 143.15C567.204 138.617 566.27 134.683 564.404 131.35C562.537 127.883 559.937 125.217 556.604 123.35C553.404 121.35 549.604 120.35 545.204 120.35C540.937 120.35 537.137 121.35 533.804 123.35C530.47 125.217 527.87 127.883 526.004 131.35C524.137 134.817 523.204 138.817 523.204 143.35C523.204 147.75 524.137 151.75 526.004 155.35C527.87 158.817 530.47 161.55 533.804 163.55C537.137 165.417 540.937 166.35 545.204 166.35Z" fill="white"/>
<path d="M58.3942 177.927C56.8103 181.056 58.1009 184.876 61.3877 186.211C79.3085 193.492 99.1607 195.089 118.139 190.686C137.118 186.282 154.145 176.128 166.857 161.741C169.188 159.102 168.603 155.118 165.762 153.016L149.593 141.055C146.752 138.953 142.738 139.54 140.304 142.088C132.32 150.45 121.976 156.362 110.527 159.018C99.0775 161.675 87.1337 160.934 76.1854 156.965C72.8486 155.755 68.9915 156.995 67.4076 160.123L58.3942 177.927Z" fill="white"/>
<path d="M13.4482 64.2591C10.1788 62.8547 6.36734 64.3615 5.19343 67.7212C-1.87383 87.9474 -1.74484 110.073 5.68233 130.31C13.1095 150.548 27.3224 167.502 45.7942 178.351C48.8625 180.153 52.7433 178.836 54.3278 175.649L63.3447 157.515C64.9292 154.329 63.6073 150.489 60.6108 148.57C49.6917 141.577 41.2943 131.174 36.786 118.89C32.2777 106.606 31.9523 93.2397 35.7546 80.841C36.7981 77.4385 35.3225 73.6553 32.0531 72.2509L13.4482 64.2591Z" fill="white"/>
<path d="M101.59 6.83964C101.899 3.2592 99.2892 0.0789703 95.751 0.0148925C78.5612 -0.296416 61.5802 4.27773 46.7519 13.2836C31.9237 22.2895 19.9208 35.3186 12.0569 50.8462C10.4383 54.0423 11.962 57.882 15.2136 59.3002L33.7174 67.371C36.969 68.7892 40.7074 67.2396 42.4462 64.1093C47.4908 55.028 54.7906 47.3972 63.6644 42.0077C72.5383 36.6182 82.6114 33.6976 92.8756 33.4812C96.4136 33.4066 99.5267 30.795 99.8352 27.2146L101.59 6.83964Z" fill="white"/>
<path d="M167.422 40.8672C170.27 38.76 170.884 34.7301 168.581 32.0398C161.498 23.7672 152.994 16.7972 143.452 11.4668C133.909 6.13639 123.51 2.54693 112.743 0.848476C109.241 0.296127 106.122 2.92699 105.813 6.45263L104.055 26.5157C103.746 30.0414 106.37 33.1145 109.847 33.8013C115.965 35.0094 121.867 37.1739 127.332 40.2267C132.797 43.2795 137.732 47.1688 141.964 51.7416C144.369 54.3412 148.366 54.9659 151.214 52.8587L167.422 40.8672Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -1,49 +0,0 @@
<svg width="800" height="400" viewBox="0 0 800 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M331.8 292V146.8H362.4V292H331.8Z" fill="url(#paint0_linear_104_124)"/>
<path d="M424.548 294C415.615 294 407.615 291.8 400.548 287.4C393.615 283 388.082 277 383.948 269.4C379.948 261.8 377.948 253.133 377.948 243.4C377.948 233.667 379.948 225 383.948 217.4C388.082 209.8 393.615 203.8 400.548 199.4C407.615 195 415.615 192.8 424.548 192.8C431.082 192.8 436.948 194.067 442.148 196.6C447.482 199.133 451.815 202.667 455.148 207.2C458.482 211.6 460.348 216.667 460.748 222.4V264.4C460.348 270.133 458.482 275.267 455.148 279.8C451.948 284.2 447.682 287.667 442.348 290.2C437.015 292.733 431.082 294 424.548 294ZM430.748 266.4C437.282 266.4 442.548 264.267 446.548 260C450.548 255.6 452.548 250.067 452.548 243.4C452.548 238.867 451.615 234.867 449.748 231.4C448.015 227.933 445.482 225.267 442.148 223.4C438.948 221.4 435.215 220.4 430.948 220.4C426.682 220.4 422.882 221.4 419.548 223.4C416.348 225.267 413.748 227.933 411.748 231.4C409.882 234.867 408.948 238.867 408.948 243.4C408.948 247.8 409.882 251.733 411.748 255.2C413.615 258.667 416.215 261.4 419.548 263.4C422.882 265.4 426.615 266.4 430.748 266.4ZM451.348 292V265.8L455.948 242.2L451.348 218.6V194.8H481.348V292H451.348Z" fill="url(#paint1_linear_104_124)"/>
<path d="M502.894 292V194.8H533.494V292H502.894ZM533.494 238.6L520.694 228.6C523.227 217.267 527.494 208.467 533.494 202.2C539.494 195.933 547.827 192.8 558.494 192.8C563.16 192.8 567.227 193.533 570.694 195C574.294 196.333 577.427 198.467 580.094 201.4L561.894 224.4C560.56 222.933 558.894 221.8 556.894 221C554.894 220.2 552.627 219.8 550.094 219.8C545.027 219.8 540.96 221.4 537.894 224.6C534.96 227.667 533.494 232.333 533.494 238.6Z" fill="url(#paint2_linear_104_124)"/>
<path d="M588.245 292V194.8H618.845V292H588.245ZM603.645 181.4C598.845 181.4 594.845 179.8 591.645 176.6C588.579 173.267 587.045 169.267 587.045 164.6C587.045 159.8 588.579 155.8 591.645 152.6C594.845 149.4 598.845 147.8 603.645 147.8C608.445 147.8 612.379 149.4 615.445 152.6C618.512 155.8 620.045 159.8 620.045 164.6C620.045 169.267 618.512 173.267 615.445 176.6C612.379 179.8 608.445 181.4 603.645 181.4Z" fill="url(#paint3_linear_104_124)"/>
<path d="M687.194 294.2C677.194 294.2 668.127 292 659.994 287.6C651.994 283.067 645.66 276.933 640.994 269.2C636.327 261.467 633.994 252.8 633.994 243.2C633.994 233.6 636.327 225 640.994 217.4C645.66 209.8 651.994 203.8 659.994 199.4C667.994 194.867 677.06 192.6 687.194 192.6C697.327 192.6 706.394 194.8 714.394 199.2C722.394 203.6 728.727 209.667 733.394 217.4C738.06 225 740.394 233.6 740.394 243.2C740.394 252.8 738.06 261.467 733.394 269.2C728.727 276.933 722.394 283.067 714.394 287.6C706.394 292 697.327 294.2 687.194 294.2ZM687.194 266.4C691.594 266.4 695.46 265.467 698.794 263.6C702.127 261.6 704.66 258.867 706.394 255.4C708.26 251.8 709.194 247.733 709.194 243.2C709.194 238.667 708.26 234.733 706.394 231.4C704.527 227.933 701.927 225.267 698.594 223.4C695.394 221.4 691.594 220.4 687.194 220.4C682.927 220.4 679.127 221.4 675.794 223.4C672.46 225.267 669.86 227.933 667.994 231.4C666.127 234.867 665.194 238.867 665.194 243.4C665.194 247.8 666.127 251.8 667.994 255.4C669.86 258.867 672.46 261.6 675.794 263.6C679.127 265.467 682.927 266.4 687.194 266.4Z" fill="url(#paint4_linear_104_124)"/>
<path d="M142.075 320.852C139.663 325.703 141.628 331.626 146.634 333.697C173.927 344.985 204.162 347.462 233.066 340.635C261.97 333.807 287.902 318.062 307.262 295.753C310.813 291.661 309.922 285.484 305.595 282.225L280.97 263.678C276.643 260.419 270.529 261.329 266.823 265.28C254.664 278.246 238.909 287.413 221.472 291.532C204.035 295.651 185.845 294.502 169.171 288.348C164.089 286.472 158.214 288.394 155.802 293.245L142.075 320.852Z" fill="url(#paint5_linear_104_124)"/>
<path d="M76.2161 149.27C71.2388 147.132 65.4361 149.426 63.6489 154.541C52.8897 185.333 53.0861 219.017 64.3932 249.827C75.7004 280.637 97.3382 306.447 125.46 322.964C130.131 325.708 136.039 323.702 138.451 318.85L152.179 291.244C154.591 286.392 152.579 280.547 148.017 277.625C131.393 266.978 118.609 251.142 111.746 232.44C104.882 213.739 104.387 193.39 110.175 174.515C111.764 169.335 109.518 163.575 104.54 161.437L76.2161 149.27Z" fill="url(#paint6_linear_104_124)"/>
<path d="M209.516 66.3108C209.988 60.9133 205.994 56.119 200.578 56.0225C174.265 55.5532 148.272 62.4487 125.574 76.0252C102.876 89.6016 84.5026 109.243 72.4651 132.651C69.9874 137.469 72.3198 143.258 77.2972 145.396L105.621 157.562C110.599 159.7 116.321 157.364 118.983 152.645C126.705 138.955 137.879 127.452 151.462 119.327C165.045 111.202 180.464 106.8 196.176 106.473C201.592 106.361 206.357 102.424 206.829 97.0263L209.516 66.3108Z" fill="url(#paint7_linear_104_124)"/>
<path d="M307.263 118.728C311.615 115.502 312.553 109.332 309.034 105.213C298.212 92.5486 285.217 81.878 270.636 73.7174C256.055 65.5569 240.165 60.0616 223.713 57.4614C218.362 56.6158 213.597 60.6435 213.124 66.041L210.438 96.7565C209.966 102.154 213.975 106.859 219.289 107.91C228.636 109.76 237.655 113.073 246.005 117.747C254.356 122.421 261.897 128.375 268.363 135.376C272.038 139.356 278.145 140.312 282.497 137.086L307.263 118.728Z" fill="url(#paint8_linear_104_124)"/>
<defs>
<linearGradient id="paint0_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint1_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint2_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint3_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint4_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint5_linear_104_124" x1="315.701" y1="70.2952" x2="259.223" y2="163.668" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint6_linear_104_124" x1="312.078" y1="68.2937" x2="255.6" y2="161.666" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint7_linear_104_124" x1="313.159" y1="64.4189" x2="256.681" y2="157.791" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint8_linear_104_124" x1="316.768" y1="64.1491" x2="260.29" y2="157.521" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -44,4 +44,10 @@
<DependentUpon>AccountFormView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="devsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -10,17 +10,19 @@ public class NetworthSumConverter : IMultiValueConverter
{
public object? Convert(IList<object?>? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is null || value.Count < 2 || value.Any(x => x is null)) return 0;
if (value[0] is double incomeD && (value[1] is double expenseD))
if (value is null || value.Count < 2) return 0;
var symbol = value.Count >= 3 && value[2] is string s ? s : "$";
if (value[0] is double incomeD && value[1] is double expenseD)
{
var net = incomeD - expenseD;
return (net < 0 ? $"- ${Math.Abs(net):F2}" : $"+ ${Math.Abs(net):F2}");
return net < 0 ? $"- {symbol}{Math.Abs(net):F2}" : $"+ {symbol}{Math.Abs(net):F2}";
}
if (value[0] is decimal incomeDec && (value[1] is decimal expenseDec))
if (value[0] is decimal incomeDec && value[1] is decimal expenseDec)
{
var net = incomeDec - expenseDec;
return (net < 0 ? $"-${Math.Abs(net):F2}" : $"+${Math.Abs(net):F2}");
return net < 0 ? $"-{symbol}{Math.Abs(net):F2}" : $"+{symbol}{Math.Abs(net):F2}";
}
return 0;

View File

@@ -12,6 +12,7 @@ using Clario.Models;
using Clario.Models.GeneralModels;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Clario.Messages;
using CommunityToolkit.Mvvm.Messaging;
using Supabase.Postgrest;
using FileOptions = Supabase.Storage.FileOptions;
@@ -78,12 +79,13 @@ public partial class GeneralDataRepo : ObservableObject
if (result.Models.Count >= 1)
{
var resultItem = LinkTransactionCategories(result.Models[0]);
LinkTransactionAccounts(resultItem);
Transactions.Add(resultItem);
}
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
return;
}
}
@@ -98,11 +100,16 @@ public partial class GeneralDataRepo : ObservableObject
if (item is null) return;
var index = Transactions.IndexOf(item);
if (index != -1) Transactions[index] = LinkTransactionCategories(result.Model);
if (index != -1)
{
var enriched = LinkTransactionCategories(result.Model);
LinkTransactionAccounts(enriched);
Transactions[index] = enriched;
}
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -117,7 +124,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -135,7 +142,7 @@ public partial class GeneralDataRepo : ObservableObject
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
var accounts = await SupabaseService.Client.From<Account>().Get();
Accounts = new ObservableCollection<Account>(accounts.Models);
return accounts.Models;
return accounts.Models.OrderBy(x=>x.IsPrimary).ThenBy(x=>x.CreatedAt).ToList();
}
public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
@@ -150,27 +157,29 @@ public partial class GeneralDataRepo : ObservableObject
{
var budgets = Budgets;
var outputList = new List<Budget>();
var primarySymbol = CurrencyService.GetSymbol(PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD");
foreach (var budget in budgets)
{
budget.Category = Categories.FirstOrDefault(x => x.Id == budget.CategoryId);
budget.PrimarySymbol = primarySymbol;
switch (budget.Period.ToLower())
{
case "monthly":
var budgetTransactions = Transactions.Where(x =>
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = budgetTransactions.Sum(x => x.Amount);
budget.Spent = budgetTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = budgetTransactions.Count;
break;
case "quarterly":
var quarterTransactions = Transactions.Where(x =>
x.Date.Month >= CurrentPeriod.Month - 3 && x.Date.Month <= CurrentPeriod.Month && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = quarterTransactions.Sum(x => x.Amount);
budget.Spent = quarterTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = quarterTransactions.Count;
break;
case "yearly":
var yearTransactions = Transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = yearTransactions.Sum(x => x.Amount);
budget.Spent = yearTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = yearTransactions.Count;
break;
}
@@ -226,7 +235,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
return null;
}
}
@@ -244,7 +253,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -268,7 +277,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -319,7 +328,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -337,7 +346,7 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -353,11 +362,43 @@ public partial class GeneralDataRepo : ObservableObject
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
public Account? PrimaryAccount => Accounts.FirstOrDefault(a => a.IsPrimary);
/// <summary>
/// Clears is_primary on the current primary account (if different from <paramref name="newPrimaryId"/>).
/// The caller must still save the new primary account via InsertAccount or UpdateAccount.
/// </summary>
public async Task SetPrimaryAccountAsync(Guid newPrimaryId)
{
try
{
var old = Accounts.FirstOrDefault(a => a.IsPrimary && a.Id != newPrimaryId);
if (old is null) return;
old.IsPrimary = false;
var result = await SupabaseService.Client.From<Account>().Update(old);
if (result.Model is null) return;
var idx = Accounts.IndexOf(old);
if (idx != -1) Accounts[idx] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
public async Task RefreshLiveRatesAndEnrich()
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
await CurrencyService.RefreshLiveRatesAsync(primaryCurrency, Accounts.Select(a => a.Currency));
LinkTransactionAccounts();
WeakReferenceMessenger.Default.Send(new RatesRefreshed());
}
public void LinkTransactionCategories()
{
foreach (var transaction in Transactions)
@@ -372,6 +413,45 @@ public partial class GeneralDataRepo : ObservableObject
return transaction;
}
public void LinkTransactionAccounts()
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
var primarySymbol = CurrencyService.GetSymbol(primaryCurrency);
foreach (var tx in Transactions)
{
EnrichTransactionAccount(tx, primaryCurrency, primarySymbol);
}
}
public Transaction LinkTransactionAccounts(Transaction tx)
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
var primarySymbol = CurrencyService.GetSymbol(primaryCurrency);
EnrichTransactionAccount(tx, primaryCurrency, primarySymbol);
return tx;
}
private void EnrichTransactionAccount(Transaction tx, string primaryCurrency, string primarySymbol)
{
var account = Accounts.FirstOrDefault(a => a.Id == tx.AccountId);
var accountCurrency = account?.Currency ?? primaryCurrency;
tx.AccountCurrency = accountCurrency;
tx.IsMultiCurrency = !accountCurrency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase);
tx.PrimaryAmountFormatted = $"{primarySymbol}{tx.ConvertedAmount:N2}";
tx.OriginalAmountFormatted = tx.IsMultiCurrency
? $"{CurrencyService.GetSymbol(accountCurrency)}{tx.Amount:N2}"
: string.Empty;
}
public async Task UpdateSavingsGoal(decimal? goal)
{
var profile = Profile;
profile.SavingsGoal = goal;
var result = await SupabaseService.Client.From<Profile>().Update(profile);
if (result.Models.Count < 1) return;
Profile = result.Models[0];
}
public async Task UpdateProfile(Profile profile)
{
var result = await SupabaseService.Client.From<Profile>().Update(profile);

View File

@@ -0,0 +1,3 @@
namespace Clario.Messages;
public class RatesRefreshed { }

View File

@@ -193,10 +193,16 @@
<TextBlock Text="Income"
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalIncome, StringFormat='$0.00'}"
FontSize="13"
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
Foreground="{DynamicResource AccentGreen}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimaryCurrencySymbol" />
<Binding Path="TotalIncome" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
@@ -212,10 +218,16 @@
<TextBlock Text="Expenses"
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalExpenses, StringFormat='$0.00'}"
FontSize="13"
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{DynamicResource AccentRed}" />
Foreground="{DynamicResource AccentRed}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimaryCurrencySymbol" />
<Binding Path="TotalExpenses" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
@@ -238,6 +250,7 @@
<MultiBinding Converter="{StaticResource NetworthSumConverter}">
<Binding Path="TotalIncome" />
<Binding Path="TotalExpenses" />
<Binding Path="PrimaryCurrencySymbol" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
@@ -313,13 +326,22 @@
</StackPanel>
</StackPanel>
<!-- Amount -->
<TextBlock Grid.Column="2"
Text="{Binding Amount, StringFormat='$0.00'}"
<!-- Dual-currency amount display -->
<StackPanel Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding PrimaryAmountFormatted}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
VerticalAlignment="Center" />
HorizontalAlignment="Right"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
<TextBlock Text="{Binding OriginalAmountFormatted}"
FontSize="11"
HorizontalAlignment="Right"
IsVisible="{Binding IsMultiCurrency}"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Grid>
</Border>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Clario.Services;
using Newtonsoft.Json;
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;
@@ -38,6 +39,8 @@ public class Account : BaseModel
[Column("color")] public string Color { get; set; } = string.Empty;
[Column("is_primary")] public bool IsPrimary { get; set; }
[JsonIgnore] public int TransactionsCount { get; set; }
[JsonIgnore] public int IncomeTransactionsThisMonth { get; set; }
[JsonIgnore] public int ExpenseTransactionsThisMonth { get; set; }
@@ -48,4 +51,11 @@ public class Account : BaseModel
[JsonIgnore] public bool isCredit => Type == "Credit";
[JsonIgnore] public decimal CreditUtilizationPerc => (CurrentBalance < 0 ? CurrentBalance * -1 : 0) / (CreditLimit == 0 ? 1 : CreditLimit) ?? 1;
[JsonIgnore] public bool GroupHeader { get; set; } = false;
[JsonIgnore] public string CurrencySymbol => CurrencyService.GetSymbol(Currency);
[JsonIgnore] public string CurrentBalanceFormatted => $"{CurrencySymbol}{CurrentBalance:N2}";
[JsonIgnore] public string TotalIncomeFormatted => $"{CurrencySymbol}{TotalIncomeThisMonth:N2}";
[JsonIgnore] public string TotalExpenseFormatted => $"{CurrencySymbol}{TotalExpenseThisMonth:N2}";
[JsonIgnore] public string MonthlyIncreaseFormatted =>
$"{(MonthlyIncrease >= 0 ? "+" : "-")}{CurrencySymbol}{Math.Abs(MonthlyIncrease):N2}";
}

View File

@@ -30,6 +30,7 @@ public class Budget : BaseModel
[JsonIgnore] public Category? Category { get; set; }
[JsonIgnore] public int TransactionsCount { get; set; }
[JsonIgnore] public decimal Spent { get; set; }
[JsonIgnore] public string PrimarySymbol { get; set; } = "$";
[JsonIgnore] public decimal Remaining => LimitAmount - Spent;
[JsonIgnore] public double PercentageUsed => LimitAmount > 0 ? Math.Round((double)(Spent / LimitAmount), 2) : 0;
@@ -37,14 +38,15 @@ public class Budget : BaseModel
[JsonIgnore] public bool IsWarning => !IsOverBudget && PercentageUsed * 100 >= AlertThreshold;
[JsonIgnore] public bool IsOnTrack => PercentageUsed * 100 < AlertThreshold;
[JsonIgnore] public string SpentFormatted => $"${Spent:N0}";
[JsonIgnore] public string AmountFormatted => $"of ${LimitAmount:N0}";
[JsonIgnore] public string SpentFormatted => $"{PrimarySymbol}{Spent:N0}";
[JsonIgnore] public string LimitFormatted => $"{PrimarySymbol}{LimitAmount:N0}";
[JsonIgnore] public string AmountFormatted => $"of {PrimarySymbol}{LimitAmount:N0}";
[JsonIgnore] public string PercentageFormatted => $"{PercentageUsed:P0} used";
[JsonIgnore]
public string RemainingFormatted => IsOverBudget
? $"${Math.Abs(Remaining):N0} over"
: $"${Remaining:N0} left";
? $"{PrimarySymbol}{Math.Abs(Remaining):N0} over"
: $"{PrimarySymbol}{Remaining:N0} left";
[JsonIgnore] public bool GroupHeader { get; set; } = false;
}

View File

@@ -0,0 +1,9 @@
namespace Clario.Models;
public class TestDefaults
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Clario.Data;
using Clario.Services;
using Newtonsoft.Json;
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;
@@ -32,5 +33,20 @@ public class Transaction : BaseModel
[Column("created_at")] public DateTime CreatedAt { get; set; }
[Column("exchange_rate")] public decimal? ExchangeRate { get; set; }
// Set during enrichment by GeneralDataRepo.LinkTransactionAccounts
[JsonIgnore] public string AccountCurrency { get; set; } = "";
[JsonIgnore] public string PrimaryAmountFormatted { get; set; } = "";
[JsonIgnore] public string OriginalAmountFormatted { get; set; } = "";
[JsonIgnore] public decimal ConvertedAmount =>
!string.IsNullOrEmpty(AccountCurrency) && CurrencyService.LiveRates.TryGetValue(AccountCurrency, out var liveRate)
? Amount * liveRate
: (ExchangeRate.HasValue ? Amount * ExchangeRate.Value : Amount);
[JsonIgnore] public bool IsMultiCurrency { get; set; }
[JsonIgnore] public string PrimaryAmountSignFormatted =>
Type == "expense" ? $"-{PrimaryAmountFormatted}" : $"+{PrimaryAmountFormatted}";
[JsonIgnore] public bool GroupHeader { get; set; } = false;
}

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace Clario.Services;
public static class CurrencyService
{
private static readonly HttpClient _http = new();
public static readonly IReadOnlyDictionary<string, string> CurrencyNames =
new Dictionary<string, string>
{
// Major
{ "USD", "US Dollar" },
{ "EUR", "Euro" },
{ "GBP", "British Pound" },
{ "JPY", "Japanese Yen" },
{ "CHF", "Swiss Franc" },
{ "CAD", "Canadian Dollar" },
{ "AUD", "Australian Dollar" },
{ "NZD", "New Zealand Dollar" },
// Asia-Pacific
{ "CNY", "Chinese Yuan" },
{ "HKD", "Hong Kong Dollar" },
{ "SGD", "Singapore Dollar" },
{ "KRW", "South Korean Won" },
{ "TWD", "Taiwan Dollar" },
{ "INR", "Indian Rupee" },
{ "PKR", "Pakistani Rupee" },
{ "BDT", "Bangladeshi Taka" },
{ "LKR", "Sri Lankan Rupee" },
{ "NPR", "Nepalese Rupee" },
{ "MMK", "Myanmar Kyat" },
{ "THB", "Thai Baht" },
{ "MYR", "Malaysian Ringgit" },
{ "IDR", "Indonesian Rupiah" },
{ "PHP", "Philippine Peso" },
{ "VND", "Vietnamese Dong" },
{ "KHR", "Cambodian Riel" },
{ "LAK", "Lao Kip" },
{ "MNT", "Mongolian Tögrög" },
{ "AFN", "Afghan Afghani" },
{ "BND", "Brunei Dollar" },
{ "MOP", "Macanese Pataca" },
// Middle East
{ "AED", "UAE Dirham" },
{ "SAR", "Saudi Riyal" },
{ "QAR", "Qatari Riyal" },
{ "KWD", "Kuwaiti Dinar" },
{ "BHD", "Bahraini Dinar" },
{ "OMR", "Omani Rial" },
{ "JOD", "Jordanian Dinar" },
{ "ILS", "Israeli Shekel" },
{ "IQD", "Iraqi Dinar" },
{ "YER", "Yemeni Rial" },
{ "LBP", "Lebanese Pound" },
// Africa
{ "EGP", "Egyptian Pound" },
{ "MAD", "Moroccan Dirham" },
{ "TND", "Tunisian Dinar" },
{ "DZD", "Algerian Dinar" },
{ "LYD", "Libyan Dinar" },
{ "NGN", "Nigerian Naira" },
{ "GHS", "Ghanaian Cedi" },
{ "KES", "Kenyan Shilling" },
{ "UGX", "Ugandan Shilling" },
{ "TZS", "Tanzanian Shilling" },
{ "ETB", "Ethiopian Birr" },
{ "ZAR", "South African Rand" },
{ "ZMW", "Zambian Kwacha" },
{ "BWP", "Botswana Pula" },
{ "MZN", "Mozambican Metical" },
{ "AOA", "Angolan Kwanza" },
{ "XOF", "West African CFA Franc" },
{ "XAF", "Central African CFA Franc" },
{ "MUR", "Mauritian Rupee" },
{ "RWF", "Rwandan Franc" },
{ "SDG", "Sudanese Pound" },
{ "MGA", "Malagasy Ariary" },
// Europe (non-EUR)
{ "SEK", "Swedish Krona" },
{ "NOK", "Norwegian Krone" },
{ "DKK", "Danish Krone" },
{ "ISK", "Icelandic Króna" },
{ "PLN", "Polish Złoty" },
{ "CZK", "Czech Koruna" },
{ "HUF", "Hungarian Forint" },
{ "RON", "Romanian Leu" },
{ "BGN", "Bulgarian Lev" },
{ "HRK", "Croatian Kuna" },
{ "RSD", "Serbian Dinar" },
{ "ALL", "Albanian Lek" },
{ "MKD", "Macedonian Denar" },
{ "BAM", "Bosnian Mark" },
{ "MDL", "Moldovan Leu" },
{ "UAH", "Ukrainian Hryvnia" },
{ "BYN", "Belarusian Ruble" },
{ "RUB", "Russian Ruble" },
{ "TRY", "Turkish Lira" },
// Caucasus & Central Asia
{ "GEL", "Georgian Lari" },
{ "AMD", "Armenian Dram" },
{ "AZN", "Azerbaijani Manat" },
{ "KZT", "Kazakhstani Tenge" },
{ "UZS", "Uzbekistani Som" },
{ "TJS", "Tajikistani Somoni" },
{ "TMT", "Turkmenistani Manat" },
{ "KGS", "Kyrgyzstani Som" },
// Americas
{ "MXN", "Mexican Peso" },
{ "BRL", "Brazilian Real" },
{ "ARS", "Argentine Peso" },
{ "CLP", "Chilean Peso" },
{ "COP", "Colombian Peso" },
{ "PEN", "Peruvian Sol" },
{ "BOB", "Bolivian Boliviano" },
{ "PYG", "Paraguayan Guaraní" },
{ "UYU", "Uruguayan Peso" },
{ "VES", "Venezuelan Bolívar" },
{ "GTQ", "Guatemalan Quetzal" },
{ "HNL", "Honduran Lempira" },
{ "NIO", "Nicaraguan Córdoba" },
{ "CRC", "Costa Rican Colón" },
{ "PAB", "Panamanian Balboa" },
{ "DOP", "Dominican Peso" },
{ "JMD", "Jamaican Dollar" },
{ "TTD", "Trinidad & Tobago Dollar" },
{ "BSD", "Bahamian Dollar" },
{ "CUP", "Cuban Peso" },
{ "HTG", "Haitian Gourde" },
{ "XCD", "Eastern Caribbean Dollar" },
{ "BBD", "Barbadian Dollar" },
{ "GYD", "Guyanese Dollar" },
{ "SRD", "Surinamese Dollar" },
};
public static IReadOnlyList<string> AvailableCurrencies { get; } =
new List<string>(CurrencyNames.Keys);
/// <summary>
/// Maps each account currency → rate to convert 1 unit to the current primary currency.
/// Populated on startup and refreshed whenever the primary currency changes.
/// </summary>
public static Dictionary<string, decimal> LiveRates { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Fetches fresh exchange rates for every currency in <paramref name="accountCurrencies"/>
/// relative to <paramref name="primaryCurrency"/> and stores them in <see cref="LiveRates"/>.
/// </summary>
public static async Task RefreshLiveRatesAsync(string primaryCurrency, IEnumerable<string> accountCurrencies)
{
var currencies = accountCurrencies.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var tasks = currencies.Select(async currency =>
{
if (currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
{
LiveRates[currency] = 1m;
return;
}
var rate = await GetExchangeRateAsync(currency, primaryCurrency);
if (rate.HasValue) LiveRates[currency] = rate.Value;
});
await Task.WhenAll(tasks);
}
/// <summary>
/// Fetches the live exchange rate from <paramref name="from"/> to <paramref name="to"/>
/// using the Frankfurter API. Returns null on failure.
/// </summary>
public static async Task<decimal?> GetExchangeRateAsync(string from, string to)
{
try
{
if (from.Equals(to, StringComparison.OrdinalIgnoreCase)) return 1m;
var url = $"https://api.frankfurter.dev/v2/rates?base={from.ToUpper()}&quotes={to.ToUpper()}";
var json = await _http.GetStringAsync(url);
var arr = JArray.Parse(json);
var rate = arr[0]?["rate"]?.Value<decimal>();
return Math.Round(rate ?? 1, 2);
}
catch
{
return null;
}
}
/// <summary>Returns the display symbol for a given ISO currency code.</summary>
public static string GetSymbol(string? code) => code?.ToUpper() switch
{
"USD" => "$",
"CAD" => "CA$",
"AUD" => "A$",
"NZD" => "NZ$",
"HKD" => "HK$",
"SGD" => "S$",
"BSD" => "B$",
"BND" => "B$",
"BBD" => "Bds$",
"EUR" => "€",
"GBP" => "£",
"EGP" => "E£",
"LBP" => "L£",
"SYP" => "S£",
"JPY" => "¥",
"CNY" => "¥",
"CHF" => "Fr",
"SEK" => "kr",
"NOK" => "kr",
"DKK" => "kr",
"ISK" => "kr",
"INR" => "₹",
"NPR" => "₨",
"PKR" => "₨",
"LKR" => "₨",
"MUR" => "₨",
"SCR" => "₨",
"BRL" => "R$",
"RUB" => "₽",
"KRW" => "₩",
"TRY" => "₺",
"ILS" => "₪",
"UAH" => "₴",
"KZT" => "₸",
"MNT" => "₮",
"THB" => "฿",
"VND" => "₫",
"PHP" => "₱",
"IDR" => "Rp",
"MYR" => "RM",
"KWD" => "KD",
"BHD" => "BD",
"OMR" => "OMR",
"JOD" => "JD",
"SAR" => "SR",
"AED" => "AED",
"QAR" => "QR",
"IQD" => "IQD",
"YER" => "YR",
"IRR" => "﷼",
"HUF" => "Ft",
"CZK" => "Kč",
"PLN" => "zł",
"RON" => "lei",
"BGN" => "лв",
"HRK" => "kn",
"RSD" => "din",
"GEL" => "₾",
"AMD" => "֏",
"AZN" => "₼",
"AFN" => "؋",
"NGN" => "₦",
"GHS" => "₵",
"ZAR" => "R",
"KES" => "Ksh",
"UGX" => "USh",
"TZS" => "TSh",
"ETB" => "Br",
"MAD" => "MAD",
"DZD" => "DA",
"TND" => "DT",
"XOF" => "CFA",
"XAF" => "FCFA",
"MXN" => "MX$",
"ARS" => "AR$",
"CLP" => "CL$",
"COP" => "CO$",
"PEN" => "S/",
"BOB" => "Bs",
"PYG" => "₲",
"UYU" => "$U",
"VES" => "Bs.S",
"GTQ" => "Q",
"HNL" => "L",
"NIO" => "C$",
"CRC" => "₡",
"DOP" => "RD$",
"JMD" => "J$",
"TTD" => "TT$",
"HTG" => "G",
"GYD" => "G$",
_ => code ?? "?"
};
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Diagnostics;
namespace Clario.Services;
public static class DebugLogger
{
[Conditional("DEBUG")]
public static void Log(object message) => Console.WriteLine(message);
}

View File

@@ -36,7 +36,7 @@ public class SupabaseService
}
catch (Exception ex)
{
Console.WriteLine($"Session restore failed: {ex.Message}");
DebugLogger.Log($"Session restore failed: {ex.Message}");
sessionStorage.Delete(); // session invalid, delete it
}
}

View File

@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Styling;
using Clario.Theme;
namespace Clario.Services;
@@ -21,11 +22,14 @@ public class ThemeService
{
"dark" => ThemeVariant.Dark,
"light" => ThemeVariant.Light,
"latte" => CustomAppThemeVariants.CatppuccinLatte,
"macchiato" => CustomAppThemeVariants.CatppuccinMacchiato,
"mocha" => CustomAppThemeVariants.CatppuccinMocha,
_ => ThemeVariant.Default
};
app.RequestedThemeVariant = themeVariant;
}
public static bool IsDarkTheme => Application.Current?.RequestedThemeVariant == ThemeVariant.Dark;
public static bool IsLightTheme => Application.Current?.RequestedThemeVariant == ThemeVariant.Light;
public static bool IsDarkTheme => Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
public static bool IsLightTheme => Application.Current?.ActualThemeVariant == ThemeVariant.Light;
}

View File

@@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Clario.CustomControls">
xmlns:cc="clr-namespace:Clario.CustomControls"
xmlns:theme="clr-namespace:Clario.Theme">
<Design.PreviewWith>
<Border Padding="20">
<cc:DateRangePicker SelectionMode="SingleRange"
@@ -59,15 +60,21 @@
<!-- LAYOUT -->
<x:Double x:Key="SidebarWidth">220</x:Double>
<!-- FLYOUT PRESENTER THEME -->
<ControlTheme x:Key="TransparentFlyoutPresenter"
TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
</ControlTheme>
<!-- Shared Logo Assets -->
<!-- Icon only, transparent bg -->
<x:String x:Key="LogoIconPrimaryTransparentSvg">avares://Clario/Assets/Logo/logo-icon-primary-transparent.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryTransparent1x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryTransparent2x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryTransparent4x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-512.png</Bitmap>
<!-- Combined (icon + text), transparent bg -->
<x:String x:Key="LogoCombinedPrimaryTransparentSvg">avares://Clario/Assets/Logo/logo-combined-primary-transparent.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryTransparent1x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryTransparent2x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryTransparent4x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-768x192.png</Bitmap>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
@@ -96,6 +103,10 @@
<SolidColorBrush x:Key="AccentOrange" Color="#FF7E5E" />
<SolidColorBrush x:Key="AccentPink" Color="#FF5E9B" />
<SolidColorBrush x:Key="DangerButtonBackground" Color="#2A0D0D" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#3A1515" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#3A1515" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#4A1C1C" />
<!-- SVG COLORS -->
@@ -148,6 +159,26 @@
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#6B8AEF" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#4A5A8A" />
<!-- logos -->
<!-- Icon only, dark bg -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<!-- Icon only, neutral -->
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<!-- Combined (icon + text) -->
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<!-- BACKGROUNDS -->
@@ -175,6 +206,11 @@
<SolidColorBrush x:Key="AccentOrange" Color="#E8622A" />
<SolidColorBrush x:Key="AccentPink" Color="#D4306A" />
<SolidColorBrush x:Key="DangerButtonBackground" Color="#FFE8E8" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#FFCCCC" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#FFD5D5" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#FFBCBC" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#E8EEFF" />
<SolidColorBrush x:Key="IconBgGreen" Color="#E0F5EC" />
@@ -213,13 +249,334 @@
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" />
<Bitmap x:Key="key">path</Bitmap>
<!-- logos -->
<!-- Icon only, light bg -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-512.png</Bitmap>
<!-- Icon only, neutral -->
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-512.png</Bitmap>
<!-- Combined (icon + text) -->
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinLatte}">
<!-- BACKGROUNDS -->
<SolidColorBrush x:Key="BgBase" Color="#e6e9ef" /> <!-- Mantle — app background -->
<SolidColorBrush x:Key="BgSidebar" Color="#dce0e8" /> <!-- Crust — sidebar (darker than bg) -->
<SolidColorBrush x:Key="BgSurface" Color="#eff1f5" /> <!-- Base — cards (lightest, elevated) -->
<SolidColorBrush x:Key="BgHover" Color="#ccd0da" /> <!-- Surface 0 — hover -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#bcc0cc" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="BorderAccent" Color="#8839ef" /> <!-- Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#4c4f69" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#5c5f77" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#6c6f85" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#9ca0b0" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#1e66f5" />
<SolidColorBrush x:Key="AccentGreen" Color="#40a02b" />
<SolidColorBrush x:Key="AccentYellow" Color="#df8e1d" />
<SolidColorBrush x:Key="AccentRed" Color="#d20f39" />
<SolidColorBrush x:Key="AccentPurple" Color="#8839ef" />
<SolidColorBrush x:Key="AccentOrange" Color="#fe640b" />
<SolidColorBrush x:Key="AccentPink" Color="#ea76cb" />
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#fce0e5" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#f7b8c4" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#f9cdd6" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#f4a5b6" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#d0dcfd" />
<SolidColorBrush x:Key="IconBgGreen" Color="#d6eecf" />
<SolidColorBrush x:Key="IconBgOrange" Color="#fee9d4" />
<SolidColorBrush x:Key="IconBgRed" Color="#fce0e5" />
<SolidColorBrush x:Key="IconBgPurple" Color="#e8d5fc" />
<SolidColorBrush x:Key="IconBgPink" Color="#fbd5f3" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#d6eecf" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#fce0e5" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#fdf0d0" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#d0dcfd" />
<SolidColorBrush x:Key="DividerAlpha" Color="#bcc0cc" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #eff1f5; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #4c4f69; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #5c5f77; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6c6f85; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #9ca0b0; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #1e66f5; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #40a02b; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #df8e1d; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #d20f39; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #8839ef; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #fe640b; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #ea76cb; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #eff1f5; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #4c4f69; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #5c5f77; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6c6f85; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #9ca0b0; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #1e66f5; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #40a02b; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #df8e1d; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #d20f39; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #8839ef; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #fe640b; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #ea76cb; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#bcc0cc" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#acb0be" />
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#eff1f5" />
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#7c7f93" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#7151c8" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#5a3db5" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#c9aafc" />
<!-- LOGOS -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinMacchiato}">
<!-- BACKGROUNDS -->
<!-- JetBrains: panelBackground=Mantle, editorBackground=Base, popup=Surface0, hover=Surface1 -->
<SolidColorBrush x:Key="BgBase" Color="#24273a" /> <!-- Base -->
<SolidColorBrush x:Key="BgSidebar" Color="#1e2030" /> <!-- Mantle -->
<SolidColorBrush x:Key="BgSurface" Color="#363a4f" /> <!-- Surface 0 -->
<SolidColorBrush x:Key="BgHover" Color="#494d64" /> <!-- Surface 1 -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#5b6078" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="BorderAccent" Color="#c6a0f6" /> <!-- Mauve — JB focusColor=Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#cad3f5" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#b8c0e0" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#a5adcb" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#6e738d" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#8aadf4" /> <!-- Blue -->
<SolidColorBrush x:Key="AccentGreen" Color="#a6da95" /> <!-- Green -->
<SolidColorBrush x:Key="AccentYellow" Color="#eed49f" /> <!-- Yellow -->
<SolidColorBrush x:Key="AccentRed" Color="#ed8796" /> <!-- Red -->
<SolidColorBrush x:Key="AccentPurple" Color="#c6a0f6" /> <!-- Mauve -->
<SolidColorBrush x:Key="AccentOrange" Color="#f5a97f" /> <!-- Peach -->
<SolidColorBrush x:Key="AccentPink" Color="#f5bde6" /> <!-- Pink -->
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#3a1a22" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#5a2535" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#4a2030" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#6a2d3f" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#1f2c4a" />
<SolidColorBrush x:Key="IconBgGreen" Color="#1e3328" />
<SolidColorBrush x:Key="IconBgOrange" Color="#3a2615" />
<SolidColorBrush x:Key="IconBgRed" Color="#3a1a22" />
<SolidColorBrush x:Key="IconBgPurple" Color="#2a1f3d" />
<SolidColorBrush x:Key="IconBgPink" Color="#38163a" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#1e3328" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#3a1a22" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#35290f" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#1f2c4a" />
<SolidColorBrush x:Key="DividerAlpha" Color="#5b6078" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #24273a; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cad3f5; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #b8c0e0; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a5adcb; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6e738d; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #8aadf4; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6da95; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #eed49f; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #ed8796; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #c6a0f6; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5a97f; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5bde6; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #24273a; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cad3f5; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #b8c0e0; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a5adcb; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6e738d; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #8aadf4; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6da95; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #eed49f; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #ed8796; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #c6a0f6; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5a97f; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5bde6; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#494d64" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#5b6078" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#24273a" /> <!-- Base -->
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#939ab7" /> <!-- Overlay 2 -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#d4baff" /> <!-- Mauve brightened -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#a980e8" /> <!-- Mauve pressed -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#4a3a60" /><!-- Mauve muted -->
<!-- LOGOS -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinMocha}">
<!-- BACKGROUNDS -->
<!-- JetBrains: panelBackground=Mantle, editorBackground=Base, popup=Surface0, hover=Surface1 -->
<SolidColorBrush x:Key="BgBase" Color="#1e1e2e" /> <!-- Base -->
<SolidColorBrush x:Key="BgSidebar" Color="#181825" /> <!-- Mantle -->
<SolidColorBrush x:Key="BgSurface" Color="#313244" /> <!-- Surface 0 -->
<SolidColorBrush x:Key="BgHover" Color="#45475a" /> <!-- Surface 1 -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#585b70" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="BorderAccent" Color="#cba6f7" /> <!-- Mauve — JB focusColor=Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#cdd6f4" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#bac2de" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#a6adc8" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#6c7086" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#89b4fa" /> <!-- Blue -->
<SolidColorBrush x:Key="AccentGreen" Color="#a6e3a1" /> <!-- Green -->
<SolidColorBrush x:Key="AccentYellow" Color="#f9e2af" /> <!-- Yellow -->
<SolidColorBrush x:Key="AccentRed" Color="#f38ba8" /> <!-- Red -->
<SolidColorBrush x:Key="AccentPurple" Color="#cba6f7" /> <!-- Mauve -->
<SolidColorBrush x:Key="AccentOrange" Color="#fab387" /> <!-- Peach -->
<SolidColorBrush x:Key="AccentPink" Color="#f5c2e7" /> <!-- Pink -->
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#3d1424" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#5e1e38" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#4d1a2e" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#6e2442" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#1a2847" />
<SolidColorBrush x:Key="IconBgGreen" Color="#1a3025" />
<SolidColorBrush x:Key="IconBgOrange" Color="#3d2410" />
<SolidColorBrush x:Key="IconBgRed" Color="#3d1424" />
<SolidColorBrush x:Key="IconBgPurple" Color="#291c3f" />
<SolidColorBrush x:Key="IconBgPink" Color="#3a1035" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#1a3025" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#3d1424" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#3a2c0a" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#1a2847" />
<SolidColorBrush x:Key="DividerAlpha" Color="#585b70" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #1e1e2e; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cdd6f4; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #bac2de; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6adc8; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6c7086; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #89b4fa; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6e3a1; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f9e2af; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f38ba8; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cba6f7; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #fab387; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5c2e7; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #1e1e2e; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cdd6f4; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #bac2de; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6adc8; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6c7086; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #89b4fa; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6e3a1; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f9e2af; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f38ba8; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cba6f7; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #fab387; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5c2e7; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#45475a" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#585b70" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#1e1e2e" /> <!-- Base -->
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#9399b2" /> <!-- Overlay 2 -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#d9baff" /> <!-- Mauve brightened -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#ae87e8" /> <!-- Mauve pressed -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#3d2a5e" /><!-- Mauve muted -->
<!-- LOGOS -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<ControlTheme x:Key="TransparentFlyoutPresenter"
TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
</ControlTheme>
</ResourceDictionary>
</Styles.Resources>
<!-- WINDOW / SHELL -->
<Style Selector="Window, FlyoutPresenter, ToolTip">
@@ -594,6 +951,30 @@
<Setter Property="FocusAdorner" Value="{x:Null}" />
</Style>
<!-- DANGER BUTTON -->
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource DangerButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource DangerButtonBorder}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource AccentRed}" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource DangerButtonBackgroundBrush}" />
<Setter Property="Opacity" Value="0.85" />
</Style>
<Style Selector="Button.danger:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource DangerButtonBorderBrush}" />
</Style>
<Style Selector="Button.danger:disabled /template/ ContentPresenter">
<Setter Property="Opacity" Value="0.4" />
</Style>
<!-- BASE BUTTON -->

View File

@@ -0,0 +1,10 @@
using Avalonia.Styling;
namespace Clario.Theme;
public static class CustomAppThemeVariants
{
public static readonly ThemeVariant CatppuccinLatte = new("CatppuccinLatte", ThemeVariant.Light);
public static readonly ThemeVariant CatppuccinMacchiato = new("CatppuccinMacchiato", ThemeVariant.Dark);
public static readonly ThemeVariant CatppuccinMocha = new("CatppuccinMocha", ThemeVariant.Dark);
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -36,6 +37,39 @@ public partial class AccountFormViewModel : ViewModelBase
[ObservableProperty] private string _currency = "USD";
[ObservableProperty] private bool _isPrimary = false;
[ObservableProperty] private string _currencySearch = "";
[ObservableProperty] private int _currencyPage = 1;
private const int CurrencyPageSize = 30;
public List<string> FilteredCurrencies =>
string.IsNullOrWhiteSpace(CurrencySearch)
? CurrencyService.AvailableCurrencies.ToList()
: CurrencyService.AvailableCurrencies
.Where(c => c.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase)
|| (CurrencyService.CurrencyNames.TryGetValue(c, out var name)
&& name.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase)))
.ToList();
public List<string> VisibleCurrencies => FilteredCurrencies.Take(CurrencyPage * CurrencyPageSize).ToList();
public bool HasMoreCurrencies => FilteredCurrencies.Count > CurrencyPage * CurrencyPageSize;
partial void OnCurrencySearchChanged(string value)
{
_currencyPage = 1;
OnPropertyChanged(nameof(FilteredCurrencies));
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
partial void OnCurrencyPageChanged(int value)
{
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
[ObservableProperty] private string? _creditLimit;
[ObservableProperty] private List<DateTime>? _openedAtDates;
@@ -47,8 +81,6 @@ public partial class AccountFormViewModel : ViewModelBase
// ── Options ─────────────────────────────────────────────
[ObservableProperty] private List<string> _accountTypes = new() { "Cash", "Checking", "Savings", "Credit", "Investment", "Other" };
[ObservableProperty] private List<string> _currencies = new() { "USD", "EUR", "GBP", "CAD", "AUD" };
[ObservableProperty] private List<string> _icons = new() { "wallet", "credit-card", "banknote", "landmark", "piggy-bank", "dollar-sign" };
// ── Validation ──────────────────────────────────────────
@@ -80,6 +112,21 @@ public partial class AccountFormViewModel : ViewModelBase
OnPropertyChanged(nameof(IsCredit));
}
[RelayCommand]
private void SetCurrency(string currency) => Currency = currency;
[RelayCommand]
private void LoadMoreCurrencies() => CurrencyPage++;
private void ResetCurrencyFilter()
{
_currencySearch = "";
_currencyPage = 1;
OnPropertyChanged(nameof(CurrencySearch));
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
[RelayCommand]
private async Task Save()
{
@@ -114,10 +161,11 @@ public partial class AccountFormViewModel : ViewModelBase
{
if (IsEditMode && _editingId.HasValue)
{
if (IsPrimary) await DataRepo.General.SetPrimaryAccountAsync(_editingId.Value);
var updated = new Account
{
Id = _editingId.Value,
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
Name = Name.Trim(),
Type = SelectedType,
Institution = Institution?.Trim(),
@@ -128,16 +176,19 @@ public partial class AccountFormViewModel : ViewModelBase
OpenedAt = OpenedAtDates?[0],
Icon = SelectedIcon,
Color = SelectedColor,
IsPrimary = IsPrimary,
};
await DataRepo.General.UpdateAccount(updated);
ResultAccount = updated;
}
else
{
var newId = Guid.NewGuid();
if (IsPrimary) await DataRepo.General.SetPrimaryAccountAsync(newId);
var account = new Account
{
Id = Guid.NewGuid(),
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id!),
Id = newId,
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
Name = Name.Trim(),
Type = SelectedType,
Institution = Institution?.Trim(),
@@ -148,6 +199,7 @@ public partial class AccountFormViewModel : ViewModelBase
OpenedAt = OpenedAtDates?[0],
Icon = SelectedIcon,
Color = SelectedColor,
IsPrimary = IsPrimary,
};
var result = await DataRepo.General.InsertAccount(account);
ResultAccount = result;
@@ -158,7 +210,7 @@ public partial class AccountFormViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
@@ -180,7 +232,9 @@ public partial class AccountFormViewModel : ViewModelBase
Institution = null;
Mask = null;
OpeningBalance = "0.00";
ResetCurrencyFilter();
Currency = DataRepo.General.Profile?.Currency ?? "USD";
IsPrimary = false;
CreditLimit = null;
OpenedAtDates = null;
SelectedIcon = "wallet";
@@ -194,12 +248,14 @@ public partial class AccountFormViewModel : ViewModelBase
{
IsEditMode = true;
_editingId = account.Id;
CurrencySearch = "";
Name = account.Name;
SelectedType = account.Type;
SelectedType = AccountTypes.FirstOrDefault(t => t.Equals(account.Type, StringComparison.OrdinalIgnoreCase)) ?? account.Type;
Institution = account.Institution;
Mask = account.Mask;
OpeningBalance = account.OpeningBalance.ToString("0.00");
Currency = account.Currency;
IsPrimary = account.IsPrimary;
CreditLimit = account.CreditLimit?.ToString("0.00");
OpenedAtDates = account.OpenedAt.HasValue ? new List<DateTime> { account.OpenedAt.Value } : null;
SelectedIcon = account.Icon;

View File

@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -17,6 +18,7 @@ public partial class AccountsViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Account> _visibleAccounts = new();
[ObservableProperty] private decimal _totalBalance;
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private Account? _selectedAccount;
[ObservableProperty] private bool _isAccountDeletionConfirmationVisible;
public bool CanDeleteAccount => VisibleAccounts.Count > 1;
@@ -39,6 +41,8 @@ public partial class AccountsViewModel : ViewModelBase
private void FetchAndProcessAccountInfo()
{
TotalBalance = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
{
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
@@ -52,7 +56,10 @@ public partial class AccountsViewModel : ViewModelBase
var lastMonthBalance = accountTransactions.Where(t => t.Date.Month == DateTime.Now.AddMonths(-1).Month && t.Type == "income")
.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
account.MonthlyIncrease = account.TotalIncomeThisMonth - account.TotalExpenseThisMonth - lastMonthBalance;
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalBalance += account.CurrentBalance;
else
TotalBalance += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
}
}
@@ -83,7 +90,11 @@ public partial class AccountsViewModel : ViewModelBase
VisibleAccounts.Clear();
foreach (var type in accountTypes)
{
var accountsOfType = AppData.Accounts.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase)).ToList();
var accountsOfType = AppData.Accounts
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(a => a.IsPrimary)
.ThenBy(a => a.CreatedAt)
.ToList();
if (accountsOfType.Any())
{
var header = new Account { Name = type.ToUpper(), GroupHeader = true };

View File

@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -34,17 +38,25 @@ public partial class AuthViewModel : ViewModelBase
public AuthViewModel()
{
Console.WriteLine("auth vm loaded");
DebugLogger.Log("auth vm loaded");
setDefaults();
}
[Conditional("DEBUG")]
private void setDefaults()
{
FirstName = "clario";
LastName = "testing";
Email = "Clario@testing.com";
Password = "1234ABCD6767";
ConfirmPassword = "1234ABCD6767";
if (!File.Exists("devsettings.json")) return;
var json = File.ReadAllText("devsettings.json");
var config = JsonSerializer.Deserialize<Wrapper>(json);
if (config?.TestDefaults is null) return;
FirstName = config.TestDefaults.FirstName;
LastName = config.TestDefaults.LastName;
Email = config.TestDefaults.Email;
Password = config.TestDefaults.Password;
ConfirmPassword = config.TestDefaults.Password;
ThemeService.SwitchToTheme("system");
}
@@ -74,7 +86,7 @@ public partial class AuthViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -110,7 +122,7 @@ public partial class AuthViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -124,3 +136,8 @@ public partial class AuthViewModel : ViewModelBase
!string.IsNullOrWhiteSpace(_email) &&
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
}
class Wrapper
{
public TestDefaults TestDefaults { get; set; }
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -101,7 +102,7 @@ public partial class BudgetFormViewModel : ViewModelBase
var updated = new Budget
{
Id = _editingId.Value,
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
CategoryId = SelectedCategory.Id,
LimitAmount = amt,
Period = Period,
@@ -117,7 +118,7 @@ public partial class BudgetFormViewModel : ViewModelBase
var budget = new Budget
{
Id = Guid.NewGuid(),
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id!),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
CategoryId = SelectedCategory.Id,
LimitAmount = amt,
Period = Period,
@@ -134,7 +135,7 @@ public partial class BudgetFormViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
@@ -151,7 +152,7 @@ public partial class BudgetFormViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to delete budget.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}

View File

@@ -5,7 +5,10 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Messages;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
@@ -37,11 +40,16 @@ public partial class BudgetViewModel : ViewModelBase
public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget.";
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
public string TotalLeftFormatted => TotalLeft.ToString("C0") + " left";
private string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
public string TotalLeftFormatted => $"{PrimarySymbol}{TotalLeft:N0} left";
public string SavingsHint => TotalLeft >= (AppData.Profile != null ? AppData.Profile.SavingsGoal : 0)
? "You're on track!"
: $"Reduce your spending by ${Math.Round((AppData.Profile != null ? AppData.Profile.SavingsGoal ?? 0 : 0) - TotalLeft)} to hit your goal.";
public bool HasSavingsGoal => AppData.Profile?.SavingsGoal is > 0;
public bool IsSavingsGoalMet => HasSavingsGoal && TotalLeft >= (AppData.Profile!.SavingsGoal ?? 0);
public string SavingsHint => IsSavingsGoalMet
? "You're on track to meet your savings goal this month!"
: $"Reduce your spending by {PrimarySymbol}{((AppData.Profile?.SavingsGoal ?? 0) - TotalLeft):N0} to hit your goal.";
private int _onTrackCount;
private int _approachingCount;
@@ -57,12 +65,18 @@ public partial class BudgetViewModel : ViewModelBase
public string PeriodDaysLeftFormatted => PeriodDaysLeft == 1 ? PeriodDaysLeft + " day left" : PeriodDaysLeft + " days left";
public string DailyBudgetLeftFormatted =>
((TotalBudgeted - TotalSpent) / ((PeriodDaysLeft == 0) ? 1 : PeriodDaysLeft)).ToString("C", new CultureInfo("en-US"));
$"{PrimarySymbol}{((TotalBudgeted - TotalSpent) / ((PeriodDaysLeft == 0) ? 1 : PeriodDaysLeft)):N2}";
public BudgetViewModel()
{
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
};
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize());
_ = Initialize();
}
@@ -72,10 +86,11 @@ public partial class BudgetViewModel : ViewModelBase
{
await ProcessBudgets();
ProcessChartData();
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -92,6 +107,12 @@ public partial class BudgetViewModel : ViewModelBase
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
}
[RelayCommand]
private void EditSavingsGoal()
{
((MainViewModel)parentViewModel).OpenEditSavingsGoalCommand.Execute(null);
}
private void ProcessChartData()
{
var tempCategorySpendingBreakdown = new List<(Category category, double[] spent)>();
@@ -101,7 +122,7 @@ public partial class BudgetViewModel : ViewModelBase
var spent = AppData.Transactions
.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase) &&
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year)
.Sum(x => x.Amount);
.Sum(x => x.ConvertedAmount);
if (spent == 0) continue;
double[] values = [(double)spent];
tempCategorySpendingBreakdown.Add((category, values));
@@ -115,7 +136,7 @@ public partial class BudgetViewModel : ViewModelBase
Values = x.spent,
Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)),
InnerRadius = 60,
ToolTipLabelFormatter = point => $"${point.Coordinate.PrimaryValue:N0}"
ToolTipLabelFormatter = point => $"{PrimarySymbol}{point.Coordinate.PrimaryValue:N0}"
}).ToArray();
SpendingBreakdownLegends = tempSpendingBreakdownLegends.OrderByDescending(x => x.Spent).ToList();
@@ -142,6 +163,8 @@ public partial class BudgetViewModel : ViewModelBase
OnPropertyChanged(nameof(SpentPercentageFormatted));
OnPropertyChanged(nameof(TotalLeft));
OnPropertyChanged(nameof(TotalLeftFormatted));
OnPropertyChanged(nameof(HasSavingsGoal));
OnPropertyChanged(nameof(IsSavingsGoalMet));
OnPropertyChanged(nameof(SavingsHint));
OnPropertyChanged(nameof(OnTrackCountFormatted));
OnPropertyChanged(nameof(ApproachingCountFormatted));

View File

@@ -5,6 +5,9 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Clario.Data;
using Clario.Messages;
using Clario.Services;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Clario.Models;
@@ -27,6 +30,7 @@ public partial class DashboardViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Account> _accountsSummaryData = new();
[ObservableProperty] private ObservableCollection<Transaction> _recentTransactions = new();
[ObservableProperty] private decimal _totalNetworth;
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private decimal _monthlyIncome;
private decimal _monthlyIncomeChange;
private bool _hasLastMonthIncome;
@@ -109,6 +113,7 @@ public partial class DashboardViewModel : ViewModelBase
AppData.Accounts.CollectionChanged += (s, e) => UpdateUserOverview();
AppData.Categories.CollectionChanged += (s, e) => UpdateUserOverview();
AppData.Budgets.CollectionChanged += (s, e) => UpdateUserOverview();
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview());
initialize();
}
@@ -133,13 +138,13 @@ public partial class DashboardViewModel : ViewModelBase
var lastMonth = thisMonth.AddMonths(-1);
MonthlyIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == thisMonth.Month && x.Date.Year == thisMonth.Year)
.Sum(x => x.Amount);
.Sum(x => x.ConvertedAmount);
MonthlyExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month && x.Date.Year == DateTime.Now.Year)
.Sum(x => x.Amount);
.Sum(x => x.ConvertedAmount);
var lastMonthIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.Amount);
.Sum(x => x.ConvertedAmount);
var lastMonthExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.Amount);
.Sum(x => x.ConvertedAmount);
_hasLastMonthIncome = lastMonthIncome > 0;
_hasLastMonthExpenses = lastMonthExpenses > 0;
@@ -207,7 +212,7 @@ public partial class DashboardViewModel : ViewModelBase
break;
}
var balance = categoryTransactions.Sum(x => x.Amount);
var balance = categoryTransactions.Sum(x => x.ConvertedAmount);
if (balance == 0) continue;
tempList.Add(new ColumnChartData()
{ id = category.Id, Name = category.Name, Values = [(double)balance], Fill = new SolidColorPaint(SKColor.Parse(category.Color)) });
@@ -242,11 +247,16 @@ public partial class DashboardViewModel : ViewModelBase
private void UpdateAccountsSummary()
{
TotalNetworth = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
{
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalNetworth += account.CurrentBalance;
else
TotalNetworth += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
}
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.OrderBy(x => x.CreatedAt));

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -111,7 +112,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to delete account. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
@@ -148,7 +149,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Migration failed. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
}

View File

@@ -29,11 +29,13 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private AccountFormViewModel _accountFormViewModel = null!;
[ObservableProperty] private BudgetFormViewModel _budgetFormViewModel = null!;
[ObservableProperty] private SettingsViewModel _settingsViewModel = null!;
[ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!;
[ObservableProperty] private bool _isDimmed;
[ObservableProperty] private bool _isTransactionFormVisible;
[ObservableProperty] private bool _isAccountFormVisible;
[ObservableProperty] private bool _isBudgetFormVisible;
[ObservableProperty] private bool _isSavingsGoalDialogVisible;
[ObservableProperty]
@@ -44,8 +46,13 @@ public partial class MainViewModel : ViewModelBase
public MainViewModel()
{
Console.WriteLine("main vm loaded");
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) => { Profile = AppData.Profile; });
DebugLogger.Log("main vm loaded");
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) =>
{
Profile = AppData.Profile;
_ = DataRepo.General.RefreshLiveRatesAndEnrich();
});
IsDimmed = true;
CurrentView = new LoadingViewModel();
_ = InitializeApp();
}
@@ -62,68 +69,72 @@ public partial class MainViewModel : ViewModelBase
var transactionsTask = DataRepo.General.FetchTransactions();
var accountsTask = DataRepo.General.FetchAccounts();
var budgetsTask = DataRepo.General.FetchBudgets();
await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask);
Profile = profilesTask.Result;
DataRepo.General.LinkTransactionCategories();
await DataRepo.General.RefreshLiveRatesAndEnrich();
Console.WriteLine("fetched all data");
DebugLogger.Log("fetched all data");
});
AppData.Accounts.CollectionChanged += (_, _) => _ = DataRepo.General.RefreshLiveRatesAndEnrich();
_dashboardViewModel = new DashboardViewModel()
{
parentViewModel = this
};
CurrentView = _dashboardViewModel;
Console.WriteLine("initialized DashboardViewModel");
DebugLogger.Log("initialized DashboardViewModel");
_transactionsViewModel = new TransactionsViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized TransactionsViewModel");
DebugLogger.Log("initialized TransactionsViewModel");
_accountsViewModel = new AccountsViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized AccountsViewModel");
DebugLogger.Log("initialized AccountsViewModel");
_budgetViewModel = new BudgetViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized BudgetViewModel");
DebugLogger.Log("initialized BudgetViewModel");
SettingsViewModel = new SettingsViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized SettingsViewModel");
DebugLogger.Log("initialized SettingsViewModel");
TransactionFormViewModel = new TransactionFormViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized TransactionFormViewModel");
DebugLogger.Log("initialized TransactionFormViewModel");
AccountFormViewModel = new AccountFormViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized AccountFormViewModel");
DebugLogger.Log("initialized AccountFormViewModel");
BudgetFormViewModel = new BudgetFormViewModel()
{
parentViewModel = this
};
Console.WriteLine("initialized BudgetFormViewModel");
DebugLogger.Log("initialized BudgetFormViewModel");
SetSavingsGoalDialogViewModel = new SetSavingsGoalDialogViewModel();
DebugLogger.Log("initialized SetSavingsGoalDialogViewModel");
IsDarkTheme = ThemeService.IsDarkTheme;
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
CurrentView = _dashboardViewModel;
IsDimmed = false;
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -142,7 +153,7 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -179,7 +190,7 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -198,7 +209,7 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -225,7 +236,7 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -246,7 +257,7 @@ public partial class MainViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -257,6 +268,23 @@ public partial class MainViewModel : ViewModelBase
IsBudgetFormVisible = false;
}
[RelayCommand]
public void OpenEditSavingsGoal()
{
if (IsDimmed) return;
SetSavingsGoalDialogViewModel.Setup(AppData.Profile?.SavingsGoal);
SetSavingsGoalDialogViewModel.OnSaved = CloseSavingsGoalDialog;
SetSavingsGoalDialogViewModel.OnCancelled = CloseSavingsGoalDialog;
IsSavingsGoalDialogVisible = true;
IsDimmed = true;
}
private void CloseSavingsGoalDialog()
{
IsSavingsGoalDialogVisible = false;
IsDimmed = false;
}
[RelayCommand]
private void SwitchTheme()
{

View File

@@ -0,0 +1,85 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models.GeneralModels;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Clario.ViewModels;
public partial class SetSavingsGoalDialogViewModel : ViewModelBase
{
public GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsValid))]
private string _goalInput = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public bool IsValid =>
decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) && v >= 0;
// ── Callbacks ────────────────────────────────────────────
public Action? OnSaved;
public Action? OnCancelled;
// ── Setup ────────────────────────────────────────────────
public void Setup(decimal? currentGoal)
{
GoalInput = currentGoal.HasValue
? currentGoal.Value.ToString("F2", CultureInfo.InvariantCulture)
: string.Empty;
ErrorMessage = null;
}
// ── Commands ─────────────────────────────────────────────
[RelayCommand]
private void Cancel() => OnCancelled?.Invoke();
[RelayCommand]
private async Task Save()
{
if (!decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var amount) || amount < 0)
{
ErrorMessage = "Please enter a valid amount.";
return;
}
ErrorMessage = null;
try
{
await DataRepo.General.UpdateSavingsGoal(amount);
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Failed to save savings goal. Please try again.";
DebugLogger.Log(ex);
}
}
[RelayCommand]
private async Task Clear()
{
ErrorMessage = null;
try
{
await DataRepo.General.UpdateSavingsGoal(null);
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Failed to clear savings goal. Please try again.";
DebugLogger.Log(ex);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@@ -27,7 +28,6 @@ public partial class SettingsViewModel : ViewModelBase
[ObservableProperty] private string _displayName = "";
[ObservableProperty] private string _avatarUrl = "";
[ObservableProperty] private Bitmap? _avatarImage;
[ObservableProperty] private string _selectedCurrency = "USD";
[ObservableProperty] private string _selectedTheme = "system";
[ObservableProperty] private string _selectedLanguage = "en";
@@ -77,22 +77,20 @@ public partial class SettingsViewModel : ViewModelBase
public bool HasAvatar => !string.IsNullOrEmpty(AvatarUrl);
// ── Options ──────────────────────────────────────────────
public ObservableCollection<string> Currencies { get; } = new()
{
"USD", "EUR", "GBP", "JPY", "AED", "SAR", "JOD",
"EGP", "CAD", "AUD", "CHF", "CNY", "INR", "BRL"
};
public ObservableCollection<(string Value, string Label)> Themes { get; } = new()
{
("system", "System default"),
("dark", "Dark"),
("light", "Light")
("light", "Light"),
("latte", "Catppuccin Latte"),
("macchiato", "Catppuccin Macchiato"),
("mocha", "Catppuccin Mocha")
};
public ObservableCollection<string> ThemeLabels { get; } = new()
{
"System default", "Dark", "Light"
"System default", "Dark", "Light", "Catppuccin Latte", "Catppuccin Macchiato", "Catppuccin Mocha"
};
public ObservableCollection<(string Value, string Label)> Languages { get; } = new()
@@ -112,7 +110,7 @@ public partial class SettingsViewModel : ViewModelBase
partial void OnSelectedThemeIndexChanged(int value)
{
SelectedTheme = value switch { 0 => "system", 1 => "dark", 2 => "light", _ => "system" };
SelectedTheme = value switch { 0 => "system", 1 => "dark", 2 => "light", 3 => "latte", 4 => "macchiato", 5 => "mocha", _ => "system" };
}
partial void OnSelectedLanguageIndexChanged(int value)
@@ -132,12 +130,11 @@ public partial class SettingsViewModel : ViewModelBase
DisplayName = AppData.Profile?.DisplayName ?? "";
AvatarUrl = DataRepo.General.BuildPublicUrl(AppData.Profile?.AvatarUrl) ?? "";
AvatarImage = AppData.Profile?.Avatar;
SelectedCurrency = AppData.Profile?.Currency ?? "USD";
SelectedTheme = AppData.Profile?.Theme ?? "system";
SelectedLanguage = AppData.Profile?.Language ?? "en";
// sync indices
SelectedThemeIndex = SelectedTheme switch { "dark" => 1, "light" => 2, _ => 0 };
SelectedThemeIndex = SelectedTheme switch { "dark" => 1, "light" => 2, "latte" => 3, "macchiato" => 4, "mocha" => 5, _ => 0 };
SelectedLanguageIndex = SelectedLanguage switch { "ar" => 1, _ => 0 };
// mask email
@@ -184,7 +181,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to upload avatar. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
finally
{
@@ -210,7 +207,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to remove avatar.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
finally
{
@@ -239,7 +236,7 @@ public partial class SettingsViewModel : ViewModelBase
{
Id = AppData.Profile.Id,
DisplayName = DisplayName.Trim(),
Currency = SelectedCurrency,
Currency = AppData.Profile?.Currency ?? "USD",
Theme = SelectedTheme,
Language = SelectedLanguage,
AvatarUrl = AppData.Profile.AvatarUrl,
@@ -258,7 +255,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to save profile. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
finally
{
@@ -322,7 +319,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
EmailErrorMessage = "Failed to update email. Check your password and try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
finally
{
@@ -392,7 +389,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
PasswordErrorMessage = "Failed to update password. Check your current password and try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
finally
{
@@ -412,7 +409,7 @@ public partial class SettingsViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to sign out.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
}

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Clario.Data;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -36,7 +38,17 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] private string? _note;
[ObservableProperty] private List<DateTime> _dates = [DateTime.Now];
[ObservableProperty] private DateTime? _selectedDate;
[ObservableProperty] private string _currency = "USD";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(CurrencySymbol))]
private string _currency = "USD";
public string CurrencySymbol => CurrencyService.GetSymbol(Currency);
[ObservableProperty] private string _exchangeRate = "";
[ObservableProperty] private bool _isFetchingRate = false;
[ObservableProperty] private bool _showExchangeRateField = false;
public string ExchangeRateLabel =>
$"1 {SelectedAccount?.Currency ?? "?"} = ? {AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD"}";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private Category? _selectedCategory;
@@ -72,22 +84,134 @@ public partial class TransactionFormViewModel : ViewModelBase
// ── Edit mode: original transaction ─────────────────────
private Transaction? _editingTransaction;
private Guid? _editingId;
private decimal _editingOriginalAmount;
private Guid? _editingOriginalCategoryId;
// ── Result transaction ──────────────────────────────────
public Transaction? ResultTransaction { get; set; }
// ── Budget warning ──────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasBudgetWarning), nameof(HasBudgetApproachingWarning))]
private string? _budgetWarningMessage;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasBudgetWarning), nameof(HasBudgetApproachingWarning))]
private bool _budgetWarningIsOverBudget;
public bool HasBudgetWarning => !string.IsNullOrEmpty(BudgetWarningMessage);
public bool HasBudgetApproachingWarning => HasBudgetWarning && !BudgetWarningIsOverBudget;
// ── Commands ────────────────────────────────────────────
partial void OnSelectedCategoryChanged(Category? value)
{
if (value.Type == Type) return;
Type = value.Type;
if (value.Type != Type) Type = value.Type;
CheckBudgetImpact();
}
partial void OnAmountChanged(string value)
{
CheckBudgetImpact();
}
partial void OnDatesChanged(List<DateTime> value)
{
CheckBudgetImpact();
}
partial void OnTypeChanged(string value)
{
if (value == SelectedCategory?.Type) return;
SelectedCategory = _categories.FirstOrDefault(c => c.Type == value);
CheckBudgetImpact();
}
partial void OnSelectedAccountChanged(Account? value)
{
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
var accountCurrency = value?.Currency ?? primaryCurrency;
Currency = accountCurrency;
var needsRate = !accountCurrency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase);
if (needsRate)
{
IsFetchingRate = true;
ExchangeRate = "";
}
ShowExchangeRateField = needsRate;
OnPropertyChanged(nameof(ExchangeRateLabel));
if (needsRate)
_ = FetchExchangeRateAsync(accountCurrency, primaryCurrency);
}
private async Task FetchExchangeRateAsync(string from, string to)
{
try
{
var rate = await CurrencyService.GetExchangeRateAsync(from, to);
ExchangeRate = rate.HasValue ? rate.Value.ToString("0.##########") : "";
}
catch (Exception ex)
{
DebugLogger.Log(ex);
}
finally
{
IsFetchingRate = false;
}
}
private void CheckBudgetImpact()
{
BudgetWarningMessage = null;
BudgetWarningIsOverBudget = false;
if (Type != "expense") return;
if (SelectedCategory is null) return;
Debug.WriteLine(SelectedCategory.Name);
if (!decimal.TryParse(Amount, out var newAmt) || newAmt <= 0) return;
var budget = DataRepo.General.Budgets.FirstOrDefault(b => b.CategoryId == SelectedCategory.Id);
if (budget is null) return;
var transactionDate = Dates?.FirstOrDefault() ?? DateTime.Now;
var transactions = DataRepo.General.Transactions;
decimal alreadySpent = budget.Period.ToLower() switch
{
"monthly" => transactions
.Where(t => t.CategoryId == budget.CategoryId &&
t.Date.Month == transactionDate.Month &&
t.Date.Year == transactionDate.Year)
.Sum(t => t.Amount),
"quarterly" => transactions
.Where(t => t.CategoryId == budget.CategoryId &&
t.Date.Month >= transactionDate.Month - 3 &&
t.Date.Month <= transactionDate.Month &&
t.Date.Year == transactionDate.Year)
.Sum(t => t.Amount),
"yearly" => transactions
.Where(t => t.CategoryId == budget.CategoryId &&
t.Date.Year == transactionDate.Year)
.Sum(t => t.Amount),
_ => 0
};
if (IsEditMode && _editingOriginalCategoryId == SelectedCategory.Id)
alreadySpent -= _editingOriginalAmount;
var projectedSpent = alreadySpent + newAmt;
if (projectedSpent > budget.LimitAmount)
{
var overBy = projectedSpent - budget.LimitAmount;
BudgetWarningIsOverBudget = true;
BudgetWarningMessage = $"This will exceed your {SelectedCategory.Name} budget by ${overBy:N2}";
}
else if (budget.LimitAmount > 0 && (double)(projectedSpent / budget.LimitAmount) * 100 >= budget.AlertThreshold)
{
var pct = (double)(projectedSpent / budget.LimitAmount) * 100;
BudgetWarningIsOverBudget = false;
BudgetWarningMessage = $"This will bring your {SelectedCategory.Name} budget to {pct:N0}% used";
}
}
[RelayCommand]
@@ -135,10 +259,13 @@ public partial class TransactionFormViewModel : ViewModelBase
{
if (IsEditMode && _editingId.HasValue)
{
decimal? exchangeRateValue = null;
if (ShowExchangeRateField && decimal.TryParse(ExchangeRate, out var parsedRate) && parsedRate > 0)
exchangeRateValue = parsedRate;
var updated = new Transaction
{
Id = _editingId.Value,
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
Type = Type,
Amount = amt,
Description = Description.Trim(),
@@ -147,16 +274,20 @@ public partial class TransactionFormViewModel : ViewModelBase
CategoryId = SelectedCategory.Id,
AccountId = SelectedAccount.Id,
Category = SelectedCategory,
ExchangeRate = exchangeRateValue,
};
await DataRepo.General.UpdateTransaction(updated);
ResultTransaction = updated;
}
else
{
decimal? exchangeRateValue = null;
if (ShowExchangeRateField && decimal.TryParse(ExchangeRate, out var parsedRate) && parsedRate > 0)
exchangeRateValue = parsedRate;
var transaction = new Transaction
{
Id = Guid.NewGuid(),
UserId = Guid.Parse(Services.SupabaseService.Client.Auth.CurrentUser!.Id!),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
Type = Type,
Amount = amt,
Description = Description.Trim(),
@@ -165,6 +296,7 @@ public partial class TransactionFormViewModel : ViewModelBase
CategoryId = SelectedCategory.Id,
AccountId = SelectedAccount.Id,
Category = SelectedCategory,
ExchangeRate = exchangeRateValue,
};
await DataRepo.General.InsertTransaction(transaction);
ResultTransaction = transaction;
@@ -175,7 +307,7 @@ public partial class TransactionFormViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
@@ -192,7 +324,7 @@ public partial class TransactionFormViewModel : ViewModelBase
catch (Exception ex)
{
ErrorMessage = "Failed to delete transaction.";
Console.WriteLine(ex);
DebugLogger.Log(ex);
}
}
@@ -222,8 +354,12 @@ public partial class TransactionFormViewModel : ViewModelBase
ShowDeleteConfirm = false;
IsEditMode = false;
_editingId = null;
_editingOriginalAmount = 0;
_editingOriginalCategoryId = null;
Categories = AppData.Categories;
Accounts = AppData.Accounts;
var sortedAccounts = new ObservableCollection<Account>(
AppData.Accounts.OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
Accounts = sortedAccounts;
Type = "expense";
Amount = "";
Description = "";
@@ -231,8 +367,13 @@ public partial class TransactionFormViewModel : ViewModelBase
Dates = [DateTime.Now];
ErrorMessage = null;
SelectedCategory = AppData.Categories.Count > 0 ? AppData.Categories[0] : null;
SelectedAccount = AppData.Accounts.Count > 0 ? AppData.Accounts[0] : null;
SelectedAccount = sortedAccounts.Count > 0 ? sortedAccounts[0] : null;
ShowExchangeRateField = false;
ExchangeRate = "";
IsFetchingRate = false;
ResultTransaction = null;
BudgetWarningMessage = null;
BudgetWarningIsOverBudget = false;
}
/// <summary>Call this to open the form for editing an existing transaction.</summary>
@@ -242,8 +383,11 @@ public partial class TransactionFormViewModel : ViewModelBase
ShowDeleteConfirm = false;
IsEditMode = true;
_editingId = transaction.Id;
_editingOriginalAmount = transaction.Amount;
_editingOriginalCategoryId = transaction.CategoryId;
Categories = AppData.Categories;
Accounts = AppData.Accounts;
Accounts = new ObservableCollection<Account>(
AppData.Accounts.OrderByDescending(a => a.IsPrimary).ThenBy(a => a.CreatedAt));
Type = transaction.Type;
Amount = transaction.Amount.ToString("0.00");
Description = transaction.Description;
@@ -254,6 +398,18 @@ public partial class TransactionFormViewModel : ViewModelBase
?? (AppData.Categories.Count > 0 ? AppData.Categories[0] : null);
SelectedAccount = AppData.Accounts.FirstOrDefault(a => a.Id == transaction.AccountId)
?? (AppData.Accounts.Count > 0 ? AppData.Accounts[0] : null);
if (transaction.ExchangeRate.HasValue)
{
ShowExchangeRateField = true;
ExchangeRate = transaction.ExchangeRate.Value.ToString("0.##########");
}
else
{
ShowExchangeRateField = false;
ExchangeRate = "";
}
IsFetchingRate = false;
ResultTransaction = transaction;
CheckBudgetImpact();
}
}

View File

@@ -6,6 +6,7 @@ using System.Linq;
using Clario.Data;
using Clario.Messages;
using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -68,6 +69,9 @@ public partial class TransactionsViewModel : ViewModelBase
[ObservableProperty] private int _incomeCount;
[ObservableProperty] private string _dateRangeLabel = "";
public string PrimaryCurrencySymbol =>
CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private Category _selectedCategory;
[ObservableProperty] private Account _selectedAccount;
@@ -92,6 +96,7 @@ public partial class TransactionsViewModel : ViewModelBase
InitializeAccounts();
LoadPage(1);
};
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => LoadPage(CurrentPage));
Initialize();
}
@@ -219,9 +224,9 @@ public partial class TransactionsViewModel : ViewModelBase
}
// Calculate totals based on date-filtered transactions
TotalExpenses = filtered.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.Amount));
TotalIncome = filtered.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.Amount));
// Calculate totals based on date-filtered transactions (converted to primary currency)
TotalExpenses = filtered.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
TotalIncome = filtered.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
if (SelectedCategory.Name != "All Categories")
filtered = filtered.Where(x => x.CategoryId == SelectedCategory.Id);
@@ -332,7 +337,7 @@ public partial class TransactionsViewModel : ViewModelBase
}
catch (Exception e)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
@@ -361,8 +366,8 @@ public partial class TransactionsViewModel : ViewModelBase
private void CalculateMonthlyFinancials()
{
TotalExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
TotalIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.Amount));
TotalExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount));
TotalIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount));
ExpensesCount = AppData.Transactions.Count(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month);
IncomeCount = AppData.Transactions.Count(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month);
}

View File

@@ -7,7 +7,8 @@
xmlns:behaviors="clr-namespace:Clario.Behaviors"
mc:Ignorable="d"
x:Class="Clario.Views.AccountFormView"
x:DataType="vm:AccountFormViewModel">
x:DataType="vm:AccountFormViewModel"
x:Name="AccountFormRoot">
<Design.DataContext>
<vm:AccountFormViewModel />
</Design.DataContext>
@@ -158,17 +159,47 @@
<!-- Currency -->
<StackPanel Grid.Column="2" Spacing="6">
<TextBlock Text="CURRENCY" Classes="label" />
<TextBox Text="{Binding CurrencySearch, Mode=TwoWay}"
Watermark="Search..."
FontSize="12"
Height="32"
Padding="10,0"
VerticalContentAlignment="Center" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}">
<ComboBox ItemsSource="{Binding Currencies}"
SelectedItem="{Binding Currency, Mode=TwoWay}"
Background="Transparent"
BorderThickness="0"
Padding="12,10"
FontSize="13"
HorizontalAlignment="Stretch" />
CornerRadius="{DynamicResource RadiusControl}"
Padding="8">
<ScrollViewer MaxHeight="100"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding FilteredCurrencies}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Classes="nav"
Content="{Binding}"
Margin="2"
Padding="8,4"
CornerRadius="6"
FontSize="12"
Command="{Binding DataContext.SetCurrencyCommand, ElementName=AccountFormRoot}"
CommandParameter="{Binding}">
<Classes.accented>
<MultiBinding Converter="{StaticResource EqualValueConverter}">
<Binding Path="." />
<Binding Path="DataContext.Currency" ElementName="AccountFormRoot" />
</MultiBinding>
</Classes.accented>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</StackPanel>
</Grid>
@@ -273,6 +304,22 @@
</StackPanel>
</Grid>
<!-- ── Primary account toggle ──────────── -->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="PRIMARY ACCOUNT" Classes="label" />
<TextBlock Text="Sets this account's currency as the reference currency"
FontSize="11"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding IsPrimary, Mode=TwoWay}"
OffContent=""
OnContent=""
VerticalAlignment="Center" />
</Grid>
<!-- ── Validation error ─────────────── -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"

View File

@@ -41,8 +41,15 @@
</Border>
<TextBlock Grid.Column="1" Text="Total Net Worth" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Text="{Binding TotalBalance,StringFormat='$0.00'}" FontSize="17" FontWeight="Bold"
Foreground="{DynamicResource AccentBlue}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" FontSize="17" FontWeight="Bold"
Foreground="{DynamicResource AccentBlue}" VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalBalance" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Border>
<ItemsControl ItemsSource="{Binding VisibleAccounts}">
@@ -73,8 +80,21 @@
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="3">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
Foreground="{DynamicResource TextPrimary}" VerticalAlignment="Center" />
<Border Classes="badge-green"
IsVisible="{Binding IsPrimary}"
CornerRadius="4"
Padding="5,2"
Background="{DynamicResource IconBgGreen}"
VerticalAlignment="Center">
<TextBlock Text="PRIMARY"
FontSize="9"
FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Institution}" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding Mask, Converter={StaticResource MaskToStringConverter}}" FontSize="12"
@@ -83,7 +103,7 @@
</StackPanel>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Spacing="4">
<TextBlock Text="{Binding CurrentBalance,StringFormat='$0.00'}" FontSize="15" FontWeight="Bold"
<TextBlock Text="{Binding CurrentBalanceFormatted}" FontSize="15" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Right" />
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Right"
IsVisible="{Binding !isCredit}">
@@ -105,7 +125,7 @@
</Svg.Css>
</Svg>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding MonthlyIncrease,Converter={StaticResource DecimalSignConverter}}"
<TextBlock Text="{Binding MonthlyIncreaseFormatted}"
FontSize="11">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource DecimalColorConverter}">
@@ -203,8 +223,20 @@
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="1" Spacing="3" Margin="0,0,0,14">
<TextBlock Text="CURRENCY" Classes="label" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding SelectedAccount.Currency}" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
Foreground="{DynamicResource TextPrimary}" VerticalAlignment="Center" />
<Border IsVisible="{Binding SelectedAccount.IsPrimary}"
CornerRadius="4"
Background="{DynamicResource IconBgGreen}"
Padding="5,2"
VerticalAlignment="Center">
<TextBlock Text="PRIMARY"
FontSize="9"
FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" />
</Border>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="0" Grid.Row="2" Spacing="3">
<TextBlock Text="OPENED" Classes="label" />
@@ -235,7 +267,7 @@
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="Money In" FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding SelectedAccount.TotalIncomeThisMonth, StringFormat='$0.00'}" FontSize="14" FontWeight="SemiBold"
<TextBlock Text="{Binding SelectedAccount.TotalIncomeFormatted}" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource AccentGreen}" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.IncomeTransactionsThisMonth}" FontSize="11"
@@ -248,7 +280,7 @@
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
<TextBlock Text="Money Out" FontSize="11" Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding SelectedAccount.TotalExpenseThisMonth,StringFormat='$0.00'}" FontSize="14" FontWeight="SemiBold"
<TextBlock Text="{Binding SelectedAccount.TotalExpenseFormatted}" FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource AccentRed}" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.ExpenseTransactionsThisMonth}" FontSize="11"
@@ -348,8 +380,15 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Net Worth" FontSize="12" Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="{Binding TotalBalance,StringFormat='$0.00'}" FontSize="13" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Grid.Column="1" FontSize="13" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalBalance" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</StackPanel>
</Border>
@@ -370,15 +409,18 @@
</StackPanel>
</Button>
<!-- Delete -->
<Button Background="#2A0D0D" BorderBrush="#3A1515" BorderThickness="1" CornerRadius="10" Padding="14,10"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Cursor="Hand"
Command="{Binding RequestDeleteAccountCommand}" CommandParameter="{Binding SelectedAccount}"
<Button Classes="danger"
CornerRadius="10"
Padding="14,10"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Command="{Binding RequestDeleteAccountCommand}"
CommandParameter="{Binding SelectedAccount}"
IsEnabled="{Binding CanDeleteAccount}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14" Css="{DynamicResource SvgRed}" />
<StackPanel Spacing="1">
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" Foreground="#FF5E5E" />
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" />
<TextBlock Text="Permanently removes all data" FontSize="11" Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</StackPanel>

View File

@@ -38,7 +38,7 @@
CornerRadius="16"
Height="80"
HorizontalAlignment="Center" Margin="0 0 0 10">
<Image Source="../Assets/logo-textmark.png" />
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
</Border>
<!-- REPLACE: app name -->
<StackPanel Spacing="4" HorizontalAlignment="Center">

View File

@@ -277,25 +277,16 @@
</Border>
<!-- ── Delete button (edit mode only) ── -->
<Button HorizontalAlignment="Stretch"
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#1A0808"
BorderBrush="#3A1515"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0,10"
Margin="0,0,0,10"
IsVisible="{Binding IsEditMode}"
Command="{Binding RequestDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="Delete Budget"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FF5E5E"
VerticalAlignment="Center" />
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Delete Budget" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>

View File

@@ -267,14 +267,26 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Budgeted" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to TotalBudgetedFormatted -->
<TextBlock Grid.Column="1" Text="{Binding TotalBudgeted, StringFormat='$0'}" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalBudgeted" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Spent" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to TotalSpentFormatted -->
<TextBlock Grid.Column="1" Text="{Binding TotalSpent, StringFormat='$0'}" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalSpent" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<!-- Overall progress bar -->
@@ -539,42 +551,107 @@
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<!-- REPLACE: Command="{Binding EditSavingsGoalCommand}" -->
<Button Grid.Column="1"
Background="Transparent"
BorderThickness="0"
Padding="4"
Cursor="Hand">
Cursor="Hand"
Command="{Binding EditSavingsGoalCommand}">
<Svg Path="../Assets/Icons/pencil.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
<!-- Empty state — no goal set -->
<StackPanel IsVisible="{Binding !HasSavingsGoal}"
Spacing="12"
HorizontalAlignment="Center">
<Border Background="{DynamicResource IconBgBlue}"
CornerRadius="12"
Width="44" Height="44"
HorizontalAlignment="Center">
<Svg Path="../Assets/Icons/target.svg" Width="20" Height="20" Css="{DynamicResource SvgBlue}" />
</Border>
<TextBlock Text="No savings goal set"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center" />
<TextBlock Text="Set a monthly savings target to track how much of your remaining budget you're saving."
FontSize="11"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center" />
<Button Classes="accented"
HorizontalAlignment="Center"
Padding="16,8"
Command="{Binding EditSavingsGoalCommand}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/plus.svg" Width="12" Height="12"
Css="{DynamicResource SvgBase}" />
<TextBlock Text="Set Goal"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- Goal content — goal is set -->
<StackPanel IsVisible="{Binding HasSavingsGoal}" Spacing="14">
<!-- Goal amount vs projected -->
<StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to SavingsGoalFormatted -->
<TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="PrimarySymbol" />
<Binding Path="AppData.Profile.SavingsGoal" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Projected savings" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<!-- REPLACE: bind to ProjectedSavingsFormatted -->
<TextBlock Grid.Column="1" Text="{Binding TotalLeftFormatted}" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource AccentYellow}" />
</Grid>
</StackPanel>
<!-- REPLACE: Value="{Binding SavingsGoalPercentage}" -->
<ProgressBar Classes="yellow" Value="{Binding TotalLeft}" Minimum="0" Maximum="{Binding AppData.Profile.SavingsGoal}" Height="6" />
<ProgressBar Classes="yellow"
Classes.green="{Binding IsSavingsGoalMet}"
Value="{Binding TotalLeft}"
Minimum="0"
Maximum="{Binding AppData.Profile.SavingsGoal}"
Height="6" />
<Border Background="{DynamicResource BadgeBgYellow}"
<!-- On track hint -->
<Border IsVisible="{Binding IsSavingsGoalMet}"
Background="{DynamicResource IconBgGreen}"
CornerRadius="10"
Padding="12,8">
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Svg Grid.Column="0" Path="../Assets/Icons/info.svg" Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }" />
<Svg Grid.Column="0" Path="../Assets/Icons/check.svg" Width="13" Height="13"
Css="{DynamicResource SvgGreen}" />
<TextBlock Grid.Column="1" Text="{Binding SavingsHint}"
FontSize="12"
Foreground="{DynamicResource AccentGreen}"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- Behind hint -->
<Border IsVisible="{Binding !IsSavingsGoalMet}"
Background="{DynamicResource BadgeBgYellow}"
CornerRadius="10"
Padding="12,8">
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Svg Grid.Column="0" Path="../Assets/Icons/info.svg" Width="13" Height="13"
Css="{DynamicResource SvgYellow}" />
<TextBlock Grid.Column="1" Text="{Binding SavingsHint}"
FontSize="12"
Foreground="{DynamicResource AccentYellow}"
@@ -582,6 +659,8 @@
VerticalAlignment="Center" />
</Grid>
</Border>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>

View File

@@ -8,7 +8,8 @@
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
MinWidth="780" MinHeight="600"
x:DataType="vm:DashboardViewModel"
x:Class="Clario.Views.DashboardView">
x:Class="Clario.Views.DashboardView"
x:Name="DashboardRoot">
<Design.DataContext>
<vm:DashboardViewModel />
</Design.DataContext>
@@ -53,8 +54,14 @@
</Border>
<TextBlock Classes="label" Text="MONTHLY INCOME" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding MonthlyIncome, StringFormat='$0.00'}" FontSize="{StaticResource FontSizePageTitle}" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="{StaticResource FontSizePageTitle}" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="MonthlyIncome" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="badge-green">
<TextBlock Text="{Binding MonthlyIncomeChangeFormatted}" FontSize="{StaticResource FontSizeLabel}" FontWeight="SemiBold"
@@ -73,8 +80,14 @@
</Border>
<TextBlock Classes="label" Text="MONTHLY EXPENSES" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding MonthlyExpenses, StringFormat='$0.00'}" FontSize="{StaticResource FontSizePageTitle}" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock FontSize="{StaticResource FontSizePageTitle}" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="MonthlyExpenses" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="badge-red">
<TextBlock Text="{Binding MonthlyExpenseChangeFormatted}" FontSize="{StaticResource FontSizeLabel}" FontWeight="SemiBold"
@@ -156,10 +169,16 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="model:ColumnChartData">
<TextBlock Text="{Binding Values, Converter={StaticResource FirstValueConverter},StringFormat='$0'}"
HorizontalAlignment="Center" Margin="4 0" TextTrimming="CharacterEllipsis"
<TextBlock HorizontalAlignment="Center" Margin="4 0" TextTrimming="CharacterEllipsis"
Foreground="{Binding Fill, Converter={StaticResource SkPaintToBrushConverter}}"
FontWeight="SemiBold" />
FontWeight="SemiBold">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N0}">
<Binding Path="DataContext.PrimarySymbol" ElementName="DashboardRoot" />
<Binding Path="Values" Converter="{StaticResource FirstValueConverter}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -206,20 +225,20 @@
</StackPanel>
<Panel Grid.Column="1">
<StackPanel Orientation="Horizontal" IsVisible="{Binding !IsOverBudget}">
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="{StaticResource FontSizeMeta}"
<TextBlock Text="{Binding SpentFormatted}" FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text=" / " FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}"
<TextBlock Text="{Binding LimitFormatted}"
FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="{StaticResource FontSizeMeta}"
<TextBlock Text="{Binding SpentFormatted}" FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource AccentRed}" />
<TextBlock Text=" / " FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource AccentRed}" />
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}"
<TextBlock Text="{Binding LimitFormatted}"
FontSize="{StaticResource FontSizeMeta}"
Foreground="{DynamicResource AccentRed}" />
</StackPanel>
@@ -295,15 +314,10 @@
Classes="muted" />
</StackPanel>
</StackPanel>
<TextBlock Grid.Column="2" FontSize="{StaticResource FontSizeAmount}" FontWeight="SemiBold"
<TextBlock Grid.Column="2" Text="{Binding PrimaryAmountSignFormatted}"
FontSize="{StaticResource FontSizeAmount}" FontWeight="SemiBold"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource AmountSignConverter}">
<Binding Path="Amount" /> <Binding Path="Type" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
@@ -357,7 +371,7 @@
<TextBlock Text="{Binding Mask, Converter={StaticResource MaskToStringConverter}}"
FontSize="{StaticResource FontSizeLabel}" Classes="muted" />
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding CurrentBalance, StringFormat='$0'}"
<TextBlock Grid.Column="2" Text="{Binding CurrentBalanceFormatted}"
FontSize="{StaticResource FontSizeBody}" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" VerticalAlignment="Center" />
</Grid>
@@ -370,8 +384,14 @@
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Total Balance" FontSize="{StaticResource FontSizeBody}" FontWeight="SemiBold"
Foreground="{DynamicResource TextMuted}" />
<TextBlock Grid.Column="1" Text="{Binding TotalNetworth, StringFormat=$0}"
FontSize="{StaticResource FontSizeSectionHeading}" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}" />
<TextBlock Grid.Column="1" FontSize="{StaticResource FontSizeSectionHeading}" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimarySymbol" />
<Binding Path="TotalNetworth" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</StackPanel>
</Grid>

View File

@@ -1,94 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1400" d:DesignHeight="800"
x:Class="Clario.Views.LockscreenView">
<Panel>
<Border ZIndex="3" Background="#d11e1e2e" MinWidth="400" MinHeight="200" HorizontalAlignment="Left" VerticalAlignment="Top"
BorderBrush="{DynamicResource AccentBlue}" BorderThickness="2" Margin="4" CornerRadius="{StaticResource RadiusIcon}">
<Grid RowDefinitions="Auto,*" Margin="12" RowSpacing="12">
<TextBox Grid.Row="0" Height="48" Background="#d1181825" VerticalContentAlignment="Center" Watermark="Search..." Padding="20 0" FontSize="18"/>
<StackPanel Grid.Row="1" Spacing="8">
<Button Classes="nav" Classes.active="True" Content="📸 Camera" Margin="0" Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Stretch" FontSize="18"
FontWeight="SemiBold" Padding="8 12" />
<Button Classes="nav" Content="🎵 Music" Margin="0" Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Stretch" FontSize="18"
FontWeight="SemiBold" Padding="8 12" />
<Button Classes="nav" Content="🌐 Browser" Margin="0" Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Stretch"
FontSize="18" FontWeight="SemiBold" Padding="8 12" />
<Button Classes="nav" Content="🎮 Game" Margin="0" Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Stretch" FontSize="18"
FontWeight="SemiBold" Padding="8 12" />
<Button Classes="nav" Content="☁️ Weather" Margin="0" Foreground="{DynamicResource TextPrimary}" HorizontalAlignment="Stretch"
FontSize="18" FontWeight="SemiBold" Padding="8 12" />
</StackPanel>
</Grid>
</Border>
<!-- here I have 2 image layers cuz the blur on 2nd one pulls the edges a bit inwards so the 1st one is there to cover that area -->
<Image ZIndex="-1" Source="../Assets/diner-lonely-road.jpg" Stretch="UniformToFill" />
<Image ZIndex="-1" Source="../Assets/diner-lonely-road.jpg" Stretch="UniformToFill">
<Image.Effect>
<BlurEffect Radius="25"></BlurEffect>
</Image.Effect>
</Image>
<!-- 70% tint -->
<Border Background="#1E2330" Opacity="0.7" />
<!-- Clock - Date -->
<StackPanel HorizontalAlignment="Center" Margin="0 40">
<TextBlock Text="23:31" FontSize="132" HorizontalAlignment="Center" FontWeight="Bold" />
<TextBlock Text="Wednesday - 10 March 2026" FontSize="13" FontWeight="SemiBold" HorizontalAlignment="Center" />
</StackPanel>
<!-- Login -->
<Border VerticalAlignment="Bottom" Width="300" Margin=" 0 80">
<StackPanel>
<!-- pfp with border -->
<Border CornerRadius="25" BorderBrush="#1A2240" BorderThickness="2" Height="54" Width="54">
<Image Source="../Assets/eddypfp.png" Height="50">
<Image.Clip>
<RectangleGeometry Rect="0,0,50,50" RadiusX="25" RadiusY="25"></RectangleGeometry>
</Image.Clip>
</Image>
</Border>
<!-- spacer -->
<Border Height="8" />
<!-- Password textbox + confirm button -->
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
<TextBox Grid.Column="0" Watermark="Password" FontSize="13" Height="36" Padding="12,0" VerticalContentAlignment="Center" Opacity="1" />
<Button Grid.Column="1" Height="36" Width="36" CornerRadius="{DynamicResource RadiusControl}" Background="#7B9CFF"
BorderThickness="1" BorderBrush="{DynamicResource BorderAccent}" Padding="6">
<Svg Path="../Assets/Icons/chevron-right.svg" Css="path { stroke: #0D0F14; }"></Svg>
</Button>
</Grid>
</StackPanel>
</Border>
<!-- Media -->
<Border Width="300" Height="50" Background="#13161E" CornerRadius="{DynamicResource RadiusControl}" VerticalAlignment="Bottom"
Margin="0 20">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="10 0" ColumnSpacing="10">
<Svg Grid.Column="0" Path="../Assets/Icons/audio-lines.svg" Height="25" />
<Grid RowDefinitions="*,Auto" Grid.Column="1" VerticalAlignment="Center">
<TextBlock Grid.Row="0" Text="I just might - Bruno Mars" VerticalAlignment="Center" FontSize="13" />
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Bottom" ColumnSpacing="4">
<!-- current time -->
<TextBlock Grid.Column="0" Text="0:52" FontSize="10" Foreground="#7A8090" />
<ProgressBar Grid.Column="1" Maximum="100" Minimum="0" Value="25" Height="3" VerticalAlignment="Center" MinWidth="0" />
<!-- total time -->
<TextBlock Grid.Column="2" Text="3:32" FontSize="10" Foreground="#7A8090" />
</Grid>
</Grid>
<!-- play/pause button -->
<Button Grid.Column="2" Height="30" Width="30" Classes="nav" Padding="6">
<Svg Path="../Assets/Icons/pause.svg" Css="path, rect { stroke: #7A8090; }" />
</Button>
</Grid>
</Border>
</Panel>
</UserControl>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Clario.Views;
public partial class LockscreenView : UserControl
{
public LockscreenView()
{
InitializeComponent();
}
}

View File

@@ -12,91 +12,13 @@
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Background="#7393B3" IsVisible="False">
<!-- Bar -->
<Border Height="30" Background="{DynamicResource BorderAccent}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="2" Margin="4 6"
CornerRadius="15">
<!-- Definitions: Content width , Fill, Content width (Fill = full size - (ContentWidth1 + ContentWidth2) -->
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Menu Button -->
<Button Grid.Column="0" CornerRadius="15 5 5 15" Classes="nav" Padding="8 3">
<Button.Flyout>
<MenuFlyout>
<MenuItem Header="About this Device" Padding="12 4" />
<Separator Margin="4 0" />
<MenuItem Header="Settings" Padding="12 4" />
<Separator Margin="4 0" />
<MenuItem Header="Sleep" Padding="12 4" />
<MenuItem Header="Restart" Padding="12 4" />
<MenuItem Header="Shutdown" Padding="12 4" />
</MenuFlyout>
</Button.Flyout>
<Svg Path="../Assets/Icons/arch.svg" Width="18" Height="18" Margin="8 0" Css="{DynamicResource SvgFillBlue}" />
</Button>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<!-- Separator -->
<Border Width="1" Background="{DynamicResource BorderSubtle}" Margin="0 5" />
<!-- Window Title -->
<TextBlock Text="Expense Tracker - Dashboard" VerticalAlignment="Center" FontWeight="SemiBold"
Foreground="{DynamicResource TextSecondary}" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<Button CornerRadius="5" Classes="nav" Padding="8 3">
<!-- Media Button -->
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/audio-lines.svg" Css="{DynamicResource SvgSecondary}" Height="16" />
<TextBlock Text="Risk it all - Bruno Mars" VerticalAlignment="Center" FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource TextSecondary}" />
</StackPanel>
</Button>
<!-- Separator -->
<Border Width="1" Background="{DynamicResource BorderSubtle}" Margin="0 5" />
<!-- Control Panel -->
<Button CornerRadius="5" Classes="nav" Padding="8 3">
<StackPanel Orientation="Horizontal" Spacing="12">
<Svg Path="../Assets/Icons/battery-charging.svg" Height="20" Css="{DynamicResource SvgSecondary}" />
<Svg Path="../Assets/Icons/volume-2.svg" Height="18" Css="{DynamicResource SvgSecondary}" />
<Svg Path="../Assets/Icons/wifi.svg" Height="18" Css="{DynamicResource SvgSecondary}" />
</StackPanel>
</Button>
<!-- Separator -->
<Border Width="1" Background="{DynamicResource BorderSubtle}" Margin="0 5" />
<!-- Date Time Panel -->
<Button CornerRadius="5 15 15 5" Classes="nav" Padding="12 3">
<TextBlock Text="Tue Mar 10 - 10:23 AM" VerticalAlignment="Center" FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource TextSecondary}" />
</Button>
</StackPanel>
<!-- Workspaces ( Center of bar, Regardless of content ) -->
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Top" HorizontalAlignment="Center" Orientation="Horizontal" Margin="8 2"
Spacing="4">
<Border Width="35" Background="{DynamicResource AccentBlue}" Height="4" CornerRadius="2" /> <!-- Active Workspace -->
<Border Width="25" Background="{DynamicResource IconBgBlue}" Height="4" CornerRadius="2" />
<Border Width="25" Background="{DynamicResource IconBgBlue}" Height="4" CornerRadius="2" />
<Border Width="25" Background="{DynamicResource IconBgBlue}" Height="4" CornerRadius="2" />
<Border Width="25" Background="{DynamicResource IconBgBlue}" Height="4" CornerRadius="2" />
</StackPanel>
</Grid>
</Border>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="220,*">
<Grid ColumnDefinitions="220,*">
<!-- ───────────────────────────────────── SIDEBAR ───────────────────────────────────── -->
<Border Grid.Column="0" Background="{DynamicResource BgSidebar}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,1,0" Padding="16,28,16,24" IsEnabled="{Binding !IsTransactionFormVisible}">
<DockPanel>
<!-- Logo / App Name -->
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,24">
<!-- <Svg Path="../Assets/logo-textmark-no-color.svg" Css="{DynamicResource SvgSecondary}" HorizontalAlignment="Stretch" -->
<!-- Margin="4,12,24,0"/> -->
</StackPanel>
<!-- User Profile (bottom) -->
<Border DockPanel.Dock="Bottom" Background="{DynamicResource BgSurface}" CornerRadius="{StaticResource RadiusInset}"
@@ -115,7 +37,7 @@
</Border>
<Border Background="{DynamicResource BorderAccent}" CornerRadius="{StaticResource RadiusPill}" Width="34"
Height="34" IsVisible="{Binding !Profile.HasAvatar}">
<TextBlock Text="N" FontSize="{StaticResource FontSizeAmount}" FontWeight="Bold"
<TextBlock Text="{Binding Profile.DisplayName[0]}" FontSize="{StaticResource FontSizeAmount}" FontWeight="Bold"
Foreground="{DynamicResource AccentBlue}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
@@ -126,15 +48,6 @@
VerticalAlignment="Center"
Foreground="{DynamicResource TextSecondary}" />
</Grid>
<Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Focusable="False"
Command="{Binding SwitchThemeCommand}" IsVisible="True">
<Panel>
<Svg Path="../Assets/Icons/sun.svg" Css="{DynamicResource SvgDisabled}"
IsVisible="{Binding !IsDarkTheme}" />
<Svg Path="../Assets/Icons/moon.svg" Css="{DynamicResource SvgDisabled}"
IsVisible="{Binding IsDarkTheme}" />
</Panel>
</Button>
<!-- <Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Command="{Binding SignOutCommand}"> -->
<!-- <ToolTip.Tip> -->
<!-- signout -->
@@ -201,10 +114,12 @@
<views:BudgetFormView
DataContext="{Binding BudgetFormViewModel}"
IsVisible="{Binding DataContext.IsBudgetFormVisible, ElementName=MainControl}" />
<views:SetSavingsGoalDialogView
DataContext="{Binding SetSavingsGoalDialogViewModel}"
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
</Grid>
</Grid>
</Grid>
</UserControl>

View File

@@ -7,7 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
MinWidth="1000" MinHeight="600"
x:Class="Clario.Views.MainWindow"
Icon="../Assets/logo-no-bg.ico"
Icon="../Assets/AppIcons/logo-icon-primary-transparent.ico"
Title="Clario"
x:CompileBindings="False">
<ContentControl Content="{Binding}" />

View File

@@ -0,0 +1,145 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Clario.ViewModels"
xmlns:behaviors="clr-namespace:Clario.Behaviors"
mc:Ignorable="d"
x:Class="Clario.Views.SetSavingsGoalDialogView"
x:DataType="vm:SetSavingsGoalDialogViewModel">
<Design.DataContext>
<vm:SetSavingsGoalDialogViewModel/>
</Design.DataContext>
<Grid>
<!-- Dim overlay -->
<Border Background="#70000000"/>
<!-- Card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource AccentBlue}"
BorderThickness="1"
CornerRadius="18"
Padding="28"
Width="380"
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="{DynamicResource IconBgBlue}"
CornerRadius="14"
Width="54" Height="54"
HorizontalAlignment="Center"
Margin="0,0,0,16">
<Svg Path="../Assets/Icons/target.svg"
Width="22" Height="22"
Css="{DynamicResource SvgBlue}"/>
</Border>
<!-- Title -->
<TextBlock Text="Set Savings Goal"
FontSize="17"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,4"/>
<!-- Subtitle -->
<TextBlock Text="Set a monthly savings target to track your progress on the budget page."
FontSize="12"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,22"/>
<!-- Amount input -->
<TextBlock Text="MONTHLY GOAL" Classes="label" Margin="0,0,0,6"/>
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Margin="0,0,0,6">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0"
Text="$"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextMuted}"
VerticalAlignment="Center"
Margin="14,0,0,0"/>
<TextBox Grid.Column="1"
Classes="ghost"
Text="{Binding GoalInput, Mode=TwoWay}"
Watermark="0.00"
FontSize="15"
FontWeight="SemiBold"
Padding="8,12">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior/>
</Interaction.Behaviors>
</TextBox>
</Grid>
</Border>
<TextBlock Text="Set to 0 to remove the goal"
FontSize="10"
Foreground="{DynamicResource TextDisabled}"
Margin="0,0,0,20"/>
<!-- Error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,0,0,16"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="13" Height="13"
Css="{DynamicResource SvgRed}"/>
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="13"
Content="Cancel"
Command="{Binding CancelCommand}"/>
<Button Classes="accented"
Margin="6,0,0,0"
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding IsValid}"
Command="{Binding SaveCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/check.svg"
Width="13" Height="13"
Css="{DynamicResource SvgBase}"/>
<TextBlock Text="Save Goal"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
</UniformGrid>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Clario.Views;
public partial class SetSavingsGoalDialogView : UserControl
{
public SetSavingsGoalDialogView()
{
InitializeComponent();
}
}

View File

@@ -5,7 +5,8 @@
xmlns:vm="clr-namespace:Clario.ViewModels"
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
x:Class="Clario.Views.SettingsView"
x:DataType="vm:SettingsViewModel">
x:DataType="vm:SettingsViewModel"
x:Name="SettingsRoot">
<Design.DataContext>
<vm:SettingsViewModel />
</Design.DataContext>
@@ -173,17 +174,8 @@
VerticalContentAlignment="Center" />
</StackPanel>
<!-- Currency + Theme -->
<Grid ColumnDefinitions="*,16,*">
<StackPanel Grid.Column="0" Spacing="6">
<TextBlock Text="CURRENCY" Classes="label" />
<ComboBox ItemsSource="{Binding Currencies}"
SelectedItem="{Binding SelectedCurrency, Mode=TwoWay}"
HorizontalAlignment="Stretch"
Padding="10,8"
FontSize="13" />
</StackPanel>
<StackPanel Grid.Column="2" Spacing="6">
<!-- Theme -->
<StackPanel Spacing="6">
<TextBlock Text="THEME" Classes="label" />
<ComboBox ItemsSource="{Binding ThemeLabels}"
SelectedIndex="{Binding SelectedThemeIndex, Mode=TwoWay}"
@@ -191,7 +183,6 @@
Padding="10,8"
FontSize="13" />
</StackPanel>
</Grid>
<!-- Language -->
<StackPanel Spacing="6">
@@ -219,7 +210,8 @@
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<Panel>
<TextBlock Text="{Binding IsSaving, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Saving...|Save Changes'}"
<TextBlock
Text="{Binding IsSaving, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Saving...|Save Changes'}"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
@@ -506,21 +498,12 @@
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
<Button Grid.Column="1"
Background="#2A0D0D"
BorderBrush="#3A1515"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Classes="danger"
Padding="16,9"
Command="{Binding SignOutCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/log-out.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="Sign Out"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FF5E5E"
VerticalAlignment="Center" />
<Svg Path="../Assets/Icons/log-out.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
<TextBlock Text="Sign Out" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>

View File

@@ -127,7 +127,7 @@
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0"
Text="$"
Text="{Binding CurrencySymbol}"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource TextMuted}"
@@ -245,6 +245,67 @@
</Grid>
<!-- ── Exchange Rate (shown for foreign-currency accounts) ── -->
<Border IsVisible="{Binding ShowExchangeRateField}"
Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="14,10"
Margin="0,0,0,16">
<StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="EXCHANGE RATE"
Classes="label"
VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
IsVisible="{Binding IsFetchingRate}"
VerticalAlignment="Center">
<Border Width="8" Height="8"
CornerRadius="4"
Background="{DynamicResource AccentBlue}"
Opacity="0.7" />
<TextBlock Text="Fetching rate..."
FontSize="11"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Grid>
<TextBlock Text="{Binding ExchangeRateLabel}"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
<!-- Loading skeleton -->
<Border IsVisible="{Binding IsFetchingRate}"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Height="36"
Opacity="0.5" />
<!-- Actual input -->
<Border IsVisible="{Binding !IsFetchingRate}"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="12,0">
<TextBox Classes="ghost"
Text="{Binding ExchangeRate, Mode=TwoWay}"
Watermark="0.00000"
FontSize="13"
Height="36"
Padding="0"
VerticalContentAlignment="Center">
<Interaction.Behaviors>
<behaviors:NumericInputBehavior />
</Interaction.Behaviors>
</TextBox>
</Border>
</StackPanel>
</Border>
<!-- ── Date ────────────────────────── -->
<TextBlock Text="DATE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
@@ -284,6 +345,46 @@
VerticalContentAlignment="Center"
Margin="0,0,0,8" />
<!-- ── Budget approaching warning ─────── -->
<Border Background="{DynamicResource BadgeBgYellow}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,8,0,8"
IsVisible="{Binding HasBudgetApproachingWarning}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/triangle-alert.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }" />
<TextBlock Text="{Binding BudgetWarningMessage}"
FontSize="12"
Foreground="{DynamicResource AccentYellow}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- ── Budget over-limit warning ──────── -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,8"
Margin="0,8,0,8"
IsVisible="{Binding BudgetWarningIsOverBudget}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding BudgetWarningMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- ── Validation error ─────────────── -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
@@ -304,25 +405,16 @@
</Border>
<!-- ── Delete button (edit mode only) ── -->
<Button HorizontalAlignment="Stretch"
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#1A0808"
BorderBrush="#3A1515"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0,10"
Margin="0,0,0,10"
IsVisible="{Binding IsEditMode}"
Command="{Binding RequestDeleteCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/trash-2.svg"
Width="13" Height="13"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="Delete Transaction"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FF5E5E"
VerticalAlignment="Center" />
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}"/>
<TextBlock Text="Delete Transaction" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
</Button>

Some files were not shown because too many files have changed in this diff Show More