Files
Clario/NEW_CHAT_CONTEXT.md
Nouredeen06 d8dea1913a
All checks were successful
Build Linux / build (push) Successful in 1m8s
Added multi-currency support, account/budget management, and settings
- Primary account determines app-wide reference currency; all totals, charts, and summaries convert to it automatically using live rates

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

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

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

- Budget create/edit/delete; savings goal dialog

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

- Removed currency selector from Settings (follows primary account)

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

- Fixed total balance overlap in dashboard accounts card
2026-04-03 02:39:51 +03:00

19 KiB

Clario — New Chat Context Summary

Paste this entire file at the start of a new chat to continue work without quality degradation.


What Clario Is

Clario is a cross-platform personal finance tracking app built by Nouredeen (based in Jordan, Windows dev environment with Arabic region — always use en-US CultureInfo for dates/numbers). The app is in active development, not yet released.

Tech stack:

  • Avalonia UI XPlat template — C# .NET 9
  • CommunityToolkit.MVVM ([ObservableProperty], [RelayCommand])
  • Supabase backend (PostgreSQL, Auth, RLS, Realtime)
  • LiveCharts2 (SkiaSharp) for charts
  • Avalonia.Controls.ColorPicker (built-in)
  • JetBrains Rider on Windows

Project Structure

Clario/                     ← shared (ViewModels, Models, Services, Data, CustomControls, Behaviors, Converters)
Clario.Desktop/             ← Windows/macOS/Linux entry point (Program.cs)
Clario.Android/             ← Android entry point (MainActivity.cs)
Clario.iOS/                 ← iOS entry point (AppDelegate.cs)
Clario.Browser/             ← Web/WASM entry point
Views/                      ← desktop AXAML views
MobileViews/                ← mobile AXAML views

Namespaces: Clario.ViewModels, Clario.Views, Clario.MobileViews, Clario.Models, Clario.Data, Clario.Services, Clario.CustomControls, Clario.Behaviors, Clario.Converters


Architecture

Platform detection

// App.axaml.cs
public static bool IsMobile { get; private set; }
IsMobile = ApplicationLifetime is ISingleViewApplicationLifetime;

ViewLocator (auto view resolution)

// mobile: tries Clario.MobileViews.{Name}ViewMobile, falls back to Clario.Views.{Name}View
// desktop: Clario.Views.{Name}View
// No DataTemplates in AXAML — all registered via ViewLocator in App.axaml

App.axaml startup flow

public override async void OnFrameworkInitializationCompleted()
{
    base.OnFrameworkInitializationCompleted();
    IsMobile = ApplicationLifetime is ISingleViewApplicationLifetime;
    // ViewLocator handles platform routing automatically

    CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
    CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");

    await SupabaseService.InitializeAsync(
        IsMobile && !(ApplicationLifetime is ISingleViewApplicationLifetime browser)
            ? new FileSessionStorage()
            : new FileSessionStorage() // BrowserSessionStorage for web
    );
    await SupabaseService.Client.Auth.RetrieveSessionAsync(); // catch { }

    var user = SupabaseService.Client.Auth.CurrentUser;
    var profile = await DataRepo.General.FetchProfileInfo();
    if (profile is not null) ThemeService.SwitchToTheme(profile.Theme);

    // desktop
    desktop.MainWindow = new MainWindow { DataContext = user != null ? new MainViewModel() : new AuthViewModel() };
    // mobile
    singleView.MainView = new MobileShellView { DataContext = user != null ? new MainViewModel() : new AuthViewModel() };
}

MainViewModel (shell)

// constructor: sets CurrentView = _dashboardViewModel immediately, then _ = InitializeApp()
// InitializeApp(): Task.WhenAll all fetches, then pushes data into child VMs, calls vm.Initialize()
// Navigation: isOnDashboard, isOnTransactions, isOnAccounts, isOnBudget (bool properties)
// Commands: GoToDashboardCommand, GoToTransactionsCommand, GoToAccountsCommand, GoToBudgetCommand
// Modal: IsTransactionFormVisible, TransactionFormViewModel instance
// DeleteAccount: IsDeleteAccountDialogVisible, DeleteAccountDialogViewModel instance
// Data lists: List<Transaction> _transactions, _categories, _accounts, _budgets, Profile

