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": [ "allow": [
"WebFetch(domain:raw.githubusercontent.com)", "WebFetch(domain:raw.githubusercontent.com)",
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")", "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 "spinnerTipsEnabled": true

3
.gitignore vendored
View File

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

View File

@@ -1,21 +1,50 @@
using Android.App; using System;
using Android.App;
using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS;
using Avalonia; using Avalonia;
using Avalonia.Android; using Avalonia.Android;
using Clario;
namespace Clario.Android; namespace Clario.Android;
[Activity( [Activity(
Label = "Clario.Android", Label = "Clario",
Theme = "@style/MyTheme.NoActionBar", Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon", Icon = "@drawable/icon",
MainLauncher = true, MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] 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> 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) protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{ {
return base.CustomizeAppBuilder(builder) return base.CustomizeAppBuilder(builder)
.WithInterFont(); .WithInterFont();
} }
} }

View File

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

View File

@@ -1,19 +1,25 @@
using System; using System;
using System.Linq;
using Avalonia; using Avalonia;
using Clario;
namespace Clario.Desktop; namespace Clario.Desktop;
sealed class Program 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] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
// Capture deep link passed as command-line arg by Windows protocol handler
BuildAvaloniaApp() var deepLink = args.FirstOrDefault(a =>
.StartWithClassicDesktopLifetime(args); a.StartsWith("clario://", StringComparison.OrdinalIgnoreCase));
if (deepLink != null)
App.PendingDeepLink = deepLink;
// Register clario:// URL scheme on Windows (idempotent)
RegisterUrlScheme();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
} }
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
@@ -22,4 +28,23 @@ sealed class Program
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.LogToTrace(); .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="../Theme/AppTheme.axaml" />
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/> <StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.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.Styles>
</Application> </Application>

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
@@ -17,6 +18,47 @@ public partial class App : Application
{ {
public static bool IsMobile { get; private set; } 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() public override void Initialize()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
@@ -73,19 +115,32 @@ public partial class App : Application
ThemeService.SwitchToTheme(profile.Theme); 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. var (accessToken, refreshToken, _) = ParseDeepLinkFragment(deepLink);
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins if (accessToken is not null)
DisableAvaloniaDataAnnotationValidation(); {
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel(); }
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) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
DebugLogger.Log("ANDROID PATH HIT"); 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"> <Compile Update="Views\AnalyticsView.axaml.cs">
<DependentUpon>AnalyticsView.axaml</DependentUpon> <DependentUpon>AnalyticsView.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\DashboardSkeletonView.axaml.cs">
<DependentUpon>DashboardSkeletonView.axaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'"> <ItemGroup Condition="'$(Configuration)' == 'Debug'">
@@ -57,4 +60,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </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> </Project>

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Clario.Models; using Clario.Models;
using Clario.Models.GeneralModels; using Clario.Models.GeneralModels;
using Clario.Services; using Clario.Services;
@@ -15,6 +16,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Clario.Messages; using Clario.Messages;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Supabase.Postgrest; using Supabase.Postgrest;
using Supabase.Realtime.PostgresChanges;
using Constants = Supabase.Realtime.Constants;
using FileOptions = Supabase.Storage.FileOptions; using FileOptions = Supabase.Storage.FileOptions;
namespace Clario.Data; namespace Clario.Data;
@@ -174,12 +177,14 @@ public partial class GeneralDataRepo : ObservableObject
LinkTransactionAccounts(enriched); LinkTransactionAccounts(enriched);
Transactions.Add(enriched); Transactions.Add(enriched);
} }
if (inResult.Models.Count >= 1) if (inResult.Models.Count >= 1)
{ {
var enriched = LinkTransactionCategories(inResult.Models[0]); var enriched = LinkTransactionCategories(inResult.Models[0]);
LinkTransactionAccounts(enriched); LinkTransactionAccounts(enriched);
Transactions.Add(enriched); Transactions.Add(enriched);
} }
// Re-enrich both so AccountDisplayText can reference the counterpart (from/to) // Re-enrich both so AccountDisplayText can reference the counterpart (from/to)
LinkTransactionAccounts(); LinkTransactionAccounts();
} }
@@ -215,6 +220,7 @@ public partial class GeneralDataRepo : ObservableObject
Transactions[index] = enriched; Transactions[index] = enriched;
} }
} }
LinkTransactionAccounts(); LinkTransactionAccounts();
} }
catch (Exception e) catch (Exception e)
@@ -306,7 +312,7 @@ public partial class GeneralDataRepo : ObservableObject
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList(); if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
var accounts = await SupabaseService.Client.From<Account>().Get(); var accounts = await SupabaseService.Client.From<Account>().Get();
Accounts = new ObservableCollection<Account>(accounts.Models); 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) public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
@@ -719,4 +725,186 @@ public partial class GeneralDataRepo : ObservableObject
if (avatarUrl.StartsWith("http")) return avatarUrl; if (avatarUrl.StartsWith("http")) return avatarUrl;
return $"{PublicBaseUrl}/{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" <Grid RowDefinitions="Auto,*,Auto"
Background="{DynamicResource BgBase}"> Background="{DynamicResource BgBase}">
<!-- ── Top bar ──────────────────────────── --> <!-- Top bar -->
<Grid Grid.Row="0" <Grid Grid.Row="0"
ColumnDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto"
Margin="16,16,16,0"> Margin="16,16,16,0">
@@ -57,7 +57,7 @@
<Border Grid.Column="2" Width="36" /> <Border Grid.Column="2" Width="36" />
</Grid> </Grid>
<!-- ── Scrollable form ───────────────────── --> <!-- Scrollable form -->
<ScrollViewer Grid.Row="1" <ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
@@ -366,7 +366,7 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- ── Bottom action bar ─────────────────── --> <!-- Bottom action bar -->
<Border Grid.Row="2" <Border Grid.Row="2"
Background="{DynamicResource BgSurface}" Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="24,48,24,48" Spacing="0"> <StackPanel Margin="24,48,24,48" Spacing="0">
<!-- ── Logo ──────────────────────────────── --> <!-- Logo -->
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36"> <StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
<Border CornerRadius="16" <Border CornerRadius="16"
Height="80" Height="80"
@@ -31,13 +31,14 @@
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
</StackPanel> </StackPanel>
<!-- ── Tab switcher ───────────────────────── --> <!-- Tab switcher -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}" CornerRadius="{DynamicResource RadiusControl}"
Padding="3" Padding="3"
Margin="0,0,0,28"> Margin="0,0,0,28"
IsVisible="{Binding ShowTabs}">
<Grid ColumnDefinitions="*,*"> <Grid ColumnDefinitions="*,*">
<Button Grid.Column="0" <Button Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@@ -69,7 +70,7 @@
</Grid> </Grid>
</Border> </Border>
<!-- ── Sign In panel ──────────────────────── --> <!-- Sign In panel -->
<StackPanel Spacing="0" IsVisible="{Binding isSignin}"> <StackPanel Spacing="0" IsVisible="{Binding isSignin}">
<!-- Email --> <!-- Email -->
@@ -150,6 +151,20 @@
</Grid> </Grid>
</Border> </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 --> <!-- Error banner -->
<Border Background="{DynamicResource BadgeBgRed}" <Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}" BorderBrush="{DynamicResource AccentRed}"
@@ -191,7 +206,7 @@
</StackPanel> </StackPanel>
<!-- ── Create Account panel ───────────────── --> <!-- Create Account panel -->
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}"> <StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
<!-- First / Last name --> <!-- First / Last name -->
@@ -374,7 +389,127 @@
</StackPanel> </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" /> <Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely." <TextBlock Text="Your data is encrypted and synced securely."
FontSize="12" FontSize="12"

View File

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

View File

@@ -17,7 +17,7 @@
<Grid RowDefinitions="Auto,*" <Grid RowDefinitions="Auto,*"
Background="{DynamicResource BgBase}"> Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── --> <!-- Top bar -->
<Grid Grid.Row="0" <Grid Grid.Row="0"
ColumnDefinitions="*,Auto" ColumnDefinitions="*,Auto"
Margin="16,16,16,12"> Margin="16,16,16,12">
@@ -74,13 +74,13 @@
</Grid> </Grid>
<!-- ── Scrollable content ────────────────── --> <!-- Scrollable content -->
<ScrollViewer Grid.Row="1" <ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="14"> <StackPanel Margin="16,0,16,24" Spacing="14">
<!-- ── Period overview strip ─────────── --> <!-- Period overview strip -->
<Grid ColumnDefinitions="*,*,*"> <Grid ColumnDefinitions="*,*,*">
<!-- Budgeted --> <!-- Budgeted -->
@@ -154,7 +154,7 @@
</Grid> </Grid>
<!-- ── Overall progress bar ──────────── --> <!-- Overall progress bar -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
@@ -195,7 +195,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Budget cards list ─────────────── --> <!-- Budget cards list -->
<ItemsControl ItemsSource="{Binding VisibleBudgets}"> <ItemsControl ItemsSource="{Binding VisibleBudgets}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
@@ -322,7 +322,7 @@
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<!-- ── Spending breakdown chart ──────── --> <!-- Spending breakdown chart -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
@@ -364,7 +364,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Period progress ───────────────── --> <!-- Period progress -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
@@ -417,7 +417,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Savings goal ──────────────────── --> <!-- Savings goal -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" 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}"> <Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
<!-- ── Top bar ────────────────────────────── --> <!-- Top bar -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12"> <Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
<StackPanel Grid.Column="0"> <StackPanel Grid.Column="0">
<TextBlock Text="Financial Overview" <TextBlock Text="Financial Overview"
@@ -47,13 +47,13 @@
</Button> </Button>
</Grid> </Grid>
<!-- ── Scrollable content ────────────────── --> <!-- Scrollable content -->
<ScrollViewer Grid.Row="1" <ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,24" Spacing="14"> <StackPanel Margin="16,0,16,24" Spacing="14">
<!-- ── KPI cards ──────────────────────── --> <!-- KPI cards -->
<Grid ColumnDefinitions="*,*"> <Grid ColumnDefinitions="*,*">
<!-- Income --> <!-- Income -->
@@ -164,7 +164,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Spending by category chart ────── --> <!-- Spending by category chart -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
@@ -271,7 +271,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Recent transactions ───────────── --> <!-- Recent transactions -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
@@ -370,19 +370,30 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Budget tracker ────────────────── --> <!-- Budget tracker -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
CornerRadius="14" CornerRadius="14"
Padding="14,14"> Padding="14,14">
<StackPanel Spacing="14"> <StackPanel Spacing="14">
<StackPanel> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Budget Tracker" <StackPanel Grid.Column="0">
FontSize="14" FontWeight="SemiBold" <TextBlock Text="Budget Tracker"
Foreground="{DynamicResource TextPrimary}" /> FontSize="14" FontWeight="SemiBold"
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" /> Foreground="{DynamicResource TextPrimary}" />
</StackPanel> <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> <Panel>
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}" <ItemsControl ItemsSource="{Binding BudgetsTrackerData}"
@@ -434,24 +445,29 @@
<!-- Empty state --> <!-- Empty state -->
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Spacing="10" Margin="0,32" Spacing="10" Margin="0,20"
IsVisible="{Binding !HasBudgetData}"> IsVisible="{Binding !HasBudgetData}">
<Svg Path="../Assets/Icons/wallet.svg" Css="{DynamicResource SvgDisabled}" <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" <TextBlock Text="No budgets set"
FontSize="14" FontWeight="SemiBold" FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource TextDisabled}" Foreground="{DynamicResource TextDisabled}"
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
<TextBlock Text="Create budgets to track your spending limits." <Button Classes="accented"
FontSize="12" Foreground="{DynamicResource TextDisabled}" HorizontalAlignment="Center"
HorizontalAlignment="Center" TextWrapping="Wrap" Padding="20,8"
TextAlignment="Center" MaxWidth="200" /> CornerRadius="10"
Command="{Binding OpenAddBudgetCommand}">
<TextBlock Text="Create Budget"
FontSize="13" FontWeight="SemiBold"
Foreground="{DynamicResource BgBase}" />
</Button>
</StackPanel> </StackPanel>
</Panel> </Panel>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Accounts summary ──────────────── --> <!-- Accounts summary -->
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"

View File

