[AI] Implement standard sizes on Button, Text and Input components (#9002)

* [AI] Add standard size system to Button, Text and Input components

* [AI] Migrate modals, settings, rules, schedules and sidebar text to standard sizes

* [AI] Migrate mobile, budget, manager, banksync and transactions text to standard sizes

* [AI] Migrate reports, formula and news text to standard sizes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #9002

* Revert "Update VRT screenshots"

This reverts commit 35c947ef54.

* [AI] Fix line-height rendering as px strings in size tokens

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #9002

* [AI] Tighten bank sync mobile row gap so larger labels fit on one line

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #9002

* [AI] Restore amount display size to preserve underline spacing

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #9002

* [AI] Pass caller-provided size through SingleAutocomplete render input

* [AI] Omit forced line-height on medium-sized controls to match the default look

* [AI] Document standard component sizes in DESIGN.md

* [AI] Adopt sidebar spacing tokens where values match exactly

* [AI] Scope the mobile touch-target rule to touch-primary control sizes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
flafleur
2026-10-05 16:41:38 +00:00
committed by GitHub
co-authored by github-actions[bot]
parent c72ce699b3
commit 8e165c0eb6
117 changed files with 850 additions and 173 deletions
+24
View File
@@ -135,6 +135,30 @@ All hex values in this file (frontmatter included) are the light-theme palette m
- **Body** (400, 16px): Default text.
- **Label** (400, 13px): Table meta, menus, dense secondary text; 12px and 10px steps exist for the tightest spots.
### Standard Component Sizes
`Button`, `Text` and `Input` accept a `size` prop: `small` | `medium` | `large` | `extra-large` (the `ComponentSize` vocabulary in `packages/component-library/src/tokens.ts`). Omitting the prop renders exactly as before — fully backward compatible. Sizes are responsive: they step up one notch on mobile/narrow for readability and touch targets, and collapse to the compact desktop scale from 512px/730px up. Breakpoint groups mirror the `useResponsive` view modes: narrow <512, small 512–729, medium 730–1099, wide ≥1100.
Text values (fontSize / lineHeight, px):
| Size | narrow | small | medium | wide |
| ----------- | ------- | ------- | ------- | ------- |
| small | 12 / 16 | 12 / 16 | 12 / 16 | 12 / 16 |
| medium | 13 / 18 | 13 / 18 | 13 / 18 | 13 / 18 |
| large | 16 / 22 | 15 / 20 | 15 / 20 | 15 / 20 |
| extra-large | 17 / 24 | 17 / 24 | 16 / 22 | 16 / 22 |
Control values for Button/Input (paddingY / paddingX / min-height, px):
| Size | narrow | small | medium | wide |
| ----------- | ------------ | ----------- | ----------- | ----------- |
| small | 3 / 8 / 24 | 3 / 8 / 24 | 3 / 8 / 24 | 3 / 8 / 24 |
| medium | 5 / 10 / — | 5 / 10 / — | 5 / 10 / — | 5 / 10 / — |
| large | 8 / 12 / 36 | 6 / 12 / 32 | 6 / 12 / 32 | 6 / 12 / 32 |
| extra-large | 10 / 14 / 40 | 8 / 12 / 36 | 8 / 12 / 36 | 8 / 12 / 36 |
`size="medium"` reproduces today's default control look exactly (5px/10px padding, 13px text, no min-height) and, on controls, inherits line-height like the default; the other sizes apply their scale line-height. `Text` always applies its size's line-height, while the omitted prop forces no line-height at all. `extra-large` narrow hits the 40px mobile touch target. The touch-target guarantee applies to touch-primary controls: at narrow, use `large` or `extra-large` for controls the user taps; `small` stays the compact 24px desktop-density choice at every breakpoint and is not meant for touch-primary controls.
### Named Rules
**The Tabular Number Rule.** Every standalone financial figure renders with `font-feature-settings: "tnum", "ss01", "ss04"` (via `FinancialText` or `styles.tnum`) so digits align in columns and disambiguate. A proportional-figure money column is a bug.
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
import { View } from './View';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
},
parameters: {
docs: {
description: {
story:
'A basic button. Without an explicit size it keeps the legacy compact look.',
},
},
},
};
export const Variants: Story = {
render: () => (
<View style={{ flexDirection: 'row', gap: 10 }}>
<Button>Normal</Button>
<Button variant="primary">Primary</Button>
<Button variant="bare">Bare</Button>
</View>
),
parameters: {
docs: {
description: {
story: 'The available button variants: normal, primary and bare.',
},
},
},
};
export const Sizes: Story = {
render: () => (
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
<Button size="extra-large">Extra Large</Button>
</View>
),
parameters: {
docs: {
description: {
story:
'Standard sizes. Values adapt to the viewport: resize the story canvas below 512px, 730px and 1100px to see the responsive steps.',
},
},
},
};
export const SizeVariantGrid: Story = {
render: () => (
<View style={{ gap: 10 }}>
{(['small', 'medium', 'large', 'extra-large'] as const).map(size => (
<View
key={size}
style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}
>
<View style={{ width: 90 }}>
{size === 'extra-large' ? 'extra-large' : size}
</View>
<Button size={size}>Normal</Button>
<Button size={size} variant="primary">
Primary
</Button>
<Button size={size} variant="bare">
Bare
</Button>
</View>
))}
</View>
),
parameters: {
docs: {
description: {
story: 'Every size across the normal, primary and bare variants.',
},
},
},
};
export const Disabled: Story = {
args: {
children: 'Disabled button',
isDisabled: true,
},
parameters: {
docs: {
description: {
story: 'Disabled buttons prevent user interaction.',
},
},
},
};
+78 -3
View File
@@ -7,6 +7,12 @@ import { css, cx } from '@emotion/css';
import { AnimatedLoading } from './icons/AnimatedLoading';
import { styles } from './styles';
import { theme } from './theme';
import {
componentSizeControl,
componentSizeText,
sizeResponsiveStyles,
type ComponentSize,
} from './tokens';
import { View } from './View';
const backgroundColor: {
@@ -123,8 +129,66 @@ const _getActiveStyles = (
}
};
type ButtonProps = ComponentPropsWithoutRef<typeof ReactAriaButton> & {
// Builds the padding/typography overrides for an explicit `size`, varying
// by breakpoint via media queries (no per-instance hooks). Bare buttons
// use uniform padding; framed buttons use the paddingX/paddingY split.
const getButtonSizeOverrides = (
size: ComponentSize,
isBare: boolean,
): Record<string, unknown> => {
const control = componentSizeControl[size];
const text = componentSizeText[size];
const getGroupStyles = (group: keyof typeof control) => {
const { paddingY, paddingX, minHeight } = control[group];
const { fontSize, lineHeight } = text[group];
return {
padding: isBare ? `${paddingY}px` : `${paddingY}px ${paddingX}px`,
...(minHeight != null ? { minHeight } : null),
fontSize,
// Medium matches today's default look, which inherits line-height;
// only the other sizes force their scale line-height.
...(size === 'medium' ? {} : { lineHeight }),
};
};
return sizeResponsiveStyles({
narrow: getGroupStyles('narrow'),
small: getGroupStyles('small'),
medium: getGroupStyles('medium'),
wide: getGroupStyles('wide'),
});
};
const buttonSizeOverrides: Record<
ComponentSize,
{ bare: Record<string, unknown>; framed: Record<string, unknown> }
> = {
small: {
bare: getButtonSizeOverrides('small', true),
framed: getButtonSizeOverrides('small', false),
},
medium: {
bare: getButtonSizeOverrides('medium', true),
framed: getButtonSizeOverrides('medium', false),
},
large: {
bare: getButtonSizeOverrides('large', true),
framed: getButtonSizeOverrides('large', false),
},
'extra-large': {
bare: getButtonSizeOverrides('extra-large', true),
framed: getButtonSizeOverrides('extra-large', false),
},
};
type ButtonProps = Omit<
ComponentPropsWithoutRef<typeof ReactAriaButton>,
'size'
> & {
variant?: ButtonVariant;
size?: ComponentSize;
bounce?: boolean;
children?: ReactNode;
};
@@ -133,11 +197,21 @@ type ButtonVariant = 'normal' | 'primary' | 'bare' | 'menu' | 'menuSelected';
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { children, variant = 'normal', bounce = true, ...restProps } = props;
const {
children,
variant = 'normal',
bounce = true,
size,
...restProps
} = props;
const variantWithDisabled: ButtonVariant | `${ButtonVariant}Disabled` =
props.isDisabled ? `${variant}Disabled` : variant;
const sizeOverrides = size
? buttonSizeOverrides[size][variant === 'bare' ? 'bare' : 'framed']
: null;
const defaultButtonClassName: string = useMemo(
() =>
css({
@@ -155,10 +229,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
transition: 'box-shadow .25s',
WebkitAppRegion: 'no-drag',
...styles.smallText,
...sizeOverrides,
'&[data-hovered]': _getHoveredStyles(variant),
'&[data-pressed]': _getActiveStyles(variant, bounce),
}),
[bounce, variant, variantWithDisabled],
[bounce, variant, variantWithDisabled, sizeOverrides],
);
const className = restProps.className;
@@ -0,0 +1,190 @@
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
import { BigInput, Input } from './Input';
import { Text } from './Text';
// jsdom's getComputedStyle does not evaluate media queries, so the base
// (narrow) values always win there. Responsive steps are asserted
// against the generated stylesheet instead: find the media block and
// check that one of the element's emotion classes declares the expected
// value inside it.
const getStyleText = () =>
Array.from(document.querySelectorAll('style'))
.map(style => style.textContent ?? '')
.join('');
const expectMediaDeclaration = (
element: Element,
media: string,
declaration: string,
) => {
const cssText = getStyleText();
const classCandidates = [...element.classList].filter(className =>
className.startsWith('css-'),
);
expect(classCandidates.length).toBeGreaterThan(0);
// Several emotion classes may carry the same media query, so scan
// every occurrence of it and check each following rule block.
let searchFrom = 0;
let isMatched = false;
for (;;) {
const mediaIndex = cssText.indexOf(media, searchFrom);
if (mediaIndex === -1) {
break;
}
const segment = cssText.slice(mediaIndex);
const nextMedia = segment.indexOf('@media', 1);
const block = nextMedia === -1 ? segment : segment.slice(0, nextMedia);
if (
classCandidates.some(
className =>
block.includes(`.${className}`) && block.includes(declaration),
)
) {
isMatched = true;
break;
}
searchFrom = mediaIndex + media.length;
}
expect(isMatched).toBe(true);
};
describe('Button size', () => {
it('keeps the legacy look when size is omitted', () => {
render(<Button>Default</Button>);
const button = screen.getByRole('button', { name: 'Default' });
const computed = getComputedStyle(button);
expect(computed.padding).toBe('5px 10px');
expect(computed.fontSize).toBe('13px');
});
it('applies small size styles', () => {
render(<Button size="small">Small</Button>);
const button = screen.getByRole('button', { name: 'Small' });
const computed = getComputedStyle(button);
expect(computed.padding).toBe('3px 8px');
expect(computed.fontSize).toBe('12px');
expect(computed.minHeight).toBe('24px');
});
it('applies extra-large size styles with responsive steps', () => {
render(<Button size="extra-large">Extra Large</Button>);
const button = screen.getByRole('button', { name: 'Extra Large' });
const computed = getComputedStyle(button);
// Base (narrow): comfortable mobile / tap-target scale.
expect(computed.fontSize).toBe('17px');
expect(computed.minHeight).toBe('40px');
// Steps down at the small and medium breakpoints.
expectMediaDeclaration(
button,
'@media (min-width: 512px)',
'min-height:36px',
);
expectMediaDeclaration(
button,
'@media (min-width: 730px)',
'font-size:16px',
);
});
it('uses uniform padding for bare buttons with an explicit size', () => {
render(
<Button size="large" variant="bare">
Bare
</Button>,
);
const button = screen.getByRole('button', { name: 'Bare' });
const computed = getComputedStyle(button);
expect(computed.padding).toBe('8px');
expectMediaDeclaration(button, '@media (min-width: 512px)', 'padding:6px');
});
});
describe('Text size', () => {
it('applies no font-size opinion when size is omitted', () => {
render(<Text data-testid="plain">Plain</Text>);
const plain = screen.getByTestId('plain');
const reference = plain.ownerDocument.createElement('span');
plain.ownerDocument.body.appendChild(reference);
expect(getComputedStyle(plain).fontSize).toBe(
getComputedStyle(reference).fontSize,
);
});
it('applies size styles when provided', () => {
render(<Text size="large">Large text</Text>);
const text = screen.getByText('Large text');
const computed = getComputedStyle(text);
// Base (narrow) value; steps down to 15px from the small breakpoint.
expect(computed.fontSize).toBe('16px');
expect(computed.lineHeight).toBe('22px');
expectMediaDeclaration(text, '@media (min-width: 512px)', 'font-size:15px');
});
});
describe('Input size', () => {
it('keeps the legacy look when size is omitted', () => {
render(<Input placeholder="Default" />);
const input = screen.getByPlaceholderText('Default');
const computed = getComputedStyle(input);
expect(computed.padding).toBe('5px');
expect(computed.fontSize).toBe('13px');
});
it('applies size styles when provided', () => {
render(<Input size="large" placeholder="Large" />);
const input = screen.getByPlaceholderText('Large');
const computed = getComputedStyle(input);
expect(computed.padding).toBe('8px');
expect(computed.fontSize).toBe('16px');
expectMediaDeclaration(input, '@media (min-width: 512px)', 'padding:6px');
});
it('keeps the legacy BigInput look when size is omitted', () => {
render(<BigInput placeholder="Big" />);
const input = screen.getByPlaceholderText('Big');
const computed = getComputedStyle(input);
expect(computed.padding).toBe('10px');
expect(computed.fontSize).toBe('15px');
expect(computed.borderStyle).toBe('none');
});
it('applies size styles to BigInput while keeping its chrome', () => {
render(<BigInput size="extra-large" placeholder="Big XL" />);
const input = screen.getByPlaceholderText('Big XL');
const computed = getComputedStyle(input);
expect(computed.padding).toBe('10px');
expect(computed.fontSize).toBe('17px');
expect(computed.borderStyle).toBe('none');
expectMediaDeclaration(input, '@media (min-width: 730px)', 'padding:8px');
});
});
@@ -213,3 +213,29 @@ export const PasswordInput: Story = {
},
},
};
export const Sizes: Story = {
render: () => (
<View style={{ gap: 10 }}>
<Input size="small" placeholder="Small" />
<Input size="medium" placeholder="Medium" />
<Input size="large" placeholder="Large" />
<Input size="extra-large" placeholder="Extra Large" />
</View>
),
decorators: [
Story => (
<View style={{ width: 250 }}>
<Story />
</View>
),
],
parameters: {
docs: {
description: {
story:
'The standard size vocabulary. Values adapt to the viewport: resize the story canvas below 512px, 730px and 1100px to see the responsive steps.',
},
},
},
};
+112 -25
View File
@@ -12,6 +12,12 @@ import { css, cx } from '@emotion/css';
import { useResponsive } from './hooks/useResponsive';
import { styles } from './styles';
import { theme } from './theme';
import {
componentSizeControl,
componentSizeText,
sizeResponsiveStyles,
type ComponentSize,
} from './tokens';
export const baseInputStyle = {
outline: 0,
@@ -23,24 +29,82 @@ export const baseInputStyle = {
border: '1px solid ' + theme.formInputBorder,
};
export 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,
});
// Builds the padding/typography overrides for an explicit `size`,
// varying by breakpoint via media queries (no hooks). Inputs use a
// uniform padding on all sides, like today's default.
const getInputSizeStyles = (size: ComponentSize): Record<string, unknown> => {
const control = componentSizeControl[size];
const text = componentSizeText[size];
export type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
const getGroupStyles = (group: keyof typeof control) => {
const { paddingY, minHeight } = control[group];
const { fontSize, lineHeight } = text[group];
return {
padding: paddingY,
...(minHeight != null ? { minHeight } : null),
fontSize,
// Medium matches today's default look, which inherits line-height;
// only the other sizes force their scale line-height.
...(size === 'medium' ? {} : { lineHeight }),
};
};
return sizeResponsiveStyles({
narrow: getGroupStyles('narrow'),
small: getGroupStyles('small'),
medium: getGroupStyles('medium'),
wide: getGroupStyles('wide'),
});
};
const inputSizeStyles = {
small: getInputSizeStyles('small'),
medium: getInputSizeStyles('medium'),
large: getInputSizeStyles('large'),
'extra-large': getInputSizeStyles('extra-large'),
} satisfies Record<ComponentSize, Record<string, unknown>>;
// Single source of truth for input class composition: base chrome, then
// either the default typography (styles.smallText) or the explicit size
// overrides, then any extra overrides — all in one css() call so there
// is never a cross-class specificity race.
const buildInputClassName = (
size?: ComponentSize,
overrides?: Record<string, unknown>,
) =>
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 },
...(size ? inputSizeStyles[size] : styles.smallText),
...overrides,
});
export const defaultInputClassName = buildInputClassName();
const inputSizeClassNames = {
small: buildInputClassName('small'),
medium: buildInputClassName('medium'),
large: buildInputClassName('large'),
'extra-large': buildInputClassName('extra-large'),
} satisfies Record<ComponentSize, string>;
export type InputProps = Omit<
ComponentPropsWithRef<typeof ReactAriaInput>,
'size'
> & {
size?: ComponentSize;
onEnter?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
onEscape?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
onChangeValue?: (
@@ -57,15 +121,20 @@ export function Input({
onChangeValue,
onUpdate,
className,
size,
...props
}: InputProps) {
const baseClassName = size
? inputSizeClassNames[size]
: defaultInputClassName;
return (
<ReactAriaInput
ref={ref}
className={
typeof className === 'function'
? renderProps => cx(defaultInputClassName, className(renderProps))
: cx(defaultInputClassName, className)
? renderProps => cx(baseClassName, className(renderProps))
: cx(baseClassName, className)
}
{...props}
onKeyUp={e => {
@@ -91,22 +160,40 @@ export function Input({
);
}
const defaultBigInputClassName = css({
padding: 10,
fontSize: 15,
// BigInput chrome: borderless with the ambient shadow instead. The
// legacy look (size omitted) keeps today's exact values (padding 10,
// fontSize 15); an explicit size swaps in the size tokens.
const bigInputChrome: Record<string, unknown> = {
border: 'none',
...styles.shadow,
'&[data-focused]': { border: 'none', ...styles.shadow },
};
const defaultBigInputClassName = buildInputClassName(undefined, {
padding: 10,
fontSize: 15,
...bigInputChrome,
});
export function BigInput({ className, ...props }: InputProps) {
const bigInputSizeClassNames = {
small: buildInputClassName('small', bigInputChrome),
medium: buildInputClassName('medium', bigInputChrome),
large: buildInputClassName('large', bigInputChrome),
'extra-large': buildInputClassName('extra-large', bigInputChrome),
} satisfies Record<ComponentSize, string>;
export function BigInput({ size, className, ...props }: InputProps) {
const baseClassName = size
? bigInputSizeClassNames[size]
: defaultBigInputClassName;
return (
<Input
{...props}
className={
typeof className === 'function'
? renderProps => cx(defaultBigInputClassName, className(renderProps))
: cx(defaultBigInputClassName, className)
? renderProps => cx(baseClassName, className(renderProps))
: cx(baseClassName, className)
}
/>
);
@@ -110,3 +110,24 @@ export const InlineUsage: Story = {
},
},
};
export const Sizes: Story = {
render: () => (
<View style={{ gap: 8 }}>
<Text size="small">Small (12px)</Text>
<Text size="medium">Medium (13px)</Text>
<Text size="large">Large (15px, 16px on mobile)</Text>
<Text size="extra-large">
Extra Large (17px on mobile/tablet, 16px on desktop)
</Text>
</View>
),
parameters: {
docs: {
description: {
story:
'The standard size vocabulary. Values adapt to the viewport: resize the story canvas below 512px, 730px and 1100px to see the responsive steps.',
},
},
},
};
+39 -3
View File
@@ -4,21 +4,57 @@ import type { HTMLProps, ReactNode, Ref } from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import {
componentSizeText,
sizeResponsiveStyles,
type ComponentSize,
} from './tokens';
type TextProps = Omit<HTMLProps<HTMLSpanElement>, 'style'> & {
type TextProps = Omit<HTMLProps<HTMLSpanElement>, 'size' | 'style'> & {
innerRef?: Ref<HTMLSpanElement>;
className?: string;
size?: ComponentSize;
children?: ReactNode;
style?: CSSProperties;
};
// Per-size typography, varying by breakpoint via media queries (no
// hooks). Precomputed once at module level so rendering stays cheap.
const getTextSizeStyles = (size: ComponentSize): Record<string, unknown> => {
const text = componentSizeText[size];
const getGroupStyles = (group: keyof typeof text) => {
const { fontSize, lineHeight } = text[group];
return { fontSize, lineHeight };
};
return sizeResponsiveStyles({
narrow: getGroupStyles('narrow'),
small: getGroupStyles('small'),
medium: getGroupStyles('medium'),
wide: getGroupStyles('wide'),
});
};
const textSizeStyles = {
small: getTextSizeStyles('small'),
medium: getTextSizeStyles('medium'),
large: getTextSizeStyles('large'),
'extra-large': getTextSizeStyles('extra-large'),
} satisfies Record<ComponentSize, Record<string, unknown>>;
export const Text = forwardRef<HTMLSpanElement, TextProps>((props, ref) => {
const { className = '', style, innerRef, ...restProps } = props;
const { className = '', style, innerRef, size, ...restProps } = props;
return (
<span
{...restProps}
ref={innerRef ?? ref}
className={cx(className, css(style))}
className={
size
? cx(className, css({ ...textSizeStyles[size], ...style }))
: cx(className, css(style))
}
/>
);
});
+124
View File
@@ -51,3 +51,127 @@ export const radius: Record<RadiusSize, number> = {
sm: 4,
pill: 999,
};
// Standard component size vocabulary shared by Button, Text and Input.
// Values are looked up per breakpoint group so components can render
// denser on desktop and more comfortable on mobile without hooks.
export type ComponentSize = 'small' | 'medium' | 'large' | 'extra-large';
// Breakpoint groups mirror the view modes in useResponsive:
// narrow <512, small 512-729, medium 730-1099, wide >=1100.
// `narrow` is the base (no media query); every other group is a
// `min-width` query at its breakpoint.
type BreakpointGroup = 'narrow' | 'small' | 'medium' | 'wide';
const breakpointGroupMediaQueries: Record<
Exclude<BreakpointGroup, 'narrow'>,
string
> = {
small: `@media (min-width: ${tokens.breakpoint_small})`,
medium: `@media (min-width: ${tokens.breakpoint_medium})`,
wide: `@media (min-width: ${tokens.breakpoint_wide})`,
};
type ComponentSizeTextValues = {
fontSize: number;
// lineHeight is on emotion's unitless list, so a plain number would
// render as a multiplier (e.g. `line-height: 18` = 18x). Always a
// px string so the value renders as an absolute line box.
lineHeight: string;
};
type ComponentSizeControlValues = {
paddingY: number;
paddingX: number;
minHeight?: number;
};
// Typographic scale, aligned with styles.ts (smallText 13, mediumText 15,
// verySmallText 12, mobileMenuItem 17). Mobile steps sizes up one notch
// for readability; desktop stays compact.
export const componentSizeText: Record<
ComponentSize,
Record<BreakpointGroup, ComponentSizeTextValues>
> = {
small: {
narrow: { fontSize: 12, lineHeight: '16px' },
small: { fontSize: 12, lineHeight: '16px' },
medium: { fontSize: 12, lineHeight: '16px' },
wide: { fontSize: 12, lineHeight: '16px' },
},
medium: {
narrow: { fontSize: 13, lineHeight: '18px' },
small: { fontSize: 13, lineHeight: '18px' },
medium: { fontSize: 13, lineHeight: '18px' },
wide: { fontSize: 13, lineHeight: '18px' },
},
large: {
narrow: { fontSize: 16, lineHeight: '22px' },
small: { fontSize: 15, lineHeight: '20px' },
medium: { fontSize: 15, lineHeight: '20px' },
wide: { fontSize: 15, lineHeight: '20px' },
},
'extra-large': {
narrow: { fontSize: 17, lineHeight: '24px' },
small: { fontSize: 17, lineHeight: '24px' },
medium: { fontSize: 16, lineHeight: '22px' },
wide: { fontSize: 16, lineHeight: '22px' },
},
};
// Padding and min-height for controls (Button, Input). The `medium` row
// reproduces today's default look exactly (5px / 5px 10px, no
// min-height). `extra-large` narrow hits the 40px mobile touch target.
export const componentSizeControl: Record<
ComponentSize,
Record<BreakpointGroup, ComponentSizeControlValues>
> = {
small: {
narrow: { paddingY: 3, paddingX: spacing.sm, minHeight: 24 },
small: { paddingY: 3, paddingX: spacing.sm, minHeight: 24 },
medium: { paddingY: 3, paddingX: spacing.sm, minHeight: 24 },
wide: { paddingY: 3, paddingX: spacing.sm, minHeight: 24 },
},
medium: {
narrow: { paddingY: 5, paddingX: 10 },
small: { paddingY: 5, paddingX: 10 },
medium: { paddingY: 5, paddingX: 10 },
wide: { paddingY: 5, paddingX: 10 },
},
large: {
narrow: { paddingY: spacing.sm, paddingX: spacing.md, minHeight: 36 },
small: { paddingY: 6, paddingX: spacing.md, minHeight: 32 },
medium: { paddingY: 6, paddingX: spacing.md, minHeight: 32 },
wide: { paddingY: 6, paddingX: spacing.md, minHeight: 32 },
},
'extra-large': {
narrow: { paddingY: 10, paddingX: 14, minHeight: 40 },
small: { paddingY: spacing.sm, paddingX: spacing.md, minHeight: 36 },
medium: { paddingY: spacing.sm, paddingX: spacing.md, minHeight: 36 },
wide: { paddingY: spacing.sm, paddingX: spacing.md, minHeight: 36 },
},
};
type ResponsiveStyleObject = Record<string, unknown>;
// Flattens a per-breakpoint-group value table into a single emotion style
// object: the `narrow` values become the base and each larger group is
// emitted as a min-width media query — but only when its values actually
// differ from the previous group, keeping generated CSS lean.
export function sizeResponsiveStyles(
values: Record<BreakpointGroup, ResponsiveStyleObject>,
): ResponsiveStyleObject {
const styles: ResponsiveStyleObject = { ...values.narrow };
let previousGroup: BreakpointGroup = 'narrow';
for (const group of ['small', 'medium', 'wide'] as const) {
if (
JSON.stringify(values[group]) !== JSON.stringify(values[previousGroup])
) {
styles[breakpointGroupMediaQueries[group]] = values[group];
}
previousGroup = group;
}
return styles;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

@@ -495,7 +495,7 @@ function SingleAutocomplete<T extends AutocompleteItem>({
<View ref={triggerRef} style={{ flexShrink: 0 }}>
{renderInput(
(() => {
const { className, style, ...restInputProps } =
const { className, style, size, ...restInputProps } =
inputProps || {};
const downshiftProps = getInputProps({
ref: inputRef,
@@ -616,6 +616,7 @@ function SingleAutocomplete<T extends AutocompleteItem>({
...downshiftProps,
...(className && { className }),
...(style && { style }),
...(size && { size }),
};
})(),
)}
@@ -132,8 +132,8 @@ function CheckboxOptionWithHelp({
</SpaceBetween>
{showHelp && (
<Text
size="medium"
style={{
fontSize: 13,
color: theme.pageTextSubdued,
}}
>
@@ -149,17 +149,17 @@ export function BuiltInProviders({
flexWrap: 'wrap',
}}
>
<Text style={{ fontSize: 17, fontWeight: 600 }}>
<Text size="extra-large" style={{ fontWeight: 600 }}>
{provider.displayName}
</Text>
{provider.isConfigured && provider.credentialSource && (
<Text
size="small"
style={{
alignSelf: 'flex-start',
borderRadius: 999,
backgroundColor: theme.buttonPrimaryBackground,
color: theme.buttonPrimaryText,
fontSize: 12,
fontWeight: 500,
padding: '2px 8px',
}}
@@ -173,11 +173,11 @@ export function BuiltInProviders({
)}
</View>
<Text
size="medium"
style={{
color: provider.isConfigured
? theme.noticeTextDark
: theme.pageTextSubdued,
fontSize: 13,
fontWeight: 500,
}}
>
@@ -259,7 +259,7 @@ export function BuiltInProviders({
{provider.supportsPerBudgetFile &&
provider.credentialSource === 'global' &&
!provider.canConfigure && (
<Text style={{ color: theme.pageTextSubdued, fontSize: 13 }}>
<Text size="medium" style={{ color: theme.pageTextSubdued }}>
<Trans>
Reset credentials before setting credentials for this
budget file.
@@ -214,7 +214,7 @@ export function EditSyncAccount({ account }: EditSyncAccountProps) {
rightContent={<ModalCloseButton onPress={() => state.close()} />}
/>
<Text style={{ fontSize: 15 }}>
<Text size="large">
<Trans>Field mapping</Trans>
</Text>
@@ -226,7 +226,7 @@ export function EditSyncAccount({ account }: EditSyncAccountProps) {
setMapping={setMapping}
/>
<Text style={{ fontSize: 15, margin: '1em 0 .5em 0' }}>
<Text size="large" style={{ margin: '1em 0 .5em 0' }}>
<Trans>Options</Trans>
</Text>
@@ -112,7 +112,8 @@ export function FieldMapping({
plain
>
<Text
style={{ whiteSpace: 'nowrap', fontSize: 13, fontWeight: 500 }}
size="medium"
style={{ whiteSpace: 'nowrap', fontWeight: 500 }}
>
{calculatedActualFieldWidth > 70 ? t('Actual field') : 'Actual'}
</Text>
@@ -150,7 +151,7 @@ export function FieldMapping({
style={{ ...commonCellStyle, paddingLeft: '10px' }}
plain
>
<Text style={{ whiteSpace: 'nowrap', fontSize: 13 }}>
<Text size="medium" style={{ whiteSpace: 'nowrap' }}>
{field.actualField}
</Text>
</Cell>
@@ -72,7 +72,7 @@ export function CleanupAutomation({
return (
<View style={{ gap: 14 }}>
<Text style={{ fontSize: 12, color: theme.pageTextLight }}>
<Text size="small" style={{ color: theme.pageTextLight }}>
<Trans>
End of month cleanup is a one-click reallocation of funds. Categories
you choose to <strong>send leftover</strong> return their surplus to a
@@ -4,6 +4,7 @@ import { Select } from '@actual-app/components/select';
import { SpaceBetween } from '@actual-app/components/space-between';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import {
dayFromDate,
firstDayOfMonth,
@@ -129,11 +130,11 @@ export const LimitAutomation = ({
</SpaceBetween>
<Text
size="small"
style={{
fontSize: 12,
color: theme.pageTextLight,
display: 'block',
marginTop: 8,
marginTop: spacing.sm,
}}
>
<Trans>
@@ -51,7 +51,7 @@ export const RemainderAutomation = ({
onBlur={commitWeight}
/>
</FormField>
<Text style={{ flex: 2, color: theme.pageTextLight, fontSize: 12 }}>
<Text size="small" style={{ flex: 2, color: theme.pageTextLight }}>
<Trans>
Categories with higher weights get a bigger share of the leftover To
Budget.
@@ -11,6 +11,7 @@ import { Input } from '@actual-app/components/input';
import { Popover } from '@actual-app/components/popover';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { send } from '@actual-app/core/platform/client/connection';
import * as monthUtils from '@actual-app/core/shared/months';
@@ -254,7 +255,7 @@ export function QueryManager({ queries, onQueriesChange }: QueryManagerProps) {
No queries defined. Click 'Add Query' to create your first query.
</Trans>
</Text>
<Text style={{ fontSize: 12, marginTop: 8 }}>
<Text size="small" style={{ marginTop: spacing.sm }}>
<Trans>
Queries allow you to reference filtered transaction data in your
formulas using QUERY("queryName") or QUERY_COUNT("queryName")
@@ -665,10 +666,10 @@ function QueryItem({
<Trans>Import Query Configuration</Trans>
</Text>
<Text
size="small"
style={{
fontSize: 12,
color: theme.pageTextSubdued,
marginBottom: 8,
marginBottom: spacing.sm,
}}
>
<Trans>Paste the JSON configuration below:</Trans>
@@ -845,10 +846,10 @@ function QueryItem({
) : null}
</View>
<View style={{ marginBottom: 8, flex: 1 }}>
<View style={{ marginBottom: spacing.sm, flex: 1 }}>
<Text
size="small"
style={{
fontSize: 12,
fontWeight: 500,
marginBottom: 6,
color: theme.pageTextSubdued,
@@ -249,8 +249,8 @@ function BudgetFileState({ file, currentUserId }: BudgetFileStateProps) {
<Trans>Owner:</Trans>
</Text>
<Text
size="small"
style={{
...styles.verySmallText,
color: theme.pageTextLight,
paddingLeft: 10,
}}
@@ -455,11 +455,11 @@ export function ConfigServer() {
{error && (
<>
<Text
size="large"
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
@@ -473,10 +473,10 @@ export function ConfigServer() {
}}
>
<Text
size="large"
style={{
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
<Trans>
@@ -89,11 +89,11 @@ export function Bootstrap() {
{error && (
<Text
size="large"
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
@@ -64,11 +64,11 @@ export function ChangePassword() {
{error && (
<Text
size="large"
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
@@ -77,11 +77,11 @@ export function ChangePassword() {
{msg && (
<Text
size="large"
style={{
marginTop: 20,
color: theme.noticeTextLight,
borderRadius: 4,
fontSize: 15,
}}
>
{msg}
@@ -228,8 +228,8 @@ function OpenIdLogin({ setError }) {
{reviewOpenIdConfiguration && (
<View style={{ marginTop: 20 }}>
<Text
size="small"
style={{
...styles.verySmallText,
color: theme.pageTextLight,
fontWeight: 'bold ',
width: '100%',
@@ -433,11 +433,11 @@ export function Login() {
{error && (
<Text
size="large"
style={{
marginTop: 20,
color: theme.errorText,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
@@ -162,8 +162,8 @@ export function OpenIdForm({
>
{!submitButtonDisabled && t('The OpenID provider URL.')}{' '}
<Text
size="small"
style={{
...styles.verySmallText,
color: theme.pageTextLight,
}}
>
@@ -246,8 +246,8 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) {
)}
<Button variant="bare" onPress={onClick}>
<Text
size="extra-large"
style={{
fontSize: 17,
fontWeight: 500,
...styles.underlinedText,
...(styles.lineClamp(2) as CSSProperties),
@@ -91,13 +91,7 @@ function AccountHeader<SheetFieldName extends SheetFields<'account'>>({
])}
>
<View style={{ flex: 1, alignItems: 'center', flexDirection: 'row' }}>
<Text
style={{
...styles.text,
fontSize: 17,
}}
data-testid="name"
>
<Text size="extra-large" data-testid="name">
{name}
</Text>
<Cheveron
@@ -3,6 +3,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { SpaceBetween } from '@actual-app/components/space-between';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { tsToRelativeTime } from '@actual-app/core/shared/util';
import type { AccountEntity } from '@actual-app/core/types/models';
@@ -37,21 +38,21 @@ export function BankSyncAccountsListItem({
borderBottomWidth: 1,
borderBottomColor: theme.tableBorder,
borderBottomStyle: 'solid',
padding: 16,
padding: spacing.lg,
width: '100%',
cursor: 'pointer',
}}
onClick={() => onAction(account, isLinked ? 'edit' : 'link')}
>
<SpaceBetween gap={60}>
<SpaceBetween gap={spacing.lg}>
<SpaceBetween
direction="vertical"
gap={5}
style={{ flex: 1, alignItems: 'flex-start' }}
>
<Text
size="large"
style={{
fontSize: 15,
fontWeight: 500,
color: theme.tableText,
}}
@@ -60,8 +61,8 @@ export function BankSyncAccountsListItem({
</Text>
{isLinked && (
<Text
size="medium"
style={{
fontSize: 13,
color: theme.pageTextSubdued,
}}
>
@@ -70,8 +71,8 @@ export function BankSyncAccountsListItem({
)}
{isLinked && lastSyncString && (
<Text
size="medium"
style={{
fontSize: 13,
color: theme.pageTextSubdued,
}}
data-vrt-mask
@@ -6,6 +6,7 @@ import { Button } from '@actual-app/components/button';
import { SpaceBetween } from '@actual-app/components/space-between';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { useUnlinkAccountMutation } from '#accounts';
@@ -126,8 +127,8 @@ export function MobileBankSyncAccountEditPage() {
overflow: 'auto',
}}
>
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 15, marginBottom: 10 }}>
<View style={{ padding: spacing.lg }}>
<Text size="large" style={{ marginBottom: 10 }}>
<Trans>Field mapping</Trans>
</Text>
@@ -140,7 +141,7 @@ export function MobileBankSyncAccountEditPage() {
isMobile
/>
<Text style={{ fontSize: 15, marginTop: 20, marginBottom: 10 }}>
<Text size="large" style={{ marginTop: 20, marginBottom: 10 }}>
<Trans>Options</Trans>
</Text>
@@ -77,8 +77,8 @@ export function SchedulesListItem({
>
{/* Schedule name */}
<Text
size="large"
style={{
fontSize: 15,
fontWeight: 600,
color: schedule.name
? theme.pageText
@@ -201,7 +201,7 @@ export function TransactionList({
backgroundColor: theme.mobilePageBackground,
}}
>
<Text style={{ fontSize: 15 }}>
<Text size="large">
<Trans>No transactions</Trans>
</Text>
</View>
@@ -196,7 +196,7 @@ function AppliedFiltersChip() {
}}
>
<SvgFilter width={12} height={12} style={{ flexShrink: 0 }} />
<Text style={{ fontSize: 12, fontWeight: 500 }}>
<Text size="small" style={{ fontWeight: 500 }}>
<Trans>Filters applied</Trans>
</Text>
</View>
@@ -7,7 +7,7 @@ import { Input } from '@actual-app/components/input';
import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { tokens } from '@actual-app/components/tokens';
import { spacing, tokens } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { dayFromDate, firstDayOfMonth } from '@actual-app/core/shared/months';
import type {
@@ -230,9 +230,9 @@ export function AutomationEditorPane({
<AutomationErrorTitle error={activeError} />
</Text>
<Text
size="small"
style={{
fontSize: 12,
marginTop: 2,
marginTop: spacing.xxs,
color: 'inherit',
display: 'block',
}}
@@ -288,11 +288,11 @@ export function AutomationEditorPane({
>
{NON_CONTRIBUTION_TYPES.has(state.displayType) && (
<Text
size="small"
style={{
fontSize: 12,
color: theme.pageTextLight,
display: 'block',
marginBottom: 4,
marginBottom: spacing.xs,
}}
>
{getDisplayTemplateMeta(state.displayType).description}
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { SvgAlertTriangle } from '@actual-app/components/icons/v2';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import { Tooltip } from '@actual-app/components/tooltip';
import { View } from '@actual-app/components/view';
@@ -165,12 +166,12 @@ export function AutomationListRow({
style={{
flexShrink: 0,
alignItems: 'flex-end',
gap: 2,
gap: spacing.xxs,
}}
>
<Text
size="small"
style={{
fontSize: 12,
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
color:
@@ -185,7 +185,7 @@ export function BudgetAutomationsBody({
}}
>
<View style={{ minWidth: 0 }}>
<Text style={{ fontSize: 12, color: theme.pageTextLight }}>
<Text size="small" style={{ color: theme.pageTextLight }}>
<Trans>Budget automation</Trans>
</Text>
<Text
@@ -68,8 +68,8 @@ export function CleanupListRow({
</View>
<View style={{ minWidth: 0, flex: 1 }}>
<Text
size="small"
style={{
fontSize: 12,
fontWeight: 600,
color: theme.pageText,
display: 'block',
@@ -3,6 +3,7 @@ import { Trans } from 'react-i18next';
import { SvgAlertTriangle } from '@actual-app/components/icons/v2';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { spacing } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { getAutomationExamples } from '#components/budget/goals/automationExamples';
@@ -50,10 +51,10 @@ export function EmptyState({ onAdd }: EmptyStateProps) {
<Trans>No automations yet</Trans>
</Text>
<Text
size="medium"
style={{
fontSize: 13,
color: theme.pageTextLight,
marginTop: 4,
marginTop: spacing.xs,
marginBottom: 22,
display: 'block',
}}
@@ -112,8 +113,8 @@ export function EmptyState({ onAdd }: EmptyStateProps) {
<Icon width={16} height={16} />
</View>
<Text
size="medium"
style={{
fontSize: 13,
fontWeight: 600,
color: theme.pageText,
}}
@@ -270,7 +270,7 @@ export function CloseAccountModal({
{!canDelete && (
<View style={{ marginBottom: 15 }}>
<Text style={{ fontSize: 12 }}>
<Text size="small">
<Trans>
You can also{' '}
<Link
@@ -196,7 +196,7 @@ export function EnableBankingInitialiseModal({
<SvgCheckCircle1
style={{ width: 14, height: 14, color: theme.noticeText }}
/>
<Text style={{ fontSize: 12, color: theme.pageTextSubdued }}>
<Text size="small" style={{ color: theme.pageTextSubdued }}>
{keyFileName}
</Text>
</View>
@@ -62,12 +62,7 @@ export function EnvelopeBalanceMenuModal({
marginBottom: 20,
}}
>
<Text
style={{
fontSize: 17,
fontWeight: 400,
}}
>
<Text size="extra-large" style={{ fontWeight: 400 }}>
<Trans>Balance</Trans>
</Text>
<BalanceWithCarryover
@@ -107,12 +107,7 @@ export function EnvelopeBudgetMenuModal({
alignItems: 'center',
}}
>
<Text
style={{
fontSize: 17,
fontWeight: 400,
}}
>
<Text size="extra-large" style={{ fontWeight: 400 }}>
<Trans>Budgeted</Trans>
</Text>
<AmountInput
@@ -67,12 +67,7 @@ export function EnvelopeIncomeBalanceMenuModal({
marginBottom: 20,
}}
>
<Text
style={{
fontSize: 17,
fontWeight: 400,
}}
>
<Text size="extra-large" style={{ fontWeight: 400 }}>
<Trans>Balance</Trans>
</Text>
<BalanceWithCarryover
@@ -9,7 +9,7 @@ type SubLabelProps = {
export function SubLabel({ title }: SubLabelProps) {
return (
<Text style={{ fontSize: 13, marginBottom: 3, color: theme.pageText }}>
<Text size="medium" style={{ marginBottom: 3, color: theme.pageText }}>
{title}
</Text>
);
@@ -564,7 +564,7 @@ export function KeyboardShortcutModal() {
padding: 20,
}}
>
<Text style={{ fontSize: 15 }}>
<Text size="large">
<Trans>
{isSearching
? 'No matching shortcuts'

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