feat(UI): Introduce new UI components for file attachments and related views (#191)
Add file management API and UI components Implement directory listing, search and CRUD operations in desktop backend Expose new Files API on frontend to list, search, and modify files
This commit is contained in:
committed by
GitHub
parent
1f23b63c0b
commit
d97181bcd2
Generated
+1
-1
@@ -2977,7 +2977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openchamber-desktop"
|
||||
version = "1.5.2"
|
||||
version = "1.5.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
@@ -53,6 +53,19 @@ pub struct CreateDirectoryResponse {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeletePathResponse {
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RenamePathResponse {
|
||||
success: bool,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileSearchHit {
|
||||
@@ -121,6 +134,34 @@ impl FsCommandError {
|
||||
FsCommandError::NotFound => "Parent directory not found".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_delete_message(&self) -> String {
|
||||
match self {
|
||||
FsCommandError::NotFound => "File or directory not found".to_string(),
|
||||
FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => {
|
||||
"Access to path denied".to_string()
|
||||
}
|
||||
FsCommandError::NotDirectory => "Specified path is not a directory".to_string(),
|
||||
FsCommandError::Other(message) => {
|
||||
let _ = message;
|
||||
"Failed to delete path".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_rename_message(&self) -> String {
|
||||
match self {
|
||||
FsCommandError::NotFound => "Source path not found".to_string(),
|
||||
FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => {
|
||||
"Access to path denied".to_string()
|
||||
}
|
||||
FsCommandError::NotDirectory => "Parent path must be a directory".to_string(),
|
||||
FsCommandError::Other(message) => {
|
||||
let _ = message;
|
||||
"Failed to rename path".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for FsCommandError {
|
||||
@@ -455,6 +496,71 @@ pub async fn create_directory(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_path(
|
||||
path: String,
|
||||
state: tauri::State<'_, DesktopRuntime>,
|
||||
) -> Result<DeletePathResponse, String> {
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("Path is required".to_string());
|
||||
}
|
||||
|
||||
let (workspace_roots, default_root) = resolve_workspace_roots(state.settings()).await;
|
||||
let resolved_path = resolve_sandboxed_path(Some(trimmed.to_string()), &workspace_roots, default_root.as_ref())
|
||||
.await
|
||||
.map_err(|err| err.to_delete_message())?;
|
||||
|
||||
let metadata = fs::metadata(&resolved_path)
|
||||
.await
|
||||
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||
|
||||
if metadata.is_dir() {
|
||||
fs::remove_dir_all(&resolved_path)
|
||||
.await
|
||||
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||
} else {
|
||||
fs::remove_file(&resolved_path)
|
||||
.await
|
||||
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||
}
|
||||
|
||||
Ok(DeletePathResponse { success: true })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_path(
|
||||
old_path: String,
|
||||
new_path: String,
|
||||
state: tauri::State<'_, DesktopRuntime>,
|
||||
) -> Result<RenamePathResponse, String> {
|
||||
let trimmed_old = old_path.trim();
|
||||
if trimmed_old.is_empty() {
|
||||
return Err("oldPath is required".to_string());
|
||||
}
|
||||
let trimmed_new = new_path.trim();
|
||||
if trimmed_new.is_empty() {
|
||||
return Err("newPath is required".to_string());
|
||||
}
|
||||
|
||||
let (workspace_roots, default_root) = resolve_workspace_roots(state.settings()).await;
|
||||
let resolved_old = resolve_sandboxed_path(Some(trimmed_old.to_string()), &workspace_roots, default_root.as_ref())
|
||||
.await
|
||||
.map_err(|err| err.to_rename_message())?;
|
||||
let resolved_new = resolve_creatable_path(trimmed_new, &workspace_roots, default_root.as_ref())
|
||||
.await
|
||||
.map_err(|err| err.to_rename_message())?;
|
||||
|
||||
fs::rename(&resolved_old, &resolved_new)
|
||||
.await
|
||||
.map_err(|err| FsCommandError::from(err).to_rename_message())?;
|
||||
|
||||
Ok(RenamePathResponse {
|
||||
success: true,
|
||||
path: normalize_path(&resolved_new),
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_sandboxed_path(
|
||||
path: Option<String>,
|
||||
workspace_roots: &[PathBuf],
|
||||
|
||||
@@ -28,7 +28,7 @@ use axum::{
|
||||
routing::{any, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use commands::files::{create_directory, exec_commands, list_directory, read_file, read_file_binary, search_files, write_file};
|
||||
use commands::files::{create_directory, delete_path, exec_commands, list_directory, read_file, read_file_binary, rename_path, search_files, write_file};
|
||||
use commands::git::{
|
||||
add_git_worktree, check_is_git_repository, checkout_branch, create_branch, create_git_commit, rename_branch,
|
||||
create_git_identity, delete_git_branch, delete_git_identity, delete_remote_branch,
|
||||
@@ -835,6 +835,8 @@ fn main() {
|
||||
list_directory,
|
||||
search_files,
|
||||
create_directory,
|
||||
delete_path,
|
||||
rename_path,
|
||||
read_file,
|
||||
read_file_binary,
|
||||
write_file,
|
||||
|
||||
@@ -192,6 +192,49 @@ export const createDesktopFilesAPI = (): FilesAPI => ({
|
||||
}
|
||||
},
|
||||
|
||||
async delete(path: string): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const normalizedPath = normalizePath(path);
|
||||
const result = await safeInvoke<{ success: boolean }>('delete_path', {
|
||||
path: normalizedPath,
|
||||
}, {
|
||||
timeout: 10000,
|
||||
onCancel: () => {
|
||||
console.warn('[FilesAPI] Delete operation timed out');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: Boolean(result?.success),
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(message || 'Failed to delete path');
|
||||
}
|
||||
},
|
||||
|
||||
async rename(oldPath: string, newPath: string): Promise<{ success: boolean; path: string }> {
|
||||
try {
|
||||
const result = await safeInvoke<{ success: boolean; path: string }>('rename_path', {
|
||||
oldPath: normalizePath(oldPath),
|
||||
newPath: normalizePath(newPath),
|
||||
}, {
|
||||
timeout: 10000,
|
||||
onCancel: () => {
|
||||
console.warn('[FilesAPI] Rename operation timed out');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: Boolean(result?.success),
|
||||
path: result?.path ? normalizePath(result.path) : normalizePath(newPath),
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(message || 'Failed to rename path');
|
||||
}
|
||||
},
|
||||
|
||||
async execCommands(commands: string[], cwd: string): Promise<{
|
||||
success: boolean;
|
||||
results: Array<{
|
||||
|
||||
@@ -221,7 +221,7 @@ export const ChatContainer: React.FC = () => {
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-background">
|
||||
<div className="chat-column space-y-4">
|
||||
<div className="chat-message-column space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex gap-3 p-4">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
|
||||
@@ -827,17 +827,17 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'group w-full',
|
||||
shouldShowHeader ? 'pt-2' : 'pt-0',
|
||||
isUser ? 'pb-2' : isFollowedByAssistant ? 'pb-0' : 'pb-2'
|
||||
shouldShowHeader ? 'pt-6' : 'pt-0',
|
||||
isUser ? 'pb-4' : isFollowedByAssistant ? 'pb-0' : 'pb-8'
|
||||
)}
|
||||
data-message-id={message.info.id}
|
||||
ref={messageContainerRef}
|
||||
>
|
||||
<div className="chat-column">
|
||||
<div className="chat-message-column relative">
|
||||
{isUser ? (
|
||||
<FadeInOnReveal>
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[85%] rounded-xl rounded-br-xs bg-primary/10 dark:bg-primary/8 px-3.5 pt-2.5 pb-1.5">
|
||||
<div className="max-w-[85%] rounded-2xl rounded-br-sm bg-primary/10 dark:bg-primary/10 px-5 py-3 shadow-sm border border-primary/5">
|
||||
<MessageBody
|
||||
messageId={message.info.id}
|
||||
parts={visibleParts}
|
||||
@@ -870,7 +870,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
</div>
|
||||
</FadeInOnReveal>
|
||||
) : (
|
||||
<div>
|
||||
<div className="relative pl-4 ml-1">
|
||||
{shouldShowHeader && (
|
||||
<MessageHeader
|
||||
isUser={isUser}
|
||||
|
||||
@@ -168,9 +168,11 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
||||
)}
|
||||
</div>
|
||||
{getFileIcon()}
|
||||
<span title={file.serverPath || displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
<div className="overflow-hidden max-w-[200px]">
|
||||
<span className="marquee-text" title={file.serverPath || displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground flex-shrink-0">
|
||||
({formatFileSize(file.size)})
|
||||
</span>
|
||||
@@ -289,9 +291,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-muted/30 border border-border/30 rounded-xl typography-meta"
|
||||
>
|
||||
{getFileIcon(file.mime)}
|
||||
<span>
|
||||
{extractFilename(file.filename)}
|
||||
</span>
|
||||
<div className="overflow-hidden max-w-[200px]">
|
||||
<span className="marquee-text">
|
||||
{extractFilename(file.filename)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2049,13 +2049,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
<span
|
||||
key={`${currentProviderId}-${currentModelId}`}
|
||||
className={cn(
|
||||
'model-controls__model-label',
|
||||
'model-controls__model-label overflow-hidden',
|
||||
controlTextSize,
|
||||
'font-medium whitespace-nowrap text-foreground truncate min-w-0',
|
||||
'font-medium whitespace-nowrap text-foreground min-w-0',
|
||||
'max-w-[260px]'
|
||||
)}
|
||||
>
|
||||
{getCurrentModelDisplayName()}
|
||||
<span className="marquee-text">
|
||||
{getCurrentModelDisplayName()}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -2169,11 +2171,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'model-controls__model-label typography-micro font-medium truncate min-w-0',
|
||||
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||
)}
|
||||
>
|
||||
{getCurrentModelDisplayName()}
|
||||
<span className="marquee-text">
|
||||
{getCurrentModelDisplayName()}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -155,7 +155,7 @@ const UserMessageBody: React.FC<{
|
||||
style={{ contain: 'layout', transform: 'translateZ(0)' }}
|
||||
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
||||
>
|
||||
<div className="leading-normal overflow-hidden text-foreground/85">
|
||||
<div className="leading-relaxed overflow-hidden text-foreground/90 text-base">
|
||||
{textParts.map((part, index) => {
|
||||
let mentionForPart: AgentMentionInfo | undefined;
|
||||
if (agentMention && mentionToken && !mentionInjected) {
|
||||
@@ -180,8 +180,7 @@ const UserMessageBody: React.FC<{
|
||||
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
||||
{(canCopyMessage && hasCopyableText) || onRevert || onFork ? (
|
||||
<div className={cn(
|
||||
"mt-1 flex items-center justify-end gap-2 opacity-0 pointer-events-none transition-opacity duration-150 group-hover/message:opacity-100 group-hover/message:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto",
|
||||
copyHintVisible && "opacity-100 pointer-events-auto"
|
||||
"mt-1 flex items-center justify-end gap-2"
|
||||
)}>
|
||||
{onRevert && (
|
||||
<Tooltip delayDuration={1000}>
|
||||
@@ -284,7 +283,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
errorMessage,
|
||||
}) => {
|
||||
|
||||
void _streamPhase;
|
||||
void _allowAnimation;
|
||||
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
|
||||
const copyHintTimeoutRef = React.useRef<number | null>(null);
|
||||
@@ -899,11 +897,9 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
}}
|
||||
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
||||
>
|
||||
<div
|
||||
className="px-3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="leading-normal overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
|
||||
className="leading-relaxed overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
|
||||
>
|
||||
{renderedParts}
|
||||
{showErrorMessage && (
|
||||
|
||||
@@ -19,7 +19,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
|
||||
|
||||
return (
|
||||
<FadeInOnReveal>
|
||||
<div className={cn('pl-3', 'mb-2')}>
|
||||
<div className={cn('mb-2')}>
|
||||
<div className={cn('flex items-center justify-between gap-2')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
|
||||
@@ -59,7 +59,7 @@ export const FixedSessionsButton: React.FC = () => {
|
||||
}
|
||||
}, [isMobile, setSessionSwitcherOpen, toggleSidebar]);
|
||||
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||
|
||||
if (isMobile || !isDesktopApp || !isMacPlatform) {
|
||||
return null;
|
||||
@@ -89,7 +89,6 @@ export const Header: React.FC = () => {
|
||||
const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette);
|
||||
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
|
||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||
|
||||
@@ -168,12 +167,11 @@ export const Header: React.FC = () => {
|
||||
setSettingsDialogOpen(true);
|
||||
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
||||
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||
|
||||
const desktopPaddingClass = React.useMemo(() => {
|
||||
if (isDesktopApp && isMacPlatform) {
|
||||
|
||||
return isSidebarOpen ? 'pl-0' : 'pl-[8.0rem]';
|
||||
return isSidebarOpen ? 'pl-2' : 'pl-[8.0rem]';
|
||||
}
|
||||
return 'pl-3';
|
||||
}, [isDesktopApp, isMacPlatform, isSidebarOpen]);
|
||||
@@ -221,15 +219,12 @@ export const Header: React.FC = () => {
|
||||
}, [updateHeaderHeight, isMobile]);
|
||||
|
||||
const handleDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||
|
||||
if ((e.target as HTMLElement).closest('button, a, input, select, textarea')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
@@ -242,11 +237,9 @@ export const Header: React.FC = () => {
|
||||
}, [isDesktopApp]);
|
||||
|
||||
const handleActiveTabDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
@@ -278,7 +271,6 @@ export const Header: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
||||
const num = parseInt(e.key, 10);
|
||||
if (num >= 1 && num <= tabs.length) {
|
||||
@@ -287,82 +279,58 @@ export const Header: React.FC = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [tabs, setActiveMainTab]);
|
||||
|
||||
const renderTab = (tab: TabConfig, isLast: boolean) => {
|
||||
const renderTab = (tab: TabConfig) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
const Icon = tab.icon;
|
||||
const isChatTab = tab.id === 'chat';
|
||||
const isGitTab = tab.id === 'git';
|
||||
|
||||
return (
|
||||
<React.Fragment key={tab.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveMainTab(tab.id)}
|
||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||
className={cn(
|
||||
'relative flex h-full items-center gap-2 px-4 typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag' : 'app-region-no-drag',
|
||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground',
|
||||
|
||||
isActive && 'after:absolute after:bottom-[-1px] after:left-0 after:right-0 after:h-[2px] after:bg-background',
|
||||
|
||||
isActive && isChatTab && isSidebarOpen && 'before:absolute before:bottom-[-1px] before:right-full before:h-px before:bg-[var(--interactive-border)] before:w-[var(--sidebar-w)]',
|
||||
|
||||
isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen) && 'border-l',
|
||||
|
||||
isGitTab && 'border-r',
|
||||
|
||||
isChatTab && !isMobile && 'min-w-[165px] max-[1024px]:min-w-0'
|
||||
)}
|
||||
style={{
|
||||
...(isActive && isChatTab && isSidebarOpen ? {
|
||||
|
||||
['--sidebar-w' as string]: (isDesktopApp && isMacPlatform) ? `${sidebarWidth}px` : '64px'
|
||||
} : {}),
|
||||
...((isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen)) || isGitTab ? { borderColor: 'var(--interactive-border)' } : {}),
|
||||
}}
|
||||
aria-label={tab.label}
|
||||
aria-selected={isActive}
|
||||
role="tab"
|
||||
>
|
||||
{isMobile ? (
|
||||
<Icon size={20} />
|
||||
) : (
|
||||
<>
|
||||
<Icon size={16} />
|
||||
<span className="header-tab-label">{tab.label}</span>
|
||||
</>
|
||||
)}
|
||||
{}
|
||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
||||
<span className="ml-1">
|
||||
<ContextUsageDisplay
|
||||
totalTokens={contextUsage.totalTokens}
|
||||
percentage={contextUsage.percentage}
|
||||
contextLimit={contextUsage.contextLimit}
|
||||
outputLimit={contextUsage.outputLimit ?? 0}
|
||||
size="compact"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{}
|
||||
{tab.badge !== undefined && tab.badge > 0 && (
|
||||
<span className="text-xs font-semibold text-primary">
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{}
|
||||
{!isLast && (
|
||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => setActiveMainTab(tab.id)}
|
||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||
className={cn(
|
||||
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isChatTab && !isMobile && 'min-w-[100px] justify-center'
|
||||
)}
|
||||
</React.Fragment>
|
||||
aria-label={tab.label}
|
||||
aria-selected={isActive}
|
||||
role="tab"
|
||||
>
|
||||
{isMobile ? (
|
||||
<Icon size={20} />
|
||||
) : (
|
||||
<>
|
||||
<Icon size={16} />
|
||||
<span className="header-tab-label">{tab.label}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
||||
<span className="ml-1">
|
||||
<ContextUsageDisplay
|
||||
totalTokens={contextUsage.totalTokens}
|
||||
percentage={contextUsage.percentage}
|
||||
contextLimit={contextUsage.contextLimit}
|
||||
outputLimit={contextUsage.outputLimit ?? 0}
|
||||
size="compact"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tab.badge !== undefined && tab.badge > 0 && (
|
||||
<span className="ml-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/10 px-1 text-[10px] font-bold text-primary">
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -376,29 +344,25 @@ export const Header: React.FC = () => {
|
||||
role="tablist"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{}
|
||||
{!(isDesktopApp && isMacPlatform) && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
aria-label="Open sessions"
|
||||
className={`${headerIconButtonClass} mr-2.5`}
|
||||
className={`${headerIconButtonClass} mr-2`}
|
||||
>
|
||||
<RiLayoutLeftLine className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
<div className="flex h-full items-center">
|
||||
{tabs.map((tab, index) => renderTab(tab, index === tabs.length - 1))}
|
||||
<div className="flex items-center gap-1 p-1 bg-background/50 rounded-lg">
|
||||
{tabs.map((tab) => renderTab(tab))}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="flex-1" />
|
||||
|
||||
{}
|
||||
<div className="flex items-center gap-1 pr-3">
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -442,7 +406,7 @@ export const Header: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
|
||||
aria-label="Open sessions"
|
||||
>
|
||||
<RiPlayListAddLine className="h-5 w-5" />
|
||||
@@ -459,9 +423,10 @@ export const Header: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-region-no-drag flex items-center gap-1.5">
|
||||
<div className="app-region-no-drag flex items-center gap-1">
|
||||
|
||||
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
|
||||
|
||||
<div className="flex items-center" role="tablist" aria-label="Main navigation">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
const Icon = tab.icon;
|
||||
@@ -482,7 +447,7 @@ export const Header: React.FC = () => {
|
||||
className={cn(
|
||||
headerIconButtonClass,
|
||||
'relative',
|
||||
isActive && 'text-foreground'
|
||||
isActive && 'text-foreground bg-secondary'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
@@ -535,7 +500,7 @@ export const Header: React.FC = () => {
|
||||
);
|
||||
|
||||
const headerClassName = cn(
|
||||
'header-safe-area border-b relative z-10',
|
||||
'header-safe-area border-b border-border/50 relative z-10',
|
||||
isDesktopApp ? 'bg-background' : 'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
|
||||
);
|
||||
|
||||
@@ -543,7 +508,7 @@ export const Header: React.FC = () => {
|
||||
<header
|
||||
ref={headerRef}
|
||||
className={headerClassName}
|
||||
style={{ borderColor: 'var(--interactive-border)', ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
||||
style={{ ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
||||
>
|
||||
{isMobile ? renderMobile() : renderDesktop()}
|
||||
</header>
|
||||
|
||||
@@ -168,19 +168,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'relative flex h-full overflow-hidden border-r',
|
||||
'relative flex h-full overflow-hidden border-r border-border',
|
||||
isDesktopApp
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
? 'bg-sidebar/95 backdrop-blur supports-[backdrop-filter]:bg-sidebar/80'
|
||||
: 'bg-sidebar',
|
||||
isResizing ? 'transition-none' : '',
|
||||
isResizing ? 'transition-none' : 'transition-[width] duration-300 ease-in-out',
|
||||
!isOpen && 'border-r-0'
|
||||
)}
|
||||
style={{
|
||||
width: `${appliedWidth}px`,
|
||||
minWidth: `${appliedWidth}px`,
|
||||
maxWidth: `${appliedWidth}px`,
|
||||
pointerEvents: !isOpen ? 'none' : undefined,
|
||||
borderColor: 'var(--interactive-border)',
|
||||
overflowX: 'clip',
|
||||
}}
|
||||
aria-hidden={!isOpen || appliedWidth === 0}
|
||||
@@ -188,8 +186,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
|
||||
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
|
||||
'absolute right-0 top-0 z-20 h-full w-[4px] cursor-col-resize hover:bg-primary/50 transition-colors',
|
||||
isResizing && 'bg-primary'
|
||||
)}
|
||||
onPointerDown={handlePointerDown}
|
||||
role="separator"
|
||||
@@ -199,7 +197,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-full flex-col transition-opacity duration-200 ease-in-out',
|
||||
'relative z-10 flex h-full flex-col transition-opacity duration-300 ease-in-out',
|
||||
!isOpen && 'pointer-events-none select-none opacity-0'
|
||||
)}
|
||||
style={{ width: `${appliedWidth}px`, overflowX: 'hidden' }}
|
||||
@@ -216,44 +214,45 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
<div className="flex-shrink-0 border-t border-border p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar-accent/10">
|
||||
<div className="flex h-full items-center justify-between gap-2">
|
||||
<button
|
||||
onClick={() => setSettingsDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-3 py-2',
|
||||
'text-sm font-semibold text-muted-foreground',
|
||||
'hover:text-foreground',
|
||||
'transition-colors'
|
||||
'flex h-8 items-center gap-2 rounded-md px-2',
|
||||
'text-sm font-semibold text-sidebar-foreground/90',
|
||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<RiSettings3Line className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
{(available || downloaded) ? (
|
||||
<button
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5',
|
||||
'text-xs font-medium',
|
||||
'bg-primary/10 text-primary',
|
||||
'hover:bg-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
<RiDownloadLine className="h-3.5 w-3.5" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2 py-1',
|
||||
'text-xs font-semibold',
|
||||
'bg-primary/10 text-primary',
|
||||
'hover:bg-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
<RiDownloadLine className="h-3.5 w-3.5" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
|
||||
) : !isDesktopApp && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setAboutDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-md p-1.5',
|
||||
'text-muted-foreground',
|
||||
'hover:text-foreground hover:bg-muted/50',
|
||||
'transition-colors'
|
||||
'flex h-8 w-8 items-center justify-center rounded-md',
|
||||
'text-sidebar-foreground/70',
|
||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<RiInformationLine className="h-4 w-4" />
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
||||
onTouchStart={isMobile ? handleLongPressStart : undefined}
|
||||
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
|
||||
>
|
||||
<RiDonutChartLine className="h-4 w-4 flex-shrink-0" />
|
||||
{!isMobile && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
||||
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
|
||||
{Math.min(percentage, 999).toFixed(1)}%
|
||||
</span>
|
||||
|
||||
@@ -16,9 +16,22 @@ import {
|
||||
RiSave3Line,
|
||||
RiSendPlane2Line,
|
||||
RiTextWrap,
|
||||
RiMore2Fill,
|
||||
RiFileAddLine,
|
||||
RiFolderAddLine,
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiFileCopyLine,
|
||||
} from '@remixicon/react';
|
||||
import { toast } from '@/components/ui';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -273,6 +286,24 @@ export const FilesView: React.FC = () => {
|
||||
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null);
|
||||
const skipDirtyOnceRef = React.useRef(false);
|
||||
|
||||
const [activeDialog, setActiveDialog] = React.useState<'createFile' | 'createFolder' | 'rename' | 'delete' | null>(null);
|
||||
const [dialogData, setDialogData] = React.useState<{ path: string; name?: string; type?: 'file' | 'directory' } | null>(null);
|
||||
const [dialogInputValue, setDialogInputValue] = React.useState('');
|
||||
const [isDialogSubmitting, setIsDialogSubmitting] = React.useState(false);
|
||||
const [contextMenuPath, setContextMenuPath] = React.useState<string | null>(null);
|
||||
|
||||
const canCreateFile = Boolean(files.writeFile);
|
||||
const canCreateFolder = Boolean(files.createDirectory);
|
||||
const canRename = Boolean(files.rename);
|
||||
const canDelete = Boolean(files.delete);
|
||||
|
||||
const handleOpenDialog = React.useCallback((type: 'createFile' | 'createFolder' | 'rename' | 'delete', data: { path: string; name?: string; type?: 'file' | 'directory' }) => {
|
||||
setActiveDialog(type);
|
||||
setDialogData(data);
|
||||
setDialogInputValue(type === 'rename' ? data.name || '' : '');
|
||||
setIsDialogSubmitting(false);
|
||||
}, []);
|
||||
|
||||
// Line selection state for commenting
|
||||
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
|
||||
const [commentText, setCommentText] = React.useState('');
|
||||
@@ -509,6 +540,78 @@ export const FilesView: React.FC = () => {
|
||||
void refreshRoot();
|
||||
}, [currentDirectory, refreshRoot, showGitignored]);
|
||||
|
||||
const handleDialogSubmit = React.useCallback(async (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
if (!dialogData || !activeDialog) return;
|
||||
|
||||
setIsDialogSubmitting(true);
|
||||
try {
|
||||
if (activeDialog === 'createFile') {
|
||||
if (!dialogInputValue.trim()) throw new Error('Filename is required');
|
||||
const parentPath = dialogData.path;
|
||||
// Handle root path or empty path
|
||||
const prefix = parentPath ? `${parentPath}/` : '';
|
||||
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||
|
||||
if (!files.writeFile) throw new Error('Write not supported');
|
||||
const result = await files.writeFile(newPath, '');
|
||||
if (result.success) {
|
||||
toast.success('File created');
|
||||
await refreshRoot();
|
||||
}
|
||||
} else if (activeDialog === 'createFolder') {
|
||||
if (!dialogInputValue.trim()) throw new Error('Folder name is required');
|
||||
const parentPath = dialogData.path;
|
||||
const prefix = parentPath ? `${parentPath}/` : '';
|
||||
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||
|
||||
const result = await files.createDirectory(newPath);
|
||||
if (result.success) {
|
||||
toast.success('Folder created');
|
||||
await refreshRoot();
|
||||
}
|
||||
} else if (activeDialog === 'rename') {
|
||||
if (!dialogInputValue.trim()) throw new Error('Name is required');
|
||||
const oldPath = dialogData.path;
|
||||
const parentDir = oldPath.split('/').slice(0, -1).join('/');
|
||||
const prefix = parentDir ? `${parentDir}/` : '';
|
||||
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||
|
||||
if (files.rename) {
|
||||
const result = await files.rename(oldPath, newPath);
|
||||
if (result.success) {
|
||||
toast.success('Renamed successfully');
|
||||
await refreshRoot();
|
||||
if (selectedFile?.path === oldPath) {
|
||||
setSelectedFile(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error("Rename not supported");
|
||||
}
|
||||
} else if (activeDialog === 'delete') {
|
||||
if (files.delete) {
|
||||
const result = await files.delete(dialogData.path);
|
||||
if (result.success) {
|
||||
toast.success('Deleted successfully');
|
||||
await refreshRoot();
|
||||
if (selectedFile?.path === dialogData.path || selectedFile?.path.startsWith(dialogData.path + '/')) {
|
||||
setSelectedFile(null);
|
||||
setFileContent('');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error("Delete not supported");
|
||||
}
|
||||
}
|
||||
setActiveDialog(null);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Operation failed');
|
||||
} finally {
|
||||
setIsDialogSubmitting(false);
|
||||
}
|
||||
}, [activeDialog, dialogData, dialogInputValue, files, refreshRoot, selectedFile]);
|
||||
|
||||
const fuzzyScore = React.useCallback((query: string, candidate: string): number | null => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) {
|
||||
@@ -853,56 +956,129 @@ export const FilesView: React.FC = () => {
|
||||
const renderTree = React.useCallback((dirPath: string, depth: number): React.ReactNode => {
|
||||
const nodes = childrenByDir[dirPath] ?? [];
|
||||
|
||||
return nodes.map((node) => {
|
||||
return nodes.map((node, index) => {
|
||||
const isDir = node.type === 'directory';
|
||||
const isExpanded = isDir && expandedDirs.has(node.path);
|
||||
const isActive = selectedFile?.path === node.path;
|
||||
const isLoading = isDir && inFlightDirsRef.current.has(node.path);
|
||||
const isLast = index === nodes.length - 1;
|
||||
|
||||
return (
|
||||
<li key={node.path}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDir) {
|
||||
void toggleDirectory(node.path);
|
||||
} else {
|
||||
void handleSelectFile(node);
|
||||
<li key={node.path} className="relative">
|
||||
{depth > 0 && (
|
||||
<>
|
||||
<span className="absolute top-3.5 left-[-12px] w-3 h-px bg-border/40" />
|
||||
{isLast && (
|
||||
<span className="absolute top-3.5 bottom-0 left-[-13px] w-[2px] bg-background" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="group relative flex items-center"
|
||||
onContextMenu={(event) => {
|
||||
if (!canRename && !canCreateFile && !canCreateFolder && !canDelete) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setContextMenuPath(node.path);
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
|
||||
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
||||
)}
|
||||
style={{ paddingLeft: `${8 + depth * 12}px` }}
|
||||
>
|
||||
{isDir ? (
|
||||
isLoading ? (
|
||||
<RiLoader4Line className="h-4 w-4 flex-shrink-0 animate-spin" />
|
||||
) : isExpanded ? (
|
||||
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
) : (
|
||||
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
)
|
||||
) : (
|
||||
getFileIcon(node.extension)
|
||||
)}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate typography-meta"
|
||||
title={node.path}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDir) {
|
||||
void toggleDirectory(node.path);
|
||||
} else {
|
||||
void handleSelectFile(node);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
|
||||
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
||||
)}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
</button>
|
||||
{isDir ? (
|
||||
isLoading ? (
|
||||
<RiLoader4Line className="h-4 w-4 flex-shrink-0 animate-spin" />
|
||||
) : isExpanded ? (
|
||||
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
) : (
|
||||
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
)
|
||||
) : (
|
||||
getFileIcon(node.extension)
|
||||
)}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate typography-meta"
|
||||
title={node.path}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
</button>
|
||||
{(canRename || canCreateFile || canCreateFolder || canDelete) && (
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 focus-within:opacity-100">
|
||||
<DropdownMenu
|
||||
open={contextMenuPath === node.path}
|
||||
onOpenChange={(open) => setContextMenuPath(open ? node.path : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<RiMore2Fill className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onCloseAutoFocus={() => setContextMenuPath(null)}>
|
||||
{canRename && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('rename', node); }}>
|
||||
<RiEditLine className="mr-2 h-4 w-4" /> Rename
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void navigator.clipboard.writeText(node.path);
|
||||
toast.success('Path copied');
|
||||
}}>
|
||||
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
|
||||
</DropdownMenuItem>
|
||||
{isDir && (canCreateFile || canCreateFolder) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{canCreateFile && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('createFile', node); }}>
|
||||
<RiFileAddLine className="mr-2 h-4 w-4" /> New File
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canCreateFolder && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('createFolder', node); }}>
|
||||
<RiFolderAddLine className="mr-2 h-4 w-4" /> New Folder
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => { e.stopPropagation(); handleOpenDialog('delete', node); }}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="mr-2 h-4 w-4" /> Delete
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDir && isExpanded && (
|
||||
<ul className="flex flex-col gap-1">
|
||||
<ul className="flex flex-col gap-1 ml-3 pl-3 border-l border-border/40 relative">
|
||||
{renderTree(node.path, depth + 1)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory]);
|
||||
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory, handleOpenDialog, canCreateFile, canCreateFolder, canRename, canDelete, contextMenuPath, setContextMenuPath]);
|
||||
|
||||
const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path));
|
||||
const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg'));
|
||||
@@ -1000,6 +1176,58 @@ export const FilesView: React.FC = () => {
|
||||
};
|
||||
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path]);
|
||||
|
||||
const renderDialogs = () => (
|
||||
<Dialog open={!!activeDialog} onOpenChange={(open) => !open && setActiveDialog(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{activeDialog === 'createFile' && 'Create File'}
|
||||
{activeDialog === 'createFolder' && 'Create Folder'}
|
||||
{activeDialog === 'rename' && 'Rename'}
|
||||
{activeDialog === 'delete' && 'Delete'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{activeDialog === 'createFile' && `Create a new file in ${dialogData?.path ?? 'root'}`}
|
||||
{activeDialog === 'createFolder' && `Create a new folder in ${dialogData?.path ?? 'root'}`}
|
||||
{activeDialog === 'rename' && `Rename ${dialogData?.name}`}
|
||||
{activeDialog === 'delete' && `Are you sure you want to delete ${dialogData?.name}? This action cannot be undone.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{activeDialog !== 'delete' && (
|
||||
<div className="py-4">
|
||||
<Input
|
||||
value={dialogInputValue}
|
||||
onChange={(e) => setDialogInputValue(e.target.value)}
|
||||
placeholder={activeDialog === 'rename' ? 'New name' : 'Name'}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
void handleDialogSubmit();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setActiveDialog(null)} disabled={isDialogSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeDialog === 'delete' ? 'destructive' : 'default'}
|
||||
onClick={() => void handleDialogSubmit()}
|
||||
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
|
||||
>
|
||||
{isDialogSubmitting ? <RiLoader4Line className="animate-spin" /> : (
|
||||
activeDialog === 'delete' ? 'Delete' : 'Confirm'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
// Comment UI component
|
||||
const renderCommentUI = () => {
|
||||
if (!lineSelection || !selectedFile) return null;
|
||||
@@ -1369,6 +1597,24 @@ export const FilesView: React.FC = () => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })}
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
title="New File"
|
||||
>
|
||||
<RiFileAddLine className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDialog('createFolder', { path: currentDirectory, type: 'directory' })}
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
title="New Folder"
|
||||
>
|
||||
<RiFolderAddLine className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0">
|
||||
<RiRefreshLine className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -1419,6 +1665,7 @@ export const FilesView: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 overflow-hidden bg-background">
|
||||
{renderDialogs()}
|
||||
{isMobile ? (
|
||||
showMobilePageContent ? (
|
||||
fileViewer
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FileDiff } from '@pierre/diffs/react';
|
||||
import { parseDiffFromFile, type FileContents, type FileDiffMetadata, type SelectedLineRange } from '@pierre/diffs';
|
||||
import { RiArrowDownSLine, RiEyeLine, RiSendPlane2Line } from '@remixicon/react';
|
||||
import { RiSendPlane2Line } from '@remixicon/react';
|
||||
|
||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
@@ -141,7 +141,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
|
||||
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
||||
const [commentText, setCommentText] = useState('');
|
||||
|
||||
const commentContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Calculate initial center synchronously to avoid flicker
|
||||
const getMainContentCenter = useCallback(() => {
|
||||
@@ -212,8 +212,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if click is inside the comment UI portal
|
||||
const commentUI = document.querySelector('[data-comment-ui]');
|
||||
if (commentUI?.contains(target)) return;
|
||||
if (commentContainerRef.current?.contains(target)) return;
|
||||
|
||||
// Check if click is inside toast (sonner)
|
||||
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
||||
@@ -302,21 +301,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
fileDiff: FileDiffMetadata;
|
||||
} | null>(null);
|
||||
|
||||
// Threshold for lazy loading (total lines > 1500 or content size > 150KB)
|
||||
const isLargeDiff = useMemo(() => {
|
||||
const totalLines = (original || '').split('\n').length + (modified || '').split('\n').length;
|
||||
const totalSize = (original?.length || 0) + (modified?.length || 0);
|
||||
return totalLines > 1500 || totalSize > 150 * 1024;
|
||||
}, [original, modified]);
|
||||
|
||||
// State for large diff loading
|
||||
const [shouldLoad, setShouldLoad] = useState(false);
|
||||
|
||||
// Parse diff when loaded (for large diffs) or always (for small diffs)
|
||||
// Pre-parse the diff with cacheKey for worker pool caching
|
||||
const fileDiff = useMemo(() => {
|
||||
// For large diffs, only parse if manually triggered
|
||||
if (isLargeDiff && !shouldLoad) return null;
|
||||
|
||||
const cacheKey = getCacheKey(fileName, original, modified);
|
||||
|
||||
// Return cached diff if inputs haven't changed
|
||||
@@ -344,7 +330,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
||||
|
||||
return diff;
|
||||
}, [fileName, original, modified, language, isLargeDiff, shouldLoad]);
|
||||
}, [fileName, original, modified, language]);
|
||||
|
||||
const options = useMemo(() => ({
|
||||
theme: {
|
||||
@@ -363,45 +349,11 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
onLineSelected: handleSelectionChange,
|
||||
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
||||
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
|
||||
|
||||
// Show placeholder for large diffs
|
||||
if (isLargeDiff && !fileDiff) {
|
||||
const originalLines = (original || '').split('\n').length;
|
||||
const modifiedLines = (modified || '').split('\n').length;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full min-h-[200px] p-6">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="p-3 rounded-full bg-muted/50">
|
||||
<RiEyeLine className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="typography-ui-header font-medium text-foreground">Large Diff</div>
|
||||
<div className="typography-meta text-muted-foreground mt-1">
|
||||
{originalLines + modifiedLines} lines
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShouldLoad(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 mt-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors typography-meta font-medium"
|
||||
>
|
||||
<RiArrowDownSLine className="size-4" />
|
||||
Load Diff
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// fileDiff should not be null here (handled by large diff placeholder above)
|
||||
if (!fileDiff) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Extracted Comment Interface Content for reuse in Portal or In-Flow
|
||||
const renderCommentContent = () => {
|
||||
if (!selection) return null;
|
||||
@@ -521,6 +473,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
: '16px'
|
||||
}}
|
||||
data-keyboard-avoid="true"
|
||||
data-comment-ui="true"
|
||||
ref={commentContainerRef}
|
||||
>
|
||||
{commentContent}
|
||||
</div>
|
||||
@@ -566,6 +520,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
}}
|
||||
data-keyboard-avoid="true"
|
||||
data-comment-ui="true"
|
||||
ref={commentContainerRef}
|
||||
>
|
||||
{commentContent}
|
||||
</div>
|
||||
|
||||
@@ -325,7 +325,6 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return '';
|
||||
}, [isDesktopApp, isMacPlatform]);
|
||||
|
||||
const showLeadingDivider = isDesktopApp && isMacPlatform;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn('flex h-full flex-col overflow-hidden', isDesktopApp ? 'bg-transparent' : 'bg-background')}>
|
||||
@@ -356,48 +355,46 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
{isMobile && <div className="flex-1" />}
|
||||
|
||||
<div className={cn('flex items-center', isMobile ? 'gap-1' : 'h-full')}>
|
||||
{/* Leading divider before first tab - only on Mac desktop */}
|
||||
{!isMobile && showLeadingDivider && <div className="h-full w-px bg-border" aria-hidden="true" />}
|
||||
{settingsSections.map(({ id, label, icon: Icon }) => {
|
||||
const isActive = activeTab === id;
|
||||
const PhosphorIcon = Icon as React.ComponentType<{ className?: string; weight?: string }>;
|
||||
<div className={cn('flex items-center gap-1', !isMobile && 'p-1 bg-background/50 rounded-lg')}>
|
||||
{settingsSections.map(({ id, label, icon: Icon }) => {
|
||||
const isActive = activeTab === id;
|
||||
const PhosphorIcon = Icon as React.ComponentType<{ className?: string; weight?: string }>;
|
||||
|
||||
if (isMobile) {
|
||||
// Mobile: icon-only buttons
|
||||
if (isMobile) {
|
||||
// Mobile: icon-only buttons
|
||||
return (
|
||||
<Tooltip key={id} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => handleTabChange(id)}
|
||||
className={cn(
|
||||
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isActive ? 'bg-secondary text-foreground shadow-sm' : 'text-muted-foreground'
|
||||
)}
|
||||
aria-pressed={isActive}
|
||||
aria-label={label}
|
||||
>
|
||||
<PhosphorIcon className="h-5 w-5" weight="regular" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: pill tabs like main header
|
||||
return (
|
||||
<Tooltip key={id} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => handleTabChange(id)}
|
||||
className={cn(
|
||||
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
aria-pressed={isActive}
|
||||
aria-label={label}
|
||||
>
|
||||
<PhosphorIcon className="h-5 w-5" weight="regular" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: full tabs with text and dividers
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => handleTabChange(id)}
|
||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||
className={cn(
|
||||
'relative flex h-full items-center gap-2 px-4 typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag' : 'app-region-no-drag',
|
||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
||||
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
||||
)}
|
||||
aria-pressed={isActive}
|
||||
aria-label={label}
|
||||
@@ -405,11 +402,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
||||
{showTabLabels && <span>{label}</span>}
|
||||
</button>
|
||||
{/* Vertical divider after each tab */}
|
||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(onClose || showProjectSwitcher) && (
|
||||
|
||||
+20
-943
@@ -1,33 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "./styles/design-system.css";
|
||||
@import "./styles/typography.css";
|
||||
@import "./styles/mobile.css";
|
||||
@source "./**/*.{ts,tsx,css}";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--oc-safe-area-top: 0px;
|
||||
--oc-safe-area-right: 0px;
|
||||
--oc-safe-area-bottom: 0px;
|
||||
--oc-safe-area-bottom-visual: 0px;
|
||||
--oc-safe-area-left: 0px;
|
||||
--oc-header-height: 56px;
|
||||
--oc-visual-viewport-offset-top: 0px;
|
||||
--oc-keyboard-inset: 0px;
|
||||
--oc-keyboard-avoid-offset: 0px;
|
||||
--oc-keyboard-home-indicator: 0px;
|
||||
--ui-regular-font-weight: 400;
|
||||
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
||||
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
||||
--padding-scale: 1;
|
||||
|
||||
/* Semantic typography defaults (must match SEMANTIC_TYPOGRAPHY) */
|
||||
--text-markdown: 0.9375rem;
|
||||
--text-code: 0.9063rem;
|
||||
--text-ui-header: 0.9375rem;
|
||||
--text-ui-label: 0.8750rem;
|
||||
--text-meta: 0.875rem;
|
||||
--text-micro: 0.875rem;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Suppress WebKit-specific hover/focus adornments on the chat textarea */
|
||||
@@ -240,399 +217,8 @@ svg.animate-spin {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Semantic Typography System Classes */
|
||||
@layer components {
|
||||
/* Semantic typography classes - font-size only */
|
||||
.typography-markdown {
|
||||
font-size: var(--text-markdown);
|
||||
}
|
||||
|
||||
.typography-code {
|
||||
font-size: var(--text-code);
|
||||
}
|
||||
|
||||
.typography-ui-header {
|
||||
font-size: var(--text-ui-header);
|
||||
}
|
||||
|
||||
.typography-ui-label {
|
||||
font-size: var(--text-ui-label);
|
||||
}
|
||||
|
||||
.typography-meta {
|
||||
font-size: var(--text-meta);
|
||||
}
|
||||
|
||||
.typography-micro {
|
||||
font-size: var(--text-micro);
|
||||
}
|
||||
|
||||
.chat-column {
|
||||
width: min(100%, 56rem);
|
||||
margin-inline: auto;
|
||||
padding-inline: calc(clamp(1rem, 3vw, 1.5rem) * var(--padding-scale, 1));
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.chat-column {
|
||||
padding-inline: calc(clamp(1.25rem, 2.5vw, 2rem) * var(--padding-scale, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced focus indicators for small interactive elements */
|
||||
.model-favorite-button {
|
||||
position: relative;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.model-favorite-button:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.model-favorite-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* FadeInOnReveal handles all animations - no custom CSS needed */
|
||||
|
||||
/* Heading typography - all use markdown size, differentiated by weight/color */
|
||||
|
||||
.typography-h1 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h1-line-height);
|
||||
letter-spacing: var(--h1-letter-spacing);
|
||||
font-weight: var(--h1-font-weight);
|
||||
}
|
||||
|
||||
.typography-h2 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h2-line-height);
|
||||
letter-spacing: var(--h2-letter-spacing);
|
||||
font-weight: var(--h2-font-weight);
|
||||
}
|
||||
|
||||
.typography-h3 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h3-line-height);
|
||||
letter-spacing: var(--h3-letter-spacing);
|
||||
font-weight: var(--h3-font-weight);
|
||||
}
|
||||
|
||||
/* UI element typography - mapped to semantic variables */
|
||||
.typography-ui-button {
|
||||
font-size: var(--text-ui-label);
|
||||
line-height: var(--ui-button-line-height);
|
||||
letter-spacing: var(--ui-button-letter-spacing);
|
||||
font-weight: var(--ui-button-font-weight);
|
||||
}
|
||||
|
||||
.typography-ui-caption {
|
||||
font-size: var(--text-micro);
|
||||
line-height: var(--ui-caption-line-height);
|
||||
letter-spacing: var(--ui-caption-letter-spacing);
|
||||
font-weight: var(--ui-caption-font-weight);
|
||||
}
|
||||
|
||||
/* Markdown typography - all use semantic markdown size */
|
||||
.typography-markdown-body {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-body-line-height);
|
||||
letter-spacing: var(--markdown-body-letter-spacing);
|
||||
font-weight: var(--markdown-body-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h1 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h1-line-height);
|
||||
letter-spacing: var(--markdown-h1-letter-spacing);
|
||||
font-weight: var(--markdown-h1-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h2 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h2-line-height);
|
||||
letter-spacing: var(--markdown-h2-letter-spacing);
|
||||
font-weight: var(--markdown-h2-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h3 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h3-line-height);
|
||||
letter-spacing: var(--markdown-h3-letter-spacing);
|
||||
font-weight: var(--markdown-h3-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h4 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h4-line-height);
|
||||
letter-spacing: var(--markdown-h4-letter-spacing);
|
||||
font-weight: var(--markdown-h4-font-weight);
|
||||
}
|
||||
|
||||
.animate-spin-once {
|
||||
animation: spin-once 0.5s linear forwards;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Default Light Theme */
|
||||
--background: oklch(0.97 0.02 85); /* Warm sand background */
|
||||
--foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--card: oklch(0.99 0.01 90); /* Bright sand card */
|
||||
--card-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--popover: oklch(0.99 0.01 90); /* Bright sand popover */
|
||||
--popover-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--primary: oklch(0.65 0.2 55); /* Primary accent - orange */
|
||||
--primary-foreground: oklch(0.99 0.01 90); /* Light text on primary */
|
||||
--secondary: oklch(0.92 0.02 80); /* Light sand */
|
||||
--secondary-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--muted: oklch(0.9 0.015 75); /* Muted sand */
|
||||
--muted-foreground: oklch(0.45 0.02 50); /* Medium warm text */
|
||||
--accent: oklch(0.92 0.02 80); /* Light sand accent */
|
||||
--accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--destructive: oklch(0.55 0.25 25); /* Warm red */
|
||||
--destructive-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||
--border: oklch(0.85 0.02 70); /* Warm border */
|
||||
--input: oklch(0.88 0.02 75); /* Input background */
|
||||
--ring: oklch(0.65 0.2 55); /* Focus ring color */
|
||||
--radius: 0.625rem;
|
||||
--chart-1: oklch(0.58 0.15 230); /* Blue */
|
||||
--chart-2: oklch(0.58 0.15 145); /* Oasis green */
|
||||
--chart-3: oklch(0.65 0.2 55); /* Orange */
|
||||
--chart-4: oklch(0.55 0.18 30); /* Rose */
|
||||
--chart-5: oklch(0.6 0.16 85); /* Golden sand */
|
||||
--sidebar: oklch(0.95 0.02 80); /* Slightly darker sand */
|
||||
--sidebar-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--sidebar-primary: oklch(0.65 0.2 55); /* Primary accent */
|
||||
--sidebar-primary-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||
--sidebar-accent: oklch(0.9 0.02 75); /* Light sand accent */
|
||||
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
||||
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
||||
--sidebar-stuck-bg: #DDDDD3; /* Desktop sidebar sticky header background */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Default Dark Theme - using OKLCH color space */
|
||||
--background: oklch(0.16 0.01 30); /* #151313 */
|
||||
--foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--card: oklch(0.19 0.01 40); /* #1C1B1A */
|
||||
--card-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--popover: oklch(0.24 0.01 40); /* #282726 */
|
||||
--popover-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--primary: oklch(0.77 0.17 85); /* #edb449 - golden sand */
|
||||
--primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||
--secondary: oklch(0.29 0.01 40); /* #343331 */
|
||||
--secondary-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--muted: oklch(0.33 0.01 40); /* #403E3C */
|
||||
--muted-foreground: oklch(0.75 0.02 80); /* #b6b4ab */
|
||||
--accent: oklch(0.29 0.01 40); /* #343331 */
|
||||
--accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--destructive: oklch(0.65 0.15 30); /* #d98678 */
|
||||
--destructive-foreground: oklch(0.9 0.02 80); /* #e1dcd0 */
|
||||
--border: oklch(0.31 0.01 35); /* #393836 */
|
||||
--input: oklch(0.33 0.01 40); /* #403E3C */
|
||||
--ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--chart-1: oklch(0.68 0.12 230); /* #5aa9d9 - blue */
|
||||
--chart-2: oklch(0.68 0.12 145); /* #81af6c - green */
|
||||
--chart-3: oklch(0.7 0.13 95); /* #c2974d - yellow */
|
||||
--chart-4: oklch(0.65 0.14 45); /* #d8886d - coral */
|
||||
--chart-5: oklch(0.68 0.12 55); /* #d29470 - orange */
|
||||
--sidebar: oklch(0.16 0.01 30); /* #151313 */
|
||||
--sidebar-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--sidebar-primary: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--sidebar-primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||
--sidebar-accent: oklch(0.24 0.01 40); /* #282726 */
|
||||
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
||||
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--sidebar-stuck-bg: #1F1F1D; /* Desktop sidebar sticky header background */
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
:root * {
|
||||
border-color: rgba(194, 151, 77, 0.2); /* Warm golden sand borders for light mode */
|
||||
}
|
||||
|
||||
.dark * {
|
||||
border-color: rgba(57, 56, 54, 0.5); /* #393836 at 50% opacity */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
/* Override Tailwind spacing scale with custom properties that respect --padding-scale */
|
||||
--spacing-0: 0;
|
||||
--spacing-0\.5: calc(0.125rem * var(--padding-scale, 1));
|
||||
--spacing-1: calc(0.25rem * var(--padding-scale, 1));
|
||||
--spacing-1\.5: calc(0.375rem * var(--padding-scale, 1));
|
||||
--spacing-2: calc(0.5rem * var(--padding-scale, 1));
|
||||
--spacing-2\.5: calc(0.625rem * var(--padding-scale, 1));
|
||||
--spacing-3: calc(0.75rem * var(--padding-scale, 1));
|
||||
--spacing-3\.5: calc(0.875rem * var(--padding-scale, 1));
|
||||
--spacing-4: calc(1rem * var(--padding-scale, 1));
|
||||
--spacing-5: calc(1.25rem * var(--padding-scale, 1));
|
||||
--spacing-6: calc(1.5rem * var(--padding-scale, 1));
|
||||
--spacing-7: calc(1.75rem * var(--padding-scale, 1));
|
||||
--spacing-8: calc(2rem * var(--padding-scale, 1));
|
||||
--spacing-9: calc(2.25rem * var(--padding-scale, 1));
|
||||
--spacing-10: calc(2.5rem * var(--padding-scale, 1));
|
||||
--spacing-11: calc(2.75rem * var(--padding-scale, 1));
|
||||
--spacing-12: calc(3rem * var(--padding-scale, 1));
|
||||
--spacing-14: calc(3.5rem * var(--padding-scale, 1));
|
||||
--spacing-16: calc(4rem * var(--padding-scale, 1));
|
||||
--spacing-20: calc(5rem * var(--padding-scale, 1));
|
||||
--spacing-24: calc(6rem * var(--padding-scale, 1));
|
||||
--spacing-28: calc(7rem * var(--padding-scale, 1));
|
||||
--spacing-32: calc(8rem * var(--padding-scale, 1));
|
||||
--spacing-36: calc(9rem * var(--padding-scale, 1));
|
||||
--spacing-40: calc(10rem * var(--padding-scale, 1));
|
||||
--spacing-44: calc(11rem * var(--padding-scale, 1));
|
||||
--spacing-48: calc(12rem * var(--padding-scale, 1));
|
||||
--spacing-52: calc(13rem * var(--padding-scale, 1));
|
||||
--spacing-56: calc(14rem * var(--padding-scale, 1));
|
||||
--spacing-60: calc(15rem * var(--padding-scale, 1));
|
||||
--spacing-64: calc(16rem * var(--padding-scale, 1));
|
||||
--spacing-72: calc(18rem * var(--padding-scale, 1));
|
||||
--spacing-80: calc(20rem * var(--padding-scale, 1));
|
||||
--spacing-96: calc(24rem * var(--padding-scale, 1));
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-status-success: var(--status-success);
|
||||
--color-status-warning: var(--status-warning);
|
||||
--color-status-error: var(--status-error);
|
||||
--color-status-info: var(--status-info);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* Markdown typography variables */
|
||||
--markdown-body-font-size: var(--text-markdown);
|
||||
--markdown-body-line-height: var(--markdown-body-line-height);
|
||||
--markdown-h1-font-size: var(--text-markdown);
|
||||
--markdown-h1-line-height: var(--markdown-h1-line-height);
|
||||
--markdown-h2-font-size: var(--text-markdown);
|
||||
--markdown-h2-line-height: var(--markdown-h2-line-height);
|
||||
--markdown-h3-font-size: var(--text-markdown);
|
||||
--markdown-h3-line-height: var(--markdown-h3-line-height);
|
||||
--markdown-h4-font-size: var(--text-markdown);
|
||||
--markdown-h4-line-height: var(--markdown-h4-line-height);
|
||||
--markdown-h5-font-size: var(--text-markdown);
|
||||
--markdown-h5-line-height: var(--markdown-h5-line-height);
|
||||
--markdown-h6-font-size: var(--text-markdown);
|
||||
--markdown-h6-line-height: var(--markdown-h6-line-height);
|
||||
--markdown-list-font-size: var(--text-markdown);
|
||||
--markdown-list-line-height: var(--markdown-list-line-height);
|
||||
--markdown-code-block-font-size: var(--text-code);
|
||||
--markdown-code-block-line-height: var(--markdown-code-block-line-height);
|
||||
|
||||
/* Markdown color variables */
|
||||
--markdown-heading1: var(--markdown-heading1);
|
||||
--markdown-heading2: var(--markdown-heading2);
|
||||
--markdown-heading3: var(--markdown-heading3);
|
||||
--markdown-heading4: var(--markdown-heading4);
|
||||
--markdown-link: var(--markdown-link);
|
||||
--markdown-link-hover: var(--markdown-link-hover);
|
||||
--markdown-list-marker: var(--markdown-list-marker);
|
||||
--markdown-inline-code: var(--markdown-inline-code);
|
||||
--markdown-inline-code-bg: var(--markdown-inline-code-bg);
|
||||
--markdown-blockquote: var(--markdown-blockquote);
|
||||
--markdown-blockquote-border: var(--markdown-blockquote-border);
|
||||
|
||||
/* Markdown spacing defaults */
|
||||
--markdown-paragraph-spacing: 0.35rem;
|
||||
--markdown-heading-primary-top: 0.75rem;
|
||||
--markdown-heading-primary-bottom: 0.35rem;
|
||||
--markdown-heading-secondary-top: 0.6rem;
|
||||
--markdown-heading-secondary-bottom: 0.3rem;
|
||||
--markdown-blockquote-spacing: 0.6rem;
|
||||
--markdown-blockquote-padding: 0.75rem;
|
||||
--markdown-divider-spacing: 0.85rem;
|
||||
--markdown-table-spacing: 0.9rem;
|
||||
--markdown-heading1-size: var(--text-markdown);
|
||||
--markdown-heading2-size: var(--text-markdown);
|
||||
--markdown-heading3-size: var(--text-markdown);
|
||||
--markdown-heading4-size: var(--text-markdown);
|
||||
--markdown-heading5-size: var(--text-markdown);
|
||||
--markdown-heading6-size: var(--text-markdown);
|
||||
--font-sans: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||
--font-heading: var(--font-sans);
|
||||
--font-mono: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||
--font-family-sans: var(--font-sans);
|
||||
--font-family-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-none;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
||||
font-weight: var(--ui-regular-font-weight, 400);
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
||||
}
|
||||
|
||||
.font-mono,
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, 'Liberation Mono', Menlo, monospace) !important;
|
||||
}
|
||||
|
||||
/* Remove focus rings globally */
|
||||
*:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.animate-spin-once {
|
||||
animation: spin-once 0.5s linear forwards;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styles */
|
||||
@@ -843,82 +429,6 @@ html:not(.dark) .chat-scroll {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Agent color system using theme variables */
|
||||
@layer components {
|
||||
/* Agent color variants - each maps to theme colors */
|
||||
.agent-primary {
|
||||
--agent-color: var(--primary);
|
||||
--agent-color-bg: var(--primary);
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
--agent-color: var(--status-info);
|
||||
--agent-color-bg: var(--status-info);
|
||||
}
|
||||
|
||||
.agent-keyword {
|
||||
--agent-color: var(--syntax-keyword);
|
||||
--agent-color-bg: var(--syntax-keyword);
|
||||
}
|
||||
|
||||
.agent-success {
|
||||
--agent-color: var(--status-success);
|
||||
--agent-color-bg: var(--status-success);
|
||||
}
|
||||
|
||||
.agent-function {
|
||||
--agent-color: var(--syntax-function);
|
||||
--agent-color-bg: var(--syntax-function);
|
||||
}
|
||||
|
||||
.agent-number {
|
||||
--agent-color: var(--syntax-number);
|
||||
--agent-color-bg: var(--syntax-number);
|
||||
}
|
||||
|
||||
.agent-type {
|
||||
--agent-color: var(--syntax-type);
|
||||
--agent-color-bg: var(--syntax-type);
|
||||
}
|
||||
|
||||
.agent-warning {
|
||||
--agent-color: var(--status-warning);
|
||||
--agent-color-bg: var(--status-warning);
|
||||
}
|
||||
|
||||
.agent-variable {
|
||||
--agent-color: var(--syntax-variable);
|
||||
--agent-color-bg: var(--syntax-variable);
|
||||
}
|
||||
|
||||
/* Apply colors with opacity for badges */
|
||||
.agent-badge {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||
border: 1px solid rgb(from var(--agent-color) r g b / 0.2);
|
||||
color: var(--agent-color);
|
||||
}
|
||||
|
||||
.agent-badge:hover {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.15);
|
||||
border-color: rgb(from var(--agent-color) r g b / 0.3);
|
||||
}
|
||||
|
||||
/* Agent dot indicator */
|
||||
.agent-dot {
|
||||
background: var(--agent-color);
|
||||
}
|
||||
|
||||
/* Agent list item */
|
||||
.agent-list-item:hover {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.05);
|
||||
}
|
||||
|
||||
.agent-list-item.active {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||
border-color: rgb(from var(--agent-color) r g b / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header tab responsiveness */
|
||||
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
|
||||
display: inline;
|
||||
@@ -930,454 +440,6 @@ html:not(.dark) .chat-scroll {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Adaptations - All Devices */
|
||||
|
||||
/* Set global device type variables */
|
||||
:root {
|
||||
--is-mobile: 0;
|
||||
--device-type: 'desktop';
|
||||
--font-scale: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) {
|
||||
--is-mobile: 1;
|
||||
--device-type: 'mobile';
|
||||
--font-scale: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for device-specific styling */
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root.desktop-runtime .mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* General mobile improvements for all mobile devices */
|
||||
@media (max-width: 1024px) {
|
||||
/* Override CSS custom properties for mobile */
|
||||
:root.mobile-pointer:not(.desktop-runtime) {
|
||||
--text-markdown: 1rem;
|
||||
--text-code: 0.875rem;
|
||||
--text-ui-header: 0.9375rem;
|
||||
--text-ui-label: 0.875rem;
|
||||
--text-meta: 0.875rem;
|
||||
--text-micro: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Force override with higher specificity */
|
||||
:root.mobile-pointer:not(.desktop-runtime) * {
|
||||
--text-markdown: 1rem !important;
|
||||
--text-code: 0.875rem !important;
|
||||
--text-ui-header: 0.9375rem !important;
|
||||
--text-ui-label: 0.875rem !important;
|
||||
--text-meta: 0.875rem !important;
|
||||
--text-micro: 0.8125rem !important;
|
||||
}
|
||||
|
||||
/* Additional override for elements using typography classes */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
|
||||
font-size: unset !important;
|
||||
}
|
||||
|
||||
/* Fix font size for tool displays */
|
||||
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
|
||||
--text-code: 0.875rem !important;
|
||||
font-size: var(--text-code) !important;
|
||||
}
|
||||
|
||||
/* Improve touch targets for mobile */
|
||||
:root.mobile-pointer:not(.desktop-runtime) button,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
/* Improve input field touch targets */
|
||||
:root.mobile-pointer:not(.desktop-runtime) input,
|
||||
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
||||
:root.mobile-pointer:not(.desktop-runtime) select {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* Fix mobile text inputs */
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) textarea {
|
||||
font-size: 16px !important; /* Prevents iOS zoom */
|
||||
}
|
||||
|
||||
/* Prevent keyboard on non-input elements */
|
||||
:root.mobile-pointer:not(.desktop-runtime)
|
||||
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Specific fix for session buttons */
|
||||
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Improve mobile spacing */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
|
||||
padding-top: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Fix mobile scroll containers */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Fix mobile viewport height */
|
||||
:root.mobile-pointer:not(.desktop-runtime),
|
||||
:root.mobile-pointer:not(.desktop-runtime) body {
|
||||
height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prevent iOS keyboard from scrolling the page */
|
||||
:root.device-mobile:not(.desktop-runtime) body,
|
||||
:root.device-mobile:not(.desktop-runtime) #root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix main layout container */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Fix header positioning */
|
||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
||||
position: fixed;
|
||||
top: var(--oc-visual-viewport-offset-top, 0px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Fix main content area */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Fix chat container */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Ensure proper flex behavior */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Mobile sidebar positioning */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
|
||||
top: var(--header-height, 3rem);
|
||||
height: calc(100vh - var(--header-height, 3rem));
|
||||
}
|
||||
|
||||
/* Set header height variable */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3rem;
|
||||
}
|
||||
|
||||
/* For mobile devices with larger header */
|
||||
@media (max-width: 768px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* For tablet devices */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS PWA Adaptations */
|
||||
|
||||
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
|
||||
@media (display-mode: standalone) {
|
||||
/* iOS-specific safe area handling with CSS variable support */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
/* Safe area handling for fixed positioned elements */
|
||||
:root {
|
||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||
}
|
||||
|
||||
.header-safe-area {
|
||||
padding-top: var(--oc-safe-area-top);
|
||||
}
|
||||
|
||||
.main-content-safe-area {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: var(--oc-safe-area-left);
|
||||
padding-right: var(--oc-safe-area-right);
|
||||
}
|
||||
|
||||
/* Safe area for bottom fixed/bottom-0 elements */
|
||||
.bottom-safe-area {
|
||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||
}
|
||||
|
||||
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
||||
.ios-keyboard-safe-area {
|
||||
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
||||
}
|
||||
|
||||
/* Prevent keyboard-induced page scroll on iOS PWA */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix iOS viewport issues */
|
||||
.flex.flex-col.h-screen {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
/* Prevent content overlap in iOS */
|
||||
.flex-1.overflow-hidden {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Update header height for iOS */
|
||||
.header-safe-area {
|
||||
--header-height: 4.5rem;
|
||||
}
|
||||
|
||||
.mobile-sidebar-top {
|
||||
--header-height: 4.5rem;
|
||||
}
|
||||
|
||||
/* Mobile typography improvements */
|
||||
.typography-markdown {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.typography-code {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-ui-header {
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
.typography-ui-label {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-meta {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-micro {
|
||||
font-size: 0.8125rem !important;
|
||||
}
|
||||
|
||||
/* Fix font size for tool displays */
|
||||
[class*="tool"], [class*="code"] {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for non-iOS standalone mode */
|
||||
@supports not (-webkit-touch-callout: none) {
|
||||
:root {
|
||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||
}
|
||||
|
||||
.header-safe-area {
|
||||
padding-top: var(--oc-safe-area-top);
|
||||
}
|
||||
|
||||
.main-content-safe-area {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: var(--oc-safe-area-left);
|
||||
padding-right: var(--oc-safe-area-right);
|
||||
}
|
||||
|
||||
.bottom-safe-area {
|
||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 6: Home indicator blur gradient fix */
|
||||
@media (display-mode: standalone) and (max-width: 768px) {
|
||||
.pwa-dialog-content {
|
||||
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
|
||||
top: calc(50% + (var(--oc-safe-area-top, 0px) * 0.22));
|
||||
max-height: calc(100vh - var(--oc-safe-area-top, 0px) - var(--oc-safe-area-bottom-visual, 0px) - 20px);
|
||||
padding: var(--pwa-dialog-padding);
|
||||
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||
--tw-translate-y: calc(-50% + (var(--oc-safe-area-top, 0px) * 0.6));
|
||||
row-gap: clamp(0.75rem, 2vw, 1.25rem);
|
||||
}
|
||||
|
||||
.pwa-dialog-content.pwa-compact {
|
||||
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
|
||||
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
|
||||
}
|
||||
|
||||
.pwa-dialog-content [data-slot="dialog-close"] {
|
||||
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||
right: var(--pwa-dialog-padding);
|
||||
}
|
||||
|
||||
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
|
||||
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
|
||||
}
|
||||
|
||||
.pwa-dialog-content .settings-page-body {
|
||||
max-width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .directory-dialog-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .directory-dialog-body .directory-grid {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.pwa-overlay-panel {
|
||||
margin-bottom: max(var(--oc-safe-area-bottom-visual, 0px), 12px);
|
||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) * 0.4);
|
||||
}
|
||||
|
||||
.pwa-overlay-panel .pwa-overlay-scroll {
|
||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (display-mode: standalone) {
|
||||
/* Home indicator blur overlay */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(from var(--background) r g b / 0.8) 60%,
|
||||
rgba(from var(--background) r g b / 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fallback for browsers without rgba(from ...) support */
|
||||
@supports not (color: rgba(from white r g b / 0.5)) {
|
||||
body::after {
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
|
||||
rgba(21, 19, 19, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Light theme fallback */
|
||||
:root body::after {
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
|
||||
rgba(248, 247, 243, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Streamdown Styles
|
||||
Minimal overrides - fonts only, using Streamdown defaults
|
||||
@@ -1565,3 +627,18 @@ html:not(.dark) .chat-scroll {
|
||||
.animate-grid-pulse {
|
||||
animation: grid-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee-scroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
.marquee-text {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group:hover .marquee-text,
|
||||
.marquee-text:hover {
|
||||
animation: marquee-scroll 5s linear infinite alternate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
@layer base {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--oc-safe-area-top: 0px;
|
||||
--oc-safe-area-right: 0px;
|
||||
--oc-safe-area-bottom: 0px;
|
||||
--oc-safe-area-bottom-visual: 0px;
|
||||
--oc-safe-area-left: 0px;
|
||||
--oc-header-height: 56px;
|
||||
--oc-visual-viewport-offset-top: 0px;
|
||||
--oc-keyboard-inset: 0px;
|
||||
--oc-keyboard-avoid-offset: 0px;
|
||||
--oc-keyboard-home-indicator: 0px;
|
||||
--ui-regular-font-weight: 400;
|
||||
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
||||
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
||||
--padding-scale: 1;
|
||||
|
||||
/* Semantic typography defaults (must match SEMANTIC_TYPOGRAPHY) */
|
||||
--text-markdown: 0.9375rem;
|
||||
--text-code: 0.9063rem;
|
||||
--text-ui-header: 0.9375rem;
|
||||
--text-ui-label: 0.8750rem;
|
||||
--text-meta: 0.875rem;
|
||||
--text-micro: 0.875rem;
|
||||
|
||||
/* Default Light Theme */
|
||||
--background: oklch(0.97 0.02 85); /* Warm sand background */
|
||||
--foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--card: oklch(0.99 0.01 90); /* Bright sand card */
|
||||
--card-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--popover: oklch(0.99 0.01 90); /* Bright sand popover */
|
||||
--popover-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--primary: oklch(0.65 0.2 55); /* Primary accent - orange */
|
||||
--primary-foreground: oklch(0.99 0.01 90); /* Light text on primary */
|
||||
--secondary: oklch(0.92 0.02 80); /* Light sand */
|
||||
--secondary-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--muted: oklch(0.9 0.015 75); /* Muted sand */
|
||||
--muted-foreground: oklch(0.45 0.02 50); /* Medium warm text */
|
||||
--accent: oklch(0.92 0.02 80); /* Light sand accent */
|
||||
--accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--destructive: oklch(0.55 0.25 25); /* Warm red */
|
||||
--destructive-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||
--border: oklch(0.85 0.02 70); /* Warm border */
|
||||
--input: oklch(0.88 0.02 75); /* Input background */
|
||||
--ring: oklch(0.65 0.2 55); /* Focus ring color */
|
||||
--radius: 0.625rem;
|
||||
--chart-1: oklch(0.58 0.15 230); /* Blue */
|
||||
--chart-2: oklch(0.58 0.15 145); /* Oasis green */
|
||||
--chart-3: oklch(0.65 0.2 55); /* Orange */
|
||||
--chart-4: oklch(0.55 0.18 30); /* Rose */
|
||||
--chart-5: oklch(0.6 0.16 85); /* Golden sand */
|
||||
--sidebar: oklch(0.95 0.02 80); /* Slightly darker sand */
|
||||
--sidebar-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--sidebar-primary: oklch(0.65 0.2 55); /* Primary accent */
|
||||
--sidebar-primary-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||
--sidebar-accent: oklch(0.9 0.02 75); /* Light sand accent */
|
||||
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
||||
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
||||
--sidebar-stuck-bg: #DDDDD3; /* Desktop sidebar sticky header background */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Default Dark Theme - using OKLCH color space */
|
||||
--background: oklch(0.16 0.01 30); /* #151313 */
|
||||
--foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--card: oklch(0.19 0.01 40); /* #1C1B1A */
|
||||
--card-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--popover: oklch(0.24 0.01 40); /* #282726 */
|
||||
--popover-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--primary: oklch(0.77 0.17 85); /* #edb449 - golden sand */
|
||||
--primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||
--secondary: oklch(0.29 0.01 40); /* #343331 */
|
||||
--secondary-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--muted: oklch(0.33 0.01 40); /* #403E3C */
|
||||
--muted-foreground: oklch(0.75 0.02 80); /* #b6b4ab */
|
||||
--accent: oklch(0.29 0.01 40); /* #343331 */
|
||||
--accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--destructive: oklch(0.65 0.15 30); /* #d98678 */
|
||||
--destructive-foreground: oklch(0.9 0.02 80); /* #e1dcd0 */
|
||||
--border: oklch(0.31 0.01 35); /* #393836 */
|
||||
--input: oklch(0.33 0.01 40); /* #403E3C */
|
||||
--ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--chart-1: oklch(0.68 0.12 230); /* #5aa9d9 - blue */
|
||||
--chart-2: oklch(0.68 0.12 145); /* #81af6c - green */
|
||||
--chart-3: oklch(0.7 0.13 95); /* #c2974d - yellow */
|
||||
--chart-4: oklch(0.65 0.14 45); /* #d8886d - coral */
|
||||
--chart-5: oklch(0.68 0.12 55); /* #d29470 - orange */
|
||||
--sidebar: oklch(0.16 0.01 30); /* #151313 */
|
||||
--sidebar-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--sidebar-primary: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--sidebar-primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||
--sidebar-accent: oklch(0.24 0.01 40); /* #282726 */
|
||||
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
||||
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||
--sidebar-stuck-bg: #1F1F1D; /* Desktop sidebar sticky header background */
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
:root * {
|
||||
border-color: rgba(194, 151, 77, 0.2); /* Warm golden sand borders for light mode */
|
||||
}
|
||||
|
||||
.dark * {
|
||||
border-color: rgba(57, 56, 54, 0.5); /* #393836 at 50% opacity */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border outline-none;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
||||
font-weight: var(--ui-regular-font-weight, 400);
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
||||
}
|
||||
|
||||
.font-mono,
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, 'Liberation Mono', Menlo, monospace) !important;
|
||||
}
|
||||
|
||||
/* Remove focus rings globally */
|
||||
*:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
/* Override Tailwind spacing scale with custom properties that respect --padding-scale */
|
||||
--spacing-0: 0;
|
||||
--spacing-0\.5: calc(0.125rem * var(--padding-scale, 1));
|
||||
--spacing-1: calc(0.25rem * var(--padding-scale, 1));
|
||||
--spacing-1\.5: calc(0.375rem * var(--padding-scale, 1));
|
||||
--spacing-2: calc(0.5rem * var(--padding-scale, 1));
|
||||
--spacing-2\.5: calc(0.625rem * var(--padding-scale, 1));
|
||||
--spacing-3: calc(0.75rem * var(--padding-scale, 1));
|
||||
--spacing-3\.5: calc(0.875rem * var(--padding-scale, 1));
|
||||
--spacing-4: calc(1rem * var(--padding-scale, 1));
|
||||
--spacing-5: calc(1.25rem * var(--padding-scale, 1));
|
||||
--spacing-6: calc(1.5rem * var(--padding-scale, 1));
|
||||
--spacing-7: calc(1.75rem * var(--padding-scale, 1));
|
||||
--spacing-8: calc(2rem * var(--padding-scale, 1));
|
||||
--spacing-9: calc(2.25rem * var(--padding-scale, 1));
|
||||
--spacing-10: calc(2.5rem * var(--padding-scale, 1));
|
||||
--spacing-11: calc(2.75rem * var(--padding-scale, 1));
|
||||
--spacing-12: calc(3rem * var(--padding-scale, 1));
|
||||
--spacing-14: calc(3.5rem * var(--padding-scale, 1));
|
||||
--spacing-16: calc(4rem * var(--padding-scale, 1));
|
||||
--spacing-20: calc(5rem * var(--padding-scale, 1));
|
||||
--spacing-24: calc(6rem * var(--padding-scale, 1));
|
||||
--spacing-28: calc(7rem * var(--padding-scale, 1));
|
||||
--spacing-32: calc(8rem * var(--padding-scale, 1));
|
||||
--spacing-36: calc(9rem * var(--padding-scale, 1));
|
||||
--spacing-40: calc(10rem * var(--padding-scale, 1));
|
||||
--spacing-44: calc(11rem * var(--padding-scale, 1));
|
||||
--spacing-48: calc(12rem * var(--padding-scale, 1));
|
||||
--spacing-52: calc(13rem * var(--padding-scale, 1));
|
||||
--spacing-56: calc(14rem * var(--padding-scale, 1));
|
||||
--spacing-60: calc(15rem * var(--padding-scale, 1));
|
||||
--spacing-64: calc(16rem * var(--padding-scale, 1));
|
||||
--spacing-72: calc(18rem * var(--padding-scale, 1));
|
||||
--spacing-80: calc(20rem * var(--padding-scale, 1));
|
||||
--spacing-96: calc(24rem * var(--padding-scale, 1));
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-status-success: var(--status-success);
|
||||
--color-status-warning: var(--status-warning);
|
||||
--color-status-error: var(--status-error);
|
||||
--color-status-info: var(--status-info);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* Markdown typography variables */
|
||||
--markdown-body-font-size: var(--text-markdown);
|
||||
--markdown-body-line-height: var(--markdown-body-line-height);
|
||||
--markdown-h1-font-size: var(--text-markdown);
|
||||
--markdown-h1-line-height: var(--markdown-h1-line-height);
|
||||
--markdown-h2-font-size: var(--text-markdown);
|
||||
--markdown-h2-line-height: var(--markdown-h2-line-height);
|
||||
--markdown-h3-font-size: var(--text-markdown);
|
||||
--markdown-h3-line-height: var(--markdown-h3-line-height);
|
||||
--markdown-h4-font-size: var(--text-markdown);
|
||||
--markdown-h4-line-height: var(--markdown-h4-line-height);
|
||||
--markdown-h5-font-size: var(--text-markdown);
|
||||
--markdown-h5-line-height: var(--markdown-h5-line-height);
|
||||
--markdown-h6-font-size: var(--text-markdown);
|
||||
--markdown-h6-line-height: var(--markdown-h6-line-height);
|
||||
--markdown-list-font-size: var(--text-markdown);
|
||||
--markdown-list-line-height: var(--markdown-list-line-height);
|
||||
--markdown-code-block-font-size: var(--text-code);
|
||||
--markdown-code-block-line-height: var(--markdown-code-block-line-height);
|
||||
|
||||
/* Markdown color variables */
|
||||
--markdown-heading1: var(--markdown-heading1);
|
||||
--markdown-heading2: var(--markdown-heading2);
|
||||
--markdown-heading3: var(--markdown-heading3);
|
||||
--markdown-heading4: var(--markdown-heading4);
|
||||
--markdown-link: var(--markdown-link);
|
||||
--markdown-link-hover: var(--markdown-link-hover);
|
||||
--markdown-list-marker: var(--markdown-list-marker);
|
||||
--markdown-inline-code: var(--markdown-inline-code);
|
||||
--markdown-inline-code-bg: var(--markdown-inline-code-bg);
|
||||
--markdown-blockquote: var(--markdown-blockquote);
|
||||
--markdown-blockquote-border: var(--markdown-blockquote-border);
|
||||
|
||||
/* Markdown spacing defaults */
|
||||
--markdown-paragraph-spacing: 0.35rem;
|
||||
--markdown-heading-primary-top: 0.75rem;
|
||||
--markdown-heading-primary-bottom: 0.35rem;
|
||||
--markdown-heading-secondary-top: 0.6rem;
|
||||
--markdown-heading-secondary-bottom: 0.3rem;
|
||||
--markdown-blockquote-spacing: 0.6rem;
|
||||
--markdown-blockquote-padding: 0.75rem;
|
||||
--markdown-divider-spacing: 0.85rem;
|
||||
--markdown-table-spacing: 0.9rem;
|
||||
--markdown-heading1-size: var(--text-markdown);
|
||||
--markdown-heading2-size: var(--text-markdown);
|
||||
--markdown-heading3-size: var(--text-markdown);
|
||||
--markdown-heading4-size: var(--text-markdown);
|
||||
--markdown-heading5-size: var(--text-markdown);
|
||||
--markdown-heading6-size: var(--text-markdown);
|
||||
--font-sans: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||
--font-heading: var(--font-sans);
|
||||
--font-mono: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||
--font-family-sans: var(--font-sans);
|
||||
--font-family-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Agent color system using theme variables */
|
||||
@layer components {
|
||||
/* Agent color variants - each maps to theme colors */
|
||||
.agent-primary {
|
||||
--agent-color: var(--primary);
|
||||
--agent-color-bg: var(--primary);
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
--agent-color: var(--status-info);
|
||||
--agent-color-bg: var(--status-info);
|
||||
}
|
||||
|
||||
.agent-keyword {
|
||||
--agent-color: var(--syntax-keyword);
|
||||
--agent-color-bg: var(--syntax-keyword);
|
||||
}
|
||||
|
||||
.agent-success {
|
||||
--agent-color: var(--status-success);
|
||||
--agent-color-bg: var(--status-success);
|
||||
}
|
||||
|
||||
.agent-function {
|
||||
--agent-color: var(--syntax-function);
|
||||
--agent-color-bg: var(--syntax-function);
|
||||
}
|
||||
|
||||
.agent-number {
|
||||
--agent-color: var(--syntax-number);
|
||||
--agent-color-bg: var(--syntax-number);
|
||||
}
|
||||
|
||||
.agent-type {
|
||||
--agent-color: var(--syntax-type);
|
||||
--agent-color-bg: var(--syntax-type);
|
||||
}
|
||||
|
||||
.agent-warning {
|
||||
--agent-color: var(--status-warning);
|
||||
--agent-color-bg: var(--status-warning);
|
||||
}
|
||||
|
||||
.agent-variable {
|
||||
--agent-color: var(--syntax-variable);
|
||||
--agent-color-bg: var(--syntax-variable);
|
||||
}
|
||||
|
||||
/* Apply colors with opacity for badges */
|
||||
.agent-badge {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||
border: 1px solid rgb(from var(--agent-color) r g b / 0.2);
|
||||
color: var(--agent-color);
|
||||
}
|
||||
|
||||
.agent-badge:hover {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.15);
|
||||
border-color: rgb(from var(--agent-color) r g b / 0.3);
|
||||
}
|
||||
|
||||
/* Agent dot indicator */
|
||||
.agent-dot {
|
||||
background: var(--agent-color);
|
||||
}
|
||||
|
||||
/* Agent list item */
|
||||
.agent-list-item:hover {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.05);
|
||||
}
|
||||
|
||||
.agent-list-item.active {
|
||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||
border-color: rgb(from var(--agent-color) r g b / 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
/* Mobile Adaptations - All Devices */
|
||||
|
||||
/* Set global device type variables */
|
||||
:root {
|
||||
--is-mobile: 0;
|
||||
--device-type: 'desktop';
|
||||
--font-scale: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) {
|
||||
--is-mobile: 1;
|
||||
--device-type: 'mobile';
|
||||
--font-scale: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for device-specific styling */
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root.desktop-runtime .mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* General mobile improvements for all mobile devices */
|
||||
@media (max-width: 1024px) {
|
||||
/* Override CSS custom properties for mobile */
|
||||
:root.mobile-pointer:not(.desktop-runtime) {
|
||||
--text-markdown: 1rem;
|
||||
--text-code: 0.875rem;
|
||||
--text-ui-header: 0.9375rem;
|
||||
--text-ui-label: 0.875rem;
|
||||
--text-meta: 0.875rem;
|
||||
--text-micro: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Force override with higher specificity */
|
||||
:root.mobile-pointer:not(.desktop-runtime) * {
|
||||
--text-markdown: 1rem !important;
|
||||
--text-code: 0.875rem !important;
|
||||
--text-ui-header: 0.9375rem !important;
|
||||
--text-ui-label: 0.875rem !important;
|
||||
--text-meta: 0.875rem !important;
|
||||
--text-micro: 0.8125rem !important;
|
||||
}
|
||||
|
||||
/* Additional override for elements using typography classes */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
|
||||
font-size: unset !important;
|
||||
}
|
||||
|
||||
/* Fix font size for tool displays */
|
||||
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
|
||||
--text-code: 0.875rem !important;
|
||||
font-size: var(--text-code) !important;
|
||||
}
|
||||
|
||||
/* Improve touch targets for mobile */
|
||||
:root.mobile-pointer:not(.desktop-runtime) button,
|
||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
/* Improve input field touch targets */
|
||||
:root.mobile-pointer:not(.desktop-runtime) input,
|
||||
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
||||
:root.mobile-pointer:not(.desktop-runtime) select {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* Fix mobile text inputs */
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
|
||||
:root.mobile-pointer:not(.desktop-runtime) textarea {
|
||||
font-size: 16px !important; /* Prevents iOS zoom */
|
||||
}
|
||||
|
||||
/* Prevent keyboard on non-input elements */
|
||||
:root.mobile-pointer:not(.desktop-runtime)
|
||||
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Specific fix for session buttons */
|
||||
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Improve mobile spacing */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
|
||||
padding-top: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Fix mobile scroll containers */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Fix mobile viewport height */
|
||||
:root.mobile-pointer:not(.desktop-runtime),
|
||||
:root.mobile-pointer:not(.desktop-runtime) body {
|
||||
height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prevent iOS keyboard from scrolling the page */
|
||||
:root.device-mobile:not(.desktop-runtime) body,
|
||||
:root.device-mobile:not(.desktop-runtime) #root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix main layout container */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Fix header positioning */
|
||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
||||
position: fixed;
|
||||
top: var(--oc-visual-viewport-offset-top, 0px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Fix main content area */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Fix chat container */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Ensure proper flex behavior */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Mobile sidebar positioning */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
|
||||
top: var(--header-height, 3rem);
|
||||
height: calc(100vh - var(--header-height, 3rem));
|
||||
}
|
||||
|
||||
/* Set header height variable */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3rem;
|
||||
}
|
||||
|
||||
/* For mobile devices with larger header */
|
||||
@media (max-width: 768px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* For tablet devices */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||
--header-height: 3.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS PWA Adaptations */
|
||||
|
||||
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
|
||||
@media (display-mode: standalone) {
|
||||
/* iOS-specific safe area handling with CSS variable support */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
/* Safe area handling for fixed positioned elements */
|
||||
:root {
|
||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||
}
|
||||
|
||||
.header-safe-area {
|
||||
padding-top: var(--oc-safe-area-top);
|
||||
}
|
||||
|
||||
.main-content-safe-area {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: var(--oc-safe-area-left);
|
||||
padding-right: var(--oc-safe-area-right);
|
||||
}
|
||||
|
||||
/* Safe area for bottom fixed/bottom-0 elements */
|
||||
.bottom-safe-area {
|
||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||
}
|
||||
|
||||
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
||||
.ios-keyboard-safe-area {
|
||||
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
||||
}
|
||||
|
||||
/* Prevent keyboard-induced page scroll on iOS PWA */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix iOS viewport issues */
|
||||
.flex.flex-col.h-screen {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
/* Prevent content overlap in iOS */
|
||||
.flex-1.overflow-hidden {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Update header height for iOS */
|
||||
.header-safe-area {
|
||||
--header-height: 4.5rem;
|
||||
}
|
||||
|
||||
.mobile-sidebar-top {
|
||||
--header-height: 4.5rem;
|
||||
}
|
||||
|
||||
/* Mobile typography improvements */
|
||||
.typography-markdown {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.typography-code {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-ui-header {
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
.typography-ui-label {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-meta {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.typography-micro {
|
||||
font-size: 0.8125rem !important;
|
||||
}
|
||||
|
||||
/* Fix font size for tool displays */
|
||||
[class*="tool"], [class*="code"] {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for non-iOS standalone mode */
|
||||
@supports not (-webkit-touch-callout: none) {
|
||||
:root {
|
||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||
}
|
||||
|
||||
.header-safe-area {
|
||||
padding-top: var(--oc-safe-area-top);
|
||||
}
|
||||
|
||||
.main-content-safe-area {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: var(--oc-safe-area-left);
|
||||
padding-right: var(--oc-safe-area-right);
|
||||
}
|
||||
|
||||
.bottom-safe-area {
|
||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 6: Home indicator blur gradient fix */
|
||||
@media (display-mode: standalone) and (max-width: 768px) {
|
||||
.pwa-dialog-content {
|
||||
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
|
||||
top: calc(50% + (var(--oc-safe-area-top, 0px) * 0.22));
|
||||
max-height: calc(100vh - var(--oc-safe-area-top, 0px) - var(--oc-safe-area-bottom-visual, 0px) - 20px);
|
||||
padding: var(--pwa-dialog-padding);
|
||||
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||
--tw-translate-y: calc(-50% + (var(--oc-safe-area-top, 0px) * 0.6));
|
||||
row-gap: clamp(0.75rem, 2vw, 1.25rem);
|
||||
}
|
||||
|
||||
.pwa-dialog-content.pwa-compact {
|
||||
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
|
||||
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
|
||||
}
|
||||
|
||||
.pwa-dialog-content [data-slot="dialog-close"] {
|
||||
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||
right: var(--pwa-dialog-padding);
|
||||
}
|
||||
|
||||
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
|
||||
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
|
||||
}
|
||||
|
||||
.pwa-dialog-content .settings-page-body {
|
||||
max-width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .directory-dialog-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.pwa-dialog-content .directory-dialog-body .directory-grid {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.pwa-overlay-panel {
|
||||
margin-bottom: max(var(--oc-safe-area-bottom-visual, 0px), 12px);
|
||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) * 0.4);
|
||||
}
|
||||
|
||||
.pwa-overlay-panel .pwa-overlay-scroll {
|
||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (display-mode: standalone) {
|
||||
/* Home indicator blur overlay */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(from var(--background) r g b / 0.8) 60%,
|
||||
rgba(from var(--background) r g b / 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fallback for browsers without rgba(from ...) support */
|
||||
@supports not (color: rgba(from white r g b / 0.5)) {
|
||||
body::after {
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
|
||||
rgba(21, 19, 19, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Light theme fallback */
|
||||
:root body::after {
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background) 0%,
|
||||
var(--background) 30%,
|
||||
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
|
||||
rgba(248, 247, 243, 0.3) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
@layer components {
|
||||
/* Semantic typography classes - font-size only */
|
||||
.typography-markdown {
|
||||
font-size: var(--text-markdown);
|
||||
}
|
||||
|
||||
.typography-code {
|
||||
font-size: var(--text-code);
|
||||
}
|
||||
|
||||
.typography-ui-header {
|
||||
font-size: var(--text-ui-header);
|
||||
}
|
||||
|
||||
.typography-ui-label {
|
||||
font-size: var(--text-ui-label);
|
||||
}
|
||||
|
||||
.typography-meta {
|
||||
font-size: var(--text-meta);
|
||||
}
|
||||
|
||||
.typography-micro {
|
||||
font-size: var(--text-micro);
|
||||
}
|
||||
|
||||
.chat-column {
|
||||
width: min(100%, 56rem);
|
||||
margin-inline: auto;
|
||||
padding-inline: calc(clamp(1rem, 3vw, 1.5rem) * var(--padding-scale, 1));
|
||||
}
|
||||
|
||||
.chat-message-column {
|
||||
width: min(100%, 56rem);
|
||||
margin-inline: auto;
|
||||
padding-inline: calc(clamp(0.75rem, 2.5vw, 1rem) * var(--padding-scale, 1));
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.chat-column {
|
||||
padding-inline: calc(clamp(1.25rem, 2.5vw, 2rem) * var(--padding-scale, 1));
|
||||
}
|
||||
|
||||
.chat-message-column {
|
||||
padding-inline: calc(clamp(1rem, 2.5vw, 1.5rem) * var(--padding-scale, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced focus indicators for small interactive elements */
|
||||
.model-favorite-button {
|
||||
position: relative;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.model-favorite-button:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.model-favorite-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Heading typography - all use markdown size, differentiated by weight/color */
|
||||
|
||||
.typography-h1 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h1-line-height);
|
||||
letter-spacing: var(--h1-letter-spacing);
|
||||
font-weight: var(--h1-font-weight);
|
||||
}
|
||||
|
||||
.typography-h2 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h2-line-height);
|
||||
letter-spacing: var(--h2-letter-spacing);
|
||||
font-weight: var(--h2-font-weight);
|
||||
}
|
||||
|
||||
.typography-h3 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--h3-line-height);
|
||||
letter-spacing: var(--h3-letter-spacing);
|
||||
font-weight: var(--h3-font-weight);
|
||||
}
|
||||
|
||||
/* UI element typography - mapped to semantic variables */
|
||||
.typography-ui-button {
|
||||
font-size: var(--text-ui-label);
|
||||
line-height: var(--ui-button-line-height);
|
||||
letter-spacing: var(--ui-button-letter-spacing);
|
||||
font-weight: var(--ui-button-font-weight);
|
||||
}
|
||||
|
||||
.typography-ui-caption {
|
||||
font-size: var(--text-micro);
|
||||
line-height: var(--ui-caption-line-height);
|
||||
letter-spacing: var(--ui-caption-letter-spacing);
|
||||
font-weight: var(--ui-caption-font-weight);
|
||||
}
|
||||
|
||||
/* Markdown typography - all use semantic markdown size */
|
||||
.typography-markdown-body {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-body-line-height);
|
||||
letter-spacing: var(--markdown-body-letter-spacing);
|
||||
font-weight: var(--markdown-body-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h1 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h1-line-height);
|
||||
letter-spacing: var(--markdown-h1-letter-spacing);
|
||||
font-weight: var(--markdown-h1-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h2 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h2-line-height);
|
||||
letter-spacing: var(--markdown-h2-letter-spacing);
|
||||
font-weight: var(--markdown-h2-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h3 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h3-line-height);
|
||||
letter-spacing: var(--markdown-h3-letter-spacing);
|
||||
font-weight: var(--markdown-h3-font-weight);
|
||||
}
|
||||
|
||||
.typography-markdown-h4 {
|
||||
font-size: var(--text-markdown);
|
||||
line-height: var(--markdown-h4-line-height);
|
||||
letter-spacing: var(--markdown-h4-letter-spacing);
|
||||
font-weight: var(--markdown-h4-font-weight);
|
||||
}
|
||||
}
|
||||
@@ -686,6 +686,45 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:fs:delete': {
|
||||
const targetPath = (payload as { path: string })?.path;
|
||||
if (!targetPath) {
|
||||
return { id, type, success: false, error: 'Path is required' };
|
||||
}
|
||||
try {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
|
||||
const resolvedPath = resolveUserPath(targetPath, workspaceRoot);
|
||||
const uri = vscode.Uri.file(resolvedPath);
|
||||
await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false });
|
||||
return { id, type, success: true, data: { success: true } };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete file';
|
||||
return { id, type, success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:fs:rename': {
|
||||
const { oldPath, newPath } = (payload as { oldPath: string; newPath: string }) || {};
|
||||
if (!oldPath) {
|
||||
return { id, type, success: false, error: 'oldPath is required' };
|
||||
}
|
||||
if (!newPath) {
|
||||
return { id, type, success: false, error: 'newPath is required' };
|
||||
}
|
||||
try {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
|
||||
const resolvedOld = resolveUserPath(oldPath, workspaceRoot);
|
||||
const resolvedNew = resolveUserPath(newPath, workspaceRoot);
|
||||
const oldUri = vscode.Uri.file(resolvedOld);
|
||||
const newUri = vscode.Uri.file(resolvedNew);
|
||||
await vscode.workspace.fs.rename(oldUri, newUri, { overwrite: false });
|
||||
return { id, type, success: true, data: { success: true, path: normalizeFsPath(resolvedNew) } };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to rename file';
|
||||
return { id, type, success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:fs:exec': {
|
||||
const { commands, cwd } = (payload as { commands: string[]; cwd: string }) || {};
|
||||
if (!Array.isArray(commands) || commands.length === 0) {
|
||||
|
||||
@@ -61,6 +61,20 @@ export const createVSCodeFilesAPI = (): FilesAPI => ({
|
||||
};
|
||||
},
|
||||
|
||||
async delete(path: string): Promise<{ success: boolean }> {
|
||||
const target = normalizePath(path);
|
||||
const data = await sendBridgeMessage<{ success: boolean }>('api:fs:delete', { path: target });
|
||||
return { success: Boolean(data?.success) };
|
||||
},
|
||||
|
||||
async rename(oldPath: string, newPath: string): Promise<{ success: boolean; path: string }> {
|
||||
const data = await sendBridgeMessage<{ success: boolean; path: string }>('api:fs:rename', { oldPath, newPath });
|
||||
return {
|
||||
success: Boolean(data?.success),
|
||||
path: typeof data?.path === 'string' ? normalizePath(data.path) : newPath,
|
||||
};
|
||||
},
|
||||
|
||||
async readFile(path: string): Promise<{ content: string; path: string }> {
|
||||
const target = normalizePath(path);
|
||||
const data = await sendBridgeMessage<{ content: string; path: string }>('api:fs:read', { path: target });
|
||||
|
||||
Reference in New Issue
Block a user