@@ -30,11 +30,11 @@
<mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3" <mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
DataContext="{Binding SetSavingsGoalDialogViewModel}" DataContext="{Binding SetSavingsGoalDialogViewModel}"
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" /> IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
<!-- ── Content area ──────────────────────── --> <!-- Content area -->
<ContentControl Grid.Row="0" <ContentControl Grid.Row="0"
Content="{Binding CurrentView}" /> Content="{Binding CurrentView}" />
<!-- ── Bottom tab bar ────────────────────── --> <!-- Bottom tab bar -->
<Border Grid.Row="1" <Border Grid.Row="1"
Background="{DynamicResource BgSurface}" Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -109,19 +109,19 @@
</StackPanel> </StackPanel>
</Button> </Button>
<!-- Budget --> <!-- More -->
<Button Grid.Column="4" <Button Grid.Column="4"
Classes="nav" Classes="nav"
Classes.active="{Binding isOnBudget}" Classes.active="{Binding isOnMore}"
Command="{Binding GoToBudgetCommand}" Command="{Binding GoToMoreCommand}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="0,6"> Padding="0,6">
<StackPanel Spacing="4" HorizontalAlignment="Center"> <StackPanel Spacing="4" HorizontalAlignment="Center">
<Svg Path="../Assets/Icons/wallet.svg" <Svg Path="../Assets/Icons/ellipsis.svg"
Width="22" Height="22" Width="22" Height="22"
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
<TextBlock Text="Budget" <TextBlock Text="More"
FontSize="10" FontSize="10"
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
</StackPanel> </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; namespace Clario.MobileViews;
@@ -8,4 +12,26 @@ public partial class MainViewMobile : UserControl
{ {
InitializeComponent(); 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,*"> <Grid RowDefinitions="Auto,*">
<!-- ── Top bar ────────────────────────────── --> <!-- Top bar -->
<Grid Grid.Row="0" <Grid Grid.Row="0"
ColumnDefinitions="*,Auto" ColumnDefinitions="*,Auto"
Margin="16,16,16,12"> Margin="16,16,16,12">
@@ -26,13 +26,13 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- ── Scrollable content ────────────────── --> <!-- Scrollable content -->
<ScrollViewer Grid.Row="1" <ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="16,0,16,32" Spacing="0"> <StackPanel Margin="16,0,16,32" Spacing="0">
<!-- ── Global success / error banners ── --> <!-- Global success / error banners -->
<Border Background="{DynamicResource IconBgGreen}" <Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}" BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1" BorderThickness="1"
@@ -78,9 +78,9 @@
</Grid> </Grid>
</Border> </Border>
<!-- ══════════════════════════════════════ <!--
SECTION: Profile SECTION: Profile
══════════════════════════════════════ --> -->
<TextBlock Text="PROFILE" Classes="label" Margin="0,0,0,10" /> <TextBlock Text="PROFILE" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -201,9 +201,9 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ══════════════════════════════════════ <!--
SECTION: Account & Security SECTION: Account & Security
══════════════════════════════════════ --> -->
<TextBlock Text="ACCOUNT &amp; SECURITY" Classes="label" Margin="0,0,0,10" /> <TextBlock Text="ACCOUNT &amp; SECURITY" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -213,7 +213,7 @@
Margin="0,0,0,20"> Margin="0,0,0,20">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<!-- ── Email row ──────────────────────── --> <!-- Email row -->
<Border BorderBrush="{DynamicResource BorderSubtle}" <Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Padding="16,0"> Padding="16,0">
@@ -323,7 +323,7 @@
</Panel> </Panel>
</Border> </Border>
<!-- ── Password row ───────────────────── --> <!-- Password row -->
<Border Padding="16,0"> <Border Padding="16,0">
<Panel> <Panel>
<!-- Display row --> <!-- Display row -->
@@ -445,9 +445,9 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ══════════════════════════════════════ <!--
SECTION: Session SECTION: Session
══════════════════════════════════════ --> -->
<TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" /> <TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" />
<Border Background="{DynamicResource BgSurface}" <Border Background="{DynamicResource BgSurface}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ public class Budget : BaseModel
[Column("created_at")] public DateTime CreatedAt { get; set; } [Column("created_at")] public DateTime CreatedAt { get; set; }
// ── not in DB ────────────────────────────────────── // not in DB
[JsonIgnore] public Category? Category { get; set; } [JsonIgnore] public Category? Category { get; set; }
[JsonIgnore] public int TransactionsCount { 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 public static class PdfExportService
{ {
// ── Print palette (readable on white paper) ─────────── // Print palette (readable on white paper)
private const string TextPrimary = "#111827"; private const string TextPrimary = "#111827";
private const string TextSecondary = "#374151"; private const string TextSecondary = "#374151";
private const string TextMuted = "#6B7280"; private const string TextMuted = "#6B7280";
@@ -80,7 +80,7 @@ public static class PdfExportService
page.MarginVertical(1.5f, Unit.Centimetre); page.MarginVertical(1.5f, Unit.Centimetre);
page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial")); page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial"));
// ── Header ──────────────────────────────────── // Header
page.Header().Column(col => page.Header().Column(col =>
{ {
col.Item().Row(row => col.Item().Row(row =>
@@ -103,10 +103,10 @@ public static class PdfExportService
col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar); col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar);
}); });
// ── Content ─────────────────────────────────── // Content
page.Content().PaddingTop(18).Column(col => page.Content().PaddingTop(18).Column(col =>
{ {
// KPI cards ───────────────────────────── // KPI cards
col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary); col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table => col.Item().PaddingTop(6).Table(table =>
{ {
@@ -141,7 +141,7 @@ public static class PdfExportService
col.Item().Height(20); col.Item().Height(20);
// Top categories ───────────────────────── // Top categories
if (topCategories.Count > 0) if (topCategories.Count > 0)
{ {
col.Item().Text("Top Spending Categories") col.Item().Text("Top Spending Categories")
@@ -189,7 +189,7 @@ public static class PdfExportService
col.Item().Height(20); col.Item().Height(20);
} }
// Transactions ─────────────────────────── // Transactions
col.Item().Text($"Transactions ({periodTxs.Count})") col.Item().Text($"Transactions ({periodTxs.Count})")
.FontSize(11).SemiBold().FontColor(TextPrimary); .FontSize(11).SemiBold().FontColor(TextPrimary);
col.Item().PaddingTop(6).Table(table => 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 => page.Footer().PaddingTop(6).BorderTop(1).BorderColor(Border).Row(row =>
{ {
row.RelativeItem().Text("Generated by Clario — Your personal finance tracker") 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="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> <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 --> <!-- Toggle Switch specific -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#E2E4ED" /> <SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#E2E4ED" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#C8CCDE" /> <SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#C8CCDE" />
@@ -249,10 +263,9 @@
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" />
<Bitmap x:Key="key">path</Bitmap>
<!-- logos --> <!-- logos -->
<!-- Icon only, light bg --> <!-- Icon only, light bg -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-light.svg</x:String> <x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-128.png</Bitmap> <Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-128.png</Bitmap>
@@ -606,7 +619,7 @@
</Style> </Style>
<Style Selector="Border.inset"> <Style Selector="Border.inset">
<Setter Property="Background" Value="{DynamicResource BgElevated}" /> <Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="CornerRadius" Value="{DynamicResource RadiusInset}" /> <Setter Property="CornerRadius" Value="{DynamicResource RadiusInset}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" /> <Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
@@ -1092,11 +1105,11 @@
<Style Selector="TextBox:disabled"> <Style Selector="TextBox:disabled">
<Setter Property="Opacity" Value="0.5" /> <Setter Property="Opacity" Value="0.5" />
<Setter Property="Foreground" Value="{DynamicResource TextDisabled}" /> <Setter Property="Foreground" Value="{DynamicResource TextDisabled}" />
<Setter Property="Background" Value="{DynamicResource BgElevated}" /> <Setter Property="Background" Value="{DynamicResource BgBase}" />
</Style> </Style>
<Style Selector="TextBox:readonly"> <Style Selector="TextBox:readonly">
<Setter Property="Background" Value="{DynamicResource BgElevated}" /> <Setter Property="Background" Value="{DynamicResource BgBase}" />
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" /> <Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
</Style> </Style>
<Style Selector="TextBox:readonly /template/ Border#PART_BorderElement"> <Style Selector="TextBox:readonly /template/ Border#PART_BorderElement">
@@ -1181,7 +1194,7 @@
</Style> </Style>
<Style Selector="ComboBox:disabled /template/ Border#Background"> <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="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="Opacity" Value="0.5" /> <Setter Property="Opacity" Value="0.5" />
</Style> </Style>
@@ -1422,12 +1435,12 @@
</Style> </Style>
<Style Selector="cc|DateRangePicker:disabled /template/ Button#PART_Button /template/ ContentPresenter"> <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="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="Opacity" Value="0.5" /> <Setter Property="Opacity" Value="0.5" />
</Style> </Style>
<!-- ── Budget Card — On Track ─────────────────────── --> <!-- Budget Card — On Track -->
<Style Selector="Border.budget-card"> <Style Selector="Border.budget-card">
<Setter Property="Background" Value="{DynamicResource BgSurface}" /> <Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" /> <Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
@@ -1437,7 +1450,7 @@
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
</Style> </Style>
<!-- ── Budget Card — Warning ──────────────────────── --> <!-- Budget Card — Warning -->
<Style Selector="Border.budget-card-warning"> <Style Selector="Border.budget-card-warning">
<Setter Property="Background" Value="{DynamicResource BgSurface}" /> <Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" /> <Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" />
@@ -1447,7 +1460,7 @@
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
</Style> </Style>
<!-- ── Budget Card — Over Budget ─────────────────── --> <!-- Budget Card — Over Budget -->
<Style Selector="Border.budget-card-over"> <Style Selector="Border.budget-card-over">
<Setter Property="Background" Value="{DynamicResource BgSurface}" /> <Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" /> <Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" />
@@ -1457,13 +1470,13 @@
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
</Style> </Style>
<!-- ── Progress Bar — Yellow ─────────────────────── --> <!-- Progress Bar — Yellow -->
<Style Selector="ProgressBar.yellow /template/ Border#PART_Indicator"> <Style Selector="ProgressBar.yellow /template/ Border#PART_Indicator">
<Setter Property="Background" Value="{DynamicResource AccentYellow}" /> <Setter Property="Background" Value="{DynamicResource AccentYellow}" />
<Setter Property="CornerRadius" Value="3" /> <Setter Property="CornerRadius" Value="3" />
</Style> </Style>
<!-- ── Badge — Warning ───────────────────────────── --> <!-- Badge — Warning -->
<Style Selector="Border.badge-warning"> <Style Selector="Border.badge-warning">
<Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" /> <Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" /> <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" />
@@ -1471,7 +1484,7 @@
<Setter Property="Padding" Value="6,2" /> <Setter Property="Padding" Value="6,2" />
</Style> </Style>
<!-- ── Badge — Over ──────────────────────────────── --> <!-- Badge — Over -->
<Style Selector="Border.badge-over"> <Style Selector="Border.badge-over">
<Setter Property="Background" Value="{DynamicResource BadgeBgRed}" /> <Setter Property="Background" Value="{DynamicResource BadgeBgRed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" /> <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" />

View File

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

View File

@@ -30,7 +30,7 @@
In WinUI Min-Width from TemplateSettings In WinUI Min-Width from TemplateSettings
basically...MinWidth of DayItem = 40, 40 * 7 = 280 + margins/padding = ~294 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) 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" <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="40,*" MinWidth="294"
Background="{DynamicResource BgSidebar}"> Background="{DynamicResource BgSidebar}">
<Grid ColumnDefinitions="5*,*,*"> <Grid ColumnDefinitions="5*,*,*">

View File

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

View File

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

View File

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

View File

@@ -35,10 +35,15 @@ public partial class AccountsViewModel : ViewModelBase
[ObservableProperty] private List<Account> _archivedAccounts = new(); [ObservableProperty] private List<Account> _archivedAccounts = new();
public bool HasArchivedAccounts => ArchivedAccounts.Count > 0; 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() public AccountsViewModel()
{ {
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); }; Track(AppData.Accounts, (_, _) => Initialize());
AppData.Transactions.CollectionChanged += (_, _) => { Initialize(); }; Track(AppData.Transactions, (_, _) => Initialize());
Initialize(); Initialize();
} }
@@ -184,6 +189,7 @@ public partial class AccountsViewModel : ViewModelBase
{ {
IsDeleteDialogVisible = false; IsDeleteDialogVisible = false;
Initialize(); Initialize();
ShouldCloseSheet = true;
}; };
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false; DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
IsDeleteDialogVisible = true; IsDeleteDialogVisible = true;

View File

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

View File

@@ -24,7 +24,7 @@ public partial class AuthViewModel : ViewModelBase
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))] [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
private string _lastName; private string _lastName;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))] [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
private string _email; private string _email;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))] [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
@@ -34,13 +34,15 @@ public partial class AuthViewModel : ViewModelBase
private string _confirmPassword; private string _confirmPassword;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount))] [NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount), nameof(isForgotPassword), nameof(ShowTabs))]
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
private string _operation = "login"; private string _operation = "login";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage; private string? _errorMessage;
[ObservableProperty] private bool _resetEmailSent;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage); public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public AuthViewModel() public AuthViewModel()
@@ -72,6 +74,30 @@ public partial class AuthViewModel : ViewModelBase
{ {
Operation = operation; Operation = operation;
ErrorMessage = null; 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))] [RelayCommand(CanExecute = nameof(canSignin))]
@@ -185,12 +211,16 @@ public partial class AuthViewModel : ViewModelBase
public bool isSignin => Operation == "login"; public bool isSignin => Operation == "login";
public bool isCreateAccount => Operation == "signup"; 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 canSignin => isSignin && !string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_password);
public bool canCreateAccount => isCreateAccount && !string.IsNullOrWhiteSpace(_firstName) && !string.IsNullOrWhiteSpace(_lastName) && public bool canCreateAccount => isCreateAccount && !string.IsNullOrWhiteSpace(_firstName) && !string.IsNullOrWhiteSpace(_lastName) &&
!string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_email) &&
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword; !string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
public bool canSendResetLink => isForgotPassword && !string.IsNullOrWhiteSpace(_email);
} }
class Wrapper class Wrapper

View File

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

View File

@@ -69,17 +69,20 @@ public partial class BudgetViewModel : ViewModelBase
public BudgetViewModel() public BudgetViewModel()
{ {
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); }; Track(AppData.Budgets, async (_, _) => await Initialize());
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); }; Track(AppData.Transactions, async (_, _) => await Initialize());
AppData.PropertyChanged += (_, e) => AppData.PropertyChanged += OnProfileChanged;
{ OnDispose(() => AppData.PropertyChanged -= OnProfileChanged);
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
};
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize()); WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize());
_ = Initialize(); _ = Initialize();
} }
private void OnProfileChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
}
private async Task Initialize() private async Task Initialize()
{ {
try try
@@ -98,19 +101,19 @@ public partial class BudgetViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void CreateBudget() private void CreateBudget()
{ {
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null); if (parentViewModel is MainViewModel main) main.OpenAddBudgetCommand.Execute(null);
} }
[RelayCommand] [RelayCommand]
private void EditBudget(Budget budget) private void EditBudget(Budget budget)
{ {
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget); if (parentViewModel is MainViewModel main) main.OpenEditBudgetCommand.Execute(budget);
} }
[RelayCommand] [RelayCommand]
private void EditSavingsGoal() private void EditSavingsGoal()
{ {
((MainViewModel)parentViewModel).OpenEditSavingsGoalCommand.Execute(null); if (parentViewModel is MainViewModel main) main.OpenEditSavingsGoalCommand.Execute(null);
} }
private void ProcessChartData() 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; public required ViewModelBase parentViewModel;
// ── Mode ──────────────────────────────────────────────── // Mode
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))]
private bool _isEditMode = false; 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 FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category"; public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category";
// ── Fields ────────────────────────────────────────────── // Fields
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _name = ""; private string _name = "";
@@ -33,7 +33,7 @@ public partial class CategoryFormViewModel : ViewModelBase
[ObservableProperty] private string _selectedColor = "#7B9CFF"; [ObservableProperty] private string _selectedColor = "#7B9CFF";
// ── Icon options ───────────────────────────────────────── // Icon options
public List<string> CategoryIcons { get; } = new() public List<string> CategoryIcons { get; } = new()
{ {
// Food & Dining // Food & Dining
@@ -59,7 +59,7 @@ public partial class CategoryFormViewModel : ViewModelBase
"receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw", "receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw",
}; };
// ── Validation ────────────────────────────────────────── // Validation
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))] [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage; private string? _errorMessage;
@@ -69,18 +69,18 @@ public partial class CategoryFormViewModel : ViewModelBase
public bool IsValid => !string.IsNullOrWhiteSpace(Name); public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4; public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4;
// ── Delete confirm sub-modal ──────────────────────────── // Delete confirm sub-modal
[ObservableProperty] private bool _showDeleteConfirm = false; [ObservableProperty] private bool _showDeleteConfirm = false;
// ── Callbacks ─────────────────────────────────────────── // Callbacks
public Action? OnSaved; public Action? OnSaved;
public Action? OnCancelled; public Action? OnCancelled;
public Action? OnDeleted; public Action? OnDeleted;
// ── Edit mode: original category ──────────────────────── // Edit mode: original category
private Guid? _editingId; private Guid? _editingId;
// ── Commands ──────────────────────────────────────────── // Commands
[RelayCommand] [RelayCommand]
private void SetType(string type) => Type = type; 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() 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) partial void OnSelectedChartTimePeriodChanged(string value)
{ {
ChartTimePeriod period = value switch var (_, _, subtitle) = DateRangeService.Resolve(value);
{ SelectedChartTimPeriodSubTitle = subtitle.Length > 0
"This Month" => ChartTimePeriod.ThisMonth, ? char.ToUpper(subtitle[0]) + subtitle.Substring(1).ToLower()
"Last Month" => ChartTimePeriod.LastMonth, : subtitle;
"This Quarter" => ChartTimePeriod.ThisQuarter,
"This Year" => ChartTimePeriod.ThisYear,
_ => ChartTimePeriod.ThisMonth
};
SelectedChartTimPeriodSubTitle = value switch UpdateSpendingByCategoryChart(value);
{
"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);
} }
public DashboardViewModel() public DashboardViewModel()
{ {
AppData.Transactions.CollectionChanged += (s, e) => UpdateUserOverview(); Track(AppData.Transactions, (_, _) => UpdateUserOverview());
AppData.Accounts.CollectionChanged += (s, e) => UpdateUserOverview(); Track(AppData.Accounts, (_, _) => UpdateUserOverview());
AppData.Categories.CollectionChanged += (s, e) => UpdateUserOverview(); Track(AppData.Categories, (_, _) => UpdateUserOverview());
AppData.Budgets.CollectionChanged += (s, e) => UpdateUserOverview(); Track(AppData.Budgets, (_, _) => UpdateUserOverview());
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview()); WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview());
initialize(); Initialize();
} }
public void initialize() public void Initialize()
{ {
UpdateUserOverview(); UpdateUserOverview();
} }
@@ -126,7 +113,7 @@ public partial class DashboardViewModel : ViewModelBase
private void UpdateUserOverview() private void UpdateUserOverview()
{ {
CalculateMonthlyValues(); CalculateMonthlyValues();
UpdateSpendingByCategoryChart(); UpdateSpendingByCategoryChart(SelectedChartTimePeriod);
_ = UpdateBudgetTracker(); _ = UpdateBudgetTracker();
UpdateRecentTransactions(); UpdateRecentTransactions();
UpdateAccountsSummary(); UpdateAccountsSummary();
@@ -175,53 +162,50 @@ public partial class DashboardViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void CreateTransaction() private void CreateTransaction()
{ {
((MainViewModel)parentViewModel).OpenAddTransaction(); if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
} }
[RelayCommand] [RelayCommand]
private void NavigateToSettings() 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>(); var tempList = new List<ColumnChartData>();
foreach (var category in AppData.Categories) foreach (var category in AppData.Categories)
{ {
var categoryTransactions = var txns = AppData.Transactions
AppData.Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase)); .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: id = category.Id,
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.Month); Name = category.Name,
break; Values = [(double)total],
Fill = new SolidColorPaint(SKColor.Parse(category.Color))
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)) });
} }
tempList = tempList.OrderByDescending(x => x.Values[0]).ToList(); 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)); AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.Where(a => !a.IsArchived).OrderBy(x => x.CreatedAt));
OnPropertyChanged(nameof(AccountsSubtitle)); OnPropertyChanged(nameof(AccountsSubtitle));
} }
private enum ChartTimePeriod
{
ThisMonth,
LastMonth,
ThisQuarter,
ThisYear
}
} }

