Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"scripts": {
"lint": "pnpm run local-web:lint && pnpm run ui:lint && pnpm run backend:lint && node scripts/check-unused-i18n-keys.mjs",
"format": "pnpm run backend:format && pnpm run web-core:format && pnpm run local-web:format && pnpm run remote-web:format",
"format": "pnpm run backend:format && pnpm run web-core:format && pnpm run local-web:format && pnpm run remote-web:format && pnpm run ui:format",
"check": "pnpm run local-web:legacy-path-guard && pnpm run local-web:check && pnpm run remote-web:check && pnpm run web-core:check && pnpm run ui:check && pnpm run backend:check",
"dev": "export FRONTEND_PORT=$(node scripts/setup-dev-environment.js frontend) && export BACKEND_PORT=$(node scripts/setup-dev-environment.js backend) && export PREVIEW_PROXY_PORT=$(node scripts/setup-dev-environment.js preview_proxy) && export VK_ALLOWED_ORIGINS=\"http://localhost:${FRONTEND_PORT}\" && export VITE_VK_SHARED_API_BASE=${VK_SHARED_API_BASE:-} && concurrently \"pnpm run backend:dev:watch\" \"pnpm run local-web:dev\"",
"test:npm": "./test-npm-package.sh",
Expand All @@ -26,6 +26,7 @@
"remote-web:format": "pnpm --filter @vibe/remote-web run format",
"ui:lint": "pnpm --filter @vibe/ui run lint",
"ui:check": "pnpm --filter @vibe/ui run check",
"ui:format": "pnpm --filter @vibe/ui run format",
"backend:lint": "cargo clippy --workspace --all-targets --features qa-mode -- -D warnings && cargo clippy --manifest-path crates/remote/Cargo.toml --all-targets -- -D warnings",
"backend:format": "cargo fmt --all && cargo fmt --all --manifest-path crates/remote/Cargo.toml",
"backend:dev": "BACKEND_PORT=$(node scripts/setup-dev-environment.js backend) pnpm run backend:dev:watch",
Expand Down
12 changes: 5 additions & 7 deletions packages/remote-web/src/app/layout/RemoteNavbarContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { useCallback, useEffect, useMemo, type ReactNode } from "react";
import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
import {
MOBILE_TABS,
Navbar,
type MobileTabId,
} from "@vibe/ui/components/Navbar";
import { MOBILE_TABS, Navbar } from "@vibe/ui/components/Navbar";
import { SettingsDialog } from "@/shared/dialogs/settings/SettingsDialog";
import { CommandBarDialog } from "@/shared/dialogs/command-bar/CommandBarDialog";
import { useMobileActiveTab } from "@/shared/stores/useUiPreferencesStore";
Expand All @@ -27,7 +23,9 @@ export function RemoteNavbarContainer({

const remoteMobileTabs = useMemo(
() =>
MOBILE_TABS.filter((t) => t.id !== "preview" && t.id !== "workspaces"),
MOBILE_TABS.filter(
(t) => t.id !== "preview" && t.id !== "workspaces" && t.id !== "files",
),
[],
);

Expand Down Expand Up @@ -83,7 +81,7 @@ export function RemoteNavbarContainer({
mobileShowBack={mobileShowBack}
onOpenSettings={handleOpenSettings}
onOpenCommandBar={handleOpenCommandBar}
mobileActiveTab={mobileActiveTab as MobileTabId}
mobileActiveTab={mobileActiveTab}
onMobileTabChange={(tab) => setMobileActiveTab(tab)}
mobileTabs={remoteMobileTabs}
showMobileTabs={isOnWorkspaceView}
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Terminal as TerminalIcon,
Desktop as DesktopIcon,
GitFork as GitForkIcon,
FolderSimple as FolderIcon,
List as ListIcon,
Gear as GearIcon,
CaretLeft as CaretLeftIcon,
Expand Down Expand Up @@ -90,13 +91,17 @@ function NavbarIconButton({
);
}

// Keep in lockstep with MobileTab in
// packages/web-core/src/shared/stores/useUiPreferencesStore.ts — ui can't
// depend on web-core, so these two unions are intentionally duplicated.
export type MobileTabId =
| 'workspaces'
| 'chat'
| 'changes'
| 'logs'
| 'preview'
| 'git';
| 'git'
| 'files';

export const MOBILE_TABS: { id: MobileTabId; icon: Icon; label: string }[] = [
{ id: 'workspaces', icon: LayoutIcon, label: 'Wksps' },
Expand All @@ -105,6 +110,7 @@ export const MOBILE_TABS: { id: MobileTabId; icon: Icon; label: string }[] = [
{ id: 'logs', icon: TerminalIcon, label: 'Logs' },
{ id: 'preview', icon: DesktopIcon, label: 'Preview' },
{ id: 'git', icon: GitForkIcon, label: 'Git' },
{ id: 'files', icon: FolderIcon, label: 'Files' },
];

export interface NavbarBreadcrumbItem {
Expand Down Expand Up @@ -264,6 +270,10 @@ export function Navbar({
<button
key={tab.id}
type="button"
aria-label={
tab.id === 'workspaces' ? 'Wksps (Workspaces)' : tab.label
}
aria-current={isActive ? 'page' : undefined}
className={cn(
'flex items-center gap-1 px-1.5 py-1 text-xs whitespace-nowrap transition-colors',
isActive
Expand All @@ -276,7 +286,7 @@ export function Navbar({
className="size-icon-sm"
weight={isActive ? 'fill' : 'regular'}
/>
<span className="hidden min-[480px]:inline">
<span className={isActive ? 'inline' : 'hidden'}>
{tab.label}
</span>
</button>
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/cn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { type ClassValue, clsx } from 'clsx';

// Intentionally bare clsx (no tailwind-merge) until the Tailwind v4 migration,
// matching web-core's cn. Never combine conflicting utilities in one cn() call;
// pass mutually exclusive values (for example, with a ternary), because cn()
// will not deduplicate or resolve which one wins.
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}
20 changes: 20 additions & 0 deletions packages/web-core/src/pages/workspaces/WorkspacesLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { CreateModeInitialState } from '@/shared/types/createMode';
import { useWorkspaceContext } from '@/shared/hooks/useWorkspaceContext';
import { usePageTitle } from '@/shared/hooks/usePageTitle';
import { useIsMobile } from '@/shared/hooks/useIsMobile';
import { useHostId } from '@/shared/providers/HostIdProvider';
import { useMobileActiveTab } from '@/shared/stores/useUiPreferencesStore';
import { cn } from '@/shared/lib/utils';
import { CreateModeProvider } from '@/features/create-mode/model/CreateModeProvider';
Expand All @@ -30,6 +31,7 @@ import {
type WorkspacesMainContainerHandle,
} from './WorkspacesMainContainer';
import { RightSidebar } from './RightSidebar';
import { WorkspaceFilesContainer } from './WorkspaceFilesContainer';
import { ChangesPanelContainer } from './ChangesPanelContainer';
import { CreateChatBoxContainer } from '@/shared/components/CreateChatBoxContainer';
import { PreviewBrowserContainer } from './PreviewBrowserContainer';
Expand Down Expand Up @@ -120,6 +122,7 @@ export function WorkspacesLayout() {

const isMobile = useIsMobile();
const [mobileTab] = useMobileActiveTab();
const hostId = useHostId();
const mainContainerRef = useRef<WorkspacesMainContainerHandle>(null);

useLayoutEffect(() => {
Expand Down Expand Up @@ -229,6 +232,11 @@ export function WorkspacesLayout() {
);

// ── Mobile layout ──────────────────────────────────────────────────
// These seven panes must stay in lockstep with MobileTabId
// (packages/ui/src/components/Navbar.tsx) and MobileTab
// (packages/web-core/src/shared/stores/useUiPreferencesStore.ts). Adding a
// tab to those unions without a matching pane here compiles cleanly but
// silently renders a dead tab button.
// Uses `hidden` CSS class (NOT conditional rendering) to preserve
// WebSocket connections and scroll positions across tab switches.
if (isMobile) {
Expand Down Expand Up @@ -355,6 +363,18 @@ export function WorkspacesLayout() {
/>
)}
</div>

{/* Files tab */}
<div
className={cn(
'flex-1 min-h-0 overflow-hidden',
mobileTab !== 'files' && 'hidden'
)}
>
{selectedWorkspace?.id && !hostId && (
<WorkspaceFilesContainer workspaceId={selectedWorkspace.id} />
)}
</div>
</div>
</ChangesViewProvider>
</ReviewProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { useSyncErrorContext } from '@/shared/hooks/useSyncErrorContext';
import { useUserOrganizations } from '@/shared/hooks/useUserOrganizations';
import { useOrganizationStore } from '@/shared/stores/useOrganizationStore';
import {
MOBILE_TABS,
Navbar,
type NavbarSectionItem,
type MobileTabId,
type NavbarSectionItem,
} from '@vibe/ui/components/Navbar';
import { Tooltip } from '@vibe/ui/components/Tooltip';
import { AppBarUserPopoverContainer } from './AppBarUserPopoverContainer';
Expand Down Expand Up @@ -37,6 +38,7 @@ import { CommandBarDialog } from '@/shared/dialogs/command-bar/CommandBarDialog'
import { SettingsDialog } from '@/shared/dialogs/settings/SettingsDialog';
import { useAppNavigation } from '@/shared/hooks/useAppNavigation';
import { getRemoteAuthDegradedMessage } from '@/shared/lib/auth/remoteAuthDegraded';
import { useHostId } from '@/shared/providers/HostIdProvider';

/**
* Check if a NavbarItem is a divider
Expand Down Expand Up @@ -117,6 +119,7 @@ export function NavbarContainer({
mobileMode?: boolean;
}) {
const { t } = useTranslation('common');
const hostId = useHostId();
const { executeAction } = useActions();
const { workspace: selectedWorkspace, isCreateMode } = useWorkspaceContext();
const syncErrorContext = useSyncErrorContext();
Expand All @@ -126,6 +129,10 @@ export function NavbarContainer({
const { isSignedIn } = useAuth();
const appNavigation = useAppNavigation();
const [mobileActiveTab, setMobileActiveTab] = useMobileActiveTab();
const mobileTabs = useMemo(
() => (hostId ? MOBILE_TABS.filter((t) => t.id !== 'files') : MOBILE_TABS),
[hostId]
);

const { data: orgsData } = useUserOrganizations();
const organizations = useMemo(
Expand Down Expand Up @@ -327,8 +334,9 @@ export function NavbarContainer({
onOpenCommandBar={handleOpenCommandBar}
onOpenSettings={handleOpenSettings}
onNavigateBack={handleNavigateBack}
mobileActiveTab={mobileActiveTab as MobileTabId}
onMobileTabChange={(tab) => setMobileActiveTab(tab)}
mobileActiveTab={mobileActiveTab}
onMobileTabChange={(tab: MobileTabId) => setMobileActiveTab(tab)}
mobileTabs={mobileTabs}
/>
);
}
16 changes: 11 additions & 5 deletions packages/web-core/src/shared/stores/useUiPreferencesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ export const RIGHT_MAIN_PANEL_MODES = {
PREVIEW: 'preview',
} as const;

// RightMainPanelMode must stay a subset of MobileTab. The assignments below
// intentionally rely on structural typing (no cast), so adding a mode without
// a matching mobile tab fails to compile.
export type RightMainPanelMode =
(typeof RIGHT_MAIN_PANEL_MODES)[keyof typeof RIGHT_MAIN_PANEL_MODES];

export type LayoutMode = 'workspaces' | 'kanban';

// Keep in lockstep with MobileTabId in
// packages/ui/src/components/Navbar.tsx — web-core can't be imported by ui,
// so these two unions are intentionally duplicated.
export type MobileTab =
| 'workspaces'
| 'chat'
| 'changes'
| 'logs'
| 'preview'
| 'git';
| 'git'
| 'files';

export type MobileFontScale = 'default' | 'small' | 'smaller';
export const DEFAULT_CREATE_DRAFT_WORKSPACE_BY_DEFAULT = false;
Expand Down Expand Up @@ -490,7 +497,7 @@ export const useUiPreferencesStore = create<State>()((set, get) => ({
listViewStatusFilter: null,

// Mobile tab state
mobileActiveTab: 'chat' as MobileTab,
mobileActiveTab: 'chat',

// Mobile font scale
mobileFontScale: loadMobileFontScale(),
Expand Down Expand Up @@ -581,8 +588,7 @@ export const useUiPreferencesStore = create<State>()((set, get) => ({
: isWideScreen()
? state.isLeftSidebarVisible
: false,
...(isMobile &&
!isCurrentlyActive && { mobileActiveTab: mode as MobileTab }),
...(isMobile && !isCurrentlyActive && { mobileActiveTab: mode }),
});
},

Expand All @@ -605,7 +611,7 @@ export const useUiPreferencesStore = create<State>()((set, get) => ({
? state.isLeftSidebarVisible
: false,
}),
...(isMobile && mode !== null && { mobileActiveTab: mode as MobileTab }),
...(isMobile && mode !== null && { mobileActiveTab: mode }),
});
},

Expand Down
Loading