feat(themes): add md theme tokens and update ionic theme to include semantic tokens (#30734)

- Updates the theme tokens interface to add more numeric tokens & semantic tokens
- Moves the `config` values into a separate `config` option which uses the `IonicConfig` interface
  - Adds a config option for `formHighlight` to `IonicConfig`
- Defines default & dark tokens for the `md` theme
- Removes the numeric tokens from the `ionic` theme and adds semantic tokens
- Remove the numerous `--ion-font-family` overrides in favor of the tokens

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-10-21 15:56:18 -04:00
committed by GitHub
parent 3fd1d5cab1
commit a7699eabbe
12 changed files with 276 additions and 121 deletions

View File

File diff suppressed because one or more lines are too long

View File

@@ -7,16 +7,9 @@
// Ionic Font Family
// --------------------------------------------------
// TODO(FW-6744): Remove this after adding the ios tokens
html.ios {
--ion-default-font: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif;
}
html.md {
--ion-default-font: "Roboto", "Helvetica Neue", sans-serif;
}
html {
--ion-dynamic-font: -apple-system-body;
--ion-font-family: var(--ion-default-font);
--ion-font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif;
}
body {

View File

@@ -8,10 +8,6 @@
--background: #{globals.$ion-bg-surface-default};
}
html {
--ionic-dynamic-font: -apple-system-body;
}
body {
background: var(--ion-background-color, #{globals.$ion-bg-body});
color: var(--ion-text-color, #{globals.$ion-text-default});

View File

@@ -2,7 +2,7 @@
// TODO(ROU-10833): add font loading solution here, as a @font-face, base64 or cdn
html {
font-family: globals.$ion-font-family;
font-family: var(--ion-font-family);
}
body {

View File

@@ -11,12 +11,16 @@ export const defaultTheme: DefaultTheme = {
dark: darkTheme,
},
formHighlight: false,
rippleEffect: false,
config: {
formHighlight: false,
rippleEffect: false,
},
spacing: {
0: '0px',
25: '1px',
50: '2px',
75: '3px',
100: '4px',
150: '6px',
200: '8px',
@@ -98,17 +102,26 @@ export const defaultTheme: DefaultTheme = {
300: '12px',
350: '14px',
400: '16px',
hairline: '0.55px',
},
radii: {
0: '0px',
25: '2px',
25: '1px',
50: '2px',
75: '3px',
100: '4px',
150: '6px',
200: '8px',
250: '10px',
300: '12px',
350: '14px',
400: '16px',
500: '20px',
600: '24px',
700: '28px',
800: '32px',
900: '36px',
1000: '40px',
full: '999px',
},

View File

@@ -14,104 +14,66 @@ export const defaultTheme: DefaultTheme = {
dark: darkTheme,
},
formHighlight: false,
rippleEffect: false,
config: {
formHighlight: true,
},
// TODO(FW-6745): see if we can remove this after the md tokens are added
fontFamily: 'initial',
fontFamily:
'-apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
spacing: {
0: '0px',
50: '2px',
100: '4px',
150: '6px',
200: '8px',
250: '10px',
300: '12px',
350: '14px',
400: '16px',
450: '18px',
500: '20px',
550: '22px',
600: '24px',
650: '26px',
700: '28px',
750: '30px',
800: '32px',
850: '34px',
900: '36px',
950: '38px',
1000: '40px',
1050: '42px',
1100: '44px',
1150: '46px',
1200: '48px',
xxxxs: 'var(--ion-spacing-25)',
xxxs: 'var(--ion-spacing-50)',
xxs: 'var(--ion-spacing-100)',
xs: 'var(--ion-spacing-150)',
sm: 'var(--ion-spacing-200)',
md: 'var(--ion-spacing-300)',
lg: 'var(--ion-spacing-400)',
xl: 'var(--ion-spacing-500)',
xxl: 'var(--ion-spacing-600)',
xxxl: 'var(--ion-spacing-700)',
xxxxl: 'var(--ion-spacing-800)',
},
scaling: {
0: '0px',
25: '1px',
50: '2px',
75: '3px',
100: '4px',
150: '6px',
200: '8px',
250: '10px',
300: '12px',
350: '14px',
400: '16px',
450: '18px',
500: '20px',
550: '22px',
600: '24px',
650: '26px',
700: '28px',
750: '30px',
800: '32px',
850: '34px',
900: '36px',
950: '38px',
1000: '40px',
1050: '42px',
1100: '44px',
1150: '46px',
1200: '48px',
1400: '56px',
1600: '64px',
1800: '72px',
2000: '80px',
2400: '96px',
2800: '112px',
3200: '128px',
3400: '136px',
3600: '144px',
4000: '160px',
5000: '200px',
6200: '248px',
7400: '296px',
9000: '360px',
},
radii: {
0: '0px',
25: '2px',
100: '4px',
200: '8px',
300: '12px',
400: '16px',
500: '20px',
800: '32px',
1000: '40px',
full: '999px',
xxxxs: 'var(--ion-scaling-300)',
xxxs: 'var(--ion-scaling-400)',
xxs: 'var(--ion-scaling-500)',
xs: 'var(--ion-scaling-600)',
sm: 'var(--ion-scaling-700)',
md: 'var(--ion-scaling-800)',
lg: 'var(--ion-scaling-1000)',
xl: 'var(--ion-scaling-1200)',
xxl: 'var(--ion-scaling-1400)',
xxxl: 'var(--ion-scaling-1800)',
xxxxl: 'var(--ion-scaling-2000)',
},
borderWidth: {
0: '0',
25: '1px',
50: '2px',
75: '3px',
100: '4px',
150: '6px',
200: '8px',
xxxxs: 'var(--ion-border-width-0)',
xxxs: 'var(--ion-border-width-25)',
xxs: 'var(--ion-border-width-50)',
xs: 'var(--ion-border-width-75)',
sm: 'var(--ion-border-width-100)',
md: 'var(--ion-border-width-150)',
lg: 'var(--ion-border-width-200)',
xl: 'var(--ion-border-width-250)',
xxl: 'var(--ion-border-width-300)',
xxxl: 'var(--ion-border-width-350)',
xxxxl: 'var(--ion-border-width-400)',
},
radii: {
xxxxs: 'var(--ion-radii-0)',
xxxs: 'var(--ion-radii-25)',
xxs: 'var(--ion-radii-50)',
xs: 'var(--ion-radii-75)',
sm: 'var(--ion-radii-100)',
md: 'var(--ion-radii-200)',
lg: 'var(--ion-radii-300)',
xl: 'var(--ion-radii-400)',
xxl: 'var(--ion-radii-500)',
xxxl: 'var(--ion-radii-1000)',
xxxxl: 'var(--ion-radii-full)',
},
};

View File

@@ -3,4 +3,68 @@ import type { DarkTheme } from '../themes.interfaces';
export const darkTheme: DarkTheme = {
...baseDarkTheme,
backgroundColor: '#121212',
backgroundColorRgb: '18, 18, 18',
textColor: '#ffffff',
textColorRgb: '255, 255, 255',
backgroundColorStep: {
50: '#1e1e1e',
100: '#2a2a2a',
150: '#363636',
200: '#414141',
250: '#4d4d4d',
300: '#595959',
350: '#656565',
400: '#717171',
450: '#7d7d7d',
500: '#898989',
550: '#949494',
600: '#a0a0a0',
650: '#acacac',
700: '#b8b8b8',
750: '#c4c4c4',
800: '#d0d0d0',
850: '#dbdbdb',
900: '#e7e7e7',
950: '#f3f3f3',
},
textColorStep: {
50: '#f3f3f3',
100: '#e7e7e7',
150: '#dbdbdb',
200: '#d0d0d0',
250: '#c4c4c4',
300: '#b8b8b8',
350: '#acacac',
400: '#a0a0a0',
450: '#949494',
500: '#898989',
550: '#7d7d7d',
600: '#717171',
650: '#656565',
700: '#595959',
750: '#4d4d4d',
800: '#414141',
850: '#363636',
900: '#2a2a2a',
950: '#1e1e1e',
},
components: {
IonCard: {
background: '#1e1e1e',
},
IonItem: {
background: '#1e1e1e',
},
IonToolbar: {
background: '#1f1f1f',
},
IonTabBar: {
background: '#1f1f1f',
},
},
};

View File

@@ -13,4 +13,67 @@ export const defaultTheme: DefaultTheme = {
light: lightTheme,
dark: darkTheme,
},
config: {
formHighlight: true,
rippleEffect: true,
},
fontFamily: '"Roboto", "Helvetica Neue", sans-serif',
spacing: {
xxxxs: 'var(--ion-spacing-25)',
xxxs: 'var(--ion-spacing-50)',
xxs: 'var(--ion-spacing-100)',
xs: 'var(--ion-spacing-150)',
sm: 'var(--ion-spacing-200)',
md: 'var(--ion-spacing-300)',
lg: 'var(--ion-spacing-400)',
xl: 'var(--ion-spacing-500)',
xxl: 'var(--ion-spacing-600)',
xxxl: 'var(--ion-spacing-700)',
xxxxl: 'var(--ion-spacing-800)',
},
scaling: {
xxxxs: 'var(--ion-scaling-300)',
xxxs: 'var(--ion-scaling-400)',
xxs: 'var(--ion-scaling-500)',
xs: 'var(--ion-scaling-600)',
sm: 'var(--ion-scaling-700)',
md: 'var(--ion-scaling-800)',
lg: 'var(--ion-scaling-900)',
xl: 'var(--ion-scaling-1000)',
xxl: 'var(--ion-scaling-1100)',
xxxl: 'var(--ion-scaling-1200)',
xxxxl: 'var(--ion-scaling-1600)',
},
borderWidth: {
xxxxs: 'var(--ion-border-width-0)',
xxxs: 'var(--ion-border-width-25)',
xxs: 'var(--ion-border-width-50)',
xs: 'var(--ion-border-width-75)',
sm: 'var(--ion-border-width-100)',
md: 'var(--ion-border-width-150)',
lg: 'var(--ion-border-width-200)',
xl: 'var(--ion-border-width-250)',
xxl: 'var(--ion-border-width-300)',
xxxl: 'var(--ion-border-width-350)',
xxxxl: 'var(--ion-border-width-400)',
},
radii: {
xxxxs: 'var(--ion-radii-0)',
xxxs: 'var(--ion-radii-25)',
xxs: 'var(--ion-radii-50)',
xs: 'var(--ion-radii-75)',
sm: 'var(--ion-radii-100)',
md: 'var(--ion-radii-200)',
lg: 'var(--ion-radii-300)',
xl: 'var(--ion-radii-400)',
xxl: 'var(--ion-radii-700)',
xxxl: 'var(--ion-radii-900)',
xxxxl: 'var(--ion-radii-full)',
},
};

View File

@@ -1,12 +1,10 @@
import type { IonicConfig } from '../utils/config';
// Platform-specific theme
export type PlatformTheme = Omit<BaseTheme, 'ios' | 'md'>;
// Base tokens for all palettes
export type BaseTheme = {
// CONFIG TOKENS
rippleEffect?: boolean;
formHighlight?: boolean;
// GLOBAL THEME TOKENS
backgroundColor?: string;
backgroundColorRgb?: string;
@@ -22,7 +20,9 @@ export type BaseTheme = {
// SPACE TOKENS
spacing?: {
0?: string;
25?: string;
50?: string;
75?: string;
100?: string;
150?: string;
200?: string;
@@ -46,6 +46,17 @@ export type BaseTheme = {
1100?: string;
1150?: string;
1200?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
};
scaling?: {
@@ -90,6 +101,17 @@ export type BaseTheme = {
6200?: string;
7400?: string;
9000?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
};
// APPEARANCE TOKENS
@@ -105,18 +127,49 @@ export type BaseTheme = {
300?: string;
350?: string;
400?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
hairline?: string;
};
radii?: {
0?: string;
25?: string;
50?: string;
75?: string;
100?: string;
150?: string;
200?: string;
250?: string;
300?: string;
350?: string;
400?: string;
500?: string;
600?: string;
700?: string;
800?: string;
900?: string;
1000?: string;
xxxxs?: string;
xxxs?: string;
xxs?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
xxl?: string;
xxxl?: string;
xxxxl?: string;
full?: string;
};
@@ -205,4 +258,6 @@ export type DefaultTheme = BaseTheme & {
light?: LightTheme;
dark?: DarkTheme;
};
config?: IonicConfig;
};

View File

@@ -12,6 +12,12 @@ export interface IonicConfig {
*/
animated?: boolean;
/**
* When it's set to `false`, it disables the form highlight effect across the app.
* Defaults to `false`.
*/
formHighlight?: boolean;
/**
* When it's set to `false`, it disables all material-design ripple-effects across the app.
* Defaults to `true`.

View File

@@ -186,6 +186,7 @@ describe('getCustomTheme', () => {
describe('generateCSSVars', () => {
it('should not generate CSS variables for an empty theme', () => {
const theme = {
name: 'test',
palette: {
light: {},
dark: {},
@@ -198,14 +199,17 @@ describe('generateCSSVars', () => {
it('should generate CSS variables for a given theme', () => {
const theme = {
name: 'test',
palette: {
light: {},
dark: {
enabled: 'system',
},
},
rippleEffect: true,
formHighlight: true,
config: {
rippleEffect: true,
formHighlight: true,
},
borderWidth: {
sm: '4px',
},
@@ -309,6 +313,7 @@ describe('injectCSS', () => {
describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme', () => {
const theme = {
name: 'test',
palette: {
light: {},
dark: {
@@ -339,6 +344,7 @@ describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme with light palette', () => {
const theme = {
name: 'test',
palette: {
light: {
color: {
@@ -425,6 +431,7 @@ describe('generateGlobalThemeCSS', () => {
it('should not include component or palette variables in global CSS', () => {
const theme = {
name: 'test',
palette: {
light: {},
dark: {
@@ -479,6 +486,7 @@ describe('generateGlobalThemeCSS', () => {
it('should generate global CSS for a given theme with dark palette enabled for system preference', () => {
const theme = {
name: 'test',
palette: {
light: {},
dark: {
@@ -598,6 +606,7 @@ describe('generateColorClasses', () => {
it('should generate color classes for a given theme', () => {
const theme = {
name: 'test',
palette: {
light: {
color: {

View File

@@ -87,7 +87,7 @@ export const generateCSSVars = (theme: any, prefix: string = CSS_PROPS_PREFIX):
}
// Do not generate CSS variables for excluded keys
const excludedKeys = ['enabled', 'ripple-effect', 'form-highlight'];
const excludedKeys = ['name', 'enabled', 'config'];
if (excludedKeys.includes(key)) {
return [];
}