Compare commits

..

3 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
d796d8ec9b low res :( 2026-01-15 20:39:07 +00:00
Matiss Janis Aboltins
69009ce8ee Merge branch 'master' into matiss/browser-tests 2026-01-14 20:50:31 +00:00
Matiss Janis Aboltins
1e290373e5 Base 2026-01-14 20:49:51 +00:00
802 changed files with 4566 additions and 10679 deletions

View File

@@ -3,7 +3,7 @@ issue_enrichment:
enabled: false
reviews:
request_changes_workflow: true
review_status: false
review_status: true
high_level_summary: false
finishing_touches:
docstrings:
@@ -12,6 +12,16 @@ reviews:
docstrings:
mode: off
enabled: false
custom_checks:
- mode: error
name: 'settings'
instructions: 'Every addition of a new setting toggle must be thoroughly evaluated against the core design principles of Actual. The settings screen is reserved for essential and foundational options only — do not introduce settings for minor UI adjustments such as sizes, paddings, colors, or margins. Prioritize preserving a simple and uncluttered user experience. Users proposing new settings must confirm in a reply to the Coderabbit comment that they have reviewed and ensured alignment with these principles. Excessive or granular UI options increase code complexity and risk confusing users, and are generally not permitted.'
- mode: error
name: 'linting'
instructions: 'Do not allow any oxlint-disable lines.'
- mode: error
name: 'typecheck'
instructions: 'Do not allow creating new components or utilities with the @ts-strict-ignore comment.'
labeling_instructions:
- label: 'suspect ai generated'
instructions: 'This issue or PR is suspected to be generated by AI.'

View File

@@ -33,11 +33,11 @@ try {
{
role: 'system',
content:
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfixes", or "Maintenance". No other text or explanation.',
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfixes: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
},
],
max_tokens: 10,
@@ -86,7 +86,7 @@ try {
// Validate the category response
const validCategories = [
'Features',
'Bugfixes',
'Bugfix',
'Enhancements',
'Maintenance',
];

View File

@@ -5,7 +5,6 @@ Activo
AESUDEF
ALZEY
Anglais
ANZ
aql
AUR
Authentik
@@ -41,13 +40,13 @@ COBADEFF
CODEOWNERS
COEP
commerzbank
COOP
Copiar
COUNTA
COUNTBLANK
countif
CREGBEBB
crt
CZK
Danske
datadir
DATEDIF
@@ -69,6 +68,7 @@ Fineco
Finicity
Fintro
Finverse
flathub
Flathub
FORTUNEO
FTNOFRP
@@ -83,7 +83,6 @@ HABAL
Hampel
HELADEF
HLOOKUP
HUF
IFERROR
IFNA
INDUSTRIEL
@@ -117,14 +116,12 @@ LKR
MAXA
mbank
mdc
metainfo
modals
Moldovan
murmurhash
NETWORKDAYS
nginx
OIDC
Okabe
overbudgeted
overbudgeting
oxc
@@ -137,6 +134,7 @@ prefs
Primoco
Priotecs
proactively
pwa
Qatari
QNTOFRP
QONTO
@@ -174,7 +172,6 @@ touchscreen
triaging
UAH
ubuntu
undici
userinfo
Userscripts
UZS

View File

@@ -278,7 +278,7 @@ async function countContributorPoints() {
if (
event.event === 'closed' &&
['not_planned', 'duplicate'].includes(event.state_reason)
event.state_reason === 'not_planned'
) {
const closer = event.actor.login;
const userStats = stats.get(closer);

33
.github/workflows/docs-release.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Release Docs to Github Pages
# Release docs on every push to master
on:
push:
branches:
- master
paths:
- 'packages/docs/**'
- '.github/workflows/docs-spelling.yml'
- '.github/actions/docs-spelling/**'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy Docs
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Docusaurus Deploy
run: |
GIT_USER=MikesGlitch \
GIT_PASS=${{ secrets.DOCS_GITHUB_PAGES_DEPLOY }} \
GIT_USER_NAME=github-actions[bot] \
GIT_USER_EMAIL=github-actions[bot]@users.noreply.github.com \
yarn deploy:docs

View File

@@ -1,48 +0,0 @@
name: Fork PR Welcome
##########################################################################################
# WARNING! This workflow uses the 'pull_request_target' event. That means that it will #
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
# a fork. This is necessary to get access to a GitHub token that can post a comment on #
# the PR. Be VERY CAREFUL about adding things to this workflow, since forks can inject #
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
# code execution in this workflow could lead to a compromise of the main repo. #
##########################################################################################
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
##########################################################################################
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
welcome:
name: Post Welcome Message
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Post welcome comment
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}
header: fork-pr-welcome
hide_and_recreate: true
hide_classify: OUTDATED
message: |
<!-- fork-pr-welcome -->
👋 Hello contributor!
We would love to review your PR! Before we can do that, please make sure:
- ✅ All CI checks pass
- ✅ The PR is moved from draft to open (if applicable)
- ✅ The "[WIP]" prefix is removed from the PR title
- ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.
We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.
For more information, please see our [Contributing Guide](https://actualbudget.org/docs/contributing/).

View File

@@ -35,10 +35,7 @@ jobs:
pkg="${packages[$key]}"
if [[ -n "${{ github.event.inputs.version }}" ]]; then
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
--package-json "./packages/$pkg/package.json" \
--version "${{ github.event.inputs.version }}" \
--update)
version="${{ github.event.inputs.version }}"
else
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
--package-json "./packages/$pkg/package.json" \

View File

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

View File

@@ -1,6 +1,6 @@
name: Publish nightly npm packages
# Nightly npm packages are built daily at midnight UTC
# Nightly npm packages are built daily
on:
schedule:
- cron: '0 0 * * *'

View File

@@ -139,8 +139,7 @@ jobs:
--head desktop-client=./head/web-stats.json \
--head loot-core=./head/loot-core-stats.json \
--head api=./head/api-stats.json \
--identifier combined \
--format pr-body > bundle-stats-comment.md
--identifier combined > bundle-stats-comment.md
- name: Post combined bundle stats comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -149,5 +148,4 @@ jobs:
run: |
node packages/ci-actions/bin/update-bundle-stats-comment.mjs \
--comment-file bundle-stats-comment.md \
--identifier combined \
--target pr-body
--identifier '<!--- bundlestats-action-comment key:combined --->'

3
.gitignore vendored
View File

@@ -76,6 +76,3 @@ build/
# Lage cache
.lage/
*storybook.log
storybook-static

View File

@@ -4,31 +4,7 @@
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 80,
"experimentalSortImports": {
"groups": [
"react",
"builtin",
"external",
"loot-core",
"parent",
"sibling",
"index",
"desktop-client"
],
"customGroups": [
{
"groupName": "react",
"elementNamePattern": ["react"]
},
{
"groupName": "loot-core",
"elementNamePattern": ["loot-core"]
},
{
"groupName": "desktop-client",
"elementNamePattern": ["@desktop-client"]
}
],
"newlinesBetween": true
}
"ignorePatterns": [
"packages/docs/*" // TOOD: fixme; temporary
]
}

View File

@@ -18,11 +18,41 @@
"FS": "readonly"
},
"rules": {
// TODO fix all these and re-enable
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/tabindex-no-positive": "off",
// Import sorting
"perfectionist/sort-named-imports": [
// TODO replace once oxfmt supports this: https://github.com/oxc-project/oxc/issues/17076
"perfectionist/sort-imports": [
"warn",
{
"groups": ["value-import", "type-import"]
"groups": [
"react",
"builtin",
"external",
"loot-core",
"parent",
"sibling",
"index",
"desktop-client"
],
"customGroups": [
{
"groupName": "react",
"elementNamePattern": "^react(-.*)?$"
},
{
"groupName": "loot-core",
"elementNamePattern": "^loot-core"
},
{
"groupName": "desktop-client",
"elementNamePattern": "^@desktop-client"
}
],
"newlinesBetween": "always"
}
],
@@ -346,9 +376,55 @@
},
"overrides": [
{
"files": ["packages/desktop-electron/**/*"],
// TODO: fix the issues in these files
"files": [
"packages/component-library/src/Menu.tsx",
"packages/desktop-client/src/components/accounts/Account.jsx",
"packages/desktop-client/src/components/accounts/MobileAccount.jsx",
"packages/desktop-client/src/components/accounts/MobileAccounts.jsx",
"packages/desktop-client/src/components/budget/BudgetCategories.jsx",
"packages/desktop-client/src/components/budget/BudgetSummaries.tsx",
"packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx",
"packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx",
"packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx",
"packages/desktop-client/src/components/budget/index.tsx",
"packages/desktop-client/src/components/budget/MobileBudget.tsx",
"packages/desktop-client/src/components/FinancesApp.tsx",
"packages/desktop-client/src/components/GlobalKeys.ts",
"packages/desktop-client/src/components/LoggedInUser.tsx",
"packages/desktop-client/src/components/manager/ManagementApp.jsx",
"packages/desktop-client/src/components/manager/subscribe/common.tsx",
"packages/desktop-client/src/components/ManageRules.tsx",
"packages/desktop-client/src/components/mobile/MobileAmountInput.jsx",
"packages/desktop-client/src/components/mobile/MobileNavTabs.tsx",
"packages/desktop-client/src/components/Modals.tsx",
"packages/desktop-client/src/components/modals/EditRule.jsx",
"packages/desktop-client/src/components/modals/ImportTransactions.jsx",
"packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx",
"packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx",
"packages/desktop-client/src/components/Notifications.tsx",
"packages/desktop-client/src/components/payees/ManagePayees.jsx",
"packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx",
"packages/desktop-client/src/components/payees/PayeeTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx",
"packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.tsx",
"packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx",
"packages/desktop-client/src/components/reports/SaveReportName.tsx",
"packages/desktop-client/src/components/reports/useReport.ts",
"packages/desktop-client/src/components/schedules/ScheduleDetails.jsx",
"packages/desktop-client/src/components/schedules/ScheduleEditModal.tsx",
"packages/desktop-client/src/components/schedules/SchedulesTable.tsx",
"packages/desktop-client/src/components/select/DateSelect.tsx",
"packages/desktop-client/src/components/sidebar/Tools.tsx",
"packages/desktop-client/src/components/sort.tsx",
"packages/desktop-client/src/hooks/useEffectAfterMount.ts",
"packages/desktop-client/src/hooks/useQuery.ts"
],
"rules": {
"react/rules-of-hooks": "off"
"react/exhaustive-deps": "off"
}
},
{

View File

@@ -259,10 +259,6 @@ Always run `yarn typecheck` before committing.
- Generate i18n files: `yarn generate:i18n`
- Custom ESLint rules enforce translation usage
### 5. Financial Number Typography
- Wrap standalone financial numbers with `FinancialText` or apply `styles.tnum` directly if wrapping is not possible
## Code Style & Conventions
### TypeScript Guidelines
@@ -557,10 +553,6 @@ When creating pull requests:
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
## Code Review Guidelines
When performing code reviews (especially for LLM agents): **see [CODE_REVIEW_GUIDELINES.md](./CODE_REVIEW_GUIDELINES.md)** for specific guidelines.
## Performance Considerations
- **Bundle Size**: Check with rollup-plugin-visualizer

View File

@@ -1,94 +0,0 @@
# CODE_REVIEW_GUIDELINES.md - Guidelines for LLM Agents Performing Code Reviews
This document provides specific guidelines for LLM agents performing code reviews on the Actual Budget codebase. These guidelines help maintain code quality, consistency, and follow the project's design principles.
## Settings Proliferation
**Do NOT add new settings for every little UI tweak.**
Actual Budget follows a design philosophy that prioritizes simplicity and avoids settings bloat. Before introducing code that adds new settings:
- Consider if the UI tweak can be achieved through existing theme/design tokens
- Evaluate whether the setting provides meaningful value to users
- Check if the change aligns with Actual's design guidelines
- Prefer hardcoded values or theme-based solutions over adding user-facing settings
## TypeScript Strict Mode Suppressions
**Do NOT approve code that adds new `@ts-strict-ignore` comments.**
The project uses strict TypeScript checking via `typescript-strict-plugin`. Adding `@ts-strict-ignore` comments undermines type safety. Instead, review should encourage:
- Fixing the underlying type issue
- Using proper type definitions
- Refactoring code to satisfy strict type checking
- Only in exceptional cases, document why strict checking cannot be applied and seek alternative solutions
## Linter Suppressions
**Do NOT approve code that adds new `eslint-disable` or `oxlint-disable` comments.**
Linter rules are in place for good reasons. Instead of suppressing them:
- Fix the underlying issue
- If the rule is incorrectly flagging valid code, consider if the code can be refactored
- Only approve suppressions if there's a documented, exceptional reason
## Type Assertions
**Prefer `x satisfies SomeType` over `x as SomeType` for type coercions.**
The `satisfies` operator provides better type safety by:
- Ensuring the value actually satisfies the type (narrowing)
- Preserving the actual type information for better inference
- Catching type mismatches at compile time
**Exception:** If you truly need to assert a type that TypeScript cannot verify (e.g., runtime type guards), use `as` but require a comment explaining why it's safe.
## Avoiding `any` and `unknown`
**Flag code that uses `any` or `unknown` unless absolutely necessary.**
The use of `any` or `unknown` should be rare and well-justified. Before approving:
- Require explicit justification for why the type cannot be determined
- Suggest using proper type definitions or generics
- Consider if the type can be narrowed or properly inferred
- Look for existing type definitions in `packages/loot-core/src/types/`
Only approve `any` or `unknown` if there's a documented, exceptional reason (e.g., interop with untyped external libraries, gradual migration).
## Internationalization (i18n)
**All user-facing strings must be translated.**
The project has custom ESLint rules (`actual/no-untranslated-strings`) that enforce i18n usage, but reviewers should actively flag untranslated strings:
- Use `Trans` component instead of `t()` function when possible
- All text visible to users must use i18n functions
- Flag hardcoded strings that should be translated
## Test Mocking
**Minimize mocked dependencies; prefer real implementations.**
When reviewing tests, encourage the use of real implementations over mocks:
- Prefer real dependencies, utilities, and data structures
- Only mock when the real implementation is impractical (e.g., external APIs, file system in unit tests)
- Ensure mocks accurately represent real behavior
Over-mocking makes tests brittle and less reliable. Real implementations provide better confidence that code works correctly.
## Financial Number Typography
Standalone financial numbers should have tabular number styles applied.
- Standalone financial numbers should be wrapped with `FinancialText` or `styles.tnum` should be applied directly if wrapping is not possible
## Related Documentation
- See [AGENTS.md](./AGENTS.md) for general development guidelines
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines
- Community documentation: [https://actualbudget.org/docs/contributing/](https://actualbudget.org/docs/contributing/)

View File

@@ -37,7 +37,7 @@ async function run() {
choices: [
{ title: '✨ Features', value: 'Features' },
{ title: '👍 Enhancements', value: 'Enhancements' },
{ title: '🐛 Bugfixes', value: 'Bugfixes' },
{ title: '🐛 Bugfix', value: 'Bugfix' },
{ title: '⚙️ Maintenance', value: 'Maintenance' },
],
},

29
bin/run-browser-tests Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
# Run browser tests (vitest browser mode) in Docker for consistent screenshot quality
# Browser tests generate screenshots that vary by environment (fonts, rendering, etc.)
# Running in Docker ensures consistent results across different machines
#
# Usage:
# yarn test:browser # Run all browser tests
# yarn test:browser AuthSettings.browser # Run specific test file
# yarn test:browser --update # Update snapshots
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
yarn
fi
TEST_ARGS=""
# Loop through all arguments
while [ $# -gt 0 ]; do
TEST_ARGS="$TEST_ARGS $1"
shift
done
echo "Running browser tests in Docker for consistent screenshot quality..."
echo "Test args: $TEST_ARGS"
echo ""
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
-c "yarn workspace @actual-app/web test --project=browser $TEST_ARGS"

View File

@@ -33,7 +33,6 @@
"start:service-plugins": "yarn workspace plugins-service watch",
"start:browser-backend": "yarn workspace loot-core watch:browser",
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"start:storybook": "yarn workspace @actual-app/components start:storybook",
"build:browser-backend": "yarn workspace loot-core build:browser",
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
"build:browser": "./bin/package-browser",
@@ -41,11 +40,11 @@
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:docs": "yarn workspace docs build",
"build:storybook": "yarn workspace @actual-app/components build:storybook",
"deploy:docs": "yarn workspace docs deploy",
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
"test": "lage test --continue",
"test:browser": "./bin/run-browser-tests",
"test:debug": "lage test --no-cache --continue",
"e2e": "yarn workspace @actual-app/web run e2e",
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",
@@ -65,7 +64,6 @@
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.3",
"@types/prompts": "^2.4.9",
"baseline-browser-mapping": "^2.9.14",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-perfectionist": "^4.15.1",
@@ -77,8 +75,8 @@
"minimatch": "^10.1.1",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.26.0",
"oxlint": "^1.41.0",
"oxfmt": "^0.22.0",
"oxlint": "^1.38.0",
"p-limit": "^7.2.0",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "26.2.0",
"version": "26.1.0",
"description": "An API for Actual",
"license": "MIT",
"files": [

View File

@@ -174,7 +174,6 @@ function parseArgs(argv) {
return {
sections,
identifier: getSingleValue(args, 'identifier') ?? 'bundle-stats',
format: getSingleValue(args, 'format') ?? 'pr-body',
};
}
@@ -464,12 +463,6 @@ const TOTAL_HEADERS = makeHeader([
'Total bundle size',
'% Changed',
]);
const SUMMARY_HEADERS = makeHeader([
'Bundle',
'Files count',
'Total bundle size',
'% Changed',
]);
const TABLE_HEADERS = makeHeader(['Asset', 'File Size', '% Changed']);
const CHUNK_TABLE_HEADERS = makeHeader(['File', 'Δ', 'Size']);
@@ -603,24 +596,6 @@ function printTotalAssetTable(statsDiff) {
return `**Total**\n${TOTAL_HEADERS}\n${printAssetTableRow(statsDiff.total)}`;
}
function printSummaryTable(sections) {
if (sections.length === 0) {
return `${SUMMARY_HEADERS}\nNo bundle stats were generated.`;
}
const rows = sections.map(section => {
const total = section.statsDiff.total;
return [
section.name,
total.name,
toFileSizeDiffCell(total),
conditionalPercentage(total.diffPercentage),
].join(' | ');
});
return `${SUMMARY_HEADERS}\n${rows.join('\n')}`;
}
function renderSection(title, statsDiff, chunkModuleDiff) {
const { total, ...groups } = statsDiff;
const parts = [`#### ${title}`, '', printTotalAssetTable({ total })];
@@ -640,30 +615,8 @@ function renderSection(title, statsDiff, chunkModuleDiff) {
return parts.join('\n');
}
function renderSections(sections) {
return sections
.map(section =>
renderSection(section.name, section.statsDiff, section.chunkDiff),
)
.join('\n\n---\n\n');
}
function getIdentifierMarkers(key) {
const label = 'bundlestats-action-comment';
return {
start: `<!--- ${label} key:${key} start --->`,
end: `<!--- ${label} key:${key} end --->`,
};
}
async function main() {
const args = parseArgs(process.argv);
const allowedFormats = new Set(['comment', 'pr-body']);
if (!allowedFormats.has(args.format)) {
throw new Error(
`Invalid format "${args.format}". Use "comment" or "pr-body".`,
);
}
console.error(
`[bundle-stats] Found ${args.sections.length} sections to process`,
@@ -701,29 +654,22 @@ async function main() {
});
}
const markers = getIdentifierMarkers(args.identifier);
const sectionsContent = renderSections(sections);
const summaryTable = printSummaryTable(sections);
const identifier = `<!--- bundlestats-action-comment key:${args.identifier} --->`;
const detailedBody = ['### Bundle Stats', '', sectionsContent].join('\n');
const commentBody = [markers.start, detailedBody, '', markers.end, ''].join(
'\n',
);
const prBody = [
markers.start,
const comment = [
'### Bundle Stats',
'',
summaryTable,
sections
.map(section =>
renderSection(section.name, section.statsDiff, section.chunkDiff),
)
.join('\n\n---\n\n'),
'',
`<details>\n<summary>View detailed bundle stats</summary>\n\n${sectionsContent}\n</details>`,
'',
markers.end,
identifier,
'',
].join('\n');
process.stdout.write(args.format === 'comment' ? commentBody : prBody);
process.stdout.write(comment);
}
main().catch(error => {

View File

@@ -19,10 +19,6 @@ const options = {
type: 'string', // nightly, hotfix, monthly, auto
short: 't',
},
version: {
type: 'string',
short: 'v',
},
update: {
type: 'boolean',
short: 'u',
@@ -48,21 +44,16 @@ try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const explicitVersion = values.version;
let newVersion;
if (explicitVersion) {
newVersion = explicitVersion;
} else {
try {
newVersion = getNextVersion({
currentVersion,
type: values.type,
currentDate: new Date(),
});
} catch (e) {
console.error(e.message);
process.exit(1);
}
try {
newVersion = getNextVersion({
currentVersion,
type: values.type,
currentDate: new Date(),
});
} catch (e) {
console.error(e.message);
process.exit(1);
}
process.stdout.write(newVersion);

View File

@@ -14,14 +14,10 @@ import process from 'node:process';
import { Octokit } from '@octokit/rest';
const BOT_BOUNDARY_MARKER = '<!--- actual-bot-sections --->';
const BOT_BOUNDARY_TEXT = `${BOT_BOUNDARY_MARKER}\n<hr />`;
function parseArgs(argv) {
const args = {
commentFile: null,
identifier: null,
target: 'comment',
};
for (let i = 2; i < argv.length; i += 2) {
@@ -45,9 +41,6 @@ function parseArgs(argv) {
case '--identifier':
args.identifier = value;
break;
case '--target':
args.target = value;
break;
default:
throw new Error(`Unknown argument "${key}".`);
}
@@ -61,12 +54,6 @@ function parseArgs(argv) {
throw new Error('Missing required argument "--identifier".');
}
if (!['comment', 'pr-body'].includes(args.target)) {
throw new Error(
`Invalid value "${args.target}" for "--target". Use "comment" or "pr-body".`,
);
}
return args;
}
@@ -123,123 +110,20 @@ function isGitHubActionsBot(comment) {
return comment.user?.login === 'github-actions[bot]';
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getIdentifierMarkers(identifier) {
if (identifier.includes('<!---')) {
return {
start: identifier,
end: null,
};
}
const label = 'bundlestats-action-comment';
return {
start: `<!--- ${label} key:${identifier} start --->`,
end: `<!--- ${label} key:${identifier} end --->`,
};
}
function upsertBlock(existingBody, block, markers) {
const body = existingBody ?? '';
if (markers.end) {
const pattern = new RegExp(
`${escapeRegExp(markers.start)}[\\s\\S]*?${escapeRegExp(markers.end)}`,
'm',
);
if (pattern.test(body)) {
return body.replace(pattern, block.trim());
}
}
if (body.trim().length === 0) {
return block.trim();
}
const separator = body.endsWith('\n') ? '\n' : '\n\n';
const boundary = body.includes(BOT_BOUNDARY_MARKER)
? ''
: `${BOT_BOUNDARY_TEXT}\n\n`;
return `${body}${separator}${boundary}${block.trim()}`;
}
async function updatePullRequestBody(
octokit,
owner,
repo,
pullNumber,
block,
markers,
) {
const { data } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pullNumber,
});
const nextBody = upsertBlock(data.body ?? '', block, markers);
await octokit.rest.pulls.update({
owner,
repo,
pull_number: pullNumber,
body: nextBody,
});
}
async function deleteExistingComment(
octokit,
owner,
repo,
issueNumber,
markers,
) {
const comments = await listComments(octokit, owner, repo, issueNumber);
const existingComment = comments.find(
comment =>
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
);
if (existingComment) {
await octokit.rest.issues.deleteComment({
owner,
repo,
comment_id: existingComment.id,
});
}
}
async function main() {
const { commentFile, identifier, target } = parseArgs(process.argv);
const { commentFile, identifier } = parseArgs(process.argv);
const commentBody = await loadCommentBody(commentFile);
const token = assertGitHubToken();
const { owner, repo } = getRepoInfo();
const issueNumber = getPullRequestNumber();
const markers = getIdentifierMarkers(identifier);
const octokit = new Octokit({ auth: token });
if (target === 'pr-body') {
await updatePullRequestBody(
octokit,
owner,
repo,
issueNumber,
commentBody,
markers,
);
await deleteExistingComment(octokit, owner, repo, issueNumber, markers);
console.log('Updated pull request body with bundle stats.');
return;
}
const comments = await listComments(octokit, owner, repo, issueNumber);
const existingComment = comments.find(
comment =>
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
isGitHubActionsBot(comment) && comment.body?.includes(identifier),
);
if (existingComment) {
@@ -250,16 +134,15 @@ async function main() {
body: commentBody,
});
console.log('Updated existing bundle stats comment.');
return;
} else {
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody,
});
console.log('Created new bundle stats comment.');
}
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody,
});
console.log('Created new bundle stats comment.');
}
main().catch(error => {

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, it, expect } from 'vitest';
import { getNextVersion } from './get-next-package-version';

View File

@@ -1,3 +0,0 @@
{
"jsPlugins": ["eslint-plugin-storybook"]
}

View File

@@ -1,43 +0,0 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import type { StorybookConfig } from '@storybook/react-vite';
import viteTsconfigPaths from 'vite-tsconfig-paths';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string) {
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
}
const config: StorybookConfig = {
stories: [
'../src/Introduction.mdx',
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-a11y'),
getAbsolutePath('@storybook/addon-docs'),
],
framework: getAbsolutePath('@storybook/react-vite'),
core: {
disableTelemetry: true,
},
async viteFinal(config) {
const { mergeConfig } = await import('vite');
return mergeConfig(config, {
// Telling Vite how to resolve path aliases
plugins: [viteTsconfigPaths({ root: '../..' })],
esbuild: {
// Needed to handle JSX in .ts/.tsx files
jsx: 'automatic',
},
});
},
};
export default config;

View File

@@ -1,74 +0,0 @@
import { addons } from 'storybook/manager-api';
import { create } from 'storybook/theming/create';
// Colors from the Actual Budget light theme palette
const purple500 = '#8719e0';
const purple400 = '#9a3de8';
const navy900 = '#102a43';
const navy700 = '#334e68';
const navy600 = '#486581';
const navy150 = '#d9e2ec';
const navy100 = '#e8ecf0';
const white = '#ffffff';
// Create a custom Storybook theme matching Actual Budget's light theme
const theme = create({
base: 'light',
brandTitle: 'Actual Budget',
brandUrl: 'https://actualbudget.org',
brandImage: 'https://actualbudget.org/img/actual.webp',
brandTarget: '_blank',
// UI colors
colorPrimary: purple500,
colorSecondary: purple400,
// App chrome
appBg: navy100,
appContentBg: white,
appPreviewBg: white,
appBorderColor: navy150,
appBorderRadius: 4,
// Fonts
fontBase:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontCode: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
// Text colors
textColor: navy900,
textInverseColor: white,
textMutedColor: navy600,
// Toolbar
barTextColor: navy700,
barHoverColor: purple500,
barSelectedColor: purple500,
barBg: white,
// Form colors
buttonBg: white,
buttonBorder: navy900,
booleanBg: navy150,
booleanSelectedBg: purple500,
inputBg: white,
inputBorder: navy900,
inputTextColor: navy900,
inputBorderRadius: 4,
});
addons.setConfig({
theme,
enableShortcuts: true,
isFullscreen: false,
isToolshown: true,
sidebar: {
collapsedRoots: [],
filters: {
patterns: item => {
// Hide stories that are marked as internal
return !item.tags?.includes('internal');
},
},
},
});

View File

@@ -1,88 +0,0 @@
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
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
import * as lightTheme from '../../desktop-client/src/style/themes/light';
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
const THEMES = {
light: lightTheme,
dark: darkTheme,
midnight: midnightTheme,
development: developmentTheme,
} as const;
type ThemeName = keyof typeof THEMES;
const ThemedStory = ({
themeName,
children,
}: {
themeName?: ThemeName;
children?: ReactNode;
}) => {
if (!themeName || !THEMES[themeName]) {
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>
{children}
</div>
);
};
const preview: Preview = {
decorators: [
(Story, { globals }) => {
const themeName = globals.theme;
return (
<ThemedStory themeName={themeName}>
<Story />
</ThemedStory>
);
},
],
globalTypes: {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
{ value: 'midnight', title: 'Midnight' },
{ value: 'development', title: 'Development' },
],
},
},
},
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
},
};
export default preview;

View File

@@ -37,9 +37,7 @@
"scripts": {
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
"test": "npm-run-all -cp 'test:*'",
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
"start:storybook": "storybook dev -p 6006",
"build:storybook": "storybook build"
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
},
"dependencies": {
"@emotion/css": "^11.13.5",
@@ -47,16 +45,10 @@
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.0",
"@storybook/addon-a11y": "^10.2.0",
"@storybook/addon-docs": "^10.2.0",
"@storybook/react-vite": "^10.2.0",
"@svgr/cli": "^8.1.0",
"@types/react": "^19.2.5",
"eslint-plugin-storybook": "^10.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"storybook": "^10.2.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"vitest": "^4.0.16"
},
"peerDependencies": {

View File

@@ -1,4 +1,4 @@
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
import { type ComponentProps, type ReactNode, type CSSProperties } from 'react';
import { Block } from './Block';
import { View } from './View';

View File

@@ -1,100 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Button } from './Button';
const meta = {
title: 'Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
onClick: { action: 'clicked' },
},
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
variant: 'primary',
bounce: false,
children: 'Button Text',
},
parameters: {
docs: {
description: {
story: `
Primary button variant uses the following theme CSS variables:
- \`--color-buttonPrimaryText\`
- \`--color-buttonPrimaryTextHover\`
- \`--color-buttonPrimaryBackground\`
- \`--color-buttonPrimaryBackgroundHover\`
- \`--color-buttonPrimaryBorder\`
- \`--color-buttonPrimaryShadow\`
- \`--color-buttonPrimaryDisabledText\`
- \`--color-buttonPrimaryDisabledBackground\`
- \`--color-buttonPrimaryDisabledBorder\`
`,
},
},
},
};
export const Normal: Story = {
args: {
variant: 'normal',
bounce: false,
children: 'Button Text',
},
parameters: {
docs: {
description: {
story: `
Normal button variant uses the following theme CSS variables:
- \`--color-buttonNormalText\`
- \`--color-buttonNormalTextHover\`
- \`--color-buttonNormalBackground\`
- \`--color-buttonNormalBackgroundHover\`
- \`--color-buttonNormalBorder\`
- \`--color-buttonNormalShadow\`
- \`--color-buttonNormalSelectedText\`
- \`--color-buttonNormalSelectedBackground\`
- \`--color-buttonNormalDisabledText\`
- \`--color-buttonNormalDisabledBackground\`
- \`--color-buttonNormalDisabledBorder\`
`,
},
},
},
};
export const Bare: Story = {
args: {
variant: 'bare',
bounce: false,
children: 'Button Text',
},
parameters: {
docs: {
description: {
story: `
Bare button variant uses the following theme CSS variables:
- \`--color-buttonBareText\`
- \`--color-buttonBareTextHover\`
- \`--color-buttonBareBackground\`
- \`--color-buttonBareBackgroundHover\`
- \`--color-buttonBareBackgroundActive\`
- \`--color-buttonBareDisabledText\`
- \`--color-buttonBareDisabledBackground\`
`,
},
},
},
};

View File

@@ -2,8 +2,8 @@ import React, {
forwardRef,
useMemo,
type ComponentPropsWithoutRef,
type CSSProperties,
type ReactNode,
type CSSProperties,
} from 'react';
import { Button as ReactAriaButton } from 'react-aria-components';

View File

@@ -1,4 +1,4 @@
import { forwardRef, type ComponentProps } from 'react';
import { type ComponentProps, forwardRef } from 'react';
import { theme } from './theme';
import { View } from './View';

View File

@@ -1,15 +1,15 @@
import { type ChangeEvent, type ReactNode } from 'react';
import {
ColorPicker as AriaColorPicker,
ColorSwatch as AriaColorSwatch,
ColorSwatchPicker as AriaColorSwatchPicker,
ColorField,
ColorSwatchPickerItem,
type ColorPickerProps as AriaColorPickerProps,
Dialog,
DialogTrigger,
parseColor,
type ColorPickerProps as AriaColorPickerProps,
ColorSwatch as AriaColorSwatch,
type ColorSwatchProps,
ColorSwatchPicker as AriaColorSwatchPicker,
ColorSwatchPickerItem,
ColorField,
parseColor,
} from 'react-aria-components';
import { css } from '@emotion/css';

View File

@@ -1,4 +1,4 @@
import { type CSSProperties, type ReactNode } from 'react';
import { type ReactNode, type CSSProperties } from 'react';
import { View } from './View';

View File

@@ -2,11 +2,11 @@ import {
Children,
cloneElement,
isValidElement,
useEffect,
useRef,
type ReactElement,
type Ref,
type RefObject,
useEffect,
useRef,
} from 'react';
type InitialFocusProps<T extends HTMLElement> = {

View File

@@ -1,8 +1,8 @@
import React, {
type ChangeEvent,
type ComponentPropsWithRef,
type FocusEvent,
type KeyboardEvent,
type FocusEvent,
} from 'react';
import { Input as ReactAriaInput } from 'react-aria-components';

View File

@@ -1,27 +0,0 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Introduction" />
# Actual Budget Component Library
Welcome to the **Actual Budget Component Library**. Explore our UI components, see how they look across different themes, and learn how to use them in your code.
### What you can do here
- ✨ **Browse components** in the sidebar
- 🎨 **Switch themes** using the toolbar above
- 📚 **Read documentation** and see code examples
- 🔍 **Test variations** and component states
- ♿ **Check accessibility** compliance
### Getting Started
Select a component from the sidebar to explore its documentation, variants, and interactive controls.
---
### Useful Links
- [Actual Budget Website](https://actualbudget.org)
- [Documentation](https://actualbudget.org/docs)
- [GitHub Repository](https://github.com/actualbudget/actual)

View File

@@ -1,4 +1,4 @@
import { forwardRef, type CSSProperties, type ReactNode } from 'react';
import { forwardRef, type ReactNode, type CSSProperties } from 'react';
import { styles } from './styles';
import { Text } from './Text';

View File

@@ -1,13 +1,13 @@
import {
useEffect,
useRef,
type ReactNode,
useState,
type ComponentProps,
type ComponentType,
type SVGProps,
type CSSProperties,
type KeyboardEvent,
type ReactNode,
type SVGProps,
useEffect,
useRef,
} from 'react';
import { Button } from './Button';
@@ -152,7 +152,7 @@ export function Menu<const NameType = string>({
<View
className={className}
style={{ outline: 'none', borderRadius: 4, overflow: 'hidden', ...style }}
tabIndex={0}
tabIndex={1}
onKeyDown={onKeyDown}
innerRef={elRef}
>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, type ComponentProps } from 'react';
import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
import { Popover as ReactAriaPopover } from 'react-aria-components';
import { css } from '@emotion/css';

View File

@@ -1,8 +1,8 @@
import React, {
forwardRef,
type HTMLProps,
type ReactNode,
type Ref,
type ReactNode,
forwardRef,
} from 'react';
import { css, cx } from '@emotion/css';

View File

@@ -1,4 +1,4 @@
import React, { useState, type SVGProps } from 'react';
import React, { type SVGProps, useState } from 'react';
export const SvgLoading = (props: SVGProps<SVGSVGElement>) => {
const { color = 'currentColor' } = props;

View File

@@ -162,11 +162,4 @@ export const styles: Record<string, any> = {
padding: 16,
cursor: 'pointer',
},
tableContainer: {
flex: 1,
border: '1px solid ' + theme.tableBorder,
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
overflow: 'hidden',
},
};

View File

@@ -13,9 +13,6 @@ export const theme = {
pageTextPositive: 'var(--color-pageTextPositive)',
pageTextLink: 'var(--color-pageTextLink)',
pageTextLinkLight: 'var(--color-pageTextLinkLight)',
numberPositive: 'var(--color-numberPositive)',
numberNegative: 'var(--color-numberNegative)',
numberNeutral: 'var(--color-numberNeutral)',
cardBackground: 'var(--color-cardBackground)',
cardBorder: 'var(--color-cardBorder)',
cardShadow: 'var(--color-cardShadow)',
@@ -46,7 +43,6 @@ export const theme = {
sidebarItemBackgroundHover: 'var(--color-sidebarItemBackgroundHover)',
sidebarItemText: 'var(--color-sidebarItemText)',
sidebarItemTextSelected: 'var(--color-sidebarItemTextSelected)',
sidebarBudgetName: 'var(--color-sidebarBudgetName)',
menuBackground: 'var(--color-menuBackground)',
menuItemBackground: 'var(--color-menuItemBackground)',
menuItemBackgroundHover: 'var(--color-menuItemBackgroundHover)',
@@ -190,19 +186,6 @@ export const theme = {
reportsGray: 'var(--color-reportsGray)',
reportsLabel: 'var(--color-reportsLabel)',
reportsInnerLabel: 'var(--color-reportsInnerLabel)',
reportsChartFill: 'var(--color-reportsChartFill)',
reportsNumberPositive: 'var(--color-reportsNumberPositive)',
reportsNumberNegative: 'var(--color-reportsNumberNegative)',
reportsNumberNeutral: 'var(--color-reportsNumberNeutral)',
budgetNumberPositive: 'var(--color-budgetNumberPositive)',
budgetNumberNegative: 'var(--color-budgetNumberNegative)',
budgetNumberNeutral: 'var(--color-budgetNumberNeutral)',
budgetNumberZero: 'var(--color-budgetNumberZero)',
toBudgetPositive: 'var(--color-toBudgetPositive)',
toBudgetZero: 'var(--color-toBudgetZero)',
toBudgetNegative: 'var(--color-toBudgetNegative)',
templateNumberFunded: 'var(--color-templateNumberFunded)',
templateNumberUnderFunded: 'var(--color-templateNumberUnderFunded)',
noteTagBackground: 'var(--color-noteTagBackground)',
noteTagBackgroundHover: 'var(--color-noteTagBackgroundHover)',
noteTagDefault: 'var(--color-noteTagDefault)',
@@ -218,5 +201,4 @@ export const theme = {
tooltipBackground: 'var(--color-tooltipBackground)',
tooltipBorder: 'var(--color-tooltipBorder)',
calendarCellBackground: 'var(--color-calendarCellBackground)',
overlayBackground: 'var(--color-overlayBackground)',
};

View File

@@ -8,6 +8,7 @@ coverage
test-results
playwright-report
blob-report
.vitest-attachments/
# production
build

View File

@@ -96,3 +96,48 @@ Run locally:
```sh
E2E_START_URL=https://my-remote-server.com yarn vrt
```
## Browser Tests (Vitest Browser Mode)
Browser tests (`.browser.test.tsx` files) use Vitest's browser mode to test React components with visual regression screenshots. These tests generate screenshots that can vary significantly by environment (fonts, rendering, DPI, etc.).
**IMPORTANT: For consistent screenshot quality, always run browser tests in Docker.**
### Running Browser Tests in Docker
From the project root:
```sh
# Run all browser tests
yarn test:browser:docker
# Run a specific browser test file
yarn test:browser:docker AuthSettings.browser
# Run with update flag to update snapshots
yarn test:browser:docker AuthSettings.browser --update
```
From the `packages/desktop-client` directory:
```sh
# Run all browser tests
yarn test:browser:docker
# Run a specific browser test file
yarn test:browser:docker AuthSettings.browser
# Run with update flag
yarn test:browser:docker AuthSettings.browser --update
```
### Why Docker?
Running browser tests locally will produce inconsistent screenshots due to:
- System-specific font rendering
- Different DPI/display scaling
- OS-specific rendering differences
- Font availability variations
Docker ensures all tests run in the same standardized environment (`mcr.microsoft.com/playwright:v1.56.0-jammy`), producing consistent, reproducible screenshots.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

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