stuff
Some checks failed
Build Linux / build (push) Failing after 23s

This commit is contained in:
2026-04-09 23:53:55 +03:00
parent 61ff949c19
commit 90b2abd587
80 changed files with 3539 additions and 856 deletions

View File

@@ -3,7 +3,11 @@
"allow": [
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")",
"Bash(dotnet build:*)"
"Bash(dotnet build:*)",
"WebFetch(domain:git.nouredeen.dev)",
"WebFetch(domain:supabase.com)",
"Bash(grep:*)",
"Bash(cmd:*)"
]
},
"spinnerTipsEnabled": true

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ obj/
publish/
*.tar.gz
Clario/devsettings.json
.env

View File

@@ -1,18 +1,47 @@
using Android.App;
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Avalonia;
using Avalonia.Android;
using Clario;
namespace Clario.Android;
[Activity(
Label = "Clario.Android",
Label = "Clario",
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "clario",
DataHost = "auth")]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override void OnCreate(Bundle? savedInstanceState)
{
// Capture deep link before Avalonia initializes
var uri = Intent?.DataString;
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
App.PendingDeepLink = uri;
base.OnCreate(savedInstanceState);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
// Called when app is already running (SingleTop) and link is opened again
var uri = intent?.DataString;
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
_ = App.HandleDeepLink(uri);
}
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)

View File

@@ -2,7 +2,7 @@
"GeneralSettings": {
"NetProjectPath": "Clario.Desktop.csproj",
"ApplicationName": "Clario",
"Version": "0.4.0",
"Version": "0.6.0",
"PackageName": {
"$type": "msbuild",
"property": "AssemblyName"
@@ -13,11 +13,11 @@
}
},
"LinuxSettings": {
"AppIcon": "../Clario/Assets/Logo.png",
"AppIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
"CreateBinSymlink": "True"
},
"Win32Settings": {
"InstallerIcon": "../Clario/Assets/Clario-Logo.svg",
"InstallerIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
"Company": "Clario",
"IncludeUninstaller": "True"
},

View File

@@ -1,19 +1,25 @@
using System;
using System;
using System.Linq;
using Avalonia;
using Clario;
namespace Clario.Desktop;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
// Capture deep link passed as command-line arg by Windows protocol handler
var deepLink = args.FirstOrDefault(a =>
a.StartsWith("clario://", StringComparison.OrdinalIgnoreCase));
if (deepLink != null)
App.PendingDeepLink = deepLink;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Register clario:// URL scheme on Windows (idempotent)
RegisterUrlScheme();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
@@ -22,4 +28,23 @@ sealed class Program
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
private static void RegisterUrlScheme()
{
if (!OperatingSystem.IsWindows()) return;
try
{
var exe = Environment.ProcessPath
?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
if (exe is null) return;
using var key = Microsoft.Win32.Registry.CurrentUser
.CreateSubKey(@"SOFTWARE\Classes\clario");
key.SetValue("", "URL:Clario Protocol");
key.SetValue("URL Protocol", "");
using var cmd = key.CreateSubKey(@"shell\open\command");
cmd.SetValue("", $"\"{exe}\" \"%1\"");
}
catch { /* ignore — no registry write access in sandboxed environments */ }
}
}

View File

@@ -36,5 +36,15 @@
<StyleInclude Source="../Theme/AppTheme.axaml" />
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="avares://FluentAvalonia.ProgressRing/Styling/Controls/ProgressRing.axaml" />
<!-- Must come after ColorPicker Fluent.xaml to override Width="64" setter -->
<Styles>
<Style Selector="ColorPicker">
<Setter Property="Width" Value="NaN" />
</Style>
<Style Selector="ColorPicker /template/ DropDownButton">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</Styles>
</Application.Styles>
</Application>

View File

@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
@@ -17,6 +18,47 @@ public partial class App : Application
{
public static bool IsMobile { get; private set; }
/// <summary>Set before OnFrameworkInitializationCompleted runs (from Program.cs or MainActivity).</summary>
public static string? PendingDeepLink { get; set; }
/// <summary>Called from MainActivity.OnNewIntent when app is already running.</summary>
public static async Task HandleDeepLink(string deepLink)
{
var (accessToken, refreshToken, type) = ParseDeepLinkFragment(deepLink);
if (type != "recovery" || accessToken is null) return;
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
var vm = new ResetPasswordViewModel();
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow!.DataContext = vm;
else if (Current?.ApplicationLifetime is ISingleViewApplicationLifetime sv)
sv.MainView!.DataContext = vm;
});
}
private static (string? accessToken, string? refreshToken, string? type) ParseDeepLinkFragment(string url)
{
var hash = url.IndexOf('#');
if (hash < 0) return default;
string? at = null, rt = null, type = null;
foreach (var part in url[(hash + 1)..].Split('&'))
{
var eq = part.IndexOf('=');
if (eq < 0) continue;
var val = Uri.UnescapeDataString(part[(eq + 1)..]);
switch (part[..eq])
{
case "access_token": at = val; break;
case "refresh_token": rt = val; break;
case "type": type = val; break;
}
}
return (at, rt, type);
}
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -73,19 +115,32 @@ public partial class App : Application
ThemeService.SwitchToTheme(profile.Theme);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
// Check for deep link from password reset email
ViewModelBase targetViewModel;
if (PendingDeepLink is { } deepLink && deepLink.Contains("type=recovery"))
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
var (accessToken, refreshToken, _) = ParseDeepLinkFragment(deepLink);
if (accessToken is not null)
{
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
}
PendingDeepLink = null;
targetViewModel = new ResetPasswordViewModel();
}
else
{
targetViewModel = user is not null ? new MainViewModel() : new AuthViewModel();
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow!.DataContext = targetViewModel;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
singleViewPlatform.MainView!.DataContext = targetViewModel;
}
}

View File

@@ -50,6 +50,9 @@
<Compile Update="Views\AnalyticsView.axaml.cs">
<DependentUpon>AnalyticsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\DashboardSkeletonView.axaml.cs">
<DependentUpon>DashboardSkeletonView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
@@ -57,4 +60,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
<AndroidKeyStore>true</AndroidKeyStore>
<AndroidSigningKeyStore>clario.keystore</AndroidSigningKeyStore>
<AndroidSigningKeyAlias>clario</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningKeyPass>
<AndroidSigningStorePass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningStorePass>
</PropertyGroup>
</Project>

View File

@@ -9,7 +9,7 @@
the internal template parts at ControlTheme priority, making
external /template/ style selectors unreliable for CalendarButton.
Replacing the entire ControlTheme is the only reliable approach.
-->
-->
<ControlTheme x:Key="{x:Type CalendarButton}" TargetType="CalendarButton">
<Setter Property="MinWidth" Value="40" />
<Setter Property="MinHeight" Value="40" />
@@ -69,7 +69,7 @@
</Styles.Resources>
<!-- ============================================================ -->
<!-- DateRangePicker control template -->
<!-- DateRangePicker control template -->
<!-- ============================================================ -->
<Style Selector="local|DateRangePicker">
@@ -86,6 +86,7 @@
<Grid>
<Button x:Name="PART_Button"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
BorderBrush="{TemplateBinding BorderBrush}"
@@ -147,7 +148,7 @@
</Style>
<!-- ============================================================ -->
<!-- CalendarItem: nav header buttons (prev / title / next) -->
<!-- CalendarItem: nav header buttons (prev / title / next) -->
<!-- ============================================================ -->
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton">

View File

@@ -25,7 +25,6 @@ public class DateRangePicker : TemplatedControl
set => SetValue(SelectionModeProperty, value);
}
// FIX: Use DirectProperty to avoid shared-instance default and get proper TwoWay support
private IList<DateTime> _selectedDates = new List<DateTime>();
public static readonly DirectProperty<DateRangePicker, IList<DateTime>> SelectedDatesProperty =
@@ -41,7 +40,6 @@ public class DateRangePicker : TemplatedControl
set => SetAndRaise(SelectedDatesProperty, ref _selectedDates, value);
}
// FIX: Add defaultBindingMode: TwoWay so changes propagate back to the ViewModel
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedDate),
@@ -116,7 +114,6 @@ public class DateRangePicker : TemplatedControl
if (_isSyncing) return;
if (_popup is null || !_popup.IsOpen) return;
// FIX: Ignore clicks on the nav buttons/header — only react to day cell clicks
if (e.Source is not Control source) return;
if (source.TemplatedParent is CalendarDayButton == false &&
source.FindAncestorOfType<CalendarDayButton>() is null)

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Clario.Models;
using Clario.Models.GeneralModels;
using Clario.Services;
@@ -15,6 +16,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Clario.Messages;
using CommunityToolkit.Mvvm.Messaging;
using Supabase.Postgrest;
using Supabase.Realtime.PostgresChanges;
using Constants = Supabase.Realtime.Constants;
using FileOptions = Supabase.Storage.FileOptions;
namespace Clario.Data;
@@ -174,12 +177,14 @@ public partial class GeneralDataRepo : ObservableObject
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
if (inResult.Models.Count >= 1)
{
var enriched = LinkTransactionCategories(inResult.Models[0]);
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
// Re-enrich both so AccountDisplayText can reference the counterpart (from/to)
LinkTransactionAccounts();
}
@@ -215,6 +220,7 @@ public partial class GeneralDataRepo : ObservableObject
Transactions[index] = enriched;
}
}
LinkTransactionAccounts();
}
catch (Exception e)
@@ -306,7 +312,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.OrderBy(x=>x.IsPrimary).ThenBy(x=>x.CreatedAt).ToList();
return accounts.Models.OrderBy(x => x.IsPrimary).ThenBy(x => x.CreatedAt).ToList();
}
public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
@@ -719,4 +725,186 @@ public partial class GeneralDataRepo : ObservableObject
if (avatarUrl.StartsWith("http")) return avatarUrl;
return $"{PublicBaseUrl}/{avatarUrl}";
}
public void StartRealtimeSync()
{
if (SupabaseService.Client.Auth.CurrentUser?.Id is null) return;
DebugLogger.Log("[Realtime] StartRealtimeSync: registering listeners");
// Transactions
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedTransaction = c.Model<Transaction>();
if (insertedTransaction is null) { DebugLogger.Log("[Realtime] Transaction INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction INSERT: {insertedTransaction.Id} ({insertedTransaction.Description})");
Dispatcher.UIThread.Post(() =>
{
if (Transactions.Any(x => x.Id == insertedTransaction.Id)) { DebugLogger.Log($"[Realtime] Transaction INSERT: skipped duplicate {insertedTransaction.Id}"); return; }
LinkTransactionCategories(insertedTransaction);
LinkTransactionAccounts(insertedTransaction);
Transactions.Add(insertedTransaction);
DebugLogger.Log($"[Realtime] Transaction INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedTransaction = c.Model<Transaction>();
if (updatedTransaction is null) { DebugLogger.Log("[Realtime] Transaction UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction UPDATE: {updatedTransaction.Id} ({updatedTransaction.Description})");
Dispatcher.UIThread.Post(() =>
{
var idx = Transactions.ToList().FindIndex(x => x.Id == updatedTransaction.Id);
if (idx == -1) { DebugLogger.Log($"[Realtime] Transaction UPDATE: id {updatedTransaction.Id} not found in collection"); return; }
LinkTransactionCategories(updatedTransaction);
LinkTransactionAccounts(updatedTransaction);
Transactions[idx] = updatedTransaction;
DebugLogger.Log($"[Realtime] Transaction UPDATE: replaced at index {idx}");
});
});
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedTransaction = c.OldModel<Transaction>();
if (deletedTransaction is null) { DebugLogger.Log("[Realtime] Transaction DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction DELETE: {deletedTransaction.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Transactions.FirstOrDefault(x => x.Id == deletedTransaction.Id);
if (item is not null) { Transactions.Remove(item); DebugLogger.Log($"[Realtime] Transaction DELETE: removed {deletedTransaction.Id}"); }
else DebugLogger.Log($"[Realtime] Transaction DELETE: id {deletedTransaction.Id} not found (already removed locally)");
});
});
// Accounts
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedAccount = c.Model<Account>();
if (insertedAccount is null) { DebugLogger.Log("[Realtime] Account INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Account INSERT: {insertedAccount.Id} ({insertedAccount.Name})");
Dispatcher.UIThread.Post(() =>
{
if (Accounts.Any(x => x.Id == insertedAccount.Id)) { DebugLogger.Log($"[Realtime] Account INSERT: skipped duplicate {insertedAccount.Id}"); return; }
Accounts.Add(insertedAccount);
DebugLogger.Log($"[Realtime] Account INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedAccount = c.Model<Account>();
if (updatedAccount is null) { DebugLogger.Log("[Realtime] Account UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Account UPDATE: {updatedAccount.Id} ({updatedAccount.Name})");
Dispatcher.UIThread.Post(() =>
{
var idx = Accounts.ToList().FindIndex(x => x.Id == updatedAccount.Id);
if (idx != -1) { Accounts[idx] = updatedAccount; DebugLogger.Log($"[Realtime] Account UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Account UPDATE: id {updatedAccount.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedAccount = c.OldModel<Account>();
if (deletedAccount is null) { DebugLogger.Log("[Realtime] Account DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Account DELETE: {deletedAccount.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Accounts.FirstOrDefault(x => x.Id == deletedAccount.Id);
if (item is not null) { Accounts.Remove(item); DebugLogger.Log($"[Realtime] Account DELETE: removed {deletedAccount.Id}"); }
else DebugLogger.Log($"[Realtime] Account DELETE: id {deletedAccount.Id} not found (already removed locally)");
});
});
// Budgets
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedBudget = c.Model<Budget>();
if (insertedBudget is null) { DebugLogger.Log("[Realtime] Budget INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Budget INSERT: {insertedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
if (Budgets.Any(x => x.Id == insertedBudget.Id)) { DebugLogger.Log($"[Realtime] Budget INSERT: skipped duplicate {insertedBudget.Id}"); return; }
Budgets.Add(insertedBudget);
DebugLogger.Log($"[Realtime] Budget INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedBudget = c.Model<Budget>();
if (updatedBudget is null) { DebugLogger.Log("[Realtime] Budget UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Budget UPDATE: {updatedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
var idx = Budgets.ToList().FindIndex(x => x.Id == updatedBudget.Id);
if (idx != -1) { Budgets[idx] = updatedBudget; DebugLogger.Log($"[Realtime] Budget UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Budget UPDATE: id {updatedBudget.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedBudget = c.OldModel<Budget>();
if (deletedBudget is null) { DebugLogger.Log("[Realtime] Budget DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Budget DELETE: {deletedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Budgets.FirstOrDefault(x => x.Id == deletedBudget.Id);
if (item is not null) { Budgets.Remove(item); DebugLogger.Log($"[Realtime] Budget DELETE: removed {deletedBudget.Id}"); }
else DebugLogger.Log($"[Realtime] Budget DELETE: id {deletedBudget.Id} not found (already removed locally)");
});
});
// Categories
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedCategory = c.Model<Category>();
if (insertedCategory is null) { DebugLogger.Log("[Realtime] Category INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Category INSERT: {insertedCategory.Id} ({insertedCategory.Name})");
Dispatcher.UIThread.Post(() =>
{
if (Categories.Any(x => x.Id == insertedCategory.Id)) { DebugLogger.Log($"[Realtime] Category INSERT: skipped duplicate {insertedCategory.Id}"); return; }
Categories.Add(insertedCategory);
DebugLogger.Log($"[Realtime] Category INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var UpdatedCategory = c.Model<Category>();
if (UpdatedCategory is null) { DebugLogger.Log("[Realtime] Category UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Category UPDATE: {UpdatedCategory.Id} ({UpdatedCategory.Name})");
Dispatcher.UIThread.Post(() =>
{
var idx = Categories.ToList().FindIndex(x => x.Id == UpdatedCategory.Id);
if (idx != -1) { Categories[idx] = UpdatedCategory; DebugLogger.Log($"[Realtime] Category UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Category UPDATE: id {UpdatedCategory.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedCategory = c.OldModel<Category>();
if (deletedCategory is null) { DebugLogger.Log("[Realtime] Category DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Category DELETE: {deletedCategory.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Categories.FirstOrDefault(x => x.Id == deletedCategory.Id);
if (item is not null) { Categories.Remove(item); DebugLogger.Log($"[Realtime] Category DELETE: removed {deletedCategory.Id}"); }
else DebugLogger.Log($"[Realtime] Category DELETE: id {deletedCategory.Id} not found (already removed locally)");
});
});
// Profile
_ = SupabaseService.Client.From<Profile>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedProfile = c.Model<Profile>();
if (updatedProfile is null) { DebugLogger.Log("[Realtime] Profile UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Profile UPDATE: {updatedProfile.Id} ({updatedProfile.DisplayName})");
Dispatcher.UIThread.Post(() => Profile = updatedProfile);
});
DebugLogger.Log("[Realtime] all listeners registered");
}
}

View File

@@ -17,7 +17,7 @@
<Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ──────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0">
@@ -57,7 +57,7 @@
<Border Grid.Column="2" Width="36" />
</Grid>
<!-- ── Scrollable form ───────────────────── -->
<!-- Scrollable form -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
@@ -366,7 +366,7 @@
</StackPanel>
</ScrollViewer>
<!-- ── Bottom action bar ─────────────────── -->
<!-- Bottom action bar -->
<Border Grid.Row="2"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"

View File

@@ -17,7 +17,7 @@
<!-- Root grid — content + overlay stacked -->
<Grid>
<!-- ── Main content ───────────────────────── -->
<!-- Main content -->
<Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}">
@@ -212,7 +212,7 @@
</ScrollViewer>
</Grid>
<!-- ── Bottom sheet overlay ───────────────── -->
<!-- Bottom sheet overlay -->
<Grid IsVisible="False"
x:Name="OverlayGrid">
@@ -526,7 +526,7 @@
</Grid>
<!-- ── Dialog overlays ───────────────────── -->
<!-- Dialog overlays -->
<mobileViews:DeleteAccountDialogViewMobile IsVisible="{Binding DataContext.IsDeleteDialogVisible, ElementName=AccountsPage}"
DataContext="{Binding DeleteDialog}" />
<mobileViews:ArchiveAccountDialogViewMobile IsVisible="{Binding IsArchiveDialogVisible}" />

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
@@ -7,6 +7,7 @@ using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using Clario.Models;
using Clario.ViewModels;
namespace Clario.MobileViews;
@@ -28,6 +29,28 @@ public partial class AccountsViewMobile : UserControl
{
if (e.Source is Button { DataContext: Account }) await ShowSheet();
}, handledEventsToo: false);
DataContextChanged += (_, _) =>
{
if (DataContext is AccountsViewModel vm)
{
vm.TryCloseSheet = () =>
{
if (!_sheetVisible) return false;
_ = HideSheet();
return true;
};
vm.PropertyChanged += async (_, args) =>
{
if (args.PropertyName == nameof(AccountsViewModel.ShouldCloseSheet) && vm.ShouldCloseSheet)
{
await HideSheet();
vm.ShouldCloseSheet = false;
}
};
}
};
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
@@ -84,6 +107,7 @@ public partial class AccountsViewMobile : UserControl
public async Task HideSheet()
{
if (!_sheetVisible) return;
_sheetVisible = false;
var sheetAnim = new Animation
{
@@ -110,7 +134,6 @@ public partial class AccountsViewMobile : UserControl
await Task.WhenAll(sheetAnim.RunAsync(BottomSheet), dimAnim.RunAsync(DimOverlay));
_sheetVisible = false;
OverlayGrid.IsVisible = false;
SheetTranslate.Y = 0;
DimOverlay.Opacity = 1;

View File

@@ -16,7 +16,7 @@
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="24,48,24,48" Spacing="0">
<!-- ── Logo ──────────────────────────────── -->
<!-- Logo -->
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
<Border CornerRadius="16"
Height="80"
@@ -31,13 +31,14 @@
HorizontalAlignment="Center" />
</StackPanel>
<!-- ── Tab switcher ───────────────────────── -->
<!-- Tab switcher -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,28">
Margin="0,0,0,28"
IsVisible="{Binding ShowTabs}">
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0"
HorizontalAlignment="Stretch"
@@ -69,7 +70,7 @@
</Grid>
</Border>
<!-- ── Sign In panel ──────────────────────── -->
<!-- Sign In panel -->
<StackPanel Spacing="0" IsVisible="{Binding isSignin}">
<!-- Email -->
@@ -150,6 +151,20 @@
</Grid>
</Border>
<!-- Forgot password -->
<Button Background="Transparent"
BorderThickness="0"
Padding="0"
Cursor="Hand"
HorizontalAlignment="Right"
Margin="0,8,0,24"
Command="{Binding SetOperationCommand}"
CommandParameter="forgotPassword">
<TextBlock Text="Forgot password?"
FontSize="13"
Foreground="{DynamicResource AccentBlue}" />
</Button>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
@@ -191,7 +206,7 @@
</StackPanel>
<!-- ── Create Account panel ───────────────── -->
<!-- Create Account panel -->
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
<!-- First / Last name -->
@@ -374,7 +389,127 @@
</StackPanel>
<!-- ── Footer ──────────────────────────────── -->
<!-- FORGOT PASSWORD PANEL -->
<StackPanel Spacing="0" IsVisible="{Binding isForgotPassword}">
<!-- Back button -->
<Button Background="Transparent"
BorderThickness="0"
Padding="0"
Cursor="Hand"
HorizontalAlignment="Left"
Margin="0,0,0,24"
Command="{Binding SetOperationCommand}"
CommandParameter="login">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-left.svg"
Width="15" Height="15"
Css="{DynamicResource SvgMuted}" />
<TextBlock Text="Back to Sign In"
FontSize="13"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Button>
<TextBlock Text="Reset your password"
FontSize="20" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
Margin="0,0,0,8" />
<TextBlock Text="Enter your email and we'll send you a reset link."
FontSize="13"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
Margin="0,0,0,24" />
<!-- Success state -->
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
CornerRadius="12"
Padding="14,12"
Margin="0,0,0,16"
IsVisible="{Binding ResetEmailSent}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-check.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
<TextBlock Text="Check your email for a reset link."
FontSize="13"
Foreground="{DynamicResource AccentGreen}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Email -->
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6"
IsVisible="{Binding !ResetEmailSent}" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16"
IsVisible="{Binding !ResetEmailSent}">
<Grid ColumnDefinitions="Auto,*">
<Svg Grid.Column="0"
Path="../Assets/Icons/mail.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Watermark="you@example.com"
Text="{Binding Email}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="12"
Padding="14,10"
Margin="0,0,0,20"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="13"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Send Reset Link button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,14"
Margin="0,0,0,24"
IsVisible="{Binding !ResetEmailSent}"
Command="{Binding SendResetLinkCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/mail.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Send Reset Link"
FontSize="15" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- Footer -->
<Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely."
FontSize="12"

View File

@@ -15,7 +15,7 @@
<Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ──────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0">
@@ -71,7 +71,7 @@
<Border Grid.Column="2" Width="36" IsVisible="{Binding !IsEditMode}" />
</Grid>
<!-- ── Scrollable form ───────────────────── -->
<!-- Scrollable form -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
@@ -292,7 +292,7 @@
</StackPanel>
</ScrollViewer>
<!-- ── Bottom action bar ─────────────────── -->
<!-- Bottom action bar -->
<Border Grid.Row="2"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -330,7 +330,7 @@
</Grid>
</Border>
<!-- ── Delete confirm sub-modal ─────────── -->
<!-- Delete confirm sub-modal -->
<Grid Grid.Row="0" Grid.RowSpan="3" IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"

View File

@@ -17,7 +17,7 @@
<Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,12">
@@ -74,13 +74,13 @@
</Grid>
<!-- ── Scrollable content ────────────────── -->
<!-- Scrollable content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="14">
<!-- ── Period overview strip ─────────── -->
<!-- Period overview strip -->
<Grid ColumnDefinitions="*,*,*">
<!-- Budgeted -->
@@ -154,7 +154,7 @@
</Grid>
<!-- ── Overall progress bar ──────────── -->
<!-- Overall progress bar -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -195,7 +195,7 @@
</StackPanel>
</Border>
<!-- ── Budget cards list ─────────────── -->
<!-- Budget cards list -->
<ItemsControl ItemsSource="{Binding VisibleBudgets}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -322,7 +322,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ── Spending breakdown chart ──────── -->
<!-- Spending breakdown chart -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -364,7 +364,7 @@
</StackPanel>
</Border>
<!-- ── Period progress ───────────────── -->
<!-- Period progress -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -417,7 +417,7 @@
</StackPanel>
</Border>
<!-- ── Savings goal ──────────────────── -->
<!-- Savings goal -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"

View File

@@ -0,0 +1,168 @@
<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:model="clr-namespace:Clario.Models"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.CategoriesViewMobile"
x:DataType="vm:CategoriesViewModel"
x:Name="CategoriesPage"
Classes="mobile">
<Design.DataContext>
<vm:CategoriesViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}">
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,12">
<StackPanel Grid.Column="0">
<TextBlock Text="Categories"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
</StackPanel>
<Button Grid.Column="1"
Classes="accented"
Padding="12,8"
Command="{Binding AddCategoryCommand}">
<Svg Path="../Assets/Icons/plus.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
</Button>
</Grid>
<!-- Content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="20">
<!-- Expense section -->
<StackPanel IsVisible="{Binding HasExpenseCategories}" Spacing="8">
<TextBlock Text="EXPENSES" Classes="label" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1,0.25"
CornerRadius="14"
ClipToBounds="True">
<ItemsControl ItemsSource="{Binding ExpenseCategories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Category">
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0.75">
<Button Classes="nav"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="{DynamicResource BgSurface}"
Padding="14,12"
Command="{Binding DataContext.EditCategoryCommand, ElementName=CategoriesPage}"
CommandParameter="{Binding .}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
CornerRadius="10"
Width="36" Height="36"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="17" Height="17"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<TextBlock Grid.Column="1"
Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Svg Grid.Column="2"
Path="../Assets/Icons/chevron-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center" />
</Grid>
</Button>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</StackPanel>
<!-- Income section -->
<StackPanel IsVisible="{Binding HasIncomeCategories}" Spacing="8">
<TextBlock Text="INCOME" Classes="label" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1,0.25"
CornerRadius="14"
ClipToBounds="True">
<ItemsControl ItemsSource="{Binding IncomeCategories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="model:Category">
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0.75">
<Button Classes="nav"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="{DynamicResource BgSurface}"
Padding="14,12"
Command="{Binding DataContext.EditCategoryCommand, ElementName=CategoriesPage}"
CommandParameter="{Binding .}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
CornerRadius="10"
Width="36" Height="36"
Margin="0,0,12,0">
<Border.Background>
<SolidColorBrush
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
Opacity="0.15" />
</Border.Background>
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
Width="17" Height="17"
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
</Border>
<TextBlock Grid.Column="1"
Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Svg Grid.Column="2"
Path="../Assets/Icons/chevron-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center" />
</Grid>
</Button>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,304 @@
<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="400" d:DesignHeight="800"
x:CompileBindings="False"
x:Class="Clario.MobileViews.DashboardSkeletonViewMobile"
Classes="mobile">
<UserControl.Styles>
<Style Selector="Border.skeleton">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Style.Animations>
<Animation Duration="0:0:0.85" IterationCount="INFINITE" PlaybackDirection="Alternate" FillMode="Both">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.35" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
<!-- Top Bar -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
<StackPanel Grid.Column="0" Spacing="7">
<Border Classes="skeleton" Height="22" Width="185" CornerRadius="7" />
<Border Classes="skeleton" Height="12" Width="115" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="36" Height="36"
CornerRadius="10" Margin="0,0,8,0" VerticalAlignment="Center" />
<Border Grid.Column="2" Classes="skeleton" Width="38" Height="36"
CornerRadius="10" VerticalAlignment="Center" />
</Grid>
<!-- Scrollable Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="14">
<!-- KPI Cards 2-col grid -->
<Grid ColumnDefinitions="*,*">
<!-- Income card -->
<Border Grid.Column="0"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,12"
Margin="0,0,6,0">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="10" Width="52" VerticalAlignment="Center" />
</StackPanel>
<Border Classes="skeleton" Height="22" Width="90" CornerRadius="7" />
<Border Classes="skeleton" Height="20" Width="65" CornerRadius="20" />
</StackPanel>
</Border>
<!-- Expenses card -->
<Border Grid.Column="1"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,12"
Margin="6,0,0,0">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="10" Width="62" VerticalAlignment="Center" />
</StackPanel>
<Border Classes="skeleton" Height="22" Width="82" CornerRadius="7" />
<Border Classes="skeleton" Height="20" Width="60" CornerRadius="20" />
</StackPanel>
</Border>
</Grid>
<!-- Savings Rate full-width -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,12">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="10" Width="88" VerticalAlignment="Center" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="7" VerticalAlignment="Center"
HorizontalAlignment="Stretch" CornerRadius="4" />
<Border Grid.Column="1" Classes="skeleton" Width="36" Height="18"
Margin="12,0,0,0" CornerRadius="6" />
</Grid>
</StackPanel>
</Border>
<!-- Spending by Category chart card -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="6">
<Border Classes="skeleton" Height="14" Width="160" />
<Border Classes="skeleton" Height="11" Width="105" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="85" Height="28"
CornerRadius="{StaticResource RadiusIcon}" VerticalAlignment="Center" />
</Grid>
<!-- Chart placeholder -->
<Border Classes="skeleton" Height="180" HorizontalAlignment="Stretch" CornerRadius="8" />
<!-- Category labels -->
<Grid ColumnDefinitions="*,*,*,*">
<Border Grid.Column="0" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="1" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="2" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="3" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
</Grid>
<!-- Amount labels -->
<Grid ColumnDefinitions="*,*,*,*">
<Border Grid.Column="0" Classes="skeleton" Height="11" Width="30" HorizontalAlignment="Center" />
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="34" HorizontalAlignment="Center" />
<Border Grid.Column="2" Classes="skeleton" Height="11" Width="28" HorizontalAlignment="Center" />
<Border Grid.Column="3" Classes="skeleton" Height="11" Width="32" HorizontalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<!-- Recent Transactions card -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="5">
<Border Classes="skeleton" Height="14" Width="150" />
<Border Classes="skeleton" Height="11" Width="100" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="50" Height="12" VerticalAlignment="Center" />
</Grid>
<!-- Transaction rows inside rounded container -->
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
<StackPanel Spacing="1">
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
<Border Classes="skeleton" Height="12" Width="130" />
<Border Classes="skeleton" Height="10" Width="88" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="52" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
<Border Classes="skeleton" Height="12" Width="110" />
<Border Classes="skeleton" Height="10" Width="75" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="48" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
<Border Classes="skeleton" Height="12" Width="145" />
<Border Classes="skeleton" Height="10" Width="95" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="55" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
<Border Classes="skeleton" Height="12" Width="120" />
<Border Classes="skeleton" Height="10" Width="82" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="50" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
<Border Classes="skeleton" Height="12" Width="135" />
<Border Classes="skeleton" Height="10" Width="90" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="46" Height="12" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</Border>
<!-- Budget Tracker card -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="5">
<Border Classes="skeleton" Height="14" Width="110" />
<Border Classes="skeleton" Height="11" Width="80" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="52" Height="24"
CornerRadius="{StaticResource RadiusControl}" VerticalAlignment="Center" />
</Grid>
<!-- Budget items -->
<StackPanel Spacing="14">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="88" />
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="65" />
</Grid>
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
</StackPanel>
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="68" />
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="58" />
</Grid>
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
</StackPanel>
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="98" />
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="62" />
</Grid>
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
<!-- Accounts Summary card -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,14">
<StackPanel Spacing="14">
<StackPanel Spacing="5">
<Border Classes="skeleton" Height="14" Width="75" />
<Border Classes="skeleton" Height="11" Width="130" />
</StackPanel>
<!-- Account rows inside rounded container -->
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
<StackPanel Spacing="1">
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="90" />
<Border Classes="skeleton" Height="10" Width="55" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="58" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="70" />
<Border Classes="skeleton" Height="10" Width="48" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="52" Height="12" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
CornerRadius="9" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="82" />
<Border Classes="skeleton" Height="10" Width="52" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="12" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<!-- Total Balance row -->
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="85" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="skeleton" Height="18" Width="95" CornerRadius="7" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@@ -16,7 +16,7 @@
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
<StackPanel Grid.Column="0">
<TextBlock Text="Financial Overview"
@@ -47,13 +47,13 @@
</Button>
</Grid>
<!-- ── Scrollable content ────────────────── -->
<!-- Scrollable content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="14">
<!-- ── KPI cards ──────────────────────── -->
<!-- KPI cards -->
<Grid ColumnDefinitions="*,*">
<!-- Income -->
@@ -164,7 +164,7 @@
</StackPanel>
</Border>
<!-- ── Spending by category chart ────── -->
<!-- Spending by category chart -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -271,7 +271,7 @@
</StackPanel>
</Border>
<!-- ── Recent transactions ───────────── -->
<!-- Recent transactions -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -370,19 +370,30 @@
</StackPanel>
</Border>
<!-- ── Budget tracker ────────────────── -->
<!-- Budget tracker -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="14"
Padding="14,14">
<StackPanel Spacing="14">
<StackPanel>
<TextBlock Text="Budget Tracker"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="Budget Tracker"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" />
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" />
</StackPanel>
<Button Grid.Column="1"
Classes="nav"
VerticalAlignment="Center"
Command="{Binding NavigateToBudgetCommand}"
IsVisible="{Binding HasBudgetData}"
Padding="8,4">
<TextBlock Text="View all" FontSize="12"
Foreground="{DynamicResource AccentBlue}" />
</Button>
</Grid>
<Panel>
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}"
@@ -434,24 +445,29 @@
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Spacing="10" Margin="0,32"
Spacing="10" Margin="0,20"
IsVisible="{Binding !HasBudgetData}">
<Svg Path="../Assets/Icons/wallet.svg" Css="{DynamicResource SvgDisabled}"
Height="36" Width="36" HorizontalAlignment="Center" />
Height="32" Width="32" HorizontalAlignment="Center" />
<TextBlock Text="No budgets set"
FontSize="14" FontWeight="SemiBold"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
<TextBlock Text="Create budgets to track your spending limits."
FontSize="12" Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" TextWrapping="Wrap"
TextAlignment="Center" MaxWidth="200" />
<Button Classes="accented"
HorizontalAlignment="Center"
Padding="20,8"
CornerRadius="10"
Command="{Binding OpenAddBudgetCommand}">
<TextBlock Text="Create Budget"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}" />
</Button>
</StackPanel>
</Panel>
</StackPanel>
</Border>
<!-- ── Accounts summary ──────────────── -->
<!-- Accounts summary -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"

View File

@@ -30,11 +30,11 @@
<mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding SetSavingsGoalDialogViewModel}"
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
<!-- ── Content area ──────────────────────── -->
<!-- Content area -->
<ContentControl Grid.Row="0"
Content="{Binding CurrentView}" />
<!-- ── Bottom tab bar ────────────────────── -->
<!-- Bottom tab bar -->
<Border Grid.Row="1"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -109,19 +109,19 @@
</StackPanel>
</Button>
<!-- Budget -->
<!-- More -->
<Button Grid.Column="4"
Classes="nav"
Classes.active="{Binding isOnBudget}"
Command="{Binding GoToBudgetCommand}"
Classes.active="{Binding isOnMore}"
Command="{Binding GoToMoreCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,6">
<StackPanel Spacing="4" HorizontalAlignment="Center">
<Svg Path="../Assets/Icons/wallet.svg"
<Svg Path="../Assets/Icons/ellipsis.svg"
Width="22" Height="22"
HorizontalAlignment="Center" />
<TextBlock Text="Budget"
<TextBlock Text="More"
FontSize="10"
HorizontalAlignment="Center" />
</StackPanel>

View File

@@ -1,4 +1,8 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Clario.ViewModels;
namespace Clario.MobileViews;
@@ -8,4 +12,26 @@ public partial class MainViewMobile : UserControl
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
topLevel.BackRequested += OnBackRequested;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
topLevel.BackRequested -= OnBackRequested;
}
private void OnBackRequested(object? sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel vm)
e.Handled = vm.HandleBackNavigation();
}
}

View File

@@ -0,0 +1,147 @@
<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"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.MoreViewMobile"
x:DataType="vm:MoreViewModel"
Classes="mobile">
<Design.DataContext>
<vm:MoreViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}">
<!-- Top bar -->
<StackPanel Grid.Row="0" Margin="16,16,16,12">
<TextBlock Text="More"
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}" />
</StackPanel>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="0">
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1,0,1,0"
CornerRadius="14"
ClipToBounds="True">
<StackPanel Spacing="0">
<!-- Analytics -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,1,0,1">
<Button Classes="nav"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Padding="16,0"
MinHeight="58"
Command="{Binding GoToAnalyticsCommand}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
Background="{DynamicResource IconBgPurple}"
CornerRadius="10"
Width="36" Height="36"
Margin="0,0,14,0">
<Svg Path="../Assets/Icons/chart-no-axes-combined.svg"
Width="16" Height="16"
Css="{DynamicResource SvgPurple}" />
</Border>
<TextBlock Grid.Column="1"
Text="Analytics"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Svg Grid.Column="2"
Path="../Assets/Icons/chevron-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center" />
</Grid>
</Button>
</Border>
<!-- Budget -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1">
<Button Classes="nav"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Padding="16,0"
MinHeight="58"
Command="{Binding GoToBudgetCommand}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
Background="{DynamicResource IconBgGreen}"
CornerRadius="10"
Width="36" Height="36"
Margin="0,0,14,0">
<Svg Path="../Assets/Icons/wallet.svg"
Width="16" Height="16"
Css="{DynamicResource SvgGreen}" />
</Border>
<TextBlock Grid.Column="1"
Text="Budget"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Svg Grid.Column="2"
Path="../Assets/Icons/chevron-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center" />
</Grid>
</Button>
</Border>
<!-- Categories -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1">
<Button Classes="nav"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Padding="16,0"
MinHeight="58"
Command="{Binding GoToCategoriesCommand}">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0"
Background="{DynamicResource IconBgBlue}"
CornerRadius="10"
Width="36" Height="36"
Margin="0,0,14,0">
<Svg Path="../Assets/Icons/list.svg"
Width="16" Height="16"
Css="{DynamicResource SvgBlue}" />
</Border>
<TextBlock Grid.Column="1"
Text="Categories"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center" />
<Svg Grid.Column="2"
Path="../Assets/Icons/chevron-right.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center" />
</Grid>
</Button>
</Border>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,238 @@
<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"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.ResetPasswordViewMobile"
x:DataType="vm:ResetPasswordViewModel"
Classes="mobile"
Background="{DynamicResource BgBase}">
<Design.DataContext>
<vm:ResetPasswordViewModel />
</Design.DataContext>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="24,48,24,48" Spacing="0">
<!-- Logo -->
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
<Border CornerRadius="16"
Height="80"
Width="80"
HorizontalAlignment="Center"
Margin="0,0,0,8">
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
</Border>
<TextBlock Text="Your personal finance tracker"
FontSize="13"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center" />
</StackPanel>
<!-- Title -->
<TextBlock Text="Set new password"
FontSize="22" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,8" />
<TextBlock Text="Enter and confirm your new password below."
FontSize="13"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center"
TextWrapping="Wrap"
Margin="0,0,0,32" />
<!-- SUCCESS STATE -->
<StackPanel IsVisible="{Binding PasswordUpdated}" Spacing="16">
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
CornerRadius="12"
Padding="14,12">
<StackPanel Orientation="Horizontal" Spacing="10">
<Svg Path="../Assets/Icons/circle-check.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
<TextBlock Text="Password updated successfully."
FontSize="13"
Foreground="{DynamicResource AccentGreen}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,14"
Command="{Binding GoToSignInCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/log-in.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Sign In"
FontSize="15" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- FORM STATE -->
<StackPanel IsVisible="{Binding !PasswordUpdated}" Spacing="0">
<!-- New Password -->
<TextBlock Text="NEW PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Watermark="At least 6 characters"
Text="{Binding NewPassword}"
PasswordChar="●"
RevealPassword="{Binding #showNew.IsChecked}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2"
Name="showNew"
Background="Transparent"
BorderThickness="0"
Height="48"
Padding="12,0"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="16" Height="16"
IsVisible="{Binding #showNew.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="16" Height="16"
IsVisible="{Binding !#showNew.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Confirm Password -->
<TextBlock Text="CONFIRM PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="14,0,10,0" />
<TextBox Grid.Column="1"
Classes="ghost"
Watermark="Repeat your password"
Text="{Binding ConfirmPassword}"
PasswordChar="●"
RevealPassword="{Binding #showConfirm.IsChecked}"
FontSize="14"
Height="48"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2"
Name="showConfirm"
Background="Transparent"
BorderThickness="0"
Height="48"
Padding="12,0"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="16" Height="16"
IsVisible="{Binding #showConfirm.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="16" Height="16"
IsVisible="{Binding !#showConfirm.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="12"
Padding="14,10"
Margin="0,0,0,20"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="13"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Update password button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,14"
Margin="0,0,0,24"
Command="{Binding SetNewPasswordCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/lock.svg"
Width="16" Height="16"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Update Password"
FontSize="15" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- Footer -->
<Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely."
FontSize="12"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
</StackPanel>
</ScrollViewer>
</UserControl>

View File

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

View File

@@ -14,7 +14,7 @@
<Grid RowDefinitions="Auto,*">
<!-- ── Top bar ────────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,12">
@@ -26,13 +26,13 @@
</StackPanel>
</Grid>
<!-- ── Scrollable content ────────────────── -->
<!-- Scrollable content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,32" Spacing="0">
<!-- ── Global success / error banners ── -->
<!-- Global success / error banners -->
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
@@ -78,9 +78,9 @@
</Grid>
</Border>
<!-- ══════════════════════════════════════
<!--
SECTION: Profile
══════════════════════════════════════ -->
-->
<TextBlock Text="PROFILE" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -201,9 +201,9 @@
</StackPanel>
</Border>
<!-- ══════════════════════════════════════
<!--
SECTION: Account & Security
══════════════════════════════════════ -->
-->
<TextBlock Text="ACCOUNT &amp; SECURITY" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -213,7 +213,7 @@
Margin="0,0,0,20">
<StackPanel Spacing="0">
<!-- ── Email row ──────────────────────── -->
<!-- Email row -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1"
Padding="16,0">
@@ -323,7 +323,7 @@
</Panel>
</Border>
<!-- ── Password row ───────────────────── -->
<!-- Password row -->
<Border Padding="16,0">
<Panel>
<!-- Display row -->
@@ -445,9 +445,9 @@
</StackPanel>
</Border>
<!-- ══════════════════════════════════════
<!--
SECTION: Session
══════════════════════════════════════ -->
-->
<TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"

View File

@@ -16,7 +16,7 @@
<Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── -->
<!-- Top bar -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0">
@@ -75,7 +75,7 @@
</Grid>
<!-- ── Scrollable form ────────────────────── -->
<!-- Scrollable form -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
@@ -154,7 +154,7 @@
</Grid>
</Border>
<!-- ── Amount ──────────────────────── -->
<!-- Amount -->
<TextBlock Text="AMOUNT" Classes="label" FontSize="14" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -173,7 +173,7 @@
Margin="0,0,8,0"
Padding="0 0 0 2" />
<TextBox Grid.Column="1"
Classes="ghost"
Classes="ghost numeric"
Text="{Binding Amount, Mode=TwoWay}"
Watermark="0.00"
FontSize="32"
@@ -195,7 +195,7 @@
</Grid>
</Border>
<!-- ── Description (hidden for transfers) ─── -->
<!-- Description (hidden for transfers) -->
<TextBlock Text="DESCRIPTION" Classes="label" FontSize="14" Margin="0,0,0,6"
IsVisible="{Binding !IsTransfer}" />
<TextBox Text="{Binding Description, Mode=TwoWay}"
@@ -207,7 +207,7 @@
Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}" />
<!-- ── Category + Account (income/expense) ── -->
<!-- Category + Account (income/expense) -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}">
@@ -287,7 +287,7 @@
</Grid>
<!-- ── From + To accounts (transfer) ── -->
<!-- From + To accounts (transfer) -->
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
IsVisible="{Binding IsTransfer}">
<!-- From Account -->
@@ -367,7 +367,7 @@
</StackPanel>
</Grid>
<!-- ── Exchange Rate ──────────────────── -->
<!-- Exchange Rate -->
<Border IsVisible="{Binding ShowExchangeRateField}"
Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -424,7 +424,7 @@
</StackPanel>
</Border>
<!-- ── Date ────────────────────────── -->
<!-- Date -->
<TextBlock Text="DATE" Classes="label" FontSize="14" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -456,7 +456,7 @@
</Grid>
</Border>
<!-- ── Note ────────────────────────── -->
<!-- Note -->
<TextBlock Text="NOTE (OPTIONAL)" Classes="label" FontSize="14" Margin="0,0,0,6" />
<TextBox Text="{Binding Note, Mode=TwoWay}"
Watermark="Add a note..."
@@ -466,7 +466,7 @@
VerticalContentAlignment="Center"
Margin="0,0,0,8" />
<!-- ── Budget approaching warning ──────── -->
<!-- Budget approaching warning -->
<Border Background="{DynamicResource BadgeBgYellow}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="1"
@@ -486,7 +486,7 @@
</StackPanel>
</Border>
<!-- ── Budget over-limit warning ──────── -->
<!-- Budget over-limit warning -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -506,7 +506,7 @@
</StackPanel>
</Border>
<!-- ── Validation error ─────────────── -->
<!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -528,7 +528,7 @@
</StackPanel>
</ScrollViewer>
<!-- ── Bottom action bar ──────────────────── -->
<!-- Bottom action bar -->
<Border Grid.Row="2"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -578,7 +578,7 @@
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="#2A0D0D"
<Border Background="{DynamicResource IconBgRed}"
CornerRadius="14"
Width="52" Height="52"
HorizontalAlignment="Center"
@@ -619,7 +619,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}">
@@ -630,7 +630,7 @@
<TextBlock Text="Delete"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>

View File

@@ -9,10 +9,13 @@
x:DataType="vm:TransactionsViewModel"
x:Name="transactionsRoot"
Classes="mobile">
<Design.DataContext>
<vm:TransactionsViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,Auto,*,Auto"
Background="{DynamicResource BgBase}">
<!-- ── Top Bar ───────────────────────────── -->
<!-- Top Bar -->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="16,16,16,0">
@@ -169,7 +172,8 @@
Padding="0,10"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding ApplyFiltersCommand}">
Command="{Binding LoadPageStrCommand}"
CommandParameter="1">
<TextBlock Text="Apply"
FontSize="13"
FontWeight="SemiBold"
@@ -188,7 +192,7 @@
</Button>
</Grid>
<!-- ── Summary strip ─────────────────────── -->
<!-- Summary strip -->
<Grid Grid.Row="1"
ColumnDefinitions="*,*,*"
Margin="16,12,16,0">
@@ -271,13 +275,13 @@
</Grid>
<!-- ── Transaction list ──────────────────── -->
<!-- Transaction list -->
<ScrollViewer Grid.Row="2"
Margin="0,12,0,0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<ItemsControl ItemsSource="{Binding FilteredTransactions}">
<ItemsControl ItemsSource="{Binding PagedTransactions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
@@ -391,6 +395,22 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Load More button -->
<Button Classes="base"
Margin="16,16,16,16"
Padding="0,12"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
FontSize="13"
IsVisible="{Binding HasNextPage}"
Command="{Binding LoadMoreCommand}">
<TextBlock Text="Load More"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center" />
</Button>
<!-- Empty state -->
<StackPanel HorizontalAlignment="Center"
Spacing="12"

View File

@@ -25,7 +25,7 @@ public class Budget : BaseModel
[Column("created_at")] public DateTime CreatedAt { get; set; }
// ── not in DB ──────────────────────────────────────
// not in DB
[JsonIgnore] public Category? Category { get; set; }
[JsonIgnore] public int TransactionsCount { get; set; }

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Clario.Services;
/// <summary>Resolves a named date range option into concrete start/end dates and a display label.</summary>
public static class DateRangeService
{
private static readonly CultureInfo Culture = new("en-US");
/// <param name="option">The named range key (e.g. "This Month", "Custom Range").</param>
/// <param name="customDates">Required when option is "Custom Range".</param>
/// <returns>Null Start/End means "All Time" (no filter). Label is already uppercased for display.</returns>
public static (DateTime? Start, DateTime? End, string Label) Resolve(string option, IList<DateTime>? customDates = null)
{
var now = DateTime.Now;
return option switch
{
"Today" => (now.Date, now.Date, now.ToString("MMM d, yyyy", Culture).ToUpper()),
"This Week" => ResolveThisWeek(now),
"This Month" => ResolveThisMonth(now),
"Last Month" => ResolveLastMonth(now),
"This Quarter" => ResolveThisQuarter(now),
"This Year" => (new DateTime(now.Year, 1, 1), new DateTime(now.Year, 12, 31), now.Year.ToString()),
"Custom Range" => ResolveCustomRange(customDates, now),
_ => (null, null, "ALL TIME")
};
}
private static (DateTime?, DateTime?, string) ResolveThisWeek(DateTime now)
{
var start = now.Date.AddDays(-(int)now.DayOfWeek);
return (start, start.AddDays(6), "THIS WEEK");
}
private static (DateTime?, DateTime?, string) ResolveThisMonth(DateTime now)
{
var start = new DateTime(now.Year, now.Month, 1);
return (start, start.AddMonths(1).AddDays(-1), now.ToString("MMMM yyyy", Culture).ToUpper());
}
private static (DateTime?, DateTime?, string) ResolveLastMonth(DateTime now)
{
var lm = now.AddMonths(-1);
var start = new DateTime(lm.Year, lm.Month, 1);
return (start, start.AddMonths(1).AddDays(-1), lm.ToString("MMMM yyyy", Culture).ToUpper());
}
private static (DateTime?, DateTime?, string) ResolveThisQuarter(DateTime now)
{
var quarterMonth = now.Month - ((now.Month - 1) % 3);
var start = new DateTime(now.Year, quarterMonth, 1);
var end = start.AddMonths(3).AddDays(-1);
return (start, end, $"Q{(now.Month - 1) / 3 + 1} {now.Year}");
}
private static (DateTime?, DateTime?, string) ResolveCustomRange(IList<DateTime>? dates, DateTime now)
{
if (dates is null || dates.Count == 0)
return (now.Date, now.Date, now.ToString("MMM d, yyyy", Culture).ToUpper());
var ordered = dates.Select(d => d.Date).Distinct().OrderBy(d => d).ToList();
var start = ordered.First();
var end = ordered.Last();
var label = ordered.Count == 1
? start.ToString("MMM dd, yyyy", Culture).ToUpper()
: $"{start.ToString("MMM dd", Culture)} - {end.ToString("MMM dd, yyyy", Culture)}".ToUpper();
return (start, end, label);
}
/// <summary>Formats a date as "Today - MMM dd", "Yesterday - MMM dd", or "MMM dd, yyyy".</summary>
public static string FormatGroupHeader(DateTime date)
{
var now = DateTime.Now.Date;
if (date.Date == now) return "Today — " + date.ToString("MMM dd", Culture);
if (date.Date == now.AddDays(-1)) return "Yesterday — " + date.ToString("MMM dd", Culture);
return date.ToString("MMM dd, yyyy", Culture);
}
}

View File

@@ -19,7 +19,7 @@ namespace Clario.Services;
public static class PdfExportService
{
// ── Print palette (readable on white paper) ───────────
// Print palette (readable on white paper)
private const string TextPrimary = "#111827";
private const string TextSecondary = "#374151";
private const string TextMuted = "#6B7280";
@@ -80,7 +80,7 @@ public static class PdfExportService
page.MarginVertical(1.5f, Unit.Centimetre);
page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial"));
// ── Header ────────────────────────────────────
// Header
page.Header().Column(col =>
{
col.Item().Row(row =>
@@ -103,10 +103,10 @@ public static class PdfExportService
col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar);
});
// ── Content ───────────────────────────────────
// Content
page.Content().PaddingTop(18).Column(col =>
{
// KPI cards ─────────────────────────────
// KPI cards
col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table =>
{
@@ -141,7 +141,7 @@ public static class PdfExportService
col.Item().Height(20);
// Top categories ─────────────────────────
// Top categories
if (topCategories.Count > 0)
{
col.Item().Text("Top Spending Categories")
@@ -189,7 +189,7 @@ public static class PdfExportService
col.Item().Height(20);
}
// Transactions ───────────────────────────
// Transactions
col.Item().Text($"Transactions ({periodTxs.Count})")
.FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table =>
@@ -244,7 +244,7 @@ public static class PdfExportService
});
});
// ── Footer ────────────────────────────────────
// Footer
page.Footer().PaddingTop(6).BorderTop(1).BorderColor(Border).Row(row =>
{
row.RelativeItem().Text("Generated by Clario — Your personal finance tracker")

View File

@@ -241,6 +241,20 @@
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #E8622A; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #D4306A; }</x:String>
<!-- SVG FILL COLORS -->
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #F4F5F8; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #0F1117; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #353A4A; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6B7080; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #A0A6B8; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #3B6AFF; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #18A86B; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #D4A012; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #E53535; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6B44E0; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #E8622A; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #D4306A; }</x:String>
<!-- Toggle Switch specific -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#E2E4ED" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#C8CCDE" />
@@ -249,10 +263,9 @@
<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 -->
<!-- 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>
@@ -606,7 +619,7 @@
</Style>
<Style Selector="Border.inset">
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
<Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="CornerRadius" Value="{DynamicResource RadiusInset}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="BorderThickness" Value="1" />
@@ -1092,11 +1105,11 @@
<Style Selector="TextBox:disabled">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Foreground" Value="{DynamicResource TextDisabled}" />
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
<Setter Property="Background" Value="{DynamicResource BgBase}" />
</Style>
<Style Selector="TextBox:readonly">
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
<Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
</Style>
<Style Selector="TextBox:readonly /template/ Border#PART_BorderElement">
@@ -1181,7 +1194,7 @@
</Style>
<Style Selector="ComboBox:disabled /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
<Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="Opacity" Value="0.5" />
</Style>
@@ -1422,12 +1435,12 @@
</Style>
<Style Selector="cc|DateRangePicker:disabled /template/ Button#PART_Button /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
<Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="Opacity" Value="0.5" />
</Style>
<!-- ── Budget Card — On Track ─────────────────────── -->
<!-- Budget Card — On Track -->
<Style Selector="Border.budget-card">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
@@ -1437,7 +1450,7 @@
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── Budget Card — Warning ──────────────────────── -->
<!-- Budget Card — Warning -->
<Style Selector="Border.budget-card-warning">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" />
@@ -1447,7 +1460,7 @@
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── Budget Card — Over Budget ─────────────────── -->
<!-- Budget Card — Over Budget -->
<Style Selector="Border.budget-card-over">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" />
@@ -1457,13 +1470,13 @@
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── Progress Bar — Yellow ─────────────────────── -->
<!-- Progress Bar — Yellow -->
<Style Selector="ProgressBar.yellow /template/ Border#PART_Indicator">
<Setter Property="Background" Value="{DynamicResource AccentYellow}" />
<Setter Property="CornerRadius" Value="3" />
</Style>
<!-- ── Badge — Warning ───────────────────────────── -->
<!-- Badge — Warning -->
<Style Selector="Border.badge-warning">
<Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" />
@@ -1471,7 +1484,7 @@
<Setter Property="Padding" Value="6,2" />
</Style>
<!-- ── Badge — Over ──────────────────────────────── -->
<!-- Badge — Over -->
<Style Selector="Border.badge-over">
<Setter Property="Background" Value="{DynamicResource BadgeBgRed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" />

View File

@@ -7,7 +7,7 @@
</Border>
</Design.PreviewWith>
<!-- ── FluentCalendarButton (header + prev/next) ──────── -->
<!-- FluentCalendarButton (header + prev/next) -->
<Style Selector="Button.FluentCalendarButton, Button#PART_HeaderButton, Button#PART_PreviousButton, Button#PART_NextButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
@@ -24,7 +24,7 @@
<Setter Property="Background" Value="{DynamicResource BorderSubtle}"/>
</Style>
<!-- ── Calendar root ──────────────────────────────────── -->
<!-- Calendar root -->
<Style Selector="Calendar">
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BgSurface}"/>
@@ -32,7 +32,7 @@
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
</Style>
<!-- ── CalendarItem (the main container) ─────────────── -->
<!-- CalendarItem (the main container) -->
<Style Selector="CalendarItem">
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
@@ -154,7 +154,7 @@
</Setter>
</Style>
<!-- ── CalendarDayButton ──────────────────────────────── -->
<!-- CalendarDayButton -->
<Style Selector="CalendarDayButton">
<Setter Property="Width" Value="38"/>
<Setter Property="Height" Value="38"/>
@@ -243,7 +243,7 @@
<Setter Property="Opacity" Value="0.4"/>
</Style>
<!-- ── CalendarButton (month/year picker cells) ──────── -->
<!-- CalendarButton (month/year picker cells) -->
<Style Selector="CalendarButton">
<Setter Property="Width" Value="60"/>
<Setter Property="Height" Value="52"/>

View File

@@ -30,7 +30,7 @@
In WinUI Min-Width from TemplateSettings
basically...MinWidth of DayItem = 40, 40 * 7 = 280 + margins/padding = ~294
Viewport height is set from # of rows displayed (2-8) in Month mode, = ~290 for 6 weeks (+ day names)
-->
-->
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="40,*" MinWidth="294"
Background="{DynamicResource BgSidebar}">
<Grid ColumnDefinitions="5*,*,*">

View File

@@ -8,11 +8,11 @@
</Border>
</Design.PreviewWith>
<!-- ═══════════════════════════════════════════════════════════
<!--
RESOURCE OVERRIDES
These override the Fluent resource keys used internally
by the ColorPicker flyout template.
═══════════════════════════════════════════════════════════════ -->
-->
<Styles.Resources>
<!-- Tab strip background (top 48px bar) -->
<SolidColorBrush x:Key="SystemControlBackgroundBaseLowBrush" Color="#13161E"/>
@@ -56,9 +56,9 @@
<SolidColorBrush x:Key="ColorViewPreviewBorderBrush" Color="#1E2330"/>
</Styles.Resources>
<!-- ═══════════════════════════════════════════════════════════
<!--
ColorPicker — the drop-down button itself
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="ColorPicker">
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
@@ -86,9 +86,9 @@
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
Flyout popup wrapper
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="FlyoutPresenter.nopadding">
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
@@ -98,9 +98,9 @@
<!-- <Setter Property="BoxShadow" Value="0 8 32 0 #3C000000"/> -->
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
Tab strip inside the flyout
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabControl">
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
</Style>
@@ -129,9 +129,9 @@
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
Hex input TextBox
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox">
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
@@ -147,9 +147,9 @@
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
NumericUpDown (RGB/HSV component value inputs)
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup NumericUpDown">
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
@@ -161,9 +161,9 @@
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
ColorSlider (hue, saturation, value sliders)
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="primitives|ColorSlider">
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Height" Value="16"/>
@@ -193,9 +193,9 @@
<Setter Property="Height" Value="NaN"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
<!--
ColorPreviewer (accent color swatches at the bottom)
═══════════════════════════════════════════════════════════════ -->
-->
<Style Selector="primitives|ColorPreviewer">
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
</Style>

View File

@@ -130,7 +130,7 @@
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- DisabledState -->
<!-- DisabledState -->
<Style Selector="^:disabled">
<Setter Property="Foreground" Value="{DynamicResource TextDisabled}" />
</Style>
@@ -155,7 +155,7 @@
<Setter Property="Opacity" Value="0.4" />
</Style>
<!-- CheckedState -->
<!-- CheckedState -->
<Style Selector="^:checked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="0" />
</Style>
@@ -180,7 +180,7 @@
<Setter Property="Opacity" Value="1" />
</Style>
<!-- UncheckedState -->
<!-- UncheckedState -->
<Style Selector="^:unchecked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="1" />
</Style>

View File

@@ -14,7 +14,7 @@ public partial class AccountFormViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
// Mode
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
@@ -22,7 +22,7 @@ public partial class AccountFormViewModel : ViewModelBase
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Account";
// ── Fields ──────────────────────────────────────────────
// Fields
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _name = "";
@@ -78,12 +78,12 @@ public partial class AccountFormViewModel : ViewModelBase
[ObservableProperty] private string _selectedColor = "#3B82F6";
// ── Options ─────────────────────────────────────────────
// Options
[ObservableProperty] private List<string> _accountTypes = new() { "Cash", "Checking", "Savings", "Credit", "Investment", "Other" };
[ObservableProperty] private List<string> _icons = new() { "wallet", "credit-card", "banknote", "landmark", "piggy-bank", "dollar-sign" };
// ── Validation ──────────────────────────────────────────
// Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
@@ -95,17 +95,17 @@ public partial class AccountFormViewModel : ViewModelBase
public bool IsCredit => SelectedType == "Credit";
// ── Callbacks ───────────────────────────────────────────
// Callbacks
public Action? OnSaved;
public Action? OnCancelled;
// ── Edit mode: original account ─────────────────────────
// Edit mode: original account
private Guid? _editingId;
// ── Result account ──────────────────────────────────────
// Result account
public Account? ResultAccount { get; set; }
// ── Commands ────────────────────────────────────────────
// Commands
partial void OnSelectedTypeChanged(string value)
{
@@ -220,7 +220,7 @@ public partial class AccountFormViewModel : ViewModelBase
OnCancelled?.Invoke();
}
// ── Public setup methods ─────────────────────────────────
// Public setup methods
/// <summary>Call this to open the form for adding a new account.</summary>
public void SetupForAdd()

View File

@@ -35,10 +35,15 @@ public partial class AccountsViewModel : ViewModelBase
[ObservableProperty] private List<Account> _archivedAccounts = new();
public bool HasArchivedAccounts => ArchivedAccounts.Count > 0;
[ObservableProperty] private bool _shouldCloseSheet;
/// <summary>Set by AccountsViewMobile. Returns true and closes the sheet if it was open.</summary>
public Func<bool>? TryCloseSheet { get; set; }
public AccountsViewModel()
{
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); };
AppData.Transactions.CollectionChanged += (_, _) => { Initialize(); };
Track(AppData.Accounts, (_, _) => Initialize());
Track(AppData.Transactions, (_, _) => Initialize());
Initialize();
}
@@ -184,6 +189,7 @@ public partial class AccountsViewModel : ViewModelBase
{
IsDeleteDialogVisible = false;
Initialize();
ShouldCloseSheet = true;
};
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
IsDeleteDialogVisible = true;

View File

@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using SKColor = SkiaSharp.SKColor;
namespace Clario.ViewModels;
@@ -21,7 +22,9 @@ public partial class AnalyticsViewModel : ViewModelBase
public required ViewModelBase parentViewModel;
public GeneralDataRepo AppData => DataRepo.General;
// ── Period ───────────────────────────────────────────
private static readonly SKTypeface _interTypeface = SKTypeface.FromFamilyName("Inter");
// Period
public List<string> PeriodOptions { get; } = new()
{
"Last 30 Days", "Last 3 Months", "Last 6 Months", "Last 12 Months", "This Year"
@@ -31,7 +34,7 @@ public partial class AnalyticsViewModel : ViewModelBase
partial void OnSelectedPeriodChanged(string value) => Initialize();
// ── KPI cards ────────────────────────────────────────
// KPI cards
[ObservableProperty] private string _totalIncomeFormatted = "—";
[ObservableProperty] private string _totalExpensesFormatted = "—";
[ObservableProperty] private string _netSavingsFormatted = "—";
@@ -40,38 +43,38 @@ public partial class AnalyticsViewModel : ViewModelBase
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
// ── Cash Flow chart ──────────────────────────────────
// Cash Flow chart
[ObservableProperty] private ISeries[] _cashFlowSeries = [];
[ObservableProperty] private Axis[] _cashFlowXAxes = [];
[ObservableProperty] private Axis[] _cashFlowYAxes = [];
// ── Net Worth chart ──────────────────────────────────
// Net Worth chart
[ObservableProperty] private ISeries[] _netWorthSeries = [];
[ObservableProperty] private Axis[] _netWorthXAxes = [];
[ObservableProperty] private Axis[] _netWorthYAxes = [];
// ── Day-of-week chart ────────────────────────────────
// Day-of-week chart
[ObservableProperty] private ISeries[] _dayOfWeekSeries = [];
[ObservableProperty] private Axis[] _dayOfWeekXAxes = [];
// ── Top categories ───────────────────────────────────
// Top categories
[ObservableProperty] private ObservableCollection<CategorySpendRow> _topCategories = new();
[ObservableProperty] private bool _hasTopCategories;
// ── Income sources donut ─────────────────────────────
// Income sources donut
[ObservableProperty] private ISeries[] _incomeSourcesSeries = [];
[ObservableProperty] private bool _hasIncomeSources;
// ── State ────────────────────────────────────────────
// State
[ObservableProperty] private bool _isExporting;
[ObservableProperty] private string? _exportStatusMessage;
// ─────────────────────────────────────────────────────
//
public AnalyticsViewModel()
{
AppData.Transactions.CollectionChanged += (_, _) => Initialize();
AppData.Accounts.CollectionChanged += (_, _) => Initialize();
Track(AppData.Transactions, (_, _) => Initialize());
Track(AppData.Accounts, (_, _) => Initialize());
Initialize();
}
@@ -101,7 +104,7 @@ public partial class AnalyticsViewModel : ViewModelBase
}
}
// ── Date range ────────────────────────────────────────
// Date range
private (DateTime start, DateTime end) GetDateRange()
{
@@ -132,7 +135,7 @@ public partial class AnalyticsViewModel : ViewModelBase
return buckets;
}
// ── Section 1: KPIs ───────────────────────────────────
// Section 1: KPIs
private void ComputeKpis(List<Transaction> income, List<Transaction> expenses)
{
@@ -150,7 +153,7 @@ public partial class AnalyticsViewModel : ViewModelBase
: "—";
}
// ── Section 2: Cash Flow ──────────────────────────────
// Section 2: Cash Flow
private void BuildCashFlowChart(DateTime start, DateTime end)
{
@@ -202,7 +205,7 @@ public partial class AnalyticsViewModel : ViewModelBase
new Axis
{
Labels = labels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 11
@@ -214,7 +217,7 @@ public partial class AnalyticsViewModel : ViewModelBase
[
new Axis
{
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 10,
@@ -223,7 +226,7 @@ public partial class AnalyticsViewModel : ViewModelBase
];
}
// ── Section 3: Net Worth ──────────────────────────────
// Section 3: Net Worth
private void BuildNetWorthChart(DateTime start, DateTime end)
{
@@ -266,7 +269,7 @@ public partial class AnalyticsViewModel : ViewModelBase
new Axis
{
Labels = labels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 11
@@ -278,7 +281,7 @@ public partial class AnalyticsViewModel : ViewModelBase
[
new Axis
{
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
TicksPaint = null,
TextSize = 10,
@@ -287,7 +290,7 @@ public partial class AnalyticsViewModel : ViewModelBase
];
}
// ── Section 4: Day of Week ────────────────────────────
// Section 4: Day of Week
private void BuildDayOfWeekChart(List<Transaction> expenses, DateTime start, DateTime end)
{
@@ -331,7 +334,7 @@ public partial class AnalyticsViewModel : ViewModelBase
new Axis
{
Labels = dayLabels,
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
SeparatorsPaint = null,
TicksPaint = null,
TextSize = 11
@@ -339,7 +342,7 @@ public partial class AnalyticsViewModel : ViewModelBase
];
}
// ── Section 5: Top Categories ─────────────────────────
// Section 5: Top Categories
private void BuildTopCategories(List<Transaction> expenses)
{
@@ -372,7 +375,7 @@ public partial class AnalyticsViewModel : ViewModelBase
HasTopCategories = grouped.Count > 0;
}
// ── Section 6: Income Sources ─────────────────────────
// Section 6: Income Sources
private void BuildIncomeSourcesChart(List<Transaction> income)
{
@@ -403,7 +406,7 @@ public partial class AnalyticsViewModel : ViewModelBase
HasIncomeSources = true;
}
// ── PDF Export ────────────────────────────────────────
// PDF Export
[RelayCommand]
private async Task ExportPdf()

View File

@@ -24,7 +24,7 @@ public partial class AuthViewModel : ViewModelBase
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
private string _lastName;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
private string _email;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
@@ -34,13 +34,15 @@ public partial class AuthViewModel : ViewModelBase
private string _confirmPassword;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount))]
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount), nameof(isForgotPassword), nameof(ShowTabs))]
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
private string _operation = "login";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
[ObservableProperty] private bool _resetEmailSent;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public AuthViewModel()
@@ -72,6 +74,30 @@ public partial class AuthViewModel : ViewModelBase
{
Operation = operation;
ErrorMessage = null;
ResetEmailSent = false;
}
[RelayCommand(CanExecute = nameof(canSendResetLink))]
private async Task SendResetLink()
{
ErrorMessage = null;
try
{
await SupabaseService.Client.Auth.ResetPasswordForEmail(_email);
ResetEmailSent = true;
}
catch (GotrueException e)
{
DebugLogger.Log(e);
ErrorMessage = e.Reason == FailureHint.Reason.UserBadEmailAddress
? GetErrorMessage(AuthError.InvalidEmail)
: GetErrorMessage(AuthError.Unknown);
}
catch (Exception e)
{
DebugLogger.Log(e);
ErrorMessage = GetErrorMessage(AuthError.Unknown);
}
}
[RelayCommand(CanExecute = nameof(canSignin))]
@@ -185,12 +211,16 @@ public partial class AuthViewModel : ViewModelBase
public bool isSignin => Operation == "login";
public bool isCreateAccount => Operation == "signup";
public bool isForgotPassword => Operation == "forgotPassword";
public bool ShowTabs => !isForgotPassword;
public bool canSignin => isSignin && !string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_password);
public bool canCreateAccount => isCreateAccount && !string.IsNullOrWhiteSpace(_firstName) && !string.IsNullOrWhiteSpace(_lastName) &&
!string.IsNullOrWhiteSpace(_email) &&
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
public bool canSendResetLink => isForgotPassword && !string.IsNullOrWhiteSpace(_email);
}
class Wrapper

View File

@@ -14,7 +14,7 @@ public partial class BudgetFormViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
// Mode
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
@@ -22,7 +22,7 @@ public partial class BudgetFormViewModel : ViewModelBase
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Budget";
// ── Fields ──────────────────────────────────────────────
// Fields
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsMonthly), nameof(IsQuarterly), nameof(IsYearly), nameof(IsValid))]
private string _period = "monthly";
@@ -43,7 +43,7 @@ public partial class BudgetFormViewModel : ViewModelBase
[ObservableProperty] private bool _rollover = false;
// ── Validation ──────────────────────────────────────────
// Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
@@ -57,20 +57,20 @@ public partial class BudgetFormViewModel : ViewModelBase
decimal.TryParse(LimitAmount, out var amt) && amt > 0 &&
SelectedCategory is not null;
// ── Callbacks ───────────────────────────────────────────
// Callbacks
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Edit mode: original budget ───────────────────────────
// Edit mode: original budget
private Guid? _editingId;
// ── Result ──────────────────────────────────────────────
// Result
public Budget? ResultBudget { get; set; }
// ── Commands ────────────────────────────────────────────
// Commands
[RelayCommand]
private void SetPeriod(string period)
@@ -174,7 +174,7 @@ public partial class BudgetFormViewModel : ViewModelBase
OnCancelled?.Invoke();
}
// ── Public setup methods ─────────────────────────────────
// Public setup methods
/// <summary>Call this to open the form for adding a new budget.</summary>
public void SetupForAdd(ObservableCollection<Category> categories)

