Compare commits
1 Commits
v25.6.0
...
mobile-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4656102c69 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Bug Report
|
||||
description: File a bug report also known as an issue or problem.
|
||||
title: '[Bug]: '
|
||||
labels: ['needs triage', 'bug']
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
|
||||
8
.github/actions/setup/action.yml
vendored
@@ -16,21 +16,17 @@ runs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18.16.0
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
if: ${{ env.ACT }}
|
||||
- name: Get Node version
|
||||
id: get-node
|
||||
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
|
||||
4
.github/workflows/build.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:browser
|
||||
run: ./bin/package-browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Server
|
||||
run: yarn workspace @actual-app/sync-server build
|
||||
run: cd packages/sync-server && yarn build
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/check.yml
vendored
@@ -43,6 +43,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
2
.github/workflows/docker-edge.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
|
||||
2
.github/workflows/docker-release.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
|
||||
6
.github/workflows/e2e-test.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
|
||||
57
.github/workflows/electron-master.yml
vendored
@@ -77,68 +77,13 @@ jobs:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Process release version
|
||||
id: process_version
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Add to new release
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
|
||||
|
||||
## Desktop releases
|
||||
Please note: Microsoft store updates can sometimes lag behind the main release by a couple of days while they verify the new version.
|
||||
|
||||
<a href="https://apps.microsoft.com/detail/9p2hmlhsdbrm?cid=Github+Releases&mode=direct">
|
||||
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
|
||||
</a>
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
|
||||
publish-microsoft-store:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Install StoreBroker
|
||||
shell: powershell
|
||||
run: |
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: actual-electron-windows-latest-appx
|
||||
|
||||
- name: Submit to Microsoft Store
|
||||
shell: powershell
|
||||
run: |
|
||||
# Disable telemetry
|
||||
$global:SBDisableTelemetry = $true
|
||||
|
||||
# Authenticate against the store
|
||||
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
|
||||
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
|
||||
|
||||
# Zip and create metadata files
|
||||
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
|
||||
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
|
||||
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
|
||||
|
||||
# Submit the app
|
||||
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
|
||||
Update-ApplicationSubmission `
|
||||
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
|
||||
-SubmissionDataPath "submission.json" `
|
||||
-PackagePath "submission.zip" `
|
||||
-ReplacePackages `
|
||||
-NoStatus `
|
||||
-AutoCommit `
|
||||
-Force
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
run: node .github/actions/handle-feature-requests.js
|
||||
env:
|
||||
|
||||
2
.github/workflows/netlify-release.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
- name: Build Actual
|
||||
run: yarn build:browser
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify_deploy
|
||||
|
||||
4
.github/workflows/publish-npm-packages.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
run: yarn build:browser
|
||||
|
||||
- name: Pack the web and server packages
|
||||
run: |
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
|
||||
2
.github/workflows/update-vrt.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
diff --git a/methods/inflater.js b/methods/inflater.js
|
||||
index 8769e66e82b25541aba80b1ac6429199c9a8179f..1d4402402f0e1aaf64062c1f004c3d6e6fe93e76 100644
|
||||
--- a/methods/inflater.js
|
||||
+++ b/methods/inflater.js
|
||||
@@ -1,4 +1,4 @@
|
||||
-const version = +(process.versions ? process.versions.node : "").split(".")[0] || 0;
|
||||
+const version = +(process?.versions?.node ?? "").split(".")[0] || 0;
|
||||
|
||||
module.exports = function (/*Buffer*/ inbuf, /*number*/ expectedLength) {
|
||||
var zlib = require("zlib");
|
||||
935
.yarn/releases/yarn-4.7.0.cjs
vendored
Executable file
948
.yarn/releases/yarn-4.9.1.cjs
vendored
@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.7.0.cjs
|
||||
|
||||
@@ -45,7 +45,6 @@ yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
# required for running the sync-server server
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ async function run() {
|
||||
message: 'Release Note Type',
|
||||
type: 'select',
|
||||
choices: [
|
||||
{ title: '✨ Features', value: 'Features' },
|
||||
{ title: '👍 Enhancements', value: 'Enhancements' },
|
||||
{ title: '🐛 Bugfix', value: 'Bugfix' },
|
||||
{ title: '⚙️ Maintenance', value: 'Maintenance' },
|
||||
{ title: 'Features', value: 'Features' },
|
||||
{ title: 'Enhancements', value: 'Enhancements' },
|
||||
{ title: 'Bugfix', value: 'Bugfix' },
|
||||
{ title: 'Maintenance', value: 'Maintenance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.52.0-jammy /bin/bash \
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
|
||||
@@ -9,7 +9,6 @@ import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginRulesDir from 'eslint-plugin-rulesdir';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
@@ -85,7 +84,8 @@ const confusingBrowserGlobals = [
|
||||
'top',
|
||||
];
|
||||
|
||||
export default pluginTypescript.config(
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
@@ -120,8 +120,8 @@ export default pluginTypescript.config(
|
||||
{
|
||||
// Temporary until the sync-server is migrated to TypeScript
|
||||
files: [
|
||||
'packages/sync-server/**/*.spec.{js,jsx}',
|
||||
'packages/sync-server/**/*.test.{js,jsx}',
|
||||
'packages/sync-server/**/*.spec.js?(x)',
|
||||
'packages/sync-server/**/*.test.js?(x)',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
@@ -164,14 +164,13 @@ export default pluginTypescript.config(
|
||||
},
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginTypescript.configs.recommended,
|
||||
...pluginTypescript.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
rulesdir: pluginRulesDir,
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -540,7 +539,7 @@ export default pluginTypescript.config(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
files: ['**/*.ts?(x)'],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
@@ -608,16 +607,6 @@ export default pluginTypescript.config(
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
|
||||
rules: {
|
||||
'typescript-paths/absolute-parent-import': [
|
||||
'error',
|
||||
{ preferPathOverBaseUrl: true },
|
||||
],
|
||||
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
@@ -883,4 +872,4 @@ export default pluginTypescript.config(
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
];
|
||||
|
||||
35
package.json
@@ -31,7 +31,7 @@
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:server": "yarn build:browser",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
@@ -53,40 +53,37 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-import-resolver-typescript": "^4.2.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^15.15.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"globals": "^15.13.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"node-jq": "^6.0.1",
|
||||
"lint-staged": "^15.5.0",
|
||||
"node-jq": "^4.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.5.3",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.40.1",
|
||||
"socks": ">=2.8.3"
|
||||
"rollup": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"yarn": "^4.9.1"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,json,yml}": [
|
||||
@@ -94,7 +91,7 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
@@ -52,18 +52,10 @@ export async function batchBudgetUpdates(func) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `aqlQuery` instead.
|
||||
* This function will be removed in a future release.
|
||||
*/
|
||||
export function runQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function aqlQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.6.0",
|
||||
"version": "25.4.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
@@ -24,15 +24,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"tsc-alias": "^1.8.11",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.8.0",
|
||||
"react-aria-components": "^1.7.1",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react": "^19.1.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"vitest": "^3.1.3"
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
@@ -51,8 +50,6 @@
|
||||
"./view": "./src/View.tsx"
|
||||
},
|
||||
"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 -c vitest.web.config.ts"
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . ."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
} from 'react';
|
||||
import { Button as ReactAriaButton } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AnimatedLoading } from './icons/AnimatedLoading';
|
||||
import { styles } from './styles';
|
||||
@@ -145,24 +145,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
const defaultButtonClassName: string = useMemo(
|
||||
() =>
|
||||
css({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
padding: _getPadding(variant),
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: backgroundColor[variantWithDisabled],
|
||||
border: _getBorder(variant, variantWithDisabled),
|
||||
color: textColor[variantWithDisabled],
|
||||
transition: 'box-shadow .25s',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...styles.smallText,
|
||||
'&[data-hovered]': _getHoveredStyles(variant),
|
||||
'&[data-pressed]': _getActiveStyles(variant, bounce),
|
||||
}),
|
||||
String(
|
||||
css({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
padding: _getPadding(variant),
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: backgroundColor[variantWithDisabled],
|
||||
border: _getBorder(variant, variantWithDisabled),
|
||||
color: textColor[variantWithDisabled],
|
||||
transition: 'box-shadow .25s',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...styles.smallText,
|
||||
'&[data-hovered]': _getHoveredStyles(variant),
|
||||
'&[data-pressed]': _getActiveStyles(variant, bounce),
|
||||
}),
|
||||
),
|
||||
[bounce, variant, variantWithDisabled],
|
||||
);
|
||||
|
||||
@@ -174,8 +176,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{...restProps}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultButtonClassName, className(renderProps))
|
||||
: cx(defaultButtonClassName, className)
|
||||
? renderProps =>
|
||||
`${defaultButtonClassName} ${className(renderProps)}`
|
||||
: `${defaultButtonClassName} ${className || ''}`
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,59 +1,36 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
type Ref,
|
||||
cloneElement,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
|
||||
*/
|
||||
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
|
||||
type InitialFocusProps = {
|
||||
children:
|
||||
| ReactElement<{ inputRef: Ref<HTMLInputElement> }>
|
||||
| ((node: Ref<HTMLInputElement>) => ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* InitialFocus sets focus on its child element
|
||||
* when it mounts.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
|
||||
*/
|
||||
export function InitialFocus<T extends HTMLElement = HTMLElement>({
|
||||
children,
|
||||
}: InitialFocusProps<T>) {
|
||||
const ref = useRef<T | null>(null);
|
||||
export function InitialFocus({ children }: InitialFocusProps) {
|
||||
const node = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
if (node.current) {
|
||||
// This is needed to avoid a strange interaction with
|
||||
// `ScopeTab`, which doesn't allow it to be focused at first for
|
||||
// some reason. Need to look into it.
|
||||
setTimeout(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
if (
|
||||
ref.current instanceof HTMLInputElement ||
|
||||
ref.current instanceof HTMLTextAreaElement
|
||||
) {
|
||||
ref.current.setSelectionRange(0, 10000);
|
||||
}
|
||||
if (node.current) {
|
||||
node.current.focus();
|
||||
node.current.setSelectionRange(0, 10000);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children(ref);
|
||||
return children(node);
|
||||
}
|
||||
|
||||
const child = Children.only(children);
|
||||
if (isValidElement(child)) {
|
||||
return cloneElement(child, { ref });
|
||||
}
|
||||
throw new Error(
|
||||
'InitialFocus expects a single valid React element as its child.',
|
||||
);
|
||||
return cloneElement(children, { inputRef: node });
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { InitialFocus } from './InitialFocus';
|
||||
import { View } from './View';
|
||||
|
||||
describe('InitialFocus', () => {
|
||||
it('should focus a text input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
|
||||
it('should focus a textarea', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<textarea title="focused" />
|
||||
</InitialFocus>
|
||||
<textarea title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const textarea = component.getByTitle('focused') as HTMLTextAreaElement;
|
||||
const unfocusedTextarea = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
expect(document.activeElement).not.toBe(unfocusedTextarea);
|
||||
});
|
||||
|
||||
it('should select text in an input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" defaultValue="Hello World" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(input.selectionStart).toBe(0);
|
||||
expect(input.selectionEnd).toBe(11); // Length of "Hello World"
|
||||
});
|
||||
|
||||
it('should focus a button', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<button title="focused">Click me</button>
|
||||
</InitialFocus>
|
||||
<button title="unfocused">Do not click me</button>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const button = component.getByTitle('focused') as HTMLButtonElement;
|
||||
const unfocusedButton = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLButtonElement;
|
||||
expect(document.activeElement).toBe(button);
|
||||
expect(document.activeElement).not.toBe(unfocusedButton);
|
||||
});
|
||||
|
||||
it('should focus a custom component with ref forwarding', async () => {
|
||||
const CustomInput = forwardRef<HTMLInputElement>((props, ref) => (
|
||||
<input type="text" ref={ref} {...props} title="focused" />
|
||||
));
|
||||
CustomInput.displayName = 'CustomInput';
|
||||
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
{node => <CustomInput ref={node as Ref<HTMLInputElement>} />}
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentPropsWithRef,
|
||||
type InputHTMLAttributes,
|
||||
type KeyboardEvent,
|
||||
type FocusEvent,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
import { Input as ReactAriaInput } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { useResponsive } from './hooks/useResponsive';
|
||||
import { styles } from './styles';
|
||||
import { styles, type CSSProperties } from './styles';
|
||||
import { theme } from './theme';
|
||||
|
||||
export const baseInputStyle = {
|
||||
export const defaultInputStyle = {
|
||||
outline: 0,
|
||||
backgroundColor: theme.tableBackground,
|
||||
color: theme.formInputText,
|
||||
@@ -22,91 +20,85 @@ export const baseInputStyle = {
|
||||
border: '1px solid ' + theme.formInputBorder,
|
||||
};
|
||||
|
||||
const defaultInputClassName = css({
|
||||
...baseInputStyle,
|
||||
color: theme.formInputText,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
'&[data-focused]': {
|
||||
border: '1px solid ' + theme.formInputBorderSelected,
|
||||
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
|
||||
},
|
||||
'&[data-disabled]': {
|
||||
color: theme.formInputTextPlaceholder,
|
||||
},
|
||||
'::placeholder': { color: theme.formInputTextPlaceholder },
|
||||
...styles.smallText,
|
||||
});
|
||||
|
||||
export type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
|
||||
onEnter?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEscape?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onChangeValue?: (
|
||||
newValue: string,
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => void;
|
||||
onUpdate?: (newValue: string, event: FocusEvent<HTMLInputElement>) => void;
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
style?: CSSProperties;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEscape?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onChangeValue?: (newValue: string) => void;
|
||||
onUpdate?: (newValue: string) => void;
|
||||
};
|
||||
|
||||
export function Input({
|
||||
ref,
|
||||
style,
|
||||
inputRef,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onChangeValue,
|
||||
onUpdate,
|
||||
className,
|
||||
...props
|
||||
...nativeProps
|
||||
}: InputProps) {
|
||||
return (
|
||||
<ReactAriaInput
|
||||
ref={ref}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultInputClassName, className(renderProps))
|
||||
: cx(defaultInputClassName, className)
|
||||
}
|
||||
{...props}
|
||||
onKeyUp={e => {
|
||||
props.onKeyUp?.(e);
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={cx(
|
||||
css(
|
||||
defaultInputStyle,
|
||||
{
|
||||
color: nativeProps.disabled
|
||||
? theme.formInputTextPlaceholder
|
||||
: theme.formInputText,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
':focus': {
|
||||
border: '1px solid ' + theme.formInputBorderSelected,
|
||||
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
|
||||
},
|
||||
'::placeholder': { color: theme.formInputTextPlaceholder },
|
||||
},
|
||||
styles.smallText,
|
||||
style,
|
||||
),
|
||||
className,
|
||||
)}
|
||||
{...nativeProps}
|
||||
onKeyDown={e => {
|
||||
nativeProps.onKeyDown?.(e);
|
||||
|
||||
if (e.key === 'Enter' && onEnter) {
|
||||
onEnter(e.currentTarget.value, e);
|
||||
onEnter(e);
|
||||
}
|
||||
|
||||
if (e.key === 'Escape' && onEscape) {
|
||||
onEscape(e.currentTarget.value, e);
|
||||
onEscape(e);
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
onUpdate?.(e.currentTarget.value, e);
|
||||
props.onBlur?.(e);
|
||||
onUpdate?.(e.target.value);
|
||||
nativeProps.onBlur?.(e);
|
||||
}}
|
||||
onChange={e => {
|
||||
onChangeValue?.(e.currentTarget.value, e);
|
||||
props.onChange?.(e);
|
||||
onChangeValue?.(e.target.value);
|
||||
nativeProps.onChange?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultBigInputClassName = css({
|
||||
padding: 10,
|
||||
fontSize: 15,
|
||||
border: 'none',
|
||||
...styles.shadow,
|
||||
'&[data-focused]': { border: 'none', ...styles.shadow },
|
||||
});
|
||||
|
||||
export function BigInput({ className, ...props }: InputProps) {
|
||||
export function BigInput(props: InputProps) {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultBigInputClassName, className(renderProps))
|
||||
: cx(defaultBigInputClassName, className)
|
||||
}
|
||||
style={{
|
||||
padding: 10,
|
||||
fontSize: 15,
|
||||
border: 'none',
|
||||
...styles.shadow,
|
||||
':focus': { border: 'none', ...styles.shadow },
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const resolveExtensions = [
|
||||
'.testing.ts',
|
||||
'.web.ts',
|
||||
'.mjs',
|
||||
'.js',
|
||||
'.mts',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.json',
|
||||
'.wasm',
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve('../../../crdt/src$1'),
|
||||
},
|
||||
],
|
||||
extensions: resolveExtensions,
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
});
|
||||
@@ -15,14 +15,14 @@
|
||||
"test": "vitest --globals"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"google-protobuf": "^3.12.4",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-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.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-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
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |