Spaces:
Sleeping
Sleeping
Delete web
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- web/README.md +0 -11
- web/index.html +0 -15
- web/package.json +0 -61
- web/src/.DS_Store +0 -0
- web/src/App.tsx +0 -563
- web/src/Attributions.md +0 -3
- web/src/components/.DS_Store +0 -0
- web/src/components/ChatArea.tsx +0 -282
- web/src/components/FileUploadArea.tsx +0 -300
- web/src/components/FloatingActionButtons.tsx +0 -111
- web/src/components/GroupMembers.tsx +0 -57
- web/src/components/Header.tsx +0 -79
- web/src/components/LearningModeSelector.tsx +0 -94
- web/src/components/LeftSidebar.tsx +0 -121
- web/src/components/MemoryLine.tsx +0 -108
- web/src/components/Message.tsx +0 -100
- web/src/components/RightPanel.tsx +0 -307
- web/src/components/UserGuide.tsx +0 -151
- web/src/components/figma/ImageWithFallback.tsx +0 -27
- web/src/components/ui/accordion.tsx +0 -66
- web/src/components/ui/alert-dialog.tsx +0 -157
- web/src/components/ui/alert.tsx +0 -66
- web/src/components/ui/aspect-ratio.tsx +0 -11
- web/src/components/ui/avatar.tsx +0 -53
- web/src/components/ui/badge.tsx +0 -46
- web/src/components/ui/breadcrumb.tsx +0 -109
- web/src/components/ui/button.tsx +0 -58
- web/src/components/ui/calendar.tsx +0 -75
- web/src/components/ui/card.tsx +0 -92
- web/src/components/ui/carousel.tsx +0 -241
- web/src/components/ui/chart.tsx +0 -353
- web/src/components/ui/checkbox.tsx +0 -32
- web/src/components/ui/collapsible.tsx +0 -33
- web/src/components/ui/command.tsx +0 -177
- web/src/components/ui/context-menu.tsx +0 -252
- web/src/components/ui/dialog.tsx +0 -137
- web/src/components/ui/drawer.tsx +0 -132
- web/src/components/ui/dropdown-menu.tsx +0 -257
- web/src/components/ui/form.tsx +0 -168
- web/src/components/ui/hover-card.tsx +0 -44
- web/src/components/ui/input-otp.tsx +0 -77
- web/src/components/ui/input.tsx +0 -21
- web/src/components/ui/label.tsx +0 -24
- web/src/components/ui/menubar.tsx +0 -276
- web/src/components/ui/navigation-menu.tsx +0 -168
- web/src/components/ui/pagination.tsx +0 -127
- web/src/components/ui/popover.tsx +0 -48
- web/src/components/ui/progress.tsx +0 -31
- web/src/components/ui/radio-group.tsx +0 -45
- web/src/components/ui/resizable.tsx +0 -56
web/README.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# Clare AI Tutor UI Redesign (Copy)
|
| 3 |
-
|
| 4 |
-
This is a code bundle for Clare AI Tutor UI Redesign (Copy). The original project is available at https://www.figma.com/design/yC1iBsNtBGpBH8sXO43uah/Clare-AI-Tutor-UI-Redesign--Copy-.
|
| 5 |
-
|
| 6 |
-
## Running the code
|
| 7 |
-
|
| 8 |
-
Run `npm i` to install the dependencies.
|
| 9 |
-
|
| 10 |
-
Run `npm run dev` to start the development server.
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/index.html
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
<!DOCTYPE html>
|
| 3 |
-
<html lang="en">
|
| 4 |
-
<head>
|
| 5 |
-
<meta charset="UTF-8" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>Clare AI Tutor UI Redesign (Copy)</title>
|
| 8 |
-
</head>
|
| 9 |
-
|
| 10 |
-
<body>
|
| 11 |
-
<div id="root"></div>
|
| 12 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 13 |
-
</body>
|
| 14 |
-
</html>
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/package.json
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
{
|
| 3 |
-
"name": "Clare AI Tutor UI Redesign (Copy)",
|
| 4 |
-
"version": "0.1.0",
|
| 5 |
-
"private": true,
|
| 6 |
-
"dependencies": {
|
| 7 |
-
"@radix-ui/react-accordion": "^1.2.3",
|
| 8 |
-
"@radix-ui/react-alert-dialog": "^1.1.6",
|
| 9 |
-
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
| 10 |
-
"@radix-ui/react-avatar": "^1.1.3",
|
| 11 |
-
"@radix-ui/react-checkbox": "^1.1.4",
|
| 12 |
-
"@radix-ui/react-collapsible": "^1.1.3",
|
| 13 |
-
"@radix-ui/react-context-menu": "^2.2.6",
|
| 14 |
-
"@radix-ui/react-dialog": "^1.1.6",
|
| 15 |
-
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
| 16 |
-
"@radix-ui/react-hover-card": "^1.1.6",
|
| 17 |
-
"@radix-ui/react-label": "^2.1.2",
|
| 18 |
-
"@radix-ui/react-menubar": "^1.1.6",
|
| 19 |
-
"@radix-ui/react-navigation-menu": "^1.2.5",
|
| 20 |
-
"@radix-ui/react-popover": "^1.1.6",
|
| 21 |
-
"@radix-ui/react-progress": "^1.1.2",
|
| 22 |
-
"@radix-ui/react-radio-group": "^1.2.3",
|
| 23 |
-
"@radix-ui/react-scroll-area": "^1.2.3",
|
| 24 |
-
"@radix-ui/react-select": "^2.1.6",
|
| 25 |
-
"@radix-ui/react-separator": "^1.1.2",
|
| 26 |
-
"@radix-ui/react-slider": "^1.2.3",
|
| 27 |
-
"@radix-ui/react-slot": "^1.1.2",
|
| 28 |
-
"@radix-ui/react-switch": "^1.1.3",
|
| 29 |
-
"@radix-ui/react-tabs": "^1.1.3",
|
| 30 |
-
"@radix-ui/react-toggle": "^1.1.2",
|
| 31 |
-
"@radix-ui/react-toggle-group": "^1.1.2",
|
| 32 |
-
"@radix-ui/react-tooltip": "^1.1.8",
|
| 33 |
-
"class-variance-authority": "^0.7.1",
|
| 34 |
-
"clsx": "*",
|
| 35 |
-
"cmdk": "^1.1.1",
|
| 36 |
-
"embla-carousel-react": "^8.6.0",
|
| 37 |
-
"input-otp": "^1.4.2",
|
| 38 |
-
"lucide-react": "^0.487.0",
|
| 39 |
-
"next-themes": "^0.4.6",
|
| 40 |
-
"react": "^18.3.1",
|
| 41 |
-
"react-day-picker": "^8.10.1",
|
| 42 |
-
"react-dom": "^18.3.1",
|
| 43 |
-
"react-hook-form": "^7.55.0",
|
| 44 |
-
"react-resizable-panels": "^2.1.7",
|
| 45 |
-
"react-markdown": "^9.0.1",
|
| 46 |
-
"remark-gfm": "^4.0.0",
|
| 47 |
-
"recharts": "^2.15.2",
|
| 48 |
-
"sonner": "^2.0.3",
|
| 49 |
-
"tailwind-merge": "*",
|
| 50 |
-
"vaul": "^1.1.2"
|
| 51 |
-
},
|
| 52 |
-
"devDependencies": {
|
| 53 |
-
"@types/node": "^20.10.0",
|
| 54 |
-
"@vitejs/plugin-react-swc": "^3.10.2",
|
| 55 |
-
"vite": "6.3.5"
|
| 56 |
-
},
|
| 57 |
-
"scripts": {
|
| 58 |
-
"dev": "vite",
|
| 59 |
-
"build": "vite build"
|
| 60 |
-
}
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/.DS_Store
DELETED
|
Binary file (6.15 kB)
|
|
|
web/src/App.tsx
DELETED
|
@@ -1,563 +0,0 @@
|
|
| 1 |
-
// web/src/App.tsx
|
| 2 |
-
import React, { useState, useEffect, useMemo } from 'react';
|
| 3 |
-
import { Header } from './components/Header';
|
| 4 |
-
import { LeftSidebar } from './components/LeftSidebar';
|
| 5 |
-
import { ChatArea } from './components/ChatArea';
|
| 6 |
-
import { RightPanel } from './components/RightPanel';
|
| 7 |
-
import { FloatingActionButtons } from './components/FloatingActionButtons';
|
| 8 |
-
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
| 9 |
-
import { Button } from './components/ui/button';
|
| 10 |
-
import { Toaster } from './components/ui/sonner';
|
| 11 |
-
import { toast } from 'sonner';
|
| 12 |
-
|
| 13 |
-
export interface Message {
|
| 14 |
-
id: string;
|
| 15 |
-
role: 'user' | 'assistant';
|
| 16 |
-
content: string;
|
| 17 |
-
timestamp: Date;
|
| 18 |
-
references?: string[];
|
| 19 |
-
sender?: GroupMember;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export interface User {
|
| 23 |
-
name: string;
|
| 24 |
-
email: string; // login 输入(原始输入)
|
| 25 |
-
user_id: string; // 实际传后端的 user_id(这里等同 email/ID)
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
export interface GroupMember {
|
| 29 |
-
id: string;
|
| 30 |
-
name: string;
|
| 31 |
-
email: string;
|
| 32 |
-
avatar?: string;
|
| 33 |
-
isAI?: boolean;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
export type SpaceType = 'individual' | 'group';
|
| 37 |
-
export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
|
| 38 |
-
|
| 39 |
-
export interface UploadedFile {
|
| 40 |
-
file: File;
|
| 41 |
-
type: FileType;
|
| 42 |
-
uploaded?: boolean;
|
| 43 |
-
uploadedChunks?: number;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
|
| 47 |
-
export type Language = 'auto' | 'en' | 'zh';
|
| 48 |
-
|
| 49 |
-
type ChatApiResp = {
|
| 50 |
-
reply: string;
|
| 51 |
-
session_status_md?: string;
|
| 52 |
-
refs?: Array<{ source_file?: string; section?: string }>;
|
| 53 |
-
latency_ms?: number;
|
| 54 |
-
};
|
| 55 |
-
|
| 56 |
-
type UploadApiResp = {
|
| 57 |
-
ok: boolean;
|
| 58 |
-
added_chunks: number;
|
| 59 |
-
status_md: string;
|
| 60 |
-
};
|
| 61 |
-
|
| 62 |
-
type LoginApiResp = {
|
| 63 |
-
ok: boolean;
|
| 64 |
-
user: { name: string; user_id: string };
|
| 65 |
-
};
|
| 66 |
-
|
| 67 |
-
function mapFileTypeToDocType(t: FileType): string {
|
| 68 |
-
switch (t) {
|
| 69 |
-
case 'syllabus':
|
| 70 |
-
return 'Syllabus';
|
| 71 |
-
case 'lecture-slides':
|
| 72 |
-
return 'Lecture Slides';
|
| 73 |
-
case 'literature-review':
|
| 74 |
-
return 'Literature Review / Paper';
|
| 75 |
-
default:
|
| 76 |
-
return 'Other';
|
| 77 |
-
}
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
async function apiPostJson<T>(path: string, payload: any): Promise<T> {
|
| 81 |
-
const res = await fetch(path, {
|
| 82 |
-
method: 'POST',
|
| 83 |
-
headers: { 'Content-Type': 'application/json' },
|
| 84 |
-
body: JSON.stringify(payload),
|
| 85 |
-
});
|
| 86 |
-
if (!res.ok) {
|
| 87 |
-
const txt = await res.text().catch(() => '');
|
| 88 |
-
throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
|
| 89 |
-
}
|
| 90 |
-
return (await res.json()) as T;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
async function apiPostForm<T>(path: string, form: FormData): Promise<T> {
|
| 94 |
-
const res = await fetch(path, { method: 'POST', body: form });
|
| 95 |
-
if (!res.ok) {
|
| 96 |
-
const txt = await res.text().catch(() => '');
|
| 97 |
-
throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
|
| 98 |
-
}
|
| 99 |
-
return (await res.json()) as T;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
function App() {
|
| 103 |
-
const [isDarkMode, setIsDarkMode] = useState(() => {
|
| 104 |
-
const saved = localStorage.getItem('theme');
|
| 105 |
-
return (
|
| 106 |
-
saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
| 107 |
-
);
|
| 108 |
-
});
|
| 109 |
-
|
| 110 |
-
const [user, setUser] = useState<User | null>(null);
|
| 111 |
-
|
| 112 |
-
const [messages, setMessages] = useState<Message[]>([
|
| 113 |
-
{
|
| 114 |
-
id: '1',
|
| 115 |
-
role: 'assistant',
|
| 116 |
-
content:
|
| 117 |
-
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
|
| 118 |
-
timestamp: new Date(),
|
| 119 |
-
},
|
| 120 |
-
]);
|
| 121 |
-
|
| 122 |
-
const [learningMode, setLearningMode] = useState<LearningMode>('concept');
|
| 123 |
-
const [language, setLanguage] = useState<Language>('auto');
|
| 124 |
-
|
| 125 |
-
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
| 126 |
-
const [memoryProgress, setMemoryProgress] = useState(40);
|
| 127 |
-
|
| 128 |
-
const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
|
| 129 |
-
const [rightPanelOpen, setRightPanelOpen] = useState(false);
|
| 130 |
-
const [rightPanelVisible, setRightPanelVisible] = useState(true);
|
| 131 |
-
|
| 132 |
-
const [spaceType, setSpaceType] = useState<SpaceType>('individual');
|
| 133 |
-
|
| 134 |
-
const [exportResult, setExportResult] = useState('');
|
| 135 |
-
const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
|
| 136 |
-
|
| 137 |
-
const [groupMembers] = useState<GroupMember[]>([
|
| 138 |
-
{ id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
|
| 139 |
-
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
|
| 140 |
-
{ id: '2', name: 'Michael Chen', email: 'michael.c@university.edu' },
|
| 141 |
-
{ id: '3', name: 'Emma Williams', email: 'emma.w@university.edu' },
|
| 142 |
-
]);
|
| 143 |
-
|
| 144 |
-
useEffect(() => {
|
| 145 |
-
document.documentElement.classList.toggle('dark', isDarkMode);
|
| 146 |
-
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
| 147 |
-
}, [isDarkMode]);
|
| 148 |
-
|
| 149 |
-
// ✅ 彻底去掉 hardcode:未登录 userId 为空
|
| 150 |
-
const userId = useMemo(() => (user?.user_id || '').trim(), [user]);
|
| 151 |
-
|
| 152 |
-
// ✅ 未登录不可聊天/上传/feedback
|
| 153 |
-
const isLoggedIn = useMemo(() => !!user && !!userId, [user, userId]);
|
| 154 |
-
|
| 155 |
-
const currentDocTypeForChat = useMemo(() => {
|
| 156 |
-
const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
|
| 157 |
-
if (hasSyllabus) return 'Syllabus';
|
| 158 |
-
const hasSlides = uploadedFiles.some((f) => f.type === 'lecture-slides' && f.uploaded);
|
| 159 |
-
if (hasSlides) return 'Lecture Slides';
|
| 160 |
-
const hasLit = uploadedFiles.some((f) => f.type === 'literature-review' && f.uploaded);
|
| 161 |
-
if (hasLit) return 'Literature Review / Paper';
|
| 162 |
-
return 'Other';
|
| 163 |
-
}, [uploadedFiles]);
|
| 164 |
-
|
| 165 |
-
// ✅ 登录必须打后端 /api/login,确保 server session 有 name
|
| 166 |
-
const handleLogin = async (name: string, emailOrId: string) => {
|
| 167 |
-
const nameTrim = name.trim();
|
| 168 |
-
const idTrim = emailOrId.trim();
|
| 169 |
-
|
| 170 |
-
if (!nameTrim || !idTrim) {
|
| 171 |
-
toast.error('Please fill in both name and Email/ID');
|
| 172 |
-
return;
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
try {
|
| 176 |
-
const resp = await apiPostJson<LoginApiResp>('/api/login', {
|
| 177 |
-
name: nameTrim,
|
| 178 |
-
user_id: idTrim,
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
if (!resp?.ok) throw new Error('Login failed (ok=false)');
|
| 182 |
-
|
| 183 |
-
// 后端回来的 user_id 为准(保持一致)
|
| 184 |
-
setUser({
|
| 185 |
-
name: resp.user.name,
|
| 186 |
-
email: idTrim,
|
| 187 |
-
user_id: resp.user.user_id,
|
| 188 |
-
});
|
| 189 |
-
|
| 190 |
-
toast.success('Logged in');
|
| 191 |
-
} catch (e: any) {
|
| 192 |
-
console.error(e);
|
| 193 |
-
toast.error(`Login failed: ${e?.message || 'unknown error'}`);
|
| 194 |
-
}
|
| 195 |
-
};
|
| 196 |
-
|
| 197 |
-
const handleLogout = () => {
|
| 198 |
-
setUser(null);
|
| 199 |
-
toast.message('Logged out');
|
| 200 |
-
};
|
| 201 |
-
|
| 202 |
-
const handleSendMessage = async (content: string) => {
|
| 203 |
-
if (!content.trim()) return;
|
| 204 |
-
|
| 205 |
-
if (!isLoggedIn) {
|
| 206 |
-
toast.error('Please log in first');
|
| 207 |
-
return;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
const shouldAIRespond =
|
| 211 |
-
spaceType === 'individual' || content.toLowerCase().includes('@clare');
|
| 212 |
-
|
| 213 |
-
const sender: GroupMember | undefined =
|
| 214 |
-
spaceType === 'group' && user
|
| 215 |
-
? { id: user.user_id, name: user.name, email: user.email }
|
| 216 |
-
: undefined;
|
| 217 |
-
|
| 218 |
-
const userMessage: Message = {
|
| 219 |
-
id: Date.now().toString(),
|
| 220 |
-
role: 'user',
|
| 221 |
-
content,
|
| 222 |
-
timestamp: new Date(),
|
| 223 |
-
sender,
|
| 224 |
-
};
|
| 225 |
-
setMessages((prev) => [...prev, userMessage]);
|
| 226 |
-
|
| 227 |
-
if (!shouldAIRespond) return;
|
| 228 |
-
|
| 229 |
-
try {
|
| 230 |
-
const resp = await apiPostJson<ChatApiResp>('/api/chat', {
|
| 231 |
-
user_id: userId,
|
| 232 |
-
message: content,
|
| 233 |
-
learning_mode: learningMode,
|
| 234 |
-
language_preference: language === 'auto' ? 'Auto' : language,
|
| 235 |
-
doc_type: currentDocTypeForChat,
|
| 236 |
-
});
|
| 237 |
-
|
| 238 |
-
const refs = (resp.refs || [])
|
| 239 |
-
.map((r) => [r.source_file, r.section].filter(Boolean).join(' / '))
|
| 240 |
-
.filter(Boolean);
|
| 241 |
-
|
| 242 |
-
const assistantMessage: Message = {
|
| 243 |
-
id: (Date.now() + 1).toString(),
|
| 244 |
-
role: 'assistant',
|
| 245 |
-
content: resp.reply || '(empty reply)',
|
| 246 |
-
timestamp: new Date(),
|
| 247 |
-
references: refs.length ? refs : undefined,
|
| 248 |
-
sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
|
| 249 |
-
};
|
| 250 |
-
|
| 251 |
-
setMessages((prev) => [...prev, assistantMessage]);
|
| 252 |
-
} catch (e: any) {
|
| 253 |
-
console.error(e);
|
| 254 |
-
toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
|
| 255 |
-
setMessages((prev) => [
|
| 256 |
-
...prev,
|
| 257 |
-
{
|
| 258 |
-
id: (Date.now() + 1).toString(),
|
| 259 |
-
role: 'assistant',
|
| 260 |
-
content: `Sorry — chat request failed. ${e?.message || ''}`,
|
| 261 |
-
timestamp: new Date(),
|
| 262 |
-
},
|
| 263 |
-
]);
|
| 264 |
-
}
|
| 265 |
-
};
|
| 266 |
-
|
| 267 |
-
// ✅ 选文件:只入库,不上传
|
| 268 |
-
const handleFileUpload = (files: File[]) => {
|
| 269 |
-
if (!isLoggedIn) {
|
| 270 |
-
toast.error('Please log in first');
|
| 271 |
-
return;
|
| 272 |
-
}
|
| 273 |
-
const newFiles: UploadedFile[] = files.map((file) => ({
|
| 274 |
-
file,
|
| 275 |
-
type: 'other',
|
| 276 |
-
uploaded: false,
|
| 277 |
-
}));
|
| 278 |
-
setUploadedFiles((prev) => [...prev, ...newFiles]);
|
| 279 |
-
toast.message('Files added. Select a File Type, then click Upload.');
|
| 280 |
-
};
|
| 281 |
-
|
| 282 |
-
const handleRemoveFile = (index: number) => {
|
| 283 |
-
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
| 284 |
-
};
|
| 285 |
-
|
| 286 |
-
const handleFileTypeChange = (index: number, type: FileType) => {
|
| 287 |
-
setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
|
| 288 |
-
};
|
| 289 |
-
|
| 290 |
-
// ✅ 上传单个文件
|
| 291 |
-
const handleUploadSingle = async (index: number) => {
|
| 292 |
-
if (!isLoggedIn) {
|
| 293 |
-
toast.error('Please log in first');
|
| 294 |
-
return;
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
const target = uploadedFiles[index];
|
| 298 |
-
if (!target) return;
|
| 299 |
-
|
| 300 |
-
try {
|
| 301 |
-
const form = new FormData();
|
| 302 |
-
form.append('user_id', userId);
|
| 303 |
-
form.append('doc_type', mapFileTypeToDocType(target.type));
|
| 304 |
-
form.append('file', target.file);
|
| 305 |
-
|
| 306 |
-
const r = await apiPostForm<UploadApiResp>('/api/upload', form);
|
| 307 |
-
if (!r.ok) throw new Error('Upload response ok=false');
|
| 308 |
-
|
| 309 |
-
setUploadedFiles((prev) =>
|
| 310 |
-
prev.map((x, i) => (i === index ? { ...x, uploaded: true, uploadedChunks: r.added_chunks } : x))
|
| 311 |
-
);
|
| 312 |
-
|
| 313 |
-
toast.success(`Uploaded: ${target.file.name} (+${r.added_chunks} chunks)`);
|
| 314 |
-
} catch (e: any) {
|
| 315 |
-
console.error(e);
|
| 316 |
-
toast.error(`Upload failed: ${e?.message || 'unknown error'}`);
|
| 317 |
-
}
|
| 318 |
-
};
|
| 319 |
-
|
| 320 |
-
const handleUploadAllPending = async () => {
|
| 321 |
-
if (!isLoggedIn) {
|
| 322 |
-
toast.error('Please log in first');
|
| 323 |
-
return;
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
const pendingIdx = uploadedFiles
|
| 327 |
-
.map((f, i) => ({ f, i }))
|
| 328 |
-
.filter((x) => !x.f.uploaded)
|
| 329 |
-
.map((x) => x.i);
|
| 330 |
-
|
| 331 |
-
if (!pendingIdx.length) {
|
| 332 |
-
toast.message('No pending files to upload.');
|
| 333 |
-
return;
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
for (const idx of pendingIdx) {
|
| 337 |
-
// eslint-disable-next-line no-await-in-loop
|
| 338 |
-
await handleUploadSingle(idx);
|
| 339 |
-
}
|
| 340 |
-
};
|
| 341 |
-
|
| 342 |
-
const handleClearConversation = () => {
|
| 343 |
-
setMessages([
|
| 344 |
-
{
|
| 345 |
-
id: '1',
|
| 346 |
-
role: 'assistant',
|
| 347 |
-
content:
|
| 348 |
-
"Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
|
| 349 |
-
timestamp: new Date(),
|
| 350 |
-
},
|
| 351 |
-
]);
|
| 352 |
-
};
|
| 353 |
-
|
| 354 |
-
const handleExport = async () => {
|
| 355 |
-
if (!isLoggedIn) {
|
| 356 |
-
toast.error('Please log in first');
|
| 357 |
-
return;
|
| 358 |
-
}
|
| 359 |
-
try {
|
| 360 |
-
const r = await apiPostJson<{ markdown: string }>('/api/export', {
|
| 361 |
-
user_id: userId,
|
| 362 |
-
learning_mode: learningMode,
|
| 363 |
-
});
|
| 364 |
-
setExportResult(r.markdown || '');
|
| 365 |
-
setResultType('export');
|
| 366 |
-
toast.success('Conversation exported!');
|
| 367 |
-
} catch (e: any) {
|
| 368 |
-
toast.error(`Export failed: ${e?.message || 'unknown error'}`);
|
| 369 |
-
}
|
| 370 |
-
};
|
| 371 |
-
|
| 372 |
-
const handleSummary = async () => {
|
| 373 |
-
if (!isLoggedIn) {
|
| 374 |
-
toast.error('Please log in first');
|
| 375 |
-
return;
|
| 376 |
-
}
|
| 377 |
-
try {
|
| 378 |
-
const r = await apiPostJson<{ markdown: string }>('/api/summary', {
|
| 379 |
-
user_id: userId,
|
| 380 |
-
learning_mode: learningMode,
|
| 381 |
-
language_preference: language === 'auto' ? 'Auto' : language,
|
| 382 |
-
});
|
| 383 |
-
setExportResult(r.markdown || '');
|
| 384 |
-
setResultType('summary');
|
| 385 |
-
toast.success('Summary generated!');
|
| 386 |
-
} catch (e: any) {
|
| 387 |
-
toast.error(`Summary failed: ${e?.message || 'unknown error'}`);
|
| 388 |
-
}
|
| 389 |
-
};
|
| 390 |
-
|
| 391 |
-
const handleQuiz = () => {
|
| 392 |
-
const quiz = `# Micro-Quiz: Responsible AI
|
| 393 |
-
|
| 394 |
-
1) Which is a key principle of Responsible AI?
|
| 395 |
-
A) Profit maximization
|
| 396 |
-
B) Transparency
|
| 397 |
-
C) Rapid deployment
|
| 398 |
-
D) Cost reduction
|
| 399 |
-
`;
|
| 400 |
-
setExportResult(quiz);
|
| 401 |
-
setResultType('quiz');
|
| 402 |
-
toast.success('Quiz generated!');
|
| 403 |
-
};
|
| 404 |
-
|
| 405 |
-
useEffect(() => {
|
| 406 |
-
if (!isLoggedIn) return;
|
| 407 |
-
|
| 408 |
-
const run = async () => {
|
| 409 |
-
try {
|
| 410 |
-
const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(userId)}`);
|
| 411 |
-
if (!res.ok) return;
|
| 412 |
-
const j = await res.json();
|
| 413 |
-
const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
|
| 414 |
-
if (pct !== null) setMemoryProgress(Math.round(pct * 100));
|
| 415 |
-
} catch {
|
| 416 |
-
// ignore
|
| 417 |
-
}
|
| 418 |
-
};
|
| 419 |
-
run();
|
| 420 |
-
}, [isLoggedIn, userId]);
|
| 421 |
-
|
| 422 |
-
return (
|
| 423 |
-
<div className="min-h-screen bg-background flex flex-col">
|
| 424 |
-
<Toaster />
|
| 425 |
-
<Header
|
| 426 |
-
user={user}
|
| 427 |
-
onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
|
| 428 |
-
onUserClick={() => setRightPanelOpen(!rightPanelOpen)}
|
| 429 |
-
isDarkMode={isDarkMode}
|
| 430 |
-
onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
|
| 431 |
-
/>
|
| 432 |
-
|
| 433 |
-
<div className="flex-1 flex overflow-hidden">
|
| 434 |
-
{leftSidebarOpen && (
|
| 435 |
-
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
|
| 436 |
-
)}
|
| 437 |
-
|
| 438 |
-
<aside
|
| 439 |
-
className={`
|
| 440 |
-
fixed lg:static inset-y-0 left-0 z-50
|
| 441 |
-
w-80 bg-card border-r border-border
|
| 442 |
-
transform transition-transform duration-300 ease-in-out
|
| 443 |
-
${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
| 444 |
-
lg:translate-x-0
|
| 445 |
-
flex flex-col
|
| 446 |
-
mt-16 lg:mt-0
|
| 447 |
-
`}
|
| 448 |
-
>
|
| 449 |
-
<div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
|
| 450 |
-
<h3>Settings & Guide</h3>
|
| 451 |
-
<Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
|
| 452 |
-
<X className="h-5 w-5" />
|
| 453 |
-
</Button>
|
| 454 |
-
</div>
|
| 455 |
-
|
| 456 |
-
<LeftSidebar
|
| 457 |
-
learningMode={learningMode}
|
| 458 |
-
language={language}
|
| 459 |
-
onLearningModeChange={setLearningMode}
|
| 460 |
-
onLanguageChange={setLanguage}
|
| 461 |
-
spaceType={spaceType}
|
| 462 |
-
onSpaceTypeChange={setSpaceType}
|
| 463 |
-
groupMembers={groupMembers}
|
| 464 |
-
/>
|
| 465 |
-
</aside>
|
| 466 |
-
|
| 467 |
-
{/* ✅ 右侧 Panel 展开时,为 Chat 留出空间(避免被遮住导致 FAB 只能在收起时可见) */}
|
| 468 |
-
<main
|
| 469 |
-
className={`
|
| 470 |
-
flex-1 flex flex-col min-w-0
|
| 471 |
-
transition-all
|
| 472 |
-
${rightPanelVisible ? 'pr-[320px]' : ''}
|
| 473 |
-
`}
|
| 474 |
-
>
|
| 475 |
-
<ChatArea
|
| 476 |
-
userId={userId}
|
| 477 |
-
docType={currentDocTypeForChat}
|
| 478 |
-
messages={messages}
|
| 479 |
-
onSendMessage={handleSendMessage}
|
| 480 |
-
uploadedFiles={uploadedFiles}
|
| 481 |
-
onFileUpload={handleFileUpload}
|
| 482 |
-
onRemoveFile={handleRemoveFile}
|
| 483 |
-
onFileTypeChange={handleFileTypeChange}
|
| 484 |
-
onUploadFile={handleUploadSingle}
|
| 485 |
-
onUploadAll={handleUploadAllPending}
|
| 486 |
-
memoryProgress={memoryProgress}
|
| 487 |
-
isLoggedIn={isLoggedIn}
|
| 488 |
-
learningMode={learningMode}
|
| 489 |
-
onClearConversation={handleClearConversation}
|
| 490 |
-
onLearningModeChange={setLearningMode}
|
| 491 |
-
spaceType={spaceType}
|
| 492 |
-
/>
|
| 493 |
-
</main>
|
| 494 |
-
|
| 495 |
-
{rightPanelOpen && (
|
| 496 |
-
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
|
| 497 |
-
)}
|
| 498 |
-
|
| 499 |
-
{rightPanelVisible && (
|
| 500 |
-
<aside
|
| 501 |
-
className={`
|
| 502 |
-
fixed lg:static inset-y-0 right-0 z-50
|
| 503 |
-
w-80 bg-card border-l border-border
|
| 504 |
-
transform transition-transform duration-300 ease-in-out
|
| 505 |
-
${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
|
| 506 |
-
lg:translate-x-0
|
| 507 |
-
flex flex-col
|
| 508 |
-
mt-16 lg:mt-0
|
| 509 |
-
`}
|
| 510 |
-
>
|
| 511 |
-
<div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
|
| 512 |
-
<h3>Account & Actions</h3>
|
| 513 |
-
<Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
|
| 514 |
-
<X className="h-5 w-5" />
|
| 515 |
-
</Button>
|
| 516 |
-
</div>
|
| 517 |
-
|
| 518 |
-
<RightPanel
|
| 519 |
-
user={user}
|
| 520 |
-
onLogin={handleLogin}
|
| 521 |
-
onLogout={handleLogout}
|
| 522 |
-
isLoggedIn={isLoggedIn}
|
| 523 |
-
onClose={() => setRightPanelVisible(false)}
|
| 524 |
-
exportResult={exportResult}
|
| 525 |
-
setExportResult={setExportResult}
|
| 526 |
-
resultType={resultType}
|
| 527 |
-
setResultType={setResultType}
|
| 528 |
-
onExport={handleExport}
|
| 529 |
-
onQuiz={handleQuiz}
|
| 530 |
-
onSummary={handleSummary}
|
| 531 |
-
/>
|
| 532 |
-
</aside>
|
| 533 |
-
)}
|
| 534 |
-
|
| 535 |
-
<Button
|
| 536 |
-
variant="outline"
|
| 537 |
-
size="icon"
|
| 538 |
-
onClick={() => setRightPanelVisible(!rightPanelVisible)}
|
| 539 |
-
className={`hidden lg:flex fixed top-20 z-[70] h-8 w-5 shadow-lg transition-all rounded-l-full rounded-r-none border-r-0 ${
|
| 540 |
-
rightPanelVisible ? 'right-[320px]' : 'right-0'
|
| 541 |
-
}`}
|
| 542 |
-
title={rightPanelVisible ? 'Close panel' : 'Open panel'}
|
| 543 |
-
>
|
| 544 |
-
{rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
| 545 |
-
</Button>
|
| 546 |
-
|
| 547 |
-
{/* ✅ 右侧 Panel 收起时才显示 FloatingActionButtons(你当前逻辑保持不变) */}
|
| 548 |
-
{!rightPanelVisible && (
|
| 549 |
-
<FloatingActionButtons
|
| 550 |
-
user={user}
|
| 551 |
-
isLoggedIn={isLoggedIn}
|
| 552 |
-
onOpenPanel={() => setRightPanelVisible(true)}
|
| 553 |
-
onExport={handleExport}
|
| 554 |
-
onQuiz={handleQuiz}
|
| 555 |
-
onSummary={handleSummary}
|
| 556 |
-
/>
|
| 557 |
-
)}
|
| 558 |
-
</div>
|
| 559 |
-
</div>
|
| 560 |
-
);
|
| 561 |
-
}
|
| 562 |
-
|
| 563 |
-
export default App;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/Attributions.md
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
| 2 |
-
|
| 3 |
-
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/.DS_Store
DELETED
|
Binary file (6.15 kB)
|
|
|
web/src/components/ChatArea.tsx
DELETED
|
@@ -1,282 +0,0 @@
|
|
| 1 |
-
// web/src/components/ChatArea.tsx
|
| 2 |
-
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
| 3 |
-
import { Button } from './ui/button';
|
| 4 |
-
import { Textarea } from './ui/textarea';
|
| 5 |
-
import { Send, ArrowDown, Trash2, Share2 } from 'lucide-react';
|
| 6 |
-
import { Message } from './Message';
|
| 7 |
-
import { FileUploadArea } from './FileUploadArea';
|
| 8 |
-
import { MemoryLine } from './MemoryLine';
|
| 9 |
-
import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App';
|
| 10 |
-
import { toast } from 'sonner';
|
| 11 |
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
|
| 12 |
-
|
| 13 |
-
interface ChatAreaProps {
|
| 14 |
-
messages: MessageType[];
|
| 15 |
-
onSendMessage: (content: string) => void;
|
| 16 |
-
|
| 17 |
-
uploadedFiles: UploadedFile[];
|
| 18 |
-
onFileUpload: (files: File[]) => void;
|
| 19 |
-
onRemoveFile: (index: number) => void;
|
| 20 |
-
onFileTypeChange: (index: number, type: FileType) => void;
|
| 21 |
-
|
| 22 |
-
// ✅ feedback 需要 userId
|
| 23 |
-
userId?: string;
|
| 24 |
-
|
| 25 |
-
// ✅ 由 App.tsx 传入 currentDocTypeForChat
|
| 26 |
-
docType?: string;
|
| 27 |
-
|
| 28 |
-
memoryProgress: number;
|
| 29 |
-
isLoggedIn: boolean;
|
| 30 |
-
learningMode: LearningMode;
|
| 31 |
-
onClearConversation: () => void;
|
| 32 |
-
onLearningModeChange: (mode: LearningMode) => void;
|
| 33 |
-
spaceType: SpaceType;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
export function ChatArea({
|
| 37 |
-
messages,
|
| 38 |
-
onSendMessage,
|
| 39 |
-
uploadedFiles,
|
| 40 |
-
onFileUpload,
|
| 41 |
-
onRemoveFile,
|
| 42 |
-
onFileTypeChange,
|
| 43 |
-
userId,
|
| 44 |
-
docType = 'Other',
|
| 45 |
-
memoryProgress,
|
| 46 |
-
isLoggedIn,
|
| 47 |
-
learningMode,
|
| 48 |
-
onClearConversation,
|
| 49 |
-
onLearningModeChange,
|
| 50 |
-
spaceType,
|
| 51 |
-
}: ChatAreaProps) {
|
| 52 |
-
const [input, setInput] = useState('');
|
| 53 |
-
const [isTyping, setIsTyping] = useState(false);
|
| 54 |
-
const [showScrollButton, setShowScrollButton] = useState(false);
|
| 55 |
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 56 |
-
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
| 57 |
-
|
| 58 |
-
const lastUserMessageContent = useMemo(() => {
|
| 59 |
-
for (let i = messages.length - 1; i >= 0; i--) {
|
| 60 |
-
if (messages[i].role === 'user' && messages[i].content?.trim()) return messages[i].content;
|
| 61 |
-
}
|
| 62 |
-
return '';
|
| 63 |
-
}, [messages]);
|
| 64 |
-
|
| 65 |
-
const scrollToBottom = () => {
|
| 66 |
-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 67 |
-
};
|
| 68 |
-
|
| 69 |
-
useEffect(() => {
|
| 70 |
-
scrollToBottom();
|
| 71 |
-
}, [messages]);
|
| 72 |
-
|
| 73 |
-
useEffect(() => {
|
| 74 |
-
const handleScroll = () => {
|
| 75 |
-
if (!scrollContainerRef.current) return;
|
| 76 |
-
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
| 77 |
-
setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100);
|
| 78 |
-
};
|
| 79 |
-
|
| 80 |
-
const container = scrollContainerRef.current;
|
| 81 |
-
container?.addEventListener('scroll', handleScroll);
|
| 82 |
-
return () => container?.removeEventListener('scroll', handleScroll);
|
| 83 |
-
}, []);
|
| 84 |
-
|
| 85 |
-
const handleSubmit = (e: React.FormEvent) => {
|
| 86 |
-
e.preventDefault();
|
| 87 |
-
if (!input.trim() || !isLoggedIn) return;
|
| 88 |
-
|
| 89 |
-
onSendMessage(input);
|
| 90 |
-
setInput('');
|
| 91 |
-
setIsTyping(true);
|
| 92 |
-
setTimeout(() => setIsTyping(false), 1200);
|
| 93 |
-
};
|
| 94 |
-
|
| 95 |
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 96 |
-
if (e.key === 'Enter' && !e.shiftKey) {
|
| 97 |
-
e.preventDefault();
|
| 98 |
-
handleSubmit(e);
|
| 99 |
-
}
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
const modeLabels: Record<LearningMode, string> = {
|
| 103 |
-
concept: 'Concept Explainer',
|
| 104 |
-
socratic: 'Socratic Tutor',
|
| 105 |
-
exam: 'Exam Prep',
|
| 106 |
-
assignment: 'Assignment Helper',
|
| 107 |
-
summary: 'Quick Summary',
|
| 108 |
-
};
|
| 109 |
-
|
| 110 |
-
const handleClearClick = () => {
|
| 111 |
-
if (messages.length <= 1) {
|
| 112 |
-
toast.info('No conversation to clear');
|
| 113 |
-
return;
|
| 114 |
-
}
|
| 115 |
-
if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) {
|
| 116 |
-
onClearConversation();
|
| 117 |
-
toast.success('Conversation cleared');
|
| 118 |
-
}
|
| 119 |
-
};
|
| 120 |
-
|
| 121 |
-
const handleShareClick = () => {
|
| 122 |
-
if (messages.length <= 1) {
|
| 123 |
-
toast.info('No conversation to share');
|
| 124 |
-
return;
|
| 125 |
-
}
|
| 126 |
-
const conversationText = messages
|
| 127 |
-
.map((msg) => `${msg.role === 'user' ? 'You' : 'Clare'}: ${msg.content}`)
|
| 128 |
-
.join('\n\n');
|
| 129 |
-
|
| 130 |
-
navigator.clipboard
|
| 131 |
-
.writeText(conversationText)
|
| 132 |
-
.then(() => toast.success('Conversation copied to clipboard!'))
|
| 133 |
-
.catch(() => toast.error('Failed to copy conversation'));
|
| 134 |
-
};
|
| 135 |
-
|
| 136 |
-
return (
|
| 137 |
-
<div className="flex flex-col h-full">
|
| 138 |
-
<div className="flex-1 relative border-b-2 border-border">
|
| 139 |
-
{messages.length > 1 && (
|
| 140 |
-
<div className="absolute top-4 right-12 z-10 flex gap-2">
|
| 141 |
-
<Button
|
| 142 |
-
variant="ghost"
|
| 143 |
-
size="sm"
|
| 144 |
-
onClick={handleShareClick}
|
| 145 |
-
disabled={!isLoggedIn}
|
| 146 |
-
className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
|
| 147 |
-
>
|
| 148 |
-
<Share2 className="h-4 w-4" />
|
| 149 |
-
<span className="hidden group-hover:inline">Share</span>
|
| 150 |
-
</Button>
|
| 151 |
-
<Button
|
| 152 |
-
variant="ghost"
|
| 153 |
-
size="sm"
|
| 154 |
-
onClick={handleClearClick}
|
| 155 |
-
disabled={!isLoggedIn}
|
| 156 |
-
className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
|
| 157 |
-
>
|
| 158 |
-
<Trash2 className="h-4 w-4" />
|
| 159 |
-
<span className="hidden group-hover:inline">Clear</span>
|
| 160 |
-
</Button>
|
| 161 |
-
</div>
|
| 162 |
-
)}
|
| 163 |
-
|
| 164 |
-
<div ref={scrollContainerRef} className="h-full max-h-[600px] overflow-y-auto px-4 py-6 pb-36">
|
| 165 |
-
<div className="max-w-4xl mx-auto space-y-6">
|
| 166 |
-
{messages.map((m) => (
|
| 167 |
-
<Message
|
| 168 |
-
key={m.id}
|
| 169 |
-
message={m}
|
| 170 |
-
showSenderInfo={spaceType === 'group'}
|
| 171 |
-
userId={userId}
|
| 172 |
-
isLoggedIn={isLoggedIn}
|
| 173 |
-
learningMode={learningMode}
|
| 174 |
-
docType={docType}
|
| 175 |
-
lastUserText={lastUserMessageContent}
|
| 176 |
-
/>
|
| 177 |
-
))}
|
| 178 |
-
|
| 179 |
-
{isTyping && (
|
| 180 |
-
<div className="flex gap-3">
|
| 181 |
-
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
|
| 182 |
-
<span className="text-white text-sm">C</span>
|
| 183 |
-
</div>
|
| 184 |
-
<div className="bg-muted rounded-2xl px-4 py-3">
|
| 185 |
-
<div className="flex gap-1">
|
| 186 |
-
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '0ms' }} />
|
| 187 |
-
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '150ms' }} />
|
| 188 |
-
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '300ms' }} />
|
| 189 |
-
</div>
|
| 190 |
-
</div>
|
| 191 |
-
</div>
|
| 192 |
-
)}
|
| 193 |
-
|
| 194 |
-
<div ref={messagesEndRef} />
|
| 195 |
-
</div>
|
| 196 |
-
</div>
|
| 197 |
-
|
| 198 |
-
{showScrollButton && (
|
| 199 |
-
<div className="absolute bottom-24 left-1/2 -translate-x-1/2 z-20">
|
| 200 |
-
<Button
|
| 201 |
-
variant="secondary"
|
| 202 |
-
size="icon"
|
| 203 |
-
className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-background"
|
| 204 |
-
onClick={scrollToBottom}
|
| 205 |
-
>
|
| 206 |
-
<ArrowDown className="h-4 w-4" />
|
| 207 |
-
</Button>
|
| 208 |
-
</div>
|
| 209 |
-
)}
|
| 210 |
-
|
| 211 |
-
<div className="absolute bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-10">
|
| 212 |
-
<div className="max-w-4xl mx-auto px-4 py-4">
|
| 213 |
-
<form onSubmit={handleSubmit}>
|
| 214 |
-
<div className="relative">
|
| 215 |
-
<DropdownMenu>
|
| 216 |
-
<DropdownMenuTrigger asChild>
|
| 217 |
-
<Button
|
| 218 |
-
variant="ghost"
|
| 219 |
-
size="sm"
|
| 220 |
-
className="absolute bottom-3 left-2 gap-1.5 h-8 px-2 text-xs z-10 hover:bg-muted/50"
|
| 221 |
-
disabled={!isLoggedIn}
|
| 222 |
-
type="button"
|
| 223 |
-
>
|
| 224 |
-
<span>{modeLabels[learningMode]}</span>
|
| 225 |
-
<svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 226 |
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
| 227 |
-
</svg>
|
| 228 |
-
</Button>
|
| 229 |
-
</DropdownMenuTrigger>
|
| 230 |
-
<DropdownMenuContent align="start" className="w-56">
|
| 231 |
-
{(['concept', 'socratic', 'exam', 'assignment', 'summary'] as LearningMode[]).map((mode) => (
|
| 232 |
-
<DropdownMenuItem
|
| 233 |
-
key={mode}
|
| 234 |
-
onClick={() => onLearningModeChange(mode)}
|
| 235 |
-
className={learningMode === mode ? 'bg-accent' : ''}
|
| 236 |
-
>
|
| 237 |
-
<span className="font-medium">{modeLabels[mode]}</span>
|
| 238 |
-
</DropdownMenuItem>
|
| 239 |
-
))}
|
| 240 |
-
</DropdownMenuContent>
|
| 241 |
-
</DropdownMenu>
|
| 242 |
-
|
| 243 |
-
<Textarea
|
| 244 |
-
value={input}
|
| 245 |
-
onChange={(e) => setInput(e.target.value)}
|
| 246 |
-
onKeyDown={handleKeyDown}
|
| 247 |
-
placeholder={
|
| 248 |
-
isLoggedIn
|
| 249 |
-
? spaceType === 'group'
|
| 250 |
-
? 'Type a message... (mention @Clare to get AI assistance)'
|
| 251 |
-
: 'Ask Clare anything about the course...'
|
| 252 |
-
: 'Please log in on the right to start chatting...'
|
| 253 |
-
}
|
| 254 |
-
disabled={!isLoggedIn}
|
| 255 |
-
className="min-h-[80px] pl-4 pr-12 resize-none bg-background border-2 border-border"
|
| 256 |
-
/>
|
| 257 |
-
<Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="absolute bottom-2 right-2 rounded-full">
|
| 258 |
-
<Send className="h-4 w-4" />
|
| 259 |
-
</Button>
|
| 260 |
-
</div>
|
| 261 |
-
</form>
|
| 262 |
-
</div>
|
| 263 |
-
</div>
|
| 264 |
-
</div>
|
| 265 |
-
|
| 266 |
-
<div className="bg-card">
|
| 267 |
-
<div className="max-w-4xl mx-auto px-4 py-4">
|
| 268 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
| 269 |
-
<FileUploadArea
|
| 270 |
-
uploadedFiles={uploadedFiles}
|
| 271 |
-
onFileUpload={onFileUpload}
|
| 272 |
-
onRemoveFile={onRemoveFile}
|
| 273 |
-
onFileTypeChange={onFileTypeChange}
|
| 274 |
-
disabled={!isLoggedIn}
|
| 275 |
-
/>
|
| 276 |
-
<MemoryLine progress={memoryProgress} />
|
| 277 |
-
</div>
|
| 278 |
-
</div>
|
| 279 |
-
</div>
|
| 280 |
-
</div>
|
| 281 |
-
);
|
| 282 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/FileUploadArea.tsx
DELETED
|
@@ -1,300 +0,0 @@
|
|
| 1 |
-
import React, { useRef, useState } from 'react';
|
| 2 |
-
import { Button } from './ui/button';
|
| 3 |
-
import { Upload, File, X, FileText, Presentation, CloudUpload } from 'lucide-react';
|
| 4 |
-
import { Card } from './ui/card';
|
| 5 |
-
import { Badge } from './ui/badge';
|
| 6 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
| 7 |
-
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
|
| 8 |
-
import type { UploadedFile, FileType } from '../App';
|
| 9 |
-
|
| 10 |
-
interface FileUploadAreaProps {
|
| 11 |
-
uploadedFiles: UploadedFile[];
|
| 12 |
-
onFileUpload: (files: File[]) => void;
|
| 13 |
-
onRemoveFile: (index: number) => void;
|
| 14 |
-
onFileTypeChange: (index: number, type: FileType) => void;
|
| 15 |
-
|
| 16 |
-
// ✅ 新增:真正触发后端上传(App 里实现)
|
| 17 |
-
onUploadFile?: (index: number) => void;
|
| 18 |
-
onUploadAll?: () => void;
|
| 19 |
-
|
| 20 |
-
disabled?: boolean;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
interface PendingFile {
|
| 24 |
-
file: File;
|
| 25 |
-
type: FileType;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
export function FileUploadArea({
|
| 29 |
-
uploadedFiles,
|
| 30 |
-
onFileUpload,
|
| 31 |
-
onRemoveFile,
|
| 32 |
-
onFileTypeChange,
|
| 33 |
-
onUploadFile,
|
| 34 |
-
onUploadAll,
|
| 35 |
-
disabled = false,
|
| 36 |
-
}: FileUploadAreaProps) {
|
| 37 |
-
const [isDragging, setIsDragging] = useState(false);
|
| 38 |
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 39 |
-
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
| 40 |
-
const [showTypeDialog, setShowTypeDialog] = useState(false);
|
| 41 |
-
|
| 42 |
-
const handleDragOver = (e: React.DragEvent) => {
|
| 43 |
-
e.preventDefault();
|
| 44 |
-
if (!disabled) setIsDragging(true);
|
| 45 |
-
};
|
| 46 |
-
|
| 47 |
-
const handleDragLeave = () => {
|
| 48 |
-
setIsDragging(false);
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
const handleDrop = (e: React.DragEvent) => {
|
| 52 |
-
e.preventDefault();
|
| 53 |
-
setIsDragging(false);
|
| 54 |
-
if (disabled) return;
|
| 55 |
-
|
| 56 |
-
const files = Array.from(e.dataTransfer.files).filter((file) =>
|
| 57 |
-
['.pdf', '.docx', '.pptx'].some((ext) => file.name.toLowerCase().endsWith(ext))
|
| 58 |
-
);
|
| 59 |
-
|
| 60 |
-
if (files.length > 0) {
|
| 61 |
-
setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
|
| 62 |
-
setShowTypeDialog(true);
|
| 63 |
-
}
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 67 |
-
const files = Array.from(e.target.files || []);
|
| 68 |
-
if (files.length > 0) {
|
| 69 |
-
setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
|
| 70 |
-
setShowTypeDialog(true);
|
| 71 |
-
}
|
| 72 |
-
e.target.value = '';
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
const handleConfirmUpload = () => {
|
| 76 |
-
// 这里只“入库”,不触发后端上传(符合你现在的逻辑)
|
| 77 |
-
onFileUpload(pendingFiles.map((pf) => pf.file));
|
| 78 |
-
|
| 79 |
-
// 把用户在弹窗里选的 type 同步到父组件列表(通过 index 偏移)
|
| 80 |
-
const startIndex = uploadedFiles.length;
|
| 81 |
-
pendingFiles.forEach((pf, idx) => {
|
| 82 |
-
setTimeout(() => {
|
| 83 |
-
onFileTypeChange(startIndex + idx, pf.type);
|
| 84 |
-
}, 0);
|
| 85 |
-
});
|
| 86 |
-
|
| 87 |
-
setPendingFiles([]);
|
| 88 |
-
setShowTypeDialog(false);
|
| 89 |
-
};
|
| 90 |
-
|
| 91 |
-
const handleCancelUpload = () => {
|
| 92 |
-
setPendingFiles([]);
|
| 93 |
-
setShowTypeDialog(false);
|
| 94 |
-
};
|
| 95 |
-
|
| 96 |
-
const handlePendingFileTypeChange = (index: number, type: FileType) => {
|
| 97 |
-
setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
const getFileIcon = (filename: string) => {
|
| 101 |
-
const lower = filename.toLowerCase();
|
| 102 |
-
if (lower.endsWith('.pdf')) return FileText;
|
| 103 |
-
if (lower.endsWith('.docx')) return File;
|
| 104 |
-
if (lower.endsWith('.pptx')) return Presentation;
|
| 105 |
-
return File;
|
| 106 |
-
};
|
| 107 |
-
|
| 108 |
-
const formatFileSize = (bytes: number) => {
|
| 109 |
-
if (bytes < 1024) return bytes + ' B';
|
| 110 |
-
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
| 111 |
-
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
| 112 |
-
};
|
| 113 |
-
|
| 114 |
-
const hasPendingUploads = uploadedFiles.some((f) => !f.uploaded);
|
| 115 |
-
|
| 116 |
-
return (
|
| 117 |
-
<Card className="p-4 space-y-3">
|
| 118 |
-
<div className="flex items-center justify-between">
|
| 119 |
-
<div className="flex items-center gap-2">
|
| 120 |
-
<h4 className="text-sm">Course Materials</h4>
|
| 121 |
-
{uploadedFiles.length > 0 && <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge>}
|
| 122 |
-
</div>
|
| 123 |
-
|
| 124 |
-
{/* ✅ Upload All Pending(只在存在 pending 且提供了回调时显示) */}
|
| 125 |
-
{uploadedFiles.length > 0 && hasPendingUploads && !!onUploadAll && (
|
| 126 |
-
<Button
|
| 127 |
-
variant="outline"
|
| 128 |
-
size="sm"
|
| 129 |
-
className="h-8 text-xs gap-2"
|
| 130 |
-
disabled={disabled}
|
| 131 |
-
onClick={(e) => {
|
| 132 |
-
e.preventDefault();
|
| 133 |
-
e.stopPropagation();
|
| 134 |
-
onUploadAll();
|
| 135 |
-
}}
|
| 136 |
-
title="Upload all pending files"
|
| 137 |
-
>
|
| 138 |
-
<CloudUpload className="h-4 w-4" />
|
| 139 |
-
Upload All
|
| 140 |
-
</Button>
|
| 141 |
-
)}
|
| 142 |
-
</div>
|
| 143 |
-
|
| 144 |
-
{/* Upload Area */}
|
| 145 |
-
<div
|
| 146 |
-
onDragOver={handleDragOver}
|
| 147 |
-
onDragLeave={handleDragLeave}
|
| 148 |
-
onDrop={handleDrop}
|
| 149 |
-
className={`
|
| 150 |
-
border-2 border-dashed rounded-lg p-4 text-center transition-colors
|
| 151 |
-
${isDragging ? 'border-primary bg-accent' : 'border-border'}
|
| 152 |
-
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
|
| 153 |
-
`}
|
| 154 |
-
onClick={() => !disabled && fileInputRef.current?.click()}
|
| 155 |
-
>
|
| 156 |
-
<Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
|
| 157 |
-
<p className="text-sm text-muted-foreground mb-1">{disabled ? 'Please log in to upload' : 'Drop files or click to upload'}</p>
|
| 158 |
-
<p className="text-xs text-muted-foreground">.pdf, .docx, .pptx</p>
|
| 159 |
-
<input
|
| 160 |
-
ref={fileInputRef}
|
| 161 |
-
type="file"
|
| 162 |
-
multiple
|
| 163 |
-
accept=".pdf,.docx,.pptx"
|
| 164 |
-
onChange={handleFileSelect}
|
| 165 |
-
className="hidden"
|
| 166 |
-
disabled={disabled}
|
| 167 |
-
/>
|
| 168 |
-
</div>
|
| 169 |
-
|
| 170 |
-
{/* Uploaded Files List */}
|
| 171 |
-
{uploadedFiles.length > 0 && (
|
| 172 |
-
<div className="space-y-3 max-h-64 overflow-y-auto">
|
| 173 |
-
{uploadedFiles.map((uploadedFile, index) => {
|
| 174 |
-
const Icon = getFileIcon(uploadedFile.file.name);
|
| 175 |
-
const isUploaded = !!uploadedFile.uploaded;
|
| 176 |
-
|
| 177 |
-
return (
|
| 178 |
-
<div key={index} className="p-3 bg-muted rounded-md space-y-2">
|
| 179 |
-
<div className="flex items-center gap-2 group">
|
| 180 |
-
<Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
| 181 |
-
<div className="flex-1 min-w-0">
|
| 182 |
-
<p className="text-sm truncate">{uploadedFile.file.name}</p>
|
| 183 |
-
<p className="text-xs text-muted-foreground">{formatFileSize(uploadedFile.file.size)}</p>
|
| 184 |
-
</div>
|
| 185 |
-
|
| 186 |
-
{/* ✅ 单文件 Upload(仅未上传时显示 & 必须有回调) */}
|
| 187 |
-
{!isUploaded && !!onUploadFile && (
|
| 188 |
-
<Button
|
| 189 |
-
variant="secondary"
|
| 190 |
-
size="sm"
|
| 191 |
-
className="h-7 text-xs px-2"
|
| 192 |
-
disabled={disabled}
|
| 193 |
-
onClick={(e) => {
|
| 194 |
-
e.preventDefault();
|
| 195 |
-
e.stopPropagation();
|
| 196 |
-
onUploadFile(index);
|
| 197 |
-
}}
|
| 198 |
-
title="Upload this file to backend"
|
| 199 |
-
>
|
| 200 |
-
Upload
|
| 201 |
-
</Button>
|
| 202 |
-
)}
|
| 203 |
-
|
| 204 |
-
{/* ✅ 已上传状态 */}
|
| 205 |
-
{isUploaded && (
|
| 206 |
-
<Badge variant="secondary" className="text-[10px]">
|
| 207 |
-
Uploaded{typeof uploadedFile.uploadedChunks === 'number' ? ` (+${uploadedFile.uploadedChunks})` : ''}
|
| 208 |
-
</Badge>
|
| 209 |
-
)}
|
| 210 |
-
|
| 211 |
-
<Button
|
| 212 |
-
variant="ghost"
|
| 213 |
-
size="icon"
|
| 214 |
-
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
| 215 |
-
onClick={(e) => {
|
| 216 |
-
e.stopPropagation();
|
| 217 |
-
onRemoveFile(index);
|
| 218 |
-
}}
|
| 219 |
-
title="Remove"
|
| 220 |
-
>
|
| 221 |
-
<X className="h-3 w-3" />
|
| 222 |
-
</Button>
|
| 223 |
-
</div>
|
| 224 |
-
|
| 225 |
-
<div className="space-y-1">
|
| 226 |
-
<label className="text-xs text-muted-foreground">File Type</label>
|
| 227 |
-
<Select value={uploadedFile.type} onValueChange={(value) => onFileTypeChange(index, value as FileType)}>
|
| 228 |
-
<SelectTrigger className="h-8 text-xs">
|
| 229 |
-
<SelectValue />
|
| 230 |
-
</SelectTrigger>
|
| 231 |
-
<SelectContent>
|
| 232 |
-
<SelectItem value="syllabus">Syllabus</SelectItem>
|
| 233 |
-
<SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
|
| 234 |
-
<SelectItem value="literature-review">Literature Review / Paper</SelectItem>
|
| 235 |
-
<SelectItem value="other">Other Course Document</SelectItem>
|
| 236 |
-
</SelectContent>
|
| 237 |
-
</Select>
|
| 238 |
-
</div>
|
| 239 |
-
</div>
|
| 240 |
-
);
|
| 241 |
-
})}
|
| 242 |
-
</div>
|
| 243 |
-
)}
|
| 244 |
-
|
| 245 |
-
{/* Type Selection Dialog */}
|
| 246 |
-
{showTypeDialog && (
|
| 247 |
-
<Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
|
| 248 |
-
<DialogContent className="sm:max-w-[425px]">
|
| 249 |
-
<DialogHeader>
|
| 250 |
-
<DialogTitle>Select File Types</DialogTitle>
|
| 251 |
-
<DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
|
| 252 |
-
</DialogHeader>
|
| 253 |
-
|
| 254 |
-
<div className="space-y-3 max-h-64 overflow-y-auto">
|
| 255 |
-
{pendingFiles.map((pendingFile, index) => {
|
| 256 |
-
const Icon = getFileIcon(pendingFile.file.name);
|
| 257 |
-
return (
|
| 258 |
-
<div key={index} className="p-3 bg-muted rounded-md space-y-2">
|
| 259 |
-
<div className="flex items-center gap-2 group">
|
| 260 |
-
<Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
| 261 |
-
<div className="flex-1 min-w-0">
|
| 262 |
-
<p className="text-sm truncate">{pendingFile.file.name}</p>
|
| 263 |
-
<p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
|
| 264 |
-
</div>
|
| 265 |
-
</div>
|
| 266 |
-
|
| 267 |
-
<div className="space-y-1">
|
| 268 |
-
<label className="text-xs text-muted-foreground">File Type</label>
|
| 269 |
-
<Select
|
| 270 |
-
value={pendingFile.type}
|
| 271 |
-
onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
|
| 272 |
-
>
|
| 273 |
-
<SelectTrigger className="h-8 text-xs">
|
| 274 |
-
<SelectValue />
|
| 275 |
-
</SelectTrigger>
|
| 276 |
-
<SelectContent>
|
| 277 |
-
<SelectItem value="syllabus">Syllabus</SelectItem>
|
| 278 |
-
<SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
|
| 279 |
-
<SelectItem value="literature-review">Literature Review / Paper</SelectItem>
|
| 280 |
-
<SelectItem value="other">Other Course Document</SelectItem>
|
| 281 |
-
</SelectContent>
|
| 282 |
-
</Select>
|
| 283 |
-
</div>
|
| 284 |
-
</div>
|
| 285 |
-
);
|
| 286 |
-
})}
|
| 287 |
-
</div>
|
| 288 |
-
|
| 289 |
-
<DialogFooter>
|
| 290 |
-
<Button variant="outline" onClick={handleCancelUpload}>
|
| 291 |
-
Cancel
|
| 292 |
-
</Button>
|
| 293 |
-
<Button onClick={handleConfirmUpload}>Upload</Button>
|
| 294 |
-
</DialogFooter>
|
| 295 |
-
</DialogContent>
|
| 296 |
-
</Dialog>
|
| 297 |
-
)}
|
| 298 |
-
</Card>
|
| 299 |
-
);
|
| 300 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/FloatingActionButtons.tsx
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
import React, { useState } from 'react';
|
| 2 |
-
import { Button } from './ui/button';
|
| 3 |
-
import { Download, ClipboardList, Sparkles } from 'lucide-react';
|
| 4 |
-
import { toast } from 'sonner@2.0.3';
|
| 5 |
-
import type { User } from '../App';
|
| 6 |
-
|
| 7 |
-
interface FloatingActionButtonsProps {
|
| 8 |
-
user: User | null;
|
| 9 |
-
isLoggedIn: boolean;
|
| 10 |
-
onOpenPanel: () => void;
|
| 11 |
-
onExport: () => void;
|
| 12 |
-
onQuiz: () => void;
|
| 13 |
-
onSummary: () => void;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export function FloatingActionButtons({
|
| 17 |
-
user,
|
| 18 |
-
isLoggedIn,
|
| 19 |
-
onOpenPanel,
|
| 20 |
-
onExport,
|
| 21 |
-
onQuiz,
|
| 22 |
-
onSummary,
|
| 23 |
-
}: FloatingActionButtonsProps) {
|
| 24 |
-
const [hoveredButton, setHoveredButton] = useState<string | null>(null);
|
| 25 |
-
|
| 26 |
-
const handleAction = (action: () => void, actionName: string, shouldOpenPanel: boolean = false) => {
|
| 27 |
-
if (!isLoggedIn) {
|
| 28 |
-
toast.error('Please log in to use this feature');
|
| 29 |
-
return;
|
| 30 |
-
}
|
| 31 |
-
action();
|
| 32 |
-
if (shouldOpenPanel) {
|
| 33 |
-
onOpenPanel();
|
| 34 |
-
}
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
const buttons = [
|
| 38 |
-
{
|
| 39 |
-
id: 'export',
|
| 40 |
-
icon: Download,
|
| 41 |
-
label: 'Export Conversation',
|
| 42 |
-
action: onExport,
|
| 43 |
-
openPanel: true, // Open panel for export
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
id: 'quiz',
|
| 47 |
-
icon: ClipboardList,
|
| 48 |
-
label: "Let's Try (Micro-Quiz)",
|
| 49 |
-
action: onQuiz,
|
| 50 |
-
openPanel: false, // Don't open panel for quiz
|
| 51 |
-
},
|
| 52 |
-
{
|
| 53 |
-
id: 'summary',
|
| 54 |
-
icon: Sparkles,
|
| 55 |
-
label: 'Summarization',
|
| 56 |
-
action: onSummary,
|
| 57 |
-
openPanel: true, // Open panel for summary
|
| 58 |
-
},
|
| 59 |
-
];
|
| 60 |
-
|
| 61 |
-
return (
|
| 62 |
-
<div className="fixed right-4 bottom-[28rem] z-40 flex flex-col gap-2">
|
| 63 |
-
{buttons.map((button, index) => {
|
| 64 |
-
const Icon = button.icon;
|
| 65 |
-
const isHovered = hoveredButton === button.id;
|
| 66 |
-
|
| 67 |
-
return (
|
| 68 |
-
<div
|
| 69 |
-
key={button.id}
|
| 70 |
-
className="relative group"
|
| 71 |
-
onMouseEnter={() => setHoveredButton(button.id)}
|
| 72 |
-
onMouseLeave={() => setHoveredButton(null)}
|
| 73 |
-
>
|
| 74 |
-
{/* Tooltip */}
|
| 75 |
-
<div
|
| 76 |
-
className={`
|
| 77 |
-
absolute right-full mr-3 top-1/2 -translate-y-1/2
|
| 78 |
-
px-3 py-2 rounded-lg bg-popover border border-border
|
| 79 |
-
whitespace-nowrap text-sm shadow-lg
|
| 80 |
-
transition-all duration-200
|
| 81 |
-
${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}
|
| 82 |
-
`}
|
| 83 |
-
>
|
| 84 |
-
{button.label}
|
| 85 |
-
</div>
|
| 86 |
-
|
| 87 |
-
{/* Floating Button */}
|
| 88 |
-
<Button
|
| 89 |
-
size="icon"
|
| 90 |
-
className={`
|
| 91 |
-
h-6 w-6 rounded-full shadow-md opacity-60 hover:opacity-100
|
| 92 |
-
transition-all duration-200
|
| 93 |
-
${isLoggedIn
|
| 94 |
-
? 'bg-primary hover:bg-primary/90 text-primary-foreground'
|
| 95 |
-
: 'bg-muted hover:bg-muted/90 text-muted-foreground'
|
| 96 |
-
}
|
| 97 |
-
${isHovered ? 'scale-110' : 'scale-100'}
|
| 98 |
-
`}
|
| 99 |
-
onClick={() => handleAction(button.action, button.label, button.openPanel)}
|
| 100 |
-
style={{
|
| 101 |
-
animationDelay: `${index * 100}ms`,
|
| 102 |
-
}}
|
| 103 |
-
>
|
| 104 |
-
<Icon className="h-3 w-3" />
|
| 105 |
-
</Button>
|
| 106 |
-
</div>
|
| 107 |
-
);
|
| 108 |
-
})}
|
| 109 |
-
</div>
|
| 110 |
-
);
|
| 111 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/GroupMembers.tsx
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Users, Bot } from 'lucide-react';
|
| 3 |
-
import { Badge } from './ui/badge';
|
| 4 |
-
import type { GroupMember } from '../App';
|
| 5 |
-
|
| 6 |
-
interface GroupMembersProps {
|
| 7 |
-
members: GroupMember[];
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export function GroupMembers({ members }: GroupMembersProps) {
|
| 11 |
-
return (
|
| 12 |
-
<div className="space-y-3">
|
| 13 |
-
<div className="flex items-center gap-2">
|
| 14 |
-
<Users className="h-4 w-4 text-muted-foreground" />
|
| 15 |
-
<h3 className="text-sm">Group Members ({members.length})</h3>
|
| 16 |
-
</div>
|
| 17 |
-
|
| 18 |
-
<div className="space-y-2">
|
| 19 |
-
{members.map((member) => (
|
| 20 |
-
<div
|
| 21 |
-
key={member.id}
|
| 22 |
-
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
|
| 23 |
-
>
|
| 24 |
-
{/* Avatar */}
|
| 25 |
-
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
| 26 |
-
member.isAI
|
| 27 |
-
? 'bg-gradient-to-br from-purple-500 to-blue-500'
|
| 28 |
-
: 'bg-muted'
|
| 29 |
-
}`}>
|
| 30 |
-
{member.isAI ? (
|
| 31 |
-
<Bot className="h-4 w-4 text-white" />
|
| 32 |
-
) : (
|
| 33 |
-
<span className="text-sm">
|
| 34 |
-
{member.name.split(' ').map(n => n[0]).join('').toUpperCase()}
|
| 35 |
-
</span>
|
| 36 |
-
)}
|
| 37 |
-
</div>
|
| 38 |
-
|
| 39 |
-
{/* Member Info */}
|
| 40 |
-
<div className="flex-1 min-w-0">
|
| 41 |
-
<div className="flex items-center gap-2">
|
| 42 |
-
<p className="text-sm truncate">{member.name}</p>
|
| 43 |
-
{member.isAI && (
|
| 44 |
-
<Badge variant="secondary" className="text-xs">AI</Badge>
|
| 45 |
-
)}
|
| 46 |
-
</div>
|
| 47 |
-
<p className="text-xs text-muted-foreground truncate">{member.email}</p>
|
| 48 |
-
</div>
|
| 49 |
-
|
| 50 |
-
{/* Online Status */}
|
| 51 |
-
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" title="Online" />
|
| 52 |
-
</div>
|
| 53 |
-
))}
|
| 54 |
-
</div>
|
| 55 |
-
</div>
|
| 56 |
-
);
|
| 57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/Header.tsx
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Menu, User, BookOpen, Moon, Sun } from 'lucide-react';
|
| 3 |
-
import { Button } from './ui/button';
|
| 4 |
-
import { Badge } from './ui/badge';
|
| 5 |
-
import type { User as UserType } from '../App';
|
| 6 |
-
|
| 7 |
-
interface HeaderProps {
|
| 8 |
-
user: UserType | null;
|
| 9 |
-
onMenuClick: () => void;
|
| 10 |
-
onUserClick: () => void;
|
| 11 |
-
isDarkMode: boolean;
|
| 12 |
-
onToggleDarkMode: () => void;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export function Header({ user, onMenuClick, onUserClick, isDarkMode, onToggleDarkMode }: HeaderProps) {
|
| 16 |
-
return (
|
| 17 |
-
<header className="h-16 border-b border-border bg-card px-4 lg:px-6 flex items-center justify-between sticky top-0 z-[60]">
|
| 18 |
-
<div className="flex items-center gap-4">
|
| 19 |
-
<Button
|
| 20 |
-
variant="ghost"
|
| 21 |
-
size="icon"
|
| 22 |
-
className="lg:hidden"
|
| 23 |
-
onClick={onMenuClick}
|
| 24 |
-
>
|
| 25 |
-
<Menu className="h-5 w-5" />
|
| 26 |
-
</Button>
|
| 27 |
-
|
| 28 |
-
<div className="flex items-center gap-3">
|
| 29 |
-
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center">
|
| 30 |
-
<BookOpen className="h-6 w-6 text-white" />
|
| 31 |
-
</div>
|
| 32 |
-
<div>
|
| 33 |
-
<h1 className="text-lg sm:text-xl tracking-tight">
|
| 34 |
-
Clare <span className="text-sm font-bold text-muted-foreground hidden sm:inline ml-2">Your Personalized AI Tutor</span>
|
| 35 |
-
</h1>
|
| 36 |
-
<p className="text-xs text-muted-foreground hidden sm:block">
|
| 37 |
-
Personalized guidance, review, and intelligent reinforcement
|
| 38 |
-
</p>
|
| 39 |
-
</div>
|
| 40 |
-
</div>
|
| 41 |
-
</div>
|
| 42 |
-
|
| 43 |
-
<div className="flex items-center gap-2">
|
| 44 |
-
<Badge variant="secondary" className="hidden md:flex">
|
| 45 |
-
Module 10 – Responsible AI
|
| 46 |
-
</Badge>
|
| 47 |
-
|
| 48 |
-
<Button
|
| 49 |
-
variant="ghost"
|
| 50 |
-
size="icon"
|
| 51 |
-
onClick={onToggleDarkMode}
|
| 52 |
-
aria-label="Toggle dark mode"
|
| 53 |
-
>
|
| 54 |
-
{isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
| 55 |
-
</Button>
|
| 56 |
-
|
| 57 |
-
{user ? (
|
| 58 |
-
<Button
|
| 59 |
-
variant="outline"
|
| 60 |
-
className="gap-2"
|
| 61 |
-
onClick={onUserClick}
|
| 62 |
-
>
|
| 63 |
-
<User className="h-4 w-4" />
|
| 64 |
-
<span className="hidden sm:inline">{user.name}</span>
|
| 65 |
-
</Button>
|
| 66 |
-
) : (
|
| 67 |
-
<Button
|
| 68 |
-
variant="default"
|
| 69 |
-
className="gap-2 lg:hidden"
|
| 70 |
-
onClick={onUserClick}
|
| 71 |
-
>
|
| 72 |
-
<User className="h-4 w-4" />
|
| 73 |
-
<span>Login</span>
|
| 74 |
-
</Button>
|
| 75 |
-
)}
|
| 76 |
-
</div>
|
| 77 |
-
</header>
|
| 78 |
-
);
|
| 79 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/LearningModeSelector.tsx
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Card } from './ui/card';
|
| 3 |
-
import {
|
| 4 |
-
Lightbulb,
|
| 5 |
-
MessageCircleQuestion,
|
| 6 |
-
GraduationCap,
|
| 7 |
-
FileEdit,
|
| 8 |
-
Zap
|
| 9 |
-
} from 'lucide-react';
|
| 10 |
-
import type { LearningMode } from '../App';
|
| 11 |
-
|
| 12 |
-
interface ModeSelectorProps {
|
| 13 |
-
selectedMode: LearningMode;
|
| 14 |
-
onModeChange: (mode: LearningMode) => void;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
const modes = [
|
| 18 |
-
{
|
| 19 |
-
id: 'concept' as LearningMode,
|
| 20 |
-
icon: Lightbulb,
|
| 21 |
-
title: 'Concept Explainer',
|
| 22 |
-
description: 'Break down complex topics',
|
| 23 |
-
color: 'from-blue-500 to-blue-600',
|
| 24 |
-
},
|
| 25 |
-
{
|
| 26 |
-
id: 'socratic' as LearningMode,
|
| 27 |
-
icon: MessageCircleQuestion,
|
| 28 |
-
title: 'Socratic Tutor',
|
| 29 |
-
description: 'Learn through questions',
|
| 30 |
-
color: 'from-purple-500 to-purple-600',
|
| 31 |
-
},
|
| 32 |
-
{
|
| 33 |
-
id: 'exam' as LearningMode,
|
| 34 |
-
icon: GraduationCap,
|
| 35 |
-
title: 'Exam Prep/Quiz',
|
| 36 |
-
description: 'Test your knowledge',
|
| 37 |
-
color: 'from-green-500 to-green-600',
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
id: 'assignment' as LearningMode,
|
| 41 |
-
icon: FileEdit,
|
| 42 |
-
title: 'Assignment Helper',
|
| 43 |
-
description: 'Get homework guidance',
|
| 44 |
-
color: 'from-orange-500 to-orange-600',
|
| 45 |
-
},
|
| 46 |
-
{
|
| 47 |
-
id: 'summary' as LearningMode,
|
| 48 |
-
icon: Zap,
|
| 49 |
-
title: 'Quick Summary',
|
| 50 |
-
description: 'Fast key points review',
|
| 51 |
-
color: 'from-pink-500 to-pink-600',
|
| 52 |
-
},
|
| 53 |
-
];
|
| 54 |
-
|
| 55 |
-
export function LearningModeSelector({ selectedMode, onModeChange }: ModeSelectorProps) {
|
| 56 |
-
return (
|
| 57 |
-
<div className="space-y-2">
|
| 58 |
-
{modes.map((mode) => {
|
| 59 |
-
const Icon = mode.icon;
|
| 60 |
-
const isSelected = selectedMode === mode.id;
|
| 61 |
-
|
| 62 |
-
return (
|
| 63 |
-
<Card
|
| 64 |
-
key={mode.id}
|
| 65 |
-
className={`
|
| 66 |
-
p-3 cursor-pointer transition-all duration-200
|
| 67 |
-
${isSelected
|
| 68 |
-
? 'border-primary bg-accent shadow-sm'
|
| 69 |
-
: 'hover:border-primary/50 hover:shadow-sm'
|
| 70 |
-
}
|
| 71 |
-
`}
|
| 72 |
-
onClick={() => onModeChange(mode.id)}
|
| 73 |
-
>
|
| 74 |
-
<div className="flex items-start gap-3">
|
| 75 |
-
<div className={`
|
| 76 |
-
w-10 h-10 rounded-lg bg-gradient-to-br ${mode.color}
|
| 77 |
-
flex items-center justify-center flex-shrink-0
|
| 78 |
-
`}>
|
| 79 |
-
<Icon className="h-5 w-5 text-white" />
|
| 80 |
-
</div>
|
| 81 |
-
<div className="flex-1 min-w-0">
|
| 82 |
-
<h4 className="text-sm mb-1">{mode.title}</h4>
|
| 83 |
-
<p className="text-xs text-muted-foreground">{mode.description}</p>
|
| 84 |
-
</div>
|
| 85 |
-
{isSelected && (
|
| 86 |
-
<div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-2" />
|
| 87 |
-
)}
|
| 88 |
-
</div>
|
| 89 |
-
</Card>
|
| 90 |
-
);
|
| 91 |
-
})}
|
| 92 |
-
</div>
|
| 93 |
-
);
|
| 94 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/LeftSidebar.tsx
DELETED
|
@@ -1,121 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
| 3 |
-
import { LearningModeSelector } from './LearningModeSelector';
|
| 4 |
-
import { UserGuide } from './UserGuide';
|
| 5 |
-
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
|
| 6 |
-
import { Label } from './ui/label';
|
| 7 |
-
import { Button } from './ui/button';
|
| 8 |
-
import { RotateCcw, Settings, User, Users } from 'lucide-react';
|
| 9 |
-
import { Separator } from './ui/separator';
|
| 10 |
-
import { GroupMembers } from './GroupMembers';
|
| 11 |
-
import type { LearningMode, Language, SpaceType, GroupMember } from '../App';
|
| 12 |
-
|
| 13 |
-
interface LeftSidebarProps {
|
| 14 |
-
learningMode: LearningMode;
|
| 15 |
-
language: Language;
|
| 16 |
-
onLearningModeChange: (mode: LearningMode) => void;
|
| 17 |
-
onLanguageChange: (lang: Language) => void;
|
| 18 |
-
spaceType: SpaceType;
|
| 19 |
-
onSpaceTypeChange: (type: SpaceType) => void;
|
| 20 |
-
groupMembers: GroupMember[];
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
export function LeftSidebar({
|
| 24 |
-
learningMode,
|
| 25 |
-
language,
|
| 26 |
-
onLearningModeChange,
|
| 27 |
-
onLanguageChange,
|
| 28 |
-
spaceType,
|
| 29 |
-
onSpaceTypeChange,
|
| 30 |
-
groupMembers,
|
| 31 |
-
}: LeftSidebarProps) {
|
| 32 |
-
return (
|
| 33 |
-
<div className="flex-1 overflow-auto">
|
| 34 |
-
{/* Space Selector */}
|
| 35 |
-
<div className="p-4 border-b border-border space-y-3">
|
| 36 |
-
<Label>Workspace</Label>
|
| 37 |
-
<RadioGroup value={spaceType} onValueChange={(value) => onSpaceTypeChange(value as SpaceType)}>
|
| 38 |
-
<div className="flex items-center space-x-2 p-2 rounded-lg hover:bg-muted/50 transition-colors">
|
| 39 |
-
<RadioGroupItem value="individual" id="individual" />
|
| 40 |
-
<Label htmlFor="individual" className="cursor-pointer flex items-center gap-2 flex-1">
|
| 41 |
-
<User className="h-4 w-4" />
|
| 42 |
-
Individual Space
|
| 43 |
-
</Label>
|
| 44 |
-
</div>
|
| 45 |
-
<div className="flex items-center space-x-2 p-2 rounded-lg hover:bg-muted/50 transition-colors">
|
| 46 |
-
<RadioGroupItem value="group" id="group" />
|
| 47 |
-
<Label htmlFor="group" className="cursor-pointer flex items-center gap-2 flex-1">
|
| 48 |
-
<Users className="h-4 w-4" />
|
| 49 |
-
Group Space
|
| 50 |
-
</Label>
|
| 51 |
-
</div>
|
| 52 |
-
</RadioGroup>
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
{/* Group Members - Only show in group mode */}
|
| 56 |
-
{spaceType === 'group' && (
|
| 57 |
-
<div className="p-4 border-b border-border">
|
| 58 |
-
<GroupMembers members={groupMembers} />
|
| 59 |
-
</div>
|
| 60 |
-
)}
|
| 61 |
-
|
| 62 |
-
<Tabs defaultValue="settings" className="h-full flex flex-col">
|
| 63 |
-
<div className="px-4 pt-4">
|
| 64 |
-
<TabsList className="grid w-full grid-cols-2">
|
| 65 |
-
<TabsTrigger value="settings">Settings</TabsTrigger>
|
| 66 |
-
<TabsTrigger value="guide">Guide</TabsTrigger>
|
| 67 |
-
</TabsList>
|
| 68 |
-
</div>
|
| 69 |
-
|
| 70 |
-
<TabsContent value="settings" className="flex-1 mt-0 p-4 space-y-6">
|
| 71 |
-
<div className="space-y-4">
|
| 72 |
-
<div>
|
| 73 |
-
<h3>Model Settings</h3>
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
<div className="space-y-2">
|
| 77 |
-
<Label>Model</Label>
|
| 78 |
-
<div className="px-3 py-2 bg-muted rounded-md">
|
| 79 |
-
<code className="text-sm">gpt-4.1-mini</code>
|
| 80 |
-
</div>
|
| 81 |
-
</div>
|
| 82 |
-
|
| 83 |
-
<div className="space-y-2">
|
| 84 |
-
<Label>Language</Label>
|
| 85 |
-
<RadioGroup value={language} onValueChange={(value) => onLanguageChange(value as Language)}>
|
| 86 |
-
<div className="flex items-center space-x-2">
|
| 87 |
-
<RadioGroupItem value="auto" id="auto" />
|
| 88 |
-
<Label htmlFor="auto" className="cursor-pointer">Auto</Label>
|
| 89 |
-
</div>
|
| 90 |
-
<div className="flex items-center space-x-2">
|
| 91 |
-
<RadioGroupItem value="en" id="en" />
|
| 92 |
-
<Label htmlFor="en" className="cursor-pointer">English</Label>
|
| 93 |
-
</div>
|
| 94 |
-
<div className="flex items-center space-x-2">
|
| 95 |
-
<RadioGroupItem value="zh" id="zh" />
|
| 96 |
-
<Label htmlFor="zh" className="cursor-pointer">简体中文</Label>
|
| 97 |
-
</div>
|
| 98 |
-
</RadioGroup>
|
| 99 |
-
</div>
|
| 100 |
-
</div>
|
| 101 |
-
|
| 102 |
-
<Separator />
|
| 103 |
-
|
| 104 |
-
<div className="space-y-3">
|
| 105 |
-
<Button variant="outline" className="w-full gap-2" disabled>
|
| 106 |
-
<Settings className="h-4 w-4" />
|
| 107 |
-
System Settings
|
| 108 |
-
</Button>
|
| 109 |
-
<p className="text-xs text-muted-foreground text-center">
|
| 110 |
-
© 2025 Clare AI Teaching Assistant
|
| 111 |
-
</p>
|
| 112 |
-
</div>
|
| 113 |
-
</TabsContent>
|
| 114 |
-
|
| 115 |
-
<TabsContent value="guide" className="flex-1 mt-0 p-4">
|
| 116 |
-
<UserGuide />
|
| 117 |
-
</TabsContent>
|
| 118 |
-
</Tabs>
|
| 119 |
-
</div>
|
| 120 |
-
);
|
| 121 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/MemoryLine.tsx
DELETED
|
@@ -1,108 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { Card } from './ui/card';
|
| 3 |
-
import { Button } from './ui/button';
|
| 4 |
-
import { Progress } from './ui/progress';
|
| 5 |
-
import { Brain, Calendar, Download, Info } from 'lucide-react';
|
| 6 |
-
import {
|
| 7 |
-
Tooltip,
|
| 8 |
-
TooltipContent,
|
| 9 |
-
TooltipProvider,
|
| 10 |
-
TooltipTrigger,
|
| 11 |
-
} from './ui/tooltip';
|
| 12 |
-
import { Badge } from './ui/badge';
|
| 13 |
-
|
| 14 |
-
interface MemoryLineProps {
|
| 15 |
-
progress: number;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export function MemoryLine({ progress }: MemoryLineProps) {
|
| 19 |
-
const nextReview = progress < 25 ? 'T+7' : progress < 50 ? 'T+14' : progress < 75 ? 'T+30' : 'Complete';
|
| 20 |
-
|
| 21 |
-
return (
|
| 22 |
-
<Card className="p-4 space-y-3">
|
| 23 |
-
<div className="flex items-center justify-between">
|
| 24 |
-
<div className="flex items-center gap-2">
|
| 25 |
-
<Brain className="h-4 w-4 text-purple-500" />
|
| 26 |
-
<h4 className="text-sm">Memory Line</h4>
|
| 27 |
-
<TooltipProvider>
|
| 28 |
-
<Tooltip>
|
| 29 |
-
<TooltipTrigger asChild>
|
| 30 |
-
<Button variant="ghost" size="icon" className="h-5 w-5">
|
| 31 |
-
<Info className="h-3 w-3" />
|
| 32 |
-
</Button>
|
| 33 |
-
</TooltipTrigger>
|
| 34 |
-
<TooltipContent className="max-w-xs">
|
| 35 |
-
<p className="text-sm">
|
| 36 |
-
Track your learning retention using spaced repetition. Regular reviews help solidify understanding!
|
| 37 |
-
</p>
|
| 38 |
-
</TooltipContent>
|
| 39 |
-
</Tooltip>
|
| 40 |
-
</TooltipProvider>
|
| 41 |
-
</div>
|
| 42 |
-
<Badge variant="outline" className="text-xs">
|
| 43 |
-
{progress}%
|
| 44 |
-
</Badge>
|
| 45 |
-
</div>
|
| 46 |
-
|
| 47 |
-
{/* Progress Bar with Milestones */}
|
| 48 |
-
<div className="space-y-2">
|
| 49 |
-
<div className="relative">
|
| 50 |
-
<Progress value={progress} className="h-2" />
|
| 51 |
-
|
| 52 |
-
{/* Milestone Markers */}
|
| 53 |
-
<div className="absolute top-0 left-0 w-full h-2 flex justify-between pointer-events-none">
|
| 54 |
-
{[0, 25, 50, 75, 100].map((milestone) => (
|
| 55 |
-
<div
|
| 56 |
-
key={milestone}
|
| 57 |
-
className={`
|
| 58 |
-
w-3 h-3 rounded-full border-2 -mt-0.5
|
| 59 |
-
${progress >= milestone
|
| 60 |
-
? 'bg-primary border-primary'
|
| 61 |
-
: 'bg-background border-border'
|
| 62 |
-
}
|
| 63 |
-
`}
|
| 64 |
-
/>
|
| 65 |
-
))}
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
-
|
| 69 |
-
{/* Milestone Labels */}
|
| 70 |
-
<div className="flex justify-between text-xs text-muted-foreground">
|
| 71 |
-
<span>T+0</span>
|
| 72 |
-
<span>T+7</span>
|
| 73 |
-
<span>T+14</span>
|
| 74 |
-
<span>T+30</span>
|
| 75 |
-
<span>Done</span>
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
|
| 79 |
-
{/* Actions */}
|
| 80 |
-
<div className="flex items-center gap-2">
|
| 81 |
-
<Button variant="outline" size="sm" className="flex-1 gap-2">
|
| 82 |
-
<Calendar className="h-3 w-3" />
|
| 83 |
-
Next: {nextReview}
|
| 84 |
-
</Button>
|
| 85 |
-
<TooltipProvider>
|
| 86 |
-
<Tooltip>
|
| 87 |
-
<TooltipTrigger asChild>
|
| 88 |
-
<Button variant="ghost" size="sm" className="gap-2">
|
| 89 |
-
<Download className="h-3 w-3" />
|
| 90 |
-
Report
|
| 91 |
-
</Button>
|
| 92 |
-
</TooltipTrigger>
|
| 93 |
-
<TooltipContent>
|
| 94 |
-
<p>Download your learning progress report</p>
|
| 95 |
-
</TooltipContent>
|
| 96 |
-
</Tooltip>
|
| 97 |
-
</TooltipProvider>
|
| 98 |
-
</div>
|
| 99 |
-
|
| 100 |
-
{/* Review Explanation */}
|
| 101 |
-
<div className="pt-2 border-t border-border">
|
| 102 |
-
<p className="text-xs text-muted-foreground">
|
| 103 |
-
Based on spaced repetition for optimal retention
|
| 104 |
-
</p>
|
| 105 |
-
</div>
|
| 106 |
-
</Card>
|
| 107 |
-
);
|
| 108 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/Message.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
// web/src/components/Message.tsx
|
| 2 |
-
import React from "react";
|
| 3 |
-
import ReactMarkdown from "react-markdown";
|
| 4 |
-
import remarkGfm from "remark-gfm";
|
| 5 |
-
|
| 6 |
-
import type { Message as MessageType, LearningMode } from "../App";
|
| 7 |
-
|
| 8 |
-
interface MessageProps {
|
| 9 |
-
message: MessageType;
|
| 10 |
-
|
| 11 |
-
// existing props you are already passing in ChatArea
|
| 12 |
-
showSenderInfo?: boolean;
|
| 13 |
-
userId?: string;
|
| 14 |
-
isLoggedIn: boolean;
|
| 15 |
-
learningMode: LearningMode;
|
| 16 |
-
docType?: string;
|
| 17 |
-
lastUserText?: string;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export function Message({
|
| 21 |
-
message,
|
| 22 |
-
showSenderInfo,
|
| 23 |
-
userId,
|
| 24 |
-
isLoggedIn,
|
| 25 |
-
learningMode,
|
| 26 |
-
docType,
|
| 27 |
-
lastUserText,
|
| 28 |
-
}: MessageProps) {
|
| 29 |
-
const isUser = message.role === "user";
|
| 30 |
-
|
| 31 |
-
// If you already have avatar / sender rendering logic, keep it.
|
| 32 |
-
// The only critical change is content rendering below.
|
| 33 |
-
return (
|
| 34 |
-
<div className={`flex gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
| 35 |
-
{!isUser && (
|
| 36 |
-
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
|
| 37 |
-
<span className="text-white text-sm">C</span>
|
| 38 |
-
</div>
|
| 39 |
-
)}
|
| 40 |
-
|
| 41 |
-
<div className={`max-w-[85%] ${isUser ? "text-right" : "text-left"}`}>
|
| 42 |
-
{/* Optional sender info (group mode) */}
|
| 43 |
-
{showSenderInfo && message.sender && !isUser && (
|
| 44 |
-
<div className="text-xs text-muted-foreground mb-1">
|
| 45 |
-
{message.sender.name}
|
| 46 |
-
</div>
|
| 47 |
-
)}
|
| 48 |
-
|
| 49 |
-
<div
|
| 50 |
-
className={[
|
| 51 |
-
"rounded-2xl px-4 py-3 border",
|
| 52 |
-
isUser
|
| 53 |
-
? "bg-primary text-primary-foreground border-primary/20"
|
| 54 |
-
: "bg-muted text-foreground border-border",
|
| 55 |
-
].join(" ")}
|
| 56 |
-
>
|
| 57 |
-
{/* ✅ THE FIX: render markdown instead of plain text */}
|
| 58 |
-
<ReactMarkdown
|
| 59 |
-
remarkPlugins={[remarkGfm]}
|
| 60 |
-
className={[
|
| 61 |
-
// prose improves markdown typography; max-w-none prevents narrow column
|
| 62 |
-
"prose prose-sm max-w-none",
|
| 63 |
-
// keep readable in chat bubble
|
| 64 |
-
"prose-p:my-2 prose-li:my-1 prose-ul:my-2 prose-ol:my-2",
|
| 65 |
-
// make headings not too large inside bubble
|
| 66 |
-
"prose-h1:text-base prose-h2:text-base prose-h3:text-sm",
|
| 67 |
-
// avoid code blocks overflowing
|
| 68 |
-
"prose-pre:overflow-x-auto",
|
| 69 |
-
// inherit bubble colors
|
| 70 |
-
isUser ? "prose-invert" : "",
|
| 71 |
-
].join(" ")}
|
| 72 |
-
>
|
| 73 |
-
{message.content || ""}
|
| 74 |
-
</ReactMarkdown>
|
| 75 |
-
|
| 76 |
-
{/* If you already render references, keep your original block here */}
|
| 77 |
-
{message.references && message.references.length > 0 && (
|
| 78 |
-
<div className="mt-3 pt-3 border-t border-border/50 text-xs text-muted-foreground space-y-1">
|
| 79 |
-
<div className="font-medium">References</div>
|
| 80 |
-
{message.references.map((r, idx) => (
|
| 81 |
-
<div key={idx} className="truncate">
|
| 82 |
-
{r}
|
| 83 |
-
</div>
|
| 84 |
-
))}
|
| 85 |
-
</div>
|
| 86 |
-
)}
|
| 87 |
-
|
| 88 |
-
{/* If you already have feedback buttons inside Message, keep them here.
|
| 89 |
-
Do not change logic—only keep UI. */}
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
|
| 93 |
-
{isUser && (
|
| 94 |
-
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
| 95 |
-
<span className="text-primary-foreground text-sm">U</span>
|
| 96 |
-
</div>
|
| 97 |
-
)}
|
| 98 |
-
</div>
|
| 99 |
-
);
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/RightPanel.tsx
DELETED
|
@@ -1,307 +0,0 @@
|
|
| 1 |
-
// web/src/components/RightPanel.tsx
|
| 2 |
-
import React, { useState } from 'react';
|
| 3 |
-
import { Button } from './ui/button';
|
| 4 |
-
import { Input } from './ui/input';
|
| 5 |
-
import { Label } from './ui/label';
|
| 6 |
-
import { Card } from './ui/card';
|
| 7 |
-
import { Separator } from './ui/separator';
|
| 8 |
-
import { Textarea } from './ui/textarea';
|
| 9 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
| 10 |
-
import { LogIn, LogOut, FileText, MessageSquare, Download, ClipboardList, Sparkles } from 'lucide-react';
|
| 11 |
-
import type { User } from '../App';
|
| 12 |
-
import { toast } from 'sonner';
|
| 13 |
-
import {
|
| 14 |
-
Dialog,
|
| 15 |
-
DialogContent,
|
| 16 |
-
DialogDescription,
|
| 17 |
-
DialogHeader,
|
| 18 |
-
DialogTitle,
|
| 19 |
-
DialogFooter,
|
| 20 |
-
} from './ui/dialog';
|
| 21 |
-
|
| 22 |
-
// ✅ Markdown 渲染
|
| 23 |
-
import ReactMarkdown from 'react-markdown';
|
| 24 |
-
import remarkGfm from 'remark-gfm';
|
| 25 |
-
|
| 26 |
-
interface RightPanelProps {
|
| 27 |
-
user: User | null;
|
| 28 |
-
onLogin: (name: string, emailOrId: string) => void;
|
| 29 |
-
onLogout: () => void;
|
| 30 |
-
isLoggedIn: boolean;
|
| 31 |
-
onClose?: () => void;
|
| 32 |
-
|
| 33 |
-
exportResult: string;
|
| 34 |
-
setExportResult: (result: string) => void;
|
| 35 |
-
resultType: 'export' | 'quiz' | 'summary' | null;
|
| 36 |
-
setResultType: (type: 'export' | 'quiz' | 'summary' | null) => void;
|
| 37 |
-
|
| 38 |
-
onExport: () => void;
|
| 39 |
-
onQuiz: () => void;
|
| 40 |
-
onSummary: () => void;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
export function RightPanel({
|
| 44 |
-
user,
|
| 45 |
-
onLogin,
|
| 46 |
-
onLogout,
|
| 47 |
-
isLoggedIn,
|
| 48 |
-
exportResult,
|
| 49 |
-
setExportResult,
|
| 50 |
-
resultType,
|
| 51 |
-
setResultType,
|
| 52 |
-
onExport,
|
| 53 |
-
onQuiz,
|
| 54 |
-
onSummary,
|
| 55 |
-
}: RightPanelProps) {
|
| 56 |
-
const [showLoginForm, setShowLoginForm] = useState(false);
|
| 57 |
-
const [name, setName] = useState('');
|
| 58 |
-
const [emailOrId, setEmailOrId] = useState('');
|
| 59 |
-
|
| 60 |
-
const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false);
|
| 61 |
-
const [feedbackText, setFeedbackText] = useState('');
|
| 62 |
-
const [feedbackCategory, setFeedbackCategory] = useState<'general' | 'bug' | 'feature'>('general');
|
| 63 |
-
|
| 64 |
-
const handleLoginClick = () => {
|
| 65 |
-
if (!name.trim() || !emailOrId.trim()) {
|
| 66 |
-
toast.error('Please fill in all fields');
|
| 67 |
-
return;
|
| 68 |
-
}
|
| 69 |
-
onLogin(name.trim(), emailOrId.trim());
|
| 70 |
-
setShowLoginForm(false);
|
| 71 |
-
setName('');
|
| 72 |
-
setEmailOrId('');
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
const handleLogoutClick = () => {
|
| 76 |
-
onLogout();
|
| 77 |
-
setShowLoginForm(false);
|
| 78 |
-
};
|
| 79 |
-
|
| 80 |
-
const handleFeedbackSubmit = () => {
|
| 81 |
-
if (!feedbackText.trim()) {
|
| 82 |
-
toast.error('Please provide feedback text');
|
| 83 |
-
return;
|
| 84 |
-
}
|
| 85 |
-
console.log('Feedback submitted:', feedbackText, feedbackCategory);
|
| 86 |
-
setFeedbackDialogOpen(false);
|
| 87 |
-
setFeedbackText('');
|
| 88 |
-
toast.success('Feedback submitted!');
|
| 89 |
-
};
|
| 90 |
-
|
| 91 |
-
return (
|
| 92 |
-
<div className="flex-1 overflow-auto p-4 space-y-4">
|
| 93 |
-
{/* Account */}
|
| 94 |
-
<Card className="p-4">
|
| 95 |
-
{!isLoggedIn ? (
|
| 96 |
-
<div className="space-y-4">
|
| 97 |
-
<div className="flex flex-col items-center py-4">
|
| 98 |
-
<h3 className="mb-2">Welcome to Clare!</h3>
|
| 99 |
-
<p className="text-sm text-muted-foreground text-center mb-4">
|
| 100 |
-
Log in to start your learning session
|
| 101 |
-
</p>
|
| 102 |
-
</div>
|
| 103 |
-
|
| 104 |
-
{!showLoginForm ? (
|
| 105 |
-
<Button onClick={() => setShowLoginForm(true)} className="w-full gap-2">
|
| 106 |
-
<LogIn className="h-4 w-4" />
|
| 107 |
-
Student Login
|
| 108 |
-
</Button>
|
| 109 |
-
) : (
|
| 110 |
-
<div className="space-y-3">
|
| 111 |
-
<div className="space-y-2">
|
| 112 |
-
<Label htmlFor="name">Name</Label>
|
| 113 |
-
<Input
|
| 114 |
-
id="name"
|
| 115 |
-
value={name}
|
| 116 |
-
onChange={(e) => setName(e.target.value)}
|
| 117 |
-
placeholder="Enter your name"
|
| 118 |
-
/>
|
| 119 |
-
</div>
|
| 120 |
-
<div className="space-y-2">
|
| 121 |
-
<Label htmlFor="emailOrId">Email / Student ID</Label>
|
| 122 |
-
<Input
|
| 123 |
-
id="emailOrId"
|
| 124 |
-
value={emailOrId}
|
| 125 |
-
onChange={(e) => setEmailOrId(e.target.value)}
|
| 126 |
-
placeholder="Enter your email or ID"
|
| 127 |
-
/>
|
| 128 |
-
</div>
|
| 129 |
-
<div className="flex gap-2">
|
| 130 |
-
<Button onClick={handleLoginClick} className="flex-1">
|
| 131 |
-
Enter
|
| 132 |
-
</Button>
|
| 133 |
-
<Button variant="outline" onClick={() => setShowLoginForm(false)}>
|
| 134 |
-
Cancel
|
| 135 |
-
</Button>
|
| 136 |
-
</div>
|
| 137 |
-
</div>
|
| 138 |
-
)}
|
| 139 |
-
</div>
|
| 140 |
-
) : (
|
| 141 |
-
<div className="space-y-4">
|
| 142 |
-
<div className="flex items-center gap-3">
|
| 143 |
-
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white">
|
| 144 |
-
{user?.name?.charAt(0).toUpperCase()}
|
| 145 |
-
</div>
|
| 146 |
-
<div className="flex-1 min-w-0">
|
| 147 |
-
<h4 className="truncate">{user?.name}</h4>
|
| 148 |
-
<p className="text-sm text-muted-foreground truncate">{user?.email}</p>
|
| 149 |
-
<p className="text-xs text-muted-foreground truncate">user_id: {user?.user_id}</p>
|
| 150 |
-
</div>
|
| 151 |
-
</div>
|
| 152 |
-
|
| 153 |
-
<Button variant="destructive" onClick={handleLogoutClick} className="w-full gap-2">
|
| 154 |
-
<LogOut className="h-4 w-4" />
|
| 155 |
-
Log Out
|
| 156 |
-
</Button>
|
| 157 |
-
</div>
|
| 158 |
-
)}
|
| 159 |
-
</Card>
|
| 160 |
-
|
| 161 |
-
{/* Actions (3 buttons in one row) */}
|
| 162 |
-
<Card className="p-4">
|
| 163 |
-
<div className="flex items-center justify-between mb-3">
|
| 164 |
-
<h3 className="text-base font-medium">Actions</h3>
|
| 165 |
-
</div>
|
| 166 |
-
|
| 167 |
-
{/* ✅ 关键点:
|
| 168 |
-
- flex + flex-1 保证同宽
|
| 169 |
-
- min-w-0 防止内容撑开导致换行
|
| 170 |
-
- h-11 / rounded-xl 统一尺寸
|
| 171 |
-
*/}
|
| 172 |
-
<div className="flex gap-2">
|
| 173 |
-
<Button
|
| 174 |
-
variant="outline"
|
| 175 |
-
onClick={onExport}
|
| 176 |
-
disabled={!isLoggedIn}
|
| 177 |
-
title="Export"
|
| 178 |
-
className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
|
| 179 |
-
>
|
| 180 |
-
<Download className="h-4 w-4" />
|
| 181 |
-
</Button>
|
| 182 |
-
|
| 183 |
-
<Button
|
| 184 |
-
variant="outline"
|
| 185 |
-
onClick={onQuiz}
|
| 186 |
-
disabled={!isLoggedIn}
|
| 187 |
-
title="Quiz"
|
| 188 |
-
className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
|
| 189 |
-
>
|
| 190 |
-
<ClipboardList className="h-4 w-4" />
|
| 191 |
-
</Button>
|
| 192 |
-
|
| 193 |
-
<Button
|
| 194 |
-
variant="outline"
|
| 195 |
-
onClick={onSummary}
|
| 196 |
-
disabled={!isLoggedIn}
|
| 197 |
-
title="Summary"
|
| 198 |
-
className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
|
| 199 |
-
>
|
| 200 |
-
<Sparkles className="h-4 w-4" />
|
| 201 |
-
</Button>
|
| 202 |
-
</div>
|
| 203 |
-
</Card>
|
| 204 |
-
|
| 205 |
-
<Separator />
|
| 206 |
-
|
| 207 |
-
{/* Results */}
|
| 208 |
-
<div className="space-y-3">
|
| 209 |
-
<h3>
|
| 210 |
-
{resultType === 'export' && 'Exported Conversation'}
|
| 211 |
-
{resultType === 'quiz' && 'Micro-Quiz'}
|
| 212 |
-
{resultType === 'summary' && 'Summarization'}
|
| 213 |
-
{!resultType && 'Results'}
|
| 214 |
-
</h3>
|
| 215 |
-
|
| 216 |
-
<Card className="p-4 min-h-[200px] bg-muted/30">
|
| 217 |
-
{exportResult ? (
|
| 218 |
-
<div className="space-y-3">
|
| 219 |
-
<div className="flex items-center justify-between">
|
| 220 |
-
<FileText className="h-4 w-4 text-muted-foreground" />
|
| 221 |
-
<Button
|
| 222 |
-
variant="ghost"
|
| 223 |
-
size="sm"
|
| 224 |
-
onClick={() => {
|
| 225 |
-
navigator.clipboard.writeText(exportResult);
|
| 226 |
-
toast.success('Copied to clipboard!');
|
| 227 |
-
}}
|
| 228 |
-
>
|
| 229 |
-
Copy
|
| 230 |
-
</Button>
|
| 231 |
-
</div>
|
| 232 |
-
|
| 233 |
-
{/* ✅ Markdown render (fix **bold**, lists, code blocks, etc.) */}
|
| 234 |
-
<div className="text-sm text-foreground whitespace-pre-wrap">
|
| 235 |
-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
| 236 |
-
{exportResult}
|
| 237 |
-
</ReactMarkdown>
|
| 238 |
-
</div>
|
| 239 |
-
</div>
|
| 240 |
-
) : (
|
| 241 |
-
<div className="flex items-center justify-center h-full text-sm text-muted-foreground text-left">
|
| 242 |
-
Results (export / summary / quiz) will appear here after actions run
|
| 243 |
-
</div>
|
| 244 |
-
)}
|
| 245 |
-
</Card>
|
| 246 |
-
</div>
|
| 247 |
-
|
| 248 |
-
<Separator />
|
| 249 |
-
|
| 250 |
-
{/* Feedback */}
|
| 251 |
-
<div className="space-y-3">
|
| 252 |
-
<h3>Feedback</h3>
|
| 253 |
-
<Button
|
| 254 |
-
variant="outline"
|
| 255 |
-
className="w-full justify-start gap-2"
|
| 256 |
-
onClick={() => setFeedbackDialogOpen(true)}
|
| 257 |
-
>
|
| 258 |
-
<MessageSquare className="h-4 w-4" />
|
| 259 |
-
Provide Feedback
|
| 260 |
-
</Button>
|
| 261 |
-
</div>
|
| 262 |
-
|
| 263 |
-
<Dialog open={feedbackDialogOpen} onOpenChange={setFeedbackDialogOpen}>
|
| 264 |
-
<DialogContent className="sm:max-w-[425px]">
|
| 265 |
-
<DialogHeader>
|
| 266 |
-
<DialogTitle>Provide Feedback</DialogTitle>
|
| 267 |
-
<DialogDescription>Help us improve Clare by sharing your thoughts and suggestions.</DialogDescription>
|
| 268 |
-
</DialogHeader>
|
| 269 |
-
|
| 270 |
-
<div className="space-y-3">
|
| 271 |
-
<div className="space-y-2">
|
| 272 |
-
<Label htmlFor="feedbackCategory">Category</Label>
|
| 273 |
-
<Select value={feedbackCategory} onValueChange={(v) => setFeedbackCategory(v as any)}>
|
| 274 |
-
<SelectTrigger>
|
| 275 |
-
<SelectValue placeholder="Select a category" />
|
| 276 |
-
</SelectTrigger>
|
| 277 |
-
<SelectContent>
|
| 278 |
-
<SelectItem value="general">General Feedback</SelectItem>
|
| 279 |
-
<SelectItem value="bug">Bug Report</SelectItem>
|
| 280 |
-
<SelectItem value="feature">Feature Request</SelectItem>
|
| 281 |
-
</SelectContent>
|
| 282 |
-
</Select>
|
| 283 |
-
</div>
|
| 284 |
-
|
| 285 |
-
<div className="space-y-2">
|
| 286 |
-
<Label htmlFor="feedbackText">Feedback</Label>
|
| 287 |
-
<Textarea
|
| 288 |
-
id="feedbackText"
|
| 289 |
-
value={feedbackText}
|
| 290 |
-
onChange={(e) => setFeedbackText(e.target.value)}
|
| 291 |
-
placeholder="Enter your feedback here..."
|
| 292 |
-
className="min-h-[100px]"
|
| 293 |
-
/>
|
| 294 |
-
</div>
|
| 295 |
-
</div>
|
| 296 |
-
|
| 297 |
-
<DialogFooter>
|
| 298 |
-
<Button variant="outline" onClick={() => setFeedbackDialogOpen(false)}>
|
| 299 |
-
Cancel
|
| 300 |
-
</Button>
|
| 301 |
-
<Button onClick={handleFeedbackSubmit}>Submit</Button>
|
| 302 |
-
</DialogFooter>
|
| 303 |
-
</DialogContent>
|
| 304 |
-
</Dialog>
|
| 305 |
-
</div>
|
| 306 |
-
);
|
| 307 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/UserGuide.tsx
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import {
|
| 3 |
-
Accordion,
|
| 4 |
-
AccordionContent,
|
| 5 |
-
AccordionItem,
|
| 6 |
-
AccordionTrigger,
|
| 7 |
-
} from './ui/accordion';
|
| 8 |
-
|
| 9 |
-
export function UserGuide() {
|
| 10 |
-
return (
|
| 11 |
-
<div className="space-y-3">
|
| 12 |
-
<h3 className="mb-4">User Guide</h3>
|
| 13 |
-
|
| 14 |
-
<Accordion type="single" collapsible className="space-y-2">
|
| 15 |
-
<AccordionItem value="getting-started" className="border rounded-lg px-4">
|
| 16 |
-
<AccordionTrigger>Getting Started</AccordionTrigger>
|
| 17 |
-
<AccordionContent className="text-sm text-muted-foreground space-y-2">
|
| 18 |
-
<p>Welcome to Clare! To begin:</p>
|
| 19 |
-
<ol className="list-decimal list-inside space-y-1 ml-2">
|
| 20 |
-
<li>Log in using your student credentials</li>
|
| 21 |
-
<li>Select your preferred learning mode</li>
|
| 22 |
-
<li>Upload course materials (optional)</li>
|
| 23 |
-
<li>Start asking questions!</li>
|
| 24 |
-
</ol>
|
| 25 |
-
</AccordionContent>
|
| 26 |
-
</AccordionItem>
|
| 27 |
-
|
| 28 |
-
<AccordionItem value="modes" className="border rounded-lg px-4">
|
| 29 |
-
<AccordionTrigger>Learning Modes</AccordionTrigger>
|
| 30 |
-
<AccordionContent className="text-sm text-muted-foreground space-y-2">
|
| 31 |
-
<p><strong>Concept Explainer:</strong> Get detailed explanations of complex topics with examples.</p>
|
| 32 |
-
<p><strong>Socratic Tutor:</strong> Learn through guided questions and discovery.</p>
|
| 33 |
-
<p><strong>Exam Prep/Quiz:</strong> Test your knowledge with practice questions.</p>
|
| 34 |
-
<p><strong>Assignment Helper:</strong> Get step-by-step guidance on homework.</p>
|
| 35 |
-
<p><strong>Quick Summary:</strong> Receive concise summaries of key concepts.</p>
|
| 36 |
-
</AccordionContent>
|
| 37 |
-
</AccordionItem>
|
| 38 |
-
|
| 39 |
-
<AccordionItem value="how-it-works" className="border rounded-lg px-4">
|
| 40 |
-
<AccordionTrigger>How Clare Works</AccordionTrigger>
|
| 41 |
-
<AccordionContent className="text-sm text-muted-foreground space-y-2">
|
| 42 |
-
<p>Clare uses advanced AI to provide personalized tutoring:</p>
|
| 43 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 44 |
-
<li>Analyzes your questions and learning style</li>
|
| 45 |
-
<li>References uploaded course materials</li>
|
| 46 |
-
<li>Adapts explanations to your level</li>
|
| 47 |
-
<li>Tracks your learning progress over time</li>
|
| 48 |
-
</ul>
|
| 49 |
-
</AccordionContent>
|
| 50 |
-
</AccordionItem>
|
| 51 |
-
|
| 52 |
-
<AccordionItem value="memory-line" className="border rounded-lg px-4">
|
| 53 |
-
<AccordionTrigger>Memory Line</AccordionTrigger>
|
| 54 |
-
<AccordionContent className="text-sm text-muted-foreground space-y-2">
|
| 55 |
-
<p>The Memory Line tracks your retention using spaced repetition:</p>
|
| 56 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 57 |
-
<li><strong>T+0:</strong> Initial learning</li>
|
| 58 |
-
<li><strong>T+7:</strong> First review (1 week)</li>
|
| 59 |
-
<li><strong>T+14:</strong> Second review (2 weeks)</li>
|
| 60 |
-
<li><strong>T+30:</strong> Final review (1 month)</li>
|
| 61 |
-
</ul>
|
| 62 |
-
<p>Regular reviews help solidify your understanding!</p>
|
| 63 |
-
</AccordionContent>
|
| 64 |
-
</AccordionItem>
|
| 65 |
-
|
| 66 |
-
<AccordionItem value="progress" className="border rounded-lg px-4">
|
| 67 |
-
<AccordionTrigger>Learning Progress Report</AccordionTrigger>
|
| 68 |
-
<AccordionContent className="text-sm text-muted-foreground">
|
| 69 |
-
<p>Your progress report shows:</p>
|
| 70 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 71 |
-
<li>Topics covered and mastered</li>
|
| 72 |
-
<li>Time spent learning</li>
|
| 73 |
-
<li>Quiz scores and performance</li>
|
| 74 |
-
<li>Recommended review topics</li>
|
| 75 |
-
</ul>
|
| 76 |
-
</AccordionContent>
|
| 77 |
-
</AccordionItem>
|
| 78 |
-
|
| 79 |
-
<AccordionItem value="files" className="border rounded-lg px-4">
|
| 80 |
-
<AccordionTrigger>Using Your Files</AccordionTrigger>
|
| 81 |
-
<AccordionContent className="text-sm text-muted-foreground">
|
| 82 |
-
<p>Upload course materials to enhance Clare's responses:</p>
|
| 83 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 84 |
-
<li>Supported formats: .docx, .pdf, .pptx</li>
|
| 85 |
-
<li>Clare will reference your materials in answers</li>
|
| 86 |
-
<li>Files are processed securely and privately</li>
|
| 87 |
-
<li>You can remove files anytime</li>
|
| 88 |
-
</ul>
|
| 89 |
-
</AccordionContent>
|
| 90 |
-
</AccordionItem>
|
| 91 |
-
|
| 92 |
-
<AccordionItem value="quiz" className="border rounded-lg px-4">
|
| 93 |
-
<AccordionTrigger>Micro-Quiz</AccordionTrigger>
|
| 94 |
-
<AccordionContent className="text-sm text-muted-foreground">
|
| 95 |
-
<p>Test your understanding with quick quizzes:</p>
|
| 96 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 97 |
-
<li>Generated based on your conversation</li>
|
| 98 |
-
<li>Multiple choice and short answer questions</li>
|
| 99 |
-
<li>Instant feedback and explanations</li>
|
| 100 |
-
<li>Track your quiz performance over time</li>
|
| 101 |
-
</ul>
|
| 102 |
-
</AccordionContent>
|
| 103 |
-
</AccordionItem>
|
| 104 |
-
|
| 105 |
-
<AccordionItem value="summarization" className="border rounded-lg px-4">
|
| 106 |
-
<AccordionTrigger>Summarization</AccordionTrigger>
|
| 107 |
-
<AccordionContent className="text-sm text-muted-foreground">
|
| 108 |
-
<p>Get concise summaries of your learning session:</p>
|
| 109 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 110 |
-
<li>Key concepts discussed</li>
|
| 111 |
-
<li>Important takeaways</li>
|
| 112 |
-
<li>Topics to review</li>
|
| 113 |
-
<li>Next steps for learning</li>
|
| 114 |
-
</ul>
|
| 115 |
-
</AccordionContent>
|
| 116 |
-
</AccordionItem>
|
| 117 |
-
|
| 118 |
-
<AccordionItem value="export" className="border rounded-lg px-4">
|
| 119 |
-
<AccordionTrigger>Export Conversation</AccordionTrigger>
|
| 120 |
-
<AccordionContent className="text-sm text-muted-foreground">
|
| 121 |
-
<p>Save your conversation for later review:</p>
|
| 122 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 123 |
-
<li>Export as PDF or text file</li>
|
| 124 |
-
<li>Includes all messages and references</li>
|
| 125 |
-
<li>Perfect for study notes</li>
|
| 126 |
-
<li>Share with study groups (optional)</li>
|
| 127 |
-
</ul>
|
| 128 |
-
</AccordionContent>
|
| 129 |
-
</AccordionItem>
|
| 130 |
-
|
| 131 |
-
<AccordionItem value="faq" className="border rounded-lg px-4">
|
| 132 |
-
<AccordionTrigger>FAQ</AccordionTrigger>
|
| 133 |
-
<AccordionContent className="text-sm text-muted-foreground space-y-3">
|
| 134 |
-
<div>
|
| 135 |
-
<p className="font-medium text-foreground">Is my data private?</p>
|
| 136 |
-
<p>Yes! Your conversations and files are encrypted and never shared.</p>
|
| 137 |
-
</div>
|
| 138 |
-
<div>
|
| 139 |
-
<p className="font-medium text-foreground">Can Clare do my homework?</p>
|
| 140 |
-
<p>Clare is designed to help you learn, not do work for you. It provides guidance and explanations to help you understand concepts.</p>
|
| 141 |
-
</div>
|
| 142 |
-
<div>
|
| 143 |
-
<p className="font-medium text-foreground">How accurate is Clare?</p>
|
| 144 |
-
<p>Clare is highly accurate but should be used as a learning aid. Always verify important information with course materials.</p>
|
| 145 |
-
</div>
|
| 146 |
-
</AccordionContent>
|
| 147 |
-
</AccordionItem>
|
| 148 |
-
</Accordion>
|
| 149 |
-
</div>
|
| 150 |
-
);
|
| 151 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/figma/ImageWithFallback.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import React, { useState } from 'react'
|
| 2 |
-
|
| 3 |
-
const ERROR_IMG_SRC =
|
| 4 |
-
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
|
| 5 |
-
|
| 6 |
-
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
| 7 |
-
const [didError, setDidError] = useState(false)
|
| 8 |
-
|
| 9 |
-
const handleError = () => {
|
| 10 |
-
setDidError(true)
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
const { src, alt, style, className, ...rest } = props
|
| 14 |
-
|
| 15 |
-
return didError ? (
|
| 16 |
-
<div
|
| 17 |
-
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
| 18 |
-
style={style}
|
| 19 |
-
>
|
| 20 |
-
<div className="flex items-center justify-center w-full h-full">
|
| 21 |
-
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
) : (
|
| 25 |
-
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
|
| 26 |
-
)
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/accordion.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
|
| 5 |
-
import { ChevronDownIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function Accordion({
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
| 12 |
-
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
function AccordionItem({
|
| 16 |
-
className,
|
| 17 |
-
...props
|
| 18 |
-
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
| 19 |
-
return (
|
| 20 |
-
<AccordionPrimitive.Item
|
| 21 |
-
data-slot="accordion-item"
|
| 22 |
-
className={cn("border-b last:border-b-0", className)}
|
| 23 |
-
{...props}
|
| 24 |
-
/>
|
| 25 |
-
);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
function AccordionTrigger({
|
| 29 |
-
className,
|
| 30 |
-
children,
|
| 31 |
-
...props
|
| 32 |
-
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
| 33 |
-
return (
|
| 34 |
-
<AccordionPrimitive.Header className="flex">
|
| 35 |
-
<AccordionPrimitive.Trigger
|
| 36 |
-
data-slot="accordion-trigger"
|
| 37 |
-
className={cn(
|
| 38 |
-
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
| 39 |
-
className,
|
| 40 |
-
)}
|
| 41 |
-
{...props}
|
| 42 |
-
>
|
| 43 |
-
{children}
|
| 44 |
-
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
| 45 |
-
</AccordionPrimitive.Trigger>
|
| 46 |
-
</AccordionPrimitive.Header>
|
| 47 |
-
);
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function AccordionContent({
|
| 51 |
-
className,
|
| 52 |
-
children,
|
| 53 |
-
...props
|
| 54 |
-
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
| 55 |
-
return (
|
| 56 |
-
<AccordionPrimitive.Content
|
| 57 |
-
data-slot="accordion-content"
|
| 58 |
-
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
| 59 |
-
{...props}
|
| 60 |
-
>
|
| 61 |
-
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
| 62 |
-
</AccordionPrimitive.Content>
|
| 63 |
-
);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/alert-dialog.tsx
DELETED
|
@@ -1,157 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
import { buttonVariants } from "./button";
|
| 8 |
-
|
| 9 |
-
function AlertDialog({
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
| 12 |
-
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
function AlertDialogTrigger({
|
| 16 |
-
...props
|
| 17 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
| 18 |
-
return (
|
| 19 |
-
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
| 20 |
-
);
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function AlertDialogPortal({
|
| 24 |
-
...props
|
| 25 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
| 26 |
-
return (
|
| 27 |
-
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
| 28 |
-
);
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function AlertDialogOverlay({
|
| 32 |
-
className,
|
| 33 |
-
...props
|
| 34 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
| 35 |
-
return (
|
| 36 |
-
<AlertDialogPrimitive.Overlay
|
| 37 |
-
data-slot="alert-dialog-overlay"
|
| 38 |
-
className={cn(
|
| 39 |
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
| 40 |
-
className,
|
| 41 |
-
)}
|
| 42 |
-
{...props}
|
| 43 |
-
/>
|
| 44 |
-
);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
function AlertDialogContent({
|
| 48 |
-
className,
|
| 49 |
-
...props
|
| 50 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
| 51 |
-
return (
|
| 52 |
-
<AlertDialogPortal>
|
| 53 |
-
<AlertDialogOverlay />
|
| 54 |
-
<AlertDialogPrimitive.Content
|
| 55 |
-
data-slot="alert-dialog-content"
|
| 56 |
-
className={cn(
|
| 57 |
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
| 58 |
-
className,
|
| 59 |
-
)}
|
| 60 |
-
{...props}
|
| 61 |
-
/>
|
| 62 |
-
</AlertDialogPortal>
|
| 63 |
-
);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
function AlertDialogHeader({
|
| 67 |
-
className,
|
| 68 |
-
...props
|
| 69 |
-
}: React.ComponentProps<"div">) {
|
| 70 |
-
return (
|
| 71 |
-
<div
|
| 72 |
-
data-slot="alert-dialog-header"
|
| 73 |
-
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
| 74 |
-
{...props}
|
| 75 |
-
/>
|
| 76 |
-
);
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
function AlertDialogFooter({
|
| 80 |
-
className,
|
| 81 |
-
...props
|
| 82 |
-
}: React.ComponentProps<"div">) {
|
| 83 |
-
return (
|
| 84 |
-
<div
|
| 85 |
-
data-slot="alert-dialog-footer"
|
| 86 |
-
className={cn(
|
| 87 |
-
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
| 88 |
-
className,
|
| 89 |
-
)}
|
| 90 |
-
{...props}
|
| 91 |
-
/>
|
| 92 |
-
);
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
function AlertDialogTitle({
|
| 96 |
-
className,
|
| 97 |
-
...props
|
| 98 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
| 99 |
-
return (
|
| 100 |
-
<AlertDialogPrimitive.Title
|
| 101 |
-
data-slot="alert-dialog-title"
|
| 102 |
-
className={cn("text-lg font-semibold", className)}
|
| 103 |
-
{...props}
|
| 104 |
-
/>
|
| 105 |
-
);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
function AlertDialogDescription({
|
| 109 |
-
className,
|
| 110 |
-
...props
|
| 111 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
| 112 |
-
return (
|
| 113 |
-
<AlertDialogPrimitive.Description
|
| 114 |
-
data-slot="alert-dialog-description"
|
| 115 |
-
className={cn("text-muted-foreground text-sm", className)}
|
| 116 |
-
{...props}
|
| 117 |
-
/>
|
| 118 |
-
);
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
function AlertDialogAction({
|
| 122 |
-
className,
|
| 123 |
-
...props
|
| 124 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
| 125 |
-
return (
|
| 126 |
-
<AlertDialogPrimitive.Action
|
| 127 |
-
className={cn(buttonVariants(), className)}
|
| 128 |
-
{...props}
|
| 129 |
-
/>
|
| 130 |
-
);
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
function AlertDialogCancel({
|
| 134 |
-
className,
|
| 135 |
-
...props
|
| 136 |
-
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
| 137 |
-
return (
|
| 138 |
-
<AlertDialogPrimitive.Cancel
|
| 139 |
-
className={cn(buttonVariants({ variant: "outline" }), className)}
|
| 140 |
-
{...props}
|
| 141 |
-
/>
|
| 142 |
-
);
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
export {
|
| 146 |
-
AlertDialog,
|
| 147 |
-
AlertDialogPortal,
|
| 148 |
-
AlertDialogOverlay,
|
| 149 |
-
AlertDialogTrigger,
|
| 150 |
-
AlertDialogContent,
|
| 151 |
-
AlertDialogHeader,
|
| 152 |
-
AlertDialogFooter,
|
| 153 |
-
AlertDialogTitle,
|
| 154 |
-
AlertDialogDescription,
|
| 155 |
-
AlertDialogAction,
|
| 156 |
-
AlertDialogCancel,
|
| 157 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/alert.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
| 3 |
-
|
| 4 |
-
import { cn } from "./utils";
|
| 5 |
-
|
| 6 |
-
const alertVariants = cva(
|
| 7 |
-
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
| 8 |
-
{
|
| 9 |
-
variants: {
|
| 10 |
-
variant: {
|
| 11 |
-
default: "bg-card text-card-foreground",
|
| 12 |
-
destructive:
|
| 13 |
-
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
| 14 |
-
},
|
| 15 |
-
},
|
| 16 |
-
defaultVariants: {
|
| 17 |
-
variant: "default",
|
| 18 |
-
},
|
| 19 |
-
},
|
| 20 |
-
);
|
| 21 |
-
|
| 22 |
-
function Alert({
|
| 23 |
-
className,
|
| 24 |
-
variant,
|
| 25 |
-
...props
|
| 26 |
-
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
| 27 |
-
return (
|
| 28 |
-
<div
|
| 29 |
-
data-slot="alert"
|
| 30 |
-
role="alert"
|
| 31 |
-
className={cn(alertVariants({ variant }), className)}
|
| 32 |
-
{...props}
|
| 33 |
-
/>
|
| 34 |
-
);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 38 |
-
return (
|
| 39 |
-
<div
|
| 40 |
-
data-slot="alert-title"
|
| 41 |
-
className={cn(
|
| 42 |
-
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
| 43 |
-
className,
|
| 44 |
-
)}
|
| 45 |
-
{...props}
|
| 46 |
-
/>
|
| 47 |
-
);
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function AlertDescription({
|
| 51 |
-
className,
|
| 52 |
-
...props
|
| 53 |
-
}: React.ComponentProps<"div">) {
|
| 54 |
-
return (
|
| 55 |
-
<div
|
| 56 |
-
data-slot="alert-description"
|
| 57 |
-
className={cn(
|
| 58 |
-
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
| 59 |
-
className,
|
| 60 |
-
)}
|
| 61 |
-
{...props}
|
| 62 |
-
/>
|
| 63 |
-
);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
export { Alert, AlertTitle, AlertDescription };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/aspect-ratio.tsx
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
|
| 4 |
-
|
| 5 |
-
function AspectRatio({
|
| 6 |
-
...props
|
| 7 |
-
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
| 8 |
-
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
export { AspectRatio };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/avatar.tsx
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function Avatar({
|
| 9 |
-
className,
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
| 12 |
-
return (
|
| 13 |
-
<AvatarPrimitive.Root
|
| 14 |
-
data-slot="avatar"
|
| 15 |
-
className={cn(
|
| 16 |
-
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
|
| 17 |
-
className,
|
| 18 |
-
)}
|
| 19 |
-
{...props}
|
| 20 |
-
/>
|
| 21 |
-
);
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
function AvatarImage({
|
| 25 |
-
className,
|
| 26 |
-
...props
|
| 27 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
| 28 |
-
return (
|
| 29 |
-
<AvatarPrimitive.Image
|
| 30 |
-
data-slot="avatar-image"
|
| 31 |
-
className={cn("aspect-square size-full", className)}
|
| 32 |
-
{...props}
|
| 33 |
-
/>
|
| 34 |
-
);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function AvatarFallback({
|
| 38 |
-
className,
|
| 39 |
-
...props
|
| 40 |
-
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
| 41 |
-
return (
|
| 42 |
-
<AvatarPrimitive.Fallback
|
| 43 |
-
data-slot="avatar-fallback"
|
| 44 |
-
className={cn(
|
| 45 |
-
"bg-muted flex size-full items-center justify-center rounded-full",
|
| 46 |
-
className,
|
| 47 |
-
)}
|
| 48 |
-
{...props}
|
| 49 |
-
/>
|
| 50 |
-
);
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
export { Avatar, AvatarImage, AvatarFallback };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/badge.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
| 3 |
-
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
| 4 |
-
|
| 5 |
-
import { cn } from "./utils";
|
| 6 |
-
|
| 7 |
-
const badgeVariants = cva(
|
| 8 |
-
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
| 9 |
-
{
|
| 10 |
-
variants: {
|
| 11 |
-
variant: {
|
| 12 |
-
default:
|
| 13 |
-
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
| 14 |
-
secondary:
|
| 15 |
-
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
| 16 |
-
destructive:
|
| 17 |
-
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
| 18 |
-
outline:
|
| 19 |
-
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
| 20 |
-
},
|
| 21 |
-
},
|
| 22 |
-
defaultVariants: {
|
| 23 |
-
variant: "default",
|
| 24 |
-
},
|
| 25 |
-
},
|
| 26 |
-
);
|
| 27 |
-
|
| 28 |
-
function Badge({
|
| 29 |
-
className,
|
| 30 |
-
variant,
|
| 31 |
-
asChild = false,
|
| 32 |
-
...props
|
| 33 |
-
}: React.ComponentProps<"span"> &
|
| 34 |
-
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
| 35 |
-
const Comp = asChild ? Slot : "span";
|
| 36 |
-
|
| 37 |
-
return (
|
| 38 |
-
<Comp
|
| 39 |
-
data-slot="badge"
|
| 40 |
-
className={cn(badgeVariants({ variant }), className)}
|
| 41 |
-
{...props}
|
| 42 |
-
/>
|
| 43 |
-
);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
export { Badge, badgeVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/breadcrumb.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
| 3 |
-
import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
|
| 4 |
-
|
| 5 |
-
import { cn } from "./utils";
|
| 6 |
-
|
| 7 |
-
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
| 8 |
-
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
| 12 |
-
return (
|
| 13 |
-
<ol
|
| 14 |
-
data-slot="breadcrumb-list"
|
| 15 |
-
className={cn(
|
| 16 |
-
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
| 17 |
-
className,
|
| 18 |
-
)}
|
| 19 |
-
{...props}
|
| 20 |
-
/>
|
| 21 |
-
);
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
| 25 |
-
return (
|
| 26 |
-
<li
|
| 27 |
-
data-slot="breadcrumb-item"
|
| 28 |
-
className={cn("inline-flex items-center gap-1.5", className)}
|
| 29 |
-
{...props}
|
| 30 |
-
/>
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function BreadcrumbLink({
|
| 35 |
-
asChild,
|
| 36 |
-
className,
|
| 37 |
-
...props
|
| 38 |
-
}: React.ComponentProps<"a"> & {
|
| 39 |
-
asChild?: boolean;
|
| 40 |
-
}) {
|
| 41 |
-
const Comp = asChild ? Slot : "a";
|
| 42 |
-
|
| 43 |
-
return (
|
| 44 |
-
<Comp
|
| 45 |
-
data-slot="breadcrumb-link"
|
| 46 |
-
className={cn("hover:text-foreground transition-colors", className)}
|
| 47 |
-
{...props}
|
| 48 |
-
/>
|
| 49 |
-
);
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
| 53 |
-
return (
|
| 54 |
-
<span
|
| 55 |
-
data-slot="breadcrumb-page"
|
| 56 |
-
role="link"
|
| 57 |
-
aria-disabled="true"
|
| 58 |
-
aria-current="page"
|
| 59 |
-
className={cn("text-foreground font-normal", className)}
|
| 60 |
-
{...props}
|
| 61 |
-
/>
|
| 62 |
-
);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
function BreadcrumbSeparator({
|
| 66 |
-
children,
|
| 67 |
-
className,
|
| 68 |
-
...props
|
| 69 |
-
}: React.ComponentProps<"li">) {
|
| 70 |
-
return (
|
| 71 |
-
<li
|
| 72 |
-
data-slot="breadcrumb-separator"
|
| 73 |
-
role="presentation"
|
| 74 |
-
aria-hidden="true"
|
| 75 |
-
className={cn("[&>svg]:size-3.5", className)}
|
| 76 |
-
{...props}
|
| 77 |
-
>
|
| 78 |
-
{children ?? <ChevronRight />}
|
| 79 |
-
</li>
|
| 80 |
-
);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
function BreadcrumbEllipsis({
|
| 84 |
-
className,
|
| 85 |
-
...props
|
| 86 |
-
}: React.ComponentProps<"span">) {
|
| 87 |
-
return (
|
| 88 |
-
<span
|
| 89 |
-
data-slot="breadcrumb-ellipsis"
|
| 90 |
-
role="presentation"
|
| 91 |
-
aria-hidden="true"
|
| 92 |
-
className={cn("flex size-9 items-center justify-center", className)}
|
| 93 |
-
{...props}
|
| 94 |
-
>
|
| 95 |
-
<MoreHorizontal className="size-4" />
|
| 96 |
-
<span className="sr-only">More</span>
|
| 97 |
-
</span>
|
| 98 |
-
);
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
export {
|
| 102 |
-
Breadcrumb,
|
| 103 |
-
BreadcrumbList,
|
| 104 |
-
BreadcrumbItem,
|
| 105 |
-
BreadcrumbLink,
|
| 106 |
-
BreadcrumbPage,
|
| 107 |
-
BreadcrumbSeparator,
|
| 108 |
-
BreadcrumbEllipsis,
|
| 109 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/button.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
| 3 |
-
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
| 4 |
-
|
| 5 |
-
import { cn } from "./utils";
|
| 6 |
-
|
| 7 |
-
const buttonVariants = cva(
|
| 8 |
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 9 |
-
{
|
| 10 |
-
variants: {
|
| 11 |
-
variant: {
|
| 12 |
-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
| 13 |
-
destructive:
|
| 14 |
-
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
| 15 |
-
outline:
|
| 16 |
-
"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
| 17 |
-
secondary:
|
| 18 |
-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 19 |
-
ghost:
|
| 20 |
-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
| 21 |
-
link: "text-primary underline-offset-4 hover:underline",
|
| 22 |
-
},
|
| 23 |
-
size: {
|
| 24 |
-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
| 25 |
-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
| 26 |
-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
| 27 |
-
icon: "size-9 rounded-md",
|
| 28 |
-
},
|
| 29 |
-
},
|
| 30 |
-
defaultVariants: {
|
| 31 |
-
variant: "default",
|
| 32 |
-
size: "default",
|
| 33 |
-
},
|
| 34 |
-
},
|
| 35 |
-
);
|
| 36 |
-
|
| 37 |
-
const Button = React.forwardRef<
|
| 38 |
-
HTMLButtonElement,
|
| 39 |
-
React.ComponentProps<"button"> &
|
| 40 |
-
VariantProps<typeof buttonVariants> & {
|
| 41 |
-
asChild?: boolean;
|
| 42 |
-
}
|
| 43 |
-
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 44 |
-
const Comp = asChild ? Slot : "button";
|
| 45 |
-
|
| 46 |
-
return (
|
| 47 |
-
<Comp
|
| 48 |
-
data-slot="button"
|
| 49 |
-
className={cn(buttonVariants({ variant, size, className }))}
|
| 50 |
-
ref={ref}
|
| 51 |
-
{...props}
|
| 52 |
-
/>
|
| 53 |
-
);
|
| 54 |
-
});
|
| 55 |
-
|
| 56 |
-
Button.displayName = "Button";
|
| 57 |
-
|
| 58 |
-
export { Button, buttonVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/calendar.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import { ChevronLeft, ChevronRight } from "lucide-react@0.487.0";
|
| 5 |
-
import { DayPicker } from "react-day-picker@8.10.1";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
import { buttonVariants } from "./button";
|
| 9 |
-
|
| 10 |
-
function Calendar({
|
| 11 |
-
className,
|
| 12 |
-
classNames,
|
| 13 |
-
showOutsideDays = true,
|
| 14 |
-
...props
|
| 15 |
-
}: React.ComponentProps<typeof DayPicker>) {
|
| 16 |
-
return (
|
| 17 |
-
<DayPicker
|
| 18 |
-
showOutsideDays={showOutsideDays}
|
| 19 |
-
className={cn("p-3", className)}
|
| 20 |
-
classNames={{
|
| 21 |
-
months: "flex flex-col sm:flex-row gap-2",
|
| 22 |
-
month: "flex flex-col gap-4",
|
| 23 |
-
caption: "flex justify-center pt-1 relative items-center w-full",
|
| 24 |
-
caption_label: "text-sm font-medium",
|
| 25 |
-
nav: "flex items-center gap-1",
|
| 26 |
-
nav_button: cn(
|
| 27 |
-
buttonVariants({ variant: "outline" }),
|
| 28 |
-
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
| 29 |
-
),
|
| 30 |
-
nav_button_previous: "absolute left-1",
|
| 31 |
-
nav_button_next: "absolute right-1",
|
| 32 |
-
table: "w-full border-collapse space-x-1",
|
| 33 |
-
head_row: "flex",
|
| 34 |
-
head_cell:
|
| 35 |
-
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
| 36 |
-
row: "flex w-full mt-2",
|
| 37 |
-
cell: cn(
|
| 38 |
-
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
| 39 |
-
props.mode === "range"
|
| 40 |
-
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
| 41 |
-
: "[&:has([aria-selected])]:rounded-md",
|
| 42 |
-
),
|
| 43 |
-
day: cn(
|
| 44 |
-
buttonVariants({ variant: "ghost" }),
|
| 45 |
-
"size-8 p-0 font-normal aria-selected:opacity-100",
|
| 46 |
-
),
|
| 47 |
-
day_range_start:
|
| 48 |
-
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
| 49 |
-
day_range_end:
|
| 50 |
-
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
| 51 |
-
day_selected:
|
| 52 |
-
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
| 53 |
-
day_today: "bg-accent text-accent-foreground",
|
| 54 |
-
day_outside:
|
| 55 |
-
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
| 56 |
-
day_disabled: "text-muted-foreground opacity-50",
|
| 57 |
-
day_range_middle:
|
| 58 |
-
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
| 59 |
-
day_hidden: "invisible",
|
| 60 |
-
...classNames,
|
| 61 |
-
}}
|
| 62 |
-
components={{
|
| 63 |
-
IconLeft: ({ className, ...props }) => (
|
| 64 |
-
<ChevronLeft className={cn("size-4", className)} {...props} />
|
| 65 |
-
),
|
| 66 |
-
IconRight: ({ className, ...props }) => (
|
| 67 |
-
<ChevronRight className={cn("size-4", className)} {...props} />
|
| 68 |
-
),
|
| 69 |
-
}}
|
| 70 |
-
{...props}
|
| 71 |
-
/>
|
| 72 |
-
);
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
export { Calendar };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/card.tsx
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
|
| 3 |
-
import { cn } from "./utils";
|
| 4 |
-
|
| 5 |
-
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
| 6 |
-
return (
|
| 7 |
-
<div
|
| 8 |
-
data-slot="card"
|
| 9 |
-
className={cn(
|
| 10 |
-
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
|
| 11 |
-
className,
|
| 12 |
-
)}
|
| 13 |
-
{...props}
|
| 14 |
-
/>
|
| 15 |
-
);
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 19 |
-
return (
|
| 20 |
-
<div
|
| 21 |
-
data-slot="card-header"
|
| 22 |
-
className={cn(
|
| 23 |
-
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
| 24 |
-
className,
|
| 25 |
-
)}
|
| 26 |
-
{...props}
|
| 27 |
-
/>
|
| 28 |
-
);
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 32 |
-
return (
|
| 33 |
-
<h4
|
| 34 |
-
data-slot="card-title"
|
| 35 |
-
className={cn("leading-none", className)}
|
| 36 |
-
{...props}
|
| 37 |
-
/>
|
| 38 |
-
);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
| 42 |
-
return (
|
| 43 |
-
<p
|
| 44 |
-
data-slot="card-description"
|
| 45 |
-
className={cn("text-muted-foreground", className)}
|
| 46 |
-
{...props}
|
| 47 |
-
/>
|
| 48 |
-
);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
| 52 |
-
return (
|
| 53 |
-
<div
|
| 54 |
-
data-slot="card-action"
|
| 55 |
-
className={cn(
|
| 56 |
-
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
| 57 |
-
className,
|
| 58 |
-
)}
|
| 59 |
-
{...props}
|
| 60 |
-
/>
|
| 61 |
-
);
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
| 65 |
-
return (
|
| 66 |
-
<div
|
| 67 |
-
data-slot="card-content"
|
| 68 |
-
className={cn("px-6 [&:last-child]:pb-6", className)}
|
| 69 |
-
{...props}
|
| 70 |
-
/>
|
| 71 |
-
);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 75 |
-
return (
|
| 76 |
-
<div
|
| 77 |
-
data-slot="card-footer"
|
| 78 |
-
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
|
| 79 |
-
{...props}
|
| 80 |
-
/>
|
| 81 |
-
);
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
export {
|
| 85 |
-
Card,
|
| 86 |
-
CardHeader,
|
| 87 |
-
CardFooter,
|
| 88 |
-
CardTitle,
|
| 89 |
-
CardAction,
|
| 90 |
-
CardDescription,
|
| 91 |
-
CardContent,
|
| 92 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/carousel.tsx
DELETED
|
@@ -1,241 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import useEmblaCarousel, {
|
| 5 |
-
type UseEmblaCarouselType,
|
| 6 |
-
} from "embla-carousel-react@8.6.0";
|
| 7 |
-
import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
|
| 8 |
-
|
| 9 |
-
import { cn } from "./utils";
|
| 10 |
-
import { Button } from "./button";
|
| 11 |
-
|
| 12 |
-
type CarouselApi = UseEmblaCarouselType[1];
|
| 13 |
-
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
| 14 |
-
type CarouselOptions = UseCarouselParameters[0];
|
| 15 |
-
type CarouselPlugin = UseCarouselParameters[1];
|
| 16 |
-
|
| 17 |
-
type CarouselProps = {
|
| 18 |
-
opts?: CarouselOptions;
|
| 19 |
-
plugins?: CarouselPlugin;
|
| 20 |
-
orientation?: "horizontal" | "vertical";
|
| 21 |
-
setApi?: (api: CarouselApi) => void;
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
type CarouselContextProps = {
|
| 25 |
-
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
| 26 |
-
api: ReturnType<typeof useEmblaCarousel>[1];
|
| 27 |
-
scrollPrev: () => void;
|
| 28 |
-
scrollNext: () => void;
|
| 29 |
-
canScrollPrev: boolean;
|
| 30 |
-
canScrollNext: boolean;
|
| 31 |
-
} & CarouselProps;
|
| 32 |
-
|
| 33 |
-
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
| 34 |
-
|
| 35 |
-
function useCarousel() {
|
| 36 |
-
const context = React.useContext(CarouselContext);
|
| 37 |
-
|
| 38 |
-
if (!context) {
|
| 39 |
-
throw new Error("useCarousel must be used within a <Carousel />");
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
return context;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
function Carousel({
|
| 46 |
-
orientation = "horizontal",
|
| 47 |
-
opts,
|
| 48 |
-
setApi,
|
| 49 |
-
plugins,
|
| 50 |
-
className,
|
| 51 |
-
children,
|
| 52 |
-
...props
|
| 53 |
-
}: React.ComponentProps<"div"> & CarouselProps) {
|
| 54 |
-
const [carouselRef, api] = useEmblaCarousel(
|
| 55 |
-
{
|
| 56 |
-
...opts,
|
| 57 |
-
axis: orientation === "horizontal" ? "x" : "y",
|
| 58 |
-
},
|
| 59 |
-
plugins,
|
| 60 |
-
);
|
| 61 |
-
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
| 62 |
-
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
| 63 |
-
|
| 64 |
-
const onSelect = React.useCallback((api: CarouselApi) => {
|
| 65 |
-
if (!api) return;
|
| 66 |
-
setCanScrollPrev(api.canScrollPrev());
|
| 67 |
-
setCanScrollNext(api.canScrollNext());
|
| 68 |
-
}, []);
|
| 69 |
-
|
| 70 |
-
const scrollPrev = React.useCallback(() => {
|
| 71 |
-
api?.scrollPrev();
|
| 72 |
-
}, [api]);
|
| 73 |
-
|
| 74 |
-
const scrollNext = React.useCallback(() => {
|
| 75 |
-
api?.scrollNext();
|
| 76 |
-
}, [api]);
|
| 77 |
-
|
| 78 |
-
const handleKeyDown = React.useCallback(
|
| 79 |
-
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
| 80 |
-
if (event.key === "ArrowLeft") {
|
| 81 |
-
event.preventDefault();
|
| 82 |
-
scrollPrev();
|
| 83 |
-
} else if (event.key === "ArrowRight") {
|
| 84 |
-
event.preventDefault();
|
| 85 |
-
scrollNext();
|
| 86 |
-
}
|
| 87 |
-
},
|
| 88 |
-
[scrollPrev, scrollNext],
|
| 89 |
-
);
|
| 90 |
-
|
| 91 |
-
React.useEffect(() => {
|
| 92 |
-
if (!api || !setApi) return;
|
| 93 |
-
setApi(api);
|
| 94 |
-
}, [api, setApi]);
|
| 95 |
-
|
| 96 |
-
React.useEffect(() => {
|
| 97 |
-
if (!api) return;
|
| 98 |
-
onSelect(api);
|
| 99 |
-
api.on("reInit", onSelect);
|
| 100 |
-
api.on("select", onSelect);
|
| 101 |
-
|
| 102 |
-
return () => {
|
| 103 |
-
api?.off("select", onSelect);
|
| 104 |
-
};
|
| 105 |
-
}, [api, onSelect]);
|
| 106 |
-
|
| 107 |
-
return (
|
| 108 |
-
<CarouselContext.Provider
|
| 109 |
-
value={{
|
| 110 |
-
carouselRef,
|
| 111 |
-
api: api,
|
| 112 |
-
opts,
|
| 113 |
-
orientation:
|
| 114 |
-
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
| 115 |
-
scrollPrev,
|
| 116 |
-
scrollNext,
|
| 117 |
-
canScrollPrev,
|
| 118 |
-
canScrollNext,
|
| 119 |
-
}}
|
| 120 |
-
>
|
| 121 |
-
<div
|
| 122 |
-
onKeyDownCapture={handleKeyDown}
|
| 123 |
-
className={cn("relative", className)}
|
| 124 |
-
role="region"
|
| 125 |
-
aria-roledescription="carousel"
|
| 126 |
-
data-slot="carousel"
|
| 127 |
-
{...props}
|
| 128 |
-
>
|
| 129 |
-
{children}
|
| 130 |
-
</div>
|
| 131 |
-
</CarouselContext.Provider>
|
| 132 |
-
);
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
| 136 |
-
const { carouselRef, orientation } = useCarousel();
|
| 137 |
-
|
| 138 |
-
return (
|
| 139 |
-
<div
|
| 140 |
-
ref={carouselRef}
|
| 141 |
-
className="overflow-hidden"
|
| 142 |
-
data-slot="carousel-content"
|
| 143 |
-
>
|
| 144 |
-
<div
|
| 145 |
-
className={cn(
|
| 146 |
-
"flex",
|
| 147 |
-
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
| 148 |
-
className,
|
| 149 |
-
)}
|
| 150 |
-
{...props}
|
| 151 |
-
/>
|
| 152 |
-
</div>
|
| 153 |
-
);
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
| 157 |
-
const { orientation } = useCarousel();
|
| 158 |
-
|
| 159 |
-
return (
|
| 160 |
-
<div
|
| 161 |
-
role="group"
|
| 162 |
-
aria-roledescription="slide"
|
| 163 |
-
data-slot="carousel-item"
|
| 164 |
-
className={cn(
|
| 165 |
-
"min-w-0 shrink-0 grow-0 basis-full",
|
| 166 |
-
orientation === "horizontal" ? "pl-4" : "pt-4",
|
| 167 |
-
className,
|
| 168 |
-
)}
|
| 169 |
-
{...props}
|
| 170 |
-
/>
|
| 171 |
-
);
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
function CarouselPrevious({
|
| 175 |
-
className,
|
| 176 |
-
variant = "outline",
|
| 177 |
-
size = "icon",
|
| 178 |
-
...props
|
| 179 |
-
}: React.ComponentProps<typeof Button>) {
|
| 180 |
-
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
| 181 |
-
|
| 182 |
-
return (
|
| 183 |
-
<Button
|
| 184 |
-
data-slot="carousel-previous"
|
| 185 |
-
variant={variant}
|
| 186 |
-
size={size}
|
| 187 |
-
className={cn(
|
| 188 |
-
"absolute size-8 rounded-full",
|
| 189 |
-
orientation === "horizontal"
|
| 190 |
-
? "top-1/2 -left-12 -translate-y-1/2"
|
| 191 |
-
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
| 192 |
-
className,
|
| 193 |
-
)}
|
| 194 |
-
disabled={!canScrollPrev}
|
| 195 |
-
onClick={scrollPrev}
|
| 196 |
-
{...props}
|
| 197 |
-
>
|
| 198 |
-
<ArrowLeft />
|
| 199 |
-
<span className="sr-only">Previous slide</span>
|
| 200 |
-
</Button>
|
| 201 |
-
);
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
function CarouselNext({
|
| 205 |
-
className,
|
| 206 |
-
variant = "outline",
|
| 207 |
-
size = "icon",
|
| 208 |
-
...props
|
| 209 |
-
}: React.ComponentProps<typeof Button>) {
|
| 210 |
-
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
| 211 |
-
|
| 212 |
-
return (
|
| 213 |
-
<Button
|
| 214 |
-
data-slot="carousel-next"
|
| 215 |
-
variant={variant}
|
| 216 |
-
size={size}
|
| 217 |
-
className={cn(
|
| 218 |
-
"absolute size-8 rounded-full",
|
| 219 |
-
orientation === "horizontal"
|
| 220 |
-
? "top-1/2 -right-12 -translate-y-1/2"
|
| 221 |
-
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
| 222 |
-
className,
|
| 223 |
-
)}
|
| 224 |
-
disabled={!canScrollNext}
|
| 225 |
-
onClick={scrollNext}
|
| 226 |
-
{...props}
|
| 227 |
-
>
|
| 228 |
-
<ArrowRight />
|
| 229 |
-
<span className="sr-only">Next slide</span>
|
| 230 |
-
</Button>
|
| 231 |
-
);
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
export {
|
| 235 |
-
type CarouselApi,
|
| 236 |
-
Carousel,
|
| 237 |
-
CarouselContent,
|
| 238 |
-
CarouselItem,
|
| 239 |
-
CarouselPrevious,
|
| 240 |
-
CarouselNext,
|
| 241 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/chart.tsx
DELETED
|
@@ -1,353 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as RechartsPrimitive from "recharts@2.15.2";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
// Format: { THEME_NAME: CSS_SELECTOR }
|
| 9 |
-
const THEMES = { light: "", dark: ".dark" } as const;
|
| 10 |
-
|
| 11 |
-
export type ChartConfig = {
|
| 12 |
-
[k in string]: {
|
| 13 |
-
label?: React.ReactNode;
|
| 14 |
-
icon?: React.ComponentType;
|
| 15 |
-
} & (
|
| 16 |
-
| { color?: string; theme?: never }
|
| 17 |
-
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
| 18 |
-
);
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
type ChartContextProps = {
|
| 22 |
-
config: ChartConfig;
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
| 26 |
-
|
| 27 |
-
function useChart() {
|
| 28 |
-
const context = React.useContext(ChartContext);
|
| 29 |
-
|
| 30 |
-
if (!context) {
|
| 31 |
-
throw new Error("useChart must be used within a <ChartContainer />");
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
return context;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function ChartContainer({
|
| 38 |
-
id,
|
| 39 |
-
className,
|
| 40 |
-
children,
|
| 41 |
-
config,
|
| 42 |
-
...props
|
| 43 |
-
}: React.ComponentProps<"div"> & {
|
| 44 |
-
config: ChartConfig;
|
| 45 |
-
children: React.ComponentProps<
|
| 46 |
-
typeof RechartsPrimitive.ResponsiveContainer
|
| 47 |
-
>["children"];
|
| 48 |
-
}) {
|
| 49 |
-
const uniqueId = React.useId();
|
| 50 |
-
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
| 51 |
-
|
| 52 |
-
return (
|
| 53 |
-
<ChartContext.Provider value={{ config }}>
|
| 54 |
-
<div
|
| 55 |
-
data-slot="chart"
|
| 56 |
-
data-chart={chartId}
|
| 57 |
-
className={cn(
|
| 58 |
-
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
| 59 |
-
className,
|
| 60 |
-
)}
|
| 61 |
-
{...props}
|
| 62 |
-
>
|
| 63 |
-
<ChartStyle id={chartId} config={config} />
|
| 64 |
-
<RechartsPrimitive.ResponsiveContainer>
|
| 65 |
-
{children}
|
| 66 |
-
</RechartsPrimitive.ResponsiveContainer>
|
| 67 |
-
</div>
|
| 68 |
-
</ChartContext.Provider>
|
| 69 |
-
);
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
| 73 |
-
const colorConfig = Object.entries(config).filter(
|
| 74 |
-
([, config]) => config.theme || config.color,
|
| 75 |
-
);
|
| 76 |
-
|
| 77 |
-
if (!colorConfig.length) {
|
| 78 |
-
return null;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
return (
|
| 82 |
-
<style
|
| 83 |
-
dangerouslySetInnerHTML={{
|
| 84 |
-
__html: Object.entries(THEMES)
|
| 85 |
-
.map(
|
| 86 |
-
([theme, prefix]) => `
|
| 87 |
-
${prefix} [data-chart=${id}] {
|
| 88 |
-
${colorConfig
|
| 89 |
-
.map(([key, itemConfig]) => {
|
| 90 |
-
const color =
|
| 91 |
-
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
| 92 |
-
itemConfig.color;
|
| 93 |
-
return color ? ` --color-${key}: ${color};` : null;
|
| 94 |
-
})
|
| 95 |
-
.join("\n")}
|
| 96 |
-
}
|
| 97 |
-
`,
|
| 98 |
-
)
|
| 99 |
-
.join("\n"),
|
| 100 |
-
}}
|
| 101 |
-
/>
|
| 102 |
-
);
|
| 103 |
-
};
|
| 104 |
-
|
| 105 |
-
const ChartTooltip = RechartsPrimitive.Tooltip;
|
| 106 |
-
|
| 107 |
-
function ChartTooltipContent({
|
| 108 |
-
active,
|
| 109 |
-
payload,
|
| 110 |
-
className,
|
| 111 |
-
indicator = "dot",
|
| 112 |
-
hideLabel = false,
|
| 113 |
-
hideIndicator = false,
|
| 114 |
-
label,
|
| 115 |
-
labelFormatter,
|
| 116 |
-
labelClassName,
|
| 117 |
-
formatter,
|
| 118 |
-
color,
|
| 119 |
-
nameKey,
|
| 120 |
-
labelKey,
|
| 121 |
-
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
| 122 |
-
React.ComponentProps<"div"> & {
|
| 123 |
-
hideLabel?: boolean;
|
| 124 |
-
hideIndicator?: boolean;
|
| 125 |
-
indicator?: "line" | "dot" | "dashed";
|
| 126 |
-
nameKey?: string;
|
| 127 |
-
labelKey?: string;
|
| 128 |
-
}) {
|
| 129 |
-
const { config } = useChart();
|
| 130 |
-
|
| 131 |
-
const tooltipLabel = React.useMemo(() => {
|
| 132 |
-
if (hideLabel || !payload?.length) {
|
| 133 |
-
return null;
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
const [item] = payload;
|
| 137 |
-
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
| 138 |
-
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
| 139 |
-
const value =
|
| 140 |
-
!labelKey && typeof label === "string"
|
| 141 |
-
? config[label as keyof typeof config]?.label || label
|
| 142 |
-
: itemConfig?.label;
|
| 143 |
-
|
| 144 |
-
if (labelFormatter) {
|
| 145 |
-
return (
|
| 146 |
-
<div className={cn("font-medium", labelClassName)}>
|
| 147 |
-
{labelFormatter(value, payload)}
|
| 148 |
-
</div>
|
| 149 |
-
);
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
if (!value) {
|
| 153 |
-
return null;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
| 157 |
-
}, [
|
| 158 |
-
label,
|
| 159 |
-
labelFormatter,
|
| 160 |
-
payload,
|
| 161 |
-
hideLabel,
|
| 162 |
-
labelClassName,
|
| 163 |
-
config,
|
| 164 |
-
labelKey,
|
| 165 |
-
]);
|
| 166 |
-
|
| 167 |
-
if (!active || !payload?.length) {
|
| 168 |
-
return null;
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
const nestLabel = payload.length === 1 && indicator !== "dot";
|
| 172 |
-
|
| 173 |
-
return (
|
| 174 |
-
<div
|
| 175 |
-
className={cn(
|
| 176 |
-
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
| 177 |
-
className,
|
| 178 |
-
)}
|
| 179 |
-
>
|
| 180 |
-
{!nestLabel ? tooltipLabel : null}
|
| 181 |
-
<div className="grid gap-1.5">
|
| 182 |
-
{payload.map((item, index) => {
|
| 183 |
-
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
| 184 |
-
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
| 185 |
-
const indicatorColor = color || item.payload.fill || item.color;
|
| 186 |
-
|
| 187 |
-
return (
|
| 188 |
-
<div
|
| 189 |
-
key={item.dataKey}
|
| 190 |
-
className={cn(
|
| 191 |
-
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
| 192 |
-
indicator === "dot" && "items-center",
|
| 193 |
-
)}
|
| 194 |
-
>
|
| 195 |
-
{formatter && item?.value !== undefined && item.name ? (
|
| 196 |
-
formatter(item.value, item.name, item, index, item.payload)
|
| 197 |
-
) : (
|
| 198 |
-
<>
|
| 199 |
-
{itemConfig?.icon ? (
|
| 200 |
-
<itemConfig.icon />
|
| 201 |
-
) : (
|
| 202 |
-
!hideIndicator && (
|
| 203 |
-
<div
|
| 204 |
-
className={cn(
|
| 205 |
-
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
| 206 |
-
{
|
| 207 |
-
"h-2.5 w-2.5": indicator === "dot",
|
| 208 |
-
"w-1": indicator === "line",
|
| 209 |
-
"w-0 border-[1.5px] border-dashed bg-transparent":
|
| 210 |
-
indicator === "dashed",
|
| 211 |
-
"my-0.5": nestLabel && indicator === "dashed",
|
| 212 |
-
},
|
| 213 |
-
)}
|
| 214 |
-
style={
|
| 215 |
-
{
|
| 216 |
-
"--color-bg": indicatorColor,
|
| 217 |
-
"--color-border": indicatorColor,
|
| 218 |
-
} as React.CSSProperties
|
| 219 |
-
}
|
| 220 |
-
/>
|
| 221 |
-
)
|
| 222 |
-
)}
|
| 223 |
-
<div
|
| 224 |
-
className={cn(
|
| 225 |
-
"flex flex-1 justify-between leading-none",
|
| 226 |
-
nestLabel ? "items-end" : "items-center",
|
| 227 |
-
)}
|
| 228 |
-
>
|
| 229 |
-
<div className="grid gap-1.5">
|
| 230 |
-
{nestLabel ? tooltipLabel : null}
|
| 231 |
-
<span className="text-muted-foreground">
|
| 232 |
-
{itemConfig?.label || item.name}
|
| 233 |
-
</span>
|
| 234 |
-
</div>
|
| 235 |
-
{item.value && (
|
| 236 |
-
<span className="text-foreground font-mono font-medium tabular-nums">
|
| 237 |
-
{item.value.toLocaleString()}
|
| 238 |
-
</span>
|
| 239 |
-
)}
|
| 240 |
-
</div>
|
| 241 |
-
</>
|
| 242 |
-
)}
|
| 243 |
-
</div>
|
| 244 |
-
);
|
| 245 |
-
})}
|
| 246 |
-
</div>
|
| 247 |
-
</div>
|
| 248 |
-
);
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
const ChartLegend = RechartsPrimitive.Legend;
|
| 252 |
-
|
| 253 |
-
function ChartLegendContent({
|
| 254 |
-
className,
|
| 255 |
-
hideIcon = false,
|
| 256 |
-
payload,
|
| 257 |
-
verticalAlign = "bottom",
|
| 258 |
-
nameKey,
|
| 259 |
-
}: React.ComponentProps<"div"> &
|
| 260 |
-
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
| 261 |
-
hideIcon?: boolean;
|
| 262 |
-
nameKey?: string;
|
| 263 |
-
}) {
|
| 264 |
-
const { config } = useChart();
|
| 265 |
-
|
| 266 |
-
if (!payload?.length) {
|
| 267 |
-
return null;
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
return (
|
| 271 |
-
<div
|
| 272 |
-
className={cn(
|
| 273 |
-
"flex items-center justify-center gap-4",
|
| 274 |
-
verticalAlign === "top" ? "pb-3" : "pt-3",
|
| 275 |
-
className,
|
| 276 |
-
)}
|
| 277 |
-
>
|
| 278 |
-
{payload.map((item) => {
|
| 279 |
-
const key = `${nameKey || item.dataKey || "value"}`;
|
| 280 |
-
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
| 281 |
-
|
| 282 |
-
return (
|
| 283 |
-
<div
|
| 284 |
-
key={item.value}
|
| 285 |
-
className={cn(
|
| 286 |
-
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
| 287 |
-
)}
|
| 288 |
-
>
|
| 289 |
-
{itemConfig?.icon && !hideIcon ? (
|
| 290 |
-
<itemConfig.icon />
|
| 291 |
-
) : (
|
| 292 |
-
<div
|
| 293 |
-
className="h-2 w-2 shrink-0 rounded-[2px]"
|
| 294 |
-
style={{
|
| 295 |
-
backgroundColor: item.color,
|
| 296 |
-
}}
|
| 297 |
-
/>
|
| 298 |
-
)}
|
| 299 |
-
{itemConfig?.label}
|
| 300 |
-
</div>
|
| 301 |
-
);
|
| 302 |
-
})}
|
| 303 |
-
</div>
|
| 304 |
-
);
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
// Helper to extract item config from a payload.
|
| 308 |
-
function getPayloadConfigFromPayload(
|
| 309 |
-
config: ChartConfig,
|
| 310 |
-
payload: unknown,
|
| 311 |
-
key: string,
|
| 312 |
-
) {
|
| 313 |
-
if (typeof payload !== "object" || payload === null) {
|
| 314 |
-
return undefined;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
const payloadPayload =
|
| 318 |
-
"payload" in payload &&
|
| 319 |
-
typeof payload.payload === "object" &&
|
| 320 |
-
payload.payload !== null
|
| 321 |
-
? payload.payload
|
| 322 |
-
: undefined;
|
| 323 |
-
|
| 324 |
-
let configLabelKey: string = key;
|
| 325 |
-
|
| 326 |
-
if (
|
| 327 |
-
key in payload &&
|
| 328 |
-
typeof payload[key as keyof typeof payload] === "string"
|
| 329 |
-
) {
|
| 330 |
-
configLabelKey = payload[key as keyof typeof payload] as string;
|
| 331 |
-
} else if (
|
| 332 |
-
payloadPayload &&
|
| 333 |
-
key in payloadPayload &&
|
| 334 |
-
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
| 335 |
-
) {
|
| 336 |
-
configLabelKey = payloadPayload[
|
| 337 |
-
key as keyof typeof payloadPayload
|
| 338 |
-
] as string;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
return configLabelKey in config
|
| 342 |
-
? config[configLabelKey]
|
| 343 |
-
: config[key as keyof typeof config];
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
export {
|
| 347 |
-
ChartContainer,
|
| 348 |
-
ChartTooltip,
|
| 349 |
-
ChartTooltipContent,
|
| 350 |
-
ChartLegend,
|
| 351 |
-
ChartLegendContent,
|
| 352 |
-
ChartStyle,
|
| 353 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/checkbox.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as CheckboxPrimitive from "@radix-ui/react-checkbox@1.1.4";
|
| 5 |
-
import { CheckIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function Checkbox({
|
| 10 |
-
className,
|
| 11 |
-
...props
|
| 12 |
-
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
| 13 |
-
return (
|
| 14 |
-
<CheckboxPrimitive.Root
|
| 15 |
-
data-slot="checkbox"
|
| 16 |
-
className={cn(
|
| 17 |
-
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
| 18 |
-
className,
|
| 19 |
-
)}
|
| 20 |
-
{...props}
|
| 21 |
-
>
|
| 22 |
-
<CheckboxPrimitive.Indicator
|
| 23 |
-
data-slot="checkbox-indicator"
|
| 24 |
-
className="flex items-center justify-center text-current transition-none"
|
| 25 |
-
>
|
| 26 |
-
<CheckIcon className="size-3.5" />
|
| 27 |
-
</CheckboxPrimitive.Indicator>
|
| 28 |
-
</CheckboxPrimitive.Root>
|
| 29 |
-
);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
export { Checkbox };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/collapsible.tsx
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
|
| 4 |
-
|
| 5 |
-
function Collapsible({
|
| 6 |
-
...props
|
| 7 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
| 8 |
-
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
function CollapsibleTrigger({
|
| 12 |
-
...props
|
| 13 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
| 14 |
-
return (
|
| 15 |
-
<CollapsiblePrimitive.CollapsibleTrigger
|
| 16 |
-
data-slot="collapsible-trigger"
|
| 17 |
-
{...props}
|
| 18 |
-
/>
|
| 19 |
-
);
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
function CollapsibleContent({
|
| 23 |
-
...props
|
| 24 |
-
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
| 25 |
-
return (
|
| 26 |
-
<CollapsiblePrimitive.CollapsibleContent
|
| 27 |
-
data-slot="collapsible-content"
|
| 28 |
-
{...props}
|
| 29 |
-
/>
|
| 30 |
-
);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/command.tsx
DELETED
|
@@ -1,177 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import { Command as CommandPrimitive } from "cmdk@1.1.1";
|
| 5 |
-
import { SearchIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
import {
|
| 9 |
-
Dialog,
|
| 10 |
-
DialogContent,
|
| 11 |
-
DialogDescription,
|
| 12 |
-
DialogHeader,
|
| 13 |
-
DialogTitle,
|
| 14 |
-
} from "./dialog";
|
| 15 |
-
|
| 16 |
-
function Command({
|
| 17 |
-
className,
|
| 18 |
-
...props
|
| 19 |
-
}: React.ComponentProps<typeof CommandPrimitive>) {
|
| 20 |
-
return (
|
| 21 |
-
<CommandPrimitive
|
| 22 |
-
data-slot="command"
|
| 23 |
-
className={cn(
|
| 24 |
-
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
| 25 |
-
className,
|
| 26 |
-
)}
|
| 27 |
-
{...props}
|
| 28 |
-
/>
|
| 29 |
-
);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function CommandDialog({
|
| 33 |
-
title = "Command Palette",
|
| 34 |
-
description = "Search for a command to run...",
|
| 35 |
-
children,
|
| 36 |
-
...props
|
| 37 |
-
}: React.ComponentProps<typeof Dialog> & {
|
| 38 |
-
title?: string;
|
| 39 |
-
description?: string;
|
| 40 |
-
}) {
|
| 41 |
-
return (
|
| 42 |
-
<Dialog {...props}>
|
| 43 |
-
<DialogHeader className="sr-only">
|
| 44 |
-
<DialogTitle>{title}</DialogTitle>
|
| 45 |
-
<DialogDescription>{description}</DialogDescription>
|
| 46 |
-
</DialogHeader>
|
| 47 |
-
<DialogContent className="overflow-hidden p-0">
|
| 48 |
-
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
| 49 |
-
{children}
|
| 50 |
-
</Command>
|
| 51 |
-
</DialogContent>
|
| 52 |
-
</Dialog>
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
function CommandInput({
|
| 57 |
-
className,
|
| 58 |
-
...props
|
| 59 |
-
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
| 60 |
-
return (
|
| 61 |
-
<div
|
| 62 |
-
data-slot="command-input-wrapper"
|
| 63 |
-
className="flex h-9 items-center gap-2 border-b px-3"
|
| 64 |
-
>
|
| 65 |
-
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
| 66 |
-
<CommandPrimitive.Input
|
| 67 |
-
data-slot="command-input"
|
| 68 |
-
className={cn(
|
| 69 |
-
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
| 70 |
-
className,
|
| 71 |
-
)}
|
| 72 |
-
{...props}
|
| 73 |
-
/>
|
| 74 |
-
</div>
|
| 75 |
-
);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
function CommandList({
|
| 79 |
-
className,
|
| 80 |
-
...props
|
| 81 |
-
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
| 82 |
-
return (
|
| 83 |
-
<CommandPrimitive.List
|
| 84 |
-
data-slot="command-list"
|
| 85 |
-
className={cn(
|
| 86 |
-
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
| 87 |
-
className,
|
| 88 |
-
)}
|
| 89 |
-
{...props}
|
| 90 |
-
/>
|
| 91 |
-
);
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
function CommandEmpty({
|
| 95 |
-
...props
|
| 96 |
-
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
| 97 |
-
return (
|
| 98 |
-
<CommandPrimitive.Empty
|
| 99 |
-
data-slot="command-empty"
|
| 100 |
-
className="py-6 text-center text-sm"
|
| 101 |
-
{...props}
|
| 102 |
-
/>
|
| 103 |
-
);
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
function CommandGroup({
|
| 107 |
-
className,
|
| 108 |
-
...props
|
| 109 |
-
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
| 110 |
-
return (
|
| 111 |
-
<CommandPrimitive.Group
|
| 112 |
-
data-slot="command-group"
|
| 113 |
-
className={cn(
|
| 114 |
-
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
| 115 |
-
className,
|
| 116 |
-
)}
|
| 117 |
-
{...props}
|
| 118 |
-
/>
|
| 119 |
-
);
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
function CommandSeparator({
|
| 123 |
-
className,
|
| 124 |
-
...props
|
| 125 |
-
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
| 126 |
-
return (
|
| 127 |
-
<CommandPrimitive.Separator
|
| 128 |
-
data-slot="command-separator"
|
| 129 |
-
className={cn("bg-border -mx-1 h-px", className)}
|
| 130 |
-
{...props}
|
| 131 |
-
/>
|
| 132 |
-
);
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
function CommandItem({
|
| 136 |
-
className,
|
| 137 |
-
...props
|
| 138 |
-
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
| 139 |
-
return (
|
| 140 |
-
<CommandPrimitive.Item
|
| 141 |
-
data-slot="command-item"
|
| 142 |
-
className={cn(
|
| 143 |
-
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 144 |
-
className,
|
| 145 |
-
)}
|
| 146 |
-
{...props}
|
| 147 |
-
/>
|
| 148 |
-
);
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
function CommandShortcut({
|
| 152 |
-
className,
|
| 153 |
-
...props
|
| 154 |
-
}: React.ComponentProps<"span">) {
|
| 155 |
-
return (
|
| 156 |
-
<span
|
| 157 |
-
data-slot="command-shortcut"
|
| 158 |
-
className={cn(
|
| 159 |
-
"text-muted-foreground ml-auto text-xs tracking-widest",
|
| 160 |
-
className,
|
| 161 |
-
)}
|
| 162 |
-
{...props}
|
| 163 |
-
/>
|
| 164 |
-
);
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
export {
|
| 168 |
-
Command,
|
| 169 |
-
CommandDialog,
|
| 170 |
-
CommandInput,
|
| 171 |
-
CommandList,
|
| 172 |
-
CommandEmpty,
|
| 173 |
-
CommandGroup,
|
| 174 |
-
CommandItem,
|
| 175 |
-
CommandShortcut,
|
| 176 |
-
CommandSeparator,
|
| 177 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/context-menu.tsx
DELETED
|
@@ -1,252 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu@2.2.6";
|
| 5 |
-
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function ContextMenu({
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
| 12 |
-
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
function ContextMenuTrigger({
|
| 16 |
-
...props
|
| 17 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
| 18 |
-
return (
|
| 19 |
-
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
| 20 |
-
);
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function ContextMenuGroup({
|
| 24 |
-
...props
|
| 25 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
| 26 |
-
return (
|
| 27 |
-
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
| 28 |
-
);
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function ContextMenuPortal({
|
| 32 |
-
...props
|
| 33 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
| 34 |
-
return (
|
| 35 |
-
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
| 36 |
-
);
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
function ContextMenuSub({
|
| 40 |
-
...props
|
| 41 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
| 42 |
-
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
function ContextMenuRadioGroup({
|
| 46 |
-
...props
|
| 47 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
| 48 |
-
return (
|
| 49 |
-
<ContextMenuPrimitive.RadioGroup
|
| 50 |
-
data-slot="context-menu-radio-group"
|
| 51 |
-
{...props}
|
| 52 |
-
/>
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
function ContextMenuSubTrigger({
|
| 57 |
-
className,
|
| 58 |
-
inset,
|
| 59 |
-
children,
|
| 60 |
-
...props
|
| 61 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
| 62 |
-
inset?: boolean;
|
| 63 |
-
}) {
|
| 64 |
-
return (
|
| 65 |
-
<ContextMenuPrimitive.SubTrigger
|
| 66 |
-
data-slot="context-menu-sub-trigger"
|
| 67 |
-
data-inset={inset}
|
| 68 |
-
className={cn(
|
| 69 |
-
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 70 |
-
className,
|
| 71 |
-
)}
|
| 72 |
-
{...props}
|
| 73 |
-
>
|
| 74 |
-
{children}
|
| 75 |
-
<ChevronRightIcon className="ml-auto" />
|
| 76 |
-
</ContextMenuPrimitive.SubTrigger>
|
| 77 |
-
);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
function ContextMenuSubContent({
|
| 81 |
-
className,
|
| 82 |
-
...props
|
| 83 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
| 84 |
-
return (
|
| 85 |
-
<ContextMenuPrimitive.SubContent
|
| 86 |
-
data-slot="context-menu-sub-content"
|
| 87 |
-
className={cn(
|
| 88 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
| 89 |
-
className,
|
| 90 |
-
)}
|
| 91 |
-
{...props}
|
| 92 |
-
/>
|
| 93 |
-
);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
function ContextMenuContent({
|
| 97 |
-
className,
|
| 98 |
-
...props
|
| 99 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
| 100 |
-
return (
|
| 101 |
-
<ContextMenuPrimitive.Portal>
|
| 102 |
-
<ContextMenuPrimitive.Content
|
| 103 |
-
data-slot="context-menu-content"
|
| 104 |
-
className={cn(
|
| 105 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
| 106 |
-
className,
|
| 107 |
-
)}
|
| 108 |
-
{...props}
|
| 109 |
-
/>
|
| 110 |
-
</ContextMenuPrimitive.Portal>
|
| 111 |
-
);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
function ContextMenuItem({
|
| 115 |
-
className,
|
| 116 |
-
inset,
|
| 117 |
-
variant = "default",
|
| 118 |
-
...props
|
| 119 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
| 120 |
-
inset?: boolean;
|
| 121 |
-
variant?: "default" | "destructive";
|
| 122 |
-
}) {
|
| 123 |
-
return (
|
| 124 |
-
<ContextMenuPrimitive.Item
|
| 125 |
-
data-slot="context-menu-item"
|
| 126 |
-
data-inset={inset}
|
| 127 |
-
data-variant={variant}
|
| 128 |
-
className={cn(
|
| 129 |
-
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 130 |
-
className,
|
| 131 |
-
)}
|
| 132 |
-
{...props}
|
| 133 |
-
/>
|
| 134 |
-
);
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
function ContextMenuCheckboxItem({
|
| 138 |
-
className,
|
| 139 |
-
children,
|
| 140 |
-
checked,
|
| 141 |
-
...props
|
| 142 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
| 143 |
-
return (
|
| 144 |
-
<ContextMenuPrimitive.CheckboxItem
|
| 145 |
-
data-slot="context-menu-checkbox-item"
|
| 146 |
-
className={cn(
|
| 147 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 148 |
-
className,
|
| 149 |
-
)}
|
| 150 |
-
checked={checked}
|
| 151 |
-
{...props}
|
| 152 |
-
>
|
| 153 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 154 |
-
<ContextMenuPrimitive.ItemIndicator>
|
| 155 |
-
<CheckIcon className="size-4" />
|
| 156 |
-
</ContextMenuPrimitive.ItemIndicator>
|
| 157 |
-
</span>
|
| 158 |
-
{children}
|
| 159 |
-
</ContextMenuPrimitive.CheckboxItem>
|
| 160 |
-
);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
function ContextMenuRadioItem({
|
| 164 |
-
className,
|
| 165 |
-
children,
|
| 166 |
-
...props
|
| 167 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
| 168 |
-
return (
|
| 169 |
-
<ContextMenuPrimitive.RadioItem
|
| 170 |
-
data-slot="context-menu-radio-item"
|
| 171 |
-
className={cn(
|
| 172 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 173 |
-
className,
|
| 174 |
-
)}
|
| 175 |
-
{...props}
|
| 176 |
-
>
|
| 177 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 178 |
-
<ContextMenuPrimitive.ItemIndicator>
|
| 179 |
-
<CircleIcon className="size-2 fill-current" />
|
| 180 |
-
</ContextMenuPrimitive.ItemIndicator>
|
| 181 |
-
</span>
|
| 182 |
-
{children}
|
| 183 |
-
</ContextMenuPrimitive.RadioItem>
|
| 184 |
-
);
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
function ContextMenuLabel({
|
| 188 |
-
className,
|
| 189 |
-
inset,
|
| 190 |
-
...props
|
| 191 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
| 192 |
-
inset?: boolean;
|
| 193 |
-
}) {
|
| 194 |
-
return (
|
| 195 |
-
<ContextMenuPrimitive.Label
|
| 196 |
-
data-slot="context-menu-label"
|
| 197 |
-
data-inset={inset}
|
| 198 |
-
className={cn(
|
| 199 |
-
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
| 200 |
-
className,
|
| 201 |
-
)}
|
| 202 |
-
{...props}
|
| 203 |
-
/>
|
| 204 |
-
);
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
function ContextMenuSeparator({
|
| 208 |
-
className,
|
| 209 |
-
...props
|
| 210 |
-
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
| 211 |
-
return (
|
| 212 |
-
<ContextMenuPrimitive.Separator
|
| 213 |
-
data-slot="context-menu-separator"
|
| 214 |
-
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
| 215 |
-
{...props}
|
| 216 |
-
/>
|
| 217 |
-
);
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
function ContextMenuShortcut({
|
| 221 |
-
className,
|
| 222 |
-
...props
|
| 223 |
-
}: React.ComponentProps<"span">) {
|
| 224 |
-
return (
|
| 225 |
-
<span
|
| 226 |
-
data-slot="context-menu-shortcut"
|
| 227 |
-
className={cn(
|
| 228 |
-
"text-muted-foreground ml-auto text-xs tracking-widest",
|
| 229 |
-
className,
|
| 230 |
-
)}
|
| 231 |
-
{...props}
|
| 232 |
-
/>
|
| 233 |
-
);
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
export {
|
| 237 |
-
ContextMenu,
|
| 238 |
-
ContextMenuTrigger,
|
| 239 |
-
ContextMenuContent,
|
| 240 |
-
ContextMenuItem,
|
| 241 |
-
ContextMenuCheckboxItem,
|
| 242 |
-
ContextMenuRadioItem,
|
| 243 |
-
ContextMenuLabel,
|
| 244 |
-
ContextMenuSeparator,
|
| 245 |
-
ContextMenuShortcut,
|
| 246 |
-
ContextMenuGroup,
|
| 247 |
-
ContextMenuPortal,
|
| 248 |
-
ContextMenuSub,
|
| 249 |
-
ContextMenuSubContent,
|
| 250 |
-
ContextMenuSubTrigger,
|
| 251 |
-
ContextMenuRadioGroup,
|
| 252 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/dialog.tsx
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
|
| 5 |
-
import { XIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function Dialog({
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
| 12 |
-
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
function DialogTrigger({
|
| 16 |
-
...props
|
| 17 |
-
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
| 18 |
-
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
function DialogPortal({
|
| 22 |
-
...props
|
| 23 |
-
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
| 24 |
-
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
function DialogClose({
|
| 28 |
-
...props
|
| 29 |
-
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
| 30 |
-
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
const DialogOverlay = React.forwardRef<
|
| 34 |
-
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
| 35 |
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
| 36 |
-
>(({ className, ...props }, ref) => {
|
| 37 |
-
return (
|
| 38 |
-
<DialogPrimitive.Overlay
|
| 39 |
-
ref={ref}
|
| 40 |
-
data-slot="dialog-overlay"
|
| 41 |
-
className={cn(
|
| 42 |
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
| 43 |
-
className,
|
| 44 |
-
)}
|
| 45 |
-
{...props}
|
| 46 |
-
/>
|
| 47 |
-
);
|
| 48 |
-
});
|
| 49 |
-
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
| 50 |
-
|
| 51 |
-
function DialogContent({
|
| 52 |
-
className,
|
| 53 |
-
children,
|
| 54 |
-
...props
|
| 55 |
-
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
| 56 |
-
return (
|
| 57 |
-
<DialogPortal data-slot="dialog-portal">
|
| 58 |
-
<DialogOverlay />
|
| 59 |
-
<DialogPrimitive.Content
|
| 60 |
-
data-slot="dialog-content"
|
| 61 |
-
className={cn(
|
| 62 |
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
| 63 |
-
className,
|
| 64 |
-
)}
|
| 65 |
-
{...props}
|
| 66 |
-
>
|
| 67 |
-
{children}
|
| 68 |
-
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
| 69 |
-
<XIcon />
|
| 70 |
-
<span className="sr-only">Close</span>
|
| 71 |
-
</DialogPrimitive.Close>
|
| 72 |
-
</DialogPrimitive.Content>
|
| 73 |
-
</DialogPortal>
|
| 74 |
-
);
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 78 |
-
return (
|
| 79 |
-
<div
|
| 80 |
-
data-slot="dialog-header"
|
| 81 |
-
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
| 82 |
-
{...props}
|
| 83 |
-
/>
|
| 84 |
-
);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 88 |
-
return (
|
| 89 |
-
<div
|
| 90 |
-
data-slot="dialog-footer"
|
| 91 |
-
className={cn(
|
| 92 |
-
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
| 93 |
-
className,
|
| 94 |
-
)}
|
| 95 |
-
{...props}
|
| 96 |
-
/>
|
| 97 |
-
);
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
function DialogTitle({
|
| 101 |
-
className,
|
| 102 |
-
...props
|
| 103 |
-
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
| 104 |
-
return (
|
| 105 |
-
<DialogPrimitive.Title
|
| 106 |
-
data-slot="dialog-title"
|
| 107 |
-
className={cn("text-lg leading-none font-semibold", className)}
|
| 108 |
-
{...props}
|
| 109 |
-
/>
|
| 110 |
-
);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
function DialogDescription({
|
| 114 |
-
className,
|
| 115 |
-
...props
|
| 116 |
-
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
| 117 |
-
return (
|
| 118 |
-
<DialogPrimitive.Description
|
| 119 |
-
data-slot="dialog-description"
|
| 120 |
-
className={cn("text-muted-foreground text-sm", className)}
|
| 121 |
-
{...props}
|
| 122 |
-
/>
|
| 123 |
-
);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
export {
|
| 127 |
-
Dialog,
|
| 128 |
-
DialogClose,
|
| 129 |
-
DialogContent,
|
| 130 |
-
DialogDescription,
|
| 131 |
-
DialogFooter,
|
| 132 |
-
DialogHeader,
|
| 133 |
-
DialogOverlay,
|
| 134 |
-
DialogPortal,
|
| 135 |
-
DialogTitle,
|
| 136 |
-
DialogTrigger,
|
| 137 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/drawer.tsx
DELETED
|
@@ -1,132 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import { Drawer as DrawerPrimitive } from "vaul@1.1.2";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function Drawer({
|
| 9 |
-
...props
|
| 10 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
| 11 |
-
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
function DrawerTrigger({
|
| 15 |
-
...props
|
| 16 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
| 17 |
-
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
function DrawerPortal({
|
| 21 |
-
...props
|
| 22 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
| 23 |
-
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
function DrawerClose({
|
| 27 |
-
...props
|
| 28 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
| 29 |
-
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function DrawerOverlay({
|
| 33 |
-
className,
|
| 34 |
-
...props
|
| 35 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
| 36 |
-
return (
|
| 37 |
-
<DrawerPrimitive.Overlay
|
| 38 |
-
data-slot="drawer-overlay"
|
| 39 |
-
className={cn(
|
| 40 |
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
| 41 |
-
className,
|
| 42 |
-
)}
|
| 43 |
-
{...props}
|
| 44 |
-
/>
|
| 45 |
-
);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function DrawerContent({
|
| 49 |
-
className,
|
| 50 |
-
children,
|
| 51 |
-
...props
|
| 52 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
| 53 |
-
return (
|
| 54 |
-
<DrawerPortal data-slot="drawer-portal">
|
| 55 |
-
<DrawerOverlay />
|
| 56 |
-
<DrawerPrimitive.Content
|
| 57 |
-
data-slot="drawer-content"
|
| 58 |
-
className={cn(
|
| 59 |
-
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
| 60 |
-
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
| 61 |
-
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
| 62 |
-
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
| 63 |
-
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
| 64 |
-
className,
|
| 65 |
-
)}
|
| 66 |
-
{...props}
|
| 67 |
-
>
|
| 68 |
-
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
| 69 |
-
{children}
|
| 70 |
-
</DrawerPrimitive.Content>
|
| 71 |
-
</DrawerPortal>
|
| 72 |
-
);
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 76 |
-
return (
|
| 77 |
-
<div
|
| 78 |
-
data-slot="drawer-header"
|
| 79 |
-
className={cn("flex flex-col gap-1.5 p-4", className)}
|
| 80 |
-
{...props}
|
| 81 |
-
/>
|
| 82 |
-
);
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 86 |
-
return (
|
| 87 |
-
<div
|
| 88 |
-
data-slot="drawer-footer"
|
| 89 |
-
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
| 90 |
-
{...props}
|
| 91 |
-
/>
|
| 92 |
-
);
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
function DrawerTitle({
|
| 96 |
-
className,
|
| 97 |
-
...props
|
| 98 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
| 99 |
-
return (
|
| 100 |
-
<DrawerPrimitive.Title
|
| 101 |
-
data-slot="drawer-title"
|
| 102 |
-
className={cn("text-foreground font-semibold", className)}
|
| 103 |
-
{...props}
|
| 104 |
-
/>
|
| 105 |
-
);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
function DrawerDescription({
|
| 109 |
-
className,
|
| 110 |
-
...props
|
| 111 |
-
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
| 112 |
-
return (
|
| 113 |
-
<DrawerPrimitive.Description
|
| 114 |
-
data-slot="drawer-description"
|
| 115 |
-
className={cn("text-muted-foreground text-sm", className)}
|
| 116 |
-
{...props}
|
| 117 |
-
/>
|
| 118 |
-
);
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
export {
|
| 122 |
-
Drawer,
|
| 123 |
-
DrawerPortal,
|
| 124 |
-
DrawerOverlay,
|
| 125 |
-
DrawerTrigger,
|
| 126 |
-
DrawerClose,
|
| 127 |
-
DrawerContent,
|
| 128 |
-
DrawerHeader,
|
| 129 |
-
DrawerFooter,
|
| 130 |
-
DrawerTitle,
|
| 131 |
-
DrawerDescription,
|
| 132 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/dropdown-menu.tsx
DELETED
|
@@ -1,257 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu@2.1.6";
|
| 5 |
-
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function DropdownMenu({
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
| 12 |
-
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
function DropdownMenuPortal({
|
| 16 |
-
...props
|
| 17 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
| 18 |
-
return (
|
| 19 |
-
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
| 20 |
-
);
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function DropdownMenuTrigger({
|
| 24 |
-
...props
|
| 25 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
| 26 |
-
return (
|
| 27 |
-
<DropdownMenuPrimitive.Trigger
|
| 28 |
-
data-slot="dropdown-menu-trigger"
|
| 29 |
-
{...props}
|
| 30 |
-
/>
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function DropdownMenuContent({
|
| 35 |
-
className,
|
| 36 |
-
sideOffset = 4,
|
| 37 |
-
...props
|
| 38 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
| 39 |
-
return (
|
| 40 |
-
<DropdownMenuPrimitive.Portal>
|
| 41 |
-
<DropdownMenuPrimitive.Content
|
| 42 |
-
data-slot="dropdown-menu-content"
|
| 43 |
-
sideOffset={sideOffset}
|
| 44 |
-
className={cn(
|
| 45 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
| 46 |
-
className,
|
| 47 |
-
)}
|
| 48 |
-
{...props}
|
| 49 |
-
/>
|
| 50 |
-
</DropdownMenuPrimitive.Portal>
|
| 51 |
-
);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function DropdownMenuGroup({
|
| 55 |
-
...props
|
| 56 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
| 57 |
-
return (
|
| 58 |
-
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
| 59 |
-
);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
function DropdownMenuItem({
|
| 63 |
-
className,
|
| 64 |
-
inset,
|
| 65 |
-
variant = "default",
|
| 66 |
-
...props
|
| 67 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
| 68 |
-
inset?: boolean;
|
| 69 |
-
variant?: "default" | "destructive";
|
| 70 |
-
}) {
|
| 71 |
-
return (
|
| 72 |
-
<DropdownMenuPrimitive.Item
|
| 73 |
-
data-slot="dropdown-menu-item"
|
| 74 |
-
data-inset={inset}
|
| 75 |
-
data-variant={variant}
|
| 76 |
-
className={cn(
|
| 77 |
-
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 78 |
-
className,
|
| 79 |
-
)}
|
| 80 |
-
{...props}
|
| 81 |
-
/>
|
| 82 |
-
);
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function DropdownMenuCheckboxItem({
|
| 86 |
-
className,
|
| 87 |
-
children,
|
| 88 |
-
checked,
|
| 89 |
-
...props
|
| 90 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
| 91 |
-
return (
|
| 92 |
-
<DropdownMenuPrimitive.CheckboxItem
|
| 93 |
-
data-slot="dropdown-menu-checkbox-item"
|
| 94 |
-
className={cn(
|
| 95 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 96 |
-
className,
|
| 97 |
-
)}
|
| 98 |
-
checked={checked}
|
| 99 |
-
{...props}
|
| 100 |
-
>
|
| 101 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 102 |
-
<DropdownMenuPrimitive.ItemIndicator>
|
| 103 |
-
<CheckIcon className="size-4" />
|
| 104 |
-
</DropdownMenuPrimitive.ItemIndicator>
|
| 105 |
-
</span>
|
| 106 |
-
{children}
|
| 107 |
-
</DropdownMenuPrimitive.CheckboxItem>
|
| 108 |
-
);
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
function DropdownMenuRadioGroup({
|
| 112 |
-
...props
|
| 113 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
| 114 |
-
return (
|
| 115 |
-
<DropdownMenuPrimitive.RadioGroup
|
| 116 |
-
data-slot="dropdown-menu-radio-group"
|
| 117 |
-
{...props}
|
| 118 |
-
/>
|
| 119 |
-
);
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
function DropdownMenuRadioItem({
|
| 123 |
-
className,
|
| 124 |
-
children,
|
| 125 |
-
...props
|
| 126 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
| 127 |
-
return (
|
| 128 |
-
<DropdownMenuPrimitive.RadioItem
|
| 129 |
-
data-slot="dropdown-menu-radio-item"
|
| 130 |
-
className={cn(
|
| 131 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 132 |
-
className,
|
| 133 |
-
)}
|
| 134 |
-
{...props}
|
| 135 |
-
>
|
| 136 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 137 |
-
<DropdownMenuPrimitive.ItemIndicator>
|
| 138 |
-
<CircleIcon className="size-2 fill-current" />
|
| 139 |
-
</DropdownMenuPrimitive.ItemIndicator>
|
| 140 |
-
</span>
|
| 141 |
-
{children}
|
| 142 |
-
</DropdownMenuPrimitive.RadioItem>
|
| 143 |
-
);
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
function DropdownMenuLabel({
|
| 147 |
-
className,
|
| 148 |
-
inset,
|
| 149 |
-
...props
|
| 150 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
| 151 |
-
inset?: boolean;
|
| 152 |
-
}) {
|
| 153 |
-
return (
|
| 154 |
-
<DropdownMenuPrimitive.Label
|
| 155 |
-
data-slot="dropdown-menu-label"
|
| 156 |
-
data-inset={inset}
|
| 157 |
-
className={cn(
|
| 158 |
-
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
| 159 |
-
className,
|
| 160 |
-
)}
|
| 161 |
-
{...props}
|
| 162 |
-
/>
|
| 163 |
-
);
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
function DropdownMenuSeparator({
|
| 167 |
-
className,
|
| 168 |
-
...props
|
| 169 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
| 170 |
-
return (
|
| 171 |
-
<DropdownMenuPrimitive.Separator
|
| 172 |
-
data-slot="dropdown-menu-separator"
|
| 173 |
-
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
| 174 |
-
{...props}
|
| 175 |
-
/>
|
| 176 |
-
);
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
function DropdownMenuShortcut({
|
| 180 |
-
className,
|
| 181 |
-
...props
|
| 182 |
-
}: React.ComponentProps<"span">) {
|
| 183 |
-
return (
|
| 184 |
-
<span
|
| 185 |
-
data-slot="dropdown-menu-shortcut"
|
| 186 |
-
className={cn(
|
| 187 |
-
"text-muted-foreground ml-auto text-xs tracking-widest",
|
| 188 |
-
className,
|
| 189 |
-
)}
|
| 190 |
-
{...props}
|
| 191 |
-
/>
|
| 192 |
-
);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
function DropdownMenuSub({
|
| 196 |
-
...props
|
| 197 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
| 198 |
-
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
function DropdownMenuSubTrigger({
|
| 202 |
-
className,
|
| 203 |
-
inset,
|
| 204 |
-
children,
|
| 205 |
-
...props
|
| 206 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
| 207 |
-
inset?: boolean;
|
| 208 |
-
}) {
|
| 209 |
-
return (
|
| 210 |
-
<DropdownMenuPrimitive.SubTrigger
|
| 211 |
-
data-slot="dropdown-menu-sub-trigger"
|
| 212 |
-
data-inset={inset}
|
| 213 |
-
className={cn(
|
| 214 |
-
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
| 215 |
-
className,
|
| 216 |
-
)}
|
| 217 |
-
{...props}
|
| 218 |
-
>
|
| 219 |
-
{children}
|
| 220 |
-
<ChevronRightIcon className="ml-auto size-4" />
|
| 221 |
-
</DropdownMenuPrimitive.SubTrigger>
|
| 222 |
-
);
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
function DropdownMenuSubContent({
|
| 226 |
-
className,
|
| 227 |
-
...props
|
| 228 |
-
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
| 229 |
-
return (
|
| 230 |
-
<DropdownMenuPrimitive.SubContent
|
| 231 |
-
data-slot="dropdown-menu-sub-content"
|
| 232 |
-
className={cn(
|
| 233 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
| 234 |
-
className,
|
| 235 |
-
)}
|
| 236 |
-
{...props}
|
| 237 |
-
/>
|
| 238 |
-
);
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
export {
|
| 242 |
-
DropdownMenu,
|
| 243 |
-
DropdownMenuPortal,
|
| 244 |
-
DropdownMenuTrigger,
|
| 245 |
-
DropdownMenuContent,
|
| 246 |
-
DropdownMenuGroup,
|
| 247 |
-
DropdownMenuLabel,
|
| 248 |
-
DropdownMenuItem,
|
| 249 |
-
DropdownMenuCheckboxItem,
|
| 250 |
-
DropdownMenuRadioGroup,
|
| 251 |
-
DropdownMenuRadioItem,
|
| 252 |
-
DropdownMenuSeparator,
|
| 253 |
-
DropdownMenuShortcut,
|
| 254 |
-
DropdownMenuSub,
|
| 255 |
-
DropdownMenuSubTrigger,
|
| 256 |
-
DropdownMenuSubContent,
|
| 257 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/form.tsx
DELETED
|
@@ -1,168 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
|
| 5 |
-
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
| 6 |
-
import {
|
| 7 |
-
Controller,
|
| 8 |
-
FormProvider,
|
| 9 |
-
useFormContext,
|
| 10 |
-
useFormState,
|
| 11 |
-
type ControllerProps,
|
| 12 |
-
type FieldPath,
|
| 13 |
-
type FieldValues,
|
| 14 |
-
} from "react-hook-form@7.55.0";
|
| 15 |
-
|
| 16 |
-
import { cn } from "./utils";
|
| 17 |
-
import { Label } from "./label";
|
| 18 |
-
|
| 19 |
-
const Form = FormProvider;
|
| 20 |
-
|
| 21 |
-
type FormFieldContextValue<
|
| 22 |
-
TFieldValues extends FieldValues = FieldValues,
|
| 23 |
-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
| 24 |
-
> = {
|
| 25 |
-
name: TName;
|
| 26 |
-
};
|
| 27 |
-
|
| 28 |
-
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
| 29 |
-
{} as FormFieldContextValue,
|
| 30 |
-
);
|
| 31 |
-
|
| 32 |
-
const FormField = <
|
| 33 |
-
TFieldValues extends FieldValues = FieldValues,
|
| 34 |
-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
| 35 |
-
>({
|
| 36 |
-
...props
|
| 37 |
-
}: ControllerProps<TFieldValues, TName>) => {
|
| 38 |
-
return (
|
| 39 |
-
<FormFieldContext.Provider value={{ name: props.name }}>
|
| 40 |
-
<Controller {...props} />
|
| 41 |
-
</FormFieldContext.Provider>
|
| 42 |
-
);
|
| 43 |
-
};
|
| 44 |
-
|
| 45 |
-
const useFormField = () => {
|
| 46 |
-
const fieldContext = React.useContext(FormFieldContext);
|
| 47 |
-
const itemContext = React.useContext(FormItemContext);
|
| 48 |
-
const { getFieldState } = useFormContext();
|
| 49 |
-
const formState = useFormState({ name: fieldContext.name });
|
| 50 |
-
const fieldState = getFieldState(fieldContext.name, formState);
|
| 51 |
-
|
| 52 |
-
if (!fieldContext) {
|
| 53 |
-
throw new Error("useFormField should be used within <FormField>");
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
const { id } = itemContext;
|
| 57 |
-
|
| 58 |
-
return {
|
| 59 |
-
id,
|
| 60 |
-
name: fieldContext.name,
|
| 61 |
-
formItemId: `${id}-form-item`,
|
| 62 |
-
formDescriptionId: `${id}-form-item-description`,
|
| 63 |
-
formMessageId: `${id}-form-item-message`,
|
| 64 |
-
...fieldState,
|
| 65 |
-
};
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
type FormItemContextValue = {
|
| 69 |
-
id: string;
|
| 70 |
-
};
|
| 71 |
-
|
| 72 |
-
const FormItemContext = React.createContext<FormItemContextValue>(
|
| 73 |
-
{} as FormItemContextValue,
|
| 74 |
-
);
|
| 75 |
-
|
| 76 |
-
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
| 77 |
-
const id = React.useId();
|
| 78 |
-
|
| 79 |
-
return (
|
| 80 |
-
<FormItemContext.Provider value={{ id }}>
|
| 81 |
-
<div
|
| 82 |
-
data-slot="form-item"
|
| 83 |
-
className={cn("grid gap-2", className)}
|
| 84 |
-
{...props}
|
| 85 |
-
/>
|
| 86 |
-
</FormItemContext.Provider>
|
| 87 |
-
);
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
function FormLabel({
|
| 91 |
-
className,
|
| 92 |
-
...props
|
| 93 |
-
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 94 |
-
const { error, formItemId } = useFormField();
|
| 95 |
-
|
| 96 |
-
return (
|
| 97 |
-
<Label
|
| 98 |
-
data-slot="form-label"
|
| 99 |
-
data-error={!!error}
|
| 100 |
-
className={cn("data-[error=true]:text-destructive", className)}
|
| 101 |
-
htmlFor={formItemId}
|
| 102 |
-
{...props}
|
| 103 |
-
/>
|
| 104 |
-
);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
| 108 |
-
const { error, formItemId, formDescriptionId, formMessageId } =
|
| 109 |
-
useFormField();
|
| 110 |
-
|
| 111 |
-
return (
|
| 112 |
-
<Slot
|
| 113 |
-
data-slot="form-control"
|
| 114 |
-
id={formItemId}
|
| 115 |
-
aria-describedby={
|
| 116 |
-
!error
|
| 117 |
-
? `${formDescriptionId}`
|
| 118 |
-
: `${formDescriptionId} ${formMessageId}`
|
| 119 |
-
}
|
| 120 |
-
aria-invalid={!!error}
|
| 121 |
-
{...props}
|
| 122 |
-
/>
|
| 123 |
-
);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
| 127 |
-
const { formDescriptionId } = useFormField();
|
| 128 |
-
|
| 129 |
-
return (
|
| 130 |
-
<p
|
| 131 |
-
data-slot="form-description"
|
| 132 |
-
id={formDescriptionId}
|
| 133 |
-
className={cn("text-muted-foreground text-sm", className)}
|
| 134 |
-
{...props}
|
| 135 |
-
/>
|
| 136 |
-
);
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
| 140 |
-
const { error, formMessageId } = useFormField();
|
| 141 |
-
const body = error ? String(error?.message ?? "") : props.children;
|
| 142 |
-
|
| 143 |
-
if (!body) {
|
| 144 |
-
return null;
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
return (
|
| 148 |
-
<p
|
| 149 |
-
data-slot="form-message"
|
| 150 |
-
id={formMessageId}
|
| 151 |
-
className={cn("text-destructive text-sm", className)}
|
| 152 |
-
{...props}
|
| 153 |
-
>
|
| 154 |
-
{body}
|
| 155 |
-
</p>
|
| 156 |
-
);
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
export {
|
| 160 |
-
useFormField,
|
| 161 |
-
Form,
|
| 162 |
-
FormItem,
|
| 163 |
-
FormLabel,
|
| 164 |
-
FormControl,
|
| 165 |
-
FormDescription,
|
| 166 |
-
FormMessage,
|
| 167 |
-
FormField,
|
| 168 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/hover-card.tsx
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as HoverCardPrimitive from "@radix-ui/react-hover-card@1.1.6";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function HoverCard({
|
| 9 |
-
...props
|
| 10 |
-
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
| 11 |
-
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
function HoverCardTrigger({
|
| 15 |
-
...props
|
| 16 |
-
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
| 17 |
-
return (
|
| 18 |
-
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
| 19 |
-
);
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
function HoverCardContent({
|
| 23 |
-
className,
|
| 24 |
-
align = "center",
|
| 25 |
-
sideOffset = 4,
|
| 26 |
-
...props
|
| 27 |
-
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
| 28 |
-
return (
|
| 29 |
-
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
| 30 |
-
<HoverCardPrimitive.Content
|
| 31 |
-
data-slot="hover-card-content"
|
| 32 |
-
align={align}
|
| 33 |
-
sideOffset={sideOffset}
|
| 34 |
-
className={cn(
|
| 35 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
| 36 |
-
className,
|
| 37 |
-
)}
|
| 38 |
-
{...props}
|
| 39 |
-
/>
|
| 40 |
-
</HoverCardPrimitive.Portal>
|
| 41 |
-
);
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/input-otp.tsx
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import { OTPInput, OTPInputContext } from "input-otp@1.4.2";
|
| 5 |
-
import { MinusIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function InputOTP({
|
| 10 |
-
className,
|
| 11 |
-
containerClassName,
|
| 12 |
-
...props
|
| 13 |
-
}: React.ComponentProps<typeof OTPInput> & {
|
| 14 |
-
containerClassName?: string;
|
| 15 |
-
}) {
|
| 16 |
-
return (
|
| 17 |
-
<OTPInput
|
| 18 |
-
data-slot="input-otp"
|
| 19 |
-
containerClassName={cn(
|
| 20 |
-
"flex items-center gap-2 has-disabled:opacity-50",
|
| 21 |
-
containerClassName,
|
| 22 |
-
)}
|
| 23 |
-
className={cn("disabled:cursor-not-allowed", className)}
|
| 24 |
-
{...props}
|
| 25 |
-
/>
|
| 26 |
-
);
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
| 30 |
-
return (
|
| 31 |
-
<div
|
| 32 |
-
data-slot="input-otp-group"
|
| 33 |
-
className={cn("flex items-center gap-1", className)}
|
| 34 |
-
{...props}
|
| 35 |
-
/>
|
| 36 |
-
);
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
function InputOTPSlot({
|
| 40 |
-
index,
|
| 41 |
-
className,
|
| 42 |
-
...props
|
| 43 |
-
}: React.ComponentProps<"div"> & {
|
| 44 |
-
index: number;
|
| 45 |
-
}) {
|
| 46 |
-
const inputOTPContext = React.useContext(OTPInputContext);
|
| 47 |
-
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
| 48 |
-
|
| 49 |
-
return (
|
| 50 |
-
<div
|
| 51 |
-
data-slot="input-otp-slot"
|
| 52 |
-
data-active={isActive}
|
| 53 |
-
className={cn(
|
| 54 |
-
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
| 55 |
-
className,
|
| 56 |
-
)}
|
| 57 |
-
{...props}
|
| 58 |
-
>
|
| 59 |
-
{char}
|
| 60 |
-
{hasFakeCaret && (
|
| 61 |
-
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
| 62 |
-
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
| 63 |
-
</div>
|
| 64 |
-
)}
|
| 65 |
-
</div>
|
| 66 |
-
);
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
| 70 |
-
return (
|
| 71 |
-
<div data-slot="input-otp-separator" role="separator" {...props}>
|
| 72 |
-
<MinusIcon />
|
| 73 |
-
</div>
|
| 74 |
-
);
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/input.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
|
| 3 |
-
import { cn } from "./utils";
|
| 4 |
-
|
| 5 |
-
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
| 6 |
-
return (
|
| 7 |
-
<input
|
| 8 |
-
type={type}
|
| 9 |
-
data-slot="input"
|
| 10 |
-
className={cn(
|
| 11 |
-
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 12 |
-
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
| 13 |
-
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 14 |
-
className,
|
| 15 |
-
)}
|
| 16 |
-
{...props}
|
| 17 |
-
/>
|
| 18 |
-
);
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
export { Input };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/label.tsx
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function Label({
|
| 9 |
-
className,
|
| 10 |
-
...props
|
| 11 |
-
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 12 |
-
return (
|
| 13 |
-
<LabelPrimitive.Root
|
| 14 |
-
data-slot="label"
|
| 15 |
-
className={cn(
|
| 16 |
-
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
| 17 |
-
className,
|
| 18 |
-
)}
|
| 19 |
-
{...props}
|
| 20 |
-
/>
|
| 21 |
-
);
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export { Label };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/menubar.tsx
DELETED
|
@@ -1,276 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as MenubarPrimitive from "@radix-ui/react-menubar@1.1.6";
|
| 5 |
-
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function Menubar({
|
| 10 |
-
className,
|
| 11 |
-
...props
|
| 12 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
| 13 |
-
return (
|
| 14 |
-
<MenubarPrimitive.Root
|
| 15 |
-
data-slot="menubar"
|
| 16 |
-
className={cn(
|
| 17 |
-
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
| 18 |
-
className,
|
| 19 |
-
)}
|
| 20 |
-
{...props}
|
| 21 |
-
/>
|
| 22 |
-
);
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
function MenubarMenu({
|
| 26 |
-
...props
|
| 27 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
| 28 |
-
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function MenubarGroup({
|
| 32 |
-
...props
|
| 33 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
| 34 |
-
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function MenubarPortal({
|
| 38 |
-
...props
|
| 39 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
| 40 |
-
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
function MenubarRadioGroup({
|
| 44 |
-
...props
|
| 45 |
-
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
| 46 |
-
return (
|
| 47 |
-
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
| 48 |
-
);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
function MenubarTrigger({
|
| 52 |
-
className,
|
| 53 |
-
...props
|
| 54 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
| 55 |
-
return (
|
| 56 |
-
<MenubarPrimitive.Trigger
|
| 57 |
-
data-slot="menubar-trigger"
|
| 58 |
-
className={cn(
|
| 59 |
-
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
| 60 |
-
className,
|
| 61 |
-
)}
|
| 62 |
-
{...props}
|
| 63 |
-
/>
|
| 64 |
-
);
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
function MenubarContent({
|
| 68 |
-
className,
|
| 69 |
-
align = "start",
|
| 70 |
-
alignOffset = -4,
|
| 71 |
-
sideOffset = 8,
|
| 72 |
-
...props
|
| 73 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
| 74 |
-
return (
|
| 75 |
-
<MenubarPortal>
|
| 76 |
-
<MenubarPrimitive.Content
|
| 77 |
-
data-slot="menubar-content"
|
| 78 |
-
align={align}
|
| 79 |
-
alignOffset={alignOffset}
|
| 80 |
-
sideOffset={sideOffset}
|
| 81 |
-
className={cn(
|
| 82 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
| 83 |
-
className,
|
| 84 |
-
)}
|
| 85 |
-
{...props}
|
| 86 |
-
/>
|
| 87 |
-
</MenubarPortal>
|
| 88 |
-
);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
function MenubarItem({
|
| 92 |
-
className,
|
| 93 |
-
inset,
|
| 94 |
-
variant = "default",
|
| 95 |
-
...props
|
| 96 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
| 97 |
-
inset?: boolean;
|
| 98 |
-
variant?: "default" | "destructive";
|
| 99 |
-
}) {
|
| 100 |
-
return (
|
| 101 |
-
<MenubarPrimitive.Item
|
| 102 |
-
data-slot="menubar-item"
|
| 103 |
-
data-inset={inset}
|
| 104 |
-
data-variant={variant}
|
| 105 |
-
className={cn(
|
| 106 |
-
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 107 |
-
className,
|
| 108 |
-
)}
|
| 109 |
-
{...props}
|
| 110 |
-
/>
|
| 111 |
-
);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
function MenubarCheckboxItem({
|
| 115 |
-
className,
|
| 116 |
-
children,
|
| 117 |
-
checked,
|
| 118 |
-
...props
|
| 119 |
-
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
| 120 |
-
return (
|
| 121 |
-
<MenubarPrimitive.CheckboxItem
|
| 122 |
-
data-slot="menubar-checkbox-item"
|
| 123 |
-
className={cn(
|
| 124 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 125 |
-
className,
|
| 126 |
-
)}
|
| 127 |
-
checked={checked}
|
| 128 |
-
{...props}
|
| 129 |
-
>
|
| 130 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 131 |
-
<MenubarPrimitive.ItemIndicator>
|
| 132 |
-
<CheckIcon className="size-4" />
|
| 133 |
-
</MenubarPrimitive.ItemIndicator>
|
| 134 |
-
</span>
|
| 135 |
-
{children}
|
| 136 |
-
</MenubarPrimitive.CheckboxItem>
|
| 137 |
-
);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
function MenubarRadioItem({
|
| 141 |
-
className,
|
| 142 |
-
children,
|
| 143 |
-
...props
|
| 144 |
-
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
| 145 |
-
return (
|
| 146 |
-
<MenubarPrimitive.RadioItem
|
| 147 |
-
data-slot="menubar-radio-item"
|
| 148 |
-
className={cn(
|
| 149 |
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 150 |
-
className,
|
| 151 |
-
)}
|
| 152 |
-
{...props}
|
| 153 |
-
>
|
| 154 |
-
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
| 155 |
-
<MenubarPrimitive.ItemIndicator>
|
| 156 |
-
<CircleIcon className="size-2 fill-current" />
|
| 157 |
-
</MenubarPrimitive.ItemIndicator>
|
| 158 |
-
</span>
|
| 159 |
-
{children}
|
| 160 |
-
</MenubarPrimitive.RadioItem>
|
| 161 |
-
);
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
function MenubarLabel({
|
| 165 |
-
className,
|
| 166 |
-
inset,
|
| 167 |
-
...props
|
| 168 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
| 169 |
-
inset?: boolean;
|
| 170 |
-
}) {
|
| 171 |
-
return (
|
| 172 |
-
<MenubarPrimitive.Label
|
| 173 |
-
data-slot="menubar-label"
|
| 174 |
-
data-inset={inset}
|
| 175 |
-
className={cn(
|
| 176 |
-
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
| 177 |
-
className,
|
| 178 |
-
)}
|
| 179 |
-
{...props}
|
| 180 |
-
/>
|
| 181 |
-
);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
function MenubarSeparator({
|
| 185 |
-
className,
|
| 186 |
-
...props
|
| 187 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
| 188 |
-
return (
|
| 189 |
-
<MenubarPrimitive.Separator
|
| 190 |
-
data-slot="menubar-separator"
|
| 191 |
-
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
| 192 |
-
{...props}
|
| 193 |
-
/>
|
| 194 |
-
);
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
function MenubarShortcut({
|
| 198 |
-
className,
|
| 199 |
-
...props
|
| 200 |
-
}: React.ComponentProps<"span">) {
|
| 201 |
-
return (
|
| 202 |
-
<span
|
| 203 |
-
data-slot="menubar-shortcut"
|
| 204 |
-
className={cn(
|
| 205 |
-
"text-muted-foreground ml-auto text-xs tracking-widest",
|
| 206 |
-
className,
|
| 207 |
-
)}
|
| 208 |
-
{...props}
|
| 209 |
-
/>
|
| 210 |
-
);
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
function MenubarSub({
|
| 214 |
-
...props
|
| 215 |
-
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
| 216 |
-
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
function MenubarSubTrigger({
|
| 220 |
-
className,
|
| 221 |
-
inset,
|
| 222 |
-
children,
|
| 223 |
-
...props
|
| 224 |
-
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
| 225 |
-
inset?: boolean;
|
| 226 |
-
}) {
|
| 227 |
-
return (
|
| 228 |
-
<MenubarPrimitive.SubTrigger
|
| 229 |
-
data-slot="menubar-sub-trigger"
|
| 230 |
-
data-inset={inset}
|
| 231 |
-
className={cn(
|
| 232 |
-
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
| 233 |
-
className,
|
| 234 |
-
)}
|
| 235 |
-
{...props}
|
| 236 |
-
>
|
| 237 |
-
{children}
|
| 238 |
-
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
| 239 |
-
</MenubarPrimitive.SubTrigger>
|
| 240 |
-
);
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
function MenubarSubContent({
|
| 244 |
-
className,
|
| 245 |
-
...props
|
| 246 |
-
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
| 247 |
-
return (
|
| 248 |
-
<MenubarPrimitive.SubContent
|
| 249 |
-
data-slot="menubar-sub-content"
|
| 250 |
-
className={cn(
|
| 251 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
| 252 |
-
className,
|
| 253 |
-
)}
|
| 254 |
-
{...props}
|
| 255 |
-
/>
|
| 256 |
-
);
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
export {
|
| 260 |
-
Menubar,
|
| 261 |
-
MenubarPortal,
|
| 262 |
-
MenubarMenu,
|
| 263 |
-
MenubarTrigger,
|
| 264 |
-
MenubarContent,
|
| 265 |
-
MenubarGroup,
|
| 266 |
-
MenubarSeparator,
|
| 267 |
-
MenubarLabel,
|
| 268 |
-
MenubarItem,
|
| 269 |
-
MenubarShortcut,
|
| 270 |
-
MenubarCheckboxItem,
|
| 271 |
-
MenubarRadioGroup,
|
| 272 |
-
MenubarRadioItem,
|
| 273 |
-
MenubarSub,
|
| 274 |
-
MenubarSubTrigger,
|
| 275 |
-
MenubarSubContent,
|
| 276 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/navigation-menu.tsx
DELETED
|
@@ -1,168 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
|
| 3 |
-
import { cva } from "class-variance-authority@0.7.1";
|
| 4 |
-
import { ChevronDownIcon } from "lucide-react@0.487.0";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function NavigationMenu({
|
| 9 |
-
className,
|
| 10 |
-
children,
|
| 11 |
-
viewport = true,
|
| 12 |
-
...props
|
| 13 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
| 14 |
-
viewport?: boolean;
|
| 15 |
-
}) {
|
| 16 |
-
return (
|
| 17 |
-
<NavigationMenuPrimitive.Root
|
| 18 |
-
data-slot="navigation-menu"
|
| 19 |
-
data-viewport={viewport}
|
| 20 |
-
className={cn(
|
| 21 |
-
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
| 22 |
-
className,
|
| 23 |
-
)}
|
| 24 |
-
{...props}
|
| 25 |
-
>
|
| 26 |
-
{children}
|
| 27 |
-
{viewport && <NavigationMenuViewport />}
|
| 28 |
-
</NavigationMenuPrimitive.Root>
|
| 29 |
-
);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function NavigationMenuList({
|
| 33 |
-
className,
|
| 34 |
-
...props
|
| 35 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
| 36 |
-
return (
|
| 37 |
-
<NavigationMenuPrimitive.List
|
| 38 |
-
data-slot="navigation-menu-list"
|
| 39 |
-
className={cn(
|
| 40 |
-
"group flex flex-1 list-none items-center justify-center gap-1",
|
| 41 |
-
className,
|
| 42 |
-
)}
|
| 43 |
-
{...props}
|
| 44 |
-
/>
|
| 45 |
-
);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function NavigationMenuItem({
|
| 49 |
-
className,
|
| 50 |
-
...props
|
| 51 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
| 52 |
-
return (
|
| 53 |
-
<NavigationMenuPrimitive.Item
|
| 54 |
-
data-slot="navigation-menu-item"
|
| 55 |
-
className={cn("relative", className)}
|
| 56 |
-
{...props}
|
| 57 |
-
/>
|
| 58 |
-
);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
const navigationMenuTriggerStyle = cva(
|
| 62 |
-
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
| 63 |
-
);
|
| 64 |
-
|
| 65 |
-
function NavigationMenuTrigger({
|
| 66 |
-
className,
|
| 67 |
-
children,
|
| 68 |
-
...props
|
| 69 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
| 70 |
-
return (
|
| 71 |
-
<NavigationMenuPrimitive.Trigger
|
| 72 |
-
data-slot="navigation-menu-trigger"
|
| 73 |
-
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
| 74 |
-
{...props}
|
| 75 |
-
>
|
| 76 |
-
{children}{" "}
|
| 77 |
-
<ChevronDownIcon
|
| 78 |
-
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
| 79 |
-
aria-hidden="true"
|
| 80 |
-
/>
|
| 81 |
-
</NavigationMenuPrimitive.Trigger>
|
| 82 |
-
);
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function NavigationMenuContent({
|
| 86 |
-
className,
|
| 87 |
-
...props
|
| 88 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
| 89 |
-
return (
|
| 90 |
-
<NavigationMenuPrimitive.Content
|
| 91 |
-
data-slot="navigation-menu-content"
|
| 92 |
-
className={cn(
|
| 93 |
-
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
| 94 |
-
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
| 95 |
-
className,
|
| 96 |
-
)}
|
| 97 |
-
{...props}
|
| 98 |
-
/>
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
function NavigationMenuViewport({
|
| 103 |
-
className,
|
| 104 |
-
...props
|
| 105 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
| 106 |
-
return (
|
| 107 |
-
<div
|
| 108 |
-
className={cn(
|
| 109 |
-
"absolute top-full left-0 isolate z-50 flex justify-center",
|
| 110 |
-
)}
|
| 111 |
-
>
|
| 112 |
-
<NavigationMenuPrimitive.Viewport
|
| 113 |
-
data-slot="navigation-menu-viewport"
|
| 114 |
-
className={cn(
|
| 115 |
-
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
| 116 |
-
className,
|
| 117 |
-
)}
|
| 118 |
-
{...props}
|
| 119 |
-
/>
|
| 120 |
-
</div>
|
| 121 |
-
);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
function NavigationMenuLink({
|
| 125 |
-
className,
|
| 126 |
-
...props
|
| 127 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
| 128 |
-
return (
|
| 129 |
-
<NavigationMenuPrimitive.Link
|
| 130 |
-
data-slot="navigation-menu-link"
|
| 131 |
-
className={cn(
|
| 132 |
-
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
| 133 |
-
className,
|
| 134 |
-
)}
|
| 135 |
-
{...props}
|
| 136 |
-
/>
|
| 137 |
-
);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
function NavigationMenuIndicator({
|
| 141 |
-
className,
|
| 142 |
-
...props
|
| 143 |
-
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
| 144 |
-
return (
|
| 145 |
-
<NavigationMenuPrimitive.Indicator
|
| 146 |
-
data-slot="navigation-menu-indicator"
|
| 147 |
-
className={cn(
|
| 148 |
-
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
| 149 |
-
className,
|
| 150 |
-
)}
|
| 151 |
-
{...props}
|
| 152 |
-
>
|
| 153 |
-
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
| 154 |
-
</NavigationMenuPrimitive.Indicator>
|
| 155 |
-
);
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
export {
|
| 159 |
-
NavigationMenu,
|
| 160 |
-
NavigationMenuList,
|
| 161 |
-
NavigationMenuItem,
|
| 162 |
-
NavigationMenuContent,
|
| 163 |
-
NavigationMenuTrigger,
|
| 164 |
-
NavigationMenuLink,
|
| 165 |
-
NavigationMenuIndicator,
|
| 166 |
-
NavigationMenuViewport,
|
| 167 |
-
navigationMenuTriggerStyle,
|
| 168 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/pagination.tsx
DELETED
|
@@ -1,127 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import {
|
| 3 |
-
ChevronLeftIcon,
|
| 4 |
-
ChevronRightIcon,
|
| 5 |
-
MoreHorizontalIcon,
|
| 6 |
-
} from "lucide-react@0.487.0";
|
| 7 |
-
|
| 8 |
-
import { cn } from "./utils";
|
| 9 |
-
import { Button, buttonVariants } from "./button";
|
| 10 |
-
|
| 11 |
-
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
| 12 |
-
return (
|
| 13 |
-
<nav
|
| 14 |
-
role="navigation"
|
| 15 |
-
aria-label="pagination"
|
| 16 |
-
data-slot="pagination"
|
| 17 |
-
className={cn("mx-auto flex w-full justify-center", className)}
|
| 18 |
-
{...props}
|
| 19 |
-
/>
|
| 20 |
-
);
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function PaginationContent({
|
| 24 |
-
className,
|
| 25 |
-
...props
|
| 26 |
-
}: React.ComponentProps<"ul">) {
|
| 27 |
-
return (
|
| 28 |
-
<ul
|
| 29 |
-
data-slot="pagination-content"
|
| 30 |
-
className={cn("flex flex-row items-center gap-1", className)}
|
| 31 |
-
{...props}
|
| 32 |
-
/>
|
| 33 |
-
);
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
| 37 |
-
return <li data-slot="pagination-item" {...props} />;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
type PaginationLinkProps = {
|
| 41 |
-
isActive?: boolean;
|
| 42 |
-
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
| 43 |
-
React.ComponentProps<"a">;
|
| 44 |
-
|
| 45 |
-
function PaginationLink({
|
| 46 |
-
className,
|
| 47 |
-
isActive,
|
| 48 |
-
size = "icon",
|
| 49 |
-
...props
|
| 50 |
-
}: PaginationLinkProps) {
|
| 51 |
-
return (
|
| 52 |
-
<a
|
| 53 |
-
aria-current={isActive ? "page" : undefined}
|
| 54 |
-
data-slot="pagination-link"
|
| 55 |
-
data-active={isActive}
|
| 56 |
-
className={cn(
|
| 57 |
-
buttonVariants({
|
| 58 |
-
variant: isActive ? "outline" : "ghost",
|
| 59 |
-
size,
|
| 60 |
-
}),
|
| 61 |
-
className,
|
| 62 |
-
)}
|
| 63 |
-
{...props}
|
| 64 |
-
/>
|
| 65 |
-
);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
function PaginationPrevious({
|
| 69 |
-
className,
|
| 70 |
-
...props
|
| 71 |
-
}: React.ComponentProps<typeof PaginationLink>) {
|
| 72 |
-
return (
|
| 73 |
-
<PaginationLink
|
| 74 |
-
aria-label="Go to previous page"
|
| 75 |
-
size="default"
|
| 76 |
-
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
| 77 |
-
{...props}
|
| 78 |
-
>
|
| 79 |
-
<ChevronLeftIcon />
|
| 80 |
-
<span className="hidden sm:block">Previous</span>
|
| 81 |
-
</PaginationLink>
|
| 82 |
-
);
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function PaginationNext({
|
| 86 |
-
className,
|
| 87 |
-
...props
|
| 88 |
-
}: React.ComponentProps<typeof PaginationLink>) {
|
| 89 |
-
return (
|
| 90 |
-
<PaginationLink
|
| 91 |
-
aria-label="Go to next page"
|
| 92 |
-
size="default"
|
| 93 |
-
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
| 94 |
-
{...props}
|
| 95 |
-
>
|
| 96 |
-
<span className="hidden sm:block">Next</span>
|
| 97 |
-
<ChevronRightIcon />
|
| 98 |
-
</PaginationLink>
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
function PaginationEllipsis({
|
| 103 |
-
className,
|
| 104 |
-
...props
|
| 105 |
-
}: React.ComponentProps<"span">) {
|
| 106 |
-
return (
|
| 107 |
-
<span
|
| 108 |
-
aria-hidden
|
| 109 |
-
data-slot="pagination-ellipsis"
|
| 110 |
-
className={cn("flex size-9 items-center justify-center", className)}
|
| 111 |
-
{...props}
|
| 112 |
-
>
|
| 113 |
-
<MoreHorizontalIcon className="size-4" />
|
| 114 |
-
<span className="sr-only">More pages</span>
|
| 115 |
-
</span>
|
| 116 |
-
);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
export {
|
| 120 |
-
Pagination,
|
| 121 |
-
PaginationContent,
|
| 122 |
-
PaginationLink,
|
| 123 |
-
PaginationItem,
|
| 124 |
-
PaginationPrevious,
|
| 125 |
-
PaginationNext,
|
| 126 |
-
PaginationEllipsis,
|
| 127 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/popover.tsx
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as PopoverPrimitive from "@radix-ui/react-popover@1.1.6";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function Popover({
|
| 9 |
-
...props
|
| 10 |
-
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
| 11 |
-
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
function PopoverTrigger({
|
| 15 |
-
...props
|
| 16 |
-
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
| 17 |
-
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
function PopoverContent({
|
| 21 |
-
className,
|
| 22 |
-
align = "center",
|
| 23 |
-
sideOffset = 4,
|
| 24 |
-
...props
|
| 25 |
-
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
| 26 |
-
return (
|
| 27 |
-
<PopoverPrimitive.Portal>
|
| 28 |
-
<PopoverPrimitive.Content
|
| 29 |
-
data-slot="popover-content"
|
| 30 |
-
align={align}
|
| 31 |
-
sideOffset={sideOffset}
|
| 32 |
-
className={cn(
|
| 33 |
-
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
| 34 |
-
className,
|
| 35 |
-
)}
|
| 36 |
-
{...props}
|
| 37 |
-
/>
|
| 38 |
-
</PopoverPrimitive.Portal>
|
| 39 |
-
);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
function PopoverAnchor({
|
| 43 |
-
...props
|
| 44 |
-
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
| 45 |
-
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/progress.tsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as ProgressPrimitive from "@radix-ui/react-progress@1.1.2";
|
| 5 |
-
|
| 6 |
-
import { cn } from "./utils";
|
| 7 |
-
|
| 8 |
-
function Progress({
|
| 9 |
-
className,
|
| 10 |
-
value,
|
| 11 |
-
...props
|
| 12 |
-
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
| 13 |
-
return (
|
| 14 |
-
<ProgressPrimitive.Root
|
| 15 |
-
data-slot="progress"
|
| 16 |
-
className={cn(
|
| 17 |
-
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
| 18 |
-
className,
|
| 19 |
-
)}
|
| 20 |
-
{...props}
|
| 21 |
-
>
|
| 22 |
-
<ProgressPrimitive.Indicator
|
| 23 |
-
data-slot="progress-indicator"
|
| 24 |
-
className="bg-primary h-full w-full flex-1 transition-all"
|
| 25 |
-
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
| 26 |
-
/>
|
| 27 |
-
</ProgressPrimitive.Root>
|
| 28 |
-
);
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
export { Progress };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/radio-group.tsx
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
|
| 5 |
-
import { CircleIcon } from "lucide-react@0.487.0";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function RadioGroup({
|
| 10 |
-
className,
|
| 11 |
-
...props
|
| 12 |
-
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
| 13 |
-
return (
|
| 14 |
-
<RadioGroupPrimitive.Root
|
| 15 |
-
data-slot="radio-group"
|
| 16 |
-
className={cn("grid gap-3", className)}
|
| 17 |
-
{...props}
|
| 18 |
-
/>
|
| 19 |
-
);
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
function RadioGroupItem({
|
| 23 |
-
className,
|
| 24 |
-
...props
|
| 25 |
-
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
| 26 |
-
return (
|
| 27 |
-
<RadioGroupPrimitive.Item
|
| 28 |
-
data-slot="radio-group-item"
|
| 29 |
-
className={cn(
|
| 30 |
-
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
| 31 |
-
className,
|
| 32 |
-
)}
|
| 33 |
-
{...props}
|
| 34 |
-
>
|
| 35 |
-
<RadioGroupPrimitive.Indicator
|
| 36 |
-
data-slot="radio-group-indicator"
|
| 37 |
-
className="relative flex items-center justify-center"
|
| 38 |
-
>
|
| 39 |
-
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
| 40 |
-
</RadioGroupPrimitive.Indicator>
|
| 41 |
-
</RadioGroupPrimitive.Item>
|
| 42 |
-
);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
export { RadioGroup, RadioGroupItem };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/components/ui/resizable.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
import { GripVerticalIcon } from "lucide-react@0.487.0";
|
| 5 |
-
import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
|
| 6 |
-
|
| 7 |
-
import { cn } from "./utils";
|
| 8 |
-
|
| 9 |
-
function ResizablePanelGroup({
|
| 10 |
-
className,
|
| 11 |
-
...props
|
| 12 |
-
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
| 13 |
-
return (
|
| 14 |
-
<ResizablePrimitive.PanelGroup
|
| 15 |
-
data-slot="resizable-panel-group"
|
| 16 |
-
className={cn(
|
| 17 |
-
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
| 18 |
-
className,
|
| 19 |
-
)}
|
| 20 |
-
{...props}
|
| 21 |
-
/>
|
| 22 |
-
);
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
function ResizablePanel({
|
| 26 |
-
...props
|
| 27 |
-
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
| 28 |
-
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function ResizableHandle({
|
| 32 |
-
withHandle,
|
| 33 |
-
className,
|
| 34 |
-
...props
|
| 35 |
-
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
| 36 |
-
withHandle?: boolean;
|
| 37 |
-
}) {
|
| 38 |
-
return (
|
| 39 |
-
<ResizablePrimitive.PanelResizeHandle
|
| 40 |
-
data-slot="resizable-handle"
|
| 41 |
-
className={cn(
|
| 42 |
-
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
| 43 |
-
className,
|
| 44 |
-
)}
|
| 45 |
-
{...props}
|
| 46 |
-
>
|
| 47 |
-
{withHandle && (
|
| 48 |
-
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
| 49 |
-
<GripVerticalIcon className="size-2.5" />
|
| 50 |
-
</div>
|
| 51 |
-
)}
|
| 52 |
-
</ResizablePrimitive.PanelResizeHandle>
|
| 53 |
-
);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|