authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-12 23:46:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-12 23:46:19-07:00
log20a8f1de3668e0ff4da236d75258e2184a68d727
tree890d47d0b4be2104662ee7d15e33af64ac0d02a5
parentdb6e0531e4faac0fc5ac0e12088b01bed6e7b851
signature Signed by PGP key B5690EEEBB952194

chore(mobile): enable noUncheckedIndexedAccess and noImplicitOverride (#11538)

Co-authored-by: Claude Code <noreply@anthropic.com>

11 files changed, 67 insertions(+), 34 deletions(-)

apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx+1-1
...@@ -98,7 +98,7 @@ function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel...@@ -98,7 +98,7 @@ function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel
98 }98 }
9999
100 const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE;100 const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE;
101 return Math.max(12, Math.round(DEFAULT_HEADING_FONT_SIZES[index] * scale));101 return Math.max(12, Math.round((DEFAULT_HEADING_FONT_SIZES[index] ?? 15) * scale));
102}102}
103103
104function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle {104function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle {
apps/mobile/scripts/generate-uniwind-themes.mts+3-3
...@@ -53,7 +53,7 @@ const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity...@@ -53,7 +53,7 @@ const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity
53// These replace the remaining dark:* utility pairs. A registered palette theme is53// These replace the remaining dark:* utility pairs. A registered palette theme is
54// neither literally `light` nor `dark`, so appearance-sensitive values must also be54// neither literally `light` nor `dark`, so appearance-sensitive values must also be
55// represented as semantic variables for custom themes.55// represented as semantic variables for custom themes.
56const ADAPTIVE_COLORS = {56const ADAPTIVE_COLORS: Readonly<Record<string, readonly [light: string, dark: string]>> = {
57 "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],57 "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],
58 "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],58 "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],
59 "--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],59 "--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],
...@@ -143,9 +143,9 @@ export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>...@@ -143,9 +143,9 @@ export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>
143143
144const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>144const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>
145 Object.fromEntries(145 Object.fromEntries(
146 Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [146 Object.entries(ADAPTIVE_COLORS).map(([name, [light, dark]]) => [
147 name,147 name,
148 values[appearance === "light" ? 0 : 1],148 appearance === "light" ? light : dark,
149 ]),149 ]),
150 );150 );
151151
apps/mobile/src/components/AndroidAnchoredMenu.tsx+1-1
...@@ -125,7 +125,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -125,7 +125,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
125 return () => subscription.remove();125 return () => subscription.remove();
126 }, [anchor, close, submenuDepth]);126 }, [anchor, close, submenuDepth]);
127127
128 const parent = path.length > 0 ? path[path.length - 1] : null;128 const parent = path[path.length - 1] ?? null;
129 const levelActions = (parent?.subactions ?? props.actions).filter(129 const levelActions = (parent?.subactions ?? props.actions).filter(
130 (action) => !(action.attributes?.hidden ?? false),130 (action) => !(action.attributes?.hidden ?? false),
131 );131 );
apps/mobile/src/features/review/nativeReviewDiffAdapter.ts+4-1
...@@ -53,7 +53,7 @@ function opaqueNativeHexColor(color: string, background: string): string {...@@ -53,7 +53,7 @@ function opaqueNativeHexColor(color: string, background: string): string {
53 const alpha = rgba[4] === undefined ? 1 : Math.min(1, Math.max(0, Number(rgba[4])));53 const alpha = rgba[4] === undefined ? 1 : Math.min(1, Math.max(0, Number(rgba[4])));
54 const channels = [1, 2, 3].map((index) => {54 const channels = [1, 2, 3].map((index) => {
55 const foreground = Number(rgba[index]);55 const foreground = Number(rgba[index]);
56 const behind = Number.parseInt(backgroundHex[index], 16);56 const behind = Number.parseInt(backgroundHex[index] ?? "0", 16);
57 return Math.round(foreground * alpha + behind * (1 - alpha));57 return Math.round(foreground * alpha + behind * (1 - alpha));
58 });58 });
59 return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;59 return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
...@@ -358,6 +358,9 @@ function addNativeWordDiffRanges(...@@ -358,6 +358,9 @@ function addNativeWordDiffRanges(
358 for (let pairIndex = 0; pairIndex < pairedCount; pairIndex += 1) {358 for (let pairIndex = 0; pairIndex < pairedCount; pairIndex += 1) {
359 const deletedRowIndex = deletedRowIndexes[pairIndex];359 const deletedRowIndex = deletedRowIndexes[pairIndex];
360 const addedRowIndex = addedRowIndexes[pairIndex];360 const addedRowIndex = addedRowIndexes[pairIndex];
361 if (deletedRowIndex === undefined || addedRowIndex === undefined) {
362 continue;
363 }
361 const deletedRow = nextRows[deletedRowIndex];364 const deletedRow = nextRows[deletedRowIndex];
362 const addedRow = nextRows[addedRowIndex];365 const addedRow = nextRows[addedRowIndex];
363 if (!deletedRow?.content || !addedRow?.content) {366 if (!deletedRow?.content || !addedRow?.content) {
apps/mobile/src/features/review/reviewModel.test.ts+2-2
...@@ -84,7 +84,7 @@ describe("buildReviewSectionItems", () => {...@@ -84,7 +84,7 @@ describe("buildReviewSectionItems", () => {
84 },84 },
85 ];85 ];
8686
87 const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]);87 const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]!);
88 const items = buildReviewSectionItems({88 const items = buildReviewSectionItems({
89 checkpoints,89 checkpoints,
90 gitSections,90 gitSections,
...@@ -92,7 +92,7 @@ describe("buildReviewSectionItems", () => {...@@ -92,7 +92,7 @@ describe("buildReviewSectionItems", () => {
92 [loadedTurnId]: "diff --git a/loaded.ts b/loaded.ts",92 [loadedTurnId]: "diff --git a/loaded.ts b/loaded.ts",
93 },93 },
94 loadingTurnIds: {94 loadingTurnIds: {
95 [getReviewSectionIdForCheckpoint(checkpoints[1])]: true,95 [getReviewSectionIdForCheckpoint(checkpoints[1]!)]: true,
96 },96 },
97 loadingGitSections: false,97 loadingGitSections: false,
98 });98 });
apps/mobile/src/features/terminal/terminalTheme.ts+27-3
...@@ -1,4 +1,8 @@...@@ -1,4 +1,8 @@
1import { BUILT_IN_THEMES, getThemeColorsForAppearance } from "@t3tools/shared/themePalettes";1import {
2 BUILT_IN_THEMES,
3 T3_CHAT_THEME,
4 getThemeColorsForAppearance,
5} from "@t3tools/shared/themePalettes";
26
3import {7import {
4 getMobileThemeVariables,8 getMobileThemeVariables,
...@@ -15,9 +19,29 @@ export interface TerminalTheme {...@@ -15,9 +19,29 @@ export interface TerminalTheme {
15 readonly border: string;19 readonly border: string;
16 readonly cursorForeground: string;20 readonly cursorForeground: string;
17 readonly cursorBackground: string;21 readonly cursorBackground: string;
18 readonly palette: readonly string[];22 /** The 16 ANSI colors, in order. A fixed tuple so indexed reads are never undefined. */
23 readonly palette: TerminalPalette;
19}24}
2025
26type TerminalPalette = readonly [
27 string,
28 string,
29 string,
30 string,
31 string,
32 string,
33 string,
34 string,
35 string,
36 string,
37 string,
38 string,
39 string,
40 string,
41 string,
42 string,
43];
44
21const PIERRE_LIGHT_THEME: TerminalTheme = {45const PIERRE_LIGHT_THEME: TerminalTheme = {
22 // Pierre terminal palette with the app's shared screen background.46 // Pierre terminal palette with the app's shared screen background.
23 background: "#f2f2f7",47 background: "#f2f2f7",
...@@ -85,7 +109,7 @@ export function getMobileTerminalTheme(...@@ -85,7 +109,7 @@ export function getMobileTerminalTheme(
85 const base = getPierreTerminalTheme(scheme);109 const base = getPierreTerminalTheme(scheme);
86 if (themeId === "t3-code" || themeId === "material-you") return base;110 if (themeId === "t3-code" || themeId === "material-you") return base;
87111
88 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];112 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
89 const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors;113 const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors;
90 const colors = getMobileThemeVariables(themeId, scheme);114 const colors = getMobileThemeVariables(themeId, scheme);
91 const background = themeColorToNativeColor(palette.terminalBackground);115 const background = themeColorToNativeColor(palette.terminalBackground);
apps/mobile/src/lib/mobileTheme.test-support.ts+1-1
...@@ -14,7 +14,7 @@ export function readDefaultMobileThemeVariables(...@@ -14,7 +14,7 @@ export function readDefaultMobileThemeVariables(
14 return Object.fromEntries(14 return Object.fromEntries(
15 Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [15 Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [
16 name,16 name,
17 value.trim(),17 (value ?? "").trim(),
18 ]),18 ]),
19 ) as MobileThemeVariables;19 ) as MobileThemeVariables;
20}20}
apps/mobile/src/lib/mobileTheme.test.ts+6-6
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes";2import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES, T3_CHAT_THEME } from "@t3tools/shared/themePalettes";
3import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";3import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";
44
5import {5import {
...@@ -49,7 +49,7 @@ function compositeOver(overlay: string, background: string): string {...@@ -49,7 +49,7 @@ function compositeOver(overlay: string, background: string): string {
4949
50describe("mobile themes", () => {50describe("mobile themes", () => {
51 it("declares every runtime theme variable in the static stylesheet", () => {51 it("declares every runtime theme variable in the static stylesheet", () => {
52 const generatedVariables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");52 const generatedVariables = createMobileThemeVariables(T3_CHAT_THEME.colors, "light");
53 expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual(53 expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual(
54 Object.keys(generatedVariables).sort(),54 Object.keys(generatedVariables).sort(),
55 );55 );
...@@ -152,16 +152,16 @@ describe("mobile themes", () => {...@@ -152,16 +152,16 @@ describe("mobile themes", () => {
152 });152 });
153153
154 it("maps semantic palette roles onto every mobile color variable", () => {154 it("maps semantic palette roles onto every mobile color variable", () => {
155 const variables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");155 const variables = createMobileThemeVariables(T3_CHAT_THEME.colors, "light");
156 expect(Object.keys(variables)).toHaveLength(75);156 expect(Object.keys(variables)).toHaveLength(75);
157 expect(variables["--color-sheet-solid"]).toBe(157 expect(variables["--color-sheet-solid"]).toBe(
158 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.chrome),158 themeColorToNativeColor(T3_CHAT_THEME.colors.chrome),
159 );159 );
160 expect(variables["--color-warning"]).toBe(160 expect(variables["--color-warning"]).toBe(
161 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.warningSurface),161 themeColorToNativeColor(T3_CHAT_THEME.colors.warningSurface),
162 );162 );
163 expect(variables["--color-warning-foreground"]).toBe(163 expect(variables["--color-warning-foreground"]).toBe(
164 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.warningForeground),164 themeColorToNativeColor(T3_CHAT_THEME.colors.warningForeground),
165 );165 );
166 expect(variables["--color-primary"]).not.toBe(variables["--color-screen"]);166 expect(variables["--color-primary"]).not.toBe(variables["--color-screen"]);
167 expect(variables["--color-primary-shadow"]).toBe("#000000");167 expect(variables["--color-primary-shadow"]).toBe("#000000");
apps/mobile/src/lib/mobileTheme.ts+14-14
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 BUILT_IN_THEMES,2 BUILT_IN_THEMES,
3 T3_CHAT_THEME,
3 getThemeColorsForAppearance,4 getThemeColorsForAppearance,
4 MOBILE_DEFAULT_THEME_ID,5 MOBILE_DEFAULT_THEME_ID,
5 MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,6 MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,
...@@ -29,7 +30,9 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{...@@ -29,7 +30,9 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
29 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),30 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
30];31];
3132
32export type MobileThemeVariable = `--color-${string}`;33// Closed set: every key `createMobileThemeVariables` writes. Reads of a
34// misspelled variable then fail to compile instead of yielding undefined.
35export type MobileThemeVariable = keyof ReturnType<typeof createMobileThemeVariables>;
33export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;36export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;
3437
35export function normalizeMobileThemeId(value: unknown): MobileThemeId {38export function normalizeMobileThemeId(value: unknown): MobileThemeId {
...@@ -136,9 +139,9 @@ function withAlpha(color: string, alpha: number): string {...@@ -136,9 +139,9 @@ function withAlpha(color: string, alpha: number): string {
136139
137function rgbChannels(color: string): readonly [number, number, number] | null {140function rgbChannels(color: string): readonly [number, number, number] | null {
138 const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color);141 const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color);
139 return match142 if (!match) return null;
140 ? [Number.parseInt(match[1], 16), Number.parseInt(match[2], 16), Number.parseInt(match[3], 16)]143 const [, red = "0", green = "0", blue = "0"] = match;
141 : null;144 return [Number.parseInt(red, 16), Number.parseInt(green, 16), Number.parseInt(blue, 16)];
142}145}
143146
144/**147/**
...@@ -216,18 +219,15 @@ function readableMessageAccent(accent: string, surface: string): string {...@@ -216,18 +219,15 @@ function readableMessageAccent(accent: string, surface: string): string {
216}219}
217220
218export function themeColorWithAlpha(color: string, alpha: number): string {221export function themeColorWithAlpha(color: string, alpha: number): string {
219 const hex = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color);222 const channels = rgbChannels(color);
220 if (hex) {223 if (channels) {
221 return `rgba(${Number.parseInt(hex[1], 16)}, ${Number.parseInt(hex[2], 16)}, ${Number.parseInt(hex[3], 16)}, ${alpha})`;224 return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${alpha})`;
222 }225 }
223 const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/.exec(color);226 const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/.exec(color);
224 return rgb ? `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})` : color;227 return rgb ? `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})` : color;
225}228}
226229
227export function createMobileThemeVariables(230export function createMobileThemeVariables(colors: ThemeColors, appearance: MobileThemeAppearance) {
228 colors: ThemeColors,
229 appearance: MobileThemeAppearance,
230): MobileThemeVariables {
231 const c = nativeColors(colors);231 const c = nativeColors(colors);
232 return {232 return {
233 "--color-screen": c.canvas,233 "--color-screen": c.canvas,
...@@ -315,7 +315,7 @@ export function createMobileThemeVariables(...@@ -315,7 +315,7 @@ export function createMobileThemeVariables(
315}315}
316316
317export const MOBILE_THEME_VARIABLE_NAMES = Object.keys(317export const MOBILE_THEME_VARIABLE_NAMES = Object.keys(
318 createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"),318 createMobileThemeVariables(T3_CHAT_THEME.colors, "light"),
319) as ReadonlyArray<MobileThemeVariable>;319) as ReadonlyArray<MobileThemeVariable>;
320320
321export function getMobileThemeVariables(321export function getMobileThemeVariables(
...@@ -323,7 +323,7 @@ export function getMobileThemeVariables(...@@ -323,7 +323,7 @@ export function getMobileThemeVariables(
323 appearance: MobileThemeAppearance,323 appearance: MobileThemeAppearance,
324 overrides: Partial<MobileThemeVariables> | null = null,324 overrides: Partial<MobileThemeVariables> | null = null,
325): MobileThemeVariables {325): MobileThemeVariables {
326 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];326 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
327 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;327 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
328 const baseVariables = createMobileThemeVariables(colors, appearance);328 const baseVariables = createMobileThemeVariables(colors, appearance);
329329
...@@ -337,7 +337,7 @@ export function getMobileThemePreviewColors(...@@ -337,7 +337,7 @@ export function getMobileThemePreviewColors(
337): ThemePreviewColors {337): ThemePreviewColors {
338 if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you")338 if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you")
339 return STANDARD_THEME_PREVIEW_COLORS[appearance];339 return STANDARD_THEME_PREVIEW_COLORS[appearance];
340 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];340 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
341 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;341 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
342 return {342 return {
343 canvas: themeColorToNativeColor(colors.canvas),343 canvas: themeColorToNativeColor(colors.canvas),
apps/mobile/src/lib/wideMarkdownBlocks.ts+5-1
...@@ -81,7 +81,11 @@ function hasOrderedListItem(text: string): boolean {...@@ -81,7 +81,11 @@ function hasOrderedListItem(text: string): boolean {
81 const nestedMatch = INDENTED_ORDERED_LIST_ITEM.exec(line);81 const nestedMatch = INDENTED_ORDERED_LIST_ITEM.exec(line);
82 const parentMatch =82 const parentMatch =
83 previousNonEmptyLine === null ? null : ANY_LIST_ITEM.exec(previousNonEmptyLine);83 previousNonEmptyLine === null ? null : ANY_LIST_ITEM.exec(previousNonEmptyLine);
84 if (nestedMatch && parentMatch && parentMatch[1].length < nestedMatch[1].length) {84 if (
85 nestedMatch?.[1] !== undefined &&
86 parentMatch?.[1] !== undefined &&
87 parentMatch[1].length < nestedMatch[1].length
88 ) {
85 return true;89 return true;
86 }90 }
8791
apps/mobile/tsconfig.json+3-1
...@@ -2,6 +2,8 @@...@@ -2,6 +2,8 @@
2 "extends": "expo/tsconfig.base",2 "extends": "expo/tsconfig.base",
3 "compilerOptions": {3 "compilerOptions": {
4 "allowImportingTsExtensions": true,4 "allowImportingTsExtensions": true,
5 "strict": true5 "strict": true,
6 "noUncheckedIndexedAccess": true,
7 "noImplicitOverride": true
6 }8 }
7}9}