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
8 changes: 7 additions & 1 deletion apps/web/public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Plattform-Design",
"platformThemeDescription": "Visuellen Stil an Ihr Gerät anpassen",
"platformThemeAuto": "Automatisch",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Standard"
},
"advanced": {
"detailedKnowledge": "Detailliertes KI-Wissen",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Platform theme",
"platformThemeDescription": "Adjust the visual style to match your device",
"platformThemeAuto": "Auto-detect",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Default"
},
"pourOver": {
"title": "Pour-over",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Tema de plataforma",
"platformThemeDescription": "Ajusta el estilo visual según tu dispositivo",
"platformThemeAuto": "Detectar automáticamente",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Predeterminado"
},
"advanced": {
"detailedKnowledge": "Conocimiento detallado de IA",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Thème de plateforme",
"platformThemeDescription": "Adapter le style visuel à votre appareil",
"platformThemeAuto": "Détection automatique",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Par défaut"
},
"advanced": {
"detailedKnowledge": "Connaissances IA détaillées",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,13 @@
"followSystemTheme": "Follow system theme",
"followSystemDescription": "Automatically match your system's dark/light mode preference",
"backgroundAnimations": "Background animations",
"animationsDescription": "Show animated background effects"
"animationsDescription": "Show animated background effects",
"platformTheme": "Tema piattaforma",
"platformThemeDescription": "Adatta lo stile visivo al tuo dispositivo",
"platformThemeAuto": "Rilevamento automatico",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Predefinito"
},
"advanced": {
"detailedKnowledge": "Conoscenza IA dettagliata",
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/locales/sv/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,13 @@
"followSystemTheme": "Följ systemtema",
"followSystemDescription": "Matcha automatiskt ditt systems mörkt-/ljust-läge",
"backgroundAnimations": "Bakgrundsanimationer",
"animationsDescription": "Visa animerade bakgrundseffekter"
"animationsDescription": "Visa animerade bakgrundseffekter",
"platformTheme": "Plattformstema",
"platformThemeDescription": "Anpassa utseendet efter din enhet",
"platformThemeAuto": "Automatisk",
"platformThemeIos": "iOS",
"platformThemeMaterial": "Material (Android)",
"platformThemeNone": "Standard"
},
"pourOver": {
"title": "Pour Over",
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type { APIResponse, ViewState } from '@/types'
import { AmbientBackground } from '@/components/AmbientBackground'
import { useBackgroundBlobs } from '@/hooks/useBackgroundBlobs'
import { useThemePreference } from '@/hooks/useThemePreference'
import { usePlatformTheme } from '@/hooks/usePlatformTheme'
import { Sun, Moon } from '@phosphor-icons/react'
import { AI_PREFS_CHANGED_EVENT, getAiEnabled, getHideAiWhenUnavailable, getAutoSync, getAutoSyncAiDescription, syncAutoSyncFromServer } from '@/lib/aiPreferences'

Expand Down Expand Up @@ -252,6 +253,7 @@ function App() {

// Theme preference (light/dark/system)
const { mounted: themeMounted, isDark, isFollowSystem, toggleTheme, setFollowSystem } = useThemePreference()
const { theme: platformTheme, setTheme: setPlatformTheme } = usePlatformTheme()

const isHome = viewState === 'start'

Expand Down Expand Up @@ -1020,6 +1022,8 @@ function App() {
isFollowSystem={isFollowSystem}
onToggleTheme={toggleTheme}
onSetFollowSystem={setFollowSystem}
platformTheme={platformTheme}
onSetPlatformTheme={setPlatformTheme}
/>
</FeatureErrorBoundary>
)}
Expand Down
32 changes: 30 additions & 2 deletions apps/web/src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Progress } from '@/components/ui/progress'
import { hasFeature } from '@/lib/featureFlags'
Expand Down Expand Up @@ -39,6 +40,7 @@ import { useUpdateStatus } from '@/hooks/useUpdateStatus'
import { useUpdateTrigger } from '@/hooks/useUpdateTrigger'
import { MarkdownText } from '@/components/MarkdownText'
import { LanguageSelector } from '@/components/LanguageSelector'
import type { PlatformTheme } from '@/hooks/usePlatformTheme'