View File

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

View File

@@ -21,7 +21,9 @@ public partial class MainViewModel : ViewModelBase
public TransactionsViewModel _transactionsViewModel = null!; public TransactionsViewModel _transactionsViewModel = null!;
private AccountsViewModel _accountsViewModel = null!; private AccountsViewModel _accountsViewModel = null!;
private BudgetViewModel _budgetViewModel = null!; private BudgetViewModel _budgetViewModel = null!;
private CategoriesViewModel _categoriesViewModel = null!;
private AnalyticsViewModel _analyticsViewModel = null!; private AnalyticsViewModel _analyticsViewModel = null!;
private MoreViewModel _moreViewModel = null!;
GeneralDataRepo AppData => DataRepo.General; GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty] private Profile? _profile; [ObservableProperty] private Profile? _profile;
@@ -34,6 +36,9 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!; [ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!;
[ObservableProperty] private bool _isDimmed; [ObservableProperty] private bool _isDimmed;
[ObservableProperty] private bool _isMessageBoxVisible;
[ObservableProperty] private MessageBoxViewModel _messageBoxViewModel = new();
[ObservableProperty] private bool _isTransactionFormVisible; [ObservableProperty] private bool _isTransactionFormVisible;
[ObservableProperty] private bool _isAccountFormVisible; [ObservableProperty] private bool _isAccountFormVisible;
[ObservableProperty] private bool _isBudgetFormVisible; [ObservableProperty] private bool _isBudgetFormVisible;
@@ -42,7 +47,8 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] [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; private ViewModelBase? _currentView;
[ObservableProperty] private bool _isDarkTheme; [ObservableProperty] private bool _isDarkTheme;
@@ -50,13 +56,13 @@ public partial class MainViewModel : ViewModelBase
public MainViewModel() public MainViewModel()
{ {
DebugLogger.Log("main vm loaded"); DebugLogger.Log("main vm loaded");
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) => WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (s, m) =>
{ {
Profile = AppData.Profile; Profile = AppData.Profile;
_ = DataRepo.General.RefreshLiveRatesAndEnrich(); _ = DataRepo.General.RefreshLiveRatesAndEnrich();
}); });
IsDimmed = true; IsDimmed = true;
CurrentView = new LoadingViewModel(); CurrentView = new DashboardSkeletonViewModel();
_ = InitializeApp(); _ = InitializeApp();
} }
@@ -73,12 +79,12 @@ public partial class MainViewModel : ViewModelBase
var accountsTask = DataRepo.General.FetchAccounts(); var accountsTask = DataRepo.General.FetchAccounts();
var budgetsTask = DataRepo.General.FetchBudgets(); var budgetsTask = DataRepo.General.FetchBudgets();
await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask); await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask);
Profile = profilesTask.Result; Profile = profilesTask.Result;
DataRepo.General.LinkTransactionCategories(); DataRepo.General.LinkTransactionCategories();
await DataRepo.General.RefreshLiveRatesAndEnrich(); await DataRepo.General.RefreshLiveRatesAndEnrich();
DebugLogger.Log("fetched all data"); DebugLogger.Log("fetched all data");
}); });
@@ -89,12 +95,13 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this parentViewModel = this
}; };
DebugLogger.Log("initialized DashboardViewModel"); DebugLogger.Log("initialized DashboardViewModel");
_transactionsViewModel = new TransactionsViewModel() _transactionsViewModel = new TransactionsViewModel()
{ {
parentViewModel = this parentViewModel = this
}; };
DebugLogger.Log("initialized TransactionsViewModel"); DebugLogger.Log("initialized TransactionsViewModel");
_accountsViewModel = new AccountsViewModel() _accountsViewModel = new AccountsViewModel()
{ {
parentViewModel = this parentViewModel = this
@@ -106,6 +113,16 @@ public partial class MainViewModel : ViewModelBase
parentViewModel = this parentViewModel = this
}; };
DebugLogger.Log("initialized BudgetViewModel"); 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() _analyticsViewModel = new AnalyticsViewModel()
{ {
parentViewModel = this parentViewModel = this
@@ -144,6 +161,7 @@ public partial class MainViewModel : ViewModelBase
IsDarkTheme = ThemeService.IsDarkTheme; IsDarkTheme = ThemeService.IsDarkTheme;
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system"); ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
AppData.StartRealtimeSync();
CurrentView = _dashboardViewModel; CurrentView = _dashboardViewModel;
IsDimmed = false; 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] [RelayCommand]
public void OpenAddTransaction() public void OpenAddTransaction()
{ {
@@ -403,6 +439,18 @@ public partial class MainViewModel : ViewModelBase
CurrentView = _budgetViewModel; CurrentView = _budgetViewModel;
} }
[RelayCommand]
private void GoToCategories()
{
CurrentView = _categoriesViewModel;
}
[RelayCommand]
private void GoToMore()
{
CurrentView = _moreViewModel;
}
[RelayCommand] [RelayCommand]
private void GoToAnalytics() 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 isOnDashboard => CurrentView is DashboardViewModel;
public bool isOnTransactions => CurrentView is TransactionsViewModel; public bool isOnTransactions => CurrentView is TransactionsViewModel;
public bool isOnAccounts => CurrentView is AccountsViewModel; public bool isOnAccounts => CurrentView is AccountsViewModel;
public bool isOnBudget => CurrentView is BudgetViewModel; public bool isOnBudget => CurrentView is BudgetViewModel;
public bool isOnCategories => CurrentView is CategoriesViewModel;
public bool isOnAnalytics => CurrentView is AnalyticsViewModel; public bool isOnAnalytics => CurrentView is AnalyticsViewModel;
public bool isOnSettings => CurrentView is SettingsViewModel; 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 => public bool IsValid =>
decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) && v >= 0; decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) && v >= 0;
// ── Callbacks ──────────────────────────────────────────── // Callbacks
public Action? OnSaved; public Action? OnSaved;
public Action? OnCancelled; public Action? OnCancelled;
// ── Setup ──────────────────────────────────────────────── // Setup
public void Setup(decimal? currentGoal) public void Setup(decimal? currentGoal)
{ {
GoalInput = currentGoal.HasValue GoalInput = currentGoal.HasValue
@@ -39,7 +39,7 @@ public partial class SetSavingsGoalDialogViewModel : ViewModelBase
ErrorMessage = null; ErrorMessage = null;
} }
// ── Commands ───────────────────────────────────────────── // Commands
[RelayCommand] [RelayCommand]
private void Cancel() => OnCancelled?.Invoke(); private void Cancel() => OnCancelled?.Invoke();

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq; using System.Linq;
using Clario.Data; using Clario.Data;
using Clario.Messages; using Clario.Messages;
@@ -11,60 +10,98 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
// ReSharper disable PossibleMultipleEnumeration
namespace Clario.ViewModels; namespace Clario.ViewModels;
public partial class TransactionsViewModel : ViewModelBase public partial class TransactionsViewModel : ViewModelBase
{ {
public required ViewModelBase parentViewModel; 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<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = 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(); private List<Transaction> _filteredTransactions = new();
public int FilteredTransactionCount => _filteredTransactions.Count; public int FilteredTransactionCount => _filteredTransactions.Count;
[ObservableProperty] private ObservableCollection<Transaction> _pagedTransactions = new();
// ── Desktop pagination ───────────────────────────────────────────────────
private int _pageSize = 25; private int _pageSize = 25;
[ObservableProperty] private int _pageSizeIndex; [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; private int _currentPage = 1;
[ObservableProperty] private string _paginationSummaryText; [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 ObservableCollection<int> _visiblePageNumbers = new(); [ObservableProperty] private ObservableCollection<int> _visiblePageNumbers = new();
public int TotalPages => (int)Math.Ceiling(FilteredTransactions.Count / (double)_pageSize);
public bool HasNoTransactions => FilteredTransactions.Count == 0; public int TotalPages => (int)Math.Ceiling(_filteredAll.Count / (double)_pageSize);
public bool HasNextPage => CurrentPage < TotalPages; public bool HasNoTransactions => _filteredAll.Count == 0;
public bool HasPreviousPage => CurrentPage > 1; 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 _totalExpenses;
[ObservableProperty] private double _totalIncome; [ObservableProperty] private double _totalIncome;
@@ -75,261 +112,23 @@ public partial class TransactionsViewModel : ViewModelBase
public string PrimaryCurrencySymbol => public string PrimaryCurrencySymbol =>
CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD"); CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private string _searchText = ""; // ── Constructor ──────────────────────────────────────────────────────────
[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";
public TransactionsViewModel() public TransactionsViewModel()
{ {
AppData.Transactions.CollectionChanged += (_, _) => Track(AppData.Transactions, (_, _) =>
{ {
InitializeCategories(); InitializeCategories();
InitializeAccounts(); InitializeAccounts();
LoadPage(1); Refresh();
}; });
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => LoadPage(CurrentPage));
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => Refresh());
Initialize(); Initialize();
} }
partial void OnPageSizeIndexChanged(int value) // ── Initialization ───────────────────────────────────────────────────────
{
_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);
}
}
public void Initialize() public void Initialize()
{ {
@@ -337,9 +136,7 @@ public partial class TransactionsViewModel : ViewModelBase
{ {
InitializeCategories(); InitializeCategories();
InitializeAccounts(); InitializeAccounts();
CalculateMonthlyFinancials(); CalculateMonthlyFinancials();
CurrentPage = 1; CurrentPage = 1;
OnPropertyChanged(nameof(TotalPages)); OnPropertyChanged(nameof(TotalPages));
ResetFilters(); ResetFilters();
@@ -354,59 +151,274 @@ public partial class TransactionsViewModel : ViewModelBase
private void InitializeCategories() private void InitializeCategories()
{ {
Categories.Clear(); Categories.Clear();
Categories.Insert(0, new Category() { Name = "All Categories" }); Categories.Insert(0, new Category { Name = "All Categories" });
foreach (var appDataCategory in AppData.Categories) foreach (var cat in AppData.Categories)
{ Categories.Add(cat);
Categories.Add(appDataCategory);
}
SelectedCategory = Categories.First(); SelectedCategory = Categories.First();
} }
private void InitializeAccounts() private void InitializeAccounts()
{ {
Accounts.Clear(); Accounts.Clear();
Accounts.Insert(0, new Account() { Name = "All Accounts" }); Accounts.Insert(0, new Account { Name = "All Accounts" });
foreach (var appDataAccount in AppData.Accounts) foreach (var acc in AppData.Accounts)
{ Accounts.Add(acc);
Accounts.Add(appDataAccount);
}
SelectedAccount = Accounts.First(); SelectedAccount = Accounts.First();
} }
private void CalculateMonthlyFinancials() private void CalculateMonthlyFinancials()
{ {
TotalExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount)); var now = DateTime.Now;
TotalIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount)); var monthly = AppData.Transactions
ExpensesCount = AppData.Transactions.Count(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month); .Where(x => x.Date.Month == now.Month && x.Date.Year == now.Year);
IncomeCount = AppData.Transactions.Count(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month); 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); var index = list.IndexOf(item);
if (index == -1) return new List<T>(); if (index == -1) return new List<T>();
var start = Math.Max(0, Math.Min(index - count / 2, list.Count - count));
var half = count / 2; return list.GetRange(start, Math.Min(count, list.Count - start));
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);
} }
// ── Navigation ───────────────────────────────────────────────────────────
[RelayCommand] [RelayCommand]
private void CreateTransaction() private void CreateTransaction()
{ {
((MainViewModel)parentViewModel).OpenAddTransaction(); if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
} }
[RelayCommand] [RelayCommand]
private void EditTransaction(Transaction transaction) 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; 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 /> <vm:AccountFormViewModel />
</Design.DataContext> </Design.DataContext>
<!-- ── Dim overlay ───────────────────────── --> <!-- Dim overlay -->
<Grid> <Grid>
<Border Background="#70000000" /> <Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── --> <!-- Modal card -->
<Border HorizontalAlignment="Center" <Border HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="{DynamicResource BgSurface}" Background="{DynamicResource BgSurface}"
@@ -29,7 +29,7 @@
BoxShadow="0 24 72 0 #60000000"> BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<!-- ── Header ──────────────────────── --> <!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24"> <Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0" <Border Grid.Column="0"
CornerRadius="10" CornerRadius="10"
@@ -66,7 +66,7 @@
</Button> </Button>
</Grid> </Grid>
<!-- ── Name ──────────────────────── --> <!-- Name -->
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Name, Mode=TwoWay}" <TextBox Text="{Binding Name, Mode=TwoWay}"
Watermark="e.g. Main Checking" Watermark="e.g. Main Checking"
@@ -76,7 +76,7 @@
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Margin="0,0,0,16" /> Margin="0,0,0,16" />
<!-- ── Type ─────────────────────────── --> <!-- Type -->
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -92,7 +92,7 @@
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
</Border> </Border>
<!-- ── Institution + Mask ──────────── --> <!-- Institution + Mask -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"> <Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Institution --> <!-- Institution -->
<StackPanel Grid.Column="0" Spacing="6"> <StackPanel Grid.Column="0" Spacing="6">
@@ -122,7 +122,7 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- ── Opening Balance + Currency ──────────── --> <!-- Opening Balance + Currency -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"> <Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Opening Balance --> <!-- Opening Balance -->
<StackPanel Grid.Column="0" Spacing="6"> <StackPanel Grid.Column="0" Spacing="6">
@@ -204,7 +204,7 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- ── Credit Limit (if type is credit) ──────────── --> <!-- Credit Limit (if type is credit) -->
<StackPanel Spacing="6" Margin="0,0,0,16" IsVisible="{Binding IsCredit}"> <StackPanel Spacing="6" Margin="0,0,0,16" IsVisible="{Binding IsCredit}">
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" /> <TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
@@ -236,7 +236,7 @@
</Border> </Border>
</StackPanel> </StackPanel>
<!-- ── Opened At ──────────────────────── --> <!-- Opened At -->
<TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -250,7 +250,7 @@
Padding="12,10" /> Padding="12,10" />
</Border> </Border>
<!-- ── Icon + Color ──────────── --> <!-- Icon + Color -->
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"> <Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
<!-- Icon --> <!-- Icon -->
<StackPanel Grid.Column="0" Spacing="6"> <StackPanel Grid.Column="0" Spacing="6">
@@ -304,7 +304,7 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- ── Primary account toggle ──────────── --> <!-- Primary account toggle -->
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16"> <Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2"> <StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="PRIMARY ACCOUNT" Classes="label" /> <TextBlock Text="PRIMARY ACCOUNT" Classes="label" />
@@ -320,7 +320,7 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Grid> </Grid>
<!-- ── Validation error ─────────────── --> <!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}" <Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}" BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1" BorderThickness="1"
@@ -339,7 +339,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Actions ──────────────────────── --> <!-- Actions -->
<UniformGrid Rows="1"> <UniformGrid Rows="1">
<Button Classes="base" <Button Classes="base"
Margin="0,0,6,0" Margin="0,0,6,0"

