feat(ui): improve mobile sidebar and dashboard layout (#67)

* feat(ui): improve mobile sidebar and dashboard layout

- Enlarge compass logo on dashboard page (size-14 idle, size-10 active)
- Reposition logo higher with -mt-16 margin
- Add 6rem spacing between logo and chat
- Remove feedback hover button from bottom right
- Add event-based feedback dialog opening for mobile sidebar
- Remove feedback buttons from site header (mobile and desktop)
- Add mobile theme toggle button to header
- Increase mobile menu hitbox to size-10
- Reduce search hitbox to separate clickable area
- Remove redundant Compass/Get Help/Assistant/Search from sidebar
- Rename "People" to "Team"
- Add mobile-only feedback button to sidebar footer
- Reduce mobile sidebar width to 10rem max-width
- Center sidebar menu icons and labels on mobile
- Clean up mobile-specific padding variants

* chore: add local development setup system

- Create .dev-setup directory with patches and scripts
- Add apply-dev.sh to easily enable local dev without WorkOS
- Add restore-dev.sh to revert to original code
- Document all changes in README.md
- Store cloudflare-context.ts in files/ as new dev-only file
- Support re-apply patches for fresh development sessions

This allows running Compass locally without WorkOS authentication
for development and testing purposes.

---------

Co-authored-by: Avery Felts <averyfelts@Averys-MacBook-Air.local>
This commit is contained in:
aaf2tbz 2026-02-11 12:49:51 -07:00 committed by GitHub
parent 04180d4305
commit 33b427ed33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 296 additions and 92 deletions

2
.dev-setup/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Dev setup patches directory
# Patches and scripts for local development without WorkOS authentication

104
.dev-setup/README.md Normal file
View File

@ -0,0 +1,104 @@
# Local Development Setup
This directory contains patches and scripts to enable local development without WorkOS authentication.
## What This Does
These patches modify Compass to run in development mode without requiring WorkOS SSO authentication:
1. **Bypasses WorkOS auth checks** - Middleware redirects `/` to `/dashboard` when WorkOS isn't configured
2. **Mock D1 database** - Returns empty arrays for queries instead of throwing errors
3. **Wraps Cloudflare context** - Returns mock `{ env: { DB: null }, ctx: {} }` when WorkOS isn't configured
4. **Skips OpenNext initialization** - Only runs when WorkOS API keys are properly set
## How to Use
### Quick Start
From the compass directory:
```bash
.dev-setup/apply-dev.sh
```
This will apply all necessary patches to enable local development.
### Manual Application
If the automated script fails, you can apply patches manually:
1. **middleware.ts** - Redirects root to dashboard, allows all requests without auth
2. **lib/auth.ts** - Checks for "your_" and "placeholder" in WorkOS keys
3. **lib/cloudflare-context.ts** - New file that wraps `getCloudflareContext()`
4. **db/index.ts** - Returns mock DB when `d1` parameter is `null`
5. **next.config.ts** - Only initializes OpenNext Cloudflare when WorkOS is configured
6. **.gitignore** - Ignores `src/lib/cloudflare-context.ts` so it's not committed
### Undoing Changes
To remove dev setup and restore original behavior:
```bash
git restore src/middleware.ts
git restore src/lib/auth.ts
git restore src/db/index.ts
git restore next.config.ts
git restore .gitignore
rm src/lib/cloudflare-context.ts
```
## Environment Variables
To configure WorkOS auth properly (to disable dev mode):
```env
WORKOS_API_KEY=sk_dev_xxxxx
WORKOS_CLIENT_ID=client_xxxxx
WORKOS_REDIRECT_URI=http://localhost:3000
```
With these set, the dev patches will automatically skip and use real WorkOS authentication.
## Dev Mode Indicators
When in dev mode (WorkOS not configured):
- Dashboard loads directly without login redirect
- Database queries return empty arrays instead of errors
- Cloudflare context returns null DB instead of throwing
## Files Created/Modified
### New Files (dev only)
- `src/lib/cloudflare-context.ts` - Wraps `getCloudflareContext()` with dev bypass
### Modified Files
- `src/middleware.ts` - Added WorkOS detection and dev bypass
- `src/lib/auth.ts` - Enhanced WorkOS configuration detection
- `src/db/index.ts` - Added mock DB support for null D1 parameter
- `next.config.ts` - Conditional OpenNext initialization
- `.gitignore` - Added `cloudflare-context.ts` to prevent commits
## Future Development
Place any new dev-only components or patches in this directory following the same pattern:
1. **Patch files** - Store in `patches/` with `.patch` extension
2. **New files** - Store in `files/` directory
3. **Update apply-dev.sh** - Add your patches to the apply script
4. **Document here** - Update this README with changes
## Troubleshooting
**Build errors after applying patches:**
- Check that `src/lib/cloudflare-context.ts` exists
- Verify all patches applied cleanly
- Try manual patch application if automated script fails
**Auth still required:**
- Verify `.env.local` or `.dev.vars` doesn't have placeholder values
- Check that WorkOS environment variables aren't set (if you want dev mode)
- Restart dev server after applying patches
**Database errors:**
- Ensure `src/db/index.ts` patch was applied
- Check that mock DB is being returned when `d1` is null

65
.dev-setup/apply-dev.sh Executable file
View File

@ -0,0 +1,65 @@
#!/bin/bash
set -e
echo "🔧 Applying local development setup patches..."
# Check if we're in the compass directory
if [ ! -f "package.json" ] || [ ! -d "src" ]; then
echo "❌ Error: Please run this script from the compass directory"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Apply middleware patch
echo "📦 Applying middleware.ts patch..."
patch -p1 < "$SCRIPT_DIR/patches/middleware.patch" || {
echo "⚠️ middleware.ts patch failed, applying manually..."
cat "$SCRIPT_DIR/patches/middleware.patch"
}
# Apply auth patch
echo "📦 Applying auth.ts patch..."
patch -p1 < "$SCRIPT_DIR/patches/auth.patch" || {
echo "⚠️ auth.ts patch failed, applying manually..."
cat "$SCRIPT_DIR/patches/auth.patch"
}
# Apply cloudflare-context (create the wrapper file)
echo "📦 Applying cloudflare-context.ts..."
if [ ! -f "src/lib/cloudflare-context.ts" ]; then
mkdir -p src/lib
cp "$SCRIPT_DIR/files/cloudflare-context.ts" src/lib/
echo "✓ Created src/lib/cloudflare-context.ts"
else
echo "⚠️ cloudflare-context.ts already exists, skipping"
fi
# Apply db-index patch
echo "📦 Applying db/index.ts patch..."
patch -p1 < "$SCRIPT_DIR/patches/db-index.patch" || {
echo "⚠️ db/index.ts patch failed, applying manually..."
cat "$SCRIPT_DIR/patches/db-index.patch"
}
# Apply next-config patch
echo "📦 Applying next.config.ts patch..."
patch -p1 < "$SCRIPT_DIR/patches/next-config.patch" || {
echo "⚠️ next.config.ts patch failed, applying manually..."
cat "$SCRIPT_DIR/patches/next-config.patch"
}
# Update .gitignore
echo "📦 Updating .gitignore..."
patch -p1 < "$SCRIPT_DIR/patches/gitignore.patch" || {
echo "⚠️ .gitignore patch failed, applying manually..."
cat "$SCRIPT_DIR/patches/gitignore.patch"
}
echo ""
echo "✅ Development setup complete!"
echo ""
echo "📝 Notes:"
echo " - These changes allow local development without WorkOS authentication"
echo " - To use WorkOS auth, remove these changes or revert the patches"
echo " - Modified files: src/lib/cloudflare-context.ts, src/middleware.ts, src/lib/auth.ts, src/db/index.ts, next.config.ts, .gitignore"

View File

@ -0,0 +1,20 @@
import { getCloudflareContext as originalGetCloudflareContext } from "@opennextjs/cloudflare"
const isWorkOSConfigured =
process.env.WORKOS_API_KEY &&
process.env.WORKOS_CLIENT_ID &&
!process.env.WORKOS_API_KEY.includes("your_") &&
!process.env.WORKOS_API_KEY.includes("placeholder")
export async function getCloudflareContext() {
if (!isWorkOSConfigured) {
return {
env: {
DB: null,
},
ctx: {},
} as Awaited<ReturnType<typeof originalGetCloudflareContext>>
}
return originalGetCloudflareContext()
}

View File

View File

View File

View File

View File

36
.dev-setup/restore-dev.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
set -e
echo "🔄 Removing local development setup patches..."
# Check if we're in the compass directory
if [ ! -f "package.json" ] || [ ! -d "src" ]; then
echo "❌ Error: Please run this script from the compass directory"
exit 1
fi
# Restore modified files
echo "📦 Restoring modified files..."
git restore src/middleware.ts
git restore src/lib/auth.ts
git restore src/db/index.ts
git restore next.config.ts
git restore .gitignore
# Remove dev-only new file
echo "📦 Removing dev-only files..."
if [ -f "src/lib/cloudflare-context.ts" ]; then
rm src/lib/cloudflare-context.ts
echo "✓ Removed src/lib/cloudflare-context.ts"
else
echo "⚠️ src/lib/cloudflare-context.ts not found, skipping"
fi
echo ""
echo "✅ Development setup removed!"
echo ""
echo "📝 Notes:"
echo " - Original code has been restored from git"
echo " - Dev mode is now disabled"
echo " - WorkOS authentication will be required (if configured)"
echo " - To re-apply dev setup, run: .dev-setup/apply-dev.sh"

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ android/build/
android/app/build/
# Local auth bypass (dev only)
src/lib/auth-bypass.ts
src/lib/cloudflare-context.ts

View File

@ -662,7 +662,7 @@ export function ChatView({
)}
>
<span
className="mx-auto mb-2 block bg-foreground size-7"
className="mx-auto mb-2 block bg-foreground size-10"
style={LOGO_MASK}
/>
<h1 className="text-base sm:text-lg font-bold tracking-tight">
@ -682,13 +682,13 @@ export function ChatView({
: "opacity-100 translate-y-0"
)}
>
<div className="w-full max-w-2xl px-5 space-y-5 text-center">
<div>
<div className="w-full max-w-2xl px-5 space-y-4 text-center">
<div className="-mt-16">
<span
className="mx-auto mb-2 block bg-foreground size-10"
className="mx-auto mb-4 block bg-foreground size-20"
style={LOGO_MASK}
/>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
Compass
</h1>
<p className="text-muted-foreground/60 mt-1.5 text-xs px-2">
@ -705,7 +705,7 @@ export function ChatView({
status={chat.status}
isGenerating={chat.isGenerating}
onSend={handleIdleSend}
className="rounded-2xl"
className="rounded-2xl mt-[6rem]"
/>
{stats && (

View File

@ -4,13 +4,10 @@ import * as React from "react"
import {
IconAddressBook,
IconCalendarStats,
IconDashboard,
IconFiles,
IconFolder,
IconHelp,
IconMessageCircle,
IconReceipt,
IconSearch,
IconSettings,
IconTruck,
IconUsers,
@ -23,9 +20,8 @@ import { NavSecondary } from "@/components/nav-secondary"
import { NavFiles } from "@/components/nav-files"
import { NavProjects } from "@/components/nav-projects"
import { NavUser } from "@/components/nav-user"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useSettings } from "@/components/settings-provider"
import { useAgentOptional } from "@/components/agent/chat-provider"
import { openFeedbackDialog } from "@/components/feedback-widget"
import type { SidebarUser } from "@/lib/auth"
import {
Sidebar,
@ -40,18 +36,13 @@ import {
const data = {
navMain: [
{
title: "Compass",
url: "/dashboard",
icon: IconDashboard,
},
{
title: "Projects",
url: "/dashboard/projects",
icon: IconFolder,
},
{
title: "People",
title: "Team",
url: "/dashboard/people",
icon: IconUsers,
},
@ -87,11 +78,6 @@ const data = {
url: "#",
icon: IconSettings,
},
{
title: "Get Help",
url: "#",
icon: IconHelp,
},
],
}
@ -107,9 +93,7 @@ function SidebarNav({
}) {
const pathname = usePathname()
const { state, setOpen } = useSidebar()
const { open: openSearch } = useCommandMenu()
const { open: openSettings } = useSettings()
const agent = useAgentOptional()
const isExpanded = state === "expanded"
const isFilesMode = pathname?.startsWith("/dashboard/files")
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
@ -137,8 +121,6 @@ function SidebarNav({
? { ...item, onClick: openSettings }
: item
),
...(agent ? [{ title: "Assistant", icon: IconMessageCircle, onClick: agent.open }] : []),
{ title: "Search", icon: IconSearch, onClick: openSearch },
]
return (
@ -170,6 +152,8 @@ export function AppSidebar({
readonly dashboards?: ReadonlyArray<{ readonly id: string; readonly name: string }>
readonly user: SidebarUser | null
}) {
const { isMobile } = useSidebar()
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
@ -207,6 +191,18 @@ export function AppSidebar({
/>
</SidebarContent>
<SidebarFooter>
{isMobile && (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
onClick={openFeedbackDialog}
>
<IconMessageCircle />
<span>Feedback</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)}
<NavUser user={user} />
</SidebarFooter>
</Sidebar>

View File

@ -1,6 +1,6 @@
"use client"
import { createContext, useContext, useState } from "react"
import { createContext, useContext, useState, useEffect } from "react"
import { usePathname } from "next/navigation"
import { useAgentOptional } from "@/components/agent/chat-provider"
import { MessageCircle } from "lucide-react"
@ -31,6 +31,10 @@ export function useFeedback() {
return useContext(FeedbackContext)
}
export function openFeedbackDialog() {
window.dispatchEvent(new CustomEvent("open-feedback-dialog"))
}
export function FeedbackCallout() {
const { open } = useFeedback()
return (
@ -98,24 +102,15 @@ export function FeedbackWidget({ children }: { children?: React.ReactNode }) {
}
}
useEffect(() => {
const handleOpenFeedback = () => setDialogOpen(true)
window.addEventListener("open-feedback-dialog", handleOpenFeedback)
return () => window.removeEventListener("open-feedback-dialog", handleOpenFeedback)
}, [])
return (
<FeedbackContext.Provider value={{ open: () => setDialogOpen(true) }}>
{children}
<Button
onClick={() => setDialogOpen(true)}
size="icon-lg"
className={cn(
"group fixed bottom-12 right-6 z-40 gap-0 rounded-full shadow-lg transition-all duration-200 hover:w-auto hover:gap-2 hover:px-4 overflow-hidden hidden md:flex",
chatOpen && "md:translate-x-20 md:opacity-0 md:pointer-events-none"
)}
>
<MessageCircle className="size-5 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 transition-all duration-200 group-hover:max-w-40 group-hover:opacity-100">
Feedback
</span>
</Button>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-[calc(100%-2rem)] sm:max-w-md p-4 sm:p-6">
<DialogHeader className="space-y-1.5">

View File

@ -28,7 +28,6 @@ import {
import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar"
import { NotificationsPopover } from "@/components/notifications-popover"
import { useCommandMenu } from "@/components/command-menu-provider"
import { useFeedback } from "@/components/feedback-widget"
import { useAgentOptional } from "@/components/agent/chat-provider"
import { AccountModal } from "@/components/account-modal"
import { getInitials } from "@/lib/utils"
@ -43,7 +42,6 @@ export function SiteHeader({
const { open: openCommand, openWithQuery } = useCommandMenu()
const [headerQuery, setHeaderQuery] = React.useState("")
const searchInputRef = React.useRef<HTMLInputElement>(null)
const { open: openFeedback } = useFeedback()
const agentContext = useAgentOptional()
const [accountOpen, setAccountOpen] = React.useState(false)
const { toggleSidebar } = useSidebar()
@ -58,43 +56,39 @@ export function SiteHeader({
<header className="sticky top-0 z-40 flex shrink-0 items-center border-b border-border/40 bg-background/80 backdrop-blur-sm">
{/* mobile header: single unified pill */}
<div className="flex h-14 w-full items-center px-3 md:hidden">
<div
className="flex h-11 w-full items-center gap-2 rounded-full bg-muted/50 px-2.5 cursor-pointer"
onClick={openCommand}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openCommand()
}}
>
<div className="flex h-11 w-full items-center gap-2 rounded-full bg-muted/50 px-2.5">
<button
type="button"
className="flex size-8 shrink-0 items-center justify-center rounded-full -ml-0.5 hover:bg-background/60"
onClick={(e) => {
e.stopPropagation()
className="flex size-10 shrink-0 items-center justify-center rounded-full -ml-0.5 hover:bg-background/60"
onClick={() => {
toggleSidebar()
}}
aria-label="Open menu"
>
<IconMenu2 className="size-5 text-muted-foreground" />
</button>
<Button
<button
type="button"
variant="ghost"
size="sm"
className="h-8 gap-1 rounded-full border border-white/80 px-2 text-xs text-muted-foreground hover:bg-background/60 hover:text-foreground"
onClick={(e) => {
e.stopPropagation()
openFeedback()
}}
className="flex h-9 flex-1 items-center gap-2 rounded-full px-2 hover:bg-background/60"
onClick={openCommand}
aria-label="Search"
>
<IconMessageCircle className="size-3.5" />
Feedback
</Button>
<IconSearch className="size-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground text-sm flex-1">
<span className="text-muted-foreground text-sm">
Search...
</span>
</button>
<button
type="button"
className="flex size-9 shrink-0 items-center justify-center rounded-full hover:bg-accent hover:text-accent-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring text-muted-foreground"
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark")
}}
aria-label="Toggle theme"
>
<IconSun className="size-4 hidden dark:block" />
<IconMoon className="size-4 block dark:hidden" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
@ -118,9 +112,9 @@ export function SiteHeader({
Account
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setTheme(theme === "dark" ? "light" : "dark")}>
<IconSun className="hidden dark:block" />
<IconMoon className="block dark:hidden" />
Toggle theme
<IconSun className="size-4 hidden dark:block" />
<IconMoon className="size-4 block dark:hidden" />
<span>Toggle theme</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={handleLogout}>
@ -136,15 +130,6 @@ export function SiteHeader({
<div className="hidden h-12 w-full grid-cols-[1fr_minmax(0,28rem)_1fr] items-center px-4 md:grid">
<div className="flex items-center gap-1">
<SidebarTrigger className="-ml-1" />
<Button
variant="ghost"
size="sm"
className="h-7 gap-1.5 rounded-full border border-white/80 px-2 text-xs text-muted-foreground/70 hover:text-foreground"
onClick={openFeedback}
>
<IconMessageCircle className="size-3.5" />
Feedback
</Button>
</div>
<div className="relative justify-self-center w-full">
@ -182,20 +167,21 @@ export function SiteHeader({
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground/70 hover:text-foreground"
className="size-7 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
onClick={() => agentContext?.toggle()}
aria-label="Toggle assistant"
>
<IconSparkles className="size-3.5" />
<IconSparkles className="size-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground/70 hover:text-foreground"
className="size-7 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Toggle theme"
>
<IconSun className="size-3.5 hidden dark:block" />
<IconMoon className="size-3.5 block dark:hidden" />
<IconSun className="size-4 hidden dark:block" />
<IconMoon className="size-4 block dark:hidden" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@ -186,8 +186,7 @@ function Sidebar({
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-[85vw] max-w-none p-0 border-0"
className="bg-sidebar text-sidebar-foreground max-w-[10rem] p-0 border-0"
side={side}
showClose={false}
>
@ -401,7 +400,7 @@ function SidebarGroupLabel({
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 text-center sm:text-left",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
@ -470,7 +469,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-9! group-data-[collapsible=icon]:justify-center! group-data-[collapsible=icon]:p-2.5! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {