Compare commits

..

24 Commits

Author SHA1 Message Date
dependabot[bot]
9fc4ea8be7 Bump webpack-dev-server from 5.2.2 to 5.2.4
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.2 to 5.2.4.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.2...v5.2.4)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 12:55:39 +00:00
Matt Fiddaman
ecfe43fdda split release into two branches (#7844)
* split release into two branches

* update docs to match the new process

* note

* zizmor comment

* Update check-spelling metadata

* use single long lived release branch

* coderabbit

* jfdoming suggestions
2026-05-19 12:47:08 +00:00
Matt Fiddaman
132b9db11c use separate_continuous_history_consent flag from GoCardless (#7890)
* use separate_continuous_history_consent flag from GoCardless

* add logging for GCL

* note

* test
2026-05-18 22:29:46 +00:00
Emil Tveden Bjerglund
218a3c68dd Various sankey improvements and bugfixes (#7682)
* Point Overbudgeted node to Budget instead of Available funds

* Extract hidden node handling to separate function

* Fix income without payee placement and Overspent - Budgeted value

* Fix TopN. Handle negative budgeting differently.

* Better handling of negative budgeting and spending

* Disclaimer for negative budgeting

* Add account grouping in Spent view

* Fix Options menu closing on toggle

* Fix graph title not saved when changed

* [AI] Add unit tests for spreadsheet code

* Try dynamic topN selection

* Separate data fetching from data processing, to be more smooth on size changes

* Ensure that 'All' can be saved as a value

* Throttle height adjustment

* Random, but stable color assignment

* Adjust node size and Sankey card min height

* Adjust AllAccounts node color

* Generalise groupOtherCategories to work for all layers

* Generalise sorting of nodes to also work for payee > income_category

* Fix layer filtering

* Add release note

* Address coderabbit comments

* More coderabbit stuff

* Fixed wrong income classification

This could in rare cases cause a recursion error due to cycles in the graph

* Various fixes
2026-05-18 13:09:13 +00:00
Matt Fiddaman
a587ed3ebc automation UI: add increase/decrease functionality (#7881)
* add increase/decrease

* note

* coderabbit
2026-05-17 21:23:12 +00:00
Alec Bakholdin
390ed57c46 Tag autocomplete v2 (#7654)
* [AI] Make TypeScript work in test files across packages

Match the CRDT package's tsconfig pattern in loot-core, desktop-client,
api, and desktop-electron so test files participate in the project graph
(IDE intellisense, project-wide typecheck) while production builds still
emit clean declaration files.

- Remove test-file exclusions from each package's main tsconfig
- Add tsconfig.build.json for loot-core and api with test exclusions,
  used by the build scripts
- Add e2e/tsconfig.json for desktop-client and desktop-electron with
  Playwright types
- Fix latent type errors in test files now caught by typecheck
- Disable typescript/unbound-method for test files (mock matcher pattern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [AI] Address review feedback on test type fixes

- goal-template.test.ts: extract amounts to typed locals so single-value
  assertions no longer compare against unknown
- category-template-context.test.ts: replace `as unknown as DbCategory`
  double-cast with a fully-typed object using `satisfies DbCategory`
  (the previous mock had `is_income: true` which doesn't match the
  `1 | 0` shape the cast was hiding)
- api/tsconfig.build.json: broaden test exclude pattern to `**/*.test.ts`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [AI] Drop tsconfig.build.json for loot-core and api

The build-config indirection was incomplete protection: typecheck
(`tsgo -b` against the main tsconfig, which now includes test files)
already emits `*.test.d.ts` into `@types/`, and the build step does
not clean before re-emitting. The same is observable in crdt's
`dist/`, which currently contains test declarations on disk.

What actually keeps test declarations out of the npm tarball is the
`files` field in package.json — and loot-core already uses that
mechanism for source files (`\!src/**/*.test.ts`). Extending the same
pattern to `@types/` is more direct than maintaining a duplicate
tsconfig that doesn't reliably do its job.

- Delete loot-core/tsconfig.build.json; revert build to `tsgo -b`;
  add `\!@types/**/*.test.d.ts*`, `\!@types/**/__tests__/**`,
  `\!@types/**/__mocks__/**` to `files`.
- Delete api/tsconfig.build.json; revert build to
  `vite build && tsgo --emitDeclarationOnly`; add
  `\!@types/**/*.test.d.ts*` to `files`.

Verified: `yarn pack --dry-run` excludes all test declarations from
both packages while production declarations still pack (428 .d.ts
files for loot-core, methods.d.ts for api).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* first pass at implementation

* finished implementing what I feel would be a good user experience

* padding update

* added ability to use tab to select

* release notes

* updated release notes

* auto highlights first entry

* removed debug info

* linting formatting

* updated behavior with tags in the middle of stuff

* minor improvements to ux

* extracted TagAutocomplete functionality into its own file

* rewrote TagAutocomplete using react-aria-components from scratch

* linting

* restored old autocomplete since I'm no longer using it

* linting

* capped results at 10

* bugfix

* used tag css hook instead of notes tag formatter

* added mobile support

* fixed bug where inserting a tag at the end of the note wouldn't fire onChange handler

* fixed bug where note would hover even though field was not focused

* fixed bug with currentWord detection

* some more minor UI bugs with the mobile UI

* removed log statement

* added aria-label

* [autofix.ci] apply automated fixes

* name to Input

* updatedt ests

* fixed tests

* whitespace adjustment

* removed debug from test

* added tests and made a few fixes

* tried to fix vrt

* some new hooks to use

* removed log statement

* removed input from useCursorPosition

* input ref value hook now triggers on mount

* add extra padding

* hide browser autocomplete when our popup is shown to avoid doubles

* useFilteredTags hook and adjustments to mobile tag UI

* switched from opacity to height transitioning

* sorted filtered tags by startsWith first

* updated highlighting logic for mouseover

* [autofix.ci] apply automated fixes

* fix tag ordering

* button type

* [autofix.ci] apply automated fixes

* release note update

* release note to trigger ci

* reverted db change

* Revert "reverted db change"

This reverts commit 24ce5450e5.

* added fzf

---------

Co-authored-by: github-actions[bot] <matiss@mja.lv>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Alec Bakholdin <alecbakholdin.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-17 18:57:45 +00:00
Sebastián Maluk
fee8fbccd1 [AI] Fix balance forecast all future range (#7849)
* [AI] Fix forecast all future range

* [AI] Add release note for balance forecast range fix
2026-05-17 18:52:54 +00:00
mehmet turac
44f1aac59a [AI] Fix CSV preview category normalization (#7878) 2026-05-17 17:58:37 +00:00
Matiss Janis Aboltins
67a439f13f [AI] Tighten VRT per-pixel threshold so faint overlays fail (#7864)
* [AI] Tighten VRT per-pixel threshold so faint overlays fail

Playwright's default `threshold: 0.2` translates to a pixelmatch YIQ
delta cutoff of ~1408, which silently swallows low-alpha overlays. PR
#7841 striped the transactions table with rgba(..., .15); the resulting
per-pixel deltas (~270 light, ~320 dark) fell below the cutoff, so VRT
reported 0 diff pixels and passed despite a clearly visible change.

Drop `threshold` to 0.05 (cutoff ~88) so faint tints are flagged while
keeping headroom for anti-aliasing noise.

* [AI] Apply VRT threshold to electron config and drop redundant local override

- Remove `maxDiffPixels: 5` from `toMatchThemeScreenshots` — it duplicates
  the global config and obscured the fact that per-call options would
  override the global threshold if added there too.
- Mirror the threshold in desktop-electron's playwright config so its
  VRT screenshots are subject to the same sensitivity.
- Tighten the threshold comment: drop the PR/task reference per
  AGENTS.md and the worked-example numbers; keep the formula and the
  low-alpha rationale.

* Add release notes for PR #7864

* Revert "[AI] Apply VRT threshold to electron config and drop redundant local override"

This reverts commit ec789703ea.

* Revert "[AI] Tighten VRT per-pixel threshold so faint overlays fail"

This reverts commit 334954bb33.

* [AI] Remove stale release note for reverted threshold change

* Revert "[AI] Remove stale release note for reverted threshold change"

This reverts commit 90f97d95f3.

* Reapply "[AI] Tighten VRT per-pixel threshold so faint overlays fail"

This reverts commit 98d0f8b3e7.

* Reapply "[AI] Apply VRT threshold to electron config and drop redundant local override"

This reverts commit 23f94362ec.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7864

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-17 17:26:05 +00:00
Matt Fiddaman
28f7770913 deprecate rule action templating in favour of formulae (#7865)
* add deprecation warning

* note
2026-05-17 17:14:28 +00:00
Matiss Janis Aboltins
068185751c [AI] Prevent CSV formula injection in exports and CLI output (#7859)
* [AI] Neutralize CSV formula-injection in CLI output and transaction export

Prefix cells starting with =, +, -, @, tab, or CR with a single quote so
that user-controlled strings (payee/account/category/tag names, notes,
etc.) cannot evaluate as formulas when the CSV is opened in Excel,
LibreOffice Calc, or Google Sheets. Numeric values are left unprefixed.

- packages/cli/src/output.ts: harden escapeCsv used by --format csv
- packages/loot-core/src/server/transactions/export/export-to-csv.ts:
  pass a cast.string option to csv-stringify

* [AI] Simplify CSV formula-injection guard

- Drop the FormattedCell type; check typeof value === 'number' inline in
  the CSV path instead of threading an isNumeric flag through formatCellValue
- Shorten verbose comments and drop standards-body references

* [AI] Remove @ts-strict-ignore from export-to-csv test

* [AI] Add release notes for CSV formula-injection fix

* [AI] Quote CSV cells containing carriage returns

Per RFC 4180, bare \r inside an unquoted field can be interpreted as
a record terminator. Extend escapeCsv to also quote on \r so neutralized
formula payloads (e.g. "'\rHELLO") are emitted as a single cell.

* [autofix.ci] apply automated fixes

* [AI] Rephrase release note in user-facing language

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-16 21:05:40 +00:00
Matiss Janis Aboltins
8f0265e0b0 [AI] Convert built-in themes from TypeScript to CSS files (#7851)
* [AI] Convert theme color TS modules to CSS files with CSS variables

Replace `palette.ts` and `themes/{light,dark,midnight}.ts` with plain CSS
files that declare the same variables directly via `:root` blocks. The
palette becomes `--palette-*` variables; each theme defines `--color-*`
variables referencing those (or other `--color-*` for intra-theme aliases).

`theme.tsx` and Storybook's `preview.tsx` now import each CSS file as a
string via Vite's `?inline` query and render two `<style>` tags (palette +
active theme) instead of building the variable declarations at runtime
from a TS object.

Auto/dark-preference selection, `prefers-color-scheme` listening, the
custom-theme baseTheme override, and the `CustomThemeStyle` override layer
are all preserved.

* [AI] Move theme CSS files to component-library

Move palette.css and themes/{light,dark,midnight}.css from desktop-client's
style/ directory into component-library at src/themes/, and add explicit
package.json exports for each. desktop-client's theme.tsx now imports them
via `@actual-app/components/themes/*.css?inline`, and Storybook's
preview.tsx uses a normal relative path inside its own package instead of
reaching across the monorepo boundary.

* [AI] Minimize theme.tsx rename churn

Keep the original variable names — `themes[*].colors`, `themeColors` state,
`lightColors` / `darkColors` locals, and `getBaseThemeColors` — even though
they now hold CSS strings rather than color objects. Limits the diff to the
import changes, the state type, and the render path.

* [AI] Add release note for theme CSS conversion

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 21:05:02 +00:00
Matiss Janis Aboltins
3494f78c94 [AI] Restrict secrets API access to admins in OpenID mode (#7862)
* [AI] Require admin for GET /secret/:name in OpenID mode

The GET handler only verified an authenticated session, while the
sibling POST handler enforced an admin gate when the active auth
method is openid. A non-admin BASIC user in an OpenID multi-user
deployment could enumerate which admin-managed bank-sync secrets
were configured by probing 204 vs 404 responses.

Factor the auth-method + admin check into a shared helper used by
both POST and GET, and restrict the GET :name parameter to the
known SecretName enum so unrelated probing returns 404 up front.

* [AI] Simplify secrets auth guard after review

- Use existing getActiveLoginMethod() helper instead of duplicating
  the SELECT inline; drop the redundant try/catch.
- Validate the secret name against the SecretName enum before doing
  the auth-method DB query so bogus probes fail fast.
- Switch the enum membership check from hasOwnProperty.call to the
  more idiomatic in operator.
- Tighten the function-header comment to a single WHY line.
- Drop the dead else-branch from the test helper.

* [AI] Move response building back into the secrets handlers

Reshape the helper as canManageSecrets(userId) - a pure predicate over
the user. Each handler now owns its own 403 response so request/response
plumbing stays inside the route handlers.

* [AI] Undo testSecretName -> validSecretName rename

Reuse the original testSecretName / testSecretValue constants; only
the value of testSecretName changes to a real SecretName so the GET
handler's enum check accepts it.

* [AI] Add release note for #7862

* [AI] Validate POST secret name and tighten GET auth ordering

- POST /secret/ now rejects names not in the SecretName enum with 400.
- GET /secret/:name runs the admin check before the enum check so
  non-admins in OpenID mode get a uniform 403 regardless of whether
  the requested name is valid.
- Add tests for both: admin POST success in OpenID mode, and POST 400
  for unknown secret names.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 20:59:31 +00:00
Matiss Janis Aboltins
46c350613c [AI] Upgrade vite-plugin-node-polyfills to 0.27.0 (#7860)
* [AI] Upgrade vite-plugin-node-polyfills to 0.27.0

Bumps `vite-plugin-node-polyfills` from `^0.26.0` to `^0.27.0` in
`packages/loot-core`.

Changelog: https://github.com/davidmyersdev/vite-plugin-node-polyfills/releases/tag/v0.27.0
Related upstream issue (already referenced from `vite.config.mts`):
https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/142

* [AI] Add release note for vite-plugin-node-polyfills upgrade

* [AI] Remove stale upstream issue reference from vite config

The link pointed at davidmyersdev/vite-plugin-node-polyfills#142, which
no longer needs a workaround note alongside the `nodePolyfills` call.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 15:03:52 +00:00
Matt Fiddaman
9b19cd2616 automation UI: various tweaks and fixes (#7832)
* warning when only a balance cap is used

* add tooltip for short descriptions

* fix parsing issue with template 0 up to templates

* preselect value to make deletion easier

* note

* fix balance cap note

* fix tests

* remove recurring

* Goal/Automation wording
2026-05-15 22:13:30 +00:00
Matiss Janis Aboltins
1f101077d6 [AI] zizmor: add environment references to secret-consuming workflows (#7856)
* [AI] Reference dedicated environments for workflows using secrets

Assigns each secret-consuming workflow to a dedicated GitHub
environment so zizmor's secrets-without-environment audit passes:

- ai-generated-release-notes.yml -> ai-release-notes
- docs-spelling.yml (update job) -> docs-spelling
- i18n-string-extract-master.yml -> i18n
- release-notes.yml -> pr-automation
- vrt-update-apply.yml -> pr-automation

* [AI] Rename release notes file to match PR #7856

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 21:45:12 +00:00
Matiss Janis Aboltins
62d7c0e479 [AI] zizmor: fix template injection in setup action's Lage cache step (#7858)
* [AI] Fix template injection in setup action's Lage cache step

The 'Ensure Lage cache directory exists' step expanded
${{ inputs.working-directory }} directly into the shell command via
format(), which zizmor flags as a code-injection risk. Pass the input
through an env var and reference it with shell expansion instead.

* [AI] Add release note for template injection fix

* [AI] Rename release note to match PR #7858

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 21:34:22 +00:00
dependabot[bot]
740392941d Bump qs from 6.13.0 to 6.15.1 (#7857)
Bumps [qs](https://github.com/ljharb/qs) from 6.13.0 to 6.15.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.15.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 20:53:31 +00:00
dependabot[bot]
d4528e18ea Bump lodash-es from 4.17.21 to 4.18.1 (#7854)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 20:23:31 +00:00
Matiss Janis Aboltins
3d47eae87b [AI] Replace GitHub Actions with native gh CLI commands (#7852)
* [AI] Replace superfluous actions flagged by zizmor

Address zizmor's `superfluous-actions` audit by replacing actions whose
functionality is already provided by the runner's pre-installed `gh` CLI:

- `actions-ecosystem/action-add-labels` -> `gh issue edit --add-label`
- `peter-evans/create-or-update-comment` -> `gh issue comment`
- `softprops/action-gh-release` -> `gh release create` / `gh release upload`

For the Electron release workflow, the create step is race-safe across
the three matrix OS jobs that share the same draft release.

* [AI] Simplify electron release upload script

- Drop the `gh release view` existence check; `gh release create ... || true`
  already handles the matrix-job race against the same draft release.
- Use `extglob` to exclude `Actual-windows.exe` inline instead of looping
  over `.exe` separately.

* Add release notes for PR #7852

* [AI] Narrow error suppression on gh release create

Only swallow the "already_exists" error from the parallel-matrix race;
propagate any other failure (auth, network, API) instead of masking it.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 19:51:40 +00:00
Sebastián Maluk
90a1e9bdd3 [AI] Color balance forecast line by zero crossing (#7850)
* [AI] Color forecast line by zero crossing

* [AI] Add release note for forecast line coloring
2026-05-15 17:50:40 +00:00
Matiss Janis Aboltins
329a7e81e7 [AI] Keep the mobile page header mounted across navigation to avoid flashing (#7842)
* [AI] Keep the mobile page header mounted across navigation to avoid flashing

On mobile, navigating between pages unmounted and remounted the page header
(including its background), causing a visible flash while the next page
rendered. Mobile pages now publish their header content through a context to a
single persistent `MobilePageHeaderSlot` rendered by the app shell, so only the
header content swaps while its background stays put.

* [AI] Render the persistent mobile page header via a portal

Replaces the context/state + useLayoutEffect plumbing for the persistent
mobile header with a portal into the `MobilePageHeaderSlot` DOM node, so
header content reconciles in place without re-rendering the provider on every
page render.

* Add release notes for PR #7842

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 16:13:14 +00:00
Sebastián Maluk
e8d95fdf6b [AI] feat: add an experimental balance forecast report (#7310)
* [AI] feat: add balance forecast backend

* [AI] feat: add balance forecast report UI

* [AI] feat: gate balance forecast behind an experimental flag

* [AI] Include account-less schedules in balance forecast via explicit flag

- Add includeAccountlessSchedules to forecast/generate and normalize
  schedules without an account into FORECAST_UNASSIGNED_ACCOUNT_ID
- When enabled, append synthetic bucket and rule stub; skip transfer legs
  for unassigned schedules
- Balance forecast UI sets the flag when widget meta has no account filter
- Add loot-core tests for include vs exclude behavior

* [AI] Improve balance forecast chart refresh UX

Keep forecast charts stable during refetches and let the Y-axis scale to forecast data so balance changes remain visible.

* [AI] Document balance forecast report

Add experimental user documentation and navigation links for the new balance forecast report.

* [AI] Link balance forecast experimental flag to feedback issue #7669

* docs: add PR release notes

* [AI] chore: rerun CI
2026-05-15 02:07:32 +00:00
Maksim Zhukau
2e0342574f [AI] fix: match no transactions when "has tags" input lacks # (closes #7797) (#7808)
* [AI] fix: match no transactions when "has tags" input lacks `#` (closes #7797)

The SQL-side `hasTags` filter extracts `#tag` patterns from the user
input and `$and`s them together. When the input has no `#` (e.g. user
types `foo` instead of `#foo`), the extraction returns an empty array
and the resulting `$and: []` matches every transaction.

Mirror the empty-`oneOf` behaviour and return the match-nothing
sentinel (`{ id: null }`) in that case.

* [AI] Add release notes entry for #7808

---------

Co-authored-by: MaksZhukov <maks_zhukov_97@users.noreply.github.com>
2026-05-14 23:36:14 +00:00
230 changed files with 9592 additions and 2501 deletions

View File

@@ -11,6 +11,7 @@ ANZ
aql
AUR
Authentik
autogen
AVERAGEA
BANKA
BANKINTER

View File

@@ -1,6 +1,17 @@
name: Generate release notes
description: Generate release documentation from release note files
inputs:
release-branch:
description: 'The release/X.Y.Z branch to read release notes from'
required: true
notes-branch:
description: 'The release-notes/X.Y.Z branch to write generated docs to'
required: true
version:
description: 'The release version (e.g. 26.5.0)'
required: true
runs:
using: composite
steps:
@@ -14,4 +25,7 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ inputs.release-branch }}
NOTES_BRANCH: ${{ inputs.notes-branch }}
VERSION: ${{ inputs.version }}
run: node packages/ci-actions/bin/release-notes-generate.mjs

View File

@@ -39,8 +39,10 @@ runs:
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
- name: Ensure Lage cache directory exists
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
run: mkdir -p "$WORKING_DIRECTORY/.lage"
shell: bash
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
- name: Cache Lage
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
if: ${{ inputs.cache == 'true' }}

View File

@@ -9,6 +9,7 @@ jobs:
# Only run on PR comments from CodeRabbit bot
if: github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]'
runs-on: ubuntu-latest
environment: ai-release-notes
timeout-minutes: 10
permissions:
contents: write

View File

@@ -6,10 +6,6 @@ on:
- cron: '0 17 25 * *'
workflow_dispatch:
inputs:
ref:
description: 'Commit or branch to release'
required: true
default: 'master'
version:
description: 'Version number for the release (optional)'
required: false
@@ -31,8 +27,9 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.ref || 'master' }}
persist-credentials: false
ref: master
fetch-depth: 0
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
- name: Set up environment
uses: ./.github/actions/setup
@@ -41,41 +38,25 @@ jobs:
cache: 'false'
download-translations: 'false'
- name: Bump package versions
id: bump_package_versions
- name: Determine target version
id: version
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
declare -A packages=(
[web]="desktop-client"
[electron]="desktop-electron"
[sync]="sync-server"
[api]="api"
[cli]="cli"
[core]="loot-core"
)
declare -A new_versions
args=(--package-json ./packages/desktop-client/package.json --type auto)
if [[ -n "$INPUT_VERSION" ]]; then
args+=(--version "$INPUT_VERSION")
fi
yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts "${args[@]}"
for key in "${!packages[@]}"; do
pkg="${packages[$key]}"
if [[ -n "$INPUT_VERSION" ]]; then
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--version "$INPUT_VERSION" \
--update)
else
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--type auto \
--update)
fi
new_versions[$key]="$version"
done
echo "version=${new_versions[web]}" >> "$GITHUB_OUTPUT"
- name: Determine previous tag
id: prev_tag
env:
GH_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN || github.token }}
run: |
prev_tag=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null) || prev_tag=""
echo "tag=${prev_tag:-master}" >> "$GITHUB_OUTPUT"
- name: Compute release date
id: release_date
@@ -90,15 +71,72 @@ jobs:
echo "date=$(date -d '+1 month' '+%Y-%m-01')" >> "$GITHUB_OUTPUT"
fi
- name: Create release branch and PR
- name: Validate target version
env:
GH_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
TYPE: ${{ steps.version.outputs.type }}
shell: bash
run: |
if gh api "repos/${GITHUB_REPOSITORY}/branches/release" --silent >/dev/null 2>&1; then
current_version=$(gh api "repos/${GITHUB_REPOSITORY}/contents/packages/desktop-client/package.json?ref=release" --jq '.content' | base64 -d | jq -r .version)
latest=$(printf '%s\n%s\n' "$VERSION" "$current_version" | sort -V | tail -1)
if [[ "$latest" != "$VERSION" || "$current_version" == "$VERSION" ]]; then
echo "::error::Target version $VERSION is not newer than current release version $current_version"
exit 1
fi
fi
echo "Type: $TYPE (target=$VERSION, current=${current_version:-none})"
- name: Apply version bumps
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
for pkg in desktop-client desktop-electron sync-server api cli loot-core; do
yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--version "$VERSION" \
--update
done
- name: Open release-notes branch and PR
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
commit-message: '🔖 (${{ steps.version.outputs.version }})'
title: '🔖 (${{ steps.version.outputs.version }})'
body: |
Generated by [cut-release-branch.yml](../tree/master/.github/workflows/cut-release-branch.yml)
Release branch: [`release`](../compare/${{ steps.prev_tag.outputs.tag }}...release)
<!-- release-date:${{ steps.release_date.outputs.date }} -->
branch: 'release/${{ steps.bump_package_versions.outputs.version }}'
branch: 'release-notes/${{ steps.version.outputs.version }}'
base: master
- name: Update release branch
env:
GH_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
TYPE: ${{ steps.version.outputs.type }}
shell: bash
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
if gh api "repos/${GITHUB_REPOSITORY}/branches/release" --silent >/dev/null 2>&1; then
git fetch origin release
git checkout release
if [[ "$TYPE" == "monthly" ]]; then
git fetch origin master
git merge --no-edit -X theirs origin/master
git diff --exit-code origin/master
fi
fi
git fetch origin "release-notes/$VERSION"
git cherry-pick FETCH_HEAD
git push origin HEAD:refs/heads/release

View File

@@ -146,6 +146,7 @@ jobs:
pull-requests: write
actions: read
runs-on: ubuntu-latest
environment: docs-spelling
if: ${{
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&

View File

@@ -100,10 +100,11 @@ jobs:
path: |
packages/desktop-electron/dist/*.appx
- name: Add to new release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
draft: true
body: |
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
RELEASE_NOTES: |
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
## Desktop releases
@@ -114,13 +115,27 @@ jobs:
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="12" height="1" alt="" />
<a href="https://flathub.org/apps/com.actualbudget.actual"><img width="165" style="margin-left:12px;" alt="Get it on Flathub" src="https://flathub.org/api/badge?locale=en" /></a>
</p>
files: |
run: |
# The matrix runs three OS jobs in parallel against one release;
# only ignore the "already exists" error that the race losers hit.
if ! create_output=$(gh release create "$TAG" --draft --title "$TAG" --notes "$RELEASE_NOTES" 2>&1); then
if [[ "$create_output" != *already_exists* ]]; then
echo "$create_output" >&2
exit 1
fi
fi
shopt -s extglob nullglob
files=(
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/!(Actual-windows).exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
packages/desktop-electron/dist/*.appx
)
if [ ${#files[@]} -gt 0 ]; then
gh release upload "$TAG" --clobber "${files[@]}"
fi
outputs:
version: ${{ steps.process_version.outputs.version }}

View File

@@ -12,6 +12,7 @@ permissions:
jobs:
extract-and-upload-i18n-strings:
runs-on: ubuntu-latest
environment: i18n
if: github.repository == 'actualbudget/actual'
steps:
- name: Check out main repository

View File

@@ -11,21 +11,21 @@ jobs:
needs-votes:
if: ${{ github.event.label.name == 'feature' }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
steps:
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
with:
labels: needs votes
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Add needs votes label
run: gh issue edit "$ISSUE_NUMBER" --add-label "needs votes"
- name: Add reactions
uses: aidan-mundy/react-to-issue@109392cac5159c2df6c47c8ab3b5d6b708852fe5 # v1.1.2
with:
issue-number: ${{ github.event.issue.number }}
reactions: '+1'
- name: Create comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ github.event.issue.number }}
body: |
env:
COMMENT_BODY: |
:sparkles: Thanks for sharing your idea! :sparkles:
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
@@ -35,7 +35,6 @@ jobs:
Don't forget to upvote the top comment with 👍!
<!-- feature-auto-close-comment -->
run: gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY"
- name: Close Issue
run: gh issue close "https://github.com/actualbudget/actual/issues/${{ github.event.issue.number }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh issue close "$ISSUE_NUMBER"

View File

@@ -2,6 +2,9 @@ name: Release notes
on:
pull_request:
push:
branches:
- release
permissions:
contents: write
@@ -12,8 +15,13 @@ concurrency:
cancel-in-progress: true
jobs:
release-notes:
check-release-notes:
if: >-
github.event_name == 'pull_request'
&& github.head_ref != 'release'
&& startsWith(github.head_ref, 'release-notes/') == false
runs-on: ubuntu-latest
environment: pr-automation
steps:
- name: Check if triggered by bot
id: bot-check
@@ -36,9 +44,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_UPDATE_TOKEN || github.token }}
# Need to be able to commit release notes after generation
persist-credentials: true
persist-credentials: false
- name: Get changed files
if: steps.bot-check.outputs.skip != 'true'
@@ -58,13 +64,34 @@ jobs:
- name: Check release notes
if: >-
steps.bot-check.outputs.skip != 'true'
&& startsWith(github.head_ref, 'release/') == false
&& steps.changed-files.outputs.only_docs != 'true'
uses: ./.github/actions/release-notes/check
generate-release-notes:
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: release
steps:
- name: Resolve version
id: version
env:
GH_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN || github.token }}
run: |
version=$(gh api "repos/${GITHUB_REPOSITORY}/contents/packages/desktop-client/package.json?ref=release" --jq '.content' | base64 -d | jq -r .version)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "notes_branch=release-notes/$version" >> "$GITHUB_OUTPUT"
- name: Checkout release-notes branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.version.outputs.notes_branch }}
fetch-depth: 0
token: ${{ secrets.ACTIONS_UPDATE_TOKEN || github.token }}
persist-credentials: true
- name: Generate release notes
if: >-
steps.bot-check.outputs.skip != 'true'
&& startsWith(github.head_ref, 'release/') == true
&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
uses: ./.github/actions/release-notes/generate
with:
release-branch: release
notes-branch: ${{ steps.version.outputs.notes_branch }}
version: ${{ steps.version.outputs.version }}

View File

@@ -16,6 +16,7 @@ jobs:
apply-vrt-updates:
name: Apply VRT Updates
runs-on: ubuntu-latest
environment: pr-automation
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download patch artifact

View File

@@ -80,6 +80,18 @@ try {
process.stdout.write(newVersion);
if (process.env.GITHUB_OUTPUT) {
const resolvedType = newVersion.includes('-nightly.')
? 'nightly'
: newVersion.split('.')[2] === '0'
? 'monthly'
: 'hotfix';
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`version=${newVersion}\ntype=${resolvedType}\n`,
);
}
if (values.update) {
packageJson.version = newVersion;
fs.writeFileSync(

View File

@@ -15,6 +15,16 @@ const exec = promisify(childProcess.exec);
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
const releaseBranch = process.env.RELEASE_BRANCH;
const notesBranch = process.env.NOTES_BRANCH;
const version = process.env.VERSION;
if (!releaseBranch || !notesBranch || !version) {
throw new Error(
'RELEASE_BRANCH, NOTES_BRANCH, and VERSION env vars are required',
);
}
const apiResult = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
@@ -44,24 +54,38 @@ const apiResult = await fetch('https://api.github.com/graphql', {
variables: {
name: repo,
owner,
headRefName: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
headRefName: notesBranch,
},
}),
}).then(res => res.json());
await collapsedLog('API Response', apiResult);
const prData = apiResult.data.repository.pullRequests.edges[0].node;
const version = prData.headRefName.split('/')[1].replace(/^v/, '');
const slug = version.replace(/\./g, '-');
const author = process.env.GITHUB_ACTOR || 'TODO';
const commitMessage = `Generate release notes for v${version}`;
const prData = apiResult.data.repository.pullRequests.edges[0]?.node;
if (!prData) {
console.error(`No PR found for branch ${notesBranch}`);
process.exit(1);
}
const releaseDateMatch = (prData.body || '').match(
/<!-- release-date:(\d{4}-\d{2}-\d{2}) -->/,
);
const releaseDate = releaseDateMatch ? releaseDateMatch[1] : 'TODO';
if (!releaseDateMatch) {
console.error(
`PR for ${notesBranch} body missing <!-- release-date:YYYY-MM-DD --> marker`,
);
process.exit(1);
}
const releaseDate = releaseDateMatch[1];
const author = process.env.GITHUB_ACTOR;
if (!author) {
console.error('::error::GITHUB_ACTOR env var is not set');
process.exit(1);
}
const slug = version.replace(/\./g, '-');
const commitMessage = `Generate release notes for v${version}`;
const botName = 'github-actions[bot]';
const botEmail = '41898282+github-actions[bot]@users.noreply.github.com';
@@ -72,17 +96,8 @@ await exec(`git config user.email '${botEmail}'`);
const AUTOGEN_MARKER = '<!-- release-notes:auto-generated -->';
await group('Prepare branch', async () => {
if (process.env.GITHUB_HEAD_REF) {
await exec(`git fetch origin ${process.env.GITHUB_HEAD_REF}`, {
stdio: 'inherit',
});
await exec(`git checkout ${process.env.GITHUB_HEAD_REF}`, {
stdio: 'inherit',
});
}
// recover deleted release note files from previous generation commits
const baseRef = process.env.GITHUB_BASE_REF || 'master';
const baseRef = 'master';
await exec(`git fetch origin ${baseRef}`, { stdio: 'inherit' });
const { stdout: mergeBase } = await exec(
`git merge-base HEAD origin/${baseRef}`,
@@ -110,6 +125,11 @@ await group('Prepare branch', async () => {
await fs.unlink(patchPath).catch(() => undefined);
}
}
await exec(`git fetch origin ${releaseBranch}`, { stdio: 'inherit' });
await exec(`git checkout origin/${releaseBranch} -- upcoming-release-notes`, {
stdio: 'inherit',
});
});
const { notesByCategory, files } = await parseReleaseNotes(

View File

@@ -154,6 +154,50 @@ describe('formatOutput', () => {
expect(result).toContain('166500');
expect(result).not.toContain('1665.00');
});
describe('formula-injection neutralization', () => {
it.each([['=1+1'], ['+1+1'], ['-2+3'], ['@SUM(1+1)'], ['\tHELLO']])(
'prefixes a leading %j with a single quote',
payload => {
const data = [{ val: payload }];
const result = formatOutput(data, 'csv');
expect(result).toBe(`val\n'${payload}`);
},
);
it('prefixes and quotes a leading carriage return', () => {
const data = [{ val: '\rHELLO' }];
const result = formatOutput(data, 'csv');
expect(result).toBe('val\n"\'\rHELLO"');
});
it('quotes values containing a carriage return mid-string', () => {
const data = [{ val: 'line1\rline2' }];
const result = formatOutput(data, 'csv');
expect(result).toBe('val\n"line1\rline2"');
});
it('neutralizes formula triggers even when the value also needs quoting', () => {
const data = [{ val: '=HYPERLINK("http://attacker/?d="&B2,"x")' }];
const result = formatOutput(data, 'csv');
const lines = result.split('\n');
expect(lines[1]).toBe(
'"\'=HYPERLINK(""http://attacker/?d=""&B2,""x"")"',
);
});
it('does not neutralize trigger characters that appear mid-string', () => {
const data = [{ val: 'a+b' }];
const result = formatOutput(data, 'csv');
expect(result).toBe('val\na+b');
});
it('does not prefix negative amount values', () => {
const data = [{ amount: -2500 }];
const result = formatOutput(data, 'csv');
expect(result).toBe('amount\n-25.00');
});
});
});
});

View File

@@ -73,9 +73,7 @@ function formatCsv(data: unknown): string {
if (data && typeof data === 'object') {
const entries = Object.entries(data);
const header = entries.map(([k]) => escapeCsv(k)).join(',');
const values = entries
.map(([k, v]) => escapeCsv(formatCellValue(k, v)))
.join(',');
const values = entries.map(([k, v]) => formatCsvCell(k, v)).join(',');
return header + '\n' + values;
}
return String(data);
@@ -89,14 +87,31 @@ function formatCsv(data: unknown): string {
const header = keys.map(k => escapeCsv(k)).join(',');
const rows = data.map(row => {
const r = row as Record<string, unknown>;
return keys.map(k => escapeCsv(formatCellValue(k, r[k]))).join(',');
return keys.map(k => formatCsvCell(k, r[k])).join(',');
});
return [header, ...rows].join('\n');
}
const FORMULA_TRIGGERS = /^[=+\-@\t\r]/;
function formatCsvCell(key: string, value: unknown): string {
let formatted = formatCellValue(key, value);
// Skip neutralization for numeric values so legitimate negative amounts
// like "-25.00" aren't quoted as text.
if (typeof value !== 'number' && FORMULA_TRIGGERS.test(formatted)) {
formatted = "'" + formatted;
}
return escapeCsv(formatted);
}
function escapeCsv(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
if (
value.includes(',') ||
value.includes('"') ||
value.includes('\n') ||
value.includes('\r')
) {
return '"' + value.replace(/"/g, '""') + '"';
}
return value;

View File

@@ -2,19 +2,15 @@ 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
// oxlint-disable-next-line actual/enforce-boundaries
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
// oxlint-disable-next-line actual/enforce-boundaries
import * as lightTheme from '../../desktop-client/src/style/themes/light';
// oxlint-disable-next-line actual/enforce-boundaries
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
import darkThemeCss from '../src/themes/dark.css?inline';
import lightThemeCss from '../src/themes/light.css?inline';
import midnightThemeCss from '../src/themes/midnight.css?inline';
import paletteCss from '../src/themes/palette.css?inline';
const THEMES = {
light: lightTheme,
dark: darkTheme,
midnight: midnightTheme,
light: lightThemeCss,
dark: darkThemeCss,
midnight: midnightThemeCss,
} as const;
type ThemeName = keyof typeof THEMES;
@@ -30,13 +26,10 @@ const ThemedStory = ({
throw new Error(`No theme specified`);
}
const css = Object.entries(THEMES[themeName])
.map(([key, value]) => `--color-${key}: ${value};`)
.join('\n');
return (
<div>
<style>{`:root {\n${css}}`}</style>
<style>{paletteCss}</style>
<style>{THEMES[themeName]}</style>
{children}
</div>
);

View File

@@ -31,6 +31,10 @@
"./text": "./src/Text.tsx",
"./text-one-line": "./src/TextOneLine.tsx",
"./theme": "./src/theme.ts",
"./themes/palette.css": "./src/themes/palette.css",
"./themes/light.css": "./src/themes/light.css",
"./themes/dark.css": "./src/themes/dark.css",
"./themes/midnight.css": "./src/themes/midnight.css",
"./tokens": "./src/tokens.ts",
"./toggle": "./src/Toggle.tsx",
"./tooltip": "./src/Tooltip.tsx",

View File

@@ -0,0 +1,250 @@
:root {
--color-pageBackground: var(--palette-gray900);
--color-pageBackgroundModalActive: var(--palette-gray800);
--color-pageBackgroundTopLeft: var(--palette-navy800);
--color-pageBackgroundBottomRight: var(--palette-gray700);
--color-pageBackgroundLineTop: var(--palette-purple400);
--color-pageBackgroundLineMid: var(--palette-navy900);
--color-pageBackgroundLineBottom: var(--palette-navy150);
--color-pageText: var(--palette-navy150);
--color-pageTextLight: var(--palette-navy300);
--color-pageTextSubdued: var(--palette-navy500);
--color-pageTextDark: var(--palette-navy100);
--color-pageTextPositive: var(--palette-purple200);
--color-pageTextLink: var(--palette-purple400);
--color-pageTextLinkLight: var(--palette-purple200);
--color-cardBackground: var(--palette-gray800);
--color-cardBorder: var(--palette-purple400);
--color-cardShadow: var(--palette-navy700);
--color-tableBackground: var(--palette-navy800);
--color-tableRowBackgroundHover: var(--palette-navy700);
--color-tableText: var(--palette-navy150);
--color-tableTextLight: var(--color-tableText);
--color-tableTextSubdued: var(--palette-navy500);
--color-tableTextSelected: var(--palette-navy150);
--color-tableTextHover: var(--palette-navy400);
--color-tableTextInactive: var(--palette-navy500);
--color-tableHeaderText: var(--palette-navy300);
--color-tableHeaderBackground: var(--palette-navy700);
--color-tableBorder: var(--palette-navy600);
--color-tableBorderSelected: var(--palette-purple400);
--color-tableBorderHover: var(--palette-purple300);
--color-tableBorderSeparator: var(--palette-navy400);
--color-tableRowBackgroundHighlight: var(--palette-purple800);
--color-tableRowBackgroundHighlightText: var(--palette-navy150);
--color-tableRowHeaderBackground: var(--palette-navy700);
--color-tableRowHeaderText: var(--palette-navy150);
--color-numberPositive: var(--palette-green300);
--color-numberNegative: var(--palette-red200);
--color-numberNeutral: var(--palette-navy500);
--color-budgetNumberNegative: var(--color-numberNegative);
--color-budgetNumberZero: var(--color-tableTextSubdued);
--color-budgetNumberNeutral: var(--color-tableText);
--color-budgetNumberPositive: var(--color-budgetNumberNeutral);
--color-templateNumberFunded: var(--color-numberPositive);
--color-templateNumberUnderFunded: var(--palette-orange300);
--color-toBudgetPositive: var(--color-numberPositive);
--color-toBudgetZero: var(--color-numberPositive);
--color-toBudgetNegative: var(--color-budgetNumberNegative);
--color-sidebarBackground: var(--palette-navy900);
--color-sidebarItemBackgroundPending: var(--palette-orange200);
--color-sidebarItemBackgroundPositive: var(--palette-green500);
--color-sidebarItemBackgroundFailed: var(--palette-red300);
--color-sidebarItemAccentSelected: var(--palette-purple200);
--color-sidebarItemBackgroundHover: var(--palette-navy700);
--color-sidebarItemText: var(--palette-navy150);
--color-sidebarItemTextSelected: var(--palette-purple200);
--color-sidebarBudgetName: var(--palette-navy300);
--color-menuBackground: var(--palette-navy800);
--color-menuItemBackground: var(--palette-navy800);
--color-menuItemBackgroundHover: var(--palette-navy500);
--color-menuItemText: var(--palette-navy100);
--color-menuItemTextHover: var(--palette-navy50);
--color-menuItemTextSelected: var(--palette-purple400);
--color-menuItemTextHeader: var(--palette-purple200);
--color-menuBorder: var(--palette-navy900);
--color-menuBorderHover: var(--palette-purple400);
--color-menuKeybindingText: var(--palette-purple200);
--color-menuAutoCompleteBackground: var(--palette-navy900);
--color-menuAutoCompleteBackgroundHover: var(--palette-navy600);
--color-menuAutoCompleteText: var(--palette-navy200);
--color-menuAutoCompleteTextHeader: var(--palette-purple200);
--color-menuAutoCompleteItemText: var(--color-menuItemText);
--color-modalBackground: var(--palette-gray800);
--color-modalBorder: var(--palette-navy600);
--color-mobileHeaderBackground: var(--palette-purple800);
--color-mobileHeaderText: var(--palette-navy150);
--color-mobileHeaderTextSubdued: var(--palette-gray200);
--color-mobileHeaderTextHover: rgba(200, 200, 200, 0.15);
--color-mobilePageBackground: var(--palette-navy700);
--color-mobileNavBackground: var(--palette-navy800);
--color-mobileNavItem: var(--palette-navy150);
--color-mobileNavItemSelected: var(--palette-purple400);
--color-mobileAccountShadow: var(--color-cardShadow);
--color-mobileAccountText: var(--palette-blue800);
--color-mobileTransactionSelected: var(--palette-purple400);
--color-mobileViewTheme: var(--color-mobileHeaderBackground);
--color-mobileConfigServerViewTheme: var(--palette-purple500);
--color-markdownNormal: var(--palette-purple700);
--color-markdownDark: var(--palette-purple500);
--color-markdownLight: var(--palette-purple800);
--color-buttonMenuText: var(--palette-navy200);
--color-buttonMenuTextHover: var(--color-buttonMenuText);
--color-buttonMenuBackground: transparent;
--color-buttonMenuBackgroundHover: rgba(200, 200, 200, 0.25);
--color-buttonMenuBorder: var(--palette-navy500);
--color-buttonMenuSelectedText: var(--palette-green800);
--color-buttonMenuSelectedTextHover: var(--palette-orange800);
--color-buttonMenuSelectedBackground: var(--palette-orange200);
--color-buttonMenuSelectedBackgroundHover: var(--palette-orange300);
--color-buttonMenuSelectedBorder: var(--color-buttonMenuSelectedBackground);
--color-buttonPrimaryText: var(--palette-white);
--color-buttonPrimaryTextHover: var(--color-buttonPrimaryText);
--color-buttonPrimaryBackground: var(--palette-purple400);
--color-buttonPrimaryBackgroundHover: var(--palette-purple600);
--color-buttonPrimaryBorder: var(--color-buttonPrimaryBackground);
--color-buttonPrimaryShadow: rgba(0, 0, 0, 0.6);
--color-buttonPrimaryDisabledText: var(--palette-navy700);
--color-buttonPrimaryDisabledBackground: var(--palette-navy400);
--color-buttonPrimaryDisabledBorder: var(
--color-buttonPrimaryDisabledBackground
);
--color-buttonNormalText: var(--palette-navy150);
--color-buttonNormalTextHover: var(--palette-navy150);
--color-buttonNormalBackground: var(--palette-navy800);
--color-buttonNormalBackgroundHover: var(--palette-navy600);
--color-buttonNormalBorder: var(--palette-navy300);
--color-buttonNormalShadow: rgba(0, 0, 0, 0.4);
--color-buttonNormalSelectedText: var(--palette-white);
--color-buttonNormalSelectedBackground: var(--palette-purple600);
--color-buttonNormalDisabledText: var(--palette-navy500);
--color-buttonNormalDisabledBackground: var(--palette-navy800);
--color-buttonNormalDisabledBorder: var(--palette-navy500);
--color-calendarText: var(--palette-navy50);
--color-calendarBackground: var(--palette-navy900);
--color-calendarItemText: var(--palette-navy150);
--color-calendarItemBackground: var(--palette-navy800);
--color-calendarSelectedBackground: var(
--color-buttonNormalSelectedBackground
);
--color-buttonBareText: var(--color-buttonNormalText);
--color-buttonBareTextHover: var(--color-buttonNormalText);
--color-buttonBareBackground: transparent;
--color-buttonBareBackgroundHover: rgba(200, 200, 200, 0.3);
--color-buttonBareBackgroundActive: rgba(200, 200, 200, 0.5);
--color-buttonBareDisabledText: var(--color-buttonNormalDisabledText);
--color-buttonBareDisabledBackground: var(--color-buttonBareBackground);
--color-noticeBackground: var(--palette-green800);
--color-noticeBackgroundLight: var(--palette-green900);
--color-noticeBackgroundDark: var(--palette-green500);
--color-noticeText: var(--palette-green300);
--color-noticeTextLight: var(--palette-green500);
--color-noticeTextDark: var(--palette-green150);
--color-noticeTextMenu: var(--palette-green500);
--color-noticeBorder: var(--palette-green800);
--color-warningBackground: var(--palette-orange800);
--color-warningText: var(--palette-orange300);
--color-warningTextLight: var(--palette-orange500);
--color-warningTextDark: var(--palette-orange100);
--color-warningBorder: var(--palette-orange500);
--color-errorBackground: var(--palette-red800);
--color-errorText: var(--palette-red200);
--color-errorTextDark: var(--palette-red150);
--color-errorTextDarker: var(--color-errorTextDark);
--color-errorTextMenu: var(--palette-red200);
--color-errorBorder: var(--palette-red500);
--color-upcomingBackground: var(--palette-purple700);
--color-upcomingText: var(--palette-purple100);
--color-upcomingBorder: var(--color-tableBorder);
--color-formLabelText: var(--palette-purple150);
--color-formLabelBackground: var(--palette-blue900);
--color-formInputBackground: var(--palette-navy800);
--color-formInputBackgroundSelected: var(--palette-navy700);
--color-formInputBackgroundSelection: var(--palette-purple400);
--color-formInputBorder: var(--palette-navy600);
--color-formInputTextReadOnlySelection: var(--palette-navy800);
--color-formInputBorderSelected: var(--palette-purple400);
--color-formInputText: var(--palette-navy150);
--color-formInputTextSelected: var(--palette-black);
--color-formInputTextPlaceholder: var(--palette-navy150);
--color-formInputTextPlaceholderSelected: var(--palette-navy100);
--color-formInputTextSelection: var(--palette-navy800);
--color-formInputShadowSelected: var(--palette-purple200);
--color-formInputTextHighlight: var(--palette-purple400);
--color-checkboxText: var(--color-tableText);
--color-checkboxBackgroundSelected: var(--palette-purple300);
--color-checkboxBorderSelected: var(--palette-purple300);
--color-checkboxShadowSelected: var(--palette-purple500);
--color-checkboxToggleBackground: var(--palette-gray700);
--color-checkboxToggleBackgroundSelected: var(--palette-purple300);
--color-checkboxToggleDisabled: var(--palette-gray400);
--color-pillBackground: var(--palette-navy800);
--color-pillBackgroundLight: var(--palette-navy900);
--color-pillText: var(--palette-navy200);
--color-pillTextHighlighted: var(--palette-purple200);
--color-pillBorder: var(--palette-navy700);
--color-pillBorderDark: var(--color-pillBorder);
--color-pillBackgroundSelected: var(--palette-purple600);
--color-pillTextSelected: var(--palette-navy150);
--color-pillBorderSelected: var(--palette-purple400);
--color-pillTextSubdued: var(--palette-navy500);
--color-reportsRed: var(--palette-red300);
--color-reportsBlue: var(--palette-blue400);
--color-reportsGreen: var(--palette-green400);
--color-reportsGray: var(--palette-gray400);
--color-reportsLabel: var(--color-pageText);
--color-reportsInnerLabel: var(--palette-navy800);
--color-reportsNumberPositive: var(--color-numberPositive);
--color-reportsNumberNegative: var(--color-numberNegative);
--color-reportsNumberNeutral: var(--color-numberNeutral);
--color-reportsChartFill: var(--color-reportsNumberPositive);
--color-noteTagBackground: var(--palette-purple700);
--color-noteTagBackgroundHover: var(--palette-purple500);
--color-noteTagDefault: var(--palette-purple700);
--color-noteTagText: var(--palette-purple100);
--color-budgetOtherMonth: var(--palette-navy900);
--color-budgetCurrentMonth: var(--color-tableBackground);
--color-budgetHeaderOtherMonth: var(--palette-navy800);
--color-budgetHeaderCurrentMonth: var(--color-tableHeaderBackground);
--color-floatingActionBarBackground: var(--palette-purple800);
--color-floatingActionBarBorder: var(--color-floatingActionBarBackground);
--color-floatingActionBarText: var(--palette-navy150);
--color-tooltipText: var(--palette-navy100);
--color-tooltipBackground: var(--palette-navy800);
--color-tooltipBorder: var(--palette-navy700);
--color-calendarCellBackground: var(--palette-navy900);
--color-overlayBackground: rgba(0, 0, 0, 0.3);
--color-chartQual1: var(--palette-chartQual1);
--color-chartQual2: var(--palette-chartQual2);
--color-chartQual3: var(--palette-chartQual3);
--color-chartQual4: var(--palette-chartQual4);
--color-chartQual5: var(--palette-chartQual5);
--color-chartQual6: var(--palette-chartQual6);
--color-chartQual7: var(--palette-chartQual7);
--color-chartQual8: var(--palette-chartQual8);
--color-chartQual9: var(--palette-chartQual9);
}

View File

@@ -0,0 +1,250 @@
:root {
--color-pageBackground: var(--palette-navy100);
--color-pageBackgroundModalActive: var(--palette-navy200);
--color-pageBackgroundTopLeft: var(--palette-navy100);
--color-pageBackgroundBottomRight: var(--palette-blue150);
--color-pageBackgroundLineTop: var(--palette-white);
--color-pageBackgroundLineMid: var(--palette-navy100);
--color-pageBackgroundLineBottom: var(--palette-blue150);
--color-pageText: #272630;
--color-pageTextLight: var(--palette-navy500);
--color-pageTextSubdued: var(--palette-navy300);
--color-pageTextDark: var(--palette-navy800);
--color-pageTextPositive: var(--palette-purple600);
--color-pageTextLink: var(--palette-blue600);
--color-pageTextLinkLight: var(--palette-blue300);
--color-cardBackground: var(--palette-white);
--color-cardBorder: var(--palette-purple700);
--color-cardShadow: var(--palette-navy700);
--color-tableBackground: var(--palette-white);
--color-tableRowBackgroundHover: var(--palette-navy50);
--color-tableText: var(--color-pageText);
--color-tableTextLight: var(--palette-navy400);
--color-tableTextSubdued: var(--palette-navy100);
--color-tableTextSelected: var(--palette-navy700);
--color-tableTextHover: var(--palette-navy900);
--color-tableTextInactive: var(--palette-navy500);
--color-tableHeaderText: var(--palette-navy600);
--color-tableHeaderBackground: var(--palette-white);
--color-tableBorder: var(--palette-navy100);
--color-tableBorderSelected: var(--palette-purple500);
--color-tableBorderHover: var(--palette-purple400);
--color-tableBorderSeparator: var(--palette-navy400);
--color-tableRowBackgroundHighlight: var(--palette-blue150);
--color-tableRowBackgroundHighlightText: var(--palette-navy700);
--color-tableRowHeaderBackground: var(--palette-navy50);
--color-tableRowHeaderText: var(--palette-navy800);
--color-numberPositive: var(--palette-green700);
--color-numberNegative: var(--palette-red500);
--color-numberNeutral: var(--palette-navy100);
--color-budgetNumberNegative: var(--color-numberNegative);
--color-budgetNumberZero: var(--color-tableTextSubdued);
--color-budgetNumberNeutral: var(--color-tableText);
--color-budgetNumberPositive: var(--color-budgetNumberNeutral);
--color-templateNumberFunded: var(--color-numberPositive);
--color-templateNumberUnderFunded: var(--palette-orange700);
--color-toBudgetPositive: var(--color-numberPositive);
--color-toBudgetZero: var(--color-numberPositive);
--color-toBudgetNegative: var(--color-budgetNumberNegative);
--color-sidebarBackground: var(--palette-navy900);
--color-sidebarItemBackgroundPending: var(--palette-orange200);
--color-sidebarItemBackgroundPositive: var(--palette-green500);
--color-sidebarItemBackgroundFailed: var(--palette-red300);
--color-sidebarItemBackgroundHover: var(--palette-navy800);
--color-sidebarItemAccentSelected: var(--palette-purple200);
--color-sidebarItemText: var(--palette-navy150);
--color-sidebarItemTextSelected: var(--palette-purple200);
--color-sidebarBudgetName: var(--palette-navy150);
--color-menuBackground: var(--palette-white);
--color-menuItemBackground: var(--palette-navy50);
--color-menuItemBackgroundHover: var(--palette-navy100);
--color-menuItemText: var(--palette-navy900);
--color-menuItemTextHover: var(--color-menuItemText);
--color-menuItemTextSelected: var(--palette-purple300);
--color-menuItemTextHeader: var(--palette-navy400);
--color-menuBorder: var(--palette-navy100);
--color-menuBorderHover: var(--palette-purple100);
--color-menuKeybindingText: var(--palette-navy400);
--color-menuAutoCompleteBackground: var(--palette-navy900);
--color-menuAutoCompleteBackgroundHover: var(--palette-navy600);
--color-menuAutoCompleteText: var(--palette-white);
--color-menuAutoCompleteTextHover: var(--palette-green150);
--color-menuAutoCompleteTextHeader: var(--palette-orange150);
--color-menuAutoCompleteItemTextHover: var(--color-menuAutoCompleteText);
--color-menuAutoCompleteItemText: var(--color-menuAutoCompleteText);
--color-modalBackground: var(--palette-white);
--color-modalBorder: var(--palette-white);
--color-mobileHeaderBackground: var(--palette-purple400);
--color-mobileHeaderText: var(--palette-navy50);
--color-mobileHeaderTextSubdued: var(--palette-gray200);
--color-mobileHeaderTextHover: rgba(200, 200, 200, 0.15);
--color-mobilePageBackground: var(--palette-navy50);
--color-mobileNavBackground: var(--palette-white);
--color-mobileNavItem: var(--palette-gray300);
--color-mobileNavItemSelected: var(--palette-purple500);
--color-mobileAccountShadow: var(--palette-navy300);
--color-mobileAccountText: var(--palette-blue800);
--color-mobileTransactionSelected: var(--palette-purple500);
--color-mobileViewTheme: var(--color-mobileHeaderBackground);
--color-mobileConfigServerViewTheme: var(--palette-purple500);
--color-markdownNormal: var(--palette-purple150);
--color-markdownDark: var(--palette-purple400);
--color-markdownLight: var(--palette-purple100);
--color-buttonMenuText: var(--palette-navy100);
--color-buttonMenuTextHover: var(--palette-navy50);
--color-buttonMenuBackground: transparent;
--color-buttonMenuBackgroundHover: rgba(200, 200, 200, 0.25);
--color-buttonMenuBorder: var(--palette-navy500);
--color-buttonMenuSelectedText: var(--palette-green800);
--color-buttonMenuSelectedTextHover: var(--palette-orange800);
--color-buttonMenuSelectedBackground: var(--palette-orange200);
--color-buttonMenuSelectedBackgroundHover: var(--palette-orange300);
--color-buttonMenuSelectedBorder: var(--color-buttonMenuSelectedBackground);
--color-buttonPrimaryText: var(--palette-white);
--color-buttonPrimaryTextHover: var(--color-buttonPrimaryText);
--color-buttonPrimaryBackground: var(--palette-purple500);
--color-buttonPrimaryBackgroundHover: var(--palette-purple300);
--color-buttonPrimaryBorder: var(--color-buttonPrimaryBackground);
--color-buttonPrimaryShadow: rgba(0, 0, 0, 0.3);
--color-buttonPrimaryDisabledText: var(--palette-white);
--color-buttonPrimaryDisabledBackground: var(--palette-navy300);
--color-buttonPrimaryDisabledBorder: var(
--color-buttonPrimaryDisabledBackground
);
--color-buttonNormalText: var(--palette-navy900);
--color-buttonNormalTextHover: var(--color-buttonNormalText);
--color-buttonNormalBackground: var(--palette-white);
--color-buttonNormalBackgroundHover: var(--color-buttonNormalBackground);
--color-buttonNormalBorder: var(--palette-navy150);
--color-buttonNormalShadow: rgba(0, 0, 0, 0.2);
--color-buttonNormalSelectedText: var(--palette-white);
--color-buttonNormalSelectedBackground: var(--palette-blue600);
--color-buttonNormalDisabledText: var(--palette-navy300);
--color-buttonNormalDisabledBackground: var(--color-buttonNormalBackground);
--color-buttonNormalDisabledBorder: var(--color-buttonNormalBorder);
--color-calendarText: var(--palette-navy50);
--color-calendarBackground: var(--palette-navy900);
--color-calendarItemText: var(--palette-navy150);
--color-calendarItemBackground: var(--palette-navy800);
--color-calendarSelectedBackground: var(--palette-navy500);
--color-buttonBareText: var(--color-buttonNormalText);
--color-buttonBareTextHover: var(--color-buttonNormalText);
--color-buttonBareBackground: transparent;
--color-buttonBareBackgroundHover: rgba(100, 100, 100, 0.15);
--color-buttonBareBackgroundActive: rgba(100, 100, 100, 0.25);
--color-buttonBareDisabledText: var(--color-buttonNormalDisabledText);
--color-buttonBareDisabledBackground: var(--color-buttonBareBackground);
--color-noticeBackground: var(--palette-green150);
--color-noticeBackgroundLight: var(--palette-green100);
--color-noticeBackgroundDark: var(--palette-green500);
--color-noticeText: var(--palette-green700);
--color-noticeTextLight: var(--palette-green500);
--color-noticeTextDark: var(--palette-green900);
--color-noticeTextMenu: var(--palette-green200);
--color-noticeBorder: var(--palette-green500);
--color-warningBackground: var(--palette-orange200);
--color-warningText: var(--palette-orange700);
--color-warningTextLight: var(--palette-orange500);
--color-warningTextDark: var(--palette-orange900);
--color-warningBorder: var(--palette-orange500);
--color-errorBackground: var(--palette-red100);
--color-errorText: var(--palette-red500);
--color-errorTextDark: var(--palette-red700);
--color-errorTextDarker: var(--palette-red900);
--color-errorTextMenu: var(--palette-red200);
--color-errorBorder: var(--palette-red500);
--color-upcomingBackground: var(--palette-purple100);
--color-upcomingText: var(--palette-purple700);
--color-upcomingBorder: var(--palette-purple500);
--color-formLabelText: var(--palette-blue600);
--color-formLabelBackground: var(--palette-blue200);
--color-formInputBackground: var(--palette-navy50);
--color-formInputBackgroundSelected: var(--palette-white);
--color-formInputBackgroundSelection: var(--palette-purple500);
--color-formInputBorder: var(--palette-navy150);
--color-formInputTextReadOnlySelection: var(--palette-navy50);
--color-formInputBorderSelected: var(--palette-purple500);
--color-formInputText: var(--palette-navy900);
--color-formInputTextSelected: var(--palette-navy50);
--color-formInputTextPlaceholder: var(--palette-navy300);
--color-formInputTextPlaceholderSelected: var(--palette-navy200);
--color-formInputTextSelection: var(--palette-navy100);
--color-formInputShadowSelected: var(--palette-purple300);
--color-formInputTextHighlight: var(--palette-purple200);
--color-checkboxText: var(--color-tableBackground);
--color-checkboxBackgroundSelected: var(--palette-blue500);
--color-checkboxBorderSelected: var(--palette-blue500);
--color-checkboxShadowSelected: var(--palette-blue300);
--color-checkboxToggleBackground: var(--palette-gray400);
--color-checkboxToggleBackgroundSelected: var(--palette-purple600);
--color-checkboxToggleDisabled: var(--palette-gray200);
--color-pillBackground: var(--palette-navy150);
--color-pillBackgroundLight: var(--palette-navy50);
--color-pillText: var(--palette-navy800);
--color-pillTextHighlighted: var(--palette-purple600);
--color-pillBorder: var(--palette-navy150);
--color-pillBorderDark: var(--palette-navy300);
--color-pillBackgroundSelected: var(--palette-blue150);
--color-pillTextSelected: var(--palette-blue900);
--color-pillBorderSelected: var(--palette-purple500);
--color-pillTextSubdued: var(--palette-navy200);
--color-reportsRed: var(--palette-red300);
--color-reportsBlue: var(--palette-blue400);
--color-reportsGreen: var(--palette-green400);
--color-reportsGray: var(--palette-gray400);
--color-reportsLabel: var(--palette-navy900);
--color-reportsInnerLabel: var(--palette-navy800);
--color-reportsNumberPositive: var(--color-numberPositive);
--color-reportsNumberNegative: var(--color-numberNegative);
--color-reportsNumberNeutral: var(--color-numberNeutral);
--color-reportsChartFill: var(--color-reportsNumberPositive);
--color-noteTagBackground: var(--palette-purple125);
--color-noteTagBackgroundHover: var(--palette-purple150);
--color-noteTagDefault: var(--palette-purple125);
--color-noteTagText: var(--palette-black);
--color-budgetCurrentMonth: var(--color-tableBackground);
--color-budgetOtherMonth: var(--palette-gray50);
--color-budgetHeaderCurrentMonth: var(--color-budgetOtherMonth);
--color-budgetHeaderOtherMonth: var(--palette-gray80);
--color-floatingActionBarBackground: var(--palette-purple400);
--color-floatingActionBarBorder: var(--color-floatingActionBarBackground);
--color-floatingActionBarText: var(--palette-navy50);
--color-tooltipText: var(--palette-navy900);
--color-tooltipBackground: var(--palette-white);
--color-tooltipBorder: var(--palette-navy150);
--color-calendarCellBackground: var(--palette-navy100);
--color-overlayBackground: rgba(0, 0, 0, 0.3);
--color-chartQual1: var(--palette-chartQual1);
--color-chartQual2: var(--palette-chartQual2);
--color-chartQual3: var(--palette-chartQual3);
--color-chartQual4: var(--palette-chartQual4);
--color-chartQual5: var(--palette-chartQual5);
--color-chartQual6: var(--palette-chartQual6);
--color-chartQual7: var(--palette-chartQual7);
--color-chartQual8: var(--palette-chartQual8);
--color-chartQual9: var(--palette-chartQual9);
}

View File

@@ -0,0 +1,252 @@
:root {
--color-pageBackground: var(--palette-gray600);
--color-pageBackgroundModalActive: var(--palette-gray700);
--color-pageBackgroundTopLeft: var(--palette-gray800);
--color-pageBackgroundBottomRight: var(--palette-gray700);
--color-pageBackgroundLineTop: var(--palette-purple300);
--color-pageBackgroundLineMid: var(--palette-gray900);
--color-pageBackgroundLineBottom: var(--palette-gray150);
--color-pageText: var(--palette-gray100);
--color-pageTextLight: var(--palette-gray200);
--color-pageTextSubdued: var(--palette-gray400);
--color-pageTextDark: var(--palette-gray100);
--color-pageTextPositive: var(--palette-purple200);
--color-pageTextLink: var(--palette-purple300);
--color-pageTextLinkLight: var(--palette-purple300);
--color-cardBackground: var(--palette-gray800);
--color-cardBorder: var(--palette-purple300);
--color-cardShadow: var(--palette-gray900);
--color-tableBackground: var(--palette-gray800);
--color-tableRowBackgroundHover: var(--palette-gray500);
--color-tableText: var(--palette-gray150);
--color-tableTextLight: var(--color-tableText);
--color-tableTextSubdued: var(--palette-gray500);
--color-tableTextSelected: var(--palette-gray800);
--color-tableTextHover: var(--palette-gray400);
--color-tableTextInactive: var(--palette-gray400);
--color-tableHeaderText: var(--palette-gray200);
--color-tableHeaderBackground: var(--palette-gray900);
--color-tableBorder: var(--palette-gray600);
--color-tableBorderSelected: var(--palette-purple400);
--color-tableBorderHover: var(--palette-purple300);
--color-tableBorderSeparator: var(--palette-gray400);
--color-tableRowBackgroundHighlight: var(--palette-purple150);
--color-tableRowBackgroundHighlightText: var(--palette-gray800);
--color-tableRowHeaderBackground: var(--palette-gray700);
--color-tableRowHeaderText: var(--palette-gray150);
--color-numberPositive: var(--palette-green300);
--color-numberNegative: var(--palette-red200);
--color-numberNeutral: var(--palette-gray500);
--color-budgetNumberNegative: var(--color-numberNegative);
--color-budgetNumberZero: var(--color-tableTextSubdued);
--color-budgetNumberNeutral: var(--color-tableText);
--color-budgetNumberPositive: var(--color-budgetNumberNeutral);
--color-templateNumberFunded: var(--color-numberPositive);
--color-templateNumberUnderFunded: var(--palette-orange200);
--color-toBudgetPositive: var(--color-numberPositive);
--color-toBudgetZero: var(--color-numberPositive);
--color-toBudgetNegative: var(--color-budgetNumberNegative);
--color-sidebarBackground: var(--palette-gray900);
--color-sidebarItemBackgroundPending: var(--palette-orange200);
--color-sidebarItemBackgroundPositive: var(--palette-green400);
--color-sidebarItemBackgroundFailed: var(--palette-red300);
--color-sidebarItemAccentSelected: var(--palette-purple200);
--color-sidebarItemBackgroundHover: var(--palette-gray700);
--color-sidebarItemText: var(--palette-gray100);
--color-sidebarItemTextSelected: var(--palette-purple200);
--color-sidebarBudgetName: var(--palette-gray300);
--color-menuBackground: var(--palette-gray700);
--color-menuItemBackground: var(--palette-gray200);
--color-menuItemBackgroundHover: var(--palette-gray500);
--color-menuItemText: var(--palette-gray100);
--color-menuItemTextHover: var(--palette-gray50);
--color-menuItemTextSelected: var(--palette-purple400);
--color-menuItemTextHeader: var(--palette-purple200);
--color-menuBorder: var(--palette-gray800);
--color-menuBorderHover: var(--palette-purple300);
--color-menuKeybindingText: var(--palette-purple200);
--color-menuAutoCompleteBackground: var(--palette-gray600);
--color-menuAutoCompleteBackgroundHover: var(--palette-gray500);
--color-menuAutoCompleteText: var(--palette-gray100);
--color-menuAutoCompleteTextHover: var(--palette-green400);
--color-menuAutoCompleteTextHeader: var(--palette-purple200);
--color-menuAutoCompleteItemTextHover: var(--palette-gray50);
--color-menuAutoCompleteItemText: var(--color-menuItemText);
--color-modalBackground: var(--palette-gray700);
--color-modalBorder: var(--palette-gray200);
--color-mobileHeaderBackground: var(--palette-gray900);
--color-mobileHeaderText: var(--palette-purple200);
--color-mobileHeaderTextSubdued: var(--palette-gray200);
--color-mobileHeaderTextHover: rgba(200, 200, 200, 0.15);
--color-mobilePageBackground: var(--palette-gray900);
--color-mobileNavBackground: var(--palette-gray600);
--color-mobileNavItem: var(--palette-gray150);
--color-mobileNavItemSelected: var(--palette-purple200);
--color-mobileAccountShadow: var(--color-cardShadow);
--color-mobileAccountText: var(--palette-blue800);
--color-mobileTransactionSelected: var(--palette-purple300);
--color-mobileViewTheme: var(--color-mobileHeaderBackground);
--color-mobileConfigServerViewTheme: var(--palette-purple500);
--color-markdownNormal: var(--palette-purple700);
--color-markdownDark: var(--palette-purple500);
--color-markdownLight: var(--palette-purple800);
--color-buttonMenuText: var(--palette-gray200);
--color-buttonMenuTextHover: var(--color-buttonMenuText);
--color-buttonMenuBackground: var(--palette-gray700);
--color-buttonMenuBackgroundHover: rgba(200, 200, 200, 0.25);
--color-buttonMenuBorder: var(--palette-gray500);
--color-buttonMenuSelectedText: var(--palette-green800);
--color-buttonMenuSelectedTextHover: var(--palette-orange800);
--color-buttonMenuSelectedBackground: var(--palette-orange200);
--color-buttonMenuSelectedBackgroundHover: var(--palette-gray300);
--color-buttonMenuSelectedBorder: var(--color-buttonMenuSelectedBackground);
--color-buttonPrimaryText: var(--palette-white);
--color-buttonPrimaryTextHover: var(--color-buttonPrimaryText);
--color-buttonPrimaryBackground: var(--palette-purple300);
--color-buttonPrimaryBackgroundHover: var(--color-buttonPrimaryBackground);
--color-buttonPrimaryBorder: var(--color-buttonPrimaryBackground);
--color-buttonPrimaryShadow: rgba(0, 0, 0, 0.6);
--color-buttonPrimaryDisabledText: var(--palette-gray400);
--color-buttonPrimaryDisabledBackground: var(--palette-gray700);
--color-buttonPrimaryDisabledBorder: var(
--color-buttonPrimaryDisabledBackground
);
--color-buttonNormalText: var(--palette-gray150);
--color-buttonNormalTextHover: var(--palette-gray150);
--color-buttonNormalBackground: var(--palette-gray600);
--color-buttonNormalBackgroundHover: var(--palette-gray400);
--color-buttonNormalBorder: var(--palette-gray300);
--color-buttonNormalShadow: rgba(0, 0, 0, 0.4);
--color-buttonNormalSelectedText: var(--palette-white);
--color-buttonNormalSelectedBackground: var(--palette-purple500);
--color-buttonNormalDisabledText: var(--palette-gray400);
--color-buttonNormalDisabledBackground: var(--palette-gray700);
--color-buttonNormalDisabledBorder: var(--palette-gray500);
--color-calendarText: var(--palette-gray50);
--color-calendarBackground: var(--palette-gray700);
--color-calendarItemText: var(--palette-gray150);
--color-calendarItemBackground: var(--palette-gray500);
--color-calendarSelectedBackground: var(
--color-buttonNormalSelectedBackground
);
--color-buttonBareText: var(--color-buttonNormalText);
--color-buttonBareTextHover: var(--color-buttonNormalText);
--color-buttonBareBackground: transparent;
--color-buttonBareBackgroundHover: rgba(200, 200, 200, 0.3);
--color-buttonBareBackgroundActive: rgba(200, 200, 200, 0.5);
--color-buttonBareDisabledText: var(--color-buttonNormalDisabledText);
--color-buttonBareDisabledBackground: var(--color-buttonBareBackground);
--color-noticeBackground: var(--palette-green600);
--color-noticeBackgroundLight: var(--palette-green900);
--color-noticeBackgroundDark: var(--palette-green400);
--color-noticeText: var(--palette-green300);
--color-noticeTextLight: var(--palette-green400);
--color-noticeTextDark: var(--palette-green150);
--color-noticeTextMenu: var(--palette-green400);
--color-noticeTextMenuHover: var(--palette-green700);
--color-noticeBorder: var(--palette-green800);
--color-warningBackground: var(--palette-orange800);
--color-warningText: var(--palette-orange200);
--color-warningTextLight: var(--palette-orange500);
--color-warningTextDark: var(--palette-orange100);
--color-warningBorder: var(--palette-orange500);
--color-errorBackground: var(--palette-red800);
--color-errorText: var(--palette-red200);
--color-errorTextDark: var(--palette-red150);
--color-errorTextDarker: var(--color-errorTextDark);
--color-errorTextMenu: var(--palette-red200);
--color-errorBorder: var(--palette-red500);
--color-upcomingBackground: var(--palette-purple800);
--color-upcomingText: var(--palette-purple200);
--color-upcomingBorder: var(--color-tableBorder);
--color-formLabelText: var(--palette-purple150);
--color-formLabelBackground: var(--palette-blue900);
--color-formInputBackground: var(--palette-gray800);
--color-formInputBackgroundSelected: var(--palette-gray700);
--color-formInputBackgroundSelection: var(--palette-purple400);
--color-formInputBorder: var(--palette-gray600);
--color-formInputTextReadOnlySelection: var(--palette-gray800);
--color-formInputBorderSelected: var(--palette-purple300);
--color-formInputText: var(--palette-gray150);
--color-formInputTextSelected: var(--palette-black);
--color-formInputTextPlaceholder: var(--palette-gray150);
--color-formInputTextPlaceholderSelected: var(--palette-gray100);
--color-formInputTextSelection: var(--palette-gray800);
--color-formInputShadowSelected: var(--palette-purple400);
--color-formInputTextHighlight: var(--palette-purple200);
--color-checkboxText: var(--color-tableText);
--color-checkboxBackgroundSelected: var(--palette-purple300);
--color-checkboxBorderSelected: var(--palette-purple300);
--color-checkboxShadowSelected: var(--palette-purple500);
--color-checkboxToggleBackground: var(--palette-gray400);
--color-checkboxToggleBackgroundSelected: var(--palette-purple300);
--color-checkboxToggleDisabled: var(--palette-gray700);
--color-pillBackground: var(--palette-gray500);
--color-pillBackgroundLight: var(--palette-gray900);
--color-pillText: var(--palette-gray200);
--color-pillTextHighlighted: var(--palette-purple200);
--color-pillBorder: var(--palette-gray500);
--color-pillBorderDark: var(--color-pillBorder);
--color-pillBackgroundSelected: var(--palette-purple600);
--color-pillTextSelected: var(--palette-gray150);
--color-pillBorderSelected: var(--palette-purple300);
--color-pillTextSubdued: var(--palette-gray500);
--color-reportsRed: var(--palette-red300);
--color-reportsBlue: var(--palette-blue400);
--color-reportsGreen: var(--palette-green400);
--color-reportsGray: var(--palette-gray400);
--color-reportsLabel: var(--color-pageText);
--color-reportsInnerLabel: var(--palette-navy800);
--color-reportsNumberPositive: var(--color-numberPositive);
--color-reportsNumberNegative: var(--color-numberNegative);
--color-reportsNumberNeutral: var(--color-numberNeutral);
--color-reportsChartFill: var(--color-reportsNumberPositive);
--color-noteTagBackground: var(--palette-purple800);
--color-noteTagBackgroundHover: var(--palette-purple600);
--color-noteTagDefault: var(--palette-purple700);
--color-noteTagText: var(--palette-purple100);
--color-budgetOtherMonth: var(--palette-gray700);
--color-budgetCurrentMonth: var(--color-tableBackground);
--color-budgetHeaderOtherMonth: var(--palette-gray800);
--color-budgetHeaderCurrentMonth: var(--color-tableHeaderBackground);
--color-floatingActionBarBackground: var(--palette-gray900);
--color-floatingActionBarBorder: var(--palette-purple300);
--color-floatingActionBarText: var(--palette-purple200);
--color-tooltipText: var(--palette-gray100);
--color-tooltipBackground: var(--palette-gray800);
--color-tooltipBorder: var(--palette-gray600);
--color-calendarCellBackground: var(--palette-navy900);
--color-overlayBackground: rgba(0, 0, 0, 0.3);
--color-chartQual1: var(--palette-chartQual1);
--color-chartQual2: var(--palette-chartQual2);
--color-chartQual3: var(--palette-chartQual3);
--color-chartQual4: var(--palette-chartQual4);
--color-chartQual5: var(--palette-chartQual5);
--color-chartQual6: var(--palette-chartQual6);
--color-chartQual7: var(--palette-chartQual7);
--color-chartQual8: var(--palette-chartQual8);
--color-chartQual9: var(--palette-chartQual9);
}

View File

@@ -0,0 +1,103 @@
:root {
--palette-gray50: #f6f8fa;
--palette-gray80: #f0f4f6;
--palette-gray100: #e8ecf0;
--palette-gray150: #d4dae0;
--palette-gray200: #bdc5cf;
--palette-gray300: #98a1ae;
--palette-gray400: #747c8b;
--palette-gray500: #4d5768;
--palette-gray600: #373b4a;
--palette-gray700: #242733;
--palette-gray800: #141520;
--palette-gray900: #080811;
--palette-navy50: #f7fafc;
--palette-navy100: #e8ecf0;
--palette-navy150: #d9e2ec;
--palette-navy200: #bcccdc;
--palette-navy300: #9fb3c8;
--palette-navy400: #829ab1;
--palette-navy500: #627d98;
--palette-navy600: #486581;
--palette-navy700: #334e68;
--palette-navy800: #243b53;
--palette-navy900: #102a43;
--palette-blue50: #f5fcff;
--palette-blue100: #e3f0ff;
--palette-blue150: #b3d9ff;
--palette-blue200: #8bcafd;
--palette-blue300: #66b5fa;
--palette-blue400: #40a5f7;
--palette-blue500: #2b8fed;
--palette-blue600: #1980d4;
--palette-blue700: #1271bf;
--palette-blue800: #0b5fa3;
--palette-blue900: #034388;
--palette-green50: #fafffd;
--palette-green100: #effcf6;
--palette-green150: #c6f7e2;
--palette-green200: #8eedc7;
--palette-green300: #65d6ad;
--palette-green400: #3ebd93;
--palette-green500: #27ab83;
--palette-green600: #199473;
--palette-green700: #147d64;
--palette-green800: #0c6b58;
--palette-green900: #014d40;
--palette-orange50: #fffefa;
--palette-orange100: #fffbea;
--palette-orange150: #fff7c4;
--palette-orange200: #fcf088;
--palette-orange300: #f5e35d;
--palette-orange400: #f2d047;
--palette-orange500: #e6bb20;
--palette-orange600: #d4a31c;
--palette-orange700: #b88115;
--palette-orange800: #87540d;
--palette-orange900: #733309;
--palette-red50: #fff1f1;
--palette-red100: #ffe3e3;
--palette-red150: #ffbdbd;
--palette-red200: #ff9b9b;
--palette-red300: #f86a6a;
--palette-red400: #ef4e4e;
--palette-red500: #e12d39;
--palette-red600: #cf1124;
--palette-red700: #ab091e;
--palette-red800: #8a041a;
--palette-red900: #610316;
--palette-purple50: #f9f6fe;
--palette-purple100: #f2ebfe;
--palette-purple125: #e4d4ff;
--palette-purple150: #dac4ff;
--palette-purple200: #b990ff;
--palette-purple300: #a368fc;
--palette-purple400: #9446ed;
--palette-purple500: #8719e0;
--palette-purple600: #7a0ecc;
--palette-purple700: #690cb0;
--palette-purple800: #580a94;
--palette-purple900: #44056e;
--palette-white: #ffffff;
--palette-black: #000000;
--palette-hover: #fafafa;
--palette-border: #e8ecf0;
--palette-selected: #b3d9ff;
--palette-chartQual1: #45b29d;
--palette-chartQual2: #efc94c;
--palette-chartQual3: #e27a3f;
--palette-chartQual4: #df5a49;
--palette-chartQual5: #5f91b8;
--palette-chartQual6: #e2a37f;
--palette-chartQual7: #55dbc1;
--palette-chartQual8: #efda97;
--palette-chartQual9: #df948a;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 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

View File

@@ -63,7 +63,6 @@ export const expect = baseExpect.extend({
const config = {
mask: [target.locator('[data-vrt-mask="true"]')],
maxDiffPixels: 5,
};
const page: Page = 'page' in target ? target.page() : target;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -245,7 +245,7 @@ export class AccountPage {
if (transaction.notes) {
const notesCell = transactionRow.getByTestId('notes');
await notesCell.click();
const notesInput = notesCell.getByRole('textbox');
const notesInput = notesCell.getByRole('combobox');
await this.selectInputText(notesInput);
await notesInput.pressSequentially(transaction.notes);
await this.page.keyboard.press('Tab');

View File

@@ -25,6 +25,75 @@ export class ReportsPage {
return new ReportsPage(this.page);
}
async goToBalanceForecastPage() {
const gridItems = this.pageContent.locator('.react-grid-item');
const count = await gridItems.count();
let targetItem: Locator | null = null;
for (let i = count - 1; i >= 0; i--) {
const item = gridItems.nth(i);
await item.scrollIntoViewIfNeeded();
const heading = item.getByRole('heading', { name: /^Balance Forecast/i });
if (await heading.isVisible()) {
targetItem = item;
break;
}
}
if (!targetItem) {
await this.page.evaluate(() => {
window.scrollTo(0, document.documentElement.scrollHeight);
});
const refreshedCount = await gridItems.count();
for (let i = refreshedCount - 1; i >= 0; i--) {
const item = gridItems.nth(i);
await item.scrollIntoViewIfNeeded();
const heading = item.getByRole('heading', {
name: /^Balance Forecast/i,
});
if (await heading.isVisible()) {
targetItem = item;
break;
}
}
}
if (!targetItem) {
throw new Error('No Balance Forecast dashboard card found in the grid');
}
const cardNavigateButton = targetItem.getByRole('button', {
name: /^Balance Forecast/i,
});
await Promise.all([
this.page.waitForURL(/\/reports\/forecast\//),
cardNavigateButton.click(),
]);
await this.pageContent
.getByRole('button', { name: 'Monthly' })
.waitFor({ state: 'visible' });
return new ReportsPage(this.page);
}
async selectForecastGranularity(granularity: string) {
await this.pageContent.getByRole('button', { name: 'Monthly' }).click();
const option = this.page.getByRole('button', { name: granularity });
await option.waitFor({ state: 'visible' });
await option.click();
await this.pageContent
.getByRole('button', { name: granularity })
.waitFor({ state: 'visible' });
}
async addWidget(widgetName: string) {
await this.pageContent
.getByRole('button', { name: 'Add new widget' })
.click();
await this.page.getByRole('button', { name: widgetName }).click();
}
async goToCustomReportPage() {
await this.pageContent
.getByRole('button', { name: 'Add new widget' })

View File

@@ -42,17 +42,22 @@ export class SettingsPage {
}
async enableExperimentalFeature(featureName: string) {
if (await this.advancedSettingsButton.isVisible()) {
await this.advancedSettingsButton.click();
}
await this.advancedSettingsButton.waitFor({
state: 'visible',
timeout: 2000,
});
await this.advancedSettingsButton.click();
if (await this.experimentalSettingsButton.isVisible()) {
await this.experimentalSettingsButton.click();
}
await this.experimentalSettingsButton.waitFor({
state: 'visible',
timeout: 2000,
});
await this.experimentalSettingsButton.click();
const featureCheckbox = this.page.getByRole('checkbox', {
name: featureName,
});
await featureCheckbox.waitFor({ state: 'visible' });
if (!(await featureCheckbox.isChecked())) {
await featureCheckbox.click();
}

View File

@@ -55,6 +55,28 @@ test.describe.parallel('Reports', () => {
await expect(page).toMatchThemeScreenshots();
});
test.describe('balance forecast', () => {
test.beforeEach(async () => {
const settingsPage = await navigation.goToSettingsPage();
await settingsPage.enableExperimentalFeature('Balance Forecast Report');
reportsPage = await navigation.goToReportsPage();
await reportsPage.waitToLoad();
await reportsPage.addWidget('Balance forecast');
await reportsPage.goToBalanceForecastPage();
});
test('loads balance forecast report with monthly granularity', async () => {
await expect(page).toMatchThemeScreenshots();
});
test('switches to daily granularity', async () => {
await reportsPage.selectForecastGranularity('Daily');
await expect(page).toMatchThemeScreenshots();
});
});
test.describe.parallel('custom reports', () => {
let customReportPage: CustomReportPage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 109 KiB

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