View File

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

View File

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

View File

@@ -9,16 +9,16 @@
x:Class="Clario.Views.AuthView"> x:Class="Clario.Views.AuthView">
<Grid> <Grid>
<!-- Background --> <!-- Background -->
<!-- <Calendar SelectionMode="SingleRange"> --> <!-- <Calendar SelectionMode="SingleRange"> -->
<!-- </Calendar> --> <!-- </Calendar> -->
<!-- <Border Background="{DynamicResource AccentBlue}" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400" Padding="10"> --> <!-- <Border Background="{DynamicResource AccentBlue}" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400" Padding="10"> -->
<!-- --> <!-- -->
<!-- </Border> --> <!-- </Border> -->
<!-- Center card --> <!-- Center card -->
<Border HorizontalAlignment="Center" <Border HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="{DynamicResource BgSurface}" Background="{DynamicResource BgSurface}"
@@ -30,7 +30,7 @@
BoxShadow="0 24 64 0 #40000000"> BoxShadow="0 24 64 0 #40000000">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<!-- Logo + App name --> <!-- Logo + App name -->
<StackPanel HorizontalAlignment="Center" <StackPanel HorizontalAlignment="Center"
Spacing="0" Spacing="0"
Margin="0,0,0,32"> Margin="0,0,0,32">
@@ -43,10 +43,10 @@
<!-- REPLACE: app name --> <!-- REPLACE: app name -->
<StackPanel Spacing="4" HorizontalAlignment="Center"> <StackPanel Spacing="4" HorizontalAlignment="Center">
<!-- <TextBlock Text="Clario" --> <!-- <TextBlock Text="Clario" -->
<!-- FontSize="22" --> <!-- FontSize="22" -->
<!-- FontWeight="Bold" --> <!-- FontWeight="Bold" -->
<!-- Foreground="{DynamicResource TextPrimary}" --> <!-- Foreground="{DynamicResource TextPrimary}" -->
<!-- HorizontalAlignment="Center" /> --> <!-- HorizontalAlignment="Center" /> -->
<TextBlock Text="Your personal finance tracker" <TextBlock Text="Your personal finance tracker"
FontSize="12" FontSize="12"
Foreground="{DynamicResource TextMuted}" Foreground="{DynamicResource TextMuted}"
@@ -54,13 +54,14 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- Tab switcher --> <!-- Tab switcher -->
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="1" BorderThickness="1"
CornerRadius="{DynamicResource RadiusControl}" CornerRadius="{DynamicResource RadiusControl}"
Padding="3" Padding="3"
Margin="0,0,0,26"> Margin="0,0,0,26"
IsVisible="{Binding ShowTabs}">
<Grid ColumnDefinitions="*,*"> <Grid ColumnDefinitions="*,*">
<!-- REPLACE: active state driven by IsLoginMode --> <!-- REPLACE: active state driven by IsLoginMode -->
<!-- Sign In — active --> <!-- Sign In — active -->
@@ -178,13 +179,13 @@
</Border> </Border>
<!-- Forgot password --> <!-- Forgot password -->
<!-- REPLACE: Command="{Binding ForgotPasswordCommand}" -->
<Button Background="Transparent" <Button Background="Transparent"
BorderThickness="0" BorderThickness="0"
Padding="0" Padding="0"
Cursor="Hand" Cursor="Hand"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="0,0,0,24"> Margin="0,0,0,24"
Command="{Binding SetOperationCommand}" CommandParameter="forgotPassword">
<TextBlock Text="Forgot password?" <TextBlock Text="Forgot password?"
FontSize="12" FontSize="12"
Foreground="{DynamicResource AccentBlue}" /> Foreground="{DynamicResource AccentBlue}" />
@@ -230,10 +231,129 @@
</StackPanel> </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 SIGN UP PANEL
REPLACE: IsVisible="{Binding !IsLoginMode}" REPLACE: IsVisible="{Binding !IsLoginMode}"
══════════════════════════════════ --> -->
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}"> <StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
<!-- Name row --> <!-- Name row -->
@@ -430,7 +550,7 @@
</StackPanel> </StackPanel>
<!-- Footer --> <!-- Footer -->
<Separator Margin="0,0,0,16" /> <Separator Margin="0,0,0,16" />
<TextBlock Text="Your data is encrypted and synced securely." <TextBlock Text="Your data is encrypted and synced securely."
FontSize="11" FontSize="11"

