| author | |
| committer | |
| log | 579266caa89238611ddb974ffebe9968c6a6e5d5 |
| tree | 36bb21155962260c99071defad07a8fcf4e76d25 |
| parent | 47eed9face4186635488fba5f2494eddd13f6491 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Pixel Perfect <me@pixp.cc>19 files changed, 374 insertions(+), 25 deletions(-)
apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt+40-1| ... | @@ -7,11 +7,13 @@ import android.graphics.Canvas | ... | @@ -7,11 +7,13 @@ import android.graphics.Canvas |
| 7 | import android.graphics.Paint | 7 | import android.graphics.Paint |
| 8 | import android.graphics.RectF | 8 | import android.graphics.RectF |
| 9 | import android.graphics.Typeface | 9 | import android.graphics.Typeface |
| 10 | import android.os.Build | ||
| 10 | import android.text.Editable | 11 | import android.text.Editable |
| 11 | import android.text.InputType | 12 | import android.text.InputType |
| 12 | import android.text.Spanned | 13 | import android.text.Spanned |
| 13 | import android.text.TextWatcher | 14 | import android.text.TextWatcher |
| 14 | import android.text.style.ReplacementSpan | 15 | import android.text.style.ReplacementSpan |
| 16 | import android.util.TypedValue | ||
| 15 | import android.view.Gravity | 17 | import android.view.Gravity |
| 16 | import android.view.ViewGroup | 18 | import android.view.ViewGroup |
| 17 | import android.view.inputmethod.InputMethodManager | 19 | import android.view.inputmethod.InputMethodManager |
| ... | @@ -27,6 +29,11 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( | ... | @@ -27,6 +29,11 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( |
| 27 | appContext | 29 | appContext |
| 28 | ) { | 30 | ) { |
| 29 | private val editor = SelectionAwareEditText(context) | 31 | private val editor = SelectionAwareEditText(context) |
| 32 | private val defaultHighlightColor = editor.highlightColor | ||
| 33 | private val defaultSelectionColor = context.resolveThemeColor( | ||
| 34 | android.R.attr.colorAccent, | ||
| 35 | editor.currentTextColor, | ||
| 36 | ) | ||
| 30 | private val onComposerChange by EventDispatcher() | 37 | private val onComposerChange by EventDispatcher() |
| 31 | private val onComposerSelectionChange by EventDispatcher() | 38 | private val onComposerSelectionChange by EventDispatcher() |
| 32 | private val onComposerFocus by EventDispatcher() | 39 | private val onComposerFocus by EventDispatcher() |
| ... | @@ -152,6 +159,11 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( | ... | @@ -152,6 +159,11 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( |
| 152 | val theme = JSONObject(themeJson) | 159 | val theme = JSONObject(themeJson) |
| 153 | editor.setTextColor(parseColor(theme.optString("text"), Color.BLACK)) | 160 | editor.setTextColor(parseColor(theme.optString("text"), Color.BLACK)) |
| 154 | editor.setHintTextColor(parseColor(theme.optString("placeholder"), Color.GRAY)) | 161 | editor.setHintTextColor(parseColor(theme.optString("placeholder"), Color.GRAY)) |
| 162 | if (theme.isNull("selection")) { | ||
| 163 | resetSelectionTheme() | ||
| 164 | } else { | ||
| 165 | applySelectionTheme(parseColor(theme.optString("selection"), editor.currentTextColor)) | ||
| 166 | } | ||
| 155 | chipTheme = ComposerChipTheme( | 167 | chipTheme = ComposerChipTheme( |
| 156 | chipBackground = parseColor(theme.optString("chipBackground"), chipTheme.chipBackground), | 168 | chipBackground = parseColor(theme.optString("chipBackground"), chipTheme.chipBackground), |
| 157 | chipBorder = parseColor(theme.optString("chipBorder"), chipTheme.chipBorder), | 169 | chipBorder = parseColor(theme.optString("chipBorder"), chipTheme.chipBorder), |
| ... | @@ -277,6 +289,21 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( | ... | @@ -277,6 +289,21 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( |
| 277 | editor.setLineSpacing(max(0, desiredLineHeightPx - fontHeight).toFloat(), 1f) | 289 | editor.setLineSpacing(max(0, desiredLineHeightPx - fontHeight).toFloat(), 1f) |
| 278 | } | 290 | } |
| 279 | 291 | ||
| 292 | private fun applySelectionTheme(color: Int) { | ||
| 293 | editor.highlightColor = color.withAlpha(0x52) | ||
| 294 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return | ||
| 295 | |||
| 296 | editor.textCursorDrawable = editor.textCursorDrawable?.mutate()?.apply { setTint(color) } | ||
| 297 | editor.textSelectHandle?.mutate()?.setTint(color) | ||
| 298 | editor.textSelectHandleLeft?.mutate()?.setTint(color) | ||
| 299 | editor.textSelectHandleRight?.mutate()?.setTint(color) | ||
| 300 | } | ||
| 301 | |||
| 302 | private fun resetSelectionTheme() { | ||
| 303 | applySelectionTheme(defaultSelectionColor) | ||
| 304 | editor.highlightColor = defaultHighlightColor | ||
| 305 | } | ||
| 306 | |||
| 280 | private fun currentSelectionPayload(): Map<String, Int> = | 307 | private fun currentSelectionPayload(): Map<String, Int> = |
| 281 | mapOf( | 308 | mapOf( |
| 282 | "start" to editor.selectionStart.coerceAtLeast(0), | 309 | "start" to editor.selectionStart.coerceAtLeast(0), |
| ... | @@ -335,12 +362,24 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( | ... | @@ -335,12 +362,24 @@ class T3ComposerEditorView(context: Context, appContext: AppContext) : ExpoView( |
| 335 | 362 | ||
| 336 | private fun parseColor(value: String, fallback: Int): Int = | 363 | private fun parseColor(value: String, fallback: Int): Int = |
| 337 | try { | 364 | try { |
| 338 | Color.parseColor(value) | 365 | val androidColor = if (value.length == 9 && value.startsWith("#")) { |
| 366 | "#${value.takeLast(2)}${value.substring(1, 7)}" | ||
| 367 | } else { | ||
| 368 | value | ||
| 369 | } | ||
| 370 | Color.parseColor(androidColor) | ||
| 339 | } catch (_: Exception) { | 371 | } catch (_: Exception) { |
| 340 | fallback | 372 | fallback |
| 341 | } | 373 | } |
| 342 | } | 374 | } |
| 343 | 375 | ||
| 376 | private fun Int.withAlpha(alpha: Int): Int = (this and 0x00FFFFFF) or (alpha shl 24) | ||
| 377 | |||
| 378 | private fun Context.resolveThemeColor(attribute: Int, fallback: Int): Int { | ||
| 379 | val value = TypedValue() | ||
| 380 | return if (theme.resolveAttribute(attribute, value, true)) value.data else fallback | ||
| 381 | } | ||
| 382 | |||
| 344 | private data class ComposerToken( | 383 | private data class ComposerToken( |
| 345 | val type: String, | 384 | val type: String, |
| 346 | val source: String, | 385 | val source: String, |
apps/mobile/package.json+1| ... | @@ -51,6 +51,7 @@ | ... | @@ -51,6 +51,7 @@ |
| 51 | "@expo/metro-runtime": "~57.0.14", | 51 | "@expo/metro-runtime": "~57.0.14", |
| 52 | "@expo/ui": "~57.0.14", | 52 | "@expo/ui": "~57.0.14", |
| 53 | "@legendapp/list": "catalog:", | 53 | "@legendapp/list": "catalog:", |
| 54 | "@material/material-color-utilities": "0.3.0", | ||
| 54 | "@noble/curves": "catalog:", | 55 | "@noble/curves": "catalog:", |
| 55 | "@pierre/diffs": "catalog:", | 56 | "@pierre/diffs": "catalog:", |
| 56 | "@react-native-ai/apple": "0.12.0", | 57 | "@react-native-ai/apple": "0.12.0", |
apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts+1-1| ... | @@ -66,7 +66,7 @@ function filesPatch(paths: ReadonlyArray<string>) { | ... | @@ -66,7 +66,7 @@ function filesPatch(paths: ReadonlyArray<string>) { |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) { | 68 | function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) { |
| 69 | return themeId === DEFAULT_MOBILE_THEME_ID | 69 | return themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you" |
| 70 | ? readDefaultMobileThemeVariables(appearance) | 70 | ? readDefaultMobileThemeVariables(appearance) |
| 71 | : getMobileThemeVariables(themeId, appearance); | 71 | : getMobileThemeVariables(themeId, appearance); |
| 72 | } | 72 | } |
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+60-3| ... | @@ -3,17 +3,19 @@ import { | ... | @@ -3,17 +3,19 @@ import { |
| 3 | startTransition, | 3 | startTransition, |
| 4 | use, | 4 | use, |
| 5 | useCallback, | 5 | useCallback, |
| 6 | useEffect, | ||
| 7 | useState, | ||
| 6 | useLayoutEffect, | 8 | useLayoutEffect, |
| 7 | useMemo, | 9 | useMemo, |
| 8 | useRef, | 10 | useRef, |
| 9 | type ReactNode, | 11 | type ReactNode, |
| 10 | } from "react"; | 12 | } from "react"; |
| 11 | import { Appearance, useColorScheme } from "react-native"; | 13 | import { AppState, Appearance, useColorScheme } from "react-native"; |
| 12 | 14 | ||
| 13 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | 15 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 14 | import { AsyncResult } from "effect/unstable/reactivity"; | 16 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 15 | 17 | ||
| 16 | import { ScopedTheme, Uniwind } from "uniwind"; | 18 | import { ScopedTheme, ScopedVariables, Uniwind } from "uniwind"; |
| 17 | 19 | ||
| 18 | import { | 20 | import { |
| 19 | resolveAppearance, | 21 | resolveAppearance, |
| ... | @@ -22,6 +24,10 @@ import { | ... | @@ -22,6 +24,10 @@ import { |
| 22 | } from "../../../lib/appearancePreferences"; | 24 | } from "../../../lib/appearancePreferences"; |
| 23 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences"; | 25 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences"; |
| 24 | import type { Preferences } from "../../../persistence/mobile-preferences"; | 26 | import type { Preferences } from "../../../persistence/mobile-preferences"; |
| 27 | import { isSystemColorsAvailable, readSystemColorPalettes } from "../../../lib/materialYouPalette"; | ||
| 28 | import { materialYouPaletteToMobileThemeVariables } from "../../../lib/materialYouTheme"; | ||
| 29 | import { getMobileThemeRuntimeVariables } from "../../../lib/mobileThemeVariables"; | ||
| 30 | import type { MobileThemeVariables } from "../../../lib/mobileTheme"; | ||
| 25 | import { | 31 | import { |
| 26 | createMobileThemePairPatch, | 32 | createMobileThemePairPatch, |
| 27 | createMobileThemeSelectionPatch, | 33 | createMobileThemeSelectionPatch, |
| ... | @@ -45,6 +51,13 @@ interface AppearancePreferencesContextValue { | ... | @@ -45,6 +51,13 @@ interface AppearancePreferencesContextValue { |
| 45 | readonly themeIds: MobileThemeIds; | 51 | readonly themeIds: MobileThemeIds; |
| 46 | readonly themeMode: MobileThemeMode; | 52 | readonly themeMode: MobileThemeMode; |
| 47 | readonly themeAppearance: MobileThemeAppearance; | 53 | readonly themeAppearance: MobileThemeAppearance; |
| 54 | readonly systemColorsAvailable: boolean; | ||
| 55 | readonly systemColorsActive: boolean; | ||
| 56 | readonly themeVariables: MobileThemeVariables; | ||
| 57 | readonly themeVariablesByAppearance: Readonly< | ||
| 58 | Record<MobileThemeAppearance, MobileThemeVariables> | ||
| 59 | >; | ||
| 60 | readonly systemColorPalettes: ReturnType<typeof readSystemColorPalettes>; | ||
| 48 | readonly isReady: boolean; | 61 | readonly isReady: boolean; |
| 49 | readonly setThemeIdForAppearance: ( | 62 | readonly setThemeIdForAppearance: ( |
| 50 | appearance: MobileThemeAppearance, | 63 | appearance: MobileThemeAppearance, |
| ... | @@ -81,6 +94,39 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN | ... | @@ -81,6 +94,39 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 81 | [resolvedThemeIds.dark, resolvedThemeIds.light], | 94 | [resolvedThemeIds.dark, resolvedThemeIds.light], |
| 82 | ); | 95 | ); |
| 83 | const themeId = themeIds[themeAppearance]; | 96 | const themeId = themeIds[themeAppearance]; |
| 97 | const systemColorsActive = themeId === "material-you" && isSystemColorsAvailable; | ||
| 98 | const [systemColorPalettes, setSystemColorPalettes] = useState(readSystemColorPalettes); | ||
| 99 | useEffect(() => { | ||
| 100 | if (!isSystemColorsAvailable) return; | ||
| 101 | const refresh = () => { | ||
| 102 | const next = readSystemColorPalettes(); | ||
| 103 | setSystemColorPalettes((previous) => | ||
| 104 | JSON.stringify(previous) === JSON.stringify(next) ? previous : next, | ||
| 105 | ); | ||
| 106 | }; | ||
| 107 | const subscription = AppState.addEventListener("change", (state) => { | ||
| 108 | if (state === "active") refresh(); | ||
| 109 | }); | ||
| 110 | const focusSubscription = AppState.addEventListener("focus", refresh); | ||
| 111 | return () => { | ||
| 112 | subscription.remove(); | ||
| 113 | focusSubscription.remove(); | ||
| 114 | }; | ||
| 115 | }, []); | ||
| 116 | const themeVariablesByAppearance = useMemo(() => { | ||
| 117 | const resolve = (appearance: MobileThemeAppearance) => { | ||
| 118 | const base = getMobileThemeRuntimeVariables(themeIds[appearance], appearance); | ||
| 119 | return themeIds[appearance] === "material-you" && systemColorPalettes | ||
| 120 | ? materialYouPaletteToMobileThemeVariables( | ||
| 121 | systemColorPalettes[appearance], | ||
| 122 | appearance, | ||
| 123 | base, | ||
| 124 | ) | ||
| 125 | : base; | ||
| 126 | }; | ||
| 127 | return { light: resolve("light"), dark: resolve("dark") }; | ||
| 128 | }, [themeIds, systemColorPalettes]); | ||
| 129 | const themeVariables = themeVariablesByAppearance[themeAppearance]; | ||
| 84 | const activeThemeName = getMobileUniwindThemeName(themeId, themeAppearance); | 130 | const activeThemeName = getMobileUniwindThemeName(themeId, themeAppearance); |
| 85 | const { baseFontSize, codeFontSize, codeWordBreak, terminalFontSize } = preferences; | 131 | const { baseFontSize, codeFontSize, codeWordBreak, terminalFontSize } = preferences; |
| 86 | const appearance = useMemo( | 132 | const appearance = useMemo( |
| ... | @@ -233,6 +279,11 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN | ... | @@ -233,6 +279,11 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 233 | themeIds, | 279 | themeIds, |
| 234 | themeMode, | 280 | themeMode, |
| 235 | themeAppearance, | 281 | themeAppearance, |
| 282 | systemColorsAvailable: isSystemColorsAvailable, | ||
| 283 | systemColorsActive, | ||
| 284 | themeVariables, | ||
| 285 | themeVariablesByAppearance, | ||
| 286 | systemColorPalettes, | ||
| 236 | isReady, | 287 | isReady, |
| 237 | setThemeIdForAppearance, | 288 | setThemeIdForAppearance, |
| 238 | setThemeIdForBothAppearances, | 289 | setThemeIdForBothAppearances, |
| ... | @@ -248,6 +299,10 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN | ... | @@ -248,6 +299,10 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 248 | themeIds, | 299 | themeIds, |
| 249 | themeMode, | 300 | themeMode, |
| 250 | themeAppearance, | 301 | themeAppearance, |
| 302 | systemColorsActive, | ||
| 303 | themeVariables, | ||
| 304 | themeVariablesByAppearance, | ||
| 305 | systemColorPalettes, | ||
| 251 | isReady, | 306 | isReady, |
| 252 | setThemeIdForAppearance, | 307 | setThemeIdForAppearance, |
| 253 | setThemeIdForBothAppearances, | 308 | setThemeIdForBothAppearances, |
| ... | @@ -261,7 +316,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN | ... | @@ -261,7 +316,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 261 | 316 | ||
| 262 | return ( | 317 | return ( |
| 263 | <AppearancePreferencesContext.Provider value={value}> | 318 | <AppearancePreferencesContext.Provider value={value}> |
| 264 | <ScopedTheme theme={activeThemeName}>{props.children}</ScopedTheme> | 319 | <ScopedTheme theme={activeThemeName}> |
| 320 | <ScopedVariables variables={themeVariables}>{props.children}</ScopedVariables> | ||
| 321 | </ScopedTheme> | ||
| 265 | </AppearancePreferencesContext.Provider> | 322 | </AppearancePreferencesContext.Provider> |
| 266 | ); | 323 | ); |
| 267 | } | 324 | } |
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+21-6| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { memo, useId } from "react"; | 1 | import { memo, useId } from "react"; |
| 2 | import { Pressable, View } from "react-native"; | 2 | import { Pressable, View } from "react-native"; |
| 3 | import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg"; | 3 | import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg"; |
| 4 | import { ScopedTheme } from "uniwind"; | 4 | import { ScopedTheme, ScopedVariables } from "uniwind"; |
| 5 | 5 | ||
| 6 | import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview"; | 6 | import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview"; |
| 7 | 7 | ||
| ... | @@ -38,7 +38,12 @@ const PreviewOrb = memo(function PreviewOrb(props: { | ... | @@ -38,7 +38,12 @@ const PreviewOrb = memo(function PreviewOrb(props: { |
| 38 | const idPrefix = useId().replaceAll(":", ""); | 38 | const idPrefix = useId().replaceAll(":", ""); |
| 39 | const accentGradientId = `${idPrefix}-accent-glow`; | 39 | const accentGradientId = `${idPrefix}-accent-glow`; |
| 40 | const actionGradientId = `${idPrefix}-action-glow`; | 40 | const actionGradientId = `${idPrefix}-action-glow`; |
| 41 | const colors = getMobileThemePreviewColors(props.themeId, props.appearance); | 41 | const { systemColorPalettes } = useAppearancePreferences(); |
| 42 | const palette = systemColorPalettes?.[props.appearance]; | ||
| 43 | const colors = | ||
| 44 | props.themeId === "material-you" && palette | ||
| 45 | ? { canvas: palette.surface, accent: palette.primary, messageAction: palette.tertiary } | ||
| 46 | : getMobileThemePreviewColors(props.themeId, props.appearance); | ||
| 42 | const spec = THEME_PREVIEW_RENDER_SPECS[props.appearance]; | 47 | const spec = THEME_PREVIEW_RENDER_SPECS[props.appearance]; |
| 43 | const accentRadius = Math.hypot( | 48 | const accentRadius = Math.hypot( |
| 44 | Math.max(spec.accent.center[0], 1 - spec.accent.center[0]), | 49 | Math.max(spec.accent.center[0], 1 - spec.accent.center[0]), |
| ... | @@ -204,15 +209,20 @@ function PreviewPane(props: { readonly compact?: boolean }) { | ... | @@ -204,15 +209,20 @@ function PreviewPane(props: { readonly compact?: boolean }) { |
| 204 | } | 209 | } |
| 205 | 210 | ||
| 206 | function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) { | 211 | function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) { |
| 212 | const { themeVariablesByAppearance } = useAppearancePreferences(); | ||
| 207 | if (props.mode === "system") { | 213 | if (props.mode === "system") { |
| 208 | return ( | 214 | return ( |
| 209 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> | 215 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> |
| 210 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> | 216 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> |
| 211 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.light, "light")}> | 217 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.light, "light")}> |
| 212 | <PreviewPane compact /> | 218 | <ScopedVariables variables={themeVariablesByAppearance.light}> |
| 219 | <PreviewPane compact /> | ||
| 220 | </ScopedVariables> | ||
| 213 | </ScopedTheme> | 221 | </ScopedTheme> |
| 214 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.dark, "dark")}> | 222 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.dark, "dark")}> |
| 215 | <PreviewPane compact /> | 223 | <ScopedVariables variables={themeVariablesByAppearance.dark}> |
| 224 | <PreviewPane compact /> | ||
| 225 | </ScopedVariables> | ||
| 216 | </ScopedTheme> | 226 | </ScopedTheme> |
| 217 | </View> | 227 | </View> |
| 218 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> | 228 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> |
| ... | @@ -224,7 +234,9 @@ function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: | ... | @@ -224,7 +234,9 @@ function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: |
| 224 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds[props.mode], props.mode)}> | 234 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds[props.mode], props.mode)}> |
| 225 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> | 235 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> |
| 226 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> | 236 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> |
| 227 | <PreviewPane /> | 237 | <ScopedVariables variables={themeVariablesByAppearance[props.mode]}> |
| 238 | <PreviewPane /> | ||
| 239 | </ScopedVariables> | ||
| 228 | </View> | 240 | </View> |
| 229 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> | 241 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> |
| 230 | </View> | 242 | </View> |
| ... | @@ -278,6 +290,7 @@ export function ThemeAppearanceSection() { | ... | @@ -278,6 +290,7 @@ export function ThemeAppearanceSection() { |
| 278 | setThemeMode, | 290 | setThemeMode, |
| 279 | themeIds, | 291 | themeIds, |
| 280 | themeMode, | 292 | themeMode, |
| 293 | systemColorsAvailable, | ||
| 281 | } = useAppearancePreferences(); | 294 | } = useAppearancePreferences(); |
| 282 | 295 | ||
| 283 | return ( | 296 | return ( |
| ... | @@ -302,7 +315,9 @@ export function ThemeAppearanceSection() { | ... | @@ -302,7 +315,9 @@ export function ThemeAppearanceSection() { |
| 302 | <View className="gap-3"> | 315 | <View className="gap-3"> |
| 303 | <SectionLabel>Themes</SectionLabel> | 316 | <SectionLabel>Themes</SectionLabel> |
| 304 | <View className="flex-row flex-wrap gap-3"> | 317 | <View className="flex-row flex-wrap gap-3"> |
| 305 | {MOBILE_THEME_OPTIONS.map((theme) => ( | 318 | {MOBILE_THEME_OPTIONS.filter( |
| 319 | (theme) => theme.id !== "material-you" || systemColorsAvailable, | ||
| 320 | ).map((theme) => ( | ||
| 306 | <ThemeCard | 321 | <ThemeCard |
| 307 | disabled={!isReady} | 322 | disabled={!isReady} |
| 308 | key={theme.id} | 323 | key={theme.id} |
apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx+1| ... | @@ -37,6 +37,7 @@ export function SettingsSwitchRow(props: { | ... | @@ -37,6 +37,7 @@ export function SettingsSwitchRow(props: { |
| 37 | ) : null} | 37 | ) : null} |
| 38 | </View> | 38 | </View> |
| 39 | <ThemedSwitch | 39 | <ThemedSwitch |
| 40 | accessibilityLabel={props.label} | ||
| 40 | disabled={props.disabled} | 41 | disabled={props.disabled} |
| 41 | onValueChange={props.onValueChange} | 42 | onValueChange={props.onValueChange} |
| 42 | value={props.value} | 43 | value={props.value} |
apps/mobile/src/features/terminal/terminalTheme.ts+1-1| ... | @@ -83,7 +83,7 @@ export function getMobileTerminalTheme( | ... | @@ -83,7 +83,7 @@ export function getMobileTerminalTheme( |
| 83 | scheme: TerminalAppearanceScheme, | 83 | scheme: TerminalAppearanceScheme, |
| 84 | ): TerminalTheme { | 84 | ): TerminalTheme { |
| 85 | const base = getPierreTerminalTheme(scheme); | 85 | const base = getPierreTerminalTheme(scheme); |
| 86 | if (themeId === "t3-code") return base; | 86 | if (themeId === "t3-code" || themeId === "material-you") return base; |
| 87 | 87 | ||
| 88 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; | 88 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; |
| 89 | const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors; | 89 | const palette = getThemeColorsForAppearance(theme, scheme) ?? theme.colors; |
apps/mobile/src/lib/materialYouPalette.android.ts created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | import { getMaterialColors, isDynamicColorAvailable } from "@expo/ui/jetpack-compose"; | ||
| 2 | |||
| 3 | import type { MaterialYouPalettes } from "./materialYouPalette"; | ||
| 4 | |||
| 5 | export const isSystemColorsAvailable = isDynamicColorAvailable; | ||
| 6 | |||
| 7 | export function readSystemColorPalettes(): MaterialYouPalettes | null { | ||
| 8 | if (!isDynamicColorAvailable) return null; | ||
| 9 | |||
| 10 | return { | ||
| 11 | light: getMaterialColors({ scheme: "light" }), | ||
| 12 | dark: getMaterialColors({ scheme: "dark" }), | ||
| 13 | }; | ||
| 14 | } | ||
apps/mobile/src/lib/materialYouPalette.ts created+36| ... | @@ -0,0 +1,36 @@ | ||
| 1 | export interface MaterialYouPalette { | ||
| 2 | readonly primary: string; | ||
| 3 | readonly onPrimary: string; | ||
| 4 | readonly primaryContainer: string; | ||
| 5 | readonly onPrimaryContainer: string; | ||
| 6 | readonly inversePrimary: string; | ||
| 7 | readonly secondaryContainer: string; | ||
| 8 | readonly onSecondaryContainer: string; | ||
| 9 | readonly tertiary: string; | ||
| 10 | readonly tertiaryContainer: string; | ||
| 11 | readonly onTertiaryContainer: string; | ||
| 12 | readonly surface: string; | ||
| 13 | readonly onSurface: string; | ||
| 14 | readonly onSurfaceVariant: string; | ||
| 15 | readonly surfaceContainer: string; | ||
| 16 | readonly surfaceContainerHigh: string; | ||
| 17 | readonly surfaceContainerHighest: string; | ||
| 18 | readonly surfaceContainerLow: string; | ||
| 19 | readonly surfaceContainerLowest: string; | ||
| 20 | readonly errorContainer: string; | ||
| 21 | readonly onErrorContainer: string; | ||
| 22 | readonly outline: string; | ||
| 23 | readonly outlineVariant: string; | ||
| 24 | readonly scrim: string; | ||
| 25 | } | ||
| 26 | |||
| 27 | export interface MaterialYouPalettes { | ||
| 28 | readonly light: MaterialYouPalette; | ||
| 29 | readonly dark: MaterialYouPalette; | ||
| 30 | } | ||
| 31 | |||
| 32 | export const isSystemColorsAvailable = false; | ||
| 33 | |||
| 34 | export function readSystemColorPalettes(): MaterialYouPalettes | null { | ||
| 35 | return null; | ||
| 36 | } | ||
apps/mobile/src/lib/materialYouTheme.test.ts created+65| ... | @@ -0,0 +1,65 @@ | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | ||
| 2 | |||
| 3 | import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; | ||
| 4 | import type { MaterialYouPalette } from "./materialYouPalette"; | ||
| 5 | import { materialYouPaletteToMobileThemeVariables } from "./materialYouTheme"; | ||
| 6 | |||
| 7 | const palette: MaterialYouPalette = { | ||
| 8 | primary: "#6750A4FF", | ||
| 9 | onPrimary: "#FFFFFFFF", | ||
| 10 | primaryContainer: "#EADDFFFF", | ||
| 11 | onPrimaryContainer: "#21005DFF", | ||
| 12 | inversePrimary: "#D0BCFFFF", | ||
| 13 | secondaryContainer: "#E8DEF8FF", | ||
| 14 | onSecondaryContainer: "#1D192BFF", | ||
| 15 | tertiary: "#7D5260FF", | ||
| 16 | tertiaryContainer: "#FFD8E4FF", | ||
| 17 | onTertiaryContainer: "#31111DFF", | ||
| 18 | surface: "#FFFBFEFF", | ||
| 19 | onSurface: "#1C1B1FFF", | ||
| 20 | onSurfaceVariant: "#49454FFF", | ||
| 21 | surfaceContainer: "#F3EDF7FF", | ||
| 22 | surfaceContainerHigh: "#ECE6F0FF", | ||
| 23 | surfaceContainerHighest: "#E6E0E9FF", | ||
| 24 | surfaceContainerLow: "#F7F2FAFF", | ||
| 25 | surfaceContainerLowest: "#FFFFFFFF", | ||
| 26 | errorContainer: "#F9DEDCFF", | ||
| 27 | onErrorContainer: "#410E0BFF", | ||
| 28 | outline: "#79747EFF", | ||
| 29 | outlineVariant: "#CAC4D0FF", | ||
| 30 | scrim: "#000000FF", | ||
| 31 | }; | ||
| 32 | |||
| 33 | describe("Material You system colors", () => { | ||
| 34 | it("overrides the selected theme without mutating its base variables", () => { | ||
| 35 | const base = getMobileThemeRuntimeVariables("t3-code", "dark"); | ||
| 36 | const snapshot = { ...base }; | ||
| 37 | |||
| 38 | const variables = materialYouPaletteToMobileThemeVariables(palette, "dark", base); | ||
| 39 | |||
| 40 | expect(base).toEqual(snapshot); | ||
| 41 | expect(variables["--color-screen"]).toBe(palette.surface); | ||
| 42 | expect(variables["--color-header"]).toBe(palette.surfaceContainerHigh); | ||
| 43 | expect(variables["--color-primary"]).toBe(palette.primary); | ||
| 44 | expect(variables["--color-placeholder"]).toBe("#1C1B1F9E"); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it("uses the Messages-style RCS tones for sent messages", () => { | ||
| 48 | const base = getMobileThemeRuntimeVariables("t3-code", "dark"); | ||
| 49 | const dark = materialYouPaletteToMobileThemeVariables( | ||
| 50 | { ...palette, inversePrimary: "#A31D8DFF" }, | ||
| 51 | "dark", | ||
| 52 | base, | ||
| 53 | ); | ||
| 54 | const light = materialYouPaletteToMobileThemeVariables( | ||
| 55 | { ...palette, inversePrimary: "#A31D8DFF" }, | ||
| 56 | "light", | ||
| 57 | getMobileThemeRuntimeVariables("t3-code", "light"), | ||
| 58 | ); | ||
| 59 | |||
| 60 | expect(dark["--color-user-bubble"]).toBe("#850073FF"); | ||
| 61 | expect(dark["--color-user-bubble-foreground"]).toBe("#FFD7EFFF"); | ||
| 62 | expect(light["--color-user-bubble"]).toBe(palette.primary); | ||
| 63 | expect(light["--color-user-bubble-foreground"]).toBe(palette.onPrimary); | ||
| 64 | }); | ||
| 65 | }); | ||
apps/mobile/src/lib/materialYouTheme.ts created+101| ... | @@ -0,0 +1,101 @@ | ||
| 1 | import { Hct, argbFromHex, hexFromArgb } from "@material/material-color-utilities"; | ||
| 2 | |||
| 3 | import type { MobileThemeAppearance, MobileThemeVariables } from "./mobileTheme"; | ||
| 4 | import type { MaterialYouPalette } from "./materialYouPalette"; | ||
| 5 | |||
| 6 | function 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 | |||
| 16 | function 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 | |||
| 24 | export 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-foreground": palette.onSurface, | ||
| 42 | "--color-foreground-secondary": palette.onSurfaceVariant, | ||
| 43 | "--color-foreground-muted": palette.onSurfaceVariant, | ||
| 44 | "--color-foreground-tertiary": withAlpha(palette.onSurfaceVariant, 0.58), | ||
| 45 | "--color-border": withAlpha(palette.outlineVariant, dark ? 0.5 : 0.65), | ||
| 46 | "--color-border-subtle": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46), | ||
| 47 | "--color-separator": withAlpha(palette.outlineVariant, dark ? 0.24 : 0.34), | ||
| 48 | "--color-subtle": withAlpha(palette.onSurface, 0.05), | ||
| 49 | "--color-subtle-strong": withAlpha(palette.onSurface, 0.1), | ||
| 50 | "--color-inline-skill-background": withAlpha(palette.tertiaryContainer, 0.52), | ||
| 51 | "--color-inline-skill-border": withAlpha(palette.tertiary, 0.38), | ||
| 52 | "--color-inline-skill-foreground": palette.onTertiaryContainer, | ||
| 53 | "--color-primary": palette.primary, | ||
| 54 | "--color-primary-foreground": palette.onPrimary, | ||
| 55 | "--color-primary-shadow": withAlpha(palette.scrim, dark ? 0.22 : 0.18), | ||
| 56 | "--color-secondary": palette.secondaryContainer, | ||
| 57 | "--color-secondary-foreground": palette.onSecondaryContainer, | ||
| 58 | "--color-secondary-border": withAlpha(palette.outlineVariant, dark ? 0.42 : 0.56), | ||
| 59 | "--color-switch-active-track": palette.primary, | ||
| 60 | "--color-switch-active-thumb": palette.onPrimary, | ||
| 61 | "--color-switch-inactive-track": palette.surfaceContainerHighest, | ||
| 62 | "--color-switch-inactive-thumb": palette.outline, | ||
| 63 | "--color-danger": palette.errorContainer, | ||
| 64 | "--color-danger-border": withAlpha(palette.onErrorContainer, dark ? 0.42 : 0.5), | ||
| 65 | "--color-danger-foreground": palette.onErrorContainer, | ||
| 66 | "--color-input": palette.surfaceContainerLowest, | ||
| 67 | "--color-input-border": withAlpha(palette.outline, dark ? 0.42 : 0.5), | ||
| 68 | "--color-sidebar-search": palette.surfaceContainerLowest, | ||
| 69 | "--color-placeholder": withAlpha(palette.onSurface, 0.62), | ||
| 70 | "--color-icon": palette.onSurface, | ||
| 71 | "--color-icon-muted": palette.onSurfaceVariant, | ||
| 72 | "--color-icon-subtle": withAlpha(palette.onSurfaceVariant, 0.68), | ||
| 73 | "--color-header": palette.surfaceContainerHigh, | ||
| 74 | "--color-header-border": withAlpha(palette.outlineVariant, dark ? 0.36 : 0.48), | ||
| 75 | "--color-glass-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.78 : 0.72), | ||
| 76 | "--color-glass-tint": withAlpha(palette.primaryContainer, dark ? 0.24 : 0.18), | ||
| 77 | "--color-status-bar": palette.surfaceContainerLow, | ||
| 78 | "--color-md-body": materialTone(palette.onSurfaceVariant, dark ? 90 : 10), | ||
| 79 | "--color-md-strong": materialTone(palette.onSurfaceVariant, dark ? 90 : 10), | ||
| 80 | "--color-md-link": palette.primary, | ||
| 81 | "--color-md-blockquote-border": withAlpha(palette.primary, dark ? 0.34 : 0.26), | ||
| 82 | "--color-md-blockquote-bg": withAlpha(palette.primaryContainer, dark ? 0.14 : 0.2), | ||
| 83 | "--color-md-code-bg": withAlpha(palette.onSurface, dark ? 0.08 : 0.06), | ||
| 84 | "--color-md-code-text": palette.onSurface, | ||
| 85 | "--color-md-user-code-bg": withAlpha(userBubbleForeground, dark ? 0.18 : 0.22), | ||
| 86 | "--color-md-user-code-text": userBubbleForeground, | ||
| 87 | "--color-md-user-fence-bg": withAlpha(userBubbleForeground, dark ? 0.14 : 0.18), | ||
| 88 | "--color-md-user-fence-text": userBubbleForeground, | ||
| 89 | "--color-md-hr": withAlpha(palette.outlineVariant, dark ? 0.48 : 0.6), | ||
| 90 | "--color-user-bubble": userBubble, | ||
| 91 | "--color-user-bubble-foreground": userBubbleForeground, | ||
| 92 | "--color-user-bubble-foreground-muted": withAlpha(userBubbleForeground, 0.78), | ||
| 93 | "--color-user-bubble-skill-foreground": userBubbleForeground, | ||
| 94 | "--color-backdrop": withAlpha(palette.scrim, dark ? 0.48 : 0.22), | ||
| 95 | "--color-drawer": palette.surface, | ||
| 96 | "--color-drawer-shadow": withAlpha(palette.scrim, dark ? 0.32 : 0.12), | ||
| 97 | "--color-dot-separator": withAlpha(palette.onSurface, 0.2), | ||
| 98 | "--color-wordmark": palette.onSurface, | ||
| 99 | "--color-chevron": withAlpha(palette.onSurface, 0.2), | ||
| 100 | }; | ||
| 101 | } | ||
apps/mobile/src/lib/mobileTheme.ts+5-3| ... | @@ -14,8 +14,8 @@ import { | ... | @@ -14,8 +14,8 @@ import { |
| 14 | } from "@t3tools/shared/themePreview"; | 14 | } from "@t3tools/shared/themePreview"; |
| 15 | 15 | ||
| 16 | export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID; | 16 | export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID; |
| 17 | export const MOBILE_THEME_IDS = SHARED_MOBILE_THEME_IDS; | 17 | export const MOBILE_THEME_IDS = [...SHARED_MOBILE_THEME_IDS, "material-you"] as const; |
| 18 | export type MobileThemeId = SharedMobileThemeId; | 18 | export type MobileThemeId = SharedMobileThemeId | "material-you"; |
| 19 | export type MobileThemeAppearance = ThemeAppearance; | 19 | export type MobileThemeAppearance = ThemeAppearance; |
| 20 | export type MobileThemeMode = MobileThemeAppearance | "system"; | 20 | export type MobileThemeMode = MobileThemeAppearance | "system"; |
| 21 | export type MobileThemeIds = Readonly<Record<MobileThemeAppearance, MobileThemeId>>; | 21 | export type MobileThemeIds = Readonly<Record<MobileThemeAppearance, MobileThemeId>>; |
| ... | @@ -25,6 +25,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ | ... | @@ -25,6 +25,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ |
| 25 | readonly label: string; | 25 | readonly label: string; |
| 26 | }> = [ | 26 | }> = [ |
| 27 | { id: DEFAULT_MOBILE_THEME_ID, label: "T3 Code" }, | 27 | { id: DEFAULT_MOBILE_THEME_ID, label: "T3 Code" }, |
| 28 | { id: "material-you", label: "Material You" }, | ||
| 28 | ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })), | 29 | ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })), |
| 29 | ]; | 30 | ]; |
| 30 | 31 | ||
| ... | @@ -306,7 +307,8 @@ export function getMobileThemePreviewColors( | ... | @@ -306,7 +307,8 @@ export function getMobileThemePreviewColors( |
| 306 | themeId: MobileThemeId, | 307 | themeId: MobileThemeId, |
| 307 | appearance: MobileThemeAppearance, | 308 | appearance: MobileThemeAppearance, |
| 308 | ): ThemePreviewColors { | 309 | ): ThemePreviewColors { |
| 309 | if (themeId === DEFAULT_MOBILE_THEME_ID) return STANDARD_THEME_PREVIEW_COLORS[appearance]; | 310 | if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you") |
| 311 | return STANDARD_THEME_PREVIEW_COLORS[appearance]; | ||
| 310 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; | 312 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; |
| 311 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; | 313 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; |
| 312 | return { | 314 | return { |
apps/mobile/src/lib/mobileThemeRuntime.ts+3-1| ... | @@ -42,7 +42,9 @@ export function getMobileUniwindThemeName( | ... | @@ -42,7 +42,9 @@ export function getMobileUniwindThemeName( |
| 42 | themeId: MobileThemeId, | 42 | themeId: MobileThemeId, |
| 43 | appearance: MobileThemeAppearance, | 43 | appearance: MobileThemeAppearance, |
| 44 | ): MobileUniwindThemeName { | 44 | ): MobileUniwindThemeName { |
| 45 | return themeId === DEFAULT_MOBILE_THEME_ID ? appearance : `${themeId}-${appearance}`; | 45 | return themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you" |
| 46 | ? appearance | ||
| 47 | : `${themeId}-${appearance}`; | ||
| 46 | } | 48 | } |
| 47 | 49 | ||
| 48 | /** | 50 | /** |
apps/mobile/src/lib/mobileThemeVariables.ts+1-1| ... | @@ -21,7 +21,7 @@ export function getMobileThemeRuntimeVariables( | ... | @@ -21,7 +21,7 @@ export function getMobileThemeRuntimeVariables( |
| 21 | themeId: MobileThemeId, | 21 | themeId: MobileThemeId, |
| 22 | appearance: MobileThemeAppearance, | 22 | appearance: MobileThemeAppearance, |
| 23 | ): MobileThemeVariables { | 23 | ): MobileThemeVariables { |
| 24 | return themeId === DEFAULT_MOBILE_THEME_ID | 24 | return themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you" |
| 25 | ? defaults[appearance] | 25 | ? defaults[appearance] |
| 26 | : getMobileThemeVariables(themeId, appearance); | 26 | : getMobileThemeVariables(themeId, appearance); |
| 27 | } | 27 | } |
apps/mobile/src/lib/storage.test.ts+8| ... | @@ -196,6 +196,14 @@ describe("mobile connection storage", () => { | ... | @@ -196,6 +196,14 @@ describe("mobile connection storage", () => { |
| 196 | }); | 196 | }); |
| 197 | }); | 197 | }); |
| 198 | 198 | ||
| 199 | it("persists Material You independently for each appearance", async () => { | ||
| 200 | const themes = { lightThemeId: "material-you", darkThemeId: "ocean" } as const; | ||
| 201 | await savePreferencesPatch(themes); | ||
| 202 | await expect(loadPreferences()).resolves.toEqual(themes); | ||
| 203 | await savePreferencesPatch({ lightThemeId: "t3-chat" }); | ||
| 204 | await expect(loadPreferences()).resolves.toEqual({ ...themes, lightThemeId: "t3-chat" }); | ||
| 205 | }); | ||
| 206 | |||
| 199 | it("drops the removed theme transition preference", async () => { | 207 | it("drops the removed theme transition preference", async () => { |
| 200 | mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10); | 208 | mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10); |
| 201 | 209 |
apps/mobile/src/lib/useUniwindTheme.ts+1-8| ... | @@ -1,8 +1,5 @@ | ... | @@ -1,8 +1,5 @@ |
| 1 | import { useMemo } from "react"; | ||
| 2 | |||
| 3 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | 1 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 4 | import type { MobileThemeVariables } from "./mobileTheme"; | 2 | import type { MobileThemeVariables } from "./mobileTheme"; |
| 5 | import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; | ||
| 6 | 3 | ||
| 7 | /** | 4 | /** |
| 8 | * Complete JS palette for native and third-party APIs that cannot consume a | 5 | * Complete JS palette for native and third-party APIs that cannot consume a |
| ... | @@ -13,9 +10,5 @@ import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; | ... | @@ -13,9 +10,5 @@ import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; |
| 13 | * ScopedTheme instead of subscribing every consumer to CSS-variable updates. | 10 | * ScopedTheme instead of subscribing every consumer to CSS-variable updates. |
| 14 | */ | 11 | */ |
| 15 | export function useUniwindTheme(): MobileThemeVariables { | 12 | export function useUniwindTheme(): MobileThemeVariables { |
| 16 | const { themeAppearance, themeId } = useAppearancePreferences(); | 13 | return useAppearancePreferences().themeVariables; |
| 17 | return useMemo( | ||
| 18 | () => getMobileThemeRuntimeVariables(themeId, themeAppearance), | ||
| 19 | [themeAppearance, themeId], | ||
| 20 | ); | ||
| 21 | } | 14 | } |
apps/mobile/src/native/T3ComposerEditor.native.tsx+3| ... | @@ -18,6 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; | ... | @@ -18,6 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; |
| 18 | import { MOBILE_TYPOGRAPHY } from "../lib/typography"; | 18 | import { MOBILE_TYPOGRAPHY } from "../lib/typography"; |
| 19 | import { useNativePaste } from "../lib/useNativePaste"; | 19 | import { useNativePaste } from "../lib/useNativePaste"; |
| 20 | import { useFontFamily } from "../lib/useFontFamily"; | 20 | import { useFontFamily } from "../lib/useFontFamily"; |
| 21 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | ||
| 21 | import { useUniwindTheme } from "../lib/useUniwindTheme"; | 22 | import { useUniwindTheme } from "../lib/useUniwindTheme"; |
| 22 | import { | 23 | import { |
| 23 | acknowledgeComposerNativeEvent, | 24 | acknowledgeComposerNativeEvent, |
| ... | @@ -211,7 +212,9 @@ export function ComposerEditor({ | ... | @@ -211,7 +212,9 @@ export function ComposerEditor({ |
| 211 | }, | 212 | }, |
| 212 | [], | 213 | [], |
| 213 | ); | 214 | ); |
| 215 | const { systemColorsActive } = useAppearancePreferences(); | ||
| 214 | const themeJson = JSON.stringify({ | 216 | const themeJson = JSON.stringify({ |
| 217 | selection: systemColorsActive ? theme["--color-primary"] : null, | ||
| 215 | text: theme["--color-foreground"], | 218 | text: theme["--color-foreground"], |
| 216 | placeholder: theme["--color-placeholder"], | 219 | placeholder: theme["--color-placeholder"], |
| 217 | chipBackground: theme["--color-subtle"], | 220 | chipBackground: theme["--color-subtle"], |
docs/user/appearance.md+4| ... | @@ -7,6 +7,10 @@ within each theme. Appearance preferences are saved separately on each device or | ... | @@ -7,6 +7,10 @@ within each theme. Appearance preferences are saved separately on each device or |
| 7 | Mobile has its own themes and text, code, and terminal preferences. It does not follow environment | 7 | Mobile has its own themes and text, code, and terminal preferences. It does not follow environment |
| 8 | themes or defaults. | 8 | themes or defaults. |
| 9 | 9 | ||
| 10 | On Android 12 or newer, choose the **Material You** theme in Appearance to use colors from | ||
| 11 | your wallpaper. Selecting another theme replaces those colors. Like other themes, Material You | ||
| 12 | can be selected separately for light and dark appearances. | ||
| 13 | |||
| 10 | ## Motion | 14 | ## Motion |
| 11 | 15 | ||
| 12 | The main sidebar, right panel, and terminal drawer open and close immediately by default. Move the | 16 | The main sidebar, right panel, and terminal drawer open and close immediately by default. Move the |
pnpm-lock.yaml+8| ... | @@ -247,6 +247,9 @@ importers: | ... | @@ -247,6 +247,9 @@ importers: |
| 247 | '@legendapp/list': | 247 | '@legendapp/list': |
| 248 | specifier: 'catalog:' | 248 | specifier: 'catalog:' |
| 249 | version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) | 249 | version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| 250 | '@material/material-color-utilities': | ||
| 251 | specifier: 0.3.0 | ||
| 252 | version: 0.3.0 | ||
| 250 | '@noble/curves': | 253 | '@noble/curves': |
| 251 | specifier: 'catalog:' | 254 | specifier: 'catalog:' |
| 252 | version: 1.9.1 | 255 | version: 1.9.1 |
| ... | @@ -3412,6 +3415,9 @@ packages: | ... | @@ -3412,6 +3415,9 @@ packages: |
| 3412 | resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} | 3415 | resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} |
| 3413 | engines: {node: '>= 10.0.0'} | 3416 | engines: {node: '>= 10.0.0'} |
| 3414 | 3417 | ||
| 3418 | '@material/material-color-utilities@0.3.0': | ||
| 3419 | resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==} | ||
| 3420 | |||
| 3415 | '@modelcontextprotocol/sdk@1.29.0': | 3421 | '@modelcontextprotocol/sdk@1.29.0': |
| 3416 | resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} | 3422 | resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} |
| 3417 | engines: {node: '>=18'} | 3423 | engines: {node: '>=18'} |
| ... | @@ -14208,6 +14214,8 @@ snapshots: | ... | @@ -14208,6 +14214,8 @@ snapshots: |
| 14208 | transitivePeerDependencies: | 14214 | transitivePeerDependencies: |
| 14209 | - supports-color | 14215 | - supports-color |
| 14210 | 14216 | ||
| 14217 | '@material/material-color-utilities@0.3.0': {} | ||
| 14218 | |||
| 14211 | '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': | 14219 | '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': |
| 14212 | dependencies: | 14220 | dependencies: |
| 14213 | '@hono/node-server': 1.19.14(hono@4.12.27) | 14221 | '@hono/node-server': 1.19.14(hono@4.12.27) |