| author | |
| committer | |
| log | 20a8f1de3668e0ff4da236d75258e2184a68d727 |
| tree | 890d47d0b4be2104662ee7d15e33af64ac0d02a5 |
| parent | db6e0531e4faac0fc5ac0e12088b01bed6e7b851 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 98 | } |
| 99 | 99 | |
| 100 | 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 | } |
| 103 | 103 | |
| 104 | 104 | function 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 | 53 | // These replace the remaining dark:* utility pairs. A registered palette theme is |
| 54 | 54 | // neither literally `light` nor `dark`, so appearance-sensitive values must also be |
| 55 | 55 | // represented as semantic variables for custom themes. |
| 56 | const ADAPTIVE_COLORS = { | |
| 56 | const ADAPTIVE_COLORS: Readonly<Record<string, readonly [light: string, dark: string]>> = { | |
| 57 | 57 | "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)], |
| 58 | 58 | "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)], |
| 59 | 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 | 143 | |
| 144 | 144 | const adaptiveVariablesFor = (appearance: MobileThemeAppearance) => |
| 145 | 145 | Object.fromEntries( |
| 146 | Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [ | |
| 146 | Object.entries(ADAPTIVE_COLORS).map(([name, [light, dark]]) => [ | |
| 147 | 147 | name, |
| 148 | values[appearance === "light" ? 0 : 1], | |
| 148 | appearance === "light" ? light : dark, | |
| 149 | 149 | ]), |
| 150 | 150 | ); |
| 151 | 151 |
apps/mobile/src/components/AndroidAnchoredMenu.tsx+1-1| ... | ... | @@ -125,7 +125,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 125 | 125 | return () => subscription.remove(); |
| 126 | 126 | }, [anchor, close, submenuDepth]); |
| 127 | 127 | |
| 128 | const parent = path.length > 0 ? path[path.length - 1] : null; | |
| 128 | const parent = path[path.length - 1] ?? null; | |
| 129 | 129 | const levelActions = (parent?.subactions ?? props.actions).filter( |
| 130 | 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 | 53 | const alpha = rgba[4] === undefined ? 1 : Math.min(1, Math.max(0, Number(rgba[4]))); |
| 54 | 54 | const channels = [1, 2, 3].map((index) => { |
| 55 | 55 | const foreground = Number(rgba[index]); |
| 56 | const behind = Number.parseInt(backgroundHex[index], 16); | |
| 56 | const behind = Number.parseInt(backgroundHex[index] ?? "0", 16); | |
| 57 | 57 | return Math.round(foreground * alpha + behind * (1 - alpha)); |
| 58 | 58 | }); |
| 59 | 59 | return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; |
| ... | ... | @@ -358,6 +358,9 @@ function addNativeWordDiffRanges( |
| 358 | 358 | for (let pairIndex = 0; pairIndex < pairedCount; pairIndex += 1) { |
| 359 | 359 | const deletedRowIndex = deletedRowIndexes[pairIndex]; |
| 360 | 360 | const addedRowIndex = addedRowIndexes[pairIndex]; |
| 361 | if (deletedRowIndex === undefined || addedRowIndex === undefined) { | |
| 362 | continue; | |
| 363 | } | |
| 361 | 364 | const deletedRow = nextRows[deletedRowIndex]; |
| 362 | 365 | const addedRow = nextRows[addedRowIndex]; |
| 363 | 366 | if (!deletedRow?.content || !addedRow?.content) { |
apps/mobile/src/features/review/reviewModel.test.ts+2-2| ... | ... | @@ -84,7 +84,7 @@ describe("buildReviewSectionItems", () => { |
| 84 | 84 | }, |
| 85 | 85 | ]; |
| 86 | 86 | |
| 87 | const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]); | |
| 87 | const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]!); | |
| 88 | 88 | const items = buildReviewSectionItems({ |
| 89 | 89 | checkpoints, |
| 90 | 90 | gitSections, |
| ... | ... | @@ -92,7 +92,7 @@ describe("buildReviewSectionItems", () => { |
| 92 | 92 | [loadedTurnId]: "diff --git a/loaded.ts b/loaded.ts", |
| 93 | 93 | }, |
| 94 | 94 | loadingTurnIds: { |
| 95 | [getReviewSectionIdForCheckpoint(checkpoints[1])]: true, | |
| 95 | [getReviewSectionIdForCheckpoint(checkpoints[1]!)]: true, | |
| 96 | 96 | }, |
| 97 | 97 | loadingGitSections: false, |
| 98 | 98 | }); |
apps/mobile/src/features/terminal/terminalTheme.ts+27-3| ... | ... | @@ -1,4 +1,8 @@ |
| 1 | import { BUILT_IN_THEMES, getThemeColorsForAppearance } from "@t3tools/shared/themePalettes"; | |
| 1 | import { | |
| 2 | BUILT_IN_THEMES, | |
| 3 | T3_CHAT_THEME, | |
| 4 | getThemeColorsForAppearance, | |
| 5 | } from "@t3tools/shared/themePalettes"; | |
| 2 | 6 | |
| 3 | 7 | import { |
| 4 | 8 | getMobileThemeVariables, |
| ... | ... | @@ -15,9 +19,29 @@ export interface TerminalTheme { |
| 15 | 19 | readonly border: string; |
| 16 | 20 | readonly cursorForeground: string; |
| 17 | 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 | } |
| 20 | 25 | |
| 26 | type 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 | ||
| 21 | 45 | const PIERRE_LIGHT_THEME: TerminalTheme = { |
| 22 | 46 | // Pierre terminal palette with the app's shared screen background. |
| 23 | 47 | background: "#f2f2f7", |
| ... | ... | @@ -85,7 +109,7 @@ export function getMobileTerminalTheme( |
| 85 | 109 | const base = getPierreTerminalTheme(scheme); |
| 86 | 110 | if (themeId === "t3-code" || themeId === "material-you") return base; |
| 87 | 111 | |
| 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 | 113 | const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors; |
| 90 | 114 | const colors = getMobileThemeVariables(themeId, scheme); |
| 91 | 115 | const background = themeColorToNativeColor(palette.terminalBackground); |
apps/mobile/src/lib/mobileTheme.test-support.ts+1-1| ... | ... | @@ -14,7 +14,7 @@ export function readDefaultMobileThemeVariables( |
| 14 | 14 | return Object.fromEntries( |
| 15 | 15 | Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [ |
| 16 | 16 | name, |
| 17 | value.trim(), | |
| 17 | (value ?? "").trim(), | |
| 18 | 18 | ]), |
| 19 | 19 | ) as MobileThemeVariables; |
| 20 | 20 | } |
apps/mobile/src/lib/mobileTheme.test.ts+6-6| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes"; | |
| 2 | import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES, T3_CHAT_THEME } from "@t3tools/shared/themePalettes"; | |
| 3 | 3 | import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support"; |
| 4 | 4 | |
| 5 | 5 | import { |
| ... | ... | @@ -49,7 +49,7 @@ function compositeOver(overlay: string, background: string): string { |
| 49 | 49 | |
| 50 | 50 | describe("mobile themes", () => { |
| 51 | 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 | 53 | expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual( |
| 54 | 54 | Object.keys(generatedVariables).sort(), |
| 55 | 55 | ); |
| ... | ... | @@ -152,16 +152,16 @@ describe("mobile themes", () => { |
| 152 | 152 | }); |
| 153 | 153 | |
| 154 | 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 | 156 | expect(Object.keys(variables)).toHaveLength(75); |
| 157 | 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 | 160 | expect(variables["--color-warning"]).toBe( |
| 161 | themeColorToNativeColor(BUILT_IN_THEMES[0].colors.warningSurface), | |
| 161 | themeColorToNativeColor(T3_CHAT_THEME.colors.warningSurface), | |
| 162 | 162 | ); |
| 163 | 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 | 166 | expect(variables["--color-primary"]).not.toBe(variables["--color-screen"]); |
| 167 | 167 | expect(variables["--color-primary-shadow"]).toBe("#000000"); |
apps/mobile/src/lib/mobileTheme.ts+14-14| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { |
| 2 | 2 | BUILT_IN_THEMES, |
| 3 | T3_CHAT_THEME, | |
| 3 | 4 | getThemeColorsForAppearance, |
| 4 | 5 | MOBILE_DEFAULT_THEME_ID, |
| 5 | 6 | MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS, |
| ... | ... | @@ -29,7 +30,9 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ |
| 29 | 30 | ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })), |
| 30 | 31 | ]; |
| 31 | 32 | |
| 32 | export 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. | |
| 35 | export type MobileThemeVariable = keyof ReturnType<typeof createMobileThemeVariables>; | |
| 33 | 36 | export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>; |
| 34 | 37 | |
| 35 | 38 | export function normalizeMobileThemeId(value: unknown): MobileThemeId { |
| ... | ... | @@ -136,9 +139,9 @@ function withAlpha(color: string, alpha: number): string { |
| 136 | 139 | |
| 137 | 140 | function rgbChannels(color: string): readonly [number, number, number] | null { |
| 138 | 141 | const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color); |
| 139 | return match | |
| 140 | ? [Number.parseInt(match[1], 16), Number.parseInt(match[2], 16), Number.parseInt(match[3], 16)] | |
| 141 | : null; | |
| 142 | if (!match) return null; | |
| 143 | const [, red = "0", green = "0", blue = "0"] = match; | |
| 144 | return [Number.parseInt(red, 16), Number.parseInt(green, 16), Number.parseInt(blue, 16)]; | |
| 142 | 145 | } |
| 143 | 146 | |
| 144 | 147 | /** |
| ... | ... | @@ -216,18 +219,15 @@ function readableMessageAccent(accent: string, surface: string): string { |
| 216 | 219 | } |
| 217 | 220 | |
| 218 | 221 | export function themeColorWithAlpha(color: string, alpha: number): string { |
| 219 | const hex = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(color); | |
| 220 | if (hex) { | |
| 221 | return `rgba(${Number.parseInt(hex[1], 16)}, ${Number.parseInt(hex[2], 16)}, ${Number.parseInt(hex[3], 16)}, ${alpha})`; | |
| 222 | const channels = rgbChannels(color); | |
| 223 | if (channels) { | |
| 224 | return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${alpha})`; | |
| 222 | 225 | } |
| 223 | 226 | const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/.exec(color); |
| 224 | 227 | return rgb ? `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})` : color; |
| 225 | 228 | } |
| 226 | 229 | |
| 227 | export function createMobileThemeVariables( | |
| 228 | colors: ThemeColors, | |
| 229 | appearance: MobileThemeAppearance, | |
| 230 | ): MobileThemeVariables { | |
| 230 | export function createMobileThemeVariables(colors: ThemeColors, appearance: MobileThemeAppearance) { | |
| 231 | 231 | const c = nativeColors(colors); |
| 232 | 232 | return { |
| 233 | 233 | "--color-screen": c.canvas, |
| ... | ... | @@ -315,7 +315,7 @@ export function createMobileThemeVariables( |
| 315 | 315 | } |
| 316 | 316 | |
| 317 | 317 | export const MOBILE_THEME_VARIABLE_NAMES = Object.keys( |
| 318 | createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"), | |
| 318 | createMobileThemeVariables(T3_CHAT_THEME.colors, "light"), | |
| 319 | 319 | ) as ReadonlyArray<MobileThemeVariable>; |
| 320 | 320 | |
| 321 | 321 | export function getMobileThemeVariables( |
| ... | ... | @@ -323,7 +323,7 @@ export function getMobileThemeVariables( |
| 323 | 323 | appearance: MobileThemeAppearance, |
| 324 | 324 | overrides: Partial<MobileThemeVariables> | null = null, |
| 325 | 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 | 327 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; |
| 328 | 328 | const baseVariables = createMobileThemeVariables(colors, appearance); |
| 329 | 329 | |
| ... | ... | @@ -337,7 +337,7 @@ export function getMobileThemePreviewColors( |
| 337 | 337 | ): ThemePreviewColors { |
| 338 | 338 | if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you") |
| 339 | 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 | 341 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; |
| 342 | 342 | return { |
| 343 | 343 | canvas: themeColorToNativeColor(colors.canvas), |
apps/mobile/src/lib/wideMarkdownBlocks.ts+5-1| ... | ... | @@ -81,7 +81,11 @@ function hasOrderedListItem(text: string): boolean { |
| 81 | 81 | const nestedMatch = INDENTED_ORDERED_LIST_ITEM.exec(line); |
| 82 | 82 | const parentMatch = |
| 83 | 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 | 89 | return true; |
| 86 | 90 | } |
| 87 | 91 |
apps/mobile/tsconfig.json+3-1| ... | ... | @@ -2,6 +2,8 @@ |
| 2 | 2 | "extends": "expo/tsconfig.base", |
| 3 | 3 | "compilerOptions": { |
| 4 | 4 | "allowImportingTsExtensions": true, |
| 5 | "strict": true | |
| 5 | "strict": true, | |
| 6 | "noUncheckedIndexedAccess": true, | |
| 7 | "noImplicitOverride": true | |
| 6 | 8 | } |
| 7 | 9 | } |