View File

@@ -69,17 +69,20 @@ public partial class BudgetViewModel : ViewModelBase
public BudgetViewModel()
{
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
};
Track(AppData.Budgets, async (_, _) => await Initialize());
Track(AppData.Transactions, async (_, _) => await Initialize());
AppData.PropertyChanged += OnProfileChanged;
OnDispose(() => AppData.PropertyChanged -= OnProfileChanged);
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize());
_ = Initialize();
}
private void OnProfileChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
}
private async Task Initialize()
{
try
@@ -98,19 +101,19 @@ public partial class BudgetViewModel : ViewModelBase
[RelayCommand]
private void CreateBudget()
{
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null);
if (parentViewModel is MainViewModel main) main.OpenAddBudgetCommand.Execute(null);
}
[RelayCommand]
private void EditBudget(Budget budget)
{
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
if (parentViewModel is MainViewModel main) main.OpenEditBudgetCommand.Execute(budget);
}
[RelayCommand]
private void EditSavingsGoal()
{
((MainViewModel)parentViewModel).OpenEditSavingsGoalCommand.Execute(null);
if (parentViewModel is MainViewModel main) main.OpenEditSavingsGoalCommand.Execute(null);
}
private void ProcessChartData()

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Clario.Data;
using Clario.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Clario.ViewModels;
public partial class CategoriesViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
public GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasExpenseCategories))]
private ObservableCollection<Category> _expenseCategories = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasIncomeCategories))]
private ObservableCollection<Category> _incomeCategories = new();
public bool HasExpenseCategories => ExpenseCategories.Count > 0;
public bool HasIncomeCategories => IncomeCategories.Count > 0;
public CategoriesViewModel()
{
Track(AppData.Categories, (_, _) => Initialize());
Initialize();
}
public void Initialize()
{
ExpenseCategories = new ObservableCollection<Category>(
AppData.Categories.Where(c => c.Type == "expense").OrderBy(c => c.Name));
IncomeCategories = new ObservableCollection<Category>(
AppData.Categories.Where(c => c.Type == "income").OrderBy(c => c.Name));
}
[RelayCommand]
private void EditCategory(Category category)
{
if (parentViewModel is MainViewModel mainVm)
mainVm.OpenEditCategory(category);
}
[RelayCommand]
private void AddCategory()
{
if (parentViewModel is MainViewModel mainVm)
mainVm.OpenAddCategory();
}
}

View File

@@ -13,7 +13,7 @@ public partial class CategoryFormViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
// Mode
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))]
private bool _isEditMode = false;
@@ -22,7 +22,7 @@ public partial class CategoryFormViewModel : ViewModelBase
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category";
// ── Fields ──────────────────────────────────────────────
// Fields
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _name = "";
@@ -33,7 +33,7 @@ public partial class CategoryFormViewModel : ViewModelBase
[ObservableProperty] private string _selectedColor = "#7B9CFF";
// ── Icon options ─────────────────────────────────────────
// Icon options
public List<string> CategoryIcons { get; } = new()
{
// Food & Dining
@@ -59,7 +59,7 @@ public partial class CategoryFormViewModel : ViewModelBase
"receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw",
};
// ── Validation ──────────────────────────────────────────
// Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
@@ -69,18 +69,18 @@ public partial class CategoryFormViewModel : ViewModelBase
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4;
// ── Delete confirm sub-modal ────────────────────────────
// Delete confirm sub-modal
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Callbacks ───────────────────────────────────────────
// Callbacks
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
// ── Edit mode: original category ────────────────────────
// Edit mode: original category
private Guid? _editingId;
// ── Commands ────────────────────────────────────────────
// Commands
[RelayCommand]
private void SetType(string type) => Type = type;
@@ -163,7 +163,7 @@ public partial class CategoryFormViewModel : ViewModelBase
}
}
// ── Public setup methods ─────────────────────────────────
// Public setup methods
public void SetupForAdd()
{

View File

@@ -0,0 +1,5 @@
namespace Clario.ViewModels;
public partial class DashboardSkeletonViewModel : ViewModelBase
{
}

View File

@@ -86,38 +86,25 @@ public partial class DashboardViewModel : ViewModelBase
partial void OnSelectedChartTimePeriodChanged(string value)
{
ChartTimePeriod period = value switch
{
"This Month" => ChartTimePeriod.ThisMonth,
"Last Month" => ChartTimePeriod.LastMonth,
"This Quarter" => ChartTimePeriod.ThisQuarter,
"This Year" => ChartTimePeriod.ThisYear,
_ => ChartTimePeriod.ThisMonth
};
var (_, _, subtitle) = DateRangeService.Resolve(value);
SelectedChartTimPeriodSubTitle = subtitle.Length > 0
? char.ToUpper(subtitle[0]) + subtitle.Substring(1).ToLower()
: subtitle;
SelectedChartTimPeriodSubTitle = value switch
{
"This Month" => DateTime.Now.ToString("MMMM yyyy"),
"Last Month" => DateTime.Now.AddMonths(-1).ToString("MMMM yyyy"),
"This Quarter" => $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}",
"This Year" => DateTime.Now.Year.ToString(),
_ => DateTime.Now.ToString("MMMM yyyy")
};
UpdateSpendingByCategoryChart(period);
UpdateSpendingByCategoryChart(value);
}
public DashboardViewModel()
{
AppData.Transactions.CollectionChanged += (s, e) => UpdateUserOverview();
AppData.Accounts.CollectionChanged += (s, e) => UpdateUserOverview();
AppData.Categories.CollectionChanged += (s, e) => UpdateUserOverview();
AppData.Budgets.CollectionChanged += (s, e) => UpdateUserOverview();
Track(AppData.Transactions, (_, _) => UpdateUserOverview());
Track(AppData.Accounts, (_, _) => UpdateUserOverview());
Track(AppData.Categories, (_, _) => UpdateUserOverview());
Track(AppData.Budgets, (_, _) => UpdateUserOverview());
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview());
initialize();
Initialize();
}
public void initialize()
public void Initialize()
{
UpdateUserOverview();
}
@@ -126,7 +113,7 @@ public partial class DashboardViewModel : ViewModelBase
private void UpdateUserOverview()
{
CalculateMonthlyValues();
UpdateSpendingByCategoryChart();
UpdateSpendingByCategoryChart(SelectedChartTimePeriod);
_ = UpdateBudgetTracker();
UpdateRecentTransactions();
UpdateAccountsSummary();
@@ -175,53 +162,50 @@ public partial class DashboardViewModel : ViewModelBase
[RelayCommand]
private void CreateTransaction()
{
((MainViewModel)parentViewModel).OpenAddTransaction();
if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
}
[RelayCommand]
private void NavigateToSettings()
{
((MainViewModel)parentViewModel).GoToSettingsCommand.Execute(null);
if (parentViewModel is MainViewModel main) main.GoToSettingsCommand.Execute(null);
}
private void UpdateSpendingByCategoryChart(ChartTimePeriod period = ChartTimePeriod.ThisMonth)
[RelayCommand]
private void NavigateToBudget()
{
if (parentViewModel is MainViewModel main) main.GoToBudgetCommand.Execute(null);
}
[RelayCommand]
private void OpenAddBudget()
{
if (parentViewModel is MainViewModel main) main.OpenAddBudgetCommand.Execute(null);
}
private void UpdateSpendingByCategoryChart(string period = "This Month")
{
var (start, end, _) = DateRangeService.Resolve(period);
var tempList = new List<ColumnChartData>();
foreach (var category in AppData.Categories)
{
var categoryTransactions =
AppData.Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase));
var txns = AppData.Transactions
.Where(x => x.CategoryId == category.Id
&& x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase)
&& (start is null || x.Date.Date >= start.Value)
&& (end is null || x.Date.Date <= end.Value));
switch (period)
var total = txns.Sum(x => x.ConvertedAmount);
if (total == 0) continue;
tempList.Add(new ColumnChartData
{
case ChartTimePeriod.ThisMonth:
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.Month);
break;
case ChartTimePeriod.LastMonth:
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.AddMonths(-1).Month);
break;
case ChartTimePeriod.ThisQuarter:
categoryTransactions = categoryTransactions.Where(x =>
x.Date.Month >= DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3).Month &&
x.Date.Month <= DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3).AddMonths(3).Month);
break;
case ChartTimePeriod.ThisYear:
categoryTransactions = categoryTransactions.Where(x => x.Date.Year == DateTime.Now.Year);
break;
default:
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.Month);
break;
}
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)) });
id = category.Id,
Name = category.Name,
Values = [(double)total],
Fill = new SolidColorPaint(SKColor.Parse(category.Color))
});
}
tempList = tempList.OrderByDescending(x => x.Values[0]).ToList();
@@ -268,12 +252,4 @@ public partial class DashboardViewModel : ViewModelBase
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.Where(a => !a.IsArchived).OrderBy(x => x.CreatedAt));
OnPropertyChanged(nameof(AccountsSubtitle));
}
private enum ChartTimePeriod
{
ThisMonth,
LastMonth,
ThisQuarter,
ThisYear
}
}

View File

