Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel
}

const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE;
return Math.max(12, Math.round(DEFAULT_HEADING_FONT_SIZES[index] * scale));
return Math.max(12, Math.round((DEFAULT_HEADING_FONT_SIZES[index] ?? 15) * scale));
}

function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle {
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/scripts/generate-uniwind-themes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity
// These replace the remaining dark:* utility pairs. A registered palette theme is
// neither literally `light` nor `dark`, so appearance-sensitive values must also be
// represented as semantic variables for custom themes.
const ADAPTIVE_COLORS = {
const ADAPTIVE_COLORS: Readonly<Record<string, readonly [light: string, dark: string]>> = {
"--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],
"--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],
"--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],
Expand Down Expand Up @@ -143,9 +143,9 @@ export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>

const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>
Object.fromEntries(
Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [
Object.entries(ADAPTIVE_COLORS).map(([name, [light, dark]]) => [
name,
values[appearance === "light" ? 0 : 1],
appearance === "light" ? light : dark,
]),
);

Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/AndroidAnchoredMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
return () => subscription.remove();
}, [anchor, close, submenuDepth]);

const parent = path.length > 0 ? path[path.length - 1] : null;
const parent = path[path.length - 1] ?? null;
const levelActions = (parent?.subactions ?? props.actions).filter(
(action) => !(action.attributes?.hidden ?? false),
);
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/features/review/nativeReviewDiffAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function opaqueNativeHexColor(color: string, background: string): string {
const alpha = rgba[4] === undefined ? 1 : Math.min(1, Math.max(0, Number(rgba[4])));
const channels = [1, 2, 3].map((index) => {
const foreground = Number(rgba[index]);
const behind = Number.parseInt(backgroundHex[index], 16);
const behind = Number.parseInt(backgroundHex[index] ?? "0", 16);
return Math.round(foreground * alpha + behind * (1 - alpha));
});
return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
Expand Down Expand Up @@ -358,6 +358,9 @@ function addNativeWordDiffRanges(
for (let pairIndex = 0; pairIndex < pairedCount; pairIndex += 1) {
const deletedRowIndex = deletedRowIndexes[pairIndex];
const addedRowIndex = addedRowIndexes[pairIndex];
if (deletedRowIndex === undefined || addedRowIndex === undefined) {
continue;
}
const deletedRow = nextRows[deletedRowIndex];
const addedRow = nextRows[addedRowIndex];
if (!deletedRow?.content || !addedRow?.content) {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/features/review/reviewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ describe("buildReviewSectionItems", () => {
},
];

const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]);
const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]!);
const items = buildReviewSectionItems({
checkpoints,
gitSections,
turnDiffById: {
[loadedTurnId]: "diff --git a/loaded.ts b/loaded.ts",
},
loadingTurnIds: {
[getReviewSectionIdForCheckpoint(checkpoints[1])]: true,
[getReviewSectionIdForCheckpoint(checkpoints[1]!)]: true,
},
loadingGitSections: false,
});
Expand Down
30 changes: 27 additions & 3 deletions apps/mobile/src/features/terminal/terminalTheme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BUILT_IN_THEMES, getThemeColorsForAppearance } from "@t3tools/shared/themePalettes";
import {
BUILT_IN_THEMES,
T3_CHAT_THEME,
getThemeColorsForAppearance,
} from "@t3tools/shared/themePalettes";

import {
getMobileThemeVariables,
Expand All @@ -15,9 +19,29 @@ export interface TerminalTheme {
readonly border: string;
readonly cursorForeground: string;
readonly cursorBackground: string;
readonly palette: readonly string[];
/** The 16 ANSI colors, in order. A fixed tuple so indexed reads are never undefined. */
readonly palette: TerminalPalette;
}

type TerminalPalette = readonly [
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
string,
];

const PIERRE_LIGHT_THEME: TerminalTheme = {
// Pierre terminal palette with the app's shared screen background.
background: "#f2f2f7",
Expand Down Expand Up @@ -85,7 +109,7 @@ export function getMobileTerminalTheme(
const base = getPierreTerminalTheme(scheme);
if (themeId === "t3-code" || themeId === "material-you") return base;

const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors;
const colors = getMobileThemeVariables(themeId, scheme);
const background = themeColorToNativeColor(palette.terminalBackground);
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/lib/mobileTheme.test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function readDefaultMobileThemeVariables(
return Object.fromEntries(
Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [
name,
value.trim(),
(value ?? "").trim(),
]),
) as MobileThemeVariables;
}
12 changes: 6 additions & 6 deletions apps/mobile/src/lib/mobileTheme.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vite-plus/test";
import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes";
import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES, T3_CHAT_THEME } from "@t3tools/shared/themePalettes";
import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";

import {
Expand Down Expand Up @@ -49,7 +49,7 @@ function compositeOver(overlay: string, background: string): string {

describe("mobile themes", () => {
it("declares every runtime theme variable in the static stylesheet", () => {
const generatedVariables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
const generatedVariables = createMobileThemeVariables(T3_CHAT_THEME.colors, "light");
expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual(
Object.keys(generatedVariables).sort(),
);
Expand Down Expand Up @@ -152,16 +152,16 @@ describe("mobile themes", () => {
});

it("maps semantic palette roles onto every mobile color variable", () => {
const variables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
const variables = createMobileThemeVariables(T3_CHAT_THEME.colors, "light");
expect(Object.keys(variables)).toHaveLength(75);
expect(variables["--color-sheet-solid"]).toBe(
themeColorToNativeColor(BUILT_IN_THEMES[0].colors.chrome),
themeColorToNativeColor(T3_CHAT_THEME.colors.chrome),
);
expect(variables["--color-warning"]).toBe(
themeColorToNativeColor(BUILT_IN_THEMES[0].colors.warningSurface),
themeColorToNativeColor(T3_CHAT_THEME.colors.warningSurface),
);
expect(variables["--color-warning-foreground"]).toBe(
themeColorToNativeColor(BUILT_IN_THEMES[0].colors.warningForeground),
themeColorToNativeColor(T3_CHAT_THEME.colors.warningForeground),
);
expect(variables["--color-primary"]).not.toBe(variables["--color-screen"]);
expect(variables["--color-primary-shadow"]).toBe("#000000");
Expand Down
28 changes: 14 additions & 14 deletions apps/mobile/src/lib/mobileTheme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BUILT_IN_THEMES,
T3_CHAT_THEME,
getThemeColorsForAppearance,
MOBILE_DEFAULT_THEME_ID,
MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,
Expand Down Expand Up @@ -29,7 +30,9 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
];

export type MobileThemeVariable = `--color-${string}`;
// Closed set: every key `createMobileThemeVariables` writes. Reads of a
// misspelled variable then fail to compile instead of yielding undefined.
export type MobileThemeVariable = keyof ReturnType<typeof createMobileThemeVariables>;
export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;

export function normalizeMobileThemeId(value: unknown): MobileThemeId {
Expand Down Expand Up @@ -136,9 +139,9 @@ function withAlpha(color: string, alpha: number): string {

function rgbChannels(color: string): readonly [number, number, number] | null {
const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color);
return match
? [Number.parseInt(match[1], 16), Number.parseInt(match[2], 16), Number.parseInt(match[3], 16)]
: null;
if (!match) return null;
const [, red = "0", green = "0", blue = "0"] = match;
return [Number.parseInt(red, 16), Number.parseInt(green, 16), Number.parseInt(blue, 16)];
}

/**
Expand Down Expand Up @@ -216,18 +219,15 @@ function readableMessageAccent(accent: string, surface: string): string {
}

export function themeColorWithAlpha(color: string, alpha: number): string {
const hex = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color);
if (hex) {
return `rgba(${Number.parseInt(hex[1], 16)}, ${Number.parseInt(hex[2], 16)}, ${Number.parseInt(hex[3], 16)}, ${alpha})`;
const channels = rgbChannels(color);
if (channels) {
return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${alpha})`;
}
const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/.exec(color);
return rgb ? `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})` : color;
}

export function createMobileThemeVariables(
colors: ThemeColors,
appearance: MobileThemeAppearance,
): MobileThemeVariables {
export function createMobileThemeVariables(colors: ThemeColors, appearance: MobileThemeAppearance) {
const c = nativeColors(colors);
return {
"--color-screen": c.canvas,
Expand Down Expand Up @@ -315,15 +315,15 @@ export function createMobileThemeVariables(
}

export const MOBILE_THEME_VARIABLE_NAMES = Object.keys(
createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"),
createMobileThemeVariables(T3_CHAT_THEME.colors, "light"),
) as ReadonlyArray<MobileThemeVariable>;

export function getMobileThemeVariables(
themeId: BuiltInThemeId,
appearance: MobileThemeAppearance,
overrides: Partial<MobileThemeVariables> | null = null,
): MobileThemeVariables {
const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
const baseVariables = createMobileThemeVariables(colors, appearance);

Expand All @@ -337,7 +337,7 @@ export function getMobileThemePreviewColors(
): ThemePreviewColors {
if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you")
return STANDARD_THEME_PREVIEW_COLORS[appearance];
const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME;
const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
return {
canvas: themeColorToNativeColor(colors.canvas),
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/lib/wideMarkdownBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ function hasOrderedListItem(text: string): boolean {
const nestedMatch = INDENTED_ORDERED_LIST_ITEM.exec(line);
const parentMatch =
previousNonEmptyLine === null ? null : ANY_LIST_ITEM.exec(previousNonEmptyLine);
if (nestedMatch && parentMatch && parentMatch[1].length < nestedMatch[1].length) {
if (
nestedMatch?.[1] !== undefined &&
parentMatch?.[1] !== undefined &&
parentMatch[1].length < nestedMatch[1].length
) {
return true;
}

Expand Down
4 changes: 3 additions & 1 deletion apps/mobile/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"extends": "expo/tsconfig.base",
"compilerOptions": {
"allowImportingTsExtensions": true,
"strict": true
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}
Loading