ViewModel pattern

public partial class MyViewModel : ViewModelBase
{
    public required ViewModelBase parentViewModel;
    // Data set as plain fields/properties, NOT fetched in constructor
    public void Initialize() { /* called after all data is set */ }
}

Critical rule: Never fetch data in child VM constructors. Never trigger init from partial void On{Property}Changed when VM depends on multiple properties. Always call Initialize() explicitly after object initializer sets all required fields.

DataRepo

public static class DataRepo { public static GeneralDataRepo General { get; } = new(); }
// Methods: FetchTransactions, FetchCategories, FetchAccounts, FetchBudgets, FetchProfileInfo
// InsertTransaction, UpdateTransaction, DeleteTransaction
// MigrateTransactions(fromId, toId), RecalculateAccountBalance(id), DeleteAccount(id)

Supabase Schema

Tables

  • profilesid (UUID = auth.uid()), display_name, avatar_url, currency, theme, language, savings_goal (DECIMAL), savings_goal_deadline (DATE), created_at, updated_at
  • accountsid, user_id, name, type, institution, mask, currency, balance, credit_limit, is_archived, opened_at, created_at
  • categoriesid, user_id, name, icon (lucide icon name), color (hex), type (income/expense), created_at
  • transactionsid, user_id, account_id, category_id, amount (DECIMAL, always positive), type (income/expense), description, note, date (DATE), created_at
  • budgetsid, user_id, category_id, amount, period (monthly/quarterly/yearly), alert_threshold (int, default 80), rollover (bool), created_at

RLS

  • All tables have RLS enabled
  • SELECT/UPDATE/DELETE: USING (auth.uid() = user_id)
  • INSERT: WITH CHECK (auth.uid() = user_id)
  • Profile auto-created via DB trigger on auth.users INSERT (SECURITY DEFINER)

Session persistence

  • FileSessionStorage — desktop/mobile, stores in %AppData%\Clario\session.json
  • BrowserSessionStorage — web, uses JS interop to localStorage
  • SupabaseSessionHandler implements IGotrueSessionPersistence<Session>
  • Session restored manually: await client.Auth.SetSession(accessToken, refreshToken)
  • AutoRefreshToken = true, AutoConnectRealtime = true

Models

[Table("transactions")]
public class Transaction : BaseModel
{
    [PrimaryKey("id", false)] public Guid Id { get; set; }
    [Column("user_id")]       public Guid UserId { get; set; }
    [Column("account_id")]    public Guid AccountId { get; set; }
    [Column("category_id")]   public Guid? CategoryId { get; set; }
    [Column("amount")]        public decimal Amount { get; set; }
    [Column("type")]          public string Type { get; set; }
    [Column("description")]   public string Description { get; set; }
    [Column("note")]          public string? Note { get; set; }
    [Column("date")]          public DateTime Date { get; set; }
    [Column("created_at")]    public DateTime CreatedAt { get; set; }
    // non-DB nav properties (no [Column]):
    public Category? Category { get; set; }
    public bool GroupHeader { get; set; }
    public bool IsExpense => Type == "expense";
}

// Account has: Id, UserId, Name, Type, Institution, Mask, Currency, Balance,
//   OpeningBalance, CreditLimit, IsArchived, OpenedAt, CreatedAt
//   + non-DB: Color (string hex), Icon (string lucide name), CurrentBalance,
//     TransactionsCount, MonthlyIncrease, TotalIncomeThisMonth, TotalExpenseThisMonth,
//     IncomeTransactionsThisMonth, ExpenseTransactionsThisMonth, RecentTransactions

// Category has: Id, UserId, Name, Icon, Color, Type, CreatedAt

// Budget has: Id, UserId, CategoryId, LimitAmount (was "Amount"), Period, AlertThreshold,
//   Rollover, CreatedAt
//   + non-DB: Category, Spent, TransactionsCount, GroupHeader
//   + computed: IsOnTrack, IsWarning, IsOverBudget, PercentageUsed, RemainingFormatted,
//     SpentFormatted, AmountFormatted, PercentageFormatted

