Compare commits

..

4 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
efaee98d2a Update error message 2026-01-12 15:29:58 -08:00
Joel Jeremy Marquez
0b156d1815 Add onInsert logic to useDragAndDrop 2026-01-12 15:00:45 -08:00
autofix-ci[bot]
5e9f38ea45 [autofix.ci] apply automated fixes 2026-01-12 21:54:47 +00:00
Joel Jeremy Marquez
b901e7a6bd [Mobile] Fix drag and drop across category groups 2026-01-12 13:53:34 -08:00
1336 changed files with 10541 additions and 21046 deletions

View File

@@ -3,15 +3,24 @@ issue_enrichment:
enabled: false
reviews:
request_changes_workflow: true
review_status: false
review_status: true
high_level_summary: false
finishing_touches:
docstrings:
enabled: false
pre_merge_checks:
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

@@ -8,66 +8,35 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
⚠️ **CRITICAL:** Bug reports without clear, step-by-step reproduction instructions will be closed. We cannot investigate or fix bugs without being able to reproduce them. Please take the time to provide detailed reproduction steps.
- type: markdown
attributes:
value: |
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
**Bank-sync problems (SimpleFin / GoCardless)?** Reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) first and open an issue only if the community deems the issue to be a legitimate bug in Actual.
- type: checkboxes
id: existing-issue
attributes:
label: 'Verified issue does not already exist?'
description: 'Please search to see if an issue already exists for the issue you encountered.'
options:
- label: 'I have searched and found no existing issue'
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
Describe the bug clearly and concisely. Include:
- What you were trying to do
- What you expected to happen
- What actually happened instead
- Any error messages (copy/paste the exact text)
If you're reporting an issue with imports, please include a (redacted) version of the file, and a screenshot of the import screen. You may need to zip it before uploading.
placeholder: |
I was trying to [action] when [context].
Expected: [expected behavior]
Actual: [actual behavior]
Error message: [if any]
description: Also tell us, what did you expect to happen? If you're reporting an issue with imports, please attach a (redacted) version of the file you're having trouble importing. You may need to zip it before uploading.
placeholder: Tell us what you see!
value: 'A bug happened!'
validations:
required: true
- type: markdown
attributes:
value: |
## Reproduction Steps
**REQUIRED:** Without clear reproduction steps, we cannot investigate or fix the bug. Please provide detailed, step-by-step instructions that anyone can follow to reproduce the issue.
- type: textarea
id: reproduction
attributes:
label: How can we reproduce the issue?
description: |
**This field is mandatory and must be filled out completely.**
Provide numbered, step-by-step instructions that allow us to reproduce the bug. Include:
- Specific actions you took (e.g., "Click on the Budget tab", "Enter $100 in the amount field")
- What you expected to happen
- What actually happened instead
Example format:
1. Navigate to [specific page/section]
2. Click on [specific button/link]
3. Enter [specific data] in [specific field]
4. Click [action]
5. Observe [expected vs actual behavior]
If the issue involves importing data, please attach a (redacted) sample file. You may need to zip it before uploading.
placeholder: |
1. Go to [specific location]
2. Click [specific element]
3. Enter [specific data]
4. Click [action]
5. Expected: [what should happen]
Actual: [what actually happens]
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
value: 'How can we reproduce the issue?'
validations:
required: true
- type: markdown

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

