Compare commits

..

118 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
85e08b2e9e [AI] Fix privilege escalation in sync-server /change-password and getLoginMethod (#7155)
* [AI] Fix privilege escalation in sync-server /change-password and getLoginMethod

Made-with: Cursor

* Update upcoming-release-notes/7155.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix privilege escalation issue in change-password endpoint

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-09 20:33:20 +00:00
Dustin Brewer
60e2665fcc MVP for Payee Locations (#6157)
* Phase 1: Add payee locations database schema/types

 * Add migration to create payee_locations table with proper indexes
 * Add PayeeLocationEntity type definition
 * Update database schema to include payee_locations table
 * Export PayeeLocationEntity from models index

* Phase 2: Add payee location API/services

  * Add constants for default location behavior
  * Implement location service with geolocation adapters
  * Add new API handlers for payee location operations

* Phase 3: Add location-aware UI components/hooks

 * Update mobile transaction editing with location integration
 * Enhance PayeeAutocomplete with nearby payee suggestions and forget
   functionality
 * Implement location permission and placeholder unit of measurement hooks

* Phase 4: Add YNAB5 payee location import support

 * Extend YNAB5 types to include location data from payees
 * Implement location import logic in YNAB5 importer

* Phase 5: Add unit of measurement support

 * Add unit of measurement preference setting in Format.tsx
 * Implement distance formatting utilities for imperial/metric units
 * Add useUnitOfMeasurementFormat hook for accessing preferences

* Required release note about the PR

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6157

* Actually get syncing working

This was not obvious to me, esp. with 13 years of data, but the
locations I've been inserting were local only.

Everything appeared to work.

What I failed to notice is that the locations did not sync
across devices. Of course all the location data that was imported worked
fine, but nothing new unless it was created on device.

This changes the schema and uses the proper insert/delete methods such
that syncing works.

* Remove unit of measurement preference

Display feet and meters automatically, and don't bother to format based on miles/kilometers.

* Add payeeLocations feature flag

Place the location permissions check and thus user-facing functionality behind the feature flag

* Missed adding tombstone to payee location query

* Adjust migration name to pass CI

Adjust the indexes as well

* Unify location.ts

If CodeRabbit complains again, reply that we are actively choosing a unified file

* Add bounds testing

The validation is straightforward range-checking — if it's wrong, it'll be obvious quickly. Unless there's a plan to start adding broader test coverage for that file, I'd leave it untested for now

* Prefer camelCase for the method params

* Fix the nested interactive containers

* Fix the majority of CodeRabbit nits

The remainder seem to not be related to my code change (just a lint), outdated (sql migration comment), or infeasible (sql haversine query)

* More CodeRabbit nits

* Revert unnecessary YNAB5 zip import

Turns out the payee_locations were inside the exported budget all along!

* Additional guards and other CR fixes

* Match the pattern used elsewhere in file

* YNAB5.Budget -> Budget

Missed in the merge conflict

* ci: trigger rerun

* Change import from fetch to connection module

* Correct invalid border property

Ah. I never noticed this property wasn't working. I guess the button
looked OK to me!

* Only hide the button on success

* Update packages/loot-core/src/shared/location-utils.ts

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* Update packages/loot-core/src/server/payees/app.ts

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* Fully fix typo

Guess I shouldn't commit a suggestion after waking up

* Attempting to address feedback

Manual select nearby payee and save payee location buttons to make the UX more obvi

* Remove stale file that was moved

* Additional cleanup of remnant change

Removed the references to location from a few existing entities

* Additional cleanup of remnant change

* Show the Nearby payees button even when the field is disabled

If there are nearby payees, there's not a payee already selected, and the save button isn't needed

* runQuery is not async

* Add mockNearbyPayeesResult to test

Trying to utilize the real type in the mock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-09 20:26:48 +00:00
Matiss Janis Aboltins
102be1c54d Fix shared error array in SimpleFin batch sync error handling (#7125)
* Add failing tests for SimpleFin batch sync shared error array bug

Tests prove two bugs in simpleFinBatchSync() catch block (app.ts:1100-1115):
1. All accounts share the same errors array reference
2. Errors accumulate across accounts instead of being isolated

Related: #6623, #6651, #7114

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Fix shared error array in SimpleFin batch sync catch block

When simpleFinBatchSync() threw an error, all accounts received the
same errors array by reference and errors accumulated across accounts.
Each account now gets its own isolated errors array with a single error
specific to that account, matching the pattern used by accountsBankSync().

Fixes #6623

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Remove @ts-strict-ignore from bank sync tests

Use proper non-null assertions instead of disabling strict mode.

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Add release notes for PR #7125

* [AI] Replace test() with it() to follow repo convention

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-09 19:50:18 +00:00
Julian Dominguez-Schatz
448da13cf5 Move migrations script to typescript (#7075)
* Move migrations script to typescript

* Add release notes

* Setup

* Fix

* PR feedback

* Make imports work as expected

* Rabbit
2026-03-09 07:58:03 +00:00
LeviBorodenko
41679235be [Mobile] Fix preview running balances not displaying on toggle (#7041)
* refactor(usePreviewTransactions): Move running balances to useMemo

* docs(relnotes): Add note for mobile running balance fix

* refactor(hooks): Remove unnecessary options ref
2026-03-08 22:04:52 +00:00
Matiss Janis Aboltins
73fa068fe9 [AI] Establish AI agent commit and PR guidelines (#7153)
* [AI] Extract PR/commit rules into shared agent skill

Deduplicate PR and commit instructions from AGENTS.md into a standalone
skill file at .github/agents/pr-and-commit-rules.md. This single source
of truth is consumed by both Claude Code (via CLAUDE.md @-import) and
Cursor (via .cursor/rules/pr-and-commit.mdc with alwaysApply: true).

AGENTS.md now references the shared file instead of repeating the rules
in three separate sections.

https://claude.ai/code/session_01KkHg7MYXrTyDkTw6u98Vam

* Add release notes for PR #7153

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 18:16:46 +00:00
Juulz
1fe588c143 🐞 Fix mobile transactions colors - fixes #7042 (#7047)
* Update amount styling with theme colors

* Clean up imports in TransactionListItem.tsx

Removed unused import of makeBalanceAmountStyle.

* Add release notes for bugfix in color variables

Fix color variables for mobile transaction list items.

* Change positiveColor in amount to use theme.tableText

* Change negative color style for running balance

* Fix negative color style in TransactionListItem

* Update color styles for transaction amount display

* Update upcoming-release-notes/7047.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7047

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 15:04:30 +00:00
mibragimov
edce092ae8 fix(csv-import): trim whitespace from amount strings before parsing (#7149)
* fix(csv-import): trim whitespace from amount strings before parsing

looselyParseAmount relies on a regex anchored at $ to detect decimal
markers. Trailing whitespace (e.g. from Excel-saved CSVs) shifts the
pattern match so a thousands separator is misidentified as a decimal
point, producing wildly wrong values.

Adding trim() at the top of the function eliminates trailing/leading
whitespace before any regex logic runs.

Fixes actualbudget/actual#7121

* chore: add release notes for #7149
2026-03-07 20:41:47 +00:00
dependabot[bot]
77411394f6 Bump dompurify from 3.3.0 to 3.3.2 (#7143)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.0 to 3.3.2.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.0...3.3.2)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-07 20:24:41 +00:00
dependabot[bot]
235d94478f Bump express-rate-limit from 8.2.1 to 8.2.2 (#7140)
* Bump express-rate-limit from 8.2.1 to 8.2.2

Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.2.2)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* [AI] Update express-rate-limit to 8.3.0 to fix GHSA-46wh-pxpv-q5gq vulnerability

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Add release notes for PR #7140

* [AI] Update release notes to reflect version 8.3.0

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 19:48:56 +00:00
Matiss Janis Aboltins
7e0edd43ec Sort theme catalog items alphabetically by name (#7144)
* [AI] Sort custom theme catalog options alphabetically in the UI

Sort catalog themes by name using localeCompare before rendering,
without modifying the underlying JSON data file.

https://claude.ai/code/session_01Y5SGaVYqsVWVsvXV8ZFXj3

* Add release notes for PR #7144

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 23:01:07 +00:00
Matiss Janis Aboltins
fdf5c8d0a9 [AI] Move window typings import to globals.ts (#7142)
* [AI] Remove parent path import of window.ts from desktop-client tsconfig

Replace the `../../packages/loot-core/typings/window.ts` include in
desktop-client's tsconfig.json with a proper package import. This adds
a `./typings/*` export to loot-core's package.json and creates a
globals.ts file in desktop-client that imports the window types via
the package name.

https://claude.ai/code/session_01GrgAzjWd3XvqwBTfXLerxc

* [autofix.ci] apply automated fixes

* Add release notes for PR #7142

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 23:00:50 +00:00
Matt Fiddaman
a8ec84ceac stop font size fluctuations showing in summary cards (#7092)
* stop summary cards from showing until font size settled

* note
2026-03-06 23:00:41 +00:00
Michael Clark
b727124603 Fix docker images (#7146)
* fix docker images

* release notes
2026-03-06 22:55:21 +00:00
dependabot[bot]
8bb7f207f2 Bump svgo from 3.3.2 to 3.3.3 (#7130)
Bumps [svgo](https://github.com/svg/svgo) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 3.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 21:56:01 +00:00
dependabot[bot]
6e0c15eb12 Bump immutable from 5.1.3 to 5.1.5 (#7129)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.3 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.3...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-06 21:51:14 +00:00
Michael Clark
4e2cec2c7a 🧪 Improving docker image test resiliency (#7141)
* improving test resiliency

* release notes
2026-03-06 21:02:31 +00:00
Matiss Janis Aboltins
078603cadf [AI] Implement sync recovery (#7111)
* [AI] Fix iOS/Safari sync recovery (fixes #7026): useOnVisible hook, re-fetch server version on visible, improved network-failure message

Made-with: Cursor

* Feedback: coderabbitai

* Refactor useOnVisible test: remove unnecessary resolve check and simplify callback definition
2026-03-04 23:27:15 +00:00
Joel Jeremy Marquez
b3a86b5392 Update remaining accounts hooks to return react query states (#7071)
* Update remaining accounts hooks to return react query states

* Add release notes for PR #7071

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-04 23:13:55 +00:00
Mats Nilsson
295a565e55 Fix budget analysis report padding (#7118)
* Fix budget analysis report padding

The padding on the report is too small when the value is large.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 22:08:30 +00:00
Matiss Janis Aboltins
387c8fce16 [AI] Enable TypeScript composite project references across monorepo (#7062)
* [AI] Enable TypeScript composite project references across monorepo

- Add composite and declaration emit to all package tsconfigs
- Wire root and per-package project references in dependency order
- Replace cross-package include-based typing with referenced outputs
- Fix api TS5055 by emitting declarations to decl-output
- Add desktop-client alias for tests; fix oxlint import order in vite.config
- Add UsersState.data null type and openDatabase return type for strict emit

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove obsolete TypeScript configuration for API and update build script to emit declarations directly to the output directory. This streamlines the build process and ensures compatibility with the new project structure.

* Refactor TypeScript configuration in API package to remove obsolete decl-output directory and update build scripts. The changes streamline the build process by directing declaration outputs to the @types directory, ensuring better organization and compatibility with the new project structure.

* Add TypeScript declaration emission for loot-core in desktop-electron build process

* Refactor TypeScript configuration in API package to utilize composite references and streamline build scripts. Update include and exclude patterns for improved file management, ensuring better organization of declaration outputs and migration SQL files.

* Refactor TypeScript configuration in loot-core and desktop-client packages to streamline path management and remove obsolete dependencies. Update paths in tsconfig.json files for better organization and compatibility, and adjust yarn.lock to reflect changes in workspace dependencies.

* Update desktop-electron package to utilize loot-core as a workspace dependency. Adjust TypeScript import paths and tsconfig references for improved organization and compatibility across packages.

* Enhance Vite configuration for desktop-client to support Electron-specific conditions and update loot-core package.json to include Electron as a platform for client connection. This improves compatibility for Electron builds.

* Refactor TypeScript configuration across multiple packages to streamline path management. Update tsconfig.json files in root, api, and loot-core packages to improve import paths and maintain compatibility with internal typings.

* Update package dependencies and Vite configuration across component-library and desktop-client. Add vite-tsconfig-paths to component-library and remove it from desktop-client. Refactor Storybook preview file to include a TODO for future refactoring.

* Remove Node-specific path from loot-core package.json for client connection, streamlining platform configuration for Electron.

* Remove loot-core as a workspace dependency from desktop-electron package.json

* Update tsconfig.json to remove reference to desktop-client

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 20:57:06 +00:00
YHC
c7ebfd8ad4 Add TWD Currency (#7095)
* Add New Taiwan Dollar to currency list

* Add New Taiwan Dollar to currency list

* Fix decimalPlaces for New Taiwan Dollar

Updated decimalPlaces for New Taiwan Dollar from 0 to 2, as suggested by coderabbitai, "Line 62 introduces TWD as zero-decimal, but this codebase currently has an unresolved zero-decimal conversion/storage issue. This risks incorrect persisted amounts for TWD transactions."

* Add upcoming-releass-notes

* Add New Taiwan Dollar (NT$) to available currency

* Add New Taiwan Dollar (NT$) to available currency
2026-03-04 18:46:20 +00:00
Matiss Janis Aboltins
e1f834371b [AI] Refactor YNAB importers to use server-side send() and handler API (#7050)
* [AI] Refactor YNAB importers to use server-side send() and handler API

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename 7049.md to 7050.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:33 +00:00
Matiss Janis Aboltins
4caee99955 [AI] Strengthen schedule rule types in loot-core (remove ts-expect-error) (#7051)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:20 +00:00
Matiss Janis Aboltins
286d05d187 [AI] Move ImportTransactionsOpts from api package to loot-core (#7053)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:09 +00:00
Matiss Janis Aboltins
cf05a7ea01 [AI] Typescript: low hanging fruit (#7091)
* [AI] Use loot-core workspace in desktop-electron and fix related types

- Add loot-core as workspace dependency in desktop-electron
- Import GlobalPrefsJson from loot-core package in desktop-electron
- Allow null in usersSlice data type (UsersState)
- Add explicit SQL.Database return type to openDatabase in sqlite electron

Made-with: Cursor

* Re-add loot-core as a workspace dependency in desktop-electron
2026-03-04 18:32:01 +00:00
Michael Clark
b373b612a4 :electron: Electron backups converted to zip for easy importing (#7069)
* e:electron: electron backups converted to zip for easy importing

* release notes

* fix lint

* suggestion from rabbit

* Change category from Maintenance to Enhancements
2026-03-04 18:00:07 +00:00
Copilot
3797cff716 [AI] Skip AI-generated release notes for release/ branch PRs (#7107)
* Initial plan

* [AI] Make AI-generated release notes not run on release/ branches

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* [AI] Use explicit startsWith(github.head_ref) if conditions instead of JS changes

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* Delete extra condition

* Add release notes for PR #7107

* [autofix.ci] apply automated fixes

* FIx note

* Add head branch information to PR details

* Refactor conditions for release notes generation

* Sec

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 17:40:32 +00:00
Michael Clark
9e2793d413 :electron: Flathub pr on publish of release assets (#7117)
* flathub pr on publish of release

* docs

* release notes

* rabbit feedback
2026-03-04 17:05:47 +00:00
Julian Dominguez-Schatz
3201819df9 Display/save limit/refill templates (#6693)
* Handle refill templates in automations modal

* Add release notes

* Fix typecheck

* Fix icon style

* Style fixes

* Fix migration

* Fix typecheck

* Rabbit

* Add tests
2026-03-04 16:41:07 +00:00
Josh Woodward
eca50f28b0 Fix skipping schedules that move before weekend (#7057)
* Pushing before weekend to Monday

* release notes

* lint

* Guard against null

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fixing AI commit and addressing second comment

* addressing nitpicks

* first attempt at a test

* [autofix.ci] apply automated fixes

* refactor to use condition and fix tests

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 16:40:41 +00:00
dependabot[bot]
c82ee91b12 bump bn.js from 4.12.2 to 4.12.3 (#7073)
* Bump bn.js from 4.12.2 to 4.12.3

Bumps [bn.js](https://github.com/indutny/bn.js) from 4.12.2 to 4.12.3.
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v4.12.2...v4.12.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 4.12.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-03-04 16:40:25 +00:00
Julian Dominguez-Schatz
cb8ff337dc Migrate sync server general utils to typescript (#7074)
* Migrate sync server general utils to typescript

* Add release notes

* Rabbit

* Fix
2026-03-04 16:40:24 +00:00
Matiss Janis Aboltins
c37a5a02aa [AI] Add CLAUDE.md with reference to AGENTS.md (#7105)
* [AI] Add CLAUDE.md with reference to AGENTS.md

Made-with: Cursor

* Add release notes for PR #7105

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-04 16:40:14 +00:00
Matiss Janis Aboltins
f9e09ca59b 🔖 (26.3.0) (#7097)
* 🔖 (26.3.0)

* Remove used release notes

* Add release notes for PR #7097

* Remove used release notes

* Remove used release notes

* Add release notes for version 26.3.0

* Add new terms to spelling expectation list

* Fix spelling and capitalization in release notes

Corrected spelling of 'reorganisation' to 'reorganization' and updated 'coderabbit' to 'CodeRabbit' for consistency.

* Update patterns.txt to allowlist 'CodeRabbit'

Add 'CodeRabbit' to allowlist of proper nouns.

* Clarify chart theming support in release notes

Updated the release notes to specify bar/pie chart theming support and added details about theme variables for customization.

* Remove 'CodeRabbit' from spelling expectations

* Refactor release notes and improve formatting

Reorganize release notes for clarity and update content.

* Create 2026-03-02-release-26-3-0.md

* Change release date to 2026-03-02

Updated the release date for version 26.3.0.

* Update release notes for version 26.3.0

---------

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
2026-03-03 01:23:12 +00:00
Matiss Janis Aboltins
8081b8829e [AI] Make merge-freeze-unfreeze workflow work on fork PRs (#7104)
* [AI] Make merge-freeze-unfreeze workflow work on fork PRs via pull_request_target

Made-with: Cursor

* Add release notes for the "unfreeze" workflow functionality in fork PRs

* Add empty permissions block to unfreeze job in merge-freeze-unfreeze workflow
2026-03-01 20:46:58 +00:00
Matt Fiddaman
f2f79d378c fix bugfix categorisation in contributor points counting script (#7103)
* s/bugfix/bugfixes

* note

* add alias functionality

* note update
2026-03-01 16:33:58 +00:00
Julian Dominguez-Schatz
c5cca67399 Revert "feat(currency): Add Vietnamese Dong (VND) currency" (#7100)
* Revert "feat(currency): Add Vietnamese Dong (VND) currency (#6902)"

This reverts commit 7fa9fa900b.

* Add release notes for PR #7100

* Delete 7100.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-01 07:31:29 +00:00
Matiss Janis Aboltins
eabf09587f [AI] Github action for unfreezing PRs (#7094)
* [AI] Add GitHub Action to add PR to Merge Freeze unblocked list when unfreeze label is added

Made-with: Cursor

* Rename 7093.md to 7094.md

* Add concurrency control to unfreeze job in merge-freeze-unfreeze workflow

- Introduced concurrency settings to prevent overlapping executions of the unfreeze job based on labels.
- Updated error handling to abort the process if fetching the current merge freeze status fails, ensuring unblocked PRs are not overwritten.

* Refactor Merge Freeze workflow to simplify PR unblocking process

- Updated the workflow to directly post the PR to the unblocked list without fetching the current freeze status.
- Improved error handling by ensuring the access token is set before proceeding with the API call.
2026-02-28 21:15:11 +00:00
Matiss Janis Aboltins
6022929551 [Cursor] Development environment setup (#7088)
* [AI] Add Cursor Cloud specific instructions to AGENTS.md

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Add release notes for PR #7088

* [AI] Fix Node.js minimum version requirement in AGENTS.md (#7089)

* [AI] Fix outdated Node.js version requirement in Build Failures section

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Add test budget tip to Cursor Cloud instructions

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-02-27 20:38:37 +00:00
Copilot
e65429497d [AI] Remove 'suspect ai generated' label and associated workflow (#7087)
* Initial plan

* [AI] Remove 'suspect ai generated' label and associated workflow

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* [AI] Remove 'suspect ai generated' label from coderabbit config

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add release notes for PR #7087

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-26 20:51:47 +00:00
Matiss Janis Aboltins
3758d72b65 Mobile rules item alignment (#7081)
* [AI] Fix mobile rules list items to be full width and left-aligned

- Override Button's default justifyContent/alignItems centering in
  ActionableGridListItem to use flex-start alignment
- Add width: 100% to RulesListItem's SpaceBetween to fill the item width

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Add release notes for PR #7081

* Change category from Enhancements to Bugfix

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-26 20:22:29 +00:00
Matiss Janis Aboltins
032d10ac42 [AI] Fix API build output path (dist/index.js instead of dist/api/index.js) (#7084)
* [AI] Fix API build output path (dist/index.js instead of dist/api/index.js)

- Set rootDir in packages/api/tsconfig.json so output is under dist/ not dist/api/
- Remove loot-core pegjs.ts from include; add local typings/pegjs.d.ts
- Use mkdir -p in build:migrations for idempotent build
- Exclude **/@types/** so declaration output does not conflict with input

Made-with: Cursor

* Update TypeScript configuration in api package to refine exclude patterns
2026-02-26 20:20:38 +00:00
Michael Clark
f97a89dc28 🐛 Fix file path on windows (#7076)
* fix file path on windows

* file path in migrations

* release notes
2026-02-25 15:00:10 +00:00
Juulz
a4bd301ec6 🐞 Midnight theme: Change menuAutoCompleteTextHover color - Fixes #7029 (#7048)
* Change menuAutoCompleteTextHover color to green400

* Change menuAutoCompleteTextHover color to green400 in Midnight theme.

Change menuAutoCompleteTextHover color to green400 in Midnight theme.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-24 17:29:09 +00:00
Julian Dominguez-Schatz
18072e1d8b Validate file IDs for correctness (#7067)
* Validate file IDs for correctness

* Add release notes
2026-02-24 15:32:50 +00:00
Juulz
a1e0b3f45d Rename theme 'Okabe Ito' to 'Color-blind (dark)' (#7058)
* Rename theme 'Okabe Ito' to 'Color-blind (dark)'

* Rename 'Okabe Ito' theme to 'Color-blind (dark)'

* Fix capitalization in theme name for consistency
2026-02-23 18:49:45 +00:00
Matiss Janis Aboltins
0b361e45b4 [AI] Bump version to 26.2.1 (#7052)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 15:52:58 +00:00
Matiss Janis Aboltins
b3052dda05 v26.2.1: critical security fix for simplefin, pluggy and multi-user (#7043)
* Add release notes for version 26.2.1, including critical security fixes for SimpleFin, Pluggy, and multi-user setups. Remove outdated upcoming release notes for related bugfixes.

* Add release notes for PR #7043

* Delete upcoming-release-notes/7043.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-22 14:43:50 +00:00
Matiss Janis Aboltins
31a027fc64 [AI] Add per-package tsconfigs and typescript-strict-plugin for typecheck (#7019)
* [AI] Add per-package tsconfigs and typescript-strict-plugin for typecheck

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update TypeScript configuration across multiple packages to correct plugin path key from "path" to "paths" and add reference to process-worker typings in index.electron.ts.

* Remove reference to process-worker typings in index.electron.ts and add new process-worker typings file for global Process augmentation.

* Refactor TypeScript build configurations across multiple packages by removing tsconfig.dist.json files and updating build scripts to use default TypeScript compilation. Adjusted compiler options to target ES2021 and enable declaration generation.

* Update TypeScript configuration in api package to refine include and exclude patterns for better file management.

* Update build script in api package to ensure migration SQL files are copied to the correct directory by creating the destination folder if it doesn't exist.

* Update TypeScript configurations in crdt and desktop-electron packages to refine include and exclude patterns for improved file management.

* Update TypeScript dependencies across multiple packages to include typescript-strict-plugin for enhanced type checking and maintain consistency in package.json files.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 21:26:22 +00:00
Julian Dominguez-Schatz
cfc18c240a Add limit/refill automation types (#6692)
* Add limit/refill automation components

* Add release note

* Fix typecheck

* Rabbit PR feedback

* Review
2026-02-21 20:58:54 +00:00
Matiss Janis Aboltins
a68b2acac3 [AI] Enforce file access authorization on sync API endpoints (#7040)
* [AI] Enforce file access authorization on sync API endpoints

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor file deletion authorization to return error message as text

* Refactor file upload validation to improve error handling

* Add tests to allow admin users to retrieve encryption keys and sync files for other users

- Implemented a test for admin access to retrieve encryption keys for another user's file in the /user-get-key endpoint.
- Added a test for admin users to sync another user's file in the /sync endpoint, ensuring proper response and headers.

These changes enhance the authorization checks for admin actions on user files.

* Refactor file cleanup in tests to use onTestFinished for better error handling

* Enhance admin capabilities in file management tests

* Add migration to backfill file owners with admin ID

* Enhance file access authorization in sync API

* Update migration to backfill file owners with admin ID to ensure consistent ordering in the query

* Refactor access control tests for file downloads in sync API

* Add test for non-owner file download access via user_access in sync API

This test verifies that users with appropriate access can download files owned by others, utilizing the requireFileAccess logic and UserService.countUserAccess. It ensures correct response headers and content delivery for shared files.

* Refactor file cleanup in upload and download tests to utilize onTestFinished for improved error handling

This update consolidates file cleanup logic in the test suite, ensuring that temporary files are removed after each test execution. The changes enhance the reliability of tests by consistently managing file state across various scenarios.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 08:45:48 +00:00
Matiss Janis Aboltins
a25be5c95c [AI] Remove usage of 'web' file types (#7033)
* [AI] Desktop client, E2E, loot-core, sync-server and tooling updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor database handling in various modules to use async/await for improved readability and error handling. This includes updates to database opening and closing methods across multiple files, ensuring consistent asynchronous behavior. Additionally, minor adjustments were made to encryption functions to support async operations.

* Refactor sync migration tests to utilize async/await for improved readability. Updated transaction handling to streamline event expectations and cleanup process.

* Refactor various functions to utilize async/await for improved readability and error handling. Updated service stopping, encryption, and file upload/download methods to ensure consistent asynchronous behavior across the application.

* Refactor BudgetFileSelection component to use async/await for onSelect method, enhancing error handling and readability. Update merge tests to utilize async/await for improved clarity in transaction merging expectations.

* Refactor filesystem module to use async/await for init function and related database operations, enhancing error handling and consistency across file interactions. Updated tests to reflect asynchronous behavior in database operations and file writing.

* Fix typo in init function declaration to ensure it returns a Promise<void> instead of Proise<void>.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

* Update tests to use async/await for init function in web filesystem, ensuring consistent asynchronous behavior in database operations.

* Update VRT screenshot for payees filter test to reflect recent changes

* Update filesystem module to remove web-specific implementations and streamline path handling. Refactor file operations to enhance type safety and consistency across different environments. Add tests for SQLite interactions and ensure proper handling of database transactions.

* Add release notes for maintenance: Remove usage of 'web' file types

* Refactor filesystem module to use type annotations for exports and improve consistency across methods. Remove deprecated web file handling and enhance encryption functions for better browser compatibility.

* Trigger CI

* Add asyncStorage API file to export Electron index module

* Trigger CI

* Feedback: typo

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-20 18:01:36 +00:00
Pratik Silwal
bf1947a119 Prevent single-slash paths from being parsed as filepaths (#6966)
* fix: prevent single-slash paths from being parsed as filepaths

* add release notes

* [autofix.ci] apply automated fixes

* test: add tests related to filepath

* additonal test from coderabbit

---------

Co-authored-by: Pratik Silwal <pratiksilwal@Pratiks-MacBook-Air.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-20 12:38:29 +00:00
Mats Nilsson
37ad0ed563 Hide selected accounts in the mobile filter (#7030)
When filtering for accounts in the mobile view of reports, hide the
already selected accounts.
2026-02-20 12:34:57 +00:00
Daniel Bates
89d68ea2f8 Add tooltip to imported payee in rule result window (#7031)
* Add tooltip to imported payee column in rule result window

The imported payee column in SimpleTransactionsTable was missing a
title attribute, so truncated text had no tooltip on hover. Other
columns (category, account, notes) already pass title for this purpose.

Fixes #7003

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add release notes for #7031

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Your Name <your-email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:32:13 +00:00
Kenny McCormick
1ad7b6f781 Add ACTUAL_USER_CREATION_MODE documentation to oauth-auth.md (#6935)
* Add ACTUAL_USER_CREATION_MODE documentation to oauth-auth.md

* [autofix.ci] apply automated fixes

* add note that first external auth user is admin and owner

Added details about admin permissions and server ownership for users authenticating with OpenID/OAuth2.

* improve ACTUAL_USER_CREATION_MODE environment documentation

clarify warning about server owner

* [autofix.ci] apply automated fixes

* move first user admin warning to "after setup" section of OIDC documentation

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-20 02:24:43 +00:00
Matiss Janis Aboltins
fd9ee868a6 Enhance PR template with structured sections (#6989)
* Enhance PR template with description, type of change, and checklist sections

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update PR template to streamline instructions for writing release notes

* Remove unnecessary lines from the PR template to streamline instructions for writing release notes

* Add release notes for PR #6989

* Update category in release notes

Changed category from Enhancements to Maintenance.

* Update PULL_REQUEST_TEMPLATE.md

* Update AGENTS.md and PULL_REQUEST_TEMPLATE.md to clarify PR submission guidelines

* Update 6989.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-19 22:46:30 +00:00
Matiss Janis Aboltins
0de44af1de Require authentication for SimpleFIN and Pluggy.ai endpoints (#7034)
* Add authentication middleware to SimpleFIN and Pluggy.ai endpoints

Protect /simplefin/* and /pluggyai/* routes with validateSessionMiddleware
so only authenticated users can access bank account and transaction data.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Release notes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:46:10 +00:00
Juulz
cf58712bf1 🎨 High Contrast Light theme for Actual (#7032)
* Update customThemeCatalog.json

* [autofix.ci] apply automated fixes

* Create 7032.md

🎨 High contrast light theme.

* Update customThemeCatalog.json

* Update 7032.md

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-19 21:39:25 +00:00
Dagur Páll Ammendrup
6460af3de4 Set inital focus on category when covering overspending (#7012)
* Set inital focus on category when covering overspending

* Fixup: Make sure that the amount is set

* Unused import

* Fix bug where typing an amount and pressing enter uses previous value

---------

Co-authored-by: Dagur Ammendrup <dagurp@vivaldi.com>
2026-02-19 18:51:07 +00:00
Pratik Silwal
ce890faeeb Fix Net Worth Calculations (#6968)
* fix: computed priorPeriodNetWorth and use it as a baseline for net worth calculations

* add release notes

* correct spelling

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-19 17:06:21 +00:00
Matt Fiddaman
848b86cd59 ⬆️ recharts (3.4.0 → 3.7.1) (#7022)
* recharts (3.4.1 -> 3.7.0)

* Cell & activeShape deprecation

* note

* fix textAnchor

* remove Cell in BarGraph
2026-02-19 16:28:41 +00:00
Crhistopher Suriel
ec22923f18 feat(currency): Add Dominican Peso (DOP) currency (#7028)
* feat(currency): Add Dominican Peso (DOP) currency

* Add release notes
2026-02-19 14:50:14 +00:00
dependabot[bot]
27402ee2b3 Bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 (#7020)
* Bump @isaacs/brace-expansion from 5.0.0 to 5.0.1

Bumps @isaacs/brace-expansion from 5.0.0 to 5.0.1.

---
updated-dependencies:
- dependency-name: "@isaacs/brace-expansion"
  dependency-version: 5.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-19 14:48:58 +00:00
Matiss Janis Aboltins
0472211925 [AI] lint: await-thenable, no-floating-promises (#6987)
* [AI] Desktop client, E2E, loot-core, sync-server and tooling updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor database handling in various modules to use async/await for improved readability and error handling. This includes updates to database opening and closing methods across multiple files, ensuring consistent asynchronous behavior. Additionally, minor adjustments were made to encryption functions to support async operations.

* Refactor sync migration tests to utilize async/await for improved readability. Updated transaction handling to streamline event expectations and cleanup process.

* Refactor various functions to utilize async/await for improved readability and error handling. Updated service stopping, encryption, and file upload/download methods to ensure consistent asynchronous behavior across the application.

* Refactor BudgetFileSelection component to use async/await for onSelect method, enhancing error handling and readability. Update merge tests to utilize async/await for improved clarity in transaction merging expectations.

* Refactor filesystem module to use async/await for init function and related database operations, enhancing error handling and consistency across file interactions. Updated tests to reflect asynchronous behavior in database operations and file writing.

* Fix typo in init function declaration to ensure it returns a Promise<void> instead of Proise<void>.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

* Update tests to use async/await for init function in web filesystem, ensuring consistent asynchronous behavior in database operations.

* Update VRT screenshot for payees filter test to reflect recent changes

* [AI] Fix no-floating-promises lint error in desktop-electron

Wrapped queuedClientWinLogs.map() with Promise.all and void operator to properly handle the array of promises for executing queued logs.

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Refactor promise handling in global and sync event handlers

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-19 14:22:05 +00:00
Matt Fiddaman
a38104244a ⬆️ @playwright/test (1.57.0 → 1.58.2) (#7021)
* @playwright/test (1.57.0 → 1.58.2)

* note

* disable moving nav bar animations

* vrt
2026-02-19 00:19:54 +00:00
Matiss Janis Aboltins
77b848ca84 [AI] Allow var(--name) in custom theme CSS (no fallbacks) (#7018)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 22:16:25 +00:00
Matt Fiddaman
5179ac7c2d ⬆️ mid month dependency bump (#7013)
* baseline-browser-mapping (^2.9.14 → ^2.9.19)

* minimatch (^10.1.1 → ^10.1.2)

* lage (^2.14.15 → ^2.14.17)

* react-aria-components (^1.14.0 → ^1.15.1)

* @storybook/addon-a11y (^10.2.0 → ^10.2.7)

* p-limit (^7.2.0 → ^7.3.0)

* better-sqlite3 (^12.5.0 → ^12.6.2)

* vitest (^4.0.16 → ^4.0.18)

* @storybook/addon-docs (^10.2.0 → ^10.2.7)

* @storybook/react-vite (^10.2.0 → ^10.2.7)

* eslint-plugin-storybook (^10.2.0 → ^10.2.7)

* storybook (^10.2.0 → ^10.2.7)

* @codemirror/state (^6.5.3 → ^6.5.4)

* @swc/core (^1.15.8 → ^1.15.11)

* @tanstack/react-query (^5.90.19 → ^5.90.20)

* @vitejs/plugin-basic-ssl (^2.1.3 → ^2.1.4)

* @vitejs/plugin-react (^5.1.2 → ^5.1.3)

* i18next (^25.7.4 → ^25.8.4)

* react-aria (^3.45.0 → ^3.46.0)

* react-hotkeys-hook (^5.2.1 → ^5.2.4)

* react-i18next (^16.5.1 → ^16.5.4)

* sass (^1.97.2 → ^1.97.3)

* @easyops-cn/docusaurus-search-local (^0.52.2 → ^0.52.3)

* react (^19.2.3 → ^19.2.4)

* react-dom (^19.2.3 → ^19.2.4)

* component lib peer deps

* eslint-vitest-rule-tester (^3.0.1 → ^3.1.0)

* lru-cache (^11.2.4 → ^11.2.5)

* ua-parser-js (^2.0.7 → ^2.0.9)

* cors (^2.8.5 → ^2.8.6)

* @babel/core (^7.28.5 → ^7.29.0)

* @types/node (^22.19.3 → ^22.19.10)

* react (mixed → 19.2.4)

* @testing-library/react (16.3.0 → 16.3.2)

* react-router (7.12.0 → 7.13.0)

* vite-plugin-node-polyfills (^0.24.0 → ^0.25.0)

* pluggy-sdk (^0.79.0 → ^0.83.0)

* note
2026-02-18 22:09:28 +00:00
youngcw
2bb5a861c1 📖 start of reports dashboard updates (#6976)
* start of reports dashboard updates

* spelling

* Add release notes for PR #6976

* don't need one of these

* consistent naming

* bunny fixes

* images

* image again

* extenion

* fix

* consistent naming

* naming

* fix

* last one

* Ill be done with this someday

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-18 22:07:01 +00:00
Karim Kodera
bc32f4fcde Document graph color variables for custom themes (#7011)
* Document graph color variables for custom themes

Added documentation for graph color variables in custom themes.

* [autofix.ci] apply automated fixes

* Update custom themes documentation for clarity

Clarify the impact of color palettes on custom report widget graphs and format the list of color variables.

* Update packages/docs/docs/experimental/custom-themes.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Fix color variable syntax in custom themes documentation

Updated color variable syntax for chart colors in documentation.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-18 18:24:52 +00:00
Joel Jeremy Marquez
848eaadb0f Move redux state to react-query - payees states (#6880)
* Move redux state to react-query - account states

* Fix onbudget and offbudget displaying closed accounts

* Move redux state to react-query - payees states

* Add release notes for PR #6880

* Replace usage of logger in desktop-client with console

* Address feedback on adding default data to usePayees (#6931)

* Initial plan

* Add default data to usePayees usages using inline destructuring

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Fix imports

* Update empty payees list test

* Cleanup and simplify AccountEntity definition to fix `satisfies` syntax

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-18 17:04:26 +00:00
Joel Jeremy Marquez
5ac2947342 Update useAccounts to return react query states (#7009)
* Fix redirect to accounts page when no accounts exists

* Add release notes for PR #7007

* Use isFetching

* Update useAccounts to return react query states (e.g. isPending, isFetching, etc.)

* Add release notes for PR #7009

* Delete upcoming-release-notes/7007.md

* Change category from Enhancements to Maintenance

Refactor `useAccounts` to improve data handling.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-18 05:32:42 +00:00
Joel Jeremy Marquez
da0154a41b Fix redirect to accounts page when no accounts exists (#7007)
* Fix redirect to accounts page when no accounts exists

* Add release notes for PR #7007

* Use isFetching

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-17 22:33:49 +00:00
Joel Jeremy Marquez
180a38890c Improve category server app and react query mutation error handling (#6958)
* Improve category server app and react query mutation error handling

* Add release notes for PR #6958

* Fix test

* Fix throwing async test

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Do not swallow exceptions when batching messages - propagate instead

* Update error type to make 'cause' optional

Make 'cause' property optional in error type.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-17 20:30:57 +00:00
Roy
d3f2f1f7ae Add reports to command bar (#7002)
* Add reports to command bar

* Add release notes
2026-02-17 20:03:44 +00:00
Karim Kodera
c7efb61b84 Add theming to charts and hence allowing custom themes on charts (#6909)
* Add theming to charts and hence allowing custom themes on charts

* Removing additional color scales for charts.

* Fixed return fail over value.
2026-02-17 20:00:21 +00:00
Pieter Ouwerkerk
d605d59d01 Add Tag API docs (#6979)
* Add Tag object type definition for API docs

* Add Tags nav entry to API reference sidebar

* Add Tags methods and examples to API reference
2026-02-17 15:54:25 +00:00
HadiAyache
f7227f4e62 Fix operator precedence grouping for */ and +/- (#6993)
* Fix operator precedence grouping for */ and +/-

* Add release note for #6993

* Fix exponent associativity and add regression test

---------

Co-authored-by: Hadi Ayache <hadiayache@Hadis-Mac-mini.local>
2026-02-17 09:23:15 +00:00
Joel Jeremy Marquez
253530e239 Retrofit dashboard hooks to use react-query (#6957)
* Retrofit useReports to use react-query under the hood

* Add release notes for PR #6951

* Update 6951.md

* Report mutations

* Fix react query cache not being cleared when switching budgets (#6953)

* Fix react query cache not being cleared when switching budgets

* React does not want to export function from src/index

* Release note

* Use react-query is dashboard queries and mutations

* Add release notes for PR #6957

* [autofix.ci] apply automated fixes

* Fix typecheck errors

* Coderabbit feedback

* Make error cause optional

* Rename useDashboardWidgetCopyMenu and update useDashboardWidget to accept object to prevent need to default id to empty string

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-16 22:26:32 +00:00
Julian Dominguez-Schatz
e8f6ceeb98 Address some more low-hanging fruit for ts-strict-ignore (#6992)
* Address some more low-hanging fruit for ts-strict-ignore

* Add release notes

* Fix small issues

* Rabbit
2026-02-16 15:30:27 +00:00
Matiss Janis Aboltins
c031d9aa4f fix(ios): restore status bar color on iOS 26.2 (Safari 26) (#6983)
* fix(ios): restore status bar color on iOS 26.2 (Safari 26)

Safari 26 no longer uses the theme-color meta tag for the status bar
and instead derives the tint from the body background. Set body
background-color in HTML and sync it with the app theme so the status
bar shows purple (or the active theme) instead of white.

Fixes #6946

Co-authored-by: Cursor <cursoragent@cursor.com>

* [autofix.ci] apply automated fixes

* refactor(theme): update useMetaThemeColor to accept theme values directly

Modified the useMetaThemeColor hook to accept theme color values instead of predefined keys. Updated FinancesApp and ManagementApp components to utilize the new implementation, ensuring proper theme color handling based on screen width.

* [autofix.ci] apply automated fixes

* refactor(theme): remove unused body background color in index.html and add tests for useMetaThemeColor hook

Deleted the commented-out body background color in index.html to clean up the code. Added comprehensive tests for the useMetaThemeColor hook to ensure proper handling of theme colors, including support for CSS variables and reactivity to theme changes.

* refactor(theme): improve getPropertyValueFromVarString function in useMetaThemeColor hook

* [autofix.ci] apply automated fixes

* Add release notes for PR #6983

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-15 19:19:50 +00:00
Matiss Janis Aboltins
d4e25f4047 [AI] Introduce type-aware oxlint and disable no-duplicate-type-constituents rule (#6984)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 19:05:57 +00:00
Matiss Janis Aboltins
a7f96a59fa [AI] Update CodeRabbit config for suspect AI generated labels (#6985)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 18:53:08 +00:00
youngcw
8af64ddd5e 📖 Move pluggy.ai doc out of experimental (#6975)
* move pluggy out of experimental

* remove note

* Add release notes for PR #6975

* fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 22:04:18 +00:00
Julian Dominguez-Schatz
26dbb219aa Implement missing logic for limit template type (#6690)
* core: support limit refill templates

* notes: refill templates

* core: apply refill limits during runs

* core: prioritize refill limits

* Patch

* Update release note

* Fix typecheck

* rework.  Tests and template notes still need reworked

* fix parser syntax

* Fix type issue

* Fix after rebase, support merging limit+refill

* PR feedback

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-14 21:52:48 +00:00
Alvin Zhao
c6656a2815 Include category group in transaction export (#6960)
* include category group in transaction export

* Apply suggestion from @yzAlvin

Co-authored-by: Alvin Zhao <yzalvin@duck.com>

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-14 16:31:27 +00:00
Julian Dominguez-Schatz
6358345286 Fix some low-hanging-fruit @ts-strict-ignore (#6969)
* Fix low-hanging-fruit `@ts-strict-ignore`

* Add release notes

* A few more
2026-02-14 15:58:25 +00:00
Roy
5943ae3df5 Add filter option for category groups (#6834)
* Add filter by category groups

* Add tests

* Add release notes

* [autofix.ci] apply automated fixes

* Fix typecheck findings

* Fix modal

* Address nitpick comment (filterBy)

* Fix e2e tests

* Make group a subfield of category

* Fix test by typing in autocomplete

* Replace testId with a11y lookups

* Apply new type import style rules

* Apply feedback

* Improve typing on array reduce, remove manual type coercion

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-14 15:03:11 +00:00
Matiss Janis Aboltins
7e8a118411 [AI] lint: convert oxlint warnings to errors (#6970)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 13:48:50 +00:00
Joel Jeremy Marquez
465608c76b Move redux state to react-query - account states (#6140)
* Fix typecheck errors

* Move redux state to react-query - account states

* TestProviders

* Add release notes for PR #6140

* Fix lint error

* Fix TestProviders

* Coderabbit feedback

* Cleanup

* [autofix.ci] apply automated fixes

* Fix TestProviders

* Fix onbudget and offbudget displaying closed accounts

* [skip ci] Change category to Maintenance and update migration text

* Replace logger calls in desktop-client to console

* Fix lint errors

* Clear react query on closing of budget file similar to redux resetApp action

* [autofix.ci] apply automated fixes

* Remove sendThrow

* Code review feedback

* [autofix.ci] apply automated fixes

* Fix import

* Fix import

* Coderabbit feedback

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-14 09:32:37 +00:00
Matiss Janis Aboltins
5062fa78a8 Agent instructions for commit messages and PR titles (#6964)
* [AI] Add mandatory [AI] prefix requirement for commit messages and PR titles

Co-authored-by: matiss <matiss@mja.lv>

* [autofix.ci] apply automated fixes

* Add release notes for PR #6964

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:54:48 +00:00
Matiss Janis Aboltins
e178396e48 Docs: add Claude Code Pro subscription benefit for core contributors (#6963)
* Add Claude Code Pro subscription benefit for core contributors

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add release notes for PR #6963

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 03:00:36 +00:00
Matiss Janis Aboltins
09d85bbdc5 docs: add Architecture Decision Records page with bank sync credential ADR (#6965)
* docs: add Architecture Decision Records page for controversial decisions

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add release notes for PR #6965

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 01:14:15 +00:00
Joel Jeremy Marquez
a0378c10a9 Move redux state to react-query - tags states (#6941)
* Move redux state to react-query - tags states

* Add release notes for PR #6941

* Cleanup sendThrow

* Cleanup

* Update import

* Fix import

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 19:33:21 +00:00
Adam Stück
ca944baee5 feat: show/hide reconciled transactions on mobile (#6896)
* feat: show/hide reconciled transactions on mobile

Resolves #2969

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-13 17:25:55 +00:00
Joel Jeremy Marquez
cb5237359c Rename loot-core/platform/client/fetch to connection to match server-side package (#6943)
* Rename loot-core/platform/client/fetch package to connection to match the server side package name. Also to avoid confusion with the native fetch package.

* Update connection/init method to not receive any parameter to so browser and default implementation have the same signature

* Add release notes for PR #6943

* Fix names

* Fix imports

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 16:53:51 +00:00
Alexis Vielma
68bb33e5e6 feat: add back button to reports pages (#6702)
* feat: add back button to reports pages

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6702

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 02:35:08 +00:00
J B
cf5fe67e7b API Account Object (#6915)
* api change

* docs

* lint

* release notes

* spelling

* [autofix.ci] apply automated fixes

* spelling

* whoopsie, thanks rabbit

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-13 00:18:49 +00:00
Joel Jeremy Marquez
f8b4e87a67 Update send function to propagate any errors and fix catchErrors to return the error in result when an unknown command/method is sent to the browser server (#6942)
* Fix send not returning error when catchErrors option is enabled and an unknown method error is encountered

* Add release notes for PR #6942

* Fix send to properly propagate errors from the server

* Update release note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-12 23:47:21 +00:00
Joel Jeremy Marquez
8ae90a7ad1 Retrofit useReports to use react-query under the hood (#6951)
* Retrofit useReports to use react-query under the hood

* Add release notes for PR #6951

* Update 6951.md

* Report mutations

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-12 23:47:05 +00:00
Matiss Janis Aboltins
6f7af102a6 Upgrade oxfmt and oxlint, update .oxfmtrc.json import patterns (#6955)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 23:08:35 +00:00
Piyush Katkar
96a3128305 Fix mobile budget amount inputs when hide decimal places is enabled (#6945) 2026-02-12 22:59:23 +00:00
Joel Jeremy Marquez
003efecc23 Fix react query cache not being cleared when switching budgets (#6953)
* Fix react query cache not being cleared when switching budgets

* React does not want to export function from src/index

* Release note
2026-02-12 20:12:51 +00:00
Michael Clark
155e4df219 🎨 Add remaining component stories to storybook (#6940)
* final storybook stories

* release notes

* spelling mistake
2026-02-12 08:46:24 +00:00
Joel Jeremy Marquez
67d6592333 Add refetchOnSync option to useTransactions to refetch when a server sync event is emitted (#6936)
* Migrate setupTests.js to TypeScript with proper types (#6871)

* Initial plan

* Rename setupTests.js to setupTests.ts and add proper types

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Extract Size type to avoid duplication

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Add release note for setupTests TypeScript migration

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Rename release note file to match PR number 6871

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Delete setupTests PR release note

* Add refetchOnSync to useTransactions to refetch when a server sync event is emitted

* Add release note for useTransactions refetchOnSync feature (#6937)

* Initial plan

* Add release note for PR 6936

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Coderabbit feedback

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-12 00:16:37 +00:00
An, Tran Cong Viet
7fa9fa900b feat(currency): Add Vietnamese Dong (VND) currency (#6902)
* feat(currency): add support for vietnamese dong currency

* release: add upcoming release note

* fix(currency): change the number format for vietnamese currency

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-11 20:04:06 +00:00
jintakhan
9798c26462 feat(currency): Add South Korean Won (#6846)
* Add South Korean Won

* Update currencies.ts

* Add release notes
2026-02-11 17:00:41 +00:00
Joel Jeremy Marquez
37a7d0eccd Retrofit useTransactions to use react-query under the hood (#6757)
* Retrofit useTransactions to use react-query under the hood

* Add release notes for PR #6757

* Update packages/desktop-client/src/transactions/queries.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Disable when there is no query parameter

* Fix typecheck errors

* Remove space

* Update tests

* Coderabbit: Add pageSize to query key

* Use isPending instead of isFetching

* Unexport mockStore

* Revert variables

* Change category from Enhancements to Maintenance

Refactor the useTransactions hook to improve data fetching with react-query.

* Fix lint errors

* Fix lint errors

* Migrate setupTests.js to TypeScript with proper types (#6871)

* Initial plan

* Rename setupTests.js to setupTests.ts and add proper types

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Extract Size type to avoid duplication

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Add release note for setupTests TypeScript migration

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Rename release note file to match PR number 6871

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Update transactionQueries

* Delete setupTests PR release note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-11 16:58:07 +00:00
Joel Jeremy Marquez
e3e4b13d2b Move redux state to react-query - category states [Part 2 - expose react-query states e.g. isPending, isSuccess, etc] (#6882)
* Move redux state to react-query - category states [Part 2 - expose react-query states]

* Add release notes for PR #6882

* Add default values to useCategories destructuring to prevent undefined crashes (#6884)

* Initial plan

* Add missing defaults to all useCategories usages

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Fix lint errors

* Fix rebase mistake

* Change category from Enhancements to Maintenance

Migrate state management for category from Redux to React Query and update related hooks and components.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-11 16:44:32 +00:00
Michael Clark
138ea810d6 🎨 Reorganising Storybook and more component stories (#6924)
* reorg of storybook docs and add some new components

* releaes notes

* Update meta tags for Actual Budget Design System

* Increase sidebar item font size from 14px to 16px
2026-02-11 09:01:11 +00:00
youngcw
07ff514c12 [Goals] fix tracking budget balance carryover for templates (#6922)
* fix tracking budget balance carryover for templates

* Add release notes for PR #6922

* fix note

* fix tests

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-11 01:52:49 +00:00
Gabriel J.
2ca352aaa7 fix(schedules): prevent past missed schedule dates from being marked as upcoming (#6925)
Fixes #6872
2026-02-11 01:04:42 +00:00
Gabriel J.
078da08ad5 Fix/6885 crash when rule has empty date field (#6905)
* Fix crash when rule date field loses focus while empty

Fixes #6885

* Remove ts-strict-ignore and fix types in DateSelect

* Generate release note 6905
2026-02-10 23:05:37 +00:00
samekh248
edcf893a27 Add butterfly custom theme (#6900)
* Added butterfly custom theme

* Added release notes
2026-02-10 23:02:52 +00:00
Juulz
38a72656df [FIX] Update DesktopLinkedNotes so links stay inline (#6858)
* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes so links stay inline

* Update TransactionsTable.tsx

make sure flexDirection is row.

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update NotesTagFormatter.tsx trial

* Update DesktopLinkedNotes.tsx

* Update NotesTagFormatter.tsx

* Update NotesTagFormatter.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

Add role for accessibility

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

Revert to original

* Update DesktopLinkedNotes.tsx

Try to style the bare button to make it work.

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

Reverting to current master

* Update DesktopLinkedNotes.tsx

Add nowrap styling.

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* revert TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Revert DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-10 20:33:39 +00:00
Pieter Ouwerkerk
24f698910a Add Tag API (#6746)
* Add Tag API

* Add Tag API tests

* Add Release Note for #6746

* Make release note more user-facing

* Remove unnecessary type coercion in tagModel.fromExternal

Since APITagEntity picks all properties from TagEntity, the types are
structurally identical and TypeScript can verify compatibility without
manual coercion.

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-02-10 17:22:55 +00:00
Jonathon Jongsma
cdaf06abee Remove duplication of CrossoverData type (#6928)
* Remove duplication of CrossoverData type

Move the CrossoverData type definition to crossover-spreadsheet.tsx and
import it from the Crossover and CrossoverCard files instead of having
duplicate definitions in each file.

Signed-off-by: Jonathon Jongsma <jonathon@quotidian.org>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Signed-off-by: Jonathon Jongsma <jonathon@quotidian.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-10 16:06:30 +00:00
762 changed files with 15644 additions and 6576 deletions

View File

@@ -13,8 +13,6 @@ reviews:
mode: off
enabled: false
labeling_instructions:
- label: 'suspect ai generated'
instructions: 'This issue or PR is suspected to be generated by AI.'
- label: 'API'
instructions: 'This issue or PR updates the API in `packages/api`.'
- label: 'documentation'

View File

@@ -0,0 +1,74 @@
---
description: Rules for AI-generated commits and pull requests
globs:
alwaysApply: true
---
# PR and Commit Rules for AI Agents
Canonical source: `.github/agents/pr-and-commit-rules.md`
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese** (简体中文).
## Quick-Reference Workflow
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -1 +1,21 @@
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
## Description
<!-- What does this PR do? Why is it needed? Please give context on the "why?": why do we need this change? What problem is it solving for you?-->
## Related issue(s)
<!-- e.g. Fixes #123, Relates to #456 -->
## Testing
<!-- What did you test? How can we reproduce the issue you are fixing or how can we test the feature you built? -->
## Checklist
- [ ] Release notes added (see link above)
- [ ] No obvious regressions in affected areas
- [ ] Self-review has been performed - I understand what each change in the code does and why it is needed
<!--- actual-bot-sections --->

View File

@@ -74,4 +74,4 @@ async function checkReleaseNotesExists() {
}
}
checkReleaseNotesExists();
void checkReleaseNotesExists();

View File

@@ -74,4 +74,4 @@ async function commentOnPR() {
}
}
commentOnPR();
void commentOnPR();

View File

@@ -94,4 +94,4 @@ ${summaryData.summary}
}
}
createReleaseNotesFile();
void createReleaseNotesFile();

View File

@@ -37,12 +37,14 @@ async function getPRDetails() {
console.log('- PR Author:', pr.user.login);
console.log('- PR Title:', pr.title);
console.log('- Base Branch:', pr.base.ref);
console.log('- Head Branch:', pr.head.ref);
const result = {
number: pr.number,
author: pr.user.login,
title: pr.title,
baseBranch: pr.base.ref,
headBranch: pr.head.ref,
};
setOutput('result', JSON.stringify(result));

View File

@@ -31,6 +31,7 @@ CAGLPTPL
Caixa
CAMT
cashflow
Catppuccin
Cetelem
cimode
Citi
@@ -109,6 +110,7 @@ KBCBE
Keycloak
Khurozov
KORT
KRW
Kreditbank
lage
LHV

View File

@@ -79,3 +79,6 @@
# allowlist specific non-English words with non-ASCII characters
\b(Länsförsäkringar|München|Złoty)\b
# allowlist specific proper nouns
\b(CodeRabbit)\b

70
.github/agents/pr-and-commit-rules.md vendored Normal file
View File

@@ -0,0 +1,70 @@
# PR and Commit Rules for AI Agents
This is the single source of truth for all commit and pull request rules that AI agents must follow when working with Actual Budget.
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests. This helps maintainers understand the nature of the contribution.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. Humans are expected to fill in the Description, Related issue(s), Testing, and Checklist sections.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
## Quick-Reference Workflow
Follow these steps when committing and creating PRs:
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -8,13 +8,13 @@ const CONFIG = {
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
PR_CONTRIBUTION_POINTS: {
Features: 2,
Enhancements: 2,
Bugfix: 3,
Maintenance: 2,
Unknown: 2,
},
PR_CONTRIBUTION_POINTS: [
{ categories: ['Features'], points: 2 },
{ categories: ['Enhancements'], points: 2 },
{ categories: ['Bugfixes', 'Bugfix'], points: 3 },
{ categories: ['Maintenance'], points: 2 },
{ categories: ['Unknown'], points: 2 },
],
// Point tiers for code changes (non-docs)
CODE_PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
@@ -96,7 +96,7 @@ async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) {
* @param {string} repo - Repository name.
* @param {number} prNumber - PR number.
* @param {Date} monthEnd - The end date of the month to use as base revision.
* @returns {Object} Object with category and points, or null if error.
* @returns {Promise<Object>} Object with category and points, or null if error.
*/
async function getPRCategoryAndPoints(
octokit,
@@ -130,11 +130,14 @@ async function getPRCategoryAndPoints(
'utf-8',
);
const category = parseReleaseNotesCategory(content);
const tier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
e.categories.includes(category),
);
if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) {
if (tier) {
return {
category,
points: CONFIG.PR_CONTRIBUTION_POINTS[category],
points: tier.points,
};
}
}
@@ -142,9 +145,12 @@ async function getPRCategoryAndPoints(
// Do nothing
}
const unknownTier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
e.categories.includes('Unknown'),
);
return {
category: 'Unknown',
points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown,
points: unknownTier.points,
};
}

View File

@@ -41,21 +41,12 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if PR targets master branch
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-base-branch
run: |
BASE_BRANCH=$(echo '${{ steps.pr-details.outputs.result }}' | jq -r '.baseBranch')
echo "Base branch: $BASE_BRANCH"
if [ "$BASE_BRANCH" = "master" ]; then
echo "targets_master=true" >> $GITHUB_OUTPUT
else
echo "targets_master=false" >> $GITHUB_OUTPUT
echo "PR does not target master branch, skipping release notes generation"
fi
- name: Check if release notes file already exists
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null' && steps.check-base-branch.outputs.targets_master == 'true'
if: >-
steps.check-first-comment.outputs.result == 'true' &&
steps.pr-details.outputs.result != 'null' &&
fromJSON(steps.pr-details.outputs.result).baseBranch == 'master' &&
!startsWith(fromJSON(steps.pr-details.outputs.result).headBranch, 'release/')
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:

View File

@@ -60,8 +60,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
node-version: 22
download-translations: 'false'
- name: Check migrations
run: node ./.github/actions/check-migrations.js
run: yarn workspace @actual-app/ci-actions tsx bin/check-migrations.ts

View File

@@ -87,8 +87,8 @@ jobs:
- name: Test that the docker image boots
run: |
docker run --detach --network=host actualbudget/actual-server-testing
sleep 5
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
sleep 10
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --retry-delay 1 --retry-connrefused localhost:5006
# This will use the cache from the earlier build step and not rebuild the image
# https://docs.docker.com/build/ci/github-actions/test-before-push/

View File

@@ -30,7 +30,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -53,7 +53,7 @@ jobs:
name: Functional Desktop App
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -81,7 +81,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -104,7 +104,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment

View File

@@ -156,53 +156,3 @@ jobs:
-NoStatus `
-AutoCommit `
-Force
publish-flathub:
needs: build
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Download Linux artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: actual-electron-ubuntu-22.04
- name: Calculate AppImage SHA256
id: appimage_sha256
run: |
APPIMAGE_X64_SHA256=$(sha256sum Actual-linux-x86_64.AppImage | awk '{ print $1 }')
APPIMAGE_ARM64_SHA256=$(sha256sum Actual-linux-arm64.AppImage | awk '{ print $1 }')
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
- name: Checkout Flathub repo
uses: actions/checkout@v6
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
- name: Update manifest with new SHA256
run: |
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
branch: 'release/${{ needs.build.outputs.version }}'
draft: true
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.
:link: [View release notes](https://actualbudget.org/blog/release-${{ needs.build.outputs.version }})
reviewers: 'jfdoming,MatissJanis,youngcw' # The core team that have accepted the collaborator access to the Flathub repo

View File

@@ -0,0 +1,37 @@
# When the "unfreeze" label is added to a PR, add that PR to Merge Freeze's unblocked list
# so it can be merged during a freeze. Uses pull_request_target so the workflow runs in
# the base repo and has access to MERGEFREEZE_ACCESS_TOKEN for fork PRs; it does not
# checkout or run any PR code. Requires MERGEFREEZE_ACCESS_TOKEN repo secret
# (project-specific token from Merge Freeze Web API panel for actualbudget/actual / master).
# See: https://docs.mergefreeze.com/web-api#post-freeze-status
name: Merge Freeze add PR to unblocked list
on:
pull_request_target:
types: [labeled]
jobs:
unfreeze:
if: ${{ github.event.label.name == 'unfreeze' }}
runs-on: ubuntu-latest
permissions: {}
concurrency:
group: merge-freeze-unfreeze-${{ github.ref }}-labels
cancel-in-progress: false
steps:
- name: POST to Merge Freeze add PR to unblocked list
env:
MERGEFREEZE_ACCESS_TOKEN: ${{ secrets.MERGEFREEZE_ACCESS_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
USER_NAME: ${{ github.actor }}
run: |
set -e
if [ -z "$MERGEFREEZE_ACCESS_TOKEN" ]; then
echo "::error::MERGEFREEZE_ACCESS_TOKEN secret is not set"
exit 1
fi
url="https://www.mergefreeze.com/api/branches/actualbudget/actual/master/?access_token=${MERGEFREEZE_ACCESS_TOKEN}"
payload=$(jq -n --arg user_name "$USER_NAME" --argjson pr "$PR_NUMBER" '{frozen: true, user_name: $user_name, unblocked_prs: [$pr]}')
curl -sf -X POST "$url" -H "Content-Type: application/json" -d "$payload"
echo "Merge Freeze updated: PR #$PR_NUMBER added to unblocked list."

View File

@@ -1,25 +0,0 @@
name: Remove 'suspect ai generated' label when 'AI generated' is present
on:
pull_request_target:
types: [labeled]
permissions:
pull-requests: write
jobs:
remove-suspect-label:
if: >-
${{ contains(github.event.pull_request.labels.*.name, 'AI generated') &&
contains(github.event.pull_request.labels.*.name, 'suspect ai generated') }}
runs-on: ubuntu-slim
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'suspect ai generated'
});

126
.github/workflows/publish-flathub.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
name: Publish Flathub
defaults:
run:
shell: bash
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v25.3.0)'
required: true
type: string
concurrency:
group: publish-flathub
cancel-in-progress: false
jobs:
publish-flathub:
runs-on: ubuntu-22.04
steps:
- name: Resolve version
id: resolve_version
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
if [[ "$EVENT_NAME" == "release" ]]; then
TAG="$RELEASE_TAG"
else
TAG="$INPUT_TAG"
fi
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided"
exit 1
fi
# Validate tag format (v-prefixed semver, e.g. v25.3.0 or v1.2.3-beta.1)
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid tag format: $TAG (expected v-prefixed semver, e.g. v25.3.0)"
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved tag=$TAG version=$VERSION"
- name: Verify release assets exist
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.resolve_version.outputs.tag }}"
echo "Checking release assets for tag $TAG..."
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
echo "Found assets:"
echo "$ASSETS"
if ! echo "$ASSETS" | grep -qx "Actual-linux-x86_64.AppImage"; then
echo "::error::Missing asset: Actual-linux-x86_64.AppImage"
exit 1
fi
if ! echo "$ASSETS" | grep -qx "Actual-linux-arm64.AppImage"; then
echo "::error::Missing asset: Actual-linux-arm64.AppImage"
exit 1
fi
echo "All required AppImage assets found."
- name: Calculate AppImage SHA256 (streamed)
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
echo "Streaming x86_64 AppImage to compute SHA256..."
APPIMAGE_X64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-x86_64.AppImage" | sha256sum | awk '{ print $1 }')
echo "x86_64 SHA256: $APPIMAGE_X64_SHA256"
echo "Streaming arm64 AppImage to compute SHA256..."
APPIMAGE_ARM64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-arm64.AppImage" | sha256sum | awk '{ print $1 }')
echo "arm64 SHA256: $APPIMAGE_ARM64_SHA256"
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
- name: Checkout Flathub repo
uses: actions/checkout@v6
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
- name: Update manifest with new version
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
echo "Updated manifest:"
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
branch: 'release/${{ steps.resolve_version.outputs.version }}'
title: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ steps.resolve_version.outputs.version }}.
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.resolve_version.outputs.version }})
reviewers: 'jfdoming,MatissJanis,youngcw'

View File

@@ -44,7 +44,7 @@ jobs:
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- name: Get PR details
id: pr

2
.gitignore vendored
View File

@@ -33,7 +33,9 @@ packages/desktop-electron/dist
packages/desktop-electron/loot-core
packages/desktop-client/service-worker
packages/plugins-service/dist
packages/component-library/dist
packages/loot-core/lib-dist
**/.tsbuildinfo
packages/sync-server/coverage
bundle.desktop.js
bundle.desktop.js.map

View File

@@ -18,15 +18,15 @@
"customGroups": [
{
"groupName": "react",
"elementNamePattern": ["react"]
"elementNamePattern": ["react", "react-dom/*", "react-*"]
},
{
"groupName": "loot-core",
"elementNamePattern": ["loot-core"]
"elementNamePattern": ["loot-core/**"]
},
{
"groupName": "desktop-client",
"elementNamePattern": ["@desktop-client"]
"elementNamePattern": ["@desktop-client/**"]
}
],
"newlinesBetween": true

View File

@@ -20,72 +20,72 @@
"rules": {
// Import sorting
"perfectionist/sort-named-imports": [
"warn",
"error",
{
"groups": ["value-import", "type-import"]
}
],
// Actual rules
"actual/typography": "warn",
"actual/typography": "error",
"actual/no-untranslated-strings": "error",
"actual/prefer-trans-over-t": "error",
"actual/prefer-if-statement": "warn",
"actual/prefer-if-statement": "error",
"actual/prefer-logger-over-console": "error",
"actual/object-shorthand-properties": "warn",
"actual/prefer-const": "warn",
"actual/no-anchor-tag": "warn",
"actual/no-react-default-import": "warn",
"actual/object-shorthand-properties": "error",
"actual/prefer-const": "error",
"actual/no-anchor-tag": "error",
"actual/no-react-default-import": "error",
// JSX A11y rules
"jsx-a11y/no-autofocus": [
"warn",
"error",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": [
"warn",
"error",
{
"aspects": ["noHref", "invalidHref"]
}
],
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/aria-proptypes": "error",
"jsx-a11y/aria-role": [
"warn",
"error",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/heading-has-content": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-distracting-elements": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/scope": "warn",
"jsx-a11y/aria-unsupported-elements": "error",
"jsx-a11y/heading-has-content": "error",
"jsx-a11y/iframe-has-title": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
"jsx-a11y/no-distracting-elements": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/scope": "error",
// Typescript rules
"typescript/ban-ts-comment": ["warn"],
"typescript/consistent-type-definitions": ["warn", "type"],
"typescript/ban-ts-comment": ["error"],
"typescript/consistent-type-definitions": ["error", "type"],
"typescript/consistent-type-imports": [
"warn",
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/no-implied-eval": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-implied-eval": "error",
"typescript/no-explicit-any": "error",
"typescript/no-restricted-types": [
"warn",
"error",
{
"types": {
// forbid FC as superfluous
@@ -98,19 +98,23 @@
}
}
],
"typescript/no-var-requires": "warn",
"typescript/no-var-requires": "error",
// we want to allow unions such as "{ name: DbAccount['name'] | DbPayee['name'] }"
"typescript/no-duplicate-type-constituents": "off",
"typescript/await-thenable": "error",
"typescript/no-floating-promises": "warn", // TODO: covert to error
// Import rules
"import/consistent-type-specifier-style": "warn",
"import/consistent-type-specifier-style": "error",
"import/first": "error",
"import/no-amd": "error",
"import/no-default-export": "warn",
"import/no-default-export": "error",
"import/no-webpack-loader-syntax": "error",
"import/no-useless-path-segments": "warn",
"import/no-unresolved": "warn",
"import/no-unused-modules": "warn",
"import/no-useless-path-segments": "error",
"import/no-unresolved": "error",
"import/no-unused-modules": "error",
"import/no-duplicates": [
"warn",
"error",
{
"prefer-inline": false
}
@@ -118,122 +122,122 @@
// React rules
"react/exhaustive-deps": [
"warn",
"error",
{
"additionalHooks": "(^useQuery$|^useEffectAfterMount$)"
}
],
"react/jsx-curly-brace-presence": "warn",
"react/jsx-curly-brace-presence": "error",
"react/jsx-filename-extension": [
"warn",
"error",
{
"extensions": [".jsx", ".tsx"],
"allow": "as-needed"
}
],
"react/jsx-no-comment-textnodes": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
"react/jsx-no-useless-fragment": "warn",
"react/jsx-no-useless-fragment": "error",
"react/jsx-pascal-case": [
"warn",
"error",
{
"allowAllCaps": true,
"ignore": []
}
],
"react/no-danger-with-children": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-unstable-nested-components": "warn",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-is-mounted": "error",
"react/no-unstable-nested-components": "error",
"react/require-render-return": "error",
"react/rules-of-hooks": "error",
"react/self-closing-comp": "warn",
"react/style-prop-object": "warn",
"react/jsx-boolean-value": "warn",
"react/self-closing-comp": "error",
"react/style-prop-object": "error",
"react/jsx-boolean-value": "error",
// ESLint rules
"eslint/array-callback-return": "warn",
"eslint/curly": ["warn", "multi-line", "consistent"],
"eslint/array-callback-return": "error",
"eslint/curly": ["error", "multi-line", "consistent"],
"eslint/default-case": [
"warn",
"error",
{
"commentPattern": "^no default$"
}
],
"eslint/eqeqeq": ["warn", "smart"],
"eslint/no-array-constructor": "warn",
"eslint/no-caller": "warn",
"eslint/no-cond-assign": ["warn", "except-parens"],
"eslint/no-const-assign": "warn",
"eslint/no-control-regex": "warn",
"eslint/no-delete-var": "warn",
"eslint/no-dupe-class-members": "warn",
"eslint/no-dupe-keys": "warn",
"eslint/no-duplicate-case": "warn",
"eslint/no-empty-character-class": "warn",
"eslint/no-empty-function": "warn",
"eslint/no-empty-pattern": "warn",
"eslint/no-eval": "warn",
"eslint/no-ex-assign": "warn",
"eslint/no-extend-native": "warn",
"eslint/no-extra-bind": "warn",
"eslint/no-extra-label": "warn",
"eslint/no-fallthrough": "warn",
"eslint/no-func-assign": "warn",
"eslint/no-invalid-regexp": "warn",
"eslint/no-iterator": "warn",
"eslint/no-label-var": "warn",
"eslint/no-var": "warn",
"eslint/eqeqeq": ["error", "smart"],
"eslint/no-array-constructor": "error",
"eslint/no-caller": "error",
"eslint/no-cond-assign": ["error", "except-parens"],
"eslint/no-const-assign": "error",
"eslint/no-control-regex": "error",
"eslint/no-delete-var": "error",
"eslint/no-dupe-class-members": "error",
"eslint/no-dupe-keys": "error",
"eslint/no-duplicate-case": "error",
"eslint/no-empty-character-class": "error",
"eslint/no-empty-function": "error",
"eslint/no-empty-pattern": "error",
"eslint/no-eval": "error",
"eslint/no-ex-assign": "error",
"eslint/no-extend-native": "error",
"eslint/no-extra-bind": "error",
"eslint/no-extra-label": "error",
"eslint/no-fallthrough": "error",
"eslint/no-func-assign": "error",
"eslint/no-invalid-regexp": "error",
"eslint/no-iterator": "error",
"eslint/no-label-var": "error",
"eslint/no-var": "error",
"eslint/no-labels": [
"warn",
"error",
{
"allowLoop": true,
"allowSwitch": false
}
],
"eslint/no-new-func": "warn",
"eslint/no-script-url": "warn",
"eslint/no-self-assign": "warn",
"eslint/no-self-compare": "warn",
"eslint/no-sequences": "warn",
"eslint/no-shadow-restricted-names": "warn",
"eslint/no-sparse-arrays": "warn",
"eslint/no-template-curly-in-string": "warn",
"eslint/no-this-before-super": "warn",
"eslint/no-throw-literal": "warn",
"eslint/no-unreachable": "warn",
"eslint/no-obj-calls": "warn",
"eslint/no-new-wrappers": "warn",
"eslint/no-unsafe-negation": "warn",
"eslint/no-multi-str": "warn",
"eslint/no-global-assign": "warn",
"eslint/no-lone-blocks": "warn",
"eslint/no-unused-labels": "warn",
"eslint/no-object-constructor": "warn",
"eslint/no-new-native-nonconstructor": "warn",
"eslint/no-redeclare": "warn",
"eslint/no-useless-computed-key": "warn",
"eslint/no-useless-concat": "warn",
"eslint/no-useless-escape": "warn",
"eslint/require-yield": "warn",
"eslint/getter-return": "warn",
"eslint/unicode-bom": ["warn", "never"],
"eslint/no-use-isnan": "warn",
"eslint/valid-typeof": "warn",
"eslint/no-new-func": "error",
"eslint/no-script-url": "error",
"eslint/no-self-assign": "error",
"eslint/no-self-compare": "error",
"eslint/no-sequences": "error",
"eslint/no-shadow-restricted-names": "error",
"eslint/no-sparse-arrays": "error",
"eslint/no-template-curly-in-string": "error",
"eslint/no-this-before-super": "error",
"eslint/no-throw-literal": "error",
"eslint/no-unreachable": "error",
"eslint/no-obj-calls": "error",
"eslint/no-new-wrappers": "error",
"eslint/no-unsafe-negation": "error",
"eslint/no-multi-str": "error",
"eslint/no-global-assign": "error",
"eslint/no-lone-blocks": "error",
"eslint/no-unused-labels": "error",
"eslint/no-object-constructor": "error",
"eslint/no-new-native-nonconstructor": "error",
"eslint/no-redeclare": "error",
"eslint/no-useless-computed-key": "error",
"eslint/no-useless-concat": "error",
"eslint/no-useless-escape": "error",
"eslint/require-yield": "error",
"eslint/getter-return": "error",
"eslint/unicode-bom": ["error", "never"],
"eslint/no-use-isnan": "error",
"eslint/valid-typeof": "error",
"eslint/no-useless-rename": [
"warn",
"error",
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"eslint/no-with": "warn",
"eslint/no-regex-spaces": "warn",
"eslint/no-with": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-restricted-globals": [
"warn",
"error",
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
"addEventListener",
@@ -295,7 +299,7 @@
"top"
],
"eslint/no-restricted-imports": [
"warn",
"error",
{
"paths": [
{
@@ -345,9 +349,9 @@
]
}
],
"eslint/no-useless-constructor": "warn",
"eslint/no-undef": "warn",
"eslint/no-unused-expressions": "warn"
"eslint/no-useless-constructor": "error",
"eslint/no-undef": "error",
"eslint/no-unused-expressions": "error"
},
"overrides": [
{

View File

@@ -42,6 +42,12 @@ yarn start:desktop
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
- Tests run once and exit by default (using `vitest --run`)
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for the full specification, including git safety rules, pre-commit checklist, and PR workflow.
### Task Orchestration with Lage
The project uses **[lage](https://microsoft.github.io/lage/)** (a task runner for JavaScript monorepos) to efficiently run tests and other tasks across multiple workspaces:
@@ -292,6 +298,7 @@ Always run `yarn typecheck` before committing.
**React Patterns:**
- The project uses **React Compiler** (`babel-plugin-react-compiler`) in the desktop-client. The compiler auto-memoizes component bodies, so you can omit manual `useCallback`, `useMemo`, and `React.memo` when adding or refactoring code; prefer inline callbacks and values unless a stable identity is required by a non-compiled dependency.
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
- Don't use `React.*` patterns - use named imports instead
- Use `<Link>` instead of `<a>` tags
@@ -338,11 +345,7 @@ Always maintain newlines between import groups.
**Git Commands:**
- Never update git config
- Never run destructive git operations (force push, hard reset) unless explicitly requested
- Never skip hooks (--no-verify, --no-gpg-sign)
- Never force push to main/master
- Never commit unless explicitly asked
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
## File Structure Patterns
@@ -505,7 +508,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
2. Reinstall dependencies: `yarn install`
3. Check Node.js version (requires >=20)
3. Check Node.js version (requires >=22)
4. Check Yarn version (requires ^4.9.1)
## Testing Patterns
@@ -541,6 +544,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
Before committing changes, ensure:
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
@@ -553,9 +557,7 @@ Before committing changes, ensure:
## Pull Request Guidelines
When creating pull requests:
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling.
## Code Review Guidelines
@@ -586,7 +588,7 @@ yarn install:server
## Environment Requirements
- **Node.js**: >=20
- **Node.js**: >=22
- **Yarn**: ^4.9.1 (managed by packageManager field)
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
@@ -599,3 +601,40 @@ The codebase is actively being migrated:
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
When working with older code, follow the newer patterns described in this guide.
## Cursor Cloud specific instructions
### Services overview
| Service | Command | Port | Required |
| ------------------- | ----------------------- | ---- | ----------------------------- |
| Web Frontend (Vite) | `yarn start` | 3001 | Yes |
| Sync Server | `yarn start:server-dev` | 5006 | Optional (sync features only) |
All storage is **SQLite** (file-based via `better-sqlite3`). No external databases or services are needed.
### Running the app
- `yarn start` builds the plugins-service worker, loot-core browser backend, and starts the Vite dev server on port **3001**.
- `yarn start:server-dev` starts both the sync server (port 5006) and the web frontend together.
- The Vite HMR dev server serves many unbundled modules. In constrained environments, the browser may hit `ERR_INSUFFICIENT_RESOURCES`. If that happens, use `yarn build:browser` followed by serving the built output from `packages/desktop-client/build/` with proper COOP/COEP headers (`Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`).
### Lint, test, typecheck
Standard commands documented in `package.json` scripts and the Quick Start section above:
- `yarn lint` / `yarn lint:fix` (uses oxlint + oxfmt)
- `yarn test` (lage across all workspaces)
- `yarn typecheck` (tsc + lage typecheck)
### Testing and previewing the app
When running the app for manual testing or demos, use **"View demo"** on the initial setup screen (after selecting "Don't use a server"). This creates a test budget pre-populated with realistic sample data (accounts, transactions, categories, and budgeted amounts), which is far more useful than starting with an empty budget.
### Gotchas
- The `engines` field requires **Node.js >=22** and **Yarn ^4.9.1**. The `.nvmrc` specifies `v22/*`.
- Pre-commit hook runs `lint-staged` (oxfmt + oxlint) via Husky. Run `yarn prepare` once after install to set up hooks.
- Lage caches test results in `.lage/`. If tests behave unexpectedly, clear with `rm -rf .lage`.
- Native modules (`better-sqlite3`, `bcrypt`) require build tools (`gcc`, `make`, `python3`). These are pre-installed in the Cloud VM.
- All yarn commands must be run from the repository root, never from child workspaces.

2
CLAUDE.md Normal file
View File

@@ -0,0 +1,2 @@
@AGENTS.md
@.github/agents/pr-and-commit-rules.md

View File

@@ -59,6 +59,9 @@ yarn workspace loot-core build:browser
yarn workspace @actual-app/web build:browser
yarn workspace @actual-app/sync-server build
# Emit loot-core declarations so desktop-electron (which includes typings/window.ts) can build
yarn workspace loot-core exec tsc -p tsconfig.json
yarn workspace desktop-electron update-client
(

View File

@@ -178,4 +178,4 @@ async function execAsync(cmd: string, errorLog?: string): Promise<string> {
});
}
run();
void run();

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.58.2-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -1,6 +1,9 @@
/** @type {import('lage').ConfigOptions} */
module.exports = {
pipeline: {
typecheck: {
type: 'npmScript',
},
test: {
type: 'npmScript',
options: {

View File

@@ -54,37 +54,37 @@
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"lint": "oxfmt --check . && oxlint --deny-warnings",
"lint:fix": "oxfmt . && oxlint --deny-warnings --fix",
"lint": "yarn workspace @actual-app/api clean && oxfmt --check . && oxlint --type-aware",
"lint:fix": "yarn workspace @actual-app/api clean && oxfmt . && oxlint --fix --type-aware",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"typecheck": "yarn tsc --incremental && tsc-strict",
"typecheck": "yarn workspace @actual-app/api clean && tsc -b && tsc -p tsconfig.root.json --noEmit && lage typecheck",
"jq": "./node_modules/node-jq/bin/jq",
"prepare": "husky"
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.3",
"@types/node": "^22.19.10",
"@types/prompts": "^2.4.9",
"baseline-browser-mapping": "^2.9.14",
"baseline-browser-mapping": "^2.9.19",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-perfectionist": "^4.15.1",
"eslint-plugin-typescript-paths": "^0.0.33",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lage": "^2.14.15",
"lage": "^2.14.17",
"lint-staged": "^16.2.7",
"minimatch": "^10.1.1",
"minimatch": "^10.1.2",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.26.0",
"oxlint": "^1.41.0",
"p-limit": "^7.2.0",
"oxfmt": "^0.32.0",
"oxlint": "^1.47.0",
"oxlint-tsgolint": "^0.13.0",
"p-limit": "^7.3.0",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-strict-plugin": "^2.4.4"
"typescript": "^5.9.3"
},
"resolutions": {
"rollup": "4.40.1",
@@ -95,7 +95,7 @@
"oxfmt --no-error-on-unmatched-pattern"
],
"*.{js,mjs,jsx,ts,tsx}": [
"oxlint --deny-warnings --fix"
"oxlint --fix --type-aware"
]
},
"browserslist": [

View File

@@ -356,6 +356,143 @@ describe('API CRUD operations', () => {
);
});
// apis: createTag, getTags, updateTag, deleteTag
test('Tags: successfully complete tag operations', async () => {
// Create tags
const tagId1 = await api.createTag({ tag: 'test-tag1', color: '#ff0000' });
const tagId2 = await api.createTag({
tag: 'test-tag2',
description: 'A test tag',
});
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'test-tag1',
color: '#ff0000',
}),
expect.objectContaining({
id: tagId2,
tag: 'test-tag2',
description: 'A test tag',
}),
]),
);
// Update tag
await api.updateTag(tagId1, { tag: 'updated-tag', color: '#00ff00' });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'updated-tag',
color: '#00ff00',
}),
]),
);
// Delete tag
await api.deleteTag(tagId2);
tags = await api.getTags();
expect(tags).not.toEqual(
expect.arrayContaining([expect.objectContaining({ id: tagId2 })]),
);
});
test('Tags: create tag with minimal fields', async () => {
const tagId = await api.createTag({ tag: 'minimal-tag' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'minimal-tag',
color: null,
description: null,
}),
]),
);
});
test('Tags: update single field only', async () => {
const tagId = await api.createTag({ tag: 'original', color: '#ff0000' });
// Update only color, tag and description should remain unchanged
await api.updateTag(tagId, { color: '#00ff00' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'original',
color: '#00ff00',
description: null,
}),
]),
);
});
test('Tags: handle null values correctly', async () => {
const tagId = await api.createTag({
tag: 'with-nulls',
color: null,
description: null,
});
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
color: null,
description: null,
}),
]),
);
});
test('Tags: clear optional field', async () => {
const tagId = await api.createTag({
tag: 'clearable',
color: '#ff0000',
description: 'will be cleared',
});
// Clear color by setting to null
await api.updateTag(tagId, { color: null });
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: 'will be cleared',
}),
]),
);
// Clear description by setting to null
await api.updateTag(tagId, { description: null });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: null,
}),
]),
);
});
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
test('Rules: successfully update rules', async () => {
await api.createPayee({ name: 'test-payee' });

View File

@@ -5,8 +5,10 @@ import type {
APIFileEntity,
APIPayeeEntity,
APIScheduleEntity,
APITagEntity,
} from 'loot-core/server/api-models';
import type { Query } from 'loot-core/shared/query';
import type { ImportTransactionsOpts } from 'loot-core/types/api-handlers';
import type { Handlers } from 'loot-core/types/handlers';
import type {
ImportTransactionEntity,
@@ -125,11 +127,6 @@ export function addTransactions(
});
}
export type ImportTransactionsOpts = {
defaultCleared?: boolean;
dryRun?: boolean;
};
export function importTransactions(
accountId: APIAccountEntity['id'],
transactions: ImportTransactionEntity[],
@@ -274,6 +271,25 @@ export function deletePayee(id: APIPayeeEntity['id']) {
return send('api/payee-delete', { id });
}
export function getTags() {
return send('api/tags-get');
}
export function createTag(tag: Omit<APITagEntity, 'id'>) {
return send('api/tag-create', { tag });
}
export function updateTag(
id: APITagEntity['id'],
fields: Partial<Omit<APITagEntity, 'id'>>,
) {
return send('api/tag-update', { id, fields });
}
export function deleteTag(id: APITagEntity['id']) {
return send('api/tag-delete', { id });
}
export function mergePayees(
targetId: APIPayeeEntity['id'],
mergeIds: APIPayeeEntity['id'][],

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "26.2.0",
"version": "26.3.0",
"description": "An API for Actual",
"license": "MIT",
"files": [
@@ -12,16 +12,17 @@
"scripts": {
"build:app": "yarn workspace loot-core build:api",
"build:crdt": "yarn workspace @actual-app/crdt build",
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
"build:node": "tsc && tsc-alias",
"build:migrations": "cp migrations/*.sql dist/migrations",
"build:default-db": "cp default-db.sqlite dist/",
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
"test": "yarn run clean && yarn run build:app && yarn run build:crdt && vitest --run",
"clean": "rm -rf dist @types"
"clean": "rm -rf dist @types",
"typecheck": "yarn build && tsc --noEmit && tsc-strict"
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^12.5.0",
"better-sqlite3": "^12.6.2",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^13.0.0"
@@ -29,7 +30,8 @@
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"typescript-strict-plugin": "^2.4.4",
"vitest": "^4.0.18"
},
"engines": {
"node": ">=20"

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
// Using ES2021 because that's the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
@@ -8,12 +9,18 @@
"moduleResolution": "node10",
"noEmit": false,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": ".",
"declarationDir": "@types",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"paths": {
"loot-core/*": ["./@types/loot-core/src/*"]
}
// TEMPORARY
"loot-core/*": ["../loot-core/src/*"]
},
"plugins": [{ "name": "typescript-strict-plugin", "paths": ["."] }]
},
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
"include": ["."],
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
}

1
packages/api/typings/pegjs.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '*.pegjs';

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
// overview:
// 1. Identify the migrations in packages/loot-core/migrations/* on `master` and HEAD
// 2. Make sure that any new migrations on HEAD are dated after the latest migration on `master`.
const { spawnSync } = require('child_process');
const path = require('path');
import { spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const migrationsDir = path.join(
__dirname,
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'packages',
@@ -16,7 +16,7 @@ const migrationsDir = path.join(
'migrations',
);
function readMigrations(ref) {
function readMigrations(ref: string) {
const { stdout } = spawnSync('git', [
'ls-tree',
'--name-only',

View File

@@ -3,9 +3,18 @@
"private": true,
"type": "module",
"scripts": {
"test": "vitest --run"
"tsx": "node --import=extensionless/register --experimental-strip-types",
"test": "vitest --run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"vitest": "^4.0.16"
"extensionless": "^2.0.6",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"extensionless": {
"lookFor": [
"ts"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [],
"module": "nodenext",
"moduleResolution": "nodenext",
"skipLibCheck": true,
"strict": true,
"types": ["node"],
"outDir": "dist",
"rootDir": "."
},
"include": ["src/**/*", "bin/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -13,7 +13,8 @@ function getAbsolutePath(value: string) {
}
const config: StorybookConfig = {
stories: [
'../src/Introduction.mdx',
'../src/Concepts/*.mdx',
'../src/Themes/*.mdx',
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],

View File

@@ -0,0 +1,99 @@
<!--
Override the default favicon used in the Storybook in the browser tab.
-->
<link
rel="shortcut icon"
type="image/x-icon"
href="https://design.actualbudget.org/favicon.ico"
/>
<link href="/global-styles.css" rel="stylesheet" />
<!-- Primary meta tags -->
<meta name="title" content="Actual Budget Design System" />
<meta
name="description"
content="Actual Budget is a super fast and privacy-focused app for managing your finances. At its heart is the well proven and much loved Envelope Budgeting methodology."
/>
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://design.actualbudget.org" />
<meta property="og:title" content="Actual Budget Design System" />
<meta
property="og:description"
content="Actual Budget is a super fast and privacy-focused app for managing your finances. At its heart is the well proven and much loved Envelope Budgeting methodology."
/>
<meta property="og:locale" content="en" />
<meta property="og:image" content="https://design.actualbudget.org/og.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Actual Budget Design System" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://design.actualbudget.org/og.webp" />
<!--
Override the default styles used in the Storybook svg icons for the left tree panel.
@see https://storybook.js.org/docs/react/configure/theming#css-escape-hatches
> 💡 NOTE:
>
> This is brittle way for providing custom non thenable styles for manager UI
>
> Those selectors might change on any storybook version bump.
-->
<style>
#storybook-explorer-searchfield {
font-weight: 400 !important;
font-size: 14px !important;
line-height: 14px !important;
}
.sidebar-item svg,
.sidebar-svg-icon {
color: #272630 !important;
}
.sidebar-item[data-selected='true'] svg,
.sidebar-item[data-selected='true'] .sidebar-svg-icon {
color: #ffffff !important;
}
.sidebar-subheading button,
button[data-action='collapse-ref'] {
display: flex !important;
align-items: center !important;
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
letter-spacing: -0.01em !important;
text-transform: none !important;
color: #272630 !important;
}
.sidebar-subheading:hover button,
button[data-action='collapse-ref']:hover {
background-color: transparent !important;
}
.sidebar-item {
align-items: center !important;
font-weight: 400 !important;
font-size: 16px !important;
line-height: 24px !important;
color: #272630 !important;
}
.sidebar-item a {
align-items: center !important;
}
.sidebar-item[data-selected='true'] {
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
color: #ffffff !important;
}
</style>

View File

@@ -3,6 +3,7 @@ import { type ReactNode } from 'react';
import type { Preview } from '@storybook/react-vite';
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
// TODO: this needs refactoring
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
import * as lightTheme from '../../desktop-client/src/style/themes/light';

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
/* Custom Storybook Styling */

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -39,28 +39,30 @@
"test": "npm-run-all -cp 'test:*'",
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
"start:storybook": "storybook dev -p 6006",
"build:storybook": "storybook build"
"build:storybook": "storybook build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@emotion/css": "^11.13.5",
"react-aria-components": "^1.14.0",
"react-aria-components": "^1.15.1",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.0",
"@storybook/addon-a11y": "^10.2.0",
"@storybook/addon-docs": "^10.2.0",
"@storybook/react-vite": "^10.2.0",
"@storybook/addon-a11y": "^10.2.7",
"@storybook/addon-docs": "^10.2.7",
"@storybook/react-vite": "^10.2.7",
"@svgr/cli": "^8.1.0",
"@types/react": "^19.2.5",
"eslint-plugin-storybook": "^10.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"storybook": "^10.2.0",
"vitest": "^4.0.16"
"eslint-plugin-storybook": "^10.2.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"storybook": "^10.2.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.18"
},
"peerDependencies": {
"react": ">=18.2",
"react-dom": ">=18.2"
"react": ">=19.2",
"react-dom": ">=19.2"
}
}

View File

@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { AlignedText } from './AlignedText';
const meta = {
title: 'AlignedText',
title: 'Components/AlignedText',
component: AlignedText,
parameters: {
layout: 'centered',

View File

@@ -4,7 +4,7 @@ import { Block } from './Block';
import { theme } from './theme';
const meta = {
title: 'Block',
title: 'Components/Block',
component: Block,
parameters: {
layout: 'centered',

View File

@@ -4,7 +4,7 @@ import { fn } from 'storybook/test';
import { Button } from './Button';
const meta = {
title: 'Button',
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',

View File

@@ -6,7 +6,7 @@ import { Paragraph } from './Paragraph';
import { theme } from './theme';
const meta = {
title: 'Card',
title: 'Components/Card',
component: Card,
parameters: {
layout: 'centered',

View File

@@ -8,7 +8,7 @@ import { Button } from './Button';
import { ColorPicker } from './ColorPicker';
const meta = {
title: 'ColorPicker',
title: 'Components/ColorPicker',
component: ColorPicker,
parameters: {
layout: 'centered',

View File

@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Introduction" />
<Meta title="Concepts/Introduction" />
# Actual Budget Component Library

View File

@@ -5,7 +5,7 @@ import { Input } from './Input';
import { View } from './View';
const meta = {
title: 'FormError',
title: 'Components/FormError',
component: FormError,
parameters: {
layout: 'centered',

View File

@@ -0,0 +1,86 @@
import { type Ref } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { InitialFocus } from './InitialFocus';
import { Input } from './Input';
import { View } from './View';
const meta = {
title: 'Components/InitialFocus',
component: InitialFocus,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof InitialFocus>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithInput: Story = {
args: {
children: <Input placeholder="This input will be focused on mount" />,
},
render: args => (
<View style={{ width: 300 }}>
<InitialFocus {...args} />
</View>
),
parameters: {
docs: {
description: {
story:
'InitialFocus automatically focuses its child element when the component mounts. The input will receive focus and have its text selected.',
},
},
},
};
export const WithFunctionChild: Story = {
args: {
children: <Input placeholder="Focused via function child" />,
},
render: () => (
<View style={{ width: 300 }}>
<InitialFocus>
{ref => (
<Input
ref={ref as Ref<HTMLInputElement>}
placeholder="Focused via function child"
/>
)}
</InitialFocus>
</View>
),
parameters: {
docs: {
description: {
story:
'InitialFocus can accept a function as child for components that need custom ref handling.',
},
},
},
};
export const MultipleInputsOnlyFirstFocused: Story = {
args: {
children: <Input placeholder="This one is focused" />,
},
render: args => (
<View style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<InitialFocus {...args} />
<Input placeholder="This one is not focused" />
<Input placeholder="This one is also not focused" />
</View>
),
parameters: {
docs: {
description: {
story:
'When multiple inputs are present, only the one wrapped in InitialFocus will receive initial focus.',
},
},
},
};

View File

@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { InlineField } from './InlineField';
import { Input } from './Input';
import { View } from './View';
const meta = {
title: 'Components/InlineField',
component: InlineField,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof InlineField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
label: 'Name',
width: 300,
children: <Input style={{ flex: 1 }} />,
},
parameters: {
docs: {
description: {
story:
'InlineField displays a label and input side-by-side in a horizontal layout.',
},
},
},
};
export const WithCustomLabelWidth: Story = {
args: {
label: 'Email Address',
labelWidth: 120,
width: 400,
children: <Input style={{ flex: 1 }} placeholder="user@example.com" />,
},
parameters: {
docs: {
description: {
story:
'Custom label width can be specified to accommodate longer labels.',
},
},
},
};
export const MultipleFields: Story = {
args: {
label: 'First Name',
width: 300,
},
render: args => (
<View style={{ display: 'flex', flexDirection: 'column' }}>
<InlineField {...args}>
<Input style={{ flex: 1 }} />
</InlineField>
<InlineField label="Last Name" width={300}>
<Input style={{ flex: 1 }} />
</InlineField>
<InlineField label="Email" width={300}>
<Input style={{ flex: 1 }} type="email" />
</InlineField>
</View>
),
parameters: {
docs: {
description: {
story:
'Multiple InlineFields stack vertically with consistent label alignment.',
},
},
},
};
export const WithPercentageWidth: Story = {
args: {
label: 'Description',
width: '100%',
children: <Input style={{ flex: 1 }} />,
},
decorators: [
Story => (
<View style={{ width: 400 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'Width can be specified as a percentage for responsive layouts.',
},
},
},
};

View File

@@ -0,0 +1,215 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Input } from './Input';
import { View } from './View';
const meta = {
title: 'Components/Input',
component: Input,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
placeholder: 'Enter text...',
},
decorators: [
Story => (
<View style={{ width: 250 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'A basic input field with placeholder text.',
},
},
},
};
export const WithValue: Story = {
args: {
defaultValue: 'Hello World',
},
decorators: [
Story => (
<View style={{ width: 250 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'Input with a pre-filled value.',
},
},
},
};
export const Disabled: Story = {
args: {
defaultValue: 'Disabled input',
disabled: true,
},
decorators: [
Story => (
<View style={{ width: 250 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story:
'Disabled inputs prevent user interaction and display muted text.',
},
},
},
};
export const WithOnEnter: Story = {
render: function Render() {
const [submittedValue, setSubmittedValue] = useState('');
return (
<View
style={{
width: 250,
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<Input
placeholder="Press Enter to submit"
onEnter={value => setSubmittedValue(value)}
/>
{submittedValue && <span>Submitted: {submittedValue}</span>}
</View>
);
},
parameters: {
docs: {
description: {
story: 'The onEnter callback is triggered when the user presses Enter.',
},
},
},
};
export const WithOnEscape: Story = {
render: function Render() {
const [escaped, setEscaped] = useState(false);
return (
<View
style={{
width: 250,
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<Input
placeholder="Press Escape to cancel"
onEscape={() => setEscaped(true)}
/>
{escaped && <span>Escape pressed!</span>}
</View>
);
},
parameters: {
docs: {
description: {
story:
'The onEscape callback is triggered when the user presses Escape.',
},
},
},
};
export const WithOnChangeValue: Story = {
render: function Render() {
const [value, setValue] = useState('');
return (
<View
style={{
width: 250,
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<Input
placeholder="Type something..."
onChangeValue={newValue => setValue(newValue)}
/>
<span>Current value: {value}</span>
</View>
);
},
parameters: {
docs: {
description: {
story:
'The onChangeValue callback provides the new value on each keystroke.',
},
},
},
};
export const NumberInput: Story = {
args: {
type: 'number',
placeholder: '0',
},
decorators: [
Story => (
<View style={{ width: 150 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'Input configured for numeric values.',
},
},
},
};
export const PasswordInput: Story = {
args: {
type: 'password',
placeholder: 'Enter password',
},
decorators: [
Story => (
<View style={{ width: 250 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'Password input masks the entered text.',
},
},
},
};

View File

@@ -0,0 +1,97 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Input } from './Input';
import { Label } from './Label';
import { View } from './View';
const meta = {
title: 'Components/Label',
component: Label,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Label>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: 'Username',
},
parameters: {
docs: {
description: {
story: 'A basic label component for form fields.',
},
},
},
};
export const WithInput: Story = {
args: {
title: 'Email Address',
},
render: args => (
<View style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<Label {...args} />
<Input placeholder="user@example.com" style={{ width: 250 }} />
</View>
),
parameters: {
docs: {
description: {
story: 'Label used with an input field in a vertical layout.',
},
},
},
};
export const MultipleLabels: Story = {
args: {
title: 'First Name',
},
render: args => (
<View style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<View style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<Label {...args} />
<Input style={{ width: 250 }} />
</View>
<View style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<Label title="Last Name" />
<Input style={{ width: 250 }} />
</View>
<View style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<Label title="Password" />
<Input type="password" style={{ width: 250 }} />
</View>
</View>
),
parameters: {
docs: {
description: {
story: 'Multiple labels and inputs in a form layout.',
},
},
},
};
export const CustomStyle: Story = {
args: {
title: 'Custom Styled Label',
style: {
fontSize: 16,
color: '#007bff',
textAlign: 'left',
},
},
parameters: {
docs: {
description: {
story: 'Label with custom styling applied.',
},
},
},
};

View File

@@ -0,0 +1,243 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { SvgAdd, SvgTrash } from './icons/v1';
import { SvgPencil1 } from './icons/v2';
import { Menu, type MenuItem } from './Menu';
import { Text } from './Text';
import { View } from './View';
const meta = {
title: 'Components/Menu',
component: Menu,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Menu>;
export default meta;
type Story = StoryObj<typeof meta>;
const basicItems: Array<MenuItem<string>> = [
{ name: 'edit', text: 'Edit' },
{ name: 'duplicate', text: 'Duplicate' },
{ name: 'delete', text: 'Delete' },
];
export const Default: Story = {
args: {
items: basicItems,
},
parameters: {
docs: {
description: {
story: 'A basic menu with simple text items.',
},
},
},
};
export const WithIcons: Story = {
args: {
items: [
{ name: 'add', text: 'Add New', icon: SvgAdd },
{ name: 'edit', text: 'Edit', icon: SvgPencil1 },
{ name: 'delete', text: 'Delete', icon: SvgTrash },
],
},
parameters: {
docs: {
description: {
story: 'Menu items can include icons for visual clarity.',
},
},
},
};
export const WithSeparator: Story = {
args: {
items: [
{ name: 'cut', text: 'Cut' },
{ name: 'copy', text: 'Copy' },
{ name: 'paste', text: 'Paste' },
Menu.line,
{ name: 'delete', text: 'Delete' },
],
},
parameters: {
docs: {
description: {
story: 'Menu.line creates a visual separator between menu sections.',
},
},
},
};
export const WithLabel: Story = {
args: {
items: [
{ type: Menu.label, name: 'Actions', text: 'Actions' },
{ name: 'edit', text: 'Edit' },
{ name: 'duplicate', text: 'Duplicate' },
Menu.line,
{ type: Menu.label, name: 'Danger Zone', text: 'Danger Zone' },
{ name: 'delete', text: 'Delete' },
],
},
parameters: {
docs: {
description: {
story: 'Menu.label items create section headers within the menu.',
},
},
},
};
export const WithDisabledItems: Story = {
args: {
items: [
{ name: 'edit', text: 'Edit' },
{ name: 'duplicate', text: 'Duplicate', disabled: true },
{ name: 'delete', text: 'Delete' },
],
},
parameters: {
docs: {
description: {
story: 'Disabled menu items are visually muted and non-interactive.',
},
},
},
};
export const WithKeyboardShortcuts: Story = {
args: {
items: [
{ name: 'cut', text: 'Cut', key: 'ctrl + X' },
{ name: 'copy', text: 'Copy', key: 'ctrl + C' },
{ name: 'paste', text: 'Paste', key: 'ctrl + V' },
],
},
parameters: {
docs: {
description: {
story: 'Menu items can display keyboard shortcuts.',
},
},
},
};
export const WithToggle: Story = {
args: {
items: [],
},
render: function Render() {
const [settings, setSettings] = useState({
notifications: true,
darkMode: false,
autoSave: true,
});
const items: Array<MenuItem<'notifications' | 'darkMode' | 'autoSave'>> = [
{
name: 'notifications',
text: 'Notifications',
toggle: settings.notifications,
},
{ name: 'darkMode', text: 'Dark Mode', toggle: settings.darkMode },
{ name: 'autoSave', text: 'Auto Save', toggle: settings.autoSave },
];
return (
<Menu
items={items}
onMenuSelect={name => {
setSettings(prev => ({ ...prev, [name]: !prev[name] }));
}}
/>
);
},
parameters: {
docs: {
description: {
story: 'Menu items can include toggles for boolean settings.',
},
},
},
};
export const WithHeaderAndFooter: Story = {
args: {
header: (
<View style={{ padding: 10, borderBottom: '1px solid #ccc' }}>
<Text style={{ fontWeight: 'bold' }}>Menu Title</Text>
</View>
),
footer: (
<View style={{ padding: 10, borderTop: '1px solid #ccc' }}>
<Text style={{ fontSize: 11, color: '#666' }}>3 items</Text>
</View>
),
items: basicItems,
},
parameters: {
docs: {
description: {
story: 'Menus can have custom header and footer content.',
},
},
},
};
export const WithTooltips: Story = {
args: {
items: [
{ name: 'edit', text: 'Edit', tooltip: 'Modify this item' },
{
name: 'duplicate',
text: 'Duplicate',
tooltip: 'Create a copy of this item',
},
{
name: 'delete',
text: 'Delete',
tooltip: 'Permanently remove this item',
},
],
},
parameters: {
docs: {
description: {
story: 'Menu items can have tooltips for additional context.',
},
},
},
};
export const InteractiveExample: Story = {
args: {
items: basicItems,
},
render: function Render(args) {
const [selected, setSelected] = useState<string | null>(null);
return (
<View style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<Menu {...args} onMenuSelect={name => setSelected(String(name))} />
{selected && (
<Text style={{ textAlign: 'center' }}>Selected: {selected}</Text>
)}
</View>
);
},
parameters: {
docs: {
description: {
story: 'Interactive menu that shows the selected item.',
},
},
},
};

View File

@@ -0,0 +1,134 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Paragraph } from './Paragraph';
import { View } from './View';
const meta = {
title: 'Components/Paragraph',
component: Paragraph,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Paragraph>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children:
'This is a paragraph of text. Paragraphs are used to display blocks of text content with proper line height and spacing.',
},
decorators: [
Story => (
<View style={{ width: 400 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'A basic paragraph with default styling and bottom margin.',
},
},
},
};
export const MultipleParagraphs: Story = {
render: () => (
<View style={{ width: 400 }}>
<Paragraph>
This is the first paragraph. It has a bottom margin to create spacing
between itself and the next paragraph.
</Paragraph>
<Paragraph>
This is the second paragraph. Notice the consistent spacing between
paragraphs which improves readability.
</Paragraph>
<Paragraph isLast>
This is the last paragraph. It uses the isLast prop to remove the bottom
margin since there is no following content.
</Paragraph>
</View>
),
parameters: {
docs: {
description: {
story:
'Multiple paragraphs stack with consistent spacing. Use isLast on the final paragraph.',
},
},
},
};
export const IsLast: Story = {
args: {
children: 'This paragraph has no bottom margin because isLast is true.',
isLast: true,
},
decorators: [
Story => (
<View style={{ width: 400, border: '1px dashed #ccc', padding: 10 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story:
'When isLast is true, the bottom margin is removed. Useful for the last paragraph in a section.',
},
},
},
};
export const WithCustomStyle: Story = {
args: {
children: 'This paragraph has custom styling applied.',
style: {
color: '#007bff',
fontStyle: 'italic',
fontSize: 18,
},
},
decorators: [
Story => (
<View style={{ width: 400 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story: 'Custom styles can be applied to paragraphs.',
},
},
},
};
export const LongContent: Story = {
args: {
children:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
},
decorators: [
Story => (
<View style={{ width: 400 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story:
'Longer paragraphs wrap properly and maintain consistent line height for readability.',
},
},
},
};

View File

@@ -0,0 +1,153 @@
import { useRef, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
import { Menu } from './Menu';
import { Popover } from './Popover';
import { View } from './View';
const meta = {
title: 'Components/Popover',
component: Popover,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Popover>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => {
const triggerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button ref={triggerRef} onPress={() => setIsOpen(!isOpen)}>
Toggle Popover
</Button>
<Popover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<View style={{ padding: 10 }}>Popover content</View>
</Popover>
</>
);
},
parameters: {
docs: {
description: {
story: 'A basic popover triggered by a button click.',
},
},
},
};
export const WithMenu: Story = {
render: () => {
const triggerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button ref={triggerRef} onPress={() => setIsOpen(!isOpen)}>
Open Menu
</Button>
<Popover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<Menu
onMenuSelect={() => setIsOpen(false)}
items={[
{ name: 'edit', text: 'Edit' },
{ name: 'duplicate', text: 'Duplicate' },
Menu.line,
{ name: 'delete', text: 'Delete' },
]}
/>
</Popover>
</>
);
},
parameters: {
docs: {
description: {
story:
'Popover containing a menu, a common pattern for dropdown menus.',
},
},
},
};
export const CustomPlacement: Story = {
render: () => {
const triggerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button ref={triggerRef} onPress={() => setIsOpen(!isOpen)}>
Bottom Start
</Button>
<Popover
triggerRef={triggerRef}
placement="bottom start"
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<View style={{ padding: 10 }}>
This popover is placed at bottom start.
</View>
</Popover>
</>
);
},
parameters: {
docs: {
description: {
story: 'Popover with custom placement.',
},
},
},
};
export const CustomStyle: Story = {
render: () => {
const triggerRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button ref={triggerRef} onPress={() => setIsOpen(!isOpen)}>
Styled Popover
</Button>
<Popover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
style={{ padding: 15, maxWidth: 250 }}
>
<View>
This popover has custom padding and a constrained max width for
longer content.
</View>
</Popover>
</>
);
},
parameters: {
docs: {
description: {
story: 'Popover with custom styles applied.',
},
},
},
};

View File

@@ -0,0 +1,178 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Menu } from './Menu';
import { Select } from './Select';
import { View } from './View';
const meta = {
title: 'Components/Select',
component: Select,
parameters: {
layout: 'centered',
docs: {
description: {
component: ' ', // Remove autogenerated description (generated from JSDoc) to replace with custom description below
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof Select>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
options: [
['apple', 'Apple'],
['banana', 'Banana'],
['cherry', 'Cherry'],
],
value: 'apple',
},
parameters: {
docs: {
description: {
story: 'A basic select dropdown with simple options.',
},
},
},
};
export const WithDefaultLabel: Story = {
args: {
options: [
['small', 'Small'],
['medium', 'Medium'],
['large', 'Large'],
],
value: '',
defaultLabel: 'Select a size...',
},
parameters: {
docs: {
description: {
story:
'When the selected value is not in the options, the defaultLabel is displayed.',
},
},
},
};
export const WithSeparator: Story = {
args: {
options: [
['recent-1', 'Budget 2024'],
['recent-2', 'Budget 2025'],
Menu.line,
['all', 'View All'],
],
value: 'recent-1',
},
parameters: {
docs: {
description: {
story: 'Select options can include separators using Menu.line.',
},
},
},
};
export const WithDisabledKeys: Story = {
args: {
options: [
['draft', 'Draft'],
['pending', 'Pending'],
['approved', 'Approved'],
['archived', 'Archived'],
],
value: 'draft',
disabledKeys: ['approved', 'archived'],
},
parameters: {
docs: {
description: {
story: 'Certain options can be disabled using the disabledKeys prop.',
},
},
},
};
export const BareVariant: Story = {
args: {
bare: true,
options: [
['day', 'Day'],
['week', 'Week'],
['month', 'Month'],
],
value: 'month',
},
parameters: {
docs: {
description: {
story:
'The bare variant renders the select without a bordered button style.',
},
},
},
};
export const Disabled: Story = {
args: {
options: [
['opt1', 'Option 1'],
['opt2', 'Option 2'],
],
value: 'opt1',
disabled: true,
},
parameters: {
docs: {
description: {
story: 'A disabled select that cannot be interacted with.',
},
},
},
};
export const Controlled: Story = {
args: {
options: [
['usd', 'USD - US Dollar'],
['eur', 'EUR - Euro'],
['gbp', 'GBP - British Pound'],
['jpy', 'JPY - Japanese Yen'],
],
value: 'usd',
},
render: function Render() {
const [value, setValue] = useState('usd');
return (
<View style={{ gap: 10, alignItems: 'flex-start' }}>
<Select
options={[
['usd', 'USD - US Dollar'],
['eur', 'EUR - Euro'],
['gbp', 'GBP - British Pound'],
['jpy', 'JPY - Japanese Yen'],
]}
value={value}
onChange={setValue}
/>
<span>Selected: {value}</span>
</View>
);
},
parameters: {
docs: {
description: {
story: 'A controlled select with external state management.',
},
},
},
};

View File

@@ -0,0 +1,140 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
import { SpaceBetween } from './SpaceBetween';
import { View } from './View';
const meta = {
title: 'Components/SpaceBetween',
component: SpaceBetween,
parameters: {
layout: 'centered',
},
args: {
style: {
display: 'flex',
},
},
tags: ['autodocs'],
} satisfies Meta<typeof SpaceBetween>;
export default meta;
type Story = StoryObj<typeof meta>;
const Box = ({ children }: { children: string }) => (
<View
style={{
padding: '10px 20px',
backgroundColor: '#e0e0e0',
borderRadius: 4,
display: 'flex',
}}
>
{children}
</View>
);
export const Horizontal: Story = {
args: {
direction: 'horizontal',
children: (
<>
<Box>Item 1</Box>
<Box>Item 2</Box>
<Box>Item 3</Box>
</>
),
},
parameters: {
docs: {
description: {
story:
'SpaceBetween lays out children horizontally with even spacing by default.',
},
},
},
};
export const Vertical: Story = {
args: {
direction: 'vertical',
children: (
<>
<Box>Item 1</Box>
<Box>Item 2</Box>
<Box>Item 3</Box>
</>
),
},
parameters: {
docs: {
description: {
story: 'Items laid out vertically with default spacing.',
},
},
},
};
export const CustomGap: Story = {
args: {
direction: 'horizontal',
gap: 30,
children: (
<>
<Box>Gap 30</Box>
<Box>Gap 30</Box>
<Box>Gap 30</Box>
</>
),
},
parameters: {
docs: {
description: {
story: 'Custom gap between items.',
},
},
},
};
export const NoWrap: Story = {
args: {
direction: 'horizontal',
wrap: false,
children: (
<>
<Box>No Wrap</Box>
<Box>No Wrap</Box>
<Box>No Wrap</Box>
<Box>No Wrap</Box>
</>
),
},
parameters: {
docs: {
description: {
story: 'Items will not wrap to the next line when wrap is false.',
},
},
},
};
export const WithButtons: Story = {
args: {
direction: 'horizontal',
gap: 10,
children: (
<>
<Button variant="bare">Cancel</Button>
<Button variant="primary">Save</Button>
</>
),
},
parameters: {
docs: {
description: {
story: 'A common use case: spacing action buttons.',
},
},
},
};

View File

@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Text } from './Text';
import { View } from './View';
const meta = {
title: 'Components/Text',
component: Text,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Text>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'This is a text element',
},
parameters: {
docs: {
description: {
story: 'A basic Text component renders as a span element.',
},
},
},
};
export const WithStyle: Story = {
args: {
children: 'Styled text',
style: {
fontSize: 18,
fontWeight: 'bold',
color: '#1a73e8',
},
},
parameters: {
docs: {
description: {
story: 'Text can accept custom styles via the style prop.',
},
},
},
};
export const FontSizes: Story = {
render: () => (
<View style={{ gap: 8 }}>
<Text style={{ fontSize: 12 }}>Small (12px)</Text>
<Text style={{ fontSize: 14 }}>Default (14px)</Text>
<Text style={{ fontSize: 18 }}>Medium (18px)</Text>
<Text style={{ fontSize: 24 }}>Large (24px)</Text>
<Text style={{ fontSize: 32 }}>Extra Large (32px)</Text>
</View>
),
parameters: {
docs: {
description: {
story: 'Text at various font sizes.',
},
},
},
};
export const FontWeights: Story = {
render: () => (
<View style={{ gap: 8 }}>
<Text style={{ fontWeight: 300 }}>Light (300)</Text>
<Text style={{ fontWeight: 400 }}>Normal (400)</Text>
<Text style={{ fontWeight: 500 }}>Medium (500)</Text>
<Text style={{ fontWeight: 600 }}>Semi Bold (600)</Text>
<Text style={{ fontWeight: 700 }}>Bold (700)</Text>
</View>
),
parameters: {
docs: {
description: {
story: 'Text at various font weights.',
},
},
},
};
export const InlineUsage: Story = {
render: () => (
<View>
<span>
This is regular text with{' '}
<Text style={{ fontWeight: 'bold', color: '#d32f2f' }}>
highlighted
</Text>{' '}
and{' '}
<Text style={{ fontStyle: 'italic', color: '#1a73e8' }}>
emphasized
</Text>{' '}
portions.
</span>
</View>
),
parameters: {
docs: {
description: {
story:
'Text renders as a span, making it suitable for inline styling within other text.',
},
},
},
};

View File

@@ -0,0 +1,105 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { TextOneLine } from './TextOneLine';
import { View } from './View';
const meta = {
title: 'Components/TextOneLine',
component: TextOneLine,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof TextOneLine>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children:
'This is a single line of text that will be truncated with an ellipsis if it overflows its container',
style: { maxWidth: 300 },
},
parameters: {
docs: {
description: {
story:
'TextOneLine truncates overflowing text with an ellipsis, keeping content to a single line.',
},
},
},
};
export const ShortText: Story = {
args: {
children: 'Short text',
style: { maxWidth: 300 },
},
parameters: {
docs: {
description: {
story: 'When text fits within the container, no truncation occurs.',
},
},
},
};
export const NarrowContainer: Story = {
args: {
children:
'This text will be truncated because the container is very narrow',
style: { maxWidth: 120 },
},
parameters: {
docs: {
description: {
story: 'A narrow container forces earlier truncation.',
},
},
},
};
export const ComparisonWithText: Story = {
render: () => (
<View style={{ gap: 15, maxWidth: 200 }}>
<View>
<strong>TextOneLine:</strong>
<TextOneLine>
This is a long piece of text that should be truncated
</TextOneLine>
</View>
<View>
<strong>Regular span:</strong>
<span>This is a long piece of text that will wrap normally</span>
</View>
</View>
),
parameters: {
docs: {
description: {
story:
'Comparison between TextOneLine (truncated) and regular text (wrapping).',
},
},
},
};
export const WithCustomStyle: Story = {
args: {
children: 'Bold truncated text in a constrained container',
style: {
maxWidth: 200,
fontWeight: 'bold',
fontSize: 16,
},
},
parameters: {
docs: {
description: {
story: 'TextOneLine with additional custom styles applied.',
},
},
},
};

View File

@@ -0,0 +1,11 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Themes/Theming" />
# Theming
Actual Budget supports customizable themes that allow you to personalize the look and feel of the application. You can switch between built-in themes or create your own custom themes.
For detailed information on how to create and apply custom themes, please visit the official documentation:
**[Custom Themes Documentation](https://actualbudget.org/docs/experimental/custom-themes)**

View File

@@ -0,0 +1,150 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Text } from './Text';
import { Toggle } from './Toggle';
import { View } from './View';
const meta = {
title: 'Components/Toggle',
component: Toggle,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Toggle>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Off: Story = {
args: {
id: 'toggle-off',
isOn: false,
},
parameters: {
docs: {
description: {
story: 'Toggle in the off state.',
},
},
},
};
export const On: Story = {
args: {
id: 'toggle-on',
isOn: true,
},
parameters: {
docs: {
description: {
story: 'Toggle in the on state.',
},
},
},
};
export const Disabled: Story = {
args: {
id: 'toggle-disabled',
isOn: false,
isDisabled: true,
},
parameters: {
docs: {
description: {
story: 'A disabled toggle that cannot be interacted with.',
},
},
},
};
export const DisabledOn: Story = {
args: {
id: 'toggle-disabled-on',
isOn: true,
isDisabled: true,
},
parameters: {
docs: {
description: {
story: 'A disabled toggle in the on state.',
},
},
},
};
export const Interactive: Story = {
args: {
id: 'toggle-interactive',
isOn: false,
},
render: function Render() {
const [isOn, setIsOn] = useState(false);
return (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Toggle id="toggle-interactive" isOn={isOn} onToggle={setIsOn} />
<Text>{isOn ? 'Enabled' : 'Disabled'}</Text>
</View>
);
},
parameters: {
docs: {
description: {
story: 'An interactive toggle with state feedback.',
},
},
},
};
export const WithLabels: Story = {
args: {
id: 'toggle-labels',
isOn: false,
},
render: function Render() {
const [notifications, setNotifications] = useState(true);
const [darkMode, setDarkMode] = useState(false);
const [autoSave, setAutoSave] = useState(true);
return (
<View style={{ gap: 12 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Toggle
id="toggle-notifications"
isOn={notifications}
onToggle={setNotifications}
/>
<Text>Notifications</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Toggle
id="toggle-dark-mode"
isOn={darkMode}
onToggle={setDarkMode}
/>
<Text>Dark Mode</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Toggle
id="toggle-auto-save"
isOn={autoSave}
onToggle={setAutoSave}
/>
<Text>Auto Save</Text>
</View>
</View>
);
},
parameters: {
docs: {
description: {
story: 'Multiple toggles in a settings-style layout.',
},
},
},
};

View File

@@ -0,0 +1,135 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
import { Text } from './Text';
import { Tooltip } from './Tooltip';
import { View } from './View';
const meta = {
title: 'Components/Tooltip',
component: Tooltip,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Tooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
content: 'This is a tooltip',
children: <Button>Hover me</Button>,
},
parameters: {
docs: {
description: {
story: 'A basic tooltip displayed on hover after a short delay.',
},
},
},
};
export const WithTextTrigger: Story = {
args: {
content: 'More information about this term',
children: (
<Text style={{ textDecoration: 'underline', cursor: 'help' }}>
Hover for details
</Text>
),
},
parameters: {
docs: {
description: {
story: 'A tooltip triggered by hovering over text.',
},
},
},
};
export const RichContent: Story = {
args: {
content: (
<View style={{ padding: 5, maxWidth: 200 }}>
<Text style={{ fontWeight: 'bold' }}>Tip</Text>
<Text>
You can use keyboard shortcuts to navigate faster through the
application.
</Text>
</View>
),
children: <Button>Rich Tooltip</Button>,
},
parameters: {
docs: {
description: {
story: 'Tooltip content can include rich React elements.',
},
},
},
};
export const CustomPlacement: Story = {
args: {
content: 'Tooltip',
children: <></>,
},
render: () => (
<View style={{ gap: 10, display: 'flex', flexDirection: 'row' }}>
<Tooltip content="Top placement" placement="top">
<Button>Top</Button>
</Tooltip>
<Tooltip content="Bottom placement" placement="bottom">
<Button>Bottom</Button>
</Tooltip>
<Tooltip content="Left placement" placement="left">
<Button>Left</Button>
</Tooltip>
<Tooltip content="Right placement" placement="right">
<Button>Right</Button>
</Tooltip>
</View>
),
parameters: {
docs: {
description: {
story:
'Tooltips can be placed in different positions around the trigger.',
},
},
},
};
export const DisabledTooltip: Story = {
args: {
content: 'You should not see this',
children: <Button>Hover me (disabled)</Button>,
triggerProps: { isDisabled: true },
},
parameters: {
docs: {
description: {
story:
'A tooltip can be disabled via triggerProps, preventing it from appearing.',
},
},
},
};
export const CustomDelay: Story = {
args: {
content: 'This tooltip appears after 1 second',
children: <Button>Slow Tooltip</Button>,
triggerProps: { delay: 1000 },
},
parameters: {
docs: {
description: {
story: 'The delay before the tooltip appears can be customized.',
},
},
},
};

View File

@@ -0,0 +1,215 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Text } from './Text';
import { View } from './View';
const meta = {
title: 'Components/View',
component: View,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof View>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'A basic View container',
style: { padding: 20, backgroundColor: '#f5f5f5', borderRadius: 4 },
},
parameters: {
docs: {
description: {
story:
'View is the fundamental layout building block, rendering a styled div element.',
},
},
},
};
export const FlexRow: Story = {
render: () => (
<View
style={{
display: 'flex',
flexDirection: 'row',
gap: 10,
padding: 10,
backgroundColor: '#f5f5f5',
}}
>
<View
style={{
padding: 15,
backgroundColor: '#e3f2fd',
borderRadius: 4,
}}
>
Item 1
</View>
<View
style={{
padding: 15,
backgroundColor: '#e8f5e9',
borderRadius: 4,
}}
>
Item 2
</View>
<View
style={{
padding: 15,
backgroundColor: '#fff3e0',
borderRadius: 4,
}}
>
Item 3
</View>
</View>
),
parameters: {
docs: {
description: {
story: 'Views arranged in a horizontal row using flexDirection.',
},
},
},
};
export const FlexColumn: Story = {
render: () => (
<View
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
padding: 10,
backgroundColor: '#f5f5f5',
}}
>
<View
style={{ padding: 15, backgroundColor: '#e3f2fd', borderRadius: 4 }}
>
Row 1
</View>
<View
style={{ padding: 15, backgroundColor: '#e8f5e9', borderRadius: 4 }}
>
Row 2
</View>
<View
style={{ padding: 15, backgroundColor: '#fff3e0', borderRadius: 4 }}
>
Row 3
</View>
</View>
),
parameters: {
docs: {
description: {
story: 'Views stacked vertically in a column layout.',
},
},
},
};
export const Nested: Story = {
render: () => (
<View
style={{
padding: 15,
backgroundColor: '#f5f5f5',
borderRadius: 4,
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
<Text style={{ fontWeight: 'bold', marginBottom: 10 }}>Parent View</Text>
<View
style={{
display: 'flex',
flexDirection: 'row',
gap: 10,
}}
>
<View
style={{
flex: 1,
padding: 10,
backgroundColor: '#e3f2fd',
borderRadius: 4,
}}
>
Child 1 (flex: 1)
</View>
<View
style={{
flex: 2,
padding: 10,
backgroundColor: '#e8f5e9',
borderRadius: 4,
}}
>
Child 2 (flex: 2)
</View>
</View>
</View>
),
parameters: {
docs: {
description: {
story: 'Nested Views demonstrating flex layout composition.',
},
},
},
};
export const WithNativeStyle: Story = {
args: {
children: 'View with nativeStyle',
nativeStyle: {
padding: '20px',
border: '2px dashed #999',
borderRadius: '8px',
},
},
parameters: {
docs: {
description: {
story:
'The nativeStyle prop applies styles directly via the style attribute instead of using Emotion CSS.',
},
},
},
};
export const CenteredContent: Story = {
render: () => (
<View
style={{
width: 300,
height: 200,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
borderRadius: 8,
border: '1px solid #ddd',
}}
>
<Text>Centered Content</Text>
</View>
),
parameters: {
docs: {
description: {
story: 'View used to center content both horizontally and vertically.',
},
},
},
};

View File

@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"noEmit": false,
"declaration": true,
"declarationMap": true,
"rootDir": "src",
"strict": true,
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -5,7 +5,6 @@ import { defineConfig } from 'vitest/config';
const resolveExtensions = [
'.testing.ts',
'.web.ts',
'.mjs',
'.js',
'.mts',

View File

@@ -9,10 +9,11 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build:node": "tsc --p tsconfig.dist.json",
"build:node": "tsc",
"proto:generate": "./bin/generate-proto",
"build": "rm -rf dist && yarn run build:node",
"test": "vitest --run"
"test": "vitest --run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"google-protobuf": "^3.21.4",
@@ -24,6 +25,6 @@
"protoc-gen-js": "3.21.4-4",
"ts-protoc-gen": "0.15.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"vitest": "^4.0.18"
}
}

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
// Using ES2021 because that's the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
@@ -8,8 +9,10 @@
"moduleResolution": "node10",
"noEmit": false,
"declaration": true,
"declarationMap": true,
"strict": true,
"outDir": "dist"
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": ["."],
"exclude": ["dist", "**/*.test.ts", "**/*.spec.ts"]

View File

@@ -65,10 +65,10 @@ Run manually:
```sh
# Run docker container
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.58.2-jammy /bin/bash
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.58.2-jammy /bin/bash
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
# Use the ip and port noted earlier

View File

@@ -46,7 +46,7 @@ test.describe('Mobile Accounts', () => {
await expect(accountPage.heading).toHaveText('Bank of America');
await expect(accountPage.transactionList).toBeVisible();
await expect(await accountPage.getBalance()).toBeGreaterThan(0);
expect(await accountPage.getBalance()).toBeGreaterThan(0);
await expect(accountPage.noTransactionsMessage).not.toBeVisible();
await expect(page).toMatchThemeScreenshots();

View File

@@ -158,9 +158,7 @@ test.describe('Accounts', () => {
await expect(page).toMatchThemeScreenshots();
await expect(importButton).toBeDisabled();
await expect(await importButton.innerText()).toMatch(
/Import 0 transactions/,
);
expect(await importButton.innerText()).toMatch(/Import 0 transactions/);
await accountPage.page.getByRole('button', { name: 'Close' }).click();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -56,7 +56,7 @@ export class MobileAccountPage {
* Go to transaction creation page
*/
async clickCreateTransaction() {
this.createTransactionButton.click();
await this.createTransactionButton.click();
return new MobileTransactionEntryPage(this.page);
}
}

View File

@@ -148,7 +148,7 @@ export class MobileBudgetPage {
return groupNameText;
}
#getButtonForCategoryGroup(categoryGroupName: string | RegExp) {
async #getButtonForCategoryGroup(categoryGroupName: string | RegExp) {
return this.categoryGroupRows.getByRole('button', {
name: categoryGroupName,
exact: true,
@@ -169,7 +169,7 @@ export class MobileBudgetPage {
return categoryNameText;
}
#getButtonForCategory(categoryName: string | RegExp) {
async #getButtonForCategory(categoryName: string | RegExp) {
return this.categoryRows.getByRole('button', {
name: categoryName,
exact: true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 123 KiB

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