// Profile has: Id, DisplayName, AvatarUrl, Currency, Theme, Language,
//   SavingsGoal (decimal?), SavingsGoalDeadline (DateTime?), CreatedAt, UpdatedAt

Design System

Dark theme colors

BgBase: #0D0F14     BgSurface: #13161E    BgSidebar: #0B0D12
BgElevated: #0D0F14  BgHover: #1A1E2A
BorderSubtle: #1E2330  BorderAccent: #2A3050
TextPrimary: #F0F2F8   TextSecondary: #C8D0E8  TextMuted: #7A8090  TextDisabled: #5A6070
AccentBlue: #7B9CFF    AccentGreen: #2ECC8A    AccentYellow: #F5C842
AccentRed: #FF5E5E     AccentPurple: #9B7BFF   AccentOrange: #FF7E5E  AccentPink: #FF5E9B
IconBgBlue: #1A2240    IconBgGreen: #0D2A1A    IconBgOrange: #2A1A0D
IconBgRed: #2A0D0D     IconBgPurple: #1A1A2A   IconBgPink: #1A0D1A
BadgeBgRed, BadgeBgYellow (semi-transparent dark versions of accent colors)

Light theme

Softened versions (~10% darker accents), BgBase: #EDEEF4, BgSurface: #F5F6FA

Theme switching

ThemeService.SwitchToTheme(string theme) — "dark"/"light"/"system" Profile stores theme preference. IsDarkTheme bool on MainViewModel.

Dynamic resources used everywhere

{DynamicResource BgBase}, {DynamicResource AccentBlue}, {DynamicResource TextPrimary}
{DynamicResource RadiusControl}, {DynamicResource RadiusIcon}, {DynamicResource RadiusPill}
{DynamicResource FontSizeBody}, {DynamicResource FontSizeLabel}

SVG icons (Lucide)

<Svg Path="../Assets/Icons/icon-name.svg" Width="16" Height="16" Css="{DynamicResource SvgBlue}"/>

SVG CSS resources: SvgPrimary, SvgSecondary, SvgMuted, SvgDisabled, SvgBlue, SvgGreen, SvgYellow, SvgRed, SvgPurple, SvgOrange, SvgPink Icon bg opacity pattern: <SolidColorBrush Color="{Binding Color, Converter=..., ConverterParameter=color}" Opacity="0.15"/>


Button / Style Classes

  • accented — primary action (AccentBlue bg)
  • base — secondary action
  • nav — transparent nav/toggle, Classes.active="{Binding bool}" for active state
  • account — account card button
  • ghost — transparent TextBox (all states transparent, no border)
  • label — uppercase muted label TextBlock
  • muted — TextMuted foreground
  • badge-green / badge-red / badge-warning — status badges
  • budget-card / budget-card-warning / budget-card-over — budget card borders
  • mobile — root class on mobile views (enables mobile style overrides)

ProgressBar: Classes="green" / "yellow" / "red" / "blue"

Mobile nav button override:

<Style Selector=".mobile Button.nav:pressed /template/ ContentPresenter"> Transparent </Style>
<Style Selector=".mobile Button.nav:pointerover /template/ ContentPresenter"> Transparent </Style>

View Architecture

Desktop shell (MainView.axaml)

<Grid ColumnDefinitions="220,*">
    <Border><!-- 220px sidebar with nav + user profile --></Border>
    <Grid Column="1">
        <ContentControl Content="{Binding CurrentView}"/>
        <!-- Transaction form modal overlay — IsVisible="{Binding IsTransactionFormVisible}" -->
        <!-- Delete account dialog overlay — IsVisible="{Binding IsDeleteAccountDialogVisible}" -->
    </Grid>
</Grid>

Views do NOT include sidebar. Background="{DynamicResource BgBase}" on root. Typical layout: Grid RowDefinitions="Auto,*" — top bar + content. Top bar margin: Margin="32,28,32,0".

Mobile shell (MainViewMobile.axaml)