@@ -36,13 +36,11 @@ async function getPRDetails() {
console.log('- PR Number:', pr.number);
console.log('- PR Author:', pr.user.login);
console.log('- PR Title:', pr.title);
console.log('- Base Branch:', pr.base.ref);
const result = {
number: pr.number,
author: pr.user.login,
title: pr.title,
baseBranch: pr.base.ref,
};
setOutput('result', JSON.stringify(result));

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

@@ -8,13 +8,6 @@ const CONFIG = {
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
PR_CONTRIBUTION_POINTS: {
Features: 2,
Enhancements: 2,
Bugfix: 3,
Maintenance: 2,
Unknown: 2,
},
// Point tiers for code changes (non-docs)
CODE_PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
@@ -38,116 +31,6 @@ const CONFIG = {
DOCS_FILES_PATTERN: 'packages/docs/**/*',
};
/**
* Parse category from release notes file content.
* @param {string} content - The content of the release notes file.
* @returns {string|null} The category or null if not found.
*/
function parseReleaseNotesCategory(content) {
if (!content) return null;
// Extract YAML front matter
const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!frontMatterMatch) return null;
// Extract category from front matter
const categoryMatch = frontMatterMatch[1].match(/^category:\s*(.+)$/m);
if (!categoryMatch) return null;
return categoryMatch[1].trim();
}
/**
* Get the last commit SHA on or before a given date.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {Date} beforeDate - The date to find the last commit before.
* @returns {Promise<string|null>} The commit SHA or null if not found.
*/
async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) {
try {
// Get the default branch from the repository
const { data: repoData } = await octokit.repos.get({ owner, repo });
const defaultBranch = repoData.default_branch;
const { data: commits } = await octokit.repos.listCommits({
owner,
repo,
sha: defaultBranch,
until: beforeDate.toISOString(),
per_page: 1,
});
if (commits.length > 0) {
return commits[0].sha;
}
} catch {
// If error occurs, return null to fall back to default branch
}
return null;
}
/**
* Get the category and points for a PR by reading its release notes file.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {number} prNumber - PR number.
* @param {Date} monthEnd - The end date of the month to use as base revision.
* @returns {Object} Object with category and points, or null if error.
*/
async function getPRCategoryAndPoints(
octokit,
owner,
repo,
prNumber,
monthEnd,
) {
const releaseNotesPath = `upcoming-release-notes/${prNumber}.md`;
try {
// Get the last commit of the month to use as base revision
const commitSha = await getLastCommitBeforeDate(
octokit,
owner,
repo,
monthEnd,
);
// Try to read the release notes file from the last commit of the month
const { data: fileContent } = await octokit.repos.getContent({
owner,
repo,
path: releaseNotesPath,
ref: commitSha || undefined, // Use commit SHA if available, otherwise default branch
});
if (fileContent.content) {
// Decode base64 content
const content = Buffer.from(fileContent.content, 'base64').toString(
'utf-8',
);
const category = parseReleaseNotesCategory(content);
if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) {
return {
category,
points: CONFIG.PR_CONTRIBUTION_POINTS[category],
};
}
}
} catch {
// Do nothing
}
return {
category: 'Unknown',
points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown,
};
}
/**
* Get the start and end dates for the last month.
* @returns {Object} An object containing the start and end dates.
@@ -206,7 +89,6 @@ async function countContributorPoints() {
{
codeReviews: [], // Will store objects with PR number and points for main repo changes
docsReviews: [], // Will store objects with PR number and points for docs changes
prContributions: [], // Will store objects with PR number, category, and points for PR author contributions
labelRemovals: [],
issueClosings: [],
points: 0,
@@ -320,28 +202,6 @@ async function countContributorPoints() {
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
}
} else {
// Award points to PR author if they are a core maintainer
const prAuthor = pr.user?.login;
if (prAuthor && orgMemberLogins.has(prAuthor)) {
const categoryAndPoints = await getPRCategoryAndPoints(
octokit,
owner,
repo,
pr.number,
until,
);
if (categoryAndPoints) {
const authorStats = stats.get(prAuthor);
authorStats.prContributions.push({
pr: pr.number.toString(),
category: categoryAndPoints.category,
points: categoryAndPoints.points,
});
authorStats.points += categoryAndPoints.points;
}
}
const uniqueReviewers = new Set();
reviews.data.forEach(review => {
if (
@@ -418,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);
@@ -433,7 +293,7 @@ async function countContributorPoints() {
// Print all statistics
printStats(
'Code Review Statistics',
stats => stats.codeReviews.reduce((sum, r) => sum + r.points, 0),
stats => stats.codeReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -448,7 +308,7 @@ async function countContributorPoints() {
printStats(
'Docs Review Statistics',
stats => stats.docsReviews.reduce((sum, r) => sum + r.points, 0),
stats => stats.docsReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -456,27 +316,16 @@ async function countContributorPoints() {
.join(', ')})`,
);
printStats(
'PR Contribution Statistics',
stats => stats.prContributions.reduce((sum, r) => sum + r.points, 0),
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.prContributions.map(r => `#${r.pr} (${r.points}pts - ${r.category})`)
.join(', ')})`,
);
printStats(
'"Needs Triage" Label Removal Statistics',
stats => stats.labelRemovals.length * CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION,
stats => stats.labelRemovals.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
'Issue Closing Statistics',
stats =>
stats.issueClosings.length * CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION,
stats => stats.issueClosings.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
);

View File

@@ -41,21 +41,8 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if PR targets master branch
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-base-branch
run: |
BASE_BRANCH=$(echo '${{ steps.pr-details.outputs.result }}' | jq -r '.baseBranch')
echo "Base branch: $BASE_BRANCH"
if [ "$BASE_BRANCH" = "master" ]; then
echo "targets_master=true" >> $GITHUB_OUTPUT
else
echo "targets_master=false" >> $GITHUB_OUTPUT
echo "PR does not target master branch, skipping release notes generation"
fi
- name: Check if release notes file already exists
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null' && steps.check-base-branch.outputs.targets_master == 'true'
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:

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

@@ -30,7 +30,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -53,7 +53,7 @@ jobs:
name: Functional Desktop App
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -81,7 +81,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
@@ -104,7 +104,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment

View File

@@ -199,7 +199,6 @@ jobs:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
branch: 'release/${{ needs.build.outputs.version }}'
draft: true
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.

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" \
@@ -53,7 +50,6 @@ jobs:
- name: Create PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'

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

View File

@@ -44,7 +44,7 @@ jobs:
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- name: Get PR details
id: pr

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

@@ -15,14 +15,44 @@
"vi": "readonly",
"backend": "readonly",
"importScripts": "readonly",
"FS": "readonly"
"FS": "readonly" // TODO: remove this
},
"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"
}
],
@@ -73,7 +103,13 @@
"jsx-a11y/scope": "warn",
// Typescript rules
"typescript/ban-ts-comment": ["warn"],
"typescript/ban-ts-comment": [
"warn",
{
// TODO: remove this
"ts-ignore": "allow-with-description"
}
],
"typescript/consistent-type-definitions": ["warn", "type"],
"typescript/consistent-type-imports": [
"warn",
@@ -101,7 +137,6 @@
"typescript/no-var-requires": "warn",
// Import rules
"import/consistent-type-specifier-style": "warn",
"import/first": "error",
"import/no-amd": "error",
"import/no-default-export": "warn",
@@ -112,7 +147,7 @@
"import/no-duplicates": [
"warn",
{
"prefer-inline": false
"prefer-inline": true
}
],
@@ -120,7 +155,7 @@
"react/exhaustive-deps": [
"warn",
{
"additionalHooks": "(^useQuery$|^useEffectAfterMount$)"
"additionalHooks": "(useQuery|useEffectAfterMount)"
}
],
"react/jsx-curly-brace-presence": "warn",
@@ -155,7 +190,7 @@
// ESLint rules
"eslint/array-callback-return": "warn",
"eslint/curly": ["warn", "multi-line", "consistent"],
// "eslint/curly": ["warn", "multi-line", "consistent"], // TODO: re-enable? this rule is really slow
"eslint/default-case": [
"warn",
{
@@ -334,10 +369,6 @@
"importNames": ["colors"],
"message": "Please use themes instead of colors"
},
{
"group": ["**/style/themes/*"],
"message": "Please do not import theme files directly"
},
{
"group": ["@actual-app/web/**/*"],
"message": "Please do not import `@actual-app/web` in `loot-core`"
@@ -351,9 +382,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"
}
},
{
@@ -390,18 +467,19 @@
"typescript-paths/absolute-import": ["error", { "enableAlias": false }]
}
},
{
"files": ["packages/desktop-client/src/style/themes/*"],
"rules": {
"eslint/no-restricted-imports": "off"
}
},
// TODO: enable these
{
"files": [
"packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx",
"packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx",
"packages/desktop-client/src/components/budget/BudgetCategories.tsx",
"packages/desktop-client/src/components/budget/envelope/BalanceMovementMenu.tsx",
"packages/desktop-client/src/components/ManageRules.tsx",
"packages/desktop-client/src/components/mobile/budget/ExpenseGroupList.tsx",
"packages/desktop-client/src/components/modals/EditFieldModal.tsx",
"packages/desktop-client/src/components/reports/reports/Calendar.tsx",
"packages/desktop-client/src/components/schedules/ScheduleLink.tsx",
"packages/desktop-client/src/components/ServerContext.tsx",
"packages/desktop-client/src/components/table.tsx"
],
"rules": {

View File

@@ -169,7 +169,7 @@ Custom ESLint rules specific to Actual.
- `no-untranslated-strings`: Enforces i18n usage
- `prefer-trans-over-t`: Prefers Trans component over t() function
- `prefer-logger-over-console`: Enforces using logger instead of console in `packages/loot-core/`
- `prefer-logger-over-console`: Enforces using logger instead of console
- `typography`: Typography rules
- `prefer-if-statement`: Prefers explicit if statements
@@ -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
@@ -332,6 +328,7 @@ Always maintain newlines between import groups.
**Never:**
- Use `console.*` (use logger instead - enforced by ESLint)
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
- Import colors directly - use theme instead
- Import `@actual-app/web/*` in `loot-core`
@@ -544,6 +541,7 @@ Before committing changes, ensure:
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] No new console.\* usage (use logger)
- [ ] User-facing strings are translated
- [ ] Prefer `type` over `interface`
- [ ] Named exports used (not default exports)
@@ -557,10 +555,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' },
],
},
@@ -160,8 +160,7 @@ category: ${type}
authors: [${username}]
---
${summary}
`;
${summary}`;
}
// simple exec that fails silently and returns an empty string on failure

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
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 \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_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,7 +40,6 @@
"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",
@@ -63,9 +61,8 @@
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.3",
"@types/node": "^22.19.1",
"@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 +74,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.37.0",
"p-limit": "^7.2.0",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",