@@ -12,7 +12,7 @@ namespace Clario.ViewModels;
public partial class DeleteAccountDialogViewModel : ViewModelBase
{
// ── State machine ────────────────────────────────────────
// State machine
public enum DialogStep
{
SimpleConfirm,
@@ -31,7 +31,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
public bool IsHasTransactionsStep => CurrentStep == DialogStep.HasTransactions;
public bool IsMigrateStep => CurrentStep == DialogStep.Migrate;
// ── Data ─────────────────────────────────────────────────
// Data
[ObservableProperty] private Account? _account;
public GeneralDataRepo AppData => DataRepo.General;
@@ -40,7 +40,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Account> _availableAccounts = new();
// ── Validation ───────────────────────────────────────────
// Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
@@ -50,11 +50,11 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
TargetAccount is not null &&
TargetAccount.Id != Account?.Id;
// ── Callbacks ────────────────────────────────────────────
// Callbacks
public Action? OnDeleted;
public Action? OnCancelled;
// ── Setup ────────────────────────────────────────────────
// Setup
/// <summary>
/// Call this to open the dialog for a specific account.
@@ -79,7 +79,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
: DialogStep.SimpleConfirm;
}
// ── Commands ─────────────────────────────────────────────
// Commands
[RelayCommand]
private void Cancel() => OnCancelled?.Invoke();

View File

@@ -1,8 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using Avalonia.Xaml.Interactivity;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Clario.ViewModels;
public partial class LoadingViewModel : ViewModelBase
{
}

View File

@@ -21,7 +21,9 @@ public partial class MainViewModel : ViewModelBase
public TransactionsViewModel _transactionsViewModel = null!;
private AccountsViewModel _accountsViewModel = null!;
private BudgetViewModel _budgetViewModel = null!;
private CategoriesViewModel _categoriesViewModel = null!;
private AnalyticsViewModel _analyticsViewModel = null!;
private MoreViewModel _moreViewModel = null!;
GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty] private Profile? _profile;
@@ -34,6 +36,9 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!;
[ObservableProperty] private bool _isDimmed;
[ObservableProperty] private bool _isMessageBoxVisible;
[ObservableProperty] private MessageBoxViewModel _messageBoxViewModel = new();
[ObservableProperty] private bool _isTransactionFormVisible;
[ObservableProperty] private bool _isAccountFormVisible;
[ObservableProperty] private bool _isBudgetFormVisible;
@@ -42,7 +47,8 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnAnalytics), nameof(isOnSettings))]
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnCategories), nameof(isOnAnalytics),
nameof(isOnSettings), nameof(isOnMore))]
private ViewModelBase? _currentView;
[ObservableProperty] private bool _isDarkTheme;
@@ -50,13 +56,13 @@ public partial class MainViewModel : ViewModelBase
public MainViewModel()
{
DebugLogger.Log("main vm loaded");
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) =>
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (s, m) =>
{
Profile = AppData.Profile;
_ = DataRepo.General.RefreshLiveRatesAndEnrich();
});
IsDimmed = true;
CurrentView = new LoadingViewModel();
CurrentView = new DashboardSkeletonViewModel();
_ = InitializeApp();
}
@@ -73,12 +79,12 @@ public partial class MainViewModel : ViewModelBase
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();
DebugLogger.Log("fetched all data");
});
@@ -89,12 +95,13 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this
};
DebugLogger.Log("initialized DashboardViewModel");
_transactionsViewModel = new TransactionsViewModel()
{
parentViewModel = this
};
DebugLogger.Log("initialized TransactionsViewModel");
_accountsViewModel = new AccountsViewModel()
{
parentViewModel = this
@@ -106,6 +113,16 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this
};
DebugLogger.Log("initialized BudgetViewModel");
_categoriesViewModel = new CategoriesViewModel()
{
parentViewModel = this
};
DebugLogger.Log("initialized CategoriesViewModel");
_moreViewModel = new MoreViewModel()
{
parentViewModel = this
};
DebugLogger.Log("initialized MoreViewModel");
_analyticsViewModel = new AnalyticsViewModel()
{
parentViewModel = this
@@ -144,6 +161,7 @@ public partial class MainViewModel : ViewModelBase
IsDarkTheme = ThemeService.IsDarkTheme;
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
AppData.StartRealtimeSync();
CurrentView = _dashboardViewModel;
IsDimmed = false;
}
@@ -153,6 +171,24 @@ public partial class MainViewModel : ViewModelBase
}
}
/// <summary>Shows a themed message box overlay. Safe to call from any child ViewModel.</summary>
public void ShowMessage(MessageType type, string title, string message)
{
MessageBoxViewModel.Type = type;
MessageBoxViewModel.Title = title;
MessageBoxViewModel.Message = message;
MessageBoxViewModel.OnClose = CloseMessageBox;
IsMessageBoxVisible = true;
IsDimmed = true;
}
[RelayCommand]
private void CloseMessageBox()
{
IsMessageBoxVisible = false;
IsDimmed = false;
}
[RelayCommand]
public void OpenAddTransaction()
{
@@ -403,6 +439,18 @@ public partial class MainViewModel : ViewModelBase
CurrentView = _budgetViewModel;
}
[RelayCommand]
private void GoToCategories()
{
CurrentView = _categoriesViewModel;
}
[RelayCommand]
private void GoToMore()
{
CurrentView = _moreViewModel;
}
[RelayCommand]
private void GoToAnalytics()
{
@@ -432,10 +480,74 @@ public partial class MainViewModel : ViewModelBase
}
}
/// <summary>Returns true if the back event was handled (suppress system back), false to let the system close the app.</summary>
public bool HandleBackNavigation()
{
// 1. Close deepest-nested modal first (category form sits on top of transaction form)
if (IsCategoryFormVisible)
{
CloseCategoryForm();
return true;
}
if (IsTransactionFormVisible)
{
CloseTransactionForm();
return true;
}
if (IsAccountFormVisible)
{
CloseAccountForm();
return true;
}
if (IsBudgetFormVisible)
{
CloseBudgetForm();
return true;
}
if (IsSavingsGoalDialogVisible)
{
CloseSavingsGoalDialog();
return true;
}
// 2. Close dialogs inside AccountsView
if (_accountsViewModel is { IsDeleteDialogVisible: true })
{
_accountsViewModel.IsDeleteDialogVisible = false;
return true;
}
if (_accountsViewModel is { IsArchiveDialogVisible: true })
{
_accountsViewModel.IsArchiveDialogVisible = false;
return true;
}
// 3. Close AccountsView bottom sheet
if (_accountsViewModel?.TryCloseSheet?.Invoke() == true)
return true;
// 4. Navigate back to dashboard from any non-dashboard main view
if (!isOnDashboard)
{
CurrentView = _dashboardViewModel;
return true;
}
// 5. Already on dashboard — let the system handle (closes the app)
return false;
}
public bool isOnDashboard => CurrentView is DashboardViewModel;
public bool isOnTransactions => CurrentView is TransactionsViewModel;
public bool isOnAccounts => CurrentView is AccountsViewModel;
public bool isOnBudget => CurrentView is BudgetViewModel;
public bool isOnCategories => CurrentView is CategoriesViewModel;
public bool isOnAnalytics => CurrentView is AnalyticsViewModel;
public bool isOnSettings => CurrentView is SettingsViewModel;
public bool isOnMore => CurrentView is MoreViewModel or AnalyticsViewModel or BudgetViewModel or CategoriesViewModel;
}

View File

@@ -0,0 +1,32 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
namespace Clario.ViewModels;
public enum MessageType { Error, Warning, Success, Info }
public partial class MessageBoxViewModel : ViewModelBase
{
[ObservableProperty] private MessageType _type = MessageType.Info;
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _message = "";
public bool IsError => Type == MessageType.Error;
public bool IsWarning => Type == MessageType.Warning;
public bool IsSuccess => Type == MessageType.Success;
public bool IsInfo => Type == MessageType.Info;
public Action? OnClose { get; set; }
partial void OnTypeChanged(MessageType value)
{
OnPropertyChanged(nameof(IsError));
OnPropertyChanged(nameof(IsWarning));
OnPropertyChanged(nameof(IsSuccess));
OnPropertyChanged(nameof(IsInfo));
}
[RelayCommand]
private void Close() => OnClose?.Invoke();
}

View File

@@ -0,0 +1,29 @@
using CommunityToolkit.Mvvm.Input;
namespace Clario.ViewModels;
public partial class MoreViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
[RelayCommand]
private void GoToAnalytics()
{
if (parentViewModel is MainViewModel mainVm)
mainVm.GoToAnalyticsCommand.Execute(null);
}
[RelayCommand]
private void GoToBudget()
{
if (parentViewModel is MainViewModel mainVm)
mainVm.GoToBudgetCommand.Execute(null);
}
[RelayCommand]
private void GoToCategories()
{
if (parentViewModel is MainViewModel mainVm)
mainVm.GoToCategoriesCommand.Execute(null);
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Supabase.Gotrue;
using Supabase.Gotrue.Exceptions;
namespace Clario.ViewModels;
public partial class ResetPasswordViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SetNewPasswordCommand))]
private string _newPassword = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SetNewPasswordCommand))]
private string _confirmPassword = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
[ObservableProperty] private bool _passwordUpdated;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
[RelayCommand(CanExecute = nameof(CanSetPassword))]
private async Task SetNewPassword()
{
ErrorMessage = null;
try
{
await SupabaseService.Client.Auth.Update(new UserAttributes { Password = _newPassword });
PasswordUpdated = true;
}
catch (GotrueException e)
{
DebugLogger.Log(e);
ErrorMessage = e.Reason == FailureHint.Reason.UserBadPassword
? "Password must be at least 6 characters."
: "Something went wrong. Please try again.";
}
catch (Exception e)
{
DebugLogger.Log(e);
ErrorMessage = "Something went wrong. Please try again.";
}
}
[RelayCommand]
private void GoToSignIn()
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow!.DataContext = new AuthViewModel();
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime sv)
sv.MainView!.DataContext = new AuthViewModel();
}
private bool CanSetPassword =>
!string.IsNullOrWhiteSpace(_newPassword) &&
_newPassword.Length >= 6 &&
_newPassword == _confirmPassword;
}

View File

@@ -26,11 +26,11 @@ public partial class SetSavingsGoalDialogViewModel : ViewModelBase
public bool IsValid =>
decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) && v >= 0;
// ── Callbacks ────────────────────────────────────────────
// Callbacks
public Action? OnSaved;
public Action? OnCancelled;
// ── Setup ────────────────────────────────────────────────
// Setup
public void Setup(decimal? currentGoal)
{
GoalInput = currentGoal.HasValue
@@ -39,7 +39,7 @@ public partial class SetSavingsGoalDialogViewModel : ViewModelBase
ErrorMessage = null;
}
// ── Commands ─────────────────────────────────────────────
// Commands
[RelayCommand]
private void Cancel() => OnCancelled?.Invoke();

View File

@@ -24,29 +24,29 @@ public partial class SettingsViewModel : ViewModelBase
public static readonly HttpClient _HttpClient = new();
// ── Profile fields ───────────────────────────────────────
// Profile fields
[ObservableProperty] private string _displayName = "";
[ObservableProperty] private string _avatarUrl = "";
[ObservableProperty] private Bitmap? _avatarImage;
[ObservableProperty] private string _selectedTheme = "system";
[ObservableProperty] private string _selectedLanguage = "en";
// ── Account (auth) fields ────────────────────────────────
// Account (auth) fields
[ObservableProperty] private string _maskedEmail = "";
private string _fullEmail = "";
// ── Change email flow ────────────────────────────────────
// Change email flow
[ObservableProperty] private bool _isChangingEmail = false;
[ObservableProperty] private string _newEmail = "";
[ObservableProperty] private string _emailConfirmPassword = "";
// ── Change password flow ─────────────────────────────────
// Change password flow
[ObservableProperty] private bool _isChangingPassword = false;
[ObservableProperty] private string _currentPassword = "";
[ObservableProperty] private string _newPassword = "";
[ObservableProperty] private string _confirmNewPassword = "";
// ── UI state ─────────────────────────────────────────────
// UI state
[ObservableProperty] private bool _isSaving = false;
[ObservableProperty] private bool _isUploadingAvatar = false;
@@ -76,7 +76,7 @@ public partial class SettingsViewModel : ViewModelBase
public bool HasPasswordError => !string.IsNullOrEmpty(PasswordErrorMessage);
public bool HasAvatar => !string.IsNullOrEmpty(AvatarUrl);
// ── Options ──────────────────────────────────────────────
// Options
public ObservableCollection<(string Value, string Label)> Themes { get; } = new()
{
@@ -118,7 +118,7 @@ public partial class SettingsViewModel : ViewModelBase
SelectedLanguage = value switch { 0 => "en", 1 => "ar", _ => "en" };
}
// ── Init ─────────────────────────────────────────────────
// Init
public SettingsViewModel()
{
_ = Initialize();
@@ -155,7 +155,7 @@ public partial class SettingsViewModel : ViewModelBase
return $"{visible}{masked}{domain}";
}
// ── Avatar commands ───────────────────────────────────────
// Avatar commands
[RelayCommand]
private async Task UploadAvatar()
@@ -215,7 +215,7 @@ public partial class SettingsViewModel : ViewModelBase
}
}
// ── Save profile ─────────────────────────────────────────
// Save profile
[RelayCommand]
private async Task SaveProfile()
@@ -263,7 +263,7 @@ public partial class SettingsViewModel : ViewModelBase
}
}
// ── Change email ─────────────────────────────────────────
// Change email
[RelayCommand]
private void StartChangeEmail()
@@ -327,7 +327,7 @@ public partial class SettingsViewModel : ViewModelBase
}
}
// ── Change password ──────────────────────────────────────
// Change password
[RelayCommand]
private void StartChangePassword()
@@ -397,7 +397,7 @@ public partial class SettingsViewModel : ViewModelBase
}
}
// ── Sign out ─────────────────────────────────────────────
// Sign out
[RelayCommand]
private async Task SignOut()

View File

@@ -17,7 +17,7 @@ public partial class TransactionFormViewModel : ViewModelBase
public required ViewModelBase parentViewModel;
public GeneralDataRepo AppData => DataRepo.General;
// ── Mode ────────────────────────────────────────────────
// Mode
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
@@ -25,7 +25,7 @@ public partial class TransactionFormViewModel : ViewModelBase
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : (IsTransfer ? "Save Transfer" : "Save Transaction");
// ── Fields ──────────────────────────────────────────────
// Fields
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome), nameof(IsTransfer), nameof(IsValid), nameof(FormTitle), nameof(SaveButtonLabel))]
private string _type = "expense";
@@ -64,7 +64,7 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
// ── Validation ──────────────────────────────────────────
// Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
@@ -80,7 +80,7 @@ public partial class TransactionFormViewModel : ViewModelBase
? SelectedAccount is not null && SelectedToAccount is not null && SelectedAccount.Id != SelectedToAccount.Id
: !string.IsNullOrWhiteSpace(Description) && SelectedCategory is not null && SelectedAccount is not null);
// ── Callbacks ───────────────────────────────────────────
// Callbacks
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
@@ -89,17 +89,17 @@ public partial class TransactionFormViewModel : ViewModelBase
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Edit mode: original transaction ─────────────────────
// Edit mode: original transaction
private Transaction? _editingTransaction;
private Guid? _editingId;
private Guid? _transferPairId;
private decimal _editingOriginalAmount;
private Guid? _editingOriginalCategoryId;
// ── Result transaction ──────────────────────────────────
// Result transaction
public Transaction? ResultTransaction { get; set; }
// ── Budget warning ──────────────────────────────────────
// Budget warning
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasBudgetWarning), nameof(HasBudgetApproachingWarning))]
private string? _budgetWarningMessage;
@@ -109,7 +109,7 @@ public partial class TransactionFormViewModel : ViewModelBase
public bool HasBudgetWarning => !string.IsNullOrEmpty(BudgetWarningMessage);
public bool HasBudgetApproachingWarning => HasBudgetWarning && !BudgetWarningIsOverBudget;
// ── Commands ────────────────────────────────────────────
// Commands
partial void OnSelectedCategoryChanged(Category? value)
{
@@ -414,7 +414,7 @@ public partial class TransactionFormViewModel : ViewModelBase
OnCancelled?.Invoke();
}
// ── Public setup methods ─────────────────────────────────
// Public setup methods
/// <summary>Call this to open the form for adding a new transaction.</summary>
public void SetupForAdd()

View File