<Grid Classes="mobile" RowDefinitions="*,Auto">
    <ContentControl Content="{Binding CurrentView}"/>
    <!-- Bottom tab bar: Home | Transactions | + FAB | Accounts | Budget -->
</Grid>

Mobile views: single column, Margin="16,12,16,0" top bar, no BoxShadow, no MinWidth/MinHeight.

Views built so far

Desktop: DashboardView, TransactionsView, AccountsView, BudgetView, AuthView, MainView Mobile: DashboardViewMobile, TransactionsViewMobile, AccountsViewMobile, BudgetViewMobile, MainViewMobile, AuthViewMobile (not yet built) Forms/Dialogs: TransactionFormView (add/edit modal), DeleteAccountDialogView (two-step) Shared: BudgetFormView, BudgetCardMenuView


Key Converters

Key Purpose
HexToColorConverter ConverterParameter: color→Color, css→SVG CSS, brush→IBrush
AmountColorConverter type string → AccentRed or AccentGreen brush
AmountSignConverter MultiBinding(amount, type) → +$x.xx / -$x.xx
BoolToColorConverter ConverterParameter='#hex1|#hex2' → color
BoolToCssConverter ConverterParameter='#hex1|#hex2' → SVG CSS string
SvgPathFromName "icon-name""../Assets/Icons/icon-name.svg"
DateFormatConverter DateTime → formatted string (en-US), ConverterParameter=format
EqualValueConverter MultiBinding equality → bool
DecimalColorConverter decimal sign → one of two values
NetworthSumConverter MultiBinding(income, expenses) → net formatted
PercentageConverter MultiBinding(value, total) → percentage string
MaskToStringConverter account mask → "•••• 1234"
FirstValueConverter double[] → first element
SkPaintToBrushConverter SolidColorPaint → IBrush
AccountFromIdConverter Guid → account name string

Custom Controls & Behaviors

DateRangePicker (Clario.CustomControls)

<cc:DateRangePicker SelectionMode="SingleDate" SelectedDates="{Binding Dates}"/>
<cc:DateRangePicker SelectionMode="SingleRange" SelectedDates="{Binding Dates}"/>
<cc:DateRangePicker Classes="ghost" SelectionMode="SingleDate" SelectedDates="{Binding Dates}" Padding="12,10"/>

Has SelectedDate (DateTime?) property that syncs with SelectedDates. Uses _isSyncing guard flag to prevent feedback loops. Only processes SelectedDatesChanged when popup is open (fixes instant-close bug).

NumericTextBox (Clario.CustomControls)

Subclass of TextBox, only accepts digits and one decimal point.

<local:NumericTextBox Classes="ghost" Text="{Binding Amount, Mode=TwoWay}"/>

NumericInputBehavior (Clario.Behaviors)

Alternative to NumericTextBox, attach to regular TextBox.

<TextBox><Interaction.Behaviors><behaviors:NumericInputBehavior/></Interaction.Behaviors></TextBox>

Transaction Form (TransactionFormViewModel)

  • IsEditMode, FormTitle, FormSubtitle, SaveButtonLabel (switches add/edit)
  • SetupForAdd(categories, accounts) / SetupForEdit(transaction, categories, accounts)
  • OnSaved / OnCancelled / OnDeleted — Action callbacks
  • Commands: SetTypeCommand(string), SetTodayCommand, SaveCommand, DeleteCommand, RequestDeleteCommand, CancelCommand
  • ShowDeleteConfirm bool — confirm delete sub-modal inside form
  • Validation: IsValid bool, HasError + ErrorMessage

TransactionsViewModel — Key Details

  • _allTransactions (all), _filteredTransactions (after filters), PagedTransactions (ObservableCollection, current page)
  • Page size: 25 desktop / 10 mobile
  • LoadPage(int) — desktop pagination (clears and replaces)
  • LoadMoreCommand — mobile infinite scroll (appends, does NOT call ApplyFilters)
  • GroupTransactions() — inserts date header rows, uses _isSyncing guard, processes dates in reverse order to avoid index shifting
  • Transaction.GroupHeader = true rows act as section headers with Description = date label
  • Filters: SearchText, SelectedDateRangeOption, SelectedCategory, SelectedAccount, TransactionType, SelectedSortOption
  • ApplyFilters() does NOT re-sort at end — sort is inside the switch
  • WeakReferenceMessenger.Default.Send(new TransactionsScrollToTop()) on desktop page change

