enhancement: fit mobile layout

This commit is contained in:
Faisal Amir 2025-09-22 15:06:11 +07:00
parent 003598204e
commit f639ec70d4
13 changed files with 382 additions and 298 deletions

View File

@ -1,11 +1,24 @@
<!doctype html>
<html lang="en">
<html lang="en" class="bg-app">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/jan-logo.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/jan-logo.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/images/jan-logo.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/images/jan-logo.png"
/>
<link rel="apple-touch-icon" href="/images/jan-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-visual"
/>
<title>Jan</title>
</head>
<body>

View File

@ -122,7 +122,7 @@ const LeftPanel = () => {
) {
if (currentIsSmallScreen && open) {
setLeftPanel(false)
} else if(!open) {
} else if (!open) {
setLeftPanel(true)
}
prevScreenSizeRef.current = currentIsSmallScreen
@ -141,7 +141,7 @@ const LeftPanel = () => {
return () => {
window.removeEventListener('resize', handleResize)
}
}, [setLeftPanel])
}, [open, setLeftPanel])
const currentPath = useRouterState({
select: (state) => state.location.pathname,
@ -184,7 +184,7 @@ const LeftPanel = () => {
return (
<>
{/* Backdrop overlay for small screens */}
{isSmallScreen && open && (
{isSmallScreen && open && !IS_IOS && !IS_ANDROID && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur z-30"
onClick={(e) => {
@ -207,7 +207,7 @@ const LeftPanel = () => {
isResizableContext && 'h-full w-full',
// Small screen context: fixed positioning and styling
isSmallScreen &&
'fixed h-[calc(100%-16px)] bg-app z-50 rounded-sm border border-left-panel-fg/10 m-2 px-1 w-48',
'fixed h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))] bg-main-view z-50 md:border border-left-panel-fg/10 px-1 w-full md:w-48',
// Default context: original styling
!isResizableContext &&
!isSmallScreen &&
@ -266,7 +266,8 @@ const LeftPanel = () => {
<div
className={cn(
'flex flex-col',
Object.keys(downloads).length > 0 || localDownloadingModels.size > 0
Object.keys(downloads).length > 0 ||
localDownloadingModels.size > 0
? 'h-[calc(100%-200px)]'
: 'h-[calc(100%-140px)]'
)}
@ -379,7 +380,9 @@ const LeftPanel = () => {
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
<DeleteAllThreadsDialog
onDeleteAll={deleteAllThreads}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@ -32,7 +32,10 @@ const SettingsMenu = () => {
if (!provider.active) return false
// On web version, hide llamacpp provider
if (!PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && provider.provider === 'llama.cpp') {
if (
!PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] &&
provider.provider === 'llama.cpp'
) {
return false
}
@ -92,7 +95,7 @@ const SettingsMenu = () => {
title: 'common:keyboardShortcuts',
route: route.settings.shortcuts,
hasSubMenu: false,
isEnabled: true,
isEnabled: PlatformFeatures[PlatformFeature.SHORTCUT],
},
{
title: 'common:hardware',
@ -137,7 +140,7 @@ const SettingsMenu = () => {
return (
<>
<button
className="fixed top-4 right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20"
className="fixed top-[calc(10px+env(safe-area-inset-top))] right-4 sm:hidden size-5 cursor-pointer items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10 z-20"
onClick={toggleMenu}
aria-label="Toggle settings menu"
>
@ -152,7 +155,7 @@ const SettingsMenu = () => {
'h-full w-44 shrink-0 px-1.5 pt-3 border-r border-main-view-fg/5 bg-main-view',
'sm:flex',
isMenuOpen
? 'flex fixed sm:hidden top-0 z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg'
? 'flex fixed sm:hidden top-[calc(10px+env(safe-area-inset-top))] z-10 m-1 h-[calc(100%-8px)] border-r-0 border-l bg-main-view right-0 py-8 rounded-tr-lg rounded-br-lg'
: 'hidden'
)}
>
@ -168,7 +171,9 @@ const SettingsMenu = () => {
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded [&.active]:bg-main-view-fg/5"
>
<div className="flex items-center justify-between">
<span className="text-main-view-fg/80">{t(menu.title)}</span>
<span className="text-main-view-fg/80">
{t(menu.title)}
</span>
{menu.hasSubMenu && (
<button
onClick={(e) => {
@ -194,7 +199,8 @@ const SettingsMenu = () => {
{activeProviders.map((provider) => {
const isActive = matches.some(
(match) =>
match.routeId === '/settings/providers/$providerName' &&
match.routeId ===
'/settings/providers/$providerName' &&
'providerName' in match.params &&
match.params.providerName === provider.provider
)
@ -217,7 +223,9 @@ const SettingsMenu = () => {
providerName: provider.provider,
},
...(stepSetupRemoteProvider
? { search: { step: 'setup_remote_provider' } }
? {
search: { step: 'setup_remote_provider' },
}
: {}),
})
}

View File

@ -6,6 +6,8 @@ import HeaderPage from './HeaderPage'
import { isProd } from '@/lib/version'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { localStorageKey } from '@/constants/localStorage'
import { PlatformFeatures } from '@/lib/platform/const'
import { PlatformFeature } from '@/lib/platform'
function SetupScreen() {
const { t } = useTranslation()
@ -21,7 +23,7 @@ function SetupScreen() {
<div className="flex h-full flex-col flex-justify-center">
<HeaderPage></HeaderPage>
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center ">
<div className="w-4/6 mx-auto">
<div className="w-full lg:w-4/6 mx-auto">
<div className="mb-8 text-left">
<h1 className="font-editorialnew text-main-view-fg text-4xl">
{t('setup:welcome')}
@ -31,6 +33,7 @@ function SetupScreen() {
</p>
</div>
<div className="flex gap-4 flex-col">
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
<Card
header={
<Link
@ -46,7 +49,8 @@ function SetupScreen() {
</div>
</Link>
}
></Card>
/>
)}
<Card
header={
<Link
@ -65,7 +69,7 @@ function SetupScreen() {
</h1>
</Link>
}
></Card>
/>
</div>
</div>
</div>

View File

@ -21,7 +21,7 @@ export function PromptAnalytic() {
}
return (
<div className="fixed bottom-4 right-4 z-50 p-4 shadow-lg bg-main-view w-100 border border-main-view-fg/8 rounded-lg">
<div className="fixed bottom-4 right-4 z-50 p-4 shadow-lg bg-main-view w-4/5 md:w-100 border border-main-view-fg/8 rounded-lg">
<div className="flex items-center gap-2">
<IconFileTextShield className="text-accent" />
<h2 className="font-medium text-main-view-fg/80">
@ -45,7 +45,9 @@ export function PromptAnalytic() {
>
{t('deny')}
</Button>
<Button onClick={() => handleProductAnalytics(true)}>{t('allow')}</Button>
<Button onClick={() => handleProductAnalytics(true)}>
{t('allow')}
</Button>
</div>
</div>
)

View File

@ -56,6 +56,13 @@
@layer base {
body {
@apply overflow-hidden;
background-color: white;
min-height: 100vh;
min-height: -webkit-fill-available;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
::-webkit-scrollbar {
width: 6px;
height: 6px;

View File

@ -4,7 +4,7 @@
*/
import { PlatformFeature } from './types'
import { isPlatformTauri } from './utils'
import { isPlatformTauri, isPlatformIOS, isPlatformAndroid } from './utils'
/**
* Platform Features Configuration
@ -12,28 +12,35 @@ import { isPlatformTauri } from './utils'
*/
export const PlatformFeatures: Record<PlatformFeature, boolean> = {
// Hardware monitoring and GPU usage
[PlatformFeature.HARDWARE_MONITORING]: isPlatformTauri(),
[PlatformFeature.HARDWARE_MONITORING]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Local model inference (llama.cpp)
[PlatformFeature.LOCAL_INFERENCE]: isPlatformTauri(),
[PlatformFeature.LOCAL_INFERENCE]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Local API server
[PlatformFeature.LOCAL_API_SERVER]: isPlatformTauri(),
[PlatformFeature.LOCAL_API_SERVER]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Hub/model downloads
[PlatformFeature.MODEL_HUB]: isPlatformTauri(),
[PlatformFeature.MODEL_HUB]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// System integrations (logs, file explorer, etc.)
[PlatformFeature.SYSTEM_INTEGRATIONS]: isPlatformTauri(),
[PlatformFeature.SYSTEM_INTEGRATIONS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// HTTPS proxy
[PlatformFeature.HTTPS_PROXY]: isPlatformTauri(),
[PlatformFeature.HTTPS_PROXY]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds
[PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(),
// Analytics and telemetry - disabled for web
[PlatformFeature.ANALYTICS]: isPlatformTauri(),
[PlatformFeature.ANALYTICS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Web-specific automatic model selection from jan provider - enabled for web only
[PlatformFeature.WEB_AUTO_MODEL_SELECTION]: !isPlatformTauri(),
@ -45,10 +52,12 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
[PlatformFeature.MCP_AUTO_APPROVE_TOOLS]: !isPlatformTauri(),
// MCP servers settings page - disabled for web
[PlatformFeature.MCP_SERVERS_SETTINGS]: isPlatformTauri(),
[PlatformFeature.MCP_SERVERS_SETTINGS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Extensions settings page - disabled for web
[PlatformFeature.EXTENSIONS_SETTINGS]: isPlatformTauri(),
[PlatformFeature.EXTENSIONS_SETTINGS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Assistant functionality - disabled for web
[PlatformFeature.ASSISTANTS]: isPlatformTauri(),
@ -60,5 +69,9 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
[PlatformFeature.GOOGLE_ANALYTICS]: !isPlatformTauri(),
// Alternate shortcut bindings - enabled for web only (to avoid browser conflicts)
[PlatformFeature.ALTERNATE_SHORTCUT_BINDINGS]: !isPlatformTauri(),
[PlatformFeature.ALTERNATE_SHORTCUT_BINDINGS]:
!isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
// Shortcut
[PlatformFeature.SHORTCUT]: !isPlatformIOS() && !isPlatformAndroid(),
}

View File

@ -6,7 +6,7 @@
/**
* Supported platforms
*/
export type Platform = 'tauri' | 'web'
export type Platform = 'tauri' | 'web' | 'ios' | 'android'
/**
* Platform Feature Enum
@ -16,6 +16,8 @@ export enum PlatformFeature {
// Hardware monitoring and GPU usage
HARDWARE_MONITORING = 'hardwareMonitoring',
SHORTCUT = 'shortcut',
// Local model inference (llama.cpp)
LOCAL_INFERENCE = 'localInference',

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Platform, PlatformFeature } from './types'
declare const IS_WEB_APP: boolean
@ -12,7 +13,25 @@ export const isPlatformTauri = (): boolean => {
return true
}
export const isPlatformIOS = (): boolean => {
if (typeof navigator === 'undefined') return false
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
)
}
export const isPlatformAndroid = (): boolean => {
if (typeof navigator === 'undefined') return false
return /Android/.test(navigator.userAgent)
}
export const isIOS = (): boolean => isPlatformIOS()
export const isAndroid = (): boolean => isPlatformAndroid()
export const getCurrentPlatform = (): Platform => {
if (isPlatformIOS()) return 'ios'
if (isPlatformAndroid()) return 'android'
return isPlatformTauri() ? 'tauri' : 'web'
}

View File

@ -111,13 +111,17 @@ const AppLayout = () => {
return (
<Fragment>
<AnalyticProvider />
{PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && <GoogleAnalyticsProvider />}
{PlatformFeatures[PlatformFeature.GOOGLE_ANALYTICS] && (
<GoogleAnalyticsProvider />
)}
<KeyboardShortcutsProvider />
<main className="relative h-svh text-sm antialiased select-none bg-app">
{/* Fake absolute panel top to enable window drag */}
<div className="absolute w-full h-10 z-10" data-tauri-drag-region />
<DialogAppUpdater />
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && <BackendUpdater />}
{PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && (
<BackendUpdater />
)}
{/* Use ResizablePanelGroup only on larger screens */}
{!isSmallScreen && isLeftPanelOpen ? (
@ -158,11 +162,11 @@ const AppLayout = () => {
{/* Main content panel */}
<div
className={cn(
'h-full flex w-full p-1 ',
'h-svh flex w-full md:p-1',
isLeftPanelOpen && 'w-full md:w-[calc(100%-198px)]'
)}
>
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full rounded-lg overflow-hidden">
<div className="bg-main-view text-main-view-fg border border-main-view-fg/5 w-full md:rounded-lg overflow-hidden">
<Outlet />
</div>
</div>

View File

@ -31,7 +31,7 @@ function Appareances() {
const { resetCodeBlockStyle } = useCodeblock()
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage>

View File

@ -161,7 +161,7 @@ function General() {
}, [t, checkForUpdate])
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage>
@ -181,7 +181,8 @@ function General() {
}
/>
)}
{!AUTO_UPDATER_DISABLED && PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
{!AUTO_UPDATER_DISABLED &&
PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && (
<CardItem
title={t('settings:general.checkForUpdates')}
description={t('settings:general.checkForUpdatesDesc')}
@ -248,7 +249,10 @@ function General() {
>
{isCopied ? (
<div className="flex items-center gap-1">
<IconCopyCheck size={12} className="text-accent" />
<IconCopyCheck
size={12}
className="text-accent"
/>
<span className="text-xs leading-0">
{t('settings:general.copied')}
</span>
@ -315,7 +319,10 @@ function General() {
title={t('settings:dataFolder.appLogs')}
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconLogs size={12} className="text-main-view-fg/50" />
<IconLogs
size={12}
className="text-main-view-fg/50"
/>
<span>{t('settings:general.openLogs')}</span>
</div>
</Button>
@ -327,7 +334,9 @@ function General() {
if (janDataFolder) {
try {
const logsPath = `${janDataFolder}/logs`
await serviceHub.opener().revealItemInDir(logsPath)
await serviceHub
.opener()
.revealItemInDir(logsPath)
} catch (error) {
console.error(
'Failed to reveal logs folder:',

View File

@ -443,7 +443,7 @@ function ProviderDetail() {
return (
<>
<Joyride
run={isSetup}
run={IS_IOS || IS_ANDROID ? false : isSetup}
floaterProps={{
hideArrow: true,
}}
@ -465,7 +465,7 @@ function ProviderDetail() {
skip: t('providers:joyride.skip'),
}}
/>
<div className="flex flex-col h-full">
<div className="flex flex-col h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))]">
<HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage>