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:
parent
04180d4305
commit
33b427ed33
2
.dev-setup/.gitignore
vendored
Normal file
2
.dev-setup/.gitignore
vendored
Normal 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
104
.dev-setup/README.md
Normal 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
65
.dev-setup/apply-dev.sh
Executable 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"
|
||||||
20
.dev-setup/files/cloudflare-context.ts
Normal file
20
.dev-setup/files/cloudflare-context.ts
Normal 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()
|
||||||
|
}
|
||||||
0
.dev-setup/patches/auth.patch
Normal file
0
.dev-setup/patches/auth.patch
Normal file
0
.dev-setup/patches/cloudflare-context.patch
Normal file
0
.dev-setup/patches/cloudflare-context.patch
Normal file
0
.dev-setup/patches/db-index.patch
Normal file
0
.dev-setup/patches/db-index.patch
Normal file
0
.dev-setup/patches/gitignore.patch
Normal file
0
.dev-setup/patches/gitignore.patch
Normal file
0
.dev-setup/patches/middleware.patch
Normal file
0
.dev-setup/patches/middleware.patch
Normal file
0
.dev-setup/patches/next-config.patch
Normal file
0
.dev-setup/patches/next-config.patch
Normal file
36
.dev-setup/restore-dev.sh
Executable file
36
.dev-setup/restore-dev.sh
Executable 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
1
.gitignore
vendored
@ -40,3 +40,4 @@ android/build/
|
|||||||
android/app/build/
|
android/app/build/
|
||||||
# Local auth bypass (dev only)
|
# Local auth bypass (dev only)
|
||||||
src/lib/auth-bypass.ts
|
src/lib/auth-bypass.ts
|
||||||
|
src/lib/cloudflare-context.ts
|
||||||
|
|||||||
@ -662,7 +662,7 @@ export function ChatView({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="mx-auto mb-2 block bg-foreground size-7"
|
className="mx-auto mb-2 block bg-foreground size-10"
|
||||||
style={LOGO_MASK}
|
style={LOGO_MASK}
|
||||||
/>
|
/>
|
||||||
<h1 className="text-base sm:text-lg font-bold tracking-tight">
|
<h1 className="text-base sm:text-lg font-bold tracking-tight">
|
||||||
@ -682,13 +682,13 @@ export function ChatView({
|
|||||||
: "opacity-100 translate-y-0"
|
: "opacity-100 translate-y-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-2xl px-5 space-y-5 text-center">
|
<div className="w-full max-w-2xl px-5 space-y-4 text-center">
|
||||||
<div>
|
<div className="-mt-16">
|
||||||
<span
|
<span
|
||||||
className="mx-auto mb-2 block bg-foreground size-10"
|
className="mx-auto mb-4 block bg-foreground size-20"
|
||||||
style={LOGO_MASK}
|
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
|
Compass
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground/60 mt-1.5 text-xs px-2">
|
<p className="text-muted-foreground/60 mt-1.5 text-xs px-2">
|
||||||
@ -705,7 +705,7 @@ export function ChatView({
|
|||||||
status={chat.status}
|
status={chat.status}
|
||||||
isGenerating={chat.isGenerating}
|
isGenerating={chat.isGenerating}
|
||||||
onSend={handleIdleSend}
|
onSend={handleIdleSend}
|
||||||
className="rounded-2xl"
|
className="rounded-2xl mt-[6rem]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
|
|||||||
@ -4,13 +4,10 @@ import * as React from "react"
|
|||||||
import {
|
import {
|
||||||
IconAddressBook,
|
IconAddressBook,
|
||||||
IconCalendarStats,
|
IconCalendarStats,
|
||||||
IconDashboard,
|
|
||||||
IconFiles,
|
IconFiles,
|
||||||
IconFolder,
|
IconFolder,
|
||||||
IconHelp,
|
|
||||||
IconMessageCircle,
|
IconMessageCircle,
|
||||||
IconReceipt,
|
IconReceipt,
|
||||||
IconSearch,
|
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTruck,
|
IconTruck,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
@ -23,9 +20,8 @@ import { NavSecondary } from "@/components/nav-secondary"
|
|||||||
import { NavFiles } from "@/components/nav-files"
|
import { NavFiles } from "@/components/nav-files"
|
||||||
import { NavProjects } from "@/components/nav-projects"
|
import { NavProjects } from "@/components/nav-projects"
|
||||||
import { NavUser } from "@/components/nav-user"
|
import { NavUser } from "@/components/nav-user"
|
||||||
import { useCommandMenu } from "@/components/command-menu-provider"
|
|
||||||
import { useSettings } from "@/components/settings-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 type { SidebarUser } from "@/lib/auth"
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@ -40,18 +36,13 @@ import {
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
|
||||||
title: "Compass",
|
|
||||||
url: "/dashboard",
|
|
||||||
icon: IconDashboard,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
url: "/dashboard/projects",
|
url: "/dashboard/projects",
|
||||||
icon: IconFolder,
|
icon: IconFolder,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "People",
|
title: "Team",
|
||||||
url: "/dashboard/people",
|
url: "/dashboard/people",
|
||||||
icon: IconUsers,
|
icon: IconUsers,
|
||||||
},
|
},
|
||||||
@ -87,11 +78,6 @@ const data = {
|
|||||||
url: "#",
|
url: "#",
|
||||||
icon: IconSettings,
|
icon: IconSettings,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Get Help",
|
|
||||||
url: "#",
|
|
||||||
icon: IconHelp,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,9 +93,7 @@ function SidebarNav({
|
|||||||
}) {
|
}) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { state, setOpen } = useSidebar()
|
const { state, setOpen } = useSidebar()
|
||||||
const { open: openSearch } = useCommandMenu()
|
|
||||||
const { open: openSettings } = useSettings()
|
const { open: openSettings } = useSettings()
|
||||||
const agent = useAgentOptional()
|
|
||||||
const isExpanded = state === "expanded"
|
const isExpanded = state === "expanded"
|
||||||
const isFilesMode = pathname?.startsWith("/dashboard/files")
|
const isFilesMode = pathname?.startsWith("/dashboard/files")
|
||||||
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
|
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
|
||||||
@ -137,8 +121,6 @@ function SidebarNav({
|
|||||||
? { ...item, onClick: openSettings }
|
? { ...item, onClick: openSettings }
|
||||||
: item
|
: item
|
||||||
),
|
),
|
||||||
...(agent ? [{ title: "Assistant", icon: IconMessageCircle, onClick: agent.open }] : []),
|
|
||||||
{ title: "Search", icon: IconSearch, onClick: openSearch },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -170,6 +152,8 @@ export function AppSidebar({
|
|||||||
readonly dashboards?: ReadonlyArray<{ readonly id: string; readonly name: string }>
|
readonly dashboards?: ReadonlyArray<{ readonly id: string; readonly name: string }>
|
||||||
readonly user: SidebarUser | null
|
readonly user: SidebarUser | null
|
||||||
}) {
|
}) {
|
||||||
|
const { isMobile } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@ -207,6 +191,18 @@ export function AppSidebar({
|
|||||||
/>
|
/>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
{isMobile && (
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
onClick={openFeedbackDialog}
|
||||||
|
>
|
||||||
|
<IconMessageCircle />
|
||||||
|
<span>Feedback</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
)}
|
||||||
<NavUser user={user} />
|
<NavUser user={user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { createContext, useContext, useState } from "react"
|
import { createContext, useContext, useState, useEffect } from "react"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { useAgentOptional } from "@/components/agent/chat-provider"
|
import { useAgentOptional } from "@/components/agent/chat-provider"
|
||||||
import { MessageCircle } from "lucide-react"
|
import { MessageCircle } from "lucide-react"
|
||||||
@ -31,6 +31,10 @@ export function useFeedback() {
|
|||||||
return useContext(FeedbackContext)
|
return useContext(FeedbackContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openFeedbackDialog() {
|
||||||
|
window.dispatchEvent(new CustomEvent("open-feedback-dialog"))
|
||||||
|
}
|
||||||
|
|
||||||
export function FeedbackCallout() {
|
export function FeedbackCallout() {
|
||||||
const { open } = useFeedback()
|
const { open } = useFeedback()
|
||||||
return (
|
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 (
|
return (
|
||||||
<FeedbackContext.Provider value={{ open: () => setDialogOpen(true) }}>
|
<FeedbackContext.Provider value={{ open: () => setDialogOpen(true) }}>
|
||||||
{children}
|
{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}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-w-[calc(100%-2rem)] sm:max-w-md p-4 sm:p-6">
|
<DialogContent className="max-w-[calc(100%-2rem)] sm:max-w-md p-4 sm:p-6">
|
||||||
<DialogHeader className="space-y-1.5">
|
<DialogHeader className="space-y-1.5">
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import {
|
|||||||
import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar"
|
import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar"
|
||||||
import { NotificationsPopover } from "@/components/notifications-popover"
|
import { NotificationsPopover } from "@/components/notifications-popover"
|
||||||
import { useCommandMenu } from "@/components/command-menu-provider"
|
import { useCommandMenu } from "@/components/command-menu-provider"
|
||||||
import { useFeedback } from "@/components/feedback-widget"
|
|
||||||
import { useAgentOptional } from "@/components/agent/chat-provider"
|
import { useAgentOptional } from "@/components/agent/chat-provider"
|
||||||
import { AccountModal } from "@/components/account-modal"
|
import { AccountModal } from "@/components/account-modal"
|
||||||
import { getInitials } from "@/lib/utils"
|
import { getInitials } from "@/lib/utils"
|
||||||
@ -43,7 +42,6 @@ export function SiteHeader({
|
|||||||
const { open: openCommand, openWithQuery } = useCommandMenu()
|
const { open: openCommand, openWithQuery } = useCommandMenu()
|
||||||
const [headerQuery, setHeaderQuery] = React.useState("")
|
const [headerQuery, setHeaderQuery] = React.useState("")
|
||||||
const searchInputRef = React.useRef<HTMLInputElement>(null)
|
const searchInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
const { open: openFeedback } = useFeedback()
|
|
||||||
const agentContext = useAgentOptional()
|
const agentContext = useAgentOptional()
|
||||||
const [accountOpen, setAccountOpen] = React.useState(false)
|
const [accountOpen, setAccountOpen] = React.useState(false)
|
||||||
const { toggleSidebar } = useSidebar()
|
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">
|
<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 */}
|
{/* mobile header: single unified pill */}
|
||||||
<div className="flex h-14 w-full items-center px-3 md:hidden">
|
<div className="flex h-14 w-full items-center px-3 md:hidden">
|
||||||
<div
|
<div className="flex h-11 w-full items-center gap-2 rounded-full bg-muted/50 px-2.5">
|
||||||
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()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex size-8 shrink-0 items-center justify-center rounded-full -ml-0.5 hover:bg-background/60"
|
className="flex size-10 shrink-0 items-center justify-center rounded-full -ml-0.5 hover:bg-background/60"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
e.stopPropagation()
|
|
||||||
toggleSidebar()
|
toggleSidebar()
|
||||||
}}
|
}}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<IconMenu2 className="size-5 text-muted-foreground" />
|
<IconMenu2 className="size-5 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
className="flex h-9 flex-1 items-center gap-2 rounded-full px-2 hover:bg-background/60"
|
||||||
size="sm"
|
onClick={openCommand}
|
||||||
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"
|
aria-label="Search"
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
openFeedback()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<IconMessageCircle className="size-3.5" />
|
<IconSearch className="size-4 text-muted-foreground shrink-0" />
|
||||||
Feedback
|
<span className="text-muted-foreground text-sm">
|
||||||
</Button>
|
Search...
|
||||||
<IconSearch className="size-4 text-muted-foreground shrink-0" />
|
</span>
|
||||||
<span className="text-muted-foreground text-sm flex-1">
|
</button>
|
||||||
Search...
|
<button
|
||||||
</span>
|
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>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
@ -118,9 +112,9 @@ export function SiteHeader({
|
|||||||
Account
|
Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onSelect={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
<DropdownMenuItem onSelect={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
||||||
<IconSun className="hidden dark:block" />
|
<IconSun className="size-4 hidden dark:block" />
|
||||||
<IconMoon className="block dark:hidden" />
|
<IconMoon className="size-4 block dark:hidden" />
|
||||||
Toggle theme
|
<span>Toggle theme</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onSelect={handleLogout}>
|
<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="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">
|
<div className="flex items-center gap-1">
|
||||||
<SidebarTrigger className="-ml-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>
|
||||||
|
|
||||||
<div className="relative justify-self-center w-full">
|
<div className="relative justify-self-center w-full">
|
||||||
@ -182,20 +167,21 @@ export function SiteHeader({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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()}
|
onClick={() => agentContext?.toggle()}
|
||||||
aria-label="Toggle assistant"
|
aria-label="Toggle assistant"
|
||||||
>
|
>
|
||||||
<IconSparkles className="size-3.5" />
|
<IconSparkles className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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")}
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
<IconSun className="size-3.5 hidden dark:block" />
|
<IconSun className="size-4 hidden dark:block" />
|
||||||
<IconMoon className="size-3.5 block dark:hidden" />
|
<IconMoon className="size-4 block dark:hidden" />
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@ -186,8 +186,7 @@ function Sidebar({
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar"
|
data-slot="sidebar"
|
||||||
data-mobile="true"
|
className="bg-sidebar text-sidebar-foreground max-w-[10rem] p-0 border-0"
|
||||||
className="bg-sidebar text-sidebar-foreground w-[85vw] max-w-none p-0 border-0"
|
|
||||||
side={side}
|
side={side}
|
||||||
showClose={false}
|
showClose={false}
|
||||||
>
|
>
|
||||||
@ -401,7 +400,7 @@ function SidebarGroupLabel({
|
|||||||
data-slot="sidebar-group-label"
|
data-slot="sidebar-group-label"
|
||||||
data-sidebar="group-label"
|
data-sidebar="group-label"
|
||||||
className={cn(
|
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",
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -470,7 +469,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user