Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] 7dd7546ba3 feat: add chrome-less mode for distraction-free chat
Adds a chrome-less mode that hides the top bar and composer with smooth
animations, providing a distraction-free reading experience. The mode is
toggled via a panel menu item and can be exited via a floating button.

When chrome-less is active:
- Top bar collapses with a smooth grid-template-rows animation
- Composer is force-hidden using existing auto-hide mechanics
- Three floating buttons appear (top-right): open drawer, open panel,
  exit chrome-less mode

Changes:
- store-layout-optima.ts: Add isChromeless state and setChromeless action
- useOptima.tsx: Add chromeless accessor functions
- PageCore.tsx: Wrap OptimaBar in collapsible container
- AppChat.tsx: Force-hide composer, render floating buttons
- ChatPane.tsx: Add Chrome-less toggle to panel menu

Closes #894

Co-authored-by: Enrico Ros <enricoros@users.noreply.github.com>
2026-02-19 08:01:01 +00:00
5 changed files with 131 additions and 36 deletions
+45 -4
View File
@@ -2,7 +2,10 @@ import * as React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, useTheme } from '@mui/joy';
import { Box, IconButton, useTheme } from '@mui/joy';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import MenuIcon from '@mui/icons-material/Menu';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
@@ -32,7 +35,7 @@ import { createErrorContentFragment, createTextContentFragment, DMessageAttachme
import { gcChatImageAssets } from '~/common/stores/chat/chat.gc';
import { getChatLLMId } from '~/common/stores/llms/store-llms';
import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/store-chats';
import { optimaActions, optimaOpenModels, optimaOpenPreferences } from '~/common/layout/optima/useOptima';
import { optimaActions, optimaExitChromeless, optimaOpenDrawer, optimaOpenModels, optimaOpenPanel, optimaOpenPreferences, useOptimaChromeless } from '~/common/layout/optima/useOptima';
import { useFolderStore } from '~/common/stores/folders/store-chat-folders';
import { useIsMobile, useIsTallScreen } from '~/common/components/useMatchMedia';
import { useLLM } from '~/common/stores/llms/llms.hooks';
@@ -119,6 +122,26 @@ const composerOpenMobileSx: SxProps = {
// };
// Chromeless mode floating buttons
const chromelessFloatingButtonsSx: SxProps = {
position: 'fixed',
top: '0.5rem',
right: '0.5rem',
zIndex: 25,
display: 'flex',
gap: 0.5,
opacity: 0.55,
transition: 'opacity 0.2s',
'&:hover': { opacity: 1 },
};
const chromelessButtonSx: SxProps = {
backdropFilter: 'blur(6px)',
backgroundColor: 'background.popup',
boxShadow: 'xs',
};
// Lazy-loaded Modals
const DiagramsModalLazy = React.lazy(() => import('~/modules/aifn/digrams/DiagramsModal').then(module => ({ default: module.DiagramsModal })));
const FlattenerModalLazy = React.lazy(() => import('~/modules/aifn/flatten/FlattenerModal').then(module => ({ default: module.FlattenerModal })));
@@ -214,8 +237,11 @@ export function AppChat() {
return activeFolder?.id ?? null;
});
// Chromeless mode
const isChromeless = useOptimaChromeless();
// Composer Auto-hiding
const forceComposerHide = !!beamOpenStoreInFocusedPane /* || !focusedPaneConversationId */; // auto-hide when no chat (the 'please select a conversation...' state) doesn't feel good
const forceComposerHide = isChromeless || !!beamOpenStoreInFocusedPane /* || !focusedPaneConversationId */; // auto-hide when no chat (the 'please select a conversation...' state) doesn't feel good
const composerAutoHide = useComposerAutoHide(forceComposerHide, composerHasContent);
// Window actions
@@ -774,7 +800,22 @@ export function AppChat() {
</Box>
{/* Hover zone for auto-hide */}
{!forceComposerHide && composerAutoHide.isHidden && <Box {...composerAutoHide.detectorProps} />}
{!isChromeless && !forceComposerHide && composerAutoHide.isHidden && <Box {...composerAutoHide.detectorProps} />}
{/* Chromeless mode floating buttons */}
{isChromeless && (
<Box sx={chromelessFloatingButtonsSx}>
<IconButton size='sm' variant='soft' color='neutral' onClick={optimaOpenDrawer} sx={chromelessButtonSx} aria-label='Open Drawer'>
<MenuIcon />
</IconButton>
<IconButton size='sm' variant='soft' color='neutral' onClick={optimaOpenPanel} sx={chromelessButtonSx} aria-label='Open Menu'>
<MoreVertIcon />
</IconButton>
<IconButton size='sm' variant='soft' color='neutral' onClick={optimaExitChromeless} sx={chromelessButtonSx} aria-label='Exit Chrome-less'>
<FullscreenExitIcon />
</IconButton>
</Box>
)}
{/* Diagrams */}
{!!diagramConfig && (
@@ -6,6 +6,8 @@ import AddIcon from '@mui/icons-material/Add';
import ArchiveOutlinedIcon from '@mui/icons-material/ArchiveOutlined';
import CleaningServicesOutlinedIcon from '@mui/icons-material/CleaningServicesOutlined';
import CompressIcon from '@mui/icons-material/Compress';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import EngineeringIcon from '@mui/icons-material/Engineering';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
@@ -20,7 +22,7 @@ import { CodiconSplitVertical } from '~/common/components/icons/CodiconSplitVert
import { CodiconSplitVerticalRemove } from '~/common/components/icons/CodiconSplitVerticalRemove';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { OptimaPanelGroupedList, OptimaPanelGroupGutter } from '~/common/layout/optima/panel/OptimaPanelGroupedList';
import { optimaActions } from '~/common/layout/optima/useOptima';
import { optimaActions, optimaToggleChromeless, useOptimaChromeless } from '~/common/layout/optima/useOptima';
import { useChatStore } from '~/common/stores/chat/store-chats'; // may be replaced with a dedicated hook for the chat pane
import { useLabsDevMode } from '~/common/stores/store-ux-labs';
@@ -56,6 +58,7 @@ export function ChatPane(props: {
const { canAddPane, isMultiPane } = usePaneDuplicateOrClose();
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
const labsDevMode = useLabsDevMode();
const isChromeless = useOptimaChromeless();
const { isArchived, setArchived } = useChatStore(useShallow((state) => {
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
@@ -147,6 +150,11 @@ export function ChatPane(props: {
</ListItemButton>
</ListItem>
<ListItemButton onClick={optimaToggleChromeless}>
<ListItemDecorator>{isChromeless ? <FullscreenExitIcon /> : <FullscreenIcon />}</ListItemDecorator>
{isChromeless ? 'Exit Chrome-less' : 'Chrome-less'}
</ListItemButton>
</OptimaPanelGroupedList>
{/* Chat Actions group */}
+57 -27
View File
@@ -9,6 +9,7 @@ import type { NavItemApp } from '~/common/app.nav';
// import { MobileNav } from './MobileNav';
import { OptimaBar } from '~/common/layout/optima/bar/OptimaBar';
import { optimaHasMOTD, OptimaMOTD } from '~/common/layout/optima/OptimaMOTD';
import { useOptimaChromeless } from '~/common/layout/optima/useOptima';
const pageCoreSx: SxProps = {
@@ -33,45 +34,74 @@ const pageCoreBarSx: SxProps = {
zIndex: themeZIndexPageBar,
};
const barCollapsibleOpenSx: SxProps = {
display: 'grid',
gridTemplateRows: '1fr',
transition: 'grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
};
const barCollapsibleClosedSx: SxProps = {
display: 'grid',
gridTemplateRows: '0fr',
transition: 'grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
};
const barCollapsibleInnerStyle: React.CSSProperties = {
minHeight: 0,
overflow: 'hidden',
contain: 'paint',
};
const pageCoreMobileNavSx: SxProps = {
flex: 0,
};
export const PageCore = (props: {
export function PageCore(props: {
component: React.ElementType,
currentApp?: NavItemApp,
isFull: boolean,
isMobile: boolean,
children: React.ReactNode,
}) =>
<Box
component={props.component}
sx={props.currentApp?.pageBrighter ? pageCoreBrighterSx : props.isFull ? pageCoreFullSx : pageCoreSx}
>
}) {
{/* Optional deployment MOTD */}
{optimaHasMOTD && <OptimaMOTD />}
// external state
const isChromeless = useOptimaChromeless();
{/* Responsive page bar (pluggable App Center Items and App Menu) */}
<OptimaBar
component='header'
currentApp={props.currentApp}
isMobile={props.isMobile}
sx={pageCoreBarSx}
/>
return (
<Box
component={props.component}
sx={props.currentApp?.pageBrighter ? pageCoreBrighterSx : props.isFull ? pageCoreFullSx : pageCoreSx}
>
{/* Page (NextJS) must make the assumption they're in a flex-col layout */}
{props.children}
{/* Optional deployment MOTD */}
{optimaHasMOTD && <OptimaMOTD />}
{/* [Mobile] Nav bar at the bottom */}
{/*{!!props.isMobile && (*/}
{/* <MobileNav*/}
{/* component='nav'*/}
{/* currentApp={props.currentApp}*/}
{/* hideOnFocusMode*/}
{/* sx={pageCoreMobileNavSx}*/}
{/* />*/}
{/*)}*/}
{/* Responsive page bar (pluggable App Center Items and App Menu) - collapsible for chromeless mode */}
<Box sx={isChromeless ? barCollapsibleClosedSx : barCollapsibleOpenSx}>
<div style={barCollapsibleInnerStyle}>
<OptimaBar
component='header'
currentApp={props.currentApp}
isMobile={props.isMobile}
sx={pageCoreBarSx}
/>
</div>
</Box>
</Box>;
{/* Page (NextJS) must make the assumption they're in a flex-col layout */}
{props.children}
{/* [Mobile] Nav bar at the bottom */}
{/*{!!props.isMobile && (*/}
{/* <MobileNav*/}
{/* component='nav'*/}
{/* currentApp={props.currentApp}*/}
{/* hideOnFocusMode*/}
{/* sx={pageCoreMobileNavSx}*/}
{/* />*/}
{/*)}*/}
</Box>
);
}
@@ -16,7 +16,7 @@ export type ModelOptionsContext = 'full' | 'parameters';
interface OptimaState {
// modes
// isFocusedMode: boolean; // when active, the Mobile App menu is not displayed
isChromeless: boolean; // when active, the top bar and composer are hidden, with floating buttons
// panes
drawerIsOpen: boolean;
@@ -62,7 +62,7 @@ const modalsClosedState = {
const initialState: OptimaState = {
// modes
// isFocusedMode: false,
isChromeless: false,
// panes
drawerIsOpen: initialDrawerOpen(),
@@ -81,7 +81,7 @@ const initialState: OptimaState = {
export interface OptimaActions {
// setIsFocusedMode: (isFocusedMode: boolean) => void;
setChromeless: (isChromeless: boolean) => void;
closeDrawer: () => void;
openDrawer: () => void;
@@ -168,7 +168,7 @@ export const useLayoutOptimaStore = create<OptimaState & OptimaActions>((_set, _
...initialState,
// setIsFocusedMode: (isFocusedMode) => _set({ isFocusedMode }),
setChromeless: (isChromeless) => _set({ isChromeless }),
closeDrawer: () => {
// prevent accidental immediate close (e.g. double-click, animation protection)
+16
View File
@@ -6,6 +6,22 @@ import { NavItemApp } from '~/common/app.nav';
import { useOptimaPortalHasInputs } from '~/common/layout/optima/portals/useOptimaPortalHasInputs';
// Chromeless Mode
export function optimaToggleChromeless() {
const state = useLayoutOptimaStore.getState();
state.setChromeless(!state.isChromeless);
}
export function optimaExitChromeless() {
useLayoutOptimaStore.getState().setChromeless(false);
}
export function useOptimaChromeless() {
return useLayoutOptimaStore(({ isChromeless }) => isChromeless);
}
// Drawer
export function optimaCloseDrawer() {