BudgetViewModel — Key Details

  • CurrentPeriod DateTime — navigable by month
  • ProcessBudgets() — sets Spent, TransactionsCount per budget, adds group headers to VisibleBudgets
  • ProcessChartData() — builds SpendingBreakdownChartSeries (half-donut) + SpendingBreakdownLegends
  • TotalLeft = TotalBudgeted - TotalSpent (clamped to 0)
  • SavingsHint — compares TotalLeft to Profile.SavingsGoal
  • DailyBudgetLeftFormatted = remaining budget ÷ days left in period

Charts (LiveCharts2)

Bar chart (SpendingByCategory on Dashboard)

// ISeries[] — one ColumnSeries<double> per category
// Replace whole array on update (don't use ObservableCollection + SeriesSource)
// ZoomMode="None" on CartesianChart to allow scroll passthrough
// Scroll forwarding in code-behind via PointerWheelChangedEvent tunnel handler

Half-donut (Budget spending breakdown)

<Border Height="150" ClipToBounds="True">
    <lvc:PieChart Series="{Binding ...}" Height="300" Margin="0,0,0,-150"
                  InitialRotation="-180" MaxAngle="180" LegendPosition="Hidden"/>
</Border>

TooltipLabelFormatter

TooltipLabelFormatter = point => $"${point.Coordinate.PrimaryValue:N0}"

Flyout Pattern

<Button.Flyout>
    <Flyout Placement="BottomEdgeAlignedRight"
            FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
        <views:SomeView/>
    </Flyout>
</Button.Flyout>

TransparentFlyoutPresenter ControlTheme defined in App.axaml resources (not Styles).

Modal Pattern (desktop)

Full-screen Grid overlay on top of ContentControl in MainView. IsVisible bound to bool on MainViewModel. Dim: <Border Background="#70000000"/> + centered card.

Bottom Sheet Pattern (mobile — AccountsViewMobile)

// public async Task ShowSheet() / HideSheet()
// TranslateTransform animation, CubicEaseOut 320ms up, CubicEaseIn 260ms down
// OverlayGrid.IsVisible = false by default in XAML
// Dismiss: DimOverlay.PointerPressed + CloseButton.Click
// Sheet height: BottomSheet.MaxHeight = Bounds.Height * 0.82 in OnAttachedToVisualTree

XAML Rules

  • Always {DynamicResource} for theme colors
  • Never hardcode hex except in SVG CSS strings
  • x:CompileBindings="False" on shell views with dynamic DataContext
  • Separator between list items: Spacing="1" on StackPanel + BorderSubtle background on container
  • Never MinWidth/MinHeight on mobile UserControls
  • Icon bg always: <SolidColorBrush Color="..." Opacity="0.15"/>
  • No BoxShadow on mobile

Performance Notes

  • Build in Release mode for realistic Android performance
  • Use VirtualizingStackPanel in ItemsControl for long lists
  • Mobile page size = 10
  • Remove BoxShadow from all mobile views
  • base.OnFrameworkInitializationCompleted() must be called FIRST in OnFrameworkInitializationCompleted

Things Currently In Progress / Not Yet Done

  • AuthViewMobile (not yet built)
  • Settings view (desktop + mobile, not yet designed)
  • Analytics view (not yet designed)
  • Edit account form (not yet built)
  • Edit budget form (BudgetFormView exists but not wired to data)
  • Light theme AppTheme variant file (not created yet)
  • Notification/alert system for budget threshold warnings
  • Real-time Supabase subscriptions (AutoConnectRealtime=true but not wired to UI refresh)

App Identity

  • Name: Clario
  • Logo: C-shaped segmented donut chart, 4 segments, blue/green/yellow/red gradient, on dark background
  • Tagline: "Your personal finance tracker"
  • Package: com.CompanyName.Clario (placeholder, not finalized)