interface SettingsViewProps {
onBack: () => void
Expand All @@ -48,6 +50,8 @@ interface SettingsViewProps {
isFollowSystem?: boolean
onToggleTheme?: () => void
onSetFollowSystem?: (follow: boolean) => void
platformTheme?: PlatformTheme
onSetPlatformTheme?: (theme: PlatformTheme) => void
}

interface Settings {
Expand Down Expand Up @@ -98,7 +102,7 @@ const PROGRESS_UPDATE_INTERVAL = 500
const METICULOUS_ADDON_INSTALL_SNIPPET = 'docker exec -it meticai bash -lc "cd /app/meticulous-addon && python3 -m pip install -r requirements.txt && python3 -m pip install ."'
const METICULOUS_ADDON_UPDATE_SNIPPET = 'docker exec -it meticai bash -lc "cd /app/meticulous-addon && git pull --ff-only && python3 -m pip install ."'

export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollowSystem, onToggleTheme, onSetFollowSystem }: SettingsViewProps) {
export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollowSystem, onToggleTheme, onSetFollowSystem, platformTheme, onSetPlatformTheme }: SettingsViewProps) {
const { t } = useTranslation()

const [settings, setSettings] = useState<Settings>({
Expand Down Expand Up @@ -1059,7 +1063,7 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
</div>}

{/* Appearance */}
{(onToggleBlobs !== undefined || onToggleTheme !== undefined) && (
{(onToggleBlobs !== undefined || onToggleTheme !== undefined || onSetPlatformTheme !== undefined) && (
<div className="space-y-3 pt-2 border-t border-border">
<h3 className="text-sm font-semibold tracking-wide text-muted-foreground uppercase">{t('appearance.title')}</h3>

Expand Down Expand Up @@ -1110,6 +1114,30 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo
</div>
)}

{/* Platform theme */}
{onSetPlatformTheme !== undefined && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="platform-theme-select" className="text-sm font-medium">{t('appearance.platformTheme')}</Label>
<p className="text-xs text-muted-foreground">{t('appearance.platformThemeDescription')}</p>
</div>
<Select
value={platformTheme ?? 'auto'}
onValueChange={(value) => onSetPlatformTheme(value as PlatformTheme)}
>
<SelectTrigger id="platform-theme-select" className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{t('appearance.platformThemeAuto')}</SelectItem>
<SelectItem value="ios">{t('appearance.platformThemeIos')}</SelectItem>
<SelectItem value="material">{t('appearance.platformThemeMaterial')}</SelectItem>
<SelectItem value="none">{t('appearance.platformThemeNone')}</SelectItem>
</SelectContent>
</Select>
</div>
)}

</div>
)}

Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ export { useIsMobile } from './use-mobile';

export { useSwipeNavigation } from './use-swipe-navigation';

export { usePlatformTheme } from './usePlatformTheme';
export type { PlatformTheme, DetectedPlatform } from './usePlatformTheme';

// Accessibility Hooks
export * from './a11y';
49 changes: 49 additions & 0 deletions apps/web/src/hooks/usePlatformTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState, useLayoutEffect, useCallback } from 'react'
import { STORAGE_KEYS } from '@/lib/constants'

export type PlatformTheme = 'auto' | 'ios' | 'material' | 'none'
export type DetectedPlatform = 'ios' | 'android' | 'desktop'

function detectPlatform(): DetectedPlatform {
if (typeof navigator === 'undefined') return 'desktop'
const ua = navigator.userAgent
if (/iPhone|iPad|iPod/.test(ua)) return 'ios'
if (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1) return 'ios'
if (/Android/.test(ua)) return 'android'
return 'desktop'
}

function resolveThemeClass(pref: PlatformTheme, detected: DetectedPlatform): string | null {
if (pref === 'none') return null
if (pref === 'ios') return 'ios-theme'
if (pref === 'material') return 'material-theme'
if (detected === 'ios') return 'ios-theme'
if (detected === 'android') return 'material-theme'
return null
}

export function usePlatformTheme() {
const [platform] = useState<DetectedPlatform>(detectPlatform)
const [theme, setThemeState] = useState<PlatformTheme>(() => {
try {
const stored = localStorage.getItem(STORAGE_KEYS.PLATFORM_THEME)
if (stored === 'ios' || stored === 'material' || stored === 'none') return stored
} catch { /* noop */ }
return 'auto'
})

const setTheme = useCallback((next: PlatformTheme) => {
setThemeState(next)
try { localStorage.setItem(STORAGE_KEYS.PLATFORM_THEME, next) } catch { /* noop */ }
}, [])

useLayoutEffect(() => {
const root = document.documentElement
const cls = resolveThemeClass(theme, platform)
root.classList.remove('ios-theme', 'material-theme')
if (cls) root.classList.add(cls)
return () => { root.classList.remove('ios-theme', 'material-theme') }
}, [theme, platform])

return { platform, theme, setTheme } as const
}
3 changes: 3 additions & 0 deletions apps/web/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ export const STORAGE_KEYS = {
PROFILE_LIST_CACHE: 'meticai-direct-profile-list',
DESCRIPTION_CACHE: 'meticai-direct-desc-cache',
POUR_OVER_PREFS: 'meticai-direct-pour-over-prefs',

// -- Appearance --
PLATFORM_THEME: 'meticai-platform-theme',
} as const
2 changes: 2 additions & 0 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import './i18n/config'

import "./main.css"
import "./styles/theme.css"
import "./styles/ios-theme.css"
import "./styles/material-theme.css"
import "./index.css"