@@ -1,7 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Clario.Data;
using Clario.Messages;
@@ -11,60 +10,98 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
// ReSharper disable PossibleMultipleEnumeration
namespace Clario.ViewModels;
public partial class TransactionsViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
public GeneralDataRepo AppData => DataRepo.General;
private GeneralDataRepo AppData => DataRepo.General;
// ── Filter dropdowns ────────────────────────────────────────────────────
[ObservableProperty] private ObservableCollection<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilteredTransactionCount))]
private static readonly IReadOnlyList<string> _sortOptions = new[]
{
"Date — Newest first", "Date — Oldest first",
"Amount — High to low", "Amount — Low to high",
"Category A → Z"
};
private static readonly IReadOnlyList<string> _dateRangeOptions = new[]
{
"All Time", "Today", "This Week", "This Month",
"Last Month", "This Quarter", "This Year", "Custom Range"
};
public IReadOnlyList<string> SortOptions => _sortOptions;
public IReadOnlyList<string> DateRangeOptions => _dateRangeOptions;
// ── Active filter values ─────────────────────────────────────────────────
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private Category _selectedCategory;
[ObservableProperty] private Account _selectedAccount;
[ObservableProperty] private string _selectedSortOption = _sortOptions[0];
[ObservableProperty] private string _selectedDateRangeOption = _dateRangeOptions[0];
[ObservableProperty] private List<DateTime>? _selectedDates = new()
{
new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1),
new DateTime(DateTime.Now.Year, DateTime.Now.Month,
DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month))
};
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome),
nameof(FilterTypeExpense), nameof(FilterTypeTransfer))]
private string _transactionType = "all";
public bool FilterTypeAll => TransactionType == "all";
public bool FilterTypeIncome => TransactionType == "income";
public bool FilterTypeExpense => TransactionType == "expense";
public bool FilterTypeTransfer => TransactionType == "transfer";
// ── Filtered / paged data ────────────────────────────────────────────────
private List<Transaction> _filteredAll = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FilteredTransactionCount))]
private List<Transaction> _filteredTransactions = new();
public int FilteredTransactionCount => _filteredTransactions.Count;
[ObservableProperty] private ObservableCollection<Transaction> _pagedTransactions = new();
// ── Desktop pagination ───────────────────────────────────────────────────
private int _pageSize = 25;
[ObservableProperty] private int _pageSizeIndex;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(TotalPages))] [NotifyCanExecuteChangedFor(nameof(NextPageCommand), nameof(PreviousPageCommand))]
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TotalPages))]
[NotifyCanExecuteChangedFor(nameof(NextPageCommand), nameof(PreviousPageCommand))]
private int _currentPage = 1;
[ObservableProperty] private string _paginationSummaryText;
[ObservableProperty] private ObservableCollection<Transaction> _pagedTransactions = new();
[ObservableProperty] private ObservableCollection<string> _sortOptions = new()
{
"Date — Newest first",
"Date — Oldest first",
"Amount — High to low",
"Amount — Low to high",
"Category A → Z"
};
[ObservableProperty] private ObservableCollection<string> _DateRangeOptions = new()
{
"All Time",
"Today",
"This Week",
"This Month",
"Last Month",
"This Quarter",
"This Year",
"Custom Range"
};
public List<int> PageNumbers { get; set; }
[ObservableProperty] private string _paginationSummaryText = "";
[ObservableProperty] private ObservableCollection<int> _visiblePageNumbers = new();
public int TotalPages => (int)Math.Ceiling(FilteredTransactions.Count / (double)_pageSize);
public bool HasNoTransactions => FilteredTransactions.Count == 0;
public bool HasNextPage => CurrentPage < TotalPages;
public bool HasPreviousPage => CurrentPage > 1;
public int TotalPages => (int)Math.Ceiling(_filteredAll.Count / (double)_pageSize);
public bool HasNoTransactions => _filteredAll.Count == 0;
public bool HasPreviousPage => CurrentPage > 1;
// HasNextPage differs by platform
public bool HasNextPage => App.IsMobile
? _mobileDisplayCount < _filteredAll.Count
: CurrentPage < TotalPages;
// ── Mobile infinite scroll ───────────────────────────────────────────────
/// How many real (non-header) items are currently rendered in PagedTransactions.
private int _mobileDisplayCount;
// ── Summary stats ────────────────────────────────────────────────────────
[ObservableProperty] private double _totalExpenses;
[ObservableProperty] private double _totalIncome;
@@ -75,261 +112,23 @@ public partial class TransactionsViewModel : ViewModelBase
public string PrimaryCurrencySymbol =>
CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private Category _selectedCategory;
[ObservableProperty] private Account _selectedAccount;
[ObservableProperty] private string _selectedSortOption;
[ObservableProperty] private string _selectedDateRangeOption;
[ObservableProperty] private List<DateTime>? _selectedDates = new()
{
new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1),
new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month))
};
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome), nameof(FilterTypeExpense), nameof(FilterTypeTransfer))]
private string _transactionType = "all";
// ── Constructor ──────────────────────────────────────────────────────────
public TransactionsViewModel()
{
AppData.Transactions.CollectionChanged += (_, _) =>
Track(AppData.Transactions, (_, _) =>
{
InitializeCategories();
InitializeAccounts();
LoadPage(1);
};
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => LoadPage(CurrentPage));
Refresh();
});
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => Refresh());
Initialize();
}
partial void OnPageSizeIndexChanged(int value)
{
_pageSize = value switch
{
0 => 25,
1 => 50,
2 => 100,
_ => 25
};
LoadPage(1);
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasPreviousPage));
}
partial void OnCurrentPageChanged(int value)
{
LoadPage(value);
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasPreviousPage));
}
[RelayCommand]
private void LoadPageStr(string page)
{
LoadPage(int.Parse(page));
}
[RelayCommand]
private void LoadPage(int page)
{
ApplyFilters();
if (CurrentPage != page) CurrentPage = page;
var items = FilteredTransactions.Skip((page - 1) * _pageSize)
.Take(_pageSize);
OnPropertyChanged(nameof(HasNoTransactions));
PagedTransactions.Clear();
foreach (var item in items)
PagedTransactions.Add(item);
PaginationSummaryText =
$"Showing {((page - 1) * _pageSize) + 1}-{(Math.Min(page * _pageSize, FilteredTransactions.Count))} of {FilteredTransactions.Count} transactions";
PageNumbers = Enumerable.Range(1, Math.Min(TotalPages, 5)).ToList();
var numbers = GetSurrounding(PageNumbers, page);
VisiblePageNumbers.Clear();
foreach (var number in numbers)
VisiblePageNumbers.Add(number);
WeakReferenceMessenger.Default.Send(new TransactionsScrollToTop());
GroupTransactions();
}
[RelayCommand]
private void ApplyFilters()
{
var filtered = AppData.Transactions.Where(x =>
x.Type != "transfer_in" &&
(x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| x.Note!.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
var culture = new CultureInfo("en-US");
switch (SelectedDateRangeOption)
{
case "All Time":
DateRangeLabel = "ALL TIME";
break;
case "Today":
filtered = filtered.Where(x => x.Date == DateTime.Now.Date);
DateRangeLabel = DateTime.Now.ToString("MMM d, yyyy", culture).ToUpper();
break;
case "This Week":
var startOfWeek = DateTime.Now.Date.AddDays(-(int)DateTime.Now.DayOfWeek);
var endOfWeek = startOfWeek.AddDays(6);
filtered = filtered.Where(x => x.Date.Date >= startOfWeek && x.Date.Date <= endOfWeek);
DateRangeLabel = "THIS WEEK";
break;
case "This Month":
filtered = filtered.Where(x => x.Date.Month == DateTime.Now.Month);
DateRangeLabel = DateTime.Now.ToString("MMMM yyyy", culture).ToUpper();
break;
case "Last Month":
var lastMonth = DateTime.Now.AddMonths(-1);
filtered = filtered.Where(x => x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year);
DateRangeLabel = lastMonth.ToString("MMMM yyyy", culture).ToUpper();
break;
case "This Quarter":
var startOfQuarter = DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3);
var endOfQuarter = startOfQuarter.AddMonths(3);
filtered = filtered.Where(x => x.Date >= startOfQuarter && x.Date <= endOfQuarter);
DateRangeLabel = $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}";
break;
case "This Year":
filtered = filtered.Where(x => x.Date.Year == DateTime.Now.Year);
DateRangeLabel = DateTime.Now.Year.ToString();
break;
case "Custom Range":
if (SelectedDates is not null && SelectedDates.Count > 0)
{
var ordered = SelectedDates
.Select(d => d.Date)
.Distinct()
.OrderBy(d => d)
.ToList();
var start = ordered.First();
var end = ordered.Last();
if (SelectedDates.Count == 1)
{
filtered = filtered.Where(x => x.Date.Date == start);
DateRangeLabel = start.ToString("MMM dd, yyyy", culture).ToUpper();
}
else
{
filtered = filtered.Where(x => x.Date.Date >= start && x.Date.Date <= end);
DateRangeLabel = $"{start.ToString("MMM dd", culture)} - {end.ToString("MMM dd, yyyy", culture)}".ToUpper();
}
}
break;
}
// Calculate totals based on date-filtered transactions (transfers excluded)
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);
if (SelectedAccount.Name != "All Accounts")
filtered = filtered.Where(x => x.AccountId == SelectedAccount.Id);
if (TransactionType == "income")
filtered = filtered.Where(x => x.Type == "income");
else if (TransactionType == "expense")
filtered = filtered.Where(x => x.Type == "expense");
else if (TransactionType == "transfer")
filtered = filtered.Where(x => x.IsTransfer);
switch (SelectedSortOption)
{
case "Date — Newest first":
filtered = filtered.OrderByDescending(x => x.Date);
break;
case "Date — Oldest first":
filtered = filtered.OrderBy(x => x.Date);
break;
case "Amount — High to low":
filtered = filtered.OrderByDescending(x => x.Amount);
break;
case "Amount — Low to high":
filtered = filtered.OrderBy(x => x.Amount);
break;
case "Category A → Z":
filtered = filtered.OrderBy(x => x.Category?.Name);
break;
}
FilteredTransactions = filtered.ToList();
}
[RelayCommand]
private void ResetFilters()
{
SearchText = "";
SelectedCategory = Categories.First();
SelectedAccount = Accounts.First();
TransactionType = "all";
SelectedSortOption = SortOptions.First();
SelectedDateRangeOption = DateRangeOptions.First();
LoadPage(1);
}
[RelayCommand]
private void SetTransactionType(string type)
{
TransactionType = type;
}
public bool FilterTypeAll => TransactionType == "all";
public bool FilterTypeIncome => TransactionType == "income";
public bool FilterTypeExpense => TransactionType == "expense";
public bool FilterTypeTransfer => TransactionType == "transfer";
[RelayCommand(CanExecute = nameof(HasNextPage))]
private void NextPage()
{
if (CurrentPage < TotalPages) CurrentPage++;
}
[RelayCommand(CanExecute = nameof(HasPreviousPage))]
private void PreviousPage()
{
if (CurrentPage > 1) CurrentPage--;
}
private void GroupTransactions()
{
var ToRemove = PagedTransactions.Where(x => x.GroupHeader).ToList();
foreach (var item in ToRemove)
{
PagedTransactions.Remove(item);
}
var dates = PagedTransactions
.Where(x => !x.GroupHeader)
.Select(x => x.Date.Date) // strip time
.Distinct()
.ToList();
foreach (var date in dates)
{
var index = PagedTransactions.IndexOf(PagedTransactions.First(x => x.Date.Date == date && !x.GroupHeader));
string label;
var culture = new CultureInfo("en-US");
if (date.Date == DateTime.Now.Date) label = "Today - " + date.ToString("MMM dd", culture);
else if (date.Date == DateTime.Now.AddDays(-1).Date) label = "Yesterday - " + date.ToString("MMM dd", culture);
else label = date.ToString("MMM dd, yyyy", culture);
var header = new Transaction { Description = label, Date = date, GroupHeader = true };
PagedTransactions.Insert(index, header);
}
}
// ── Initialization ───────────────────────────────────────────────────────
public void Initialize()
{
@@ -337,9 +136,7 @@ public partial class TransactionsViewModel : ViewModelBase
{
InitializeCategories();
InitializeAccounts();
CalculateMonthlyFinancials();
CurrentPage = 1;
OnPropertyChanged(nameof(TotalPages));
ResetFilters();
@@ -354,59 +151,274 @@ public partial class TransactionsViewModel : ViewModelBase
private void InitializeCategories()
{
Categories.Clear();
Categories.Insert(0, new Category() { Name = "All Categories" });
foreach (var appDataCategory in AppData.Categories)
{
Categories.Add(appDataCategory);
}
Categories.Insert(0, new Category { Name = "All Categories" });
foreach (var cat in AppData.Categories)
Categories.Add(cat);
SelectedCategory = Categories.First();
}
private void InitializeAccounts()
{
Accounts.Clear();
Accounts.Insert(0, new Account() { Name = "All Accounts" });
foreach (var appDataAccount in AppData.Accounts)
{
Accounts.Add(appDataAccount);
}
Accounts.Insert(0, new Account { Name = "All Accounts" });
foreach (var acc in AppData.Accounts)
Accounts.Add(acc);
SelectedAccount = Accounts.First();
}
private void CalculateMonthlyFinancials()
{
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);
var now = DateTime.Now;
var monthly = AppData.Transactions
.Where(x => x.Date.Month == now.Month && x.Date.Year == now.Year);
TotalExpenses = monthly.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
TotalIncome = monthly.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
ExpensesCount = monthly.Count(x => x.Type == "expense");
IncomeCount = monthly.Count(x => x.Type == "income");
}
public static List<T> GetSurrounding<T>(List<T> list, T item, int count = 5)
// ── Filter pipeline ──────────────────────────────────────────────────────
[RelayCommand]
private void ApplyFilters()
{
var filteringByAccount = SelectedAccount?.Name != "All Accounts";
// 1. Search + transfer-in visibility
IEnumerable<Transaction> source = AppData.Transactions.Where(x =>
(filteringByAccount || x.Type != "transfer_in") &&
(x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| (x.Note?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false)));
// 2. Date range
source = ApplyDateFilter(source, out var label);
DateRangeLabel = label;
// 3. Totals use the date-scoped set (before category/type filters)
CalculateTotalsFromSource(source);
// 4. Remaining filters
source = ApplyCategoryFilter(source);
source = ApplyAccountFilter(source);
source = ApplyTypeFilter(source);
source = ApplySortFilter(source);
_filteredAll = source.ToList();
FilteredTransactions = _filteredAll;
}
private IEnumerable<Transaction> ApplyDateFilter(IEnumerable<Transaction> source, out string label)
{
var (start, end, lbl) = DateRangeService.Resolve(SelectedDateRangeOption, SelectedDates);
label = lbl;
if (start is null || end is null) return source;
return source.Where(x => x.Date.Date >= start.Value && x.Date.Date <= end.Value);
}
private void CalculateTotalsFromSource(IEnumerable<Transaction> source)
{
var list = source.ToList();
TotalExpenses = list.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
TotalIncome = list.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
}
private IEnumerable<Transaction> ApplyCategoryFilter(IEnumerable<Transaction> source)
{
if (SelectedCategory?.Name == "All Categories") return source;
return source.Where(x => x.CategoryId == SelectedCategory?.Id);
}
private IEnumerable<Transaction> ApplyAccountFilter(IEnumerable<Transaction> source)
{
if (SelectedAccount?.Name == "All Accounts") return source;
return source.Where(x => x.AccountId == SelectedAccount?.Id);
}
private IEnumerable<Transaction> ApplyTypeFilter(IEnumerable<Transaction> source) =>
TransactionType switch
{
"income" => source.Where(x => x.Type == "income"),
"expense" => source.Where(x => x.Type == "expense"),
"transfer" => source.Where(x => x.IsTransfer),
_ => source
};
private IEnumerable<Transaction> ApplySortFilter(IEnumerable<Transaction> source) =>
SelectedSortOption switch
{
"Date — Oldest first" => source.OrderBy(x => x.Date),
"Amount — High to low" => source.OrderByDescending(x => x.Amount),
"Amount — Low to high" => source.OrderBy(x => x.Amount),
"Category A → Z" => source.OrderBy(x => x.Category?.Name),
_ => source.OrderByDescending(x => x.Date) // default: newest first
};
// ── Desktop pagination ───────────────────────────────────────────────────
partial void OnPageSizeIndexChanged(int value)
{
_pageSize = value switch { 1 => 50, 2 => 100, _ => 25 };
LoadPage(1);
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasPreviousPage));
}
partial void OnCurrentPageChanged(int value)
{
if (App.IsMobile) return;
LoadPage(value);
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasPreviousPage));
}
[RelayCommand]
private void LoadPageStr(string page) => LoadPage(int.Parse(page));
[RelayCommand]
private void LoadPage(int page)
{
ApplyFilters();
if (CurrentPage != page) CurrentPage = page;
var items = _filteredAll.Skip((page - 1) * _pageSize).Take(_pageSize).ToList();
PagedTransactions.Clear();
foreach (var item in items) PagedTransactions.Add(item);
OnPropertyChanged(nameof(HasNoTransactions));
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasPreviousPage));
PaginationSummaryText = _filteredAll.Count == 0
? "No transactions"
: $"Showing {(page - 1) * _pageSize + 1}{Math.Min(page * _pageSize, _filteredAll.Count)} of {_filteredAll.Count}";
var allPages = Enumerable.Range(1, Math.Max(TotalPages, 1)).ToList();
VisiblePageNumbers.Clear();
foreach (var n in GetSurrounding(allPages, page)) VisiblePageNumbers.Add(n);
WeakReferenceMessenger.Default.Send(new TransactionsScrollToTop());
GroupTransactions();
}
[RelayCommand(CanExecute = nameof(HasNextPage))]
private void NextPage() { if (CurrentPage < TotalPages) CurrentPage++; }
[RelayCommand(CanExecute = nameof(HasPreviousPage))]
private void PreviousPage() { if (CurrentPage > 1) CurrentPage--; }
// ── Mobile infinite scroll ───────────────────────────────────────────────
private void RefreshMobile()
{
_mobileDisplayCount = 0;
PagedTransactions.Clear();
AppendMobileItems(_pageSize * 3);
OnPropertyChanged(nameof(HasNoTransactions));
OnPropertyChanged(nameof(HasNextPage));
}
private void AppendMobileItems(int count)
{
var batch = _filteredAll.Skip(_mobileDisplayCount).Take(count).ToList();
foreach (var item in batch)
{
var needsHeader = _mobileDisplayCount == 0
|| item.Date.Date != _filteredAll[_mobileDisplayCount - 1].Date.Date;
if (needsHeader)
{
PagedTransactions.Add(new Transaction
{
Description = DateRangeService.FormatGroupHeader(item.Date),
Date = item.Date,
GroupHeader = true
});
}
PagedTransactions.Add(item);
_mobileDisplayCount++;
}
OnPropertyChanged(nameof(HasNextPage));
}
/// Adds 3 pages of items at once. Shown behind "Load More" button.
[RelayCommand(CanExecute = nameof(HasNextPage))]
private void LoadMore()
{
if (_mobileDisplayCount >= _filteredAll.Count) return;
AppendMobileItems(_pageSize * 3);
}
// ── Shared helpers ───────────────────────────────────────────────────────
private void Refresh()
{
CalculateMonthlyFinancials();
if (App.IsMobile) { ApplyFilters(); RefreshMobile(); }
else LoadPage(CurrentPage);
}
[RelayCommand]
private void ResetFilters()
{
SearchText = "";
SelectedCategory = Categories.FirstOrDefault() ?? new Category { Name = "All Categories" };
SelectedAccount = Accounts.FirstOrDefault() ?? new Account { Name = "All Accounts" };
TransactionType = "all";
SelectedSortOption = SortOptions[0];
SelectedDateRangeOption = DateRangeOptions[0];
if (App.IsMobile) { ApplyFilters(); RefreshMobile(); }
else LoadPage(1);
}
[RelayCommand]
private void SetTransactionType(string type) => TransactionType = type;
/// Desktop: inserts date group headers into PagedTransactions.
private void GroupTransactions()
{
// Remove all existing headers
foreach (var h in PagedTransactions.Where(x => x.GroupHeader).ToList())
PagedTransactions.Remove(h);
// Insert a header before the first item of each date group
var dates = PagedTransactions.Select(x => x.Date.Date).Distinct().ToList();
foreach (var date in dates)
{
var firstItem = PagedTransactions.FirstOrDefault(x => !x.GroupHeader && x.Date.Date == date);
if (firstItem is null) continue;
PagedTransactions.Insert(PagedTransactions.IndexOf(firstItem), new Transaction
{
Description = DateRangeService.FormatGroupHeader(date),
Date = date,
GroupHeader = true
});
}
}
private static List<T> GetSurrounding<T>(List<T> list, T item, int count = 5)
{
var index = list.IndexOf(item);
if (index == -1) return new List<T>();
var half = count / 2;
var start = Math.Max(0, index - half);
var end = Math.Min(list.Count, start + count);
// shift start back if end hit the boundary
start = Math.Max(0, end - count);
return list.GetRange(start, end - start);
var start = Math.Max(0, Math.Min(index - count / 2, list.Count - count));
return list.GetRange(start, Math.Min(count, list.Count - start));
}
// ── Navigation ───────────────────────────────────────────────────────────
[RelayCommand]
private void CreateTransaction()
{
((MainViewModel)parentViewModel).OpenAddTransaction();
if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
}
[RelayCommand]
private void EditTransaction(Transaction transaction)
{
((MainViewModel)parentViewModel).OpenEditTransaction(transaction);
if (parentViewModel is MainViewModel main) main.OpenEditTransaction(transaction);
}
}

View File

@@ -1,7 +1,36 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Specialized;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
namespace Clario.ViewModels;
public abstract class ViewModelBase : ObservableObject
public abstract class ViewModelBase : ObservableObject, IDisposable
{
private readonly System.Collections.Generic.List<Action> _cleanup = new();
/// <summary>
/// Subscribes to a CollectionChanged event and registers automatic unsubscription on Dispose.
/// </summary>
protected void Track(INotifyCollectionChanged collection, NotifyCollectionChangedEventHandler handler)
{
collection.CollectionChanged += handler;
_cleanup.Add(() => collection.CollectionChanged -= handler);
}
/// <summary>
/// Registers an arbitrary cleanup action to run on Dispose.
/// </summary>
protected void OnDispose(Action action) => _cleanup.Add(action);
protected virtual void DisposeManaged() { }
public void Dispose()
{
DisposeManaged();
foreach (var action in _cleanup) action();
_cleanup.Clear();
WeakReferenceMessenger.Default.UnregisterAll(this);
GC.SuppressFinalize(this);
}
}

View File

@@ -13,11 +13,11 @@
<vm:AccountFormViewModel />
</Design.DataContext>
<!-- ── Dim overlay ───────────────────────── -->
<!-- Dim overlay -->
<Grid>
<Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── -->
<!-- Modal card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
@@ -29,7 +29,7 @@
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- ── Header ──────────────────────── -->
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0"
CornerRadius="10"
@@ -66,7 +66,7 @@
</Button>
</Grid>
<!-- ── Name ──────────────────────── -->
<!-- Name -->
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Name, Mode=TwoWay}"
Watermark="e.g. Main Checking"
@@ -76,7 +76,7 @@
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
<!-- ── Type ─────────────────────────── -->
<!-- Type -->
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -92,7 +92,7 @@
HorizontalAlignment="Stretch" />
</Border>
<!-- ── Institution + Mask ──────────── -->
<!-- Institution + Mask -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Institution -->
<StackPanel Grid.Column="0" Spacing="6">
@@ -122,7 +122,7 @@
</StackPanel>
</Grid>
<!-- ── Opening Balance + Currency ──────────── -->
<!-- Opening Balance + Currency -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Opening Balance -->
<StackPanel Grid.Column="0" Spacing="6">
@@ -204,7 +204,7 @@
</StackPanel>
</Grid>
<!-- ── Credit Limit (if type is credit) ──────────── -->
<!-- Credit Limit (if type is credit) -->
<StackPanel Spacing="6" Margin="0,0,0,16" IsVisible="{Binding IsCredit}">
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
<Border Background="{DynamicResource BgBase}"
@@ -236,7 +236,7 @@
</Border>
</StackPanel>
<!-- ── Opened At ──────────────────────── -->
<!-- Opened At -->
<TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -250,7 +250,7 @@
Padding="12,10" />
</Border>
<!-- ── Icon + Color ──────────── -->
<!-- Icon + Color -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Icon -->
<StackPanel Grid.Column="0" Spacing="6">
@@ -304,7 +304,7 @@
</StackPanel>
</Grid>
<!-- ── Primary account toggle ──────────── -->
<!-- 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" />
@@ -320,7 +320,7 @@
VerticalAlignment="Center" />
</Grid>
<!-- ── Validation error ─────────────── -->
<!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -339,7 +339,7 @@
</StackPanel>
</Border>
<!-- ── Actions ──────────────────────── -->
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"

View File

@@ -13,7 +13,7 @@
<vm:AccountsViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
<!-- TOP BAR -->
<!-- TOP BAR -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,24">
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding ActiveAccountCount, StringFormat='{}{0} accounts'}" FontSize="12" Foreground="{DynamicResource TextMuted}" />
@@ -35,9 +35,9 @@
</StackPanel>
</Button>
</Grid>
<!-- MAIN CONTENT Left * — account cards list Right 340 — selected account detail panel -->
<!-- MAIN CONTENT Left * — account cards list Right 340 — selected account detail panel -->
<Grid Grid.Row="1" ColumnDefinitions="*,Auto">
<!-- LEFT — Account Cards -->
<!-- LEFT — Account Cards -->
<Grid Grid.Column="0" RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="0,0,20,0" Padding="8 0">
<StackPanel Spacing="12" Margin="0 0 0 28">
@@ -179,7 +179,7 @@
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Width="340" Padding="8 0"
IsVisible="{Binding SelectedAccount, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Spacing="14" Margin="0 0 0 28">
<!-- Account detail card -->
<!-- Account detail card -->
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
Padding="22">
<StackPanel Spacing="18">
@@ -263,7 +263,7 @@
</Grid>
</StackPanel>
</Border>
<!-- Monthly Flow -->
<!-- Monthly Flow -->
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
Padding="22">
<StackPanel Spacing="14">
@@ -308,7 +308,7 @@
</Grid>
</StackPanel>
</Border>
<!-- Recent Transactions -->
<!-- Recent Transactions -->
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
Padding="22">
<StackPanel Spacing="14">
@@ -375,7 +375,7 @@
</ItemsControl>
</StackPanel>
</Border>
<!-- Net Worth Contribution -->
<!-- Net Worth Contribution -->
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
Padding="22">
<StackPanel Spacing="12">
@@ -410,7 +410,7 @@
</Grid>
</StackPanel>
</Border>
<!-- Manage Account -->
<!-- Manage Account -->
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
Padding="22">
<StackPanel Spacing="10">