View File

@@ -6,7 +6,6 @@ import type {
// loot-core types
import type { InitConfig } from 'loot-core/server/main';
// oxlint-disable-next-line typescript/ban-ts-comment
// @ts-ignore: bundle not available until we build it
import * as bundle from './app/bundle.api.js';
import * as injected from './injected';

View File

@@ -1,7 +1,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import type { RuleEntity } from 'loot-core/types/models';
import { type RuleEntity } from 'loot-core/types/models';
import * as api from './index';

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": [
@@ -21,7 +21,7 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^12.5.0",
"better-sqlite3": "^12.4.1",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^13.0.0"
@@ -29,7 +29,7 @@
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"vitest": "^4.0.9"
},
"engines": {
"node": ">=20"

View File

@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/ban-ts-comment
// @ts-ignore: bundle not available until we build it
import * as bundle from './app/bundle.api.js';

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

@@ -6,6 +6,6 @@
"test": "vitest --run"
},
"devDependencies": {
"vitest": "^4.0.16"
"vitest": "^4.0.9"
}
}

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,44 +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,
},
staticDirs: ['./public'],
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

@@ -1,9 +0,0 @@
# /assets folder contain processed assets with a file hash
# They are safe for immutable caching, as filename change when content change
/assets/*
Cache-Control: public
Cache-Control: max-age=365000000
Cache-Control: immutable

View File

@@ -37,27 +37,19 @@
"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",
"react-aria-components": "^1.14.0",
"react-aria-components": "^1.13.0",
"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",
"vitest": "^4.0.16"
"react": "19.2.0",
"react-dom": "19.2.0",
"vitest": "^4.0.9"
},
"peerDependencies": {
"react": ">=18.2",

View File

@@ -1,139 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { AlignedText } from './AlignedText';
const meta = {
title: 'AlignedText',
component: AlignedText,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof AlignedText>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
left: 'Label',
right: 'Value',
style: { width: 300, display: 'flex' },
},
parameters: {
docs: {
description: {
story:
'AlignedText displays two pieces of content aligned on opposite sides.',
},
},
},
};
export const TruncateLeft: Story = {
args: {
left: 'This is a very long label that should be truncated on the left side',
right: '$100.00',
truncate: 'left',
style: { width: 250, display: 'flex' },
},
parameters: {
docs: {
description: {
story:
'When `truncate="left"`, the left content is truncated with ellipsis.',
},
},
},
};
export const TruncateRight: Story = {
args: {
left: 'Short Label',
right:
'This is a very long value that should be truncated on the right side',
truncate: 'right',
style: { width: 250, display: 'flex' },
},
parameters: {
docs: {
description: {
story:
'When `truncate="right"`, the right content is truncated with ellipsis.',
},
},
},
};
export const FinancialAmount: Story = {
args: {
left: 'Groceries',
right: '$1,234.56',
style: { width: 300, display: 'flex' },
rightStyle: { fontWeight: 'bold' },
},
parameters: {
docs: {
description: {
story:
'Example showing AlignedText used for displaying financial data.',
},
},
},
};
export const WithCustomStyles: Story = {
args: {
left: 'Category',
right: 'Amount',
style: {
width: 300,
padding: 10,
backgroundColor: '#f5f5f5',
borderRadius: 4,
display: 'flex',
},
leftStyle: { color: '#666', fontStyle: 'italic' },
rightStyle: { color: '#333', fontWeight: 'bold' },
},
};
export const MultipleRows: Story = {
args: {
left: 'Income',
right: '$5,000.00',
},
render: () => (
<div
style={{ width: 300, display: 'flex', flexDirection: 'column', gap: 8 }}
>
<AlignedText
left="Income"
right="$5,000.00"
rightStyle={{ color: 'green' }}
style={{ display: 'flex' }}
/>
<AlignedText
left="Expenses"
right="-$3,200.00"
rightStyle={{ color: 'red' }}
style={{ display: 'flex' }}
/>
<AlignedText
left="Balance"
right="$1,800.00"
style={{ borderTop: '1px solid #ccc', paddingTop: 8, display: 'flex' }}
rightStyle={{ fontWeight: 'bold' }}
/>
</div>
),
parameters: {
docs: {
description: {
story:
'Multiple AlignedText components stacked to create a summary view.',
},
},
},
};

View File

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

View File

@@ -1,111 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Block } from './Block';
import { theme } from './theme';
const meta = {
title: 'Block',
component: Block,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Block>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'This is a Block component',
},
parameters: {
docs: {
description: {
story:
'Block is a basic div wrapper that accepts Emotion CSS styles via the `style` prop.',
},
},
},
tags: ['autodocs'],
};
export const WithStyles: Story = {
args: {
children: 'Styled Block',
style: {
padding: 20,
backgroundColor: theme.cardBackground,
borderRadius: 8,
border: `1px solid ${theme.cardBorder}`,
color: theme.pageText,
},
},
};
export const WithFlexLayout: Story = {
render: () => (
<Block
style={{
display: 'flex',
gap: 10,
padding: 15,
borderRadius: 4,
color: theme.pageText,
}}
>
<Block
style={{
padding: 10,
backgroundColor: theme.cardBackground,
borderRadius: 4,
border: `1px solid ${theme.cardBorder}`,
}}
>
Item 1
</Block>
<Block
style={{
padding: 10,
backgroundColor: theme.cardBackground,
borderRadius: 4,
border: `1px solid ${theme.cardBorder}`,
}}
>
Item 2
</Block>
<Block
style={{
padding: 10,
backgroundColor: theme.cardBackground,
borderRadius: 4,
border: `1px solid ${theme.cardBorder}`,
}}
>
Item 3
</Block>
</Block>
),
parameters: {
docs: {
description: {
story: 'Block components can be nested and styled with flexbox.',
},
},
},
};
export const AsContainer: Story = {
args: {
children: 'Container Block',
style: {
width: 300,
padding: 25,
textAlign: 'center',
backgroundColor: theme.cardBackground,
border: `2px dashed ${theme.cardBorder}`,
borderRadius: 8,
color: theme.pageText,
},
},
};

View File

@@ -1,8 +1,8 @@
import type { HTMLProps, Ref } from 'react';
import { type HTMLProps, type Ref } from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type BlockProps = HTMLProps<HTMLDivElement> & {
innerRef?: Ref<HTMLDivElement>;

View File

@@ -1,99 +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>;
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

@@ -1,5 +1,10 @@
import React, { forwardRef, useMemo } from 'react';
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
import React, {
forwardRef,
useMemo,
type ComponentPropsWithoutRef,
type ReactNode,
type CSSProperties,
} from 'react';
import { Button as ReactAriaButton } from 'react-aria-components';
import { css, cx } from '@emotion/css';

View File

@@ -1,82 +0,0 @@
import { styles } from '@actual-app/components/styles';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Card } from './Card';
import { Paragraph } from './Paragraph';
import { theme } from './theme';
const meta = {
title: 'Card',
component: Card,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Card content goes here',
style: {
padding: 20,
width: 300,
color: theme.pageText,
},
},
parameters: {
docs: {
description: {
story: `
Default Card component uses the following theme CSS variables:
- \`--color-cardBackground\`
- \`--color-cardBorder\`
`,
},
},
},
};
export const WithCustomContent: Story = {
args: {
style: {
padding: 20,
width: 300,
color: theme.pageText,
},
},
render: args => (
<Card {...args}>
<h3 style={{ ...styles.largeText }}>Card Title</h3>
<Paragraph style={{ margin: 0 }}>
This is a card with more complex content including a title and
paragraph.
</Paragraph>
</Card>
),
};
export const Narrow: Story = {
args: {
children: 'Narrow card',
style: {
padding: 15,
width: 150,
color: theme.pageText,
},
},
};
export const Wide: Story = {
args: {
children: 'Wide card with more content space',
style: {
padding: 25,
width: 500,
color: theme.pageText,
},
},
};

View File

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

View File

@@ -1,108 +0,0 @@
import { useState } from 'react';
import { ColorSwatch } from 'react-aria-components';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Button } from './Button';
import { ColorPicker } from './ColorPicker';
const meta = {
title: 'ColorPicker',
component: ColorPicker,
parameters: {
layout: 'centered',
},
args: {
onChange: fn(),
children: <Button>Pick a color</Button>,
},
tags: ['autodocs'],
} satisfies Meta<typeof ColorPicker>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
defaultValue: '#690CB0',
children: <Button>Pick a color</Button>,
},
};
export const WithColorSwatch: Story = {
args: {
defaultValue: '#1976D2',
children: (
<Button style={{ padding: 4 }}>
<ColorSwatch
style={{
width: 24,
height: 24,
borderRadius: 4,
boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)',
}}
/>
</Button>
),
},
};
export const CustomColorSet: Story = {
args: {
defaultValue: '#FF0000',
columns: 4,
colorset: [
'#FF0000',
'#00FF00',
'#0000FF',
'#FFFF00',
'#FF00FF',
'#00FFFF',
'#FFA500',
'#800080',
],
children: <Button>Custom Colors</Button>,
},
parameters: {
docs: {
description: {
story:
'ColorPicker with a custom color set and different column layout.',
},
},
},
};
export const Controlled: Story = {
args: {
children: <Button>Pick a color</Button>,
},
render: () => {
const [color, setColor] = useState('#388E3C');
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<ColorPicker value={color} onChange={c => setColor(c.toString('hex'))}>
<Button style={{ padding: 4 }}>
<ColorSwatch
style={{
width: 24,
height: 24,
borderRadius: 4,
}}
/>
</Button>
</ColorPicker>
<span>Selected: {color}</span>
</div>
);
},
parameters: {
docs: {
description: {
story: 'Controlled ColorPicker with external state management.',
},
},
},
};

View File

@@ -1,18 +1,16 @@
import type { ChangeEvent, ReactNode } from 'react';
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,
ColorSwatch as AriaColorSwatch,
type ColorSwatchProps,
ColorSwatchPicker as AriaColorSwatchPicker,
ColorSwatchPickerItem,
ColorField,
parseColor,
} from 'react-aria-components';
import type {
ColorPickerProps as AriaColorPickerProps,
ColorSwatchProps,
} from 'react-aria-components';
import { css } from '@emotion/css';

View File

@@ -1,90 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { FormError } from './FormError';
import { Input } from './Input';
import { View } from './View';
const meta = {
title: 'FormError',
component: FormError,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof FormError>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'This field is required',
},
parameters: {
docs: {
description: {
story: 'FormError displays validation error messages in red text.',
},
},
},
};
export const InFormContext: Story = {
render: () => (
<View
style={{ display: 'flex', flexDirection: 'column', gap: 5, width: 250 }}
>
<Input placeholder="Email address" style={{ borderColor: 'red' }} />
<FormError>Please enter a valid email address</FormError>
</View>
),
parameters: {
docs: {
description: {
story:
'FormError displayed below an input field with validation error.',
},
},
},
};
export const MultipleErrors: Story = {
render: () => (
<View style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<FormError>Password must be at least 8 characters</FormError>
<FormError>Password must contain a number</FormError>
<FormError>Password must contain a special character</FormError>
</View>
),
parameters: {
docs: {
description: {
story:
'Multiple FormError components for displaying several validation errors.',
},
},
},
};
export const CustomStyle: Story = {
args: {
children: 'Custom styled error message',
style: {
fontSize: 14,
fontWeight: 'bold',
padding: 10,
backgroundColor: '#ffebee',
borderRadius: 4,
border: '1px solid red',
},
},
};
export const LongErrorMessage: Story = {
args: {
children:
'This is a longer error message that explains the validation issue in more detail. Please correct the input and try again.',
style: { maxWidth: 300 },
},
};

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import { forwardRef } from 'react';
import type { Ref } from 'react';
import { forwardRef, type Ref } from 'react';
import { render } from '@testing-library/react';

View File

@@ -1,8 +1,8 @@
import type { ReactNode } from 'react';
import { type ReactNode } from 'react';
import { css } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type InlineFieldProps = {
label: ReactNode;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import type {
ChangeEvent,
ComponentPropsWithRef,
FocusEvent,
KeyboardEvent,
import React, {
type ChangeEvent,
type ComponentPropsWithRef,
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,5 +1,4 @@
import { forwardRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { forwardRef, type ReactNode, type CSSProperties } from 'react';
import { styles } from './styles';
import { Text } from './Text';

View File

@@ -1,11 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import type {
ComponentProps,
ComponentType,
CSSProperties,
KeyboardEvent,
ReactNode,
SVGProps,
import {
type ReactNode,
useState,
type ComponentProps,
type ComponentType,
type SVGProps,
type CSSProperties,
type KeyboardEvent,
useEffect,
useRef,
} from 'react';
import { Button } from './Button';
@@ -150,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,8 +1,8 @@
import type { HTMLProps } from 'react';
import { type HTMLProps } from 'react';
import { css } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type ParagraphProps = HTMLProps<HTMLDivElement> & {
style?: CSSProperties;

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import 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,5 +1,4 @@
import { useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { useRef, useState, type CSSProperties } from 'react';
import { Button } from './Button';
import { SvgExpandArrow } from './icons/v0';

View File

@@ -1,7 +1,6 @@
import React from 'react';
import type { ReactNode } from 'react';
import React, { type ReactNode } from 'react';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
import { View } from './View';
type SpaceBetweenProps = {

View File

@@ -1,9 +1,13 @@
import React, { forwardRef } from 'react';
import type { HTMLProps, ReactNode, Ref } from 'react';
import React, {
type HTMLProps,
type Ref,
type ReactNode,
forwardRef,
} from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type TextProps = HTMLProps<HTMLSpanElement> & {
innerRef?: Ref<HTMLSpanElement>;

View File

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

View File

@@ -1,5 +1,4 @@
import React from 'react';
import type { CSSProperties } from 'react';
import React, { type CSSProperties } from 'react';
import { css } from '@emotion/css';

View File

@@ -1,5 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentProps, ReactNode } from 'react';
import React, {
useCallback,
useEffect,
useRef,
useState,
type ComponentProps,
type ReactNode,
} from 'react';
import { Tooltip as AriaTooltip, TooltipTrigger } from 'react-aria-components';
import { styles } from './styles';

View File

@@ -1,9 +1,8 @@
import React, { forwardRef } from 'react';
import type { HTMLProps, Ref } from 'react';
import React, { forwardRef, type HTMLProps, type Ref } from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type ViewProps = HTMLProps<HTMLDivElement> & {
className?: string;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import type { SVGProps } from 'react';
import React, { type SVGProps } from 'react';
import { css, keyframes } from '@emotion/css';

View File

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

View File

@@ -1,4 +1,4 @@
import type { Config } from '@svgr/core';
import { type Config } from '@svgr/core';
const tmpl: Config['template'] = (
{ imports, interfaces, componentName, props, jsx },

View File

@@ -12,7 +12,8 @@ const shadowLarge = {
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
};
export const styles: CSSProperties = {
// oxlint-disable-next-line typescript/no-explicit-any
export const styles: Record<string, any> = {
incomeHeaderHeight: 70,
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
monthRightPadding: 5,
@@ -90,10 +91,7 @@ export const styles: CSSProperties = {
},
shadowLarge,
tnum: {
// tnum: Tabular numbers
// ss01: Open digits
// ss04: Disambiguation w/o zero
fontFeatureSettings: '"tnum", "ss01", "ss04"',
fontFeatureSettings: '"tnum"',
},
notFixed: { fontFeatureSettings: '' },
text: {
@@ -161,11 +159,4 @@ export const styles: CSSProperties = {
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

@@ -24,6 +24,6 @@
"protoc-gen-js": "3.21.4-4",
"ts-protoc-gen": "0.15.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"vitest": "^4.0.9"
}
}

View File

@@ -7,7 +7,7 @@
// * Need to check to make sure if account exists when handling
// * transaction changes in syncing
import type { Timestamp } from './timestamp';
import { type Timestamp } from './timestamp';
/**
* Represents a node within a trinary radix trie.

View File

@@ -1,7 +1,7 @@
import murmurhash from 'murmurhash';
import { v4 as uuidv4 } from 'uuid';
import type { TrieNode } from './merkle';
import { type TrieNode } from './merkle';
/**
* Hybrid Unique Logical Clock (HULC) timestamp generator

View File

@@ -65,10 +65,10 @@ Run manually:
```sh
# Run docker container
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
# Use the ip and port noted earlier

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,9 +1,9 @@
import { join } from 'path';
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import type { AccountPage } from './page-models/account-page';
import { type AccountPage } from './page-models/account-page';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 106 KiB

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