// In direct mode (PWA on machine), intercept MeticAI proxy API calls and either
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/styles/ios-theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* iOS theme - activated via .ios-theme on <html> */
.ios-theme #root {
--font-family: -apple-system, "SF Pro Text", "SF Pro Display", system-ui, sans-serif;
--radius-factor: 1.75;
}
.ios-theme body { font-family: -apple-system, "SF Pro Text", "SF Pro Display", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
.ios-theme [data-slot="card"], .ios-theme .card { backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); background-color: rgba(255,255,255,0.72); }
.ios-theme.dark-theme [data-slot="card"], .ios-theme.dark [data-slot="card"], .ios-theme .dark [data-slot="card"], .ios-theme.dark-theme .card, .ios-theme.dark .card, .ios-theme .dark .card { background-color: rgba(30,30,30,0.72); }
.ios-theme button, .ios-theme [role="button"] { transition: transform 0.12s ease, opacity 0.12s ease; }
.ios-theme button:active, .ios-theme [role="button"]:active { transform: scale(0.97); }
.ios-theme [data-slot="switch"][data-state="checked"] { background-color: #34c759 !important; border-color: #34c759 !important; }
.ios-theme.dark-theme [data-slot="switch"][data-state="checked"], .ios-theme.dark [data-slot="switch"][data-state="checked"], .ios-theme .dark [data-slot="switch"][data-state="checked"] { background-color: #30d158 !important; border-color: #30d158 !important; }
.ios-theme .border-border { border-width: 0.5px; }
.ios-theme .border-t { border-top-width: 0.5px; }
.ios-theme .border-b { border-bottom-width: 0.5px; }
.ios-theme ::selection { background-color: rgba(0,122,255,0.25); }
.ios-theme [data-slot="select-item"], .ios-theme [data-slot="dropdown-menu-item"] { transition: background-color 0.15s ease; }
28 changes: 28 additions & 0 deletions apps/web/src/styles/material-theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* Material You theme - activated via .material-theme on <html> */
.material-theme {
--font-family: "Roboto", "Google Sans", system-ui, sans-serif;
--radius-factor: 1.5;
--elevation-1: 0 1px 2px rgba(0,0,0,0.3), 0 1px 3px 1px rgba(0,0,0,0.15);
--elevation-2: 0 1px 2px rgba(0,0,0,0.3), 0 2px 6px 2px rgba(0,0,0,0.15);
--elevation-3: 0 1px 3px rgba(0,0,0,0.3), 0 4px 8px 3px rgba(0,0,0,0.15);
}
.material-theme.dark-theme, .material-theme.dark, .material-theme .dark { --elevation-1: 0 1px 3px 1px rgba(0,0,0,0.26), 0 1px 2px rgba(0,0,0,0.44); --elevation-2: 0 2px 6px 2px rgba(0,0,0,0.26), 0 1px 2px rgba(0,0,0,0.44); --elevation-3: 0 4px 8px 3px rgba(0,0,0,0.26), 0 1px 3px rgba(0,0,0,0.44); }
.material-theme body { font-family: "Roboto", "Google Sans", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
.material-theme button, .material-theme [role="button"] { border-radius: 20px; transition: box-shadow 0.2s ease, background-color 0.2s ease; }
.material-theme .rounded-full { border-radius: 9999px; }
.material-theme button:hover, .material-theme [role="button"]:hover { box-shadow: var(--elevation-1); }
.material-theme button::after, .material-theme [role="button"]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background-color: currentColor; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; }
.material-theme button, .material-theme [role="button"] { position: relative; overflow: hidden; }
.material-theme button:hover::after, .material-theme [role="button"]:hover::after { opacity: 0.08; }
.material-theme button:focus-visible::after, .material-theme [role="button"]:focus-visible::after { opacity: 0.1; }
.material-theme button:active::after, .material-theme [role="button"]:active::after { opacity: 0.12; }
.material-theme [data-slot="card"], .material-theme .card { box-shadow: var(--elevation-1); border-radius: var(--radius-xl); transition: box-shadow 0.2s ease; }
.material-theme [data-slot="card"]:hover, .material-theme .card:hover { box-shadow: var(--elevation-2); }
.material-theme [data-slot="switch"][data-state="checked"] { background-color: #006b5e !important; border-color: #006b5e !important; }
.material-theme.dark-theme [data-slot="switch"][data-state="checked"], .material-theme.dark [data-slot="switch"][data-state="checked"], .material-theme .dark [data-slot="switch"][data-state="checked"] { background-color: #4ddbc4 !important; border-color: #4ddbc4 !important; }
.material-theme.dark-theme [data-slot="switch-thumb"][data-state="checked"], .material-theme.dark [data-slot="switch-thumb"][data-state="checked"], .material-theme .dark [data-slot="switch-thumb"][data-state="checked"] { background-color: #003731 !important; }
.material-theme [data-slot="select-trigger"] { border-radius: 12px; }
.material-theme [data-slot="select-content"] { border-radius: 12px; box-shadow: var(--elevation-2); }
.material-theme [data-slot="select-item"] { border-radius: 8px; transition: background-color 0.15s ease; }
.material-theme input, .material-theme textarea { border-radius: 12px; }
.material-theme * { transition-timing-function: cubic-bezier(0.2, 0, 0, 1); }
Loading