View File

@@ -15,7 +15,7 @@
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
<!-- ── Top Bar ── -->
<!-- Top Bar -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,24">
<StackPanel Grid.Column="0">
<TextBlock Classes="muted" Text="Insights &amp; Trends" />
@@ -45,7 +45,7 @@
</StackPanel>
</Grid>
<!-- ── Scrollable Content ── -->
<!-- Scrollable Content -->
<ScrollViewer Grid.Row="1" Name="mainScrollviewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
@@ -60,7 +60,7 @@
Foreground="{DynamicResource AccentGreen}" FontSize="12" />
</Border>
<!-- ── Section 1: KPI Cards ── -->
<!-- Section 1: KPI Cards -->
<Grid ColumnDefinitions="*,*,*,*">
<Grid.Styles>
<Style Selector="Grid > Border">
@@ -125,7 +125,7 @@
</Border>
</Grid>
<!-- ── Section 2: Cash Flow Trend ── -->
<!-- Section 2: Cash Flow Trend -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>
@@ -145,7 +145,7 @@
</StackPanel>
</Border>
<!-- ── Section 3: Net Worth ── -->
<!-- Section 3: Net Worth -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>
@@ -165,7 +165,7 @@
</StackPanel>
</Border>
<!-- ── Section 4+6: Day of Week + Income Sources ── -->
<!-- Section 4+6: Day of Week + Income Sources -->
<Grid ColumnDefinitions="*,*" >
<!-- Day of Week -->
<Border Grid.Column="0" Classes="card" Margin="0,0,10,0">
@@ -213,7 +213,7 @@
</Border>
</Grid>
<!-- ── Section 5: Top Categories ── -->
<!-- Section 5: Top Categories -->
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel>

View File

@@ -9,16 +9,16 @@
x:Class="Clario.Views.AuthView">
<Grid>
<!-- Background -->
<!-- Background -->
<!-- <Calendar SelectionMode="SingleRange"> -->
<!-- </Calendar> -->
<!-- <Border Background="{DynamicResource AccentBlue}" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400" Padding="10"> -->
<!-- -->
<!-- -->
<!-- </Border> -->
<!-- Center card -->
<!-- Center card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
@@ -30,7 +30,7 @@
BoxShadow="0 24 64 0 #40000000">
<StackPanel Spacing="0">
<!-- Logo + App name -->
<!-- Logo + App name -->
<StackPanel HorizontalAlignment="Center"
Spacing="0"
Margin="0,0,0,32">
@@ -43,10 +43,10 @@
<!-- REPLACE: app name -->
<StackPanel Spacing="4" HorizontalAlignment="Center">
<!-- <TextBlock Text="Clario" -->
<!-- FontSize="22" -->
<!-- FontWeight="Bold" -->
<!-- Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Center" /> -->
<!-- FontSize="22" -->
<!-- FontWeight="Bold" -->
<!-- Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Center" /> -->
<TextBlock Text="Your personal finance tracker"
FontSize="12"
Foreground="{DynamicResource TextMuted}"
@@ -54,13 +54,14 @@
</StackPanel>
</StackPanel>
<!-- Tab switcher -->
<!-- Tab switcher -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="3"
Margin="0,0,0,26">
Margin="0,0,0,26"
IsVisible="{Binding ShowTabs}">
<Grid ColumnDefinitions="*,*">
<!-- REPLACE: active state driven by IsLoginMode -->
<!-- Sign In — active -->
@@ -178,13 +179,13 @@
</Border>
<!-- Forgot password -->
<!-- REPLACE: Command="{Binding ForgotPasswordCommand}" -->
<Button Background="Transparent"
BorderThickness="0"
Padding="0"
Cursor="Hand"
HorizontalAlignment="Right"
Margin="0,0,0,24">
Margin="0,0,0,24"
Command="{Binding SetOperationCommand}" CommandParameter="forgotPassword">
<TextBlock Text="Forgot password?"
FontSize="12"
Foreground="{DynamicResource AccentBlue}" />
@@ -230,10 +231,129 @@
</StackPanel>
<!-- ══════════════════════════════════
<!-- FORGOT PASSWORD PANEL -->
<StackPanel Spacing="0" IsVisible="{Binding isForgotPassword}">
<!-- Back button + heading -->
<Button Background="Transparent"
BorderThickness="0"
Padding="0"
Cursor="Hand"
HorizontalAlignment="Left"
Margin="0,0,0,20"
Command="{Binding SetOperationCommand}" CommandParameter="login">
<StackPanel Orientation="Horizontal" Spacing="6">
<Svg Path="../Assets/Icons/arrow-left.svg"
Width="14" Height="14"
Css="{DynamicResource SvgMuted}" />
<TextBlock Text="Back to Sign In"
FontSize="12"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Button>
<TextBlock Text="Reset your password"
FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
Margin="0,0,0,6" />
<TextBlock Text="Enter your email and we'll send you a reset link."
FontSize="12"
Foreground="{DynamicResource TextMuted}"
TextWrapping="Wrap"
Margin="0,0,0,20" />
<!-- Success state -->
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10"
Margin="0,0,0,16"
IsVisible="{Binding ResetEmailSent}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-check.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
<TextBlock Text="Check your email for a reset link."
FontSize="12"
Foreground="{DynamicResource AccentGreen}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Email -->
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6"
IsVisible="{Binding !ResetEmailSent}" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,14"
IsVisible="{Binding !ResetEmailSent}">
<Grid ColumnDefinitions="Auto,*">
<Svg Grid.Column="0"
Path="../Assets/Icons/mail.svg"
Width="15" Height="15"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="12,0,10,0" />
<TextBox Grid.Column="1" Classes="ghost"
Watermark="you@example.com"
Text="{Binding Email}"
BorderThickness="0"
FontSize="13"
Foreground="{DynamicResource TextPrimary}"
Height="42"
Padding="0"
VerticalContentAlignment="Center" />
</Grid>
</Border>
<!-- Error message -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10"
Margin="0,0,0,16"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Send Reset Link button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,12"
Margin="0,0,0,20"
IsVisible="{Binding !ResetEmailSent}"
Command="{Binding SendResetLinkCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/mail.svg"
Width="15" Height="15"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Send Reset Link"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!--
SIGN UP PANEL
REPLACE: IsVisible="{Binding !IsLoginMode}"
══════════════════════════════════ -->
-->
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
<!-- Name row -->
@@ -430,7 +550,7 @@
</StackPanel>
<!-- Footer -->
<!-- Footer -->
<Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely."
FontSize="11"

View File

@@ -11,11 +11,11 @@
<vm:BudgetFormViewModel />
</Design.DataContext>
<!-- ── Dim overlay ───────────────────────── -->
<!-- Dim overlay -->
<Grid>
<Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── -->
<!-- Modal card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
@@ -27,7 +27,7 @@
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- ── Header ──────────────────────── -->
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0"
Background="{DynamicResource BgSurface}"
@@ -62,7 +62,7 @@
</Button>
</Grid>
<!-- ── Category ───────────────────── -->
<!-- Category -->
<TextBlock Text="CATEGORY" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -96,7 +96,7 @@
</Grid>
</Border>
<!-- ── Limit Amount ────────────────── -->
<!-- Limit Amount -->
<TextBlock Text="LIMIT AMOUNT" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -129,7 +129,7 @@
</Grid>
</Border>
<!-- ── Period ─────────────────────── -->
<!-- Period -->
<TextBlock Text="PERIOD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -187,7 +187,7 @@
</Grid>
</Border>
<!-- ── Alert Threshold ────────────── -->
<!-- Alert Threshold -->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,6">
<TextBlock Grid.Column="0" Text="ALERT THRESHOLD" Classes="label" />
<TextBlock Grid.Column="1"
@@ -223,7 +223,7 @@
</StackPanel>
</Border>
<!-- ── Rollover ───────────────────── -->
<!-- Rollover -->
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -257,7 +257,7 @@
</Grid>
</Border>
<!-- ── Validation error ─────────────── -->
<!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -276,7 +276,7 @@
</StackPanel>
</Border>
<!-- ── Delete button (edit mode only) ── -->
<!-- Delete button (edit mode only) -->
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
@@ -290,7 +290,7 @@
</StackPanel>
</Button>
<!-- ── Actions ──────────────────────── -->
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
@@ -323,7 +323,7 @@
</StackPanel>
</Border>
<!-- ── Delete confirm sub-modal ──────────────── -->
<!-- Delete confirm sub-modal -->
<Grid IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"
@@ -338,7 +338,7 @@
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="#2A0D0D"
<Border Background="{DynamicResource IconBgRed}"
CornerRadius="14"
Width="52" Height="52"
HorizontalAlignment="Center"
@@ -379,7 +379,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}">
@@ -390,7 +390,7 @@
<TextBlock Text="Delete"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>

View File

@@ -12,15 +12,15 @@
<Design.DataContext>
<vm:BudgetViewModel />
</Design.DataContext>
<!-- ═══════════════════════════════════════════════════
<!--
ROOT
═══════════════════════════════════════════════════ -->
-->
<Grid RowDefinitions="Auto,*"
Margin="32,28,32,0">
<!-- ══════════════════════════════════════════
<!--
TOP BAR
══════════════════════════════════════════ -->
-->
<Grid Grid.Row="0"
ColumnDefinitions="*,Auto"
Margin="0,0,0,24">
@@ -100,16 +100,16 @@
</StackPanel>
</Grid>
<!-- ══════════════════════════════════════════
<!--
MAIN CONTENT
Left * — budget categories
Right 320 — monthly overview panel
══════════════════════════════════════════ -->
-->
<Grid Grid.Row="1" ColumnDefinitions="*,320">
<!-- ─────────────────────────────────────
<!--
LEFT — Budget Categories
───────────────────────────────────── -->
-->
<ScrollViewer Grid.Column="0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
@@ -241,15 +241,15 @@
</ItemsControl>
</ScrollViewer>
<!-- ─────────────────────────────────────
<!--
RIGHT — Overview Panel
───────────────────────────────────── -->
-->
<ScrollViewer Grid.Column="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled" Margin="0,0,0,0" Padding="0 0 8 0">
<StackPanel Spacing="14" Margin="0 0 0 28">
<!-- ── Period Overview ───────────────── -->
<!-- Period Overview -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -363,7 +363,7 @@
</StackPanel>
</Border>
<!-- ── Days remaining in period ───────── -->
<!-- Days remaining in period -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -421,7 +421,7 @@
</StackPanel>
</Border>
<!-- ── Spending Breakdown ─────────────── -->
<!-- Spending Breakdown -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
@@ -459,84 +459,84 @@
<!-- Row: Food -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource IconBgGreen}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Food &amp; Dining" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$340" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="21%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource IconBgGreen}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Food &amp; Dining" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$340" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="21%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
<!-- Row: Housing -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentOrange}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Housing" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$540" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="34%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentOrange}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Housing" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$540" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="34%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
<!-- Row: Transport -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentBlue}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Transport" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$110" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="7%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentBlue}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Transport" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$110" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="7%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
<!-- Row: Shopping (over) -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentRed}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Shopping" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$380" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="24%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentRed}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Shopping" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$380" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="24%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
<!-- Row: Leisure -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPurple}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Entertainment" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$170" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="11%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPurple}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Entertainment" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$170" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="11%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
<!-- Row: Health -->
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPink}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Health" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$69" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="4%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPink}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
<!-- VerticalAlignment="Center" /> -->
<!-- <TextBlock Grid.Column="1" Text="Health" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
<!-- <TextBlock Text="$69" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Right" /> -->
<!-- <TextBlock Text="4%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
<!-- </StackPanel> -->
<!-- </Grid> -->
</StackPanel>
</Border>
<!-- ── Savings Goal ───────────────────── -->
<!-- Savings Goal -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"

View File

@@ -11,11 +11,11 @@
<vm:CategoryFormViewModel />
</Design.DataContext>
<!-- ── Dim overlay ───────────────────────── -->
<!-- Dim overlay -->
<Grid>
<Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── -->
<!-- Modal card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
@@ -27,7 +27,7 @@
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- ── Header ──────────────────────── -->
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0"
CornerRadius="10"
@@ -64,7 +64,7 @@
</Button>
</Grid>
<!-- ── Name ──────────────────────── -->
<!-- Name -->
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Name, Mode=TwoWay}"
Watermark="e.g. Groceries"
@@ -74,7 +74,7 @@
VerticalContentAlignment="Center"
Margin="0,0,0,16" />
<!-- ── Type toggle ─────────────────── -->
<!-- Type toggle -->
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -116,7 +116,7 @@
</Grid>
</Border>
<!-- ── Icon picker ─────────────────── -->
<!-- Icon picker -->
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -161,7 +161,7 @@
</ScrollViewer>
</Border>
<!-- ── Color ──────────────────────── -->
<!-- Color -->
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -180,7 +180,7 @@
IsAccentColorsVisible="False" />
</Border>
<!-- ── Validation error ─────────────── -->
<!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -199,7 +199,7 @@
</StackPanel>
</Border>
<!-- ── Delete button (edit mode only) ── -->
<!-- Delete button (edit mode only) -->
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
@@ -215,7 +215,7 @@
</StackPanel>
</Button>
<!-- ── Actions ──────────────────────── -->
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
@@ -248,7 +248,7 @@
</StackPanel>
</Border>
<!-- ── Delete confirm sub-modal ──────────────── -->
<!-- Delete confirm sub-modal -->
<Grid IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"
@@ -262,7 +262,7 @@
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<Border Background="#2A0D0D"
<Border Background="{DynamicResource IconBgRed}"
CornerRadius="14"
Width="52" Height="52"
HorizontalAlignment="Center"
@@ -300,7 +300,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}">
@@ -311,7 +311,7 @@
<TextBlock Text="Delete"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>

View File

@@ -0,0 +1,304 @@
<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="1180" d:DesignHeight="800"
MinWidth="780" MinHeight="600"
x:CompileBindings="False"
x:Class="Clario.Views.DashboardSkeletonView">
<UserControl.Styles>
<Style Selector="Border.skeleton">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Style.Animations>
<Animation Duration="0:0:0.85" IterationCount="INFINITE" PlaybackDirection="Alternate" FillMode="Both">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.35" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="*">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="24" Margin="32,28,32,32">
<!-- Top Bar -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="8">
<Border Classes="skeleton" Height="13" Width="155" HorizontalAlignment="Left" />
<Border Classes="skeleton" Height="26" Width="210" HorizontalAlignment="Left" CornerRadius="8" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="142" Height="36"
CornerRadius="{StaticResource RadiusControl}" VerticalAlignment="Center" />
</Grid>
<!-- KPI Cards Row -->
<Grid ColumnDefinitions="*,*,*" MaxHeight="160">
<Grid.Styles>
<Style Selector="Grid > Border.card">
<Setter Property="Margin" Value="0,0,16,0" />
</Style>
</Grid.Styles>
<!-- Monthly Income -->
<Border Grid.Column="0" Classes="card">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="11" Width="105" VerticalAlignment="Center" />
</StackPanel>
<Border Classes="skeleton" Height="30" Width="130" CornerRadius="8" />
<Border Classes="skeleton" Height="22" Width="80" CornerRadius="20" />
</StackPanel>
</Border>
<!-- Monthly Expenses -->
<Border Grid.Column="1" Classes="card">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="11" Width="125" VerticalAlignment="Center" />
</StackPanel>
<Border Classes="skeleton" Height="30" Width="115" CornerRadius="8" />
<Border Classes="skeleton" Height="22" Width="80" CornerRadius="20" />
</StackPanel>
</Border>
<!-- Savings Rate -->
<Border Grid.Column="2" Classes="card" Margin="0">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
<Border Classes="skeleton" Height="11" Width="90" VerticalAlignment="Center" />
</StackPanel>
<Border Classes="skeleton" Height="30" Width="80" CornerRadius="8" />
<Border Classes="skeleton" Height="7" HorizontalAlignment="Stretch" CornerRadius="4" />
</StackPanel>
</Border>
</Grid>
<!-- Mid Row: Chart + Budget -->
<Grid ColumnDefinitions="*,340" MaxHeight="470">
<!-- Spending Chart -->
<Border Grid.Column="0" Classes="card" Margin="0,0,16,0">
<StackPanel Spacing="20">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="7">
<Border Classes="skeleton" Height="15" Width="175" />
<Border Classes="skeleton" Height="12" Width="120" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="105" Height="32"
CornerRadius="{StaticResource RadiusIcon}" VerticalAlignment="Center" />
</Grid>
<!-- Chart area -->
<Border Classes="skeleton" Height="250" HorizontalAlignment="Stretch" CornerRadius="8" />
<!-- Category labels row -->
<Grid ColumnDefinitions="*,*,*,*,*" Margin="0,-10,0,0">
<Border Grid.Column="0" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="1" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="2" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="3" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
<Border Grid.Column="4" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
</Grid>
<!-- Amount labels row -->
<Grid ColumnDefinitions="*,*,*,*,*" Margin="0,-8,0,0">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="36" HorizontalAlignment="Center" />
<Border Grid.Column="1" Classes="skeleton" Height="13" Width="40" HorizontalAlignment="Center" />
<Border Grid.Column="2" Classes="skeleton" Height="13" Width="32" HorizontalAlignment="Center" />
<Border Grid.Column="3" Classes="skeleton" Height="13" Width="38" HorizontalAlignment="Center" />
<Border Grid.Column="4" Classes="skeleton" Height="13" Width="34" HorizontalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<!-- Budget Tracker -->
<Border Grid.Column="1" Classes="card">
<StackPanel Spacing="20">
<StackPanel Spacing="7">
<Border Classes="skeleton" Height="15" Width="115" />
<Border Classes="skeleton" Height="12" Width="85" />
</StackPanel>
<!-- Budget items -->
<StackPanel Spacing="22">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="95" />
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="70" />
</Grid>
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
<Separator Margin="-8,4" />
</StackPanel>
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="72" />
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="62" />
</Grid>
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
<Separator Margin="-8,4" />
</StackPanel>
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="105" />
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="67" />
</Grid>
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
<Separator Margin="-8,4" />
</StackPanel>
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="82" />
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="74" />
</Grid>
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</Grid>
<!-- Bottom Row: Transactions + Accounts -->
<Grid ColumnDefinitions="*,300" MaxHeight="500">
<!-- Recent Transactions -->
<Border Grid.Column="0" Classes="card" Margin="0,0,16,0">
<StackPanel Spacing="18">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="7">
<Border Classes="skeleton" Height="15" Width="160" />
<Border Classes="skeleton" Height="12" Width="115" />
</StackPanel>
<Border Grid.Column="1" Classes="skeleton" Width="58" Height="13" VerticalAlignment="Center" />
</Grid>
<!-- Transaction rows -->
<StackPanel Spacing="18">
<StackPanel Spacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
<Border Classes="skeleton" Height="13" Width="145" />
<Border Classes="skeleton" Height="11" Width="105" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="14" VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
<StackPanel Spacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
<Border Classes="skeleton" Height="13" Width="180" />
<Border Classes="skeleton" Height="11" Width="125" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="55" Height="14" VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
<StackPanel Spacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
<Border Classes="skeleton" Height="13" Width="120" />
<Border Classes="skeleton" Height="11" Width="88" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="70" Height="14" VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
<StackPanel Spacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
<Border Classes="skeleton" Height="13" Width="162" />
<Border Classes="skeleton" Height="11" Width="112" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="60" Height="14" VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
<StackPanel Spacing="0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
<Border Classes="skeleton" Height="13" Width="150" />
<Border Classes="skeleton" Height="11" Width="98" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="65" Height="14" VerticalAlignment="Center" />
</Grid>
<Separator />
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
<!-- Accounts Summary -->
<Border Grid.Column="1" Classes="card">
<Grid RowDefinitions="Auto,*,Auto" RowSpacing="18">
<StackPanel Grid.Row="0" Spacing="7">
<Border Classes="skeleton" Height="15" Width="78" />
<Border Classes="skeleton" Height="12" Width="135" />
</StackPanel>
<StackPanel Grid.Row="1" Spacing="10">
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="92" />
<Border Classes="skeleton" Height="11" Width="60" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="58" Height="13" VerticalAlignment="Center" />
</Grid>
</Border>
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="72" />
<Border Classes="skeleton" Height="11" Width="52" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="13" VerticalAlignment="Center" />
</Grid>
</Border>
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<Border Classes="skeleton" Height="12" Width="82" />
<Border Classes="skeleton" Height="11" Width="56" />
</StackPanel>
<Border Grid.Column="2" Classes="skeleton" Width="54" Height="13" VerticalAlignment="Center" />
</Grid>
</Border>
</StackPanel>
<StackPanel Spacing="18" Grid.Row="2">
<Separator />
<Grid ColumnDefinitions="*,Auto">
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="82" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="skeleton" Height="18" Width="92" CornerRadius="7" />
</Grid>
</StackPanel>
</Grid>
</Border>
</Grid>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@@ -16,7 +16,7 @@
<Grid ColumnDefinitions="*">
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Name="mainScrollviewer">
<StackPanel Spacing="24" Margin="32,28,32,32">
<!-- Top Bar -->
<!-- Top Bar -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<!-- <TextBlock Classes="muted" Text="Friday, March 6, 2026" /> -->
@@ -30,7 +30,7 @@
Cursor="Hand" Content="+ Add Transaction" Command="{Binding CreateTransactionCommand}" />
</StackPanel>
</Grid>
<!-- KPI Cards Row -->
<!-- KPI Cards Row -->
<Grid ColumnDefinitions="*,*,*" HorizontalAlignment="Stretch" MaxHeight="160">
<Grid.Styles>
<Style Selector="Grid > Border">
@@ -109,7 +109,7 @@
</StackPanel>
</Border>
</Grid>
<!-- Mid Row: Spending Chart + Budget -->
<!-- Mid Row: Spending Chart + Budget -->
<Grid ColumnDefinitions="*,340" MaxHeight="470">
<!-- Spending Breakdown -->
@@ -258,7 +258,7 @@
</ScrollViewer>
</Border>
</Grid>
<!-- Bottom Row: Recent Transactions + Accounts -->
<!-- Bottom Row: Recent Transactions + Accounts -->
<Grid ColumnDefinitions="*,300" MaxHeight="500">
<!-- Recent Transactions -->

