authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 12:55:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 12:55:11-07:00
loge0649ed7d8f8b5f21d114ff2b09ef803184f1d05
treed7fd12ae6e7ef0c4b3610a05675f8fc89c1f4358
parent4d36142c022e0dc797034933d01e200818b4083d
signature Signed by PGP key B5690EEEBB952194

perf(mobile): isolate Material You conversion on Android (#12379)


3 files changed, 112 insertions(+), 102 deletions(-)

apps/mobile/src/lib/materialYouTheme.android.ts created+108
......@@ -0,0 +1,108 @@
1import { Hct, argbFromHex, hexFromArgb } from "@material/material-color-utilities";
2
3import type { MobileThemeAppearance, MobileThemeVariables } from "./mobileTheme";
4import type { MaterialYouPalette } from "./materialYouPalette";
5
6function withAlpha(color: string, alpha: number): string {
7 const match = /^#([\da-f]{6})(?:[\da-f]{2})?$/iu.exec(color);
8 if (!match?.[1]) return color;
9 const alphaByte = Math.round(Math.min(1, Math.max(0, alpha)) * 255)
10 .toString(16)
11 .padStart(2, "0")
12 .toUpperCase();
13 return `#${match[1]}${alphaByte}`;
14}
15
16function materialTone(color: string, tone: number): string {
17 const match = /^#([\da-f]{6})(?:[\da-f]{2})?$/iu.exec(color);
18 if (!match?.[1]) return color;
19
20 const source = Hct.fromInt(argbFromHex(match[1]));
21 return `${hexFromArgb(Hct.from(source.hue, source.chroma, tone).toInt()).toUpperCase()}FF`;
22}
23
24export function materialYouPaletteToMobileThemeVariables(
25 palette: MaterialYouPalette,
26 appearance: MobileThemeAppearance,
27 base: MobileThemeVariables,
28): MobileThemeVariables {
29 const dark = appearance === "dark";
30 const userBubble = dark ? materialTone(palette.inversePrimary, 30) : palette.primary;
31 const userBubbleForeground = dark ? materialTone(palette.inversePrimary, 90) : palette.onPrimary;
32
33 return {
34 ...base,
35 "--color-screen": palette.surface,
36 "--color-sheet": withAlpha(palette.surfaceContainerLow, 0.98),
37 "--color-sheet-solid": palette.surfaceContainerLow,
38 "--color-card": palette.surfaceContainer,
39 "--color-card-alt": palette.surfaceContainerHigh,
40 "--color-card-translucent": withAlpha(palette.surfaceContainer, 0.8),
41 "--color-thread-canvas": palette.surfaceContainerLow,
42 "--color-thread-selected": palette.surfaceContainer,
43 "--color-thread-selected-foreground": palette.onSurface,
44 "--color-thread-selected-foreground-muted": palette.onSurfaceVariant,
45 "--color-composer-panel": withAlpha(palette.surfaceContainerLow, dark ? 0.92 : 0.88),
46 "--color-composer-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.9 : 0.94),
47 "--color-composer-border": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46),
48 "--color-foreground": palette.onSurface,
49 "--color-foreground-secondary": palette.onSurfaceVariant,
50 "--color-foreground-muted": palette.onSurfaceVariant,
51 "--color-foreground-tertiary": withAlpha(palette.onSurfaceVariant, 0.58),
52 "--color-border": withAlpha(palette.outlineVariant, dark ? 0.5 : 0.65),
53 "--color-border-subtle": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46),
54 "--color-separator": withAlpha(palette.outlineVariant, dark ? 0.24 : 0.34),
55 "--color-subtle": withAlpha(palette.onSurface, 0.05),
56 "--color-subtle-strong": withAlpha(palette.onSurface, 0.1),
57 "--color-inline-skill-background": withAlpha(palette.tertiaryContainer, 0.52),
58 "--color-inline-skill-border": withAlpha(palette.tertiary, 0.38),
59 "--color-inline-skill-foreground": palette.onTertiaryContainer,
60 "--color-primary": palette.primary,
61 "--color-primary-foreground": palette.onPrimary,
62 "--color-primary-shadow": withAlpha(palette.scrim, dark ? 0.22 : 0.18),
63 "--color-secondary": palette.secondaryContainer,
64 "--color-secondary-foreground": palette.onSecondaryContainer,
65 "--color-secondary-border": withAlpha(palette.outlineVariant, dark ? 0.42 : 0.56),
66 "--color-switch-active-track": palette.primary,
67 "--color-switch-active-thumb": palette.onPrimary,
68 "--color-switch-inactive-track": palette.surfaceContainerHighest,
69 "--color-switch-inactive-thumb": palette.outline,
70 "--color-danger": palette.errorContainer,
71 "--color-danger-border": withAlpha(palette.onErrorContainer, dark ? 0.42 : 0.5),
72 "--color-danger-foreground": palette.onErrorContainer,
73 "--color-input": palette.surfaceContainerLowest,
74 "--color-input-border": withAlpha(palette.outline, dark ? 0.42 : 0.5),
75 "--color-sidebar-search": palette.surfaceContainerLowest,
76 "--color-placeholder": withAlpha(palette.onSurface, 0.62),
77 "--color-icon": palette.onSurface,
78 "--color-icon-muted": palette.onSurfaceVariant,
79 "--color-icon-subtle": withAlpha(palette.onSurfaceVariant, 0.68),
80 "--color-header": palette.surfaceContainerHigh,
81 "--color-header-border": withAlpha(palette.outlineVariant, dark ? 0.36 : 0.48),
82 "--color-glass-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.78 : 0.72),
83 "--color-glass-tint": withAlpha(palette.primaryContainer, dark ? 0.24 : 0.18),
84 "--color-status-bar": palette.surfaceContainerLow,
85 "--color-md-body": materialTone(palette.onSurfaceVariant, dark ? 90 : 10),
86 "--color-md-strong": materialTone(palette.onSurfaceVariant, dark ? 90 : 10),
87 "--color-md-link": palette.primary,
88 "--color-md-blockquote-border": withAlpha(palette.primary, dark ? 0.34 : 0.26),
89 "--color-md-blockquote-bg": withAlpha(palette.primaryContainer, dark ? 0.14 : 0.2),
90 "--color-md-code-bg": withAlpha(palette.onSurface, dark ? 0.08 : 0.06),
91 "--color-md-code-text": palette.onSurface,
92 "--color-md-user-code-bg": withAlpha(userBubbleForeground, dark ? 0.18 : 0.22),
93 "--color-md-user-code-text": userBubbleForeground,
94 "--color-md-user-fence-bg": withAlpha(userBubbleForeground, dark ? 0.14 : 0.18),
95 "--color-md-user-fence-text": userBubbleForeground,
96 "--color-md-hr": withAlpha(palette.outlineVariant, dark ? 0.48 : 0.6),
97 "--color-user-bubble": userBubble,
98 "--color-user-bubble-foreground": userBubbleForeground,
99 "--color-user-bubble-foreground-muted": withAlpha(userBubbleForeground, 0.78),
100 "--color-user-bubble-skill-foreground": userBubbleForeground,
101 "--color-backdrop": withAlpha(palette.scrim, dark ? 0.48 : 0.22),
102 "--color-drawer": palette.surface,
103 "--color-drawer-shadow": withAlpha(palette.scrim, dark ? 0.32 : 0.12),
104 "--color-dot-separator": withAlpha(palette.onSurface, 0.2),
105 "--color-wordmark": palette.onSurface,
106 "--color-chevron": withAlpha(palette.onSurface, 0.2),
107 };
108}
apps/mobile/src/lib/materialYouTheme.test.ts+1-1
......@@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test";
22
33import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables";
44import type { MaterialYouPalette } from "./materialYouPalette";
5import { materialYouPaletteToMobileThemeVariables } from "./materialYouTheme";
5import { materialYouPaletteToMobileThemeVariables } from "./materialYouTheme.android";
66
77const palette: MaterialYouPalette = {
88 primary: "#6750A4FF",
apps/mobile/src/lib/materialYouTheme.ts+3-101
......@@ -1,108 +1,10 @@
1import { Hct, argbFromHex, hexFromArgb } from "@material/material-color-utilities";
2
31import type { MobileThemeAppearance, MobileThemeVariables } from "./mobileTheme";
42import type { MaterialYouPalette } from "./materialYouPalette";
53
6function withAlpha(color: string, alpha: number): string {
7 const match = /^#([\da-f]{6})(?:[\da-f]{2})?$/iu.exec(color);
8 if (!match?.[1]) return color;
9 const alphaByte = Math.round(Math.min(1, Math.max(0, alpha)) * 255)
10 .toString(16)
11 .padStart(2, "0")
12 .toUpperCase();
13 return `#${match[1]}${alphaByte}`;
14}
15
16function materialTone(color: string, tone: number): string {
17 const match = /^#([\da-f]{6})(?:[\da-f]{2})?$/iu.exec(color);
18 if (!match?.[1]) return color;
19
20 const source = Hct.fromInt(argbFromHex(match[1]));
21 return `${hexFromArgb(Hct.from(source.hue, source.chroma, tone).toInt()).toUpperCase()}FF`;
22}
23
244export function materialYouPaletteToMobileThemeVariables(
25 palette: MaterialYouPalette,
26 appearance: MobileThemeAppearance,
5 _palette: MaterialYouPalette,
6 _appearance: MobileThemeAppearance,
277 base: MobileThemeVariables,
288): MobileThemeVariables {
29 const dark = appearance === "dark";
30 const userBubble = dark ? materialTone(palette.inversePrimary, 30) : palette.primary;
31 const userBubbleForeground = dark ? materialTone(palette.inversePrimary, 90) : palette.onPrimary;
32
33 return {
34 ...base,
35 "--color-screen": palette.surface,
36 "--color-sheet": withAlpha(palette.surfaceContainerLow, 0.98),
37 "--color-sheet-solid": palette.surfaceContainerLow,
38 "--color-card": palette.surfaceContainer,
39 "--color-card-alt": palette.surfaceContainerHigh,
40 "--color-card-translucent": withAlpha(palette.surfaceContainer, 0.8),
41 "--color-thread-canvas": palette.surfaceContainerLow,
42 "--color-thread-selected": palette.surfaceContainer,
43 "--color-thread-selected-foreground": palette.onSurface,
44 "--color-thread-selected-foreground-muted": palette.onSurfaceVariant,
45 "--color-composer-panel": withAlpha(palette.surfaceContainerLow, dark ? 0.92 : 0.88),
46 "--color-composer-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.9 : 0.94),
47 "--color-composer-border": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46),
48 "--color-foreground": palette.onSurface,
49 "--color-foreground-secondary": palette.onSurfaceVariant,
50 "--color-foreground-muted": palette.onSurfaceVariant,
51 "--color-foreground-tertiary": withAlpha(palette.onSurfaceVariant, 0.58),
52 "--color-border": withAlpha(palette.outlineVariant, dark ? 0.5 : 0.65),
53 "--color-border-subtle": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46),
54 "--color-separator": withAlpha(palette.outlineVariant, dark ? 0.24 : 0.34),
55 "--color-subtle": withAlpha(palette.onSurface, 0.05),
56 "--color-subtle-strong": withAlpha(palette.onSurface, 0.1),
57 "--color-inline-skill-background": withAlpha(palette.tertiaryContainer, 0.52),
58 "--color-inline-skill-border": withAlpha(palette.tertiary, 0.38),
59 "--color-inline-skill-foreground": palette.onTertiaryContainer,
60 "--color-primary": palette.primary,
61 "--color-primary-foreground": palette.onPrimary,
62 "--color-primary-shadow": withAlpha(palette.scrim, dark ? 0.22 : 0.18),
63 "--color-secondary": palette.secondaryContainer,
64 "--color-secondary-foreground": palette.onSecondaryContainer,
65 "--color-secondary-border": withAlpha(palette.outlineVariant, dark ? 0.42 : 0.56),
66 "--color-switch-active-track": palette.primary,
67 "--color-switch-active-thumb": palette.onPrimary,
68 "--color-switch-inactive-track": palette.surfaceContainerHighest,
69 "--color-switch-inactive-thumb": palette.outline,
70 "--color-danger": palette.errorContainer,
71 "--color-danger-border": withAlpha(palette.onErrorContainer, dark ? 0.42 : 0.5),
72 "--color-danger-foreground": palette.onErrorContainer,
73 "--color-input": palette.surfaceContainerLowest,
74 "--color-input-border": withAlpha(palette.outline, dark ? 0.42 : 0.5),
75 "--color-sidebar-search": palette.surfaceContainerLowest,
76 "--color-placeholder": withAlpha(palette.onSurface, 0.62),
77 "--color-icon": palette.onSurface,
78 "--color-icon-muted": palette.onSurfaceVariant,
79 "--color-icon-subtle": withAlpha(palette.onSurfaceVariant, 0.68),
80 "--color-header": palette.surfaceContainerHigh,
81 "--color-header-border": withAlpha(palette.outlineVariant, dark ? 0.36 : 0.48),
82 "--color-glass-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.78 : 0.72),
83 "--color-glass-tint": withAlpha(palette.primaryContainer, dark ? 0.24 : 0.18),
84 "--color-status-bar": palette.surfaceContainerLow,
85 "--color-md-body": materialTone(palette.onSurfaceVariant, dark ? 90 : 10),
86 "--color-md-strong": materialTone(palette.onSurfaceVariant, dark ? 90 : 10),
87 "--color-md-link": palette.primary,
88 "--color-md-blockquote-border": withAlpha(palette.primary, dark ? 0.34 : 0.26),
89 "--color-md-blockquote-bg": withAlpha(palette.primaryContainer, dark ? 0.14 : 0.2),
90 "--color-md-code-bg": withAlpha(palette.onSurface, dark ? 0.08 : 0.06),
91 "--color-md-code-text": palette.onSurface,
92 "--color-md-user-code-bg": withAlpha(userBubbleForeground, dark ? 0.18 : 0.22),
93 "--color-md-user-code-text": userBubbleForeground,
94 "--color-md-user-fence-bg": withAlpha(userBubbleForeground, dark ? 0.14 : 0.18),
95 "--color-md-user-fence-text": userBubbleForeground,
96 "--color-md-hr": withAlpha(palette.outlineVariant, dark ? 0.48 : 0.6),
97 "--color-user-bubble": userBubble,
98 "--color-user-bubble-foreground": userBubbleForeground,
99 "--color-user-bubble-foreground-muted": withAlpha(userBubbleForeground, 0.78),
100 "--color-user-bubble-skill-foreground": userBubbleForeground,
101 "--color-backdrop": withAlpha(palette.scrim, dark ? 0.48 : 0.22),
102 "--color-drawer": palette.surface,
103 "--color-drawer-shadow": withAlpha(palette.scrim, dark ? 0.32 : 0.12),
104 "--color-dot-separator": withAlpha(palette.onSurface, 0.2),
105 "--color-wordmark": palette.onSurface,
106 "--color-chevron": withAlpha(palette.onSurface, 0.2),
107 };
9 return base;
10810}