View File

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

View File

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

View File

@@ -11,11 +11,11 @@
<vm:CategoryFormViewModel /> <vm:CategoryFormViewModel />
</Design.DataContext> </Design.DataContext>
<!-- ── Dim overlay ───────────────────────── --> <!-- Dim overlay -->
<Grid> <Grid>
<Border Background="#70000000" /> <Border Background="#70000000" />
<!-- ── Modal card ────────────────────────── --> <!-- Modal card -->
<Border HorizontalAlignment="Center" <Border HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="{DynamicResource BgSurface}" Background="{DynamicResource BgSurface}"
@@ -27,7 +27,7 @@
BoxShadow="0 24 72 0 #60000000"> BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<!-- ── Header ──────────────────────── --> <!-- Header -->
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24"> <Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
<Border Grid.Column="0" <Border Grid.Column="0"
CornerRadius="10" CornerRadius="10"
@@ -64,7 +64,7 @@
</Button> </Button>
</Grid> </Grid>
<!-- ── Name ──────────────────────── --> <!-- Name -->
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
<TextBox Text="{Binding Name, Mode=TwoWay}" <TextBox Text="{Binding Name, Mode=TwoWay}"
Watermark="e.g. Groceries" Watermark="e.g. Groceries"
@@ -74,7 +74,7 @@
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Margin="0,0,0,16" /> Margin="0,0,0,16" />
<!-- ── Type toggle ─────────────────── --> <!-- Type toggle -->
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -116,7 +116,7 @@
</Grid> </Grid>
</Border> </Border>
<!-- ── Icon picker ─────────────────── --> <!-- Icon picker -->
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -161,7 +161,7 @@
</ScrollViewer> </ScrollViewer>
</Border> </Border>
<!-- ── Color ──────────────────────── --> <!-- Color -->
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" /> <TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
<Border Background="{DynamicResource BgBase}" <Border Background="{DynamicResource BgBase}"
BorderBrush="{DynamicResource BorderSubtle}" BorderBrush="{DynamicResource BorderSubtle}"
@@ -180,7 +180,7 @@
IsAccentColorsVisible="False" /> IsAccentColorsVisible="False" />
</Border> </Border>
<!-- ── Validation error ─────────────── --> <!-- Validation error -->
<Border Background="{DynamicResource BadgeBgRed}" <Border Background="{DynamicResource BadgeBgRed}"
BorderBrush="{DynamicResource AccentRed}" BorderBrush="{DynamicResource AccentRed}"
BorderThickness="1" BorderThickness="1"
@@ -199,7 +199,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Delete button (edit mode only) ── --> <!-- Delete button (edit mode only) -->
<Button Classes="danger" <Button Classes="danger"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
@@ -215,7 +215,7 @@
</StackPanel> </StackPanel>
</Button> </Button>
<!-- ── Actions ──────────────────────── --> <!-- Actions -->
<UniformGrid Rows="1"> <UniformGrid Rows="1">
<Button Classes="base" <Button Classes="base"
Margin="0,0,6,0" Margin="0,0,6,0"
@@ -248,7 +248,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ── Delete confirm sub-modal ──────────────── --> <!-- Delete confirm sub-modal -->
<Grid IsVisible="{Binding ShowDeleteConfirm}"> <Grid IsVisible="{Binding ShowDeleteConfirm}">
<Border Background="#50000000" /> <Border Background="#50000000" />
<Border HorizontalAlignment="Center" <Border HorizontalAlignment="Center"
@@ -262,7 +262,7 @@
BoxShadow="0 24 72 0 #60000000"> BoxShadow="0 24 72 0 #60000000">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<Border Background="#2A0D0D" <Border Background="{DynamicResource IconBgRed}"
CornerRadius="14" CornerRadius="14"
Width="52" Height="52" Width="52" Height="52"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -300,7 +300,7 @@
Padding="0,11" Padding="0,11"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Background="#FF5E5E" Background="{DynamicResource AccentRed}"
BorderThickness="0" BorderThickness="0"
CornerRadius="{DynamicResource RadiusControl}" CornerRadius="{DynamicResource RadiusControl}"
Command="{Binding ConfirmDeleteCommand}"> Command="{Binding ConfirmDeleteCommand}">
@@ -311,7 +311,7 @@
<TextBlock Text="Delete" <TextBlock Text="Delete"
FontSize="13" FontSize="13"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="#FFFFFF" Foreground="{DynamicResource BgBase}"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</Button> </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="*"> <Grid ColumnDefinitions="*">
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Name="mainScrollviewer"> <ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Name="mainScrollviewer">
<StackPanel Spacing="24" Margin="32,28,32,32"> <StackPanel Spacing="24" Margin="32,28,32,32">
<!-- Top Bar --> <!-- Top Bar -->
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"> <StackPanel Grid.Column="0">
<!-- <TextBlock Classes="muted" Text="Friday, March 6, 2026" /> --> <!-- <TextBlock Classes="muted" Text="Friday, March 6, 2026" /> -->
@@ -30,7 +30,7 @@
Cursor="Hand" Content="+ Add Transaction" Command="{Binding CreateTransactionCommand}" /> Cursor="Hand" Content="+ Add Transaction" Command="{Binding CreateTransactionCommand}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- KPI Cards Row --> <!-- KPI Cards Row -->
<Grid ColumnDefinitions="*,*,*" HorizontalAlignment="Stretch" MaxHeight="160"> <Grid ColumnDefinitions="*,*,*" HorizontalAlignment="Stretch" MaxHeight="160">
<Grid.Styles> <Grid.Styles>
<Style Selector="Grid > Border"> <Style Selector="Grid > Border">
@@ -109,7 +109,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>
<!-- Mid Row: Spending Chart + Budget --> <!-- Mid Row: Spending Chart + Budget -->
<Grid ColumnDefinitions="*,340" MaxHeight="470"> <Grid ColumnDefinitions="*,340" MaxHeight="470">
<!-- Spending Breakdown --> <!-- Spending Breakdown -->
@@ -258,7 +258,7 @@
</ScrollViewer> </ScrollViewer>
</Border> </Border>
</Grid> </Grid>
<!-- Bottom Row: Recent Transactions + Accounts --> <!-- Bottom Row: Recent Transactions + Accounts -->
<Grid ColumnDefinitions="*,300" MaxHeight="500"> <Grid ColumnDefinitions="*,300" MaxHeight="500">
<!-- Recent Transactions --> <!-- Recent Transactions -->

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
<vm:MainViewModel /> <vm:MainViewModel />
</Design.DataContext> </Design.DataContext>
<Grid ColumnDefinitions="220,*"> <Grid ColumnDefinitions="220,*">
<!-- ───────────────────────────────────── SIDEBAR ───────────────────────────────────── --> <!-- SIDEBAR -->
<Border Grid.Column="0" Background="{DynamicResource BgSidebar}" BorderBrush="{DynamicResource BorderSubtle}" <Border Grid.Column="0" Background="{DynamicResource BgSidebar}" BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,1,0" Padding="16,28,16,24" IsEnabled="{Binding !IsTransactionFormVisible}"> BorderThickness="0,0,1,0" Padding="16,28,16,24" IsEnabled="{Binding !IsTransactionFormVisible}">
<DockPanel> <DockPanel>
@@ -49,9 +49,9 @@
Foreground="{DynamicResource TextSecondary}" /> Foreground="{DynamicResource TextSecondary}" />
</Grid> </Grid>
<!-- <Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Command="{Binding SignOutCommand}"> --> <!-- <Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Command="{Binding SignOutCommand}"> -->
<!-- <ToolTip.Tip> --> <!-- <ToolTip.Tip> -->
<!-- signout --> <!-- signout -->
<!-- </ToolTip.Tip> --> <!-- </ToolTip.Tip> -->
<!-- </Button> --> <!-- </Button> -->
</Grid> </Grid>
</Border> </Border>
@@ -122,7 +122,9 @@
<views:CategoryFormView <views:CategoryFormView
DataContext="{Binding CategoryFormViewModel}" DataContext="{Binding CategoryFormViewModel}"
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" /> IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
<views:MessageBoxView
DataContext="{Binding MessageBoxViewModel}"
IsVisible="{Binding DataContext.IsMessageBoxVisible, ElementName=MainControl}" />
</Grid> </Grid>
</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" Spacing="0"
MaxWidth="720"> MaxWidth="720">
<!-- ── Page header ─────────────────────────── --> <!-- Page header -->
<StackPanel Margin="0,0,0,28"> <StackPanel Margin="0,0,0,28">
<TextBlock Text="Settings" <TextBlock Text="Settings"
FontSize="26" FontSize="26"
@@ -29,7 +29,7 @@
Margin="0,4,0,0" /> Margin="0,4,0,0" />
</StackPanel> </StackPanel>
<!-- ── Global success / error banner ─────────── --> <!-- Global success / error banner -->
<Border Background="{DynamicResource IconBgGreen}" <Border Background="{DynamicResource IconBgGreen}"
BorderBrush="{DynamicResource AccentGreen}" BorderBrush="{DynamicResource AccentGreen}"
BorderThickness="1" BorderThickness="1"
@@ -75,9 +75,9 @@
</Grid> </Grid>
</Border> </Border>
<!-- ══════════════════════════════════════════════ <!--
SECTION: Profile SECTION: Profile
══════════════════════════════════════════════ --> -->
<TextBlock Text="PROFILE" <TextBlock Text="PROFILE"
Classes="label" Classes="label"
Margin="0,0,0,10" /> Margin="0,0,0,10" />
@@ -223,9 +223,9 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ══════════════════════════════════════════════ <!--
SECTION: Account Security SECTION: Account Security
══════════════════════════════════════════════ --> -->
<TextBlock Text="ACCOUNT &amp; SECURITY" <TextBlock Text="ACCOUNT &amp; SECURITY"
Classes="label" Classes="label"
Margin="0,0,0,10" /> Margin="0,0,0,10" />
@@ -238,7 +238,7 @@
Margin="0,0,0,24"> Margin="0,0,0,24">
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<!-- ── Email row ───────────────────────────── --> <!-- Email row -->
<Border BorderBrush="{DynamicResource BorderSubtle}" <Border BorderBrush="{DynamicResource BorderSubtle}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Padding="20,0"> Padding="20,0">
@@ -350,7 +350,7 @@
</Panel> </Panel>
</Border> </Border>
<!-- ── Password row ───────────────────────── --> <!-- Password row -->
<Border Padding="20,0"> <Border Padding="20,0">
<!-- Normal password display --> <!-- Normal password display -->
<Panel> <Panel>
@@ -473,9 +473,9 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- ══════════════════════════════════════════════ <!--
SECTION: Danger zone SECTION: Danger zone
══════════════════════════════════════════════ --> -->
<TextBlock Text="SESSION" <TextBlock Text="SESSION"
Classes="label" Classes="label"
Margin="0,0,0,10" /> Margin="0,0,0,10" />

View File

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

View File

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