View File

@@ -14,9 +14,9 @@
<!-- Dim overlay -->
<Border Background="#70000000"/>
<!-- ═══════════════════════════════════════
<!--
STEP 1 — Simple confirm (no transactions)
═══════════════════════════════════════ -->
-->
<Border IsVisible="{Binding IsSimpleConfirmStep}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -30,7 +30,7 @@
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="#2A0D0D"
<Border Background="{DynamicResource IconBgRed}"
CornerRadius="14"
Width="54" Height="54"
HorizontalAlignment="Center"
@@ -99,7 +99,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}">
@@ -110,7 +110,7 @@
<TextBlock Text="Delete Account"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
@@ -119,9 +119,9 @@
</StackPanel>
</Border>
<!-- ═══════════════════════════════════════
<!--
STEP 1B — Has transactions warning
═══════════════════════════════════════ -->
-->
<Border IsVisible="{Binding IsHasTransactionsStep}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -236,9 +236,9 @@
</StackPanel>
</Border>
<!-- ═══════════════════════════════════════
<!--
STEP 2 — Pick target account + confirm
═══════════════════════════════════════ -->
-->
<Border IsVisible="{Binding IsMigrateStep}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -470,7 +470,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
IsEnabled="{Binding CanMigrateAndDelete}"
@@ -482,7 +482,7 @@
<TextBlock Text="Migrate &amp; Delete"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>

View File

@@ -3,12 +3,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaProgressRing="clr-namespace:AvaloniaProgressRing;assembly=AvaloniaProgressRing"
xmlns:vm="clr-namespace:Clario.ViewModels"
Background="{DynamicResource BgBase}"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:LoadingViewModel"
x:Class="Clario.Views.LoadingView">
<Panel>
<avaloniaProgressRing:ProgressRing Width="100" Height="100" IsActive="True"
Foreground="{DynamicResource AccentBlue}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Panel>
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,*,*">
<avaloniaProgressRing:ProgressRing Grid.Row="1" Grid.Column="1"
Width="100" Height="100"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsActive="True"
Foreground="{DynamicResource AccentBlue}" />
</Grid>
</UserControl>

View File

@@ -13,7 +13,7 @@
<vm:MainViewModel />
</Design.DataContext>
<Grid ColumnDefinitions="220,*">
<!-- ───────────────────────────────────── SIDEBAR ───────────────────────────────────── -->
<!-- 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>
@@ -49,9 +49,9 @@
Foreground="{DynamicResource TextSecondary}" />
</Grid>
<!-- <Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Command="{Binding SignOutCommand}"> -->
<!-- <ToolTip.Tip> -->
<!-- signout -->
<!-- </ToolTip.Tip> -->
<!-- <ToolTip.Tip> -->
<!-- signout -->
<!-- </ToolTip.Tip> -->
<!-- </Button> -->
</Grid>
</Border>
@@ -122,7 +122,9 @@
<views:CategoryFormView
DataContext="{Binding CategoryFormViewModel}"
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
<views:MessageBoxView
DataContext="{Binding MessageBoxViewModel}"
IsVisible="{Binding DataContext.IsMessageBoxVisible, ElementName=MainControl}" />
</Grid>
</Grid>

View File

@@ -0,0 +1,92 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Clario.ViewModels"
x:Class="Clario.Views.MessageBoxView"
x:DataType="vm:MessageBoxViewModel">
<Grid>
<!-- Dim overlay -->
<Border Background="#70000000" />
<!-- Card -->
<Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{StaticResource RadiusCard}"
Padding="28,24"
MaxWidth="400"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel Spacing="20">
<!-- Icon + Title row -->
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Error icon -->
<Border Grid.Column="0"
Background="{DynamicResource IconBgRed}"
CornerRadius="{StaticResource RadiusIcon}"
Width="40" Height="40" Margin="0,0,14,0"
IsVisible="{Binding IsError}">
<Svg Path="../Assets/Icons/circle-alert.svg" Width="18" Height="18" Css="{DynamicResource SvgRed}" />
</Border>
<!-- Warning icon -->
<Border Grid.Column="0"
Background="{DynamicResource IconBgOrange}"
CornerRadius="{StaticResource RadiusIcon}"
Width="40" Height="40" Margin="0,0,14,0"
IsVisible="{Binding IsWarning}">
<Svg Path="../Assets/Icons/triangle-alert.svg" Width="18" Height="18" Css="{DynamicResource SvgYellow}" />
</Border>
<!-- Success icon -->
<Border Grid.Column="0"
Background="{DynamicResource IconBgGreen}"
CornerRadius="{StaticResource RadiusIcon}"
Width="40" Height="40" Margin="0,0,14,0"
IsVisible="{Binding IsSuccess}">
<Svg Path="../Assets/Icons/circle-check.svg" Width="18" Height="18" Css="{DynamicResource SvgGreen}" />
</Border>
<!-- Info icon -->
<Border Grid.Column="0"
Background="{DynamicResource IconBgBlue}"
CornerRadius="{StaticResource RadiusIcon}"
Width="40" Height="40" Margin="0,0,14,0"
IsVisible="{Binding IsInfo}">
<Svg Path="../Assets/Icons/info.svg" Width="18" Height="18" Css="{DynamicResource SvgBlue}" />
</Border>
<TextBlock Grid.Column="1"
Text="{Binding Title}"
FontSize="15"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}"
VerticalAlignment="Center"
TextWrapping="Wrap" />
<Button Grid.Column="2"
Background="Transparent"
BorderThickness="0"
Padding="6"
VerticalAlignment="Top"
Cursor="Hand"
Command="{Binding CloseCommand}">
<Svg Path="../Assets/Icons/x.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
</Button>
</Grid>
<!-- Message text -->
<TextBlock Text="{Binding Message}"
FontSize="13"
Foreground="{DynamicResource TextSecondary}"
TextWrapping="Wrap"
LineHeight="20" />
<!-- OK button -->
<Button Classes="accented"
HorizontalAlignment="Right"
Padding="20,9"
Command="{Binding CloseCommand}"
Content="OK" />
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,233 @@
<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"
mc:Ignorable="d" d:DesignWidth="1400" d:DesignHeight="1200"
x:DataType="vm:ResetPasswordViewModel"
x:Class="Clario.Views.ResetPasswordView">
<Grid>
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="20"
Padding="40"
Width="420"
BoxShadow="0 24 64 0 #40000000">
<StackPanel Spacing="0">
<!-- Logo -->
<Border CornerRadius="16"
Height="80"
HorizontalAlignment="Center"
Margin="0,0,0,10">
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
</Border>
<!-- Title -->
<TextBlock Text="Set new password"
FontSize="18" FontWeight="Bold"
Foreground="{DynamicResource TextPrimary}"
HorizontalAlignment="Center"
Margin="0,0,0,6" />
<TextBlock Text="Enter and confirm your new password below."
FontSize="12"
Foreground="{DynamicResource TextMuted}"
HorizontalAlignment="Center"
Margin="0,0,0,28" />
<!-- SUCCESS STATE -->
<StackPanel IsVisible="{Binding PasswordUpdated}" Spacing="16">
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-check.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
<TextBlock Text="Password updated successfully."
FontSize="12"
Foreground="{DynamicResource AccentGreen}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,12"
Command="{Binding GoToSignInCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/log-in.svg"
Width="15" Height="15"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Sign In"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- FORM STATE -->
<StackPanel IsVisible="{Binding !PasswordUpdated}" Spacing="0">
<!-- New Password -->
<TextBlock Text="NEW PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,14">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="15" Height="15"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="12,0,10,0" />
<TextBox Grid.Column="1" Classes="ghost"
Watermark="At least 6 characters"
Text="{Binding NewPassword}"
PasswordChar="●"
RevealPassword="{Binding #showNew.IsChecked}"
BorderThickness="0"
FontSize="13"
Foreground="{DynamicResource TextPrimary}"
Height="42"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2" Name="showNew"
Background="Transparent"
CornerRadius="{DynamicResource RadiusControl}"
BorderThickness="0" Height="42"
Padding="8,0"
Cursor="Hand"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="15" Height="15"
IsVisible="{Binding #showNew.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="15" Height="15"
IsVisible="{Binding !#showNew.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Confirm Password -->
<TextBlock Text="CONFIRM PASSWORD" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}"
Padding="0"
Margin="0,0,0,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<Svg Grid.Column="0"
Path="../Assets/Icons/lock.svg"
Width="15" Height="15"
Css="{DynamicResource SvgMuted}"
VerticalAlignment="Center"
Margin="12,0,10,0" />
<TextBox Grid.Column="1" Classes="ghost"
Watermark="Repeat your password"
Text="{Binding ConfirmPassword}"
PasswordChar="●"
RevealPassword="{Binding #showConfirm.IsChecked}"
BorderThickness="0"
FontSize="13"
Foreground="{DynamicResource TextPrimary}"
Height="42"
Padding="0"
VerticalContentAlignment="Center" />
<ToggleButton Grid.Column="2" Name="showConfirm"
Background="Transparent"
CornerRadius="{DynamicResource RadiusControl}"
BorderThickness="0" Height="42"
Padding="8,0"
Cursor="Hand"
VerticalAlignment="Center">
<ToggleButton.Styles>
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ToggleButton.Styles>
<Panel>
<Svg Path="../Assets/Icons/eye.svg"
Width="15" Height="15"
IsVisible="{Binding #showConfirm.IsChecked}"
Css="{DynamicResource SvgMuted}" />
<Svg Path="../Assets/Icons/eye-closed.svg"
Width="15" Height="15"
IsVisible="{Binding !#showConfirm.IsChecked}"
Css="{DynamicResource SvgMuted}" />
</Panel>
</ToggleButton>
</Grid>
</Border>
<!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
CornerRadius="10"
Padding="12,10"
Margin="0,0,0,16"
IsVisible="{Binding HasError}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/circle-alert.svg"
Width="14" Height="14"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
<TextBlock Text="{Binding ErrorMessage}"
FontSize="12"
Foreground="{DynamicResource AccentRed}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Update password button -->
<Button Classes="accented"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,12"
Margin="0,0,0,20"
Command="{Binding SetNewPasswordCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Svg Path="../Assets/Icons/lock.svg"
Width="15" Height="15"
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
<TextBlock Text="Update Password"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- Footer -->
<Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely."
FontSize="11"
Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

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

View File

@@ -17,7 +17,7 @@
Spacing="0"
MaxWidth="720">
<!-- ── Page header ─────────────────────────── -->
<!-- Page header -->
<StackPanel Margin="0,0,0,28">
<TextBlock Text="Settings"
FontSize="26"
@@ -29,7 +29,7 @@
Margin="0,4,0,0" />
</StackPanel>
<!-- ── Global success / error banner ─────────── -->
<!-- Global success / error banner -->
<Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1"
@@ -75,9 +75,9 @@
</Grid>
</Border>
<!-- ══════════════════════════════════════════════
<!--
SECTION: Profile
══════════════════════════════════════════════ -->
-->
<TextBlock Text="PROFILE"
Classes="label"
Margin="0,0,0,10" />
@@ -223,9 +223,9 @@
</StackPanel>
</Border>
<!-- ══════════════════════════════════════════════
<!--
SECTION: Account Security
══════════════════════════════════════════════ -->
-->
<TextBlock Text="ACCOUNT &amp; SECURITY"
Classes="label"
Margin="0,0,0,10" />
@@ -238,7 +238,7 @@
Margin="0,0,0,24">
<StackPanel Spacing="0">
<!-- ── Email row ───────────────────────────── -->
<!-- Email row -->
<Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1"
Padding="20,0">
@@ -350,7 +350,7 @@
</Panel>
</Border>
<!-- ── Password row ───────────────────────── -->
<!-- Password row -->
<Border Padding="20,0">
<!-- Normal password display -->
<Panel>
@@ -473,9 +473,9 @@
</StackPanel>
</Border>
<!-- ══════════════════════════════════════════════
<!--
SECTION: Danger zone
══════════════════════════════════════════════ -->
-->
<TextBlock Text="SESSION"
Classes="label"
Margin="0,0,0,10" />

View File

@@ -12,11 +12,11 @@
<vm:TransactionFormViewModel />
</Design.DataContext>
<!-- ── Dim overlay ───────────────────────── -->
<!-- Dim overlay -->
<Grid>
<Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── -->
<!-- Modal card -->
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource BgSurface}"
@@ -28,7 +28,7 @@
BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0">
<!-- ── Header ──────────────────────── -->
<!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0"
CornerRadius="10"
@@ -66,7 +66,7 @@
</Button>
</Grid>
<!-- ── Type toggle ─────────────────── -->
<!-- Type toggle -->
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -136,7 +136,7 @@
</Grid>
</Border>
<!-- ── Amount ──────────────────────── -->
<!-- Amount -->
<TextBlock Text="AMOUNT" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -174,7 +174,7 @@
</Grid>
</Border>
<!-- ── Description (hidden for transfers) ─── -->
<!-- Description (hidden for transfers) -->
<TextBlock Text="DESCRIPTION" Classes="label" Margin="0,0,0,6"
IsVisible="{Binding !IsTransfer}" />
<TextBox Text="{Binding Description, Mode=TwoWay}"
@@ -186,7 +186,7 @@
Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}" />
<!-- ── Category + Account (income/expense) ── -->
<!-- Category + Account (income/expense) -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
IsVisible="{Binding !IsTransfer}">
@@ -282,7 +282,7 @@
</Grid>
<!-- ── From + To accounts (transfer) ── -->
<!-- From + To accounts (transfer) -->
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
IsVisible="{Binding IsTransfer}">
@@ -366,7 +366,7 @@
</Grid>
<!-- ── Exchange Rate (shown for foreign-currency accounts) ── -->
<!-- Exchange Rate (shown for foreign-currency accounts) -->
<Border IsVisible="{Binding ShowExchangeRateField}"
Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -427,7 +427,7 @@
</StackPanel>
</Border>
<!-- ── Date ────────────────────────── -->
<!-- Date -->
<TextBlock Text="DATE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}"
@@ -456,7 +456,7 @@
</Grid>
</Border>
<!-- ── Note ────────────────────────── -->
<!-- Note -->
<TextBlock Text="NOTE (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Note, Mode=TwoWay}"
Watermark="Add a note..."
@@ -466,7 +466,7 @@
VerticalContentAlignment="Center"
Margin="0,0,0,8" />
<!-- ── Budget approaching warning ─────── -->
<!-- Budget approaching warning -->
<Border Background="{DynamicResource BadgeBgYellow}"
BorderBrush="{DynamicResource AccentYellow}"
BorderThickness="1"
@@ -486,7 +486,7 @@
</StackPanel>
</Border>
<!-- ── Budget over-limit warning ──────── -->
<!-- Budget over-limit warning -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -506,7 +506,7 @@
</StackPanel>
</Border>
<!-- ── Validation error ─────────────── -->
<!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1"
@@ -525,7 +525,7 @@
</StackPanel>
</Border>
<!-- ── Delete button (edit mode only) ── -->
<!-- Delete button (edit mode only) -->
<Button Classes="danger"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
@@ -539,7 +539,7 @@
</StackPanel>
</Button>
<!-- ── Actions ──────────────────────── -->
<!-- Actions -->
<UniformGrid Rows="1">
<Button Classes="base"
Margin="0,0,6,0"
@@ -573,7 +573,7 @@
</Border>
<!-- DELETE CONFIRM MODAL -->
<!-- ── Delete confirm modal ──────────────── -->
<!-- Delete confirm modal -->
<Grid IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" />
<Border HorizontalAlignment="Center"
@@ -588,7 +588,7 @@
<StackPanel Spacing="0">
<!-- Icon -->
<Border Background="#2A0D0D"
<Border Background="{DynamicResource IconBgRed}"
CornerRadius="14"
Width="52" Height="52"
HorizontalAlignment="Center"
@@ -629,7 +629,7 @@
Padding="0,11"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Background="#FF5E5E"
Background="{DynamicResource AccentRed}"
BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}">
@@ -640,7 +640,7 @@
<TextBlock Text="Delete"
FontSize="13"
FontWeight="SemiBold"
Foreground="#FFFFFF"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" />
</StackPanel>
</Button>

View File

@@ -14,9 +14,7 @@
</Design.DataContext>
<Grid ColumnDefinitions="Auto,*">
<!-- ═══════════════════════════════════════════════════
LEFT PANEL — Summary + Filters
═══════════════════════════════════════════════════ -->
<!-- LEFT PANEL — Summary + Filters -->
<Border Grid.Column="0"
Width="260"
Background="{DynamicResource BgSurface}"
@@ -28,7 +26,7 @@
Padding="16,0,16,0">
<StackPanel Spacing="0" Margin="0 28 0 28">
<!-- Period header
<!-- Period header
REPLACE: bind TextBlock texts to SelectedPeriodLabel
-->
<TextBlock Text="{Binding DateRangeLabel}"
@@ -40,7 +38,7 @@
Foreground="{DynamicResource TextPrimary}"
Margin="0,0,0,16" />
<!-- Summary stats — left accent bar style ──
<!-- Summary stats — left accent bar style
REPLACE: bind each amount + count Text
-->
<StackPanel Spacing="2">
@@ -145,7 +143,7 @@
<!-- Divider -->
<Separator Margin="0,20,0,20" />
<!-- Filters header + Reset link ──
<!-- Filters header + Reset link
REPLACE: Command="{Binding ResetFiltersCommand}" on the Button
-->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
@@ -165,7 +163,7 @@
Command="{Binding ResetFiltersCommand}" />
</Grid>
<!-- Search ──
<!-- Search
REPLACE: Text="{Binding SearchQuery, Mode=TwoWay}"
-->
<TextBox Watermark="Search transactions..."
@@ -176,7 +174,7 @@
VerticalContentAlignment="Center"
Margin="0,0,0,14" />
<!-- Date range
<!-- Date range
REPLACE: SelectedIndex="{Binding SelectedDateRangeIndex}"
-->
<TextBlock Classes="label" Text="DATE RANGE" Margin="0,0,0,6" />
@@ -276,7 +274,7 @@
Margin="0,0,0,14">
</ComboBox>
<!-- Account
<!-- Account
REPLACE: ItemsSource="{Binding Accounts}" SelectedItem="{Binding SelectedAccount}"
-->
<TextBlock Classes="label" Text="ACCOUNT" Margin="0,0,0,6" />
@@ -319,9 +317,9 @@
</ScrollViewer>
</Border>
<!-- ═══════════════════════════════════════════════════
<!--
RIGHT PANEL — Transaction List
═══════════════════════════════════════════════════ -->
-->
<Grid Grid.Column="1" RowDefinitions="Auto,*">
<!-- Top Bar -->
@@ -352,7 +350,7 @@
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center"/>
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
@@ -488,7 +486,7 @@
<Border CornerRadius="6" Padding="6,3">
<Border.Background>
<SolidColorBrush Opacity="0.15"
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}" />
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}" />
</Border.Background>
<StackPanel Orientation="Horizontal" Spacing="5">
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
@@ -517,7 +515,7 @@
</Border>
</Panel>
<TextBlock Grid.Column="3"
<TextBlock Grid.Column="3"
Text="{Binding AccountDisplayText}"
FontSize="12"
Foreground="{DynamicResource TextMuted}"