Compare commits
No commits in common. "ed6b010ecc8aba24e3c4c5a24cdb62d1a9a840f1" and "b35d2d4fe7c39b76b6c51480ade538cb786c9a7c" have entirely different histories.
ed6b010ecc
...
b35d2d4fe7
@ -342,10 +342,7 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
}
|
}
|
||||||
}, [displayParts.length]);
|
}, [displayParts.length]);
|
||||||
|
|
||||||
const activeThinkingIdx =
|
const firstThinkingIdx = displayParts.findIndex((p) => p.type === "thinking");
|
||||||
!displayDone && displayParts[displayParts.length - 1]?.type === "thinking"
|
|
||||||
? displayParts.length - 1
|
|
||||||
: -1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={contentRef} className="flex gap-4 px-4 py-3 max-w-3xl mx-auto w-full">
|
<div ref={contentRef} className="flex gap-4 px-4 py-3 max-w-3xl mx-auto w-full">
|
||||||
@ -367,12 +364,12 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
<div className="text-sm" style={{ color: "var(--text-primary)" }}>
|
<div className="text-sm" style={{ color: "var(--text-primary)" }}>
|
||||||
{displayParts.map((part, i) => {
|
{displayParts.map((part, i) => {
|
||||||
if (part.type === "thinking") {
|
if (part.type === "thinking") {
|
||||||
|
const isActivelyThinking = !displayDone && i === firstThinkingIdx;
|
||||||
return (
|
return (
|
||||||
<StreamingReasoningBlock
|
<Reasoning key={i} isStreaming={isActivelyThinking} defaultOpen={isActivelyThinking}>
|
||||||
key={i}
|
<ReasoningTrigger />
|
||||||
content={part.content}
|
<ReasoningContent>{part.content}</ReasoningContent>
|
||||||
isActivelyThinking={i === activeThinkingIdx}
|
</Reasoning>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (part.type === "tool_call") {
|
if (part.type === "tool_call") {
|
||||||
@ -410,59 +407,6 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StreamingReasoningBlock({
|
|
||||||
content,
|
|
||||||
isActivelyThinking,
|
|
||||||
}: {
|
|
||||||
content: string;
|
|
||||||
isActivelyThinking: boolean;
|
|
||||||
}) {
|
|
||||||
const [manualOpen, setManualOpen] = useState(false);
|
|
||||||
const [autoCollapsed, setAutoCollapsed] = useState(false);
|
|
||||||
const wasActivelyThinkingRef = useRef(isActivelyThinking);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const wasActivelyThinking = wasActivelyThinkingRef.current;
|
|
||||||
|
|
||||||
if (isActivelyThinking && !wasActivelyThinking) {
|
|
||||||
setAutoCollapsed(false);
|
|
||||||
setManualOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isActivelyThinking && wasActivelyThinking) {
|
|
||||||
setAutoCollapsed(false);
|
|
||||||
setManualOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasActivelyThinkingRef.current = isActivelyThinking;
|
|
||||||
}, [isActivelyThinking]);
|
|
||||||
|
|
||||||
const isOpen = isActivelyThinking ? !autoCollapsed : manualOpen;
|
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
|
||||||
(nextOpen: boolean) => {
|
|
||||||
if (isActivelyThinking) {
|
|
||||||
setAutoCollapsed(!nextOpen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setManualOpen(nextOpen);
|
|
||||||
},
|
|
||||||
[isActivelyThinking]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Reasoning
|
|
||||||
autoLifecycle={false}
|
|
||||||
isStreaming={isActivelyThinking}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
open={isOpen}
|
|
||||||
>
|
|
||||||
<ReasoningTrigger />
|
|
||||||
<ReasoningContent>{content}</ReasoningContent>
|
|
||||||
</Reasoning>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StreamingCursor() {
|
function StreamingCursor() {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { issueSummary } from "@/client/api";
|
|||||||
import { SettingsModalContext } from "@/components/settings/SettingsModalContext";
|
import { SettingsModalContext } from "@/components/settings/SettingsModalContext";
|
||||||
import { SettingsModal } from "@/components/settings/SettingsModal";
|
import { SettingsModal } from "@/components/settings/SettingsModal";
|
||||||
import { SettingsDataCacheProvider } from "@/components/settings/SettingsDataCache";
|
import { SettingsDataCacheProvider } from "@/components/settings/SettingsDataCache";
|
||||||
import { GlobalSearchPalette } from "@/components/search/GlobalSearchPalette";
|
|
||||||
import { initWsClient, getWsClient } from "@/ws/hooks";
|
import { initWsClient, getWsClient } from "@/ws/hooks";
|
||||||
import type { WsClient } from "@/ws/client";
|
import type { WsClient } from "@/ws/client";
|
||||||
|
|
||||||
@ -95,7 +94,6 @@ export function RootLayout() {
|
|||||||
<SettingsDataCacheProvider>
|
<SettingsDataCacheProvider>
|
||||||
<SettingsModalContext.Provider value={modalCtx}>
|
<SettingsModalContext.Provider value={modalCtx}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<GlobalSearchPalette />
|
|
||||||
{showSettingsModal && <SettingsModal />}
|
{showSettingsModal && <SettingsModal />}
|
||||||
</SettingsModalContext.Provider>
|
</SettingsModalContext.Provider>
|
||||||
</SettingsDataCacheProvider>
|
</SettingsDataCacheProvider>
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { useCreateRepoMutation } from "@/hooks/useReposQuery";
|
|||||||
import { useCreateRoomMutation } from "@/hooks/useRoomsQuery";
|
import { useCreateRoomMutation } from "@/hooks/useRoomsQuery";
|
||||||
import { useBoardOperations } from "@/hooks/useBoardOperations";
|
import { useBoardOperations } from "@/hooks/useBoardOperations";
|
||||||
import { useCreateSkillMutation } from "@/hooks/useSkillsQuery";
|
import { useCreateSkillMutation } from "@/hooks/useSkillsQuery";
|
||||||
import { projectInviteUser } from "@/client/api";
|
|
||||||
import type { MemberRole } from "@/client/model";
|
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
Hash,
|
Hash,
|
||||||
@ -17,33 +15,23 @@ import {
|
|||||||
FolderPlus,
|
FolderPlus,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
Kanban,
|
Kanban,
|
||||||
Zap,
|
Zap
|
||||||
Mail,
|
|
||||||
UserPlus,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
|
|
||||||
interface ProjectCreateMenuModalProps {
|
interface ProjectCreateMenuModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
initialTab?: "repo" | "channel" | "board" | "skill" | "invite";
|
initialTab?: "repo" | "channel" | "board" | "skill";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: ProjectCreateMenuModalProps) {
|
export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: ProjectCreateMenuModalProps) {
|
||||||
const { projectName } = useParams<{ projectName: string }>();
|
const { projectName } = useParams<{ projectName: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<"repo" | "channel" | "board" | "skill" | "invite">(initialTab);
|
const [activeTab, setActiveTab] = useState<"repo" | "channel" | "board" | "skill">(initialTab);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -76,12 +64,6 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
|
|||||||
});
|
});
|
||||||
const createSkill = useCreateSkillMutation(projectName);
|
const createSkill = useCreateSkillMutation(projectName);
|
||||||
|
|
||||||
// --- Invite Form State ---
|
|
||||||
const [inviteForm, setInviteForm] = useState({
|
|
||||||
email: "",
|
|
||||||
scope: "Member" as MemberRole,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCreateRepo = async (e: React.FormEvent) => {
|
const handleCreateRepo = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!repoForm.repo_name.trim()) return;
|
if (!repoForm.repo_name.trim()) return;
|
||||||
@ -167,25 +149,6 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInviteUser = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!projectName || !inviteForm.email.trim()) return;
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
await projectInviteUser(projectName, {
|
|
||||||
email: inviteForm.email.trim(),
|
|
||||||
scope: inviteForm.scope,
|
|
||||||
});
|
|
||||||
setInviteForm({ email: "", scope: "Member" });
|
|
||||||
onClose();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to send invitation.");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
<div
|
<div
|
||||||
@ -197,7 +160,7 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
|
|||||||
<div className="px-1 pt-1 border-b" style={{ backgroundColor: "var(--surface-rail)", borderColor: "var(--border-default)" }}>
|
<div className="px-1 pt-1 border-b" style={{ backgroundColor: "var(--surface-rail)", borderColor: "var(--border-default)" }}>
|
||||||
<div className="flex items-center justify-between px-4 py-2">
|
<div className="flex items-center justify-between px-4 py-2">
|
||||||
<span className="text-[14px] font-bold flex items-center gap-2" style={{ color: "var(--text-primary)" }}>
|
<span className="text-[14px] font-bold flex items-center gap-2" style={{ color: "var(--text-primary)" }}>
|
||||||
<PlusCircle className="w-4 h-4" style={{ color: "var(--accent)" }} /> Quick Start
|
<PlusCircle className="w-4 h-4" style={{ color: "var(--accent)" }} /> Quick Create
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -213,7 +176,6 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
|
|||||||
{ id: "channel", label: "Channel", icon: MessageSquarePlus },
|
{ id: "channel", label: "Channel", icon: MessageSquarePlus },
|
||||||
{ id: "board", label: "Board", icon: Kanban },
|
{ id: "board", label: "Board", icon: Kanban },
|
||||||
{ id: "skill", label: "Skill", icon: Zap },
|
{ id: "skill", label: "Skill", icon: Zap },
|
||||||
{ id: "invite", label: "Invite", icon: UserPlus },
|
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@ -419,56 +381,6 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "invite" && (
|
|
||||||
<form onSubmit={handleInviteUser} className="space-y-5">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label className="text-[11px] font-bold uppercase tracking-widest" style={{ color: "var(--text-muted)" }}>Invite by Email</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: "var(--text-muted)" }} />
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
placeholder="user@example.com"
|
|
||||||
value={inviteForm.email}
|
|
||||||
onChange={e => setInviteForm({...inviteForm, email: e.target.value})}
|
|
||||||
className="border-none h-10 pl-9 text-[14px]"
|
|
||||||
style={{ backgroundColor: "var(--surface-ground)", color: "var(--text-primary)" }}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label className="text-[11px] font-bold uppercase tracking-widest" style={{ color: "var(--text-muted)" }}>Role</Label>
|
|
||||||
<Select
|
|
||||||
value={inviteForm.scope}
|
|
||||||
onValueChange={(value) => setInviteForm({...inviteForm, scope: value as MemberRole})}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-10 border-none" style={{ backgroundColor: "var(--surface-ground)", color: "var(--text-primary)" }}>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value="Member">Member</SelectItem>
|
|
||||||
<SelectItem value="Admin">Admin</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2 flex flex-col gap-3">
|
|
||||||
{error && <p className="text-[12px] font-medium p-2 rounded border" style={{ color: "var(--destructive)", backgroundColor: "var(--destructive-alpha10)", borderColor: "var(--destructive)" }}>{error}</p>}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={!inviteForm.email.trim() || loading}
|
|
||||||
className="w-full font-bold h-10"
|
|
||||||
style={{ backgroundColor: "var(--accent)", color: "var(--accent-fg)" }}
|
|
||||||
>
|
|
||||||
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <UserPlus className="w-4 h-4 mr-2" />}
|
|
||||||
Send Invitation
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { useProjectInfo } from "@/hooks/useProjectInfo";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { isProjectAdminRole, isProjectOwnerRole } from "@/lib/project-permissions";
|
|
||||||
|
|
||||||
const NEXT_ROLE: Record<string, string> = { admin: "member", member: "admin" };
|
const NEXT_ROLE: Record<string, string> = { admin: "member", member: "admin" };
|
||||||
|
|
||||||
@ -15,8 +14,8 @@ export function MembersSettings() {
|
|||||||
const { projectName } = useParams<{ projectName: string }>();
|
const { projectName } = useParams<{ projectName: string }>();
|
||||||
const { data: projectInfo } = useProjectInfo(projectName);
|
const { data: projectInfo } = useProjectInfo(projectName);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isCurrentAdmin = isProjectAdminRole(projectInfo?.role);
|
const isCurrentAdmin = projectInfo?.role === "Owner" || projectInfo?.role === "Admin";
|
||||||
const isCurrentOwner = isProjectOwnerRole(projectInfo?.role);
|
const isCurrentOwner = projectInfo?.role === "Owner";
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["project-members-grouped", projectName],
|
queryKey: ["project-members-grouped", projectName],
|
||||||
@ -65,7 +64,7 @@ export function MembersSettings() {
|
|||||||
<div className="space-y-1 rounded-lg overflow-hidden" style={{ border: "1px solid var(--border-default)" }}>
|
<div className="space-y-1 rounded-lg overflow-hidden" style={{ border: "1px solid var(--border-default)" }}>
|
||||||
{group.members.map(member => {
|
{group.members.map(member => {
|
||||||
const isSelf = member.user_id === projectInfo?.created_by;
|
const isSelf = member.user_id === projectInfo?.created_by;
|
||||||
const canManage = isCurrentAdmin && !isProjectOwnerRole(member.scope) && !isSelf;
|
const canManage = isCurrentAdmin && member.scope !== "Owner" && !isSelf;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={member.user_id} className="flex items-center gap-3 px-4 py-3" style={{ backgroundColor: "var(--surface-elevated)", borderBottom: "1px solid var(--border-subtle)" }}>
|
<div key={member.user_id} className="flex items-center gap-3 px-4 py-3" style={{ backgroundColor: "var(--surface-elevated)", borderBottom: "1px solid var(--border-subtle)" }}>
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { Outlet, useParams, NavLink } from "react-router-dom";
|
import { Outlet, useParams, NavLink } from "react-router-dom";
|
||||||
import { useProjectInfo } from "@/hooks/useProjectInfo";
|
import { useProjectInfo } from "@/hooks/useProjectInfo";
|
||||||
import { Loader2, Lock, Settings, Users, Key, Tag, CreditCard } from "lucide-react";
|
import { Loader2, Lock, Settings, Users, Key, Tag, CreditCard } from "lucide-react";
|
||||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
|
||||||
|
|
||||||
export function ProjectSettingsLayout() {
|
export function ProjectSettingsLayout() {
|
||||||
const { projectName } = useParams<{ projectName: string }>();
|
const { projectName } = useParams<{ projectName: string }>();
|
||||||
const { data: info, isLoading, error } = useProjectInfo(projectName);
|
const { data: info, isLoading, error } = useProjectInfo(projectName);
|
||||||
|
|
||||||
const isAdmin = isProjectAdminRole(info?.role);
|
const isAdmin = info?.role === "Owner" || info?.role === "Admin";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ to: "", end: true, icon: Settings, label: "General" },
|
{ to: "", end: true, icon: Settings, label: "General" },
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
autoLifecycle?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 1000;
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
@ -65,7 +64,6 @@ export const Reasoning = memo(
|
|||||||
defaultOpen,
|
defaultOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
duration: durationProp,
|
duration: durationProp,
|
||||||
autoLifecycle = true,
|
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ReasoningProps) => {
|
}: ReasoningProps) => {
|
||||||
@ -102,16 +100,15 @@ export const Reasoning = memo(
|
|||||||
|
|
||||||
// Auto-open when streaming starts (unless explicitly closed)
|
// Auto-open when streaming starts (unless explicitly closed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoLifecycle && isStreaming && !isOpen && !isExplicitlyClosed) {
|
if (isStreaming && !isOpen && !isExplicitlyClosed) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}, [autoLifecycle, isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
}, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
||||||
|
|
||||||
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
hasEverStreamedRef.current &&
|
hasEverStreamedRef.current &&
|
||||||
autoLifecycle &&
|
|
||||||
!isStreaming &&
|
!isStreaming &&
|
||||||
isOpen &&
|
isOpen &&
|
||||||
!hasAutoClosed
|
!hasAutoClosed
|
||||||
@ -123,7 +120,7 @@ export const Reasoning = memo(
|
|||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [autoLifecycle, isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
}, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(newOpen: boolean) => {
|
(newOpen: boolean) => {
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import {useProjectInfo} from "@/hooks/useProjectInfo";
|
|||||||
import {Hash, PanelLeftClose, Plus, Search, Settings} from "lucide-react";
|
import {Hash, PanelLeftClose, Plus, Search, Settings} from "lucide-react";
|
||||||
import {CHANNEL_SIDEBAR} from "@/css/layout/styles";
|
import {CHANNEL_SIDEBAR} from "@/css/layout/styles";
|
||||||
import {ProjectCreateMenuModal} from "@/app/project";
|
import {ProjectCreateMenuModal} from "@/app/project";
|
||||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
|
||||||
import { openGlobalSearch } from "@/components/search/global-search-events";
|
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{
|
{
|
||||||
@ -62,7 +60,7 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
|
|||||||
|
|
||||||
const isSettingsActive = isActive("settings");
|
const isSettingsActive = isActive("settings");
|
||||||
|
|
||||||
const showSettings = isProjectAdminRole(projectInfo?.role);
|
const showSettings = projectInfo?.role === "Owner" || projectInfo?.role === "Admin";
|
||||||
|
|
||||||
const uncategorizedRooms = useMemo(
|
const uncategorizedRooms = useMemo(
|
||||||
() => rooms.filter((r) => !r.isMuted && !r.category),
|
() => rooms.filter((r) => !r.isMuted && !r.category),
|
||||||
@ -100,10 +98,9 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={openGlobalSearch}
|
|
||||||
className={CHANNEL_SIDEBAR.iconButton}
|
className={CHANNEL_SIDEBAR.iconButton}
|
||||||
style={{color: "var(--text-secondary)"}}
|
style={{color: "var(--text-secondary)"}}
|
||||||
title="Search (Ctrl+Alt+F)"
|
title="Search"
|
||||||
>
|
>
|
||||||
<Search className="w-[14px] h-[14px]"/>
|
<Search className="w-[14px] h-[14px]"/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import { useProjectLayout } from "@/app/project/layout";
|
|||||||
import { useProjectsQuery } from "@/hooks/useProjectsQuery";
|
import { useProjectsQuery } from "@/hooks/useProjectsQuery";
|
||||||
import { useOptionalRoom } from "@/contexts/room";
|
import { useOptionalRoom } from "@/contexts/room";
|
||||||
import { RoomSettingsModal } from "@/app/project/channel/RoomSettingsModal";
|
import { RoomSettingsModal } from "@/app/project/channel/RoomSettingsModal";
|
||||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
|
||||||
import { openGlobalSearch } from "@/components/search/global-search-events";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@ -44,18 +42,15 @@ const ME_NAV_SIBLINGS: BreadcrumbSibling[] = [
|
|||||||
{ label: "Invitations", path: "/me/invitations" },
|
{ label: "Invitations", path: "/me/invitations" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getProjectNavSiblings(projectName: string, showSettings: boolean): BreadcrumbSibling[] {
|
function getProjectNavSiblings(projectName: string): BreadcrumbSibling[] {
|
||||||
const items = [
|
return [
|
||||||
{ label: "Repository", path: `/${projectName}/repos` },
|
{ label: "Repository", path: `/${projectName}/repos` },
|
||||||
{ label: "Issues", path: `/${projectName}/issues` },
|
{ label: "Issues", path: `/${projectName}/issues` },
|
||||||
{ label: "Skills", path: `/${projectName}/skills` },
|
{ label: "Skills", path: `/${projectName}/skills` },
|
||||||
{ label: "Board", path: `/${projectName}/board` },
|
{ label: "Board", path: `/${projectName}/board` },
|
||||||
{ label: "Chat", path: `/${projectName}/chat` },
|
{ label: "Chat", path: `/${projectName}/chat` },
|
||||||
|
{ label: "Settings", path: `/${projectName}/settings` },
|
||||||
];
|
];
|
||||||
if (showSettings) {
|
|
||||||
items.push({ label: "Settings", path: `/${projectName}/settings` });
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRepoTabSiblings(repoBasePath: string): BreadcrumbSibling[] {
|
function getRepoTabSiblings(repoBasePath: string): BreadcrumbSibling[] {
|
||||||
@ -72,7 +67,6 @@ function getRepoTabSiblings(repoBasePath: string): BreadcrumbSibling[] {
|
|||||||
function getSegmentSiblings(
|
function getSegmentSiblings(
|
||||||
segment: BreadcrumbSegment,
|
segment: BreadcrumbSegment,
|
||||||
projects: Array<{ name: string; display_name: string }>,
|
projects: Array<{ name: string; display_name: string }>,
|
||||||
canManageProject = false,
|
|
||||||
): BreadcrumbSibling[] | null {
|
): BreadcrumbSibling[] | null {
|
||||||
if (segment.isLast) return null;
|
if (segment.isLast) return null;
|
||||||
|
|
||||||
@ -92,7 +86,7 @@ function getSegmentSiblings(
|
|||||||
|
|
||||||
if (depth === 2) {
|
if (depth === 2) {
|
||||||
if (parts[0] === "me") return ME_NAV_SIBLINGS;
|
if (parts[0] === "me") return ME_NAV_SIBLINGS;
|
||||||
return getProjectNavSiblings(parts[0], canManageProject);
|
return getProjectNavSiblings(parts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth >= 3 && parts[1] === "repo") {
|
if (depth >= 3 && parts[1] === "repo") {
|
||||||
@ -174,10 +168,9 @@ const TOOLBAR_ICONS = [
|
|||||||
export const Header = memo(function Header() {
|
export const Header = memo(function Header() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { segments, projects } = useBreadcrumbs();
|
const { segments, projects } = useBreadcrumbs();
|
||||||
const { isProjectMember, projectInfo, showMembers, setShowMembers } = useProjectLayout();
|
const { isProjectMember, showMembers, setShowMembers } = useProjectLayout();
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const roomContext = useOptionalRoom();
|
const roomContext = useOptionalRoom();
|
||||||
const canManageProject = isProjectAdminRole(projectInfo?.role);
|
|
||||||
|
|
||||||
const handleCopy = useCallback((e: React.MouseEvent, text: string) => {
|
const handleCopy = useCallback((e: React.MouseEvent, text: string) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -202,7 +195,7 @@ export const Header = memo(function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{segments.map((segment, idx) => {
|
{segments.map((segment, idx) => {
|
||||||
const siblings = getSegmentSiblings(segment, projects, canManageProject);
|
const siblings = getSegmentSiblings(segment, projects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={segment.path + idx} className="flex items-center gap-1 min-w-0">
|
<span key={segment.path + idx} className="flex items-center gap-1 min-w-0">
|
||||||
@ -304,10 +297,9 @@ export const Header = memo(function Header() {
|
|||||||
{TOOLBAR_ICONS.map((icon, i) => (
|
{TOOLBAR_ICONS.map((icon, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={icon.label === "Search" ? openGlobalSearch : undefined}
|
|
||||||
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
|
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
|
||||||
style={{ color: "var(--text-secondary)" }}
|
style={{ color: "var(--text-secondary)" }}
|
||||||
title={icon.label === "Search" ? "Search (Ctrl+Alt+F)" : icon.label}
|
title={icon.label}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-[18px] h-[18px]"
|
className="w-[18px] h-[18px]"
|
||||||
|
|||||||
@ -1,248 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
Boxes,
|
|
||||||
FolderGit2,
|
|
||||||
Hash,
|
|
||||||
Kanban,
|
|
||||||
LayoutDashboard,
|
|
||||||
MessageSquare,
|
|
||||||
Search,
|
|
||||||
Settings,
|
|
||||||
Sparkles,
|
|
||||||
Ticket,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { search as globalSearch } from "@/client/api";
|
|
||||||
import { useBoardsQuery } from "@/hooks/useBoardsQuery";
|
|
||||||
import { useProjectInfo } from "@/hooks/useProjectInfo";
|
|
||||||
import { useProjectsQuery } from "@/hooks/useProjectsQuery";
|
|
||||||
import { useProjectReposQuery } from "@/hooks/useReposQuery";
|
|
||||||
import { useRoomsQuery } from "@/hooks/useRoomsQuery";
|
|
||||||
import { isProjectAdminRole } from "@/lib/project-permissions";
|
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandSeparator,
|
|
||||||
CommandShortcut,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import { OPEN_GLOBAL_SEARCH_EVENT } from "@/components/search/global-search-events";
|
|
||||||
|
|
||||||
function useDebouncedValue(value: string, delay = 180) {
|
|
||||||
const [debounced, setDebounced] = useState(value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = window.setTimeout(() => setDebounced(value), delay);
|
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
}, [delay, value]);
|
|
||||||
|
|
||||||
return debounced;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjectNameFromPath(pathname: string) {
|
|
||||||
const first = pathname.split("/").filter(Boolean)[0];
|
|
||||||
if (!first || first === "me" || first === "auth" || first === "explore") return undefined;
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GlobalSearchPalette() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const debouncedQuery = useDebouncedValue(query.trim());
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const activeProjectName = getProjectNameFromPath(location.pathname);
|
|
||||||
|
|
||||||
const { data: projects = [] } = useProjectsQuery();
|
|
||||||
const { data: projectInfo } = useProjectInfo(activeProjectName);
|
|
||||||
const isProjectMember = !!projectInfo?.role;
|
|
||||||
const canManageProject = isProjectAdminRole(projectInfo?.role);
|
|
||||||
const { data: repos = [] } = useProjectReposQuery(isProjectMember ? activeProjectName : undefined);
|
|
||||||
const { data: roomsData } = useRoomsQuery(isProjectMember ? activeProjectName : undefined);
|
|
||||||
const { data: boards = [] } = useBoardsQuery(isProjectMember ? activeProjectName : undefined);
|
|
||||||
|
|
||||||
const remoteResults = useQuery({
|
|
||||||
queryKey: ["global-search-palette", debouncedQuery],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await globalSearch({ q: debouncedQuery, type: "projects,repos,issues", page: 1, per_page: 8 });
|
|
||||||
return res.data?.data ?? null;
|
|
||||||
},
|
|
||||||
enabled: open && debouncedQuery.length >= 2,
|
|
||||||
staleTime: 30_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey && event.altKey && event.key.toLowerCase() === "f") {
|
|
||||||
event.preventDefault();
|
|
||||||
setOpen((current) => !current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onOpen = () => setOpen(true);
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
window.addEventListener(OPEN_GLOBAL_SEARCH_EVENT, onOpen);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
window.removeEventListener(OPEN_GLOBAL_SEARCH_EVENT, onOpen);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const rooms = roomsData?.rooms ?? [];
|
|
||||||
const quickActions = useMemo(() => {
|
|
||||||
if (!activeProjectName) {
|
|
||||||
return [
|
|
||||||
{ label: "My Home", description: "Open personal overview", path: "/me", icon: LayoutDashboard },
|
|
||||||
{ label: "Explore Projects", description: "Find public projects", path: "/explore", icon: Search },
|
|
||||||
{ label: "Chat", description: "Open personal chat", path: "/me/chat", icon: MessageSquare },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ label: "Project Overview", description: activeProjectName, path: `/${activeProjectName}`, icon: LayoutDashboard },
|
|
||||||
{ label: "Repositories", description: activeProjectName, path: `/${activeProjectName}/repos`, icon: FolderGit2 },
|
|
||||||
{ label: "Issues", description: activeProjectName, path: `/${activeProjectName}/issues`, icon: Ticket },
|
|
||||||
{ label: "Boards", description: activeProjectName, path: `/${activeProjectName}/board`, icon: Kanban },
|
|
||||||
{ label: "Chat", description: activeProjectName, path: `/${activeProjectName}/chat`, icon: MessageSquare },
|
|
||||||
...(canManageProject
|
|
||||||
? [{ label: "Project Settings", description: activeProjectName, path: `/${activeProjectName}/settings`, icon: Settings }]
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
}, [activeProjectName, canManageProject]);
|
|
||||||
|
|
||||||
const go = (path: string) => {
|
|
||||||
setOpen(false);
|
|
||||||
setQuery("");
|
|
||||||
navigate(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenChange = (nextOpen: boolean) => {
|
|
||||||
setOpen(nextOpen);
|
|
||||||
if (!nextOpen) setQuery("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandDialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
title="Search"
|
|
||||||
description="Jump to projects, rooms, repositories, boards, and issues."
|
|
||||||
className="max-w-2xl"
|
|
||||||
>
|
|
||||||
<CommandInput
|
|
||||||
autoFocus
|
|
||||||
placeholder="Search projects, rooms, repos, boards, issues..."
|
|
||||||
value={query}
|
|
||||||
onValueChange={setQuery}
|
|
||||||
/>
|
|
||||||
<CommandList className="max-h-[min(70vh,520px)]">
|
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
|
||||||
|
|
||||||
<CommandGroup heading="Quick Switch">
|
|
||||||
{quickActions.map((item) => (
|
|
||||||
<CommandItem key={item.path} value={`${item.label} ${item.description}`} onSelect={() => go(item.path)}>
|
|
||||||
<item.icon />
|
|
||||||
<span className="truncate">{item.label}</span>
|
|
||||||
<CommandShortcut>{item.description}</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
|
|
||||||
<CommandSeparator />
|
|
||||||
|
|
||||||
<CommandGroup heading="Projects">
|
|
||||||
{projects.map((project) => (
|
|
||||||
<CommandItem
|
|
||||||
key={project.uid}
|
|
||||||
value={`${project.display_name} ${project.name} project`}
|
|
||||||
onSelect={() => go(`/${project.name}`)}
|
|
||||||
>
|
|
||||||
<Boxes />
|
|
||||||
<span className="truncate">{project.display_name || project.name}</span>
|
|
||||||
<CommandShortcut>{project.name}</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
|
|
||||||
{activeProjectName && (
|
|
||||||
<>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup heading={`Rooms in ${activeProjectName}`}>
|
|
||||||
{rooms.map((room) => (
|
|
||||||
<CommandItem
|
|
||||||
key={room.id}
|
|
||||||
value={`${room.room_name} room channel ${activeProjectName}`}
|
|
||||||
onSelect={() => go(`/${activeProjectName}/channel/${room.id}`)}
|
|
||||||
>
|
|
||||||
<Hash />
|
|
||||||
<span className="truncate">{room.room_name}</span>
|
|
||||||
<CommandShortcut>room</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
|
|
||||||
<CommandGroup heading="Repositories">
|
|
||||||
{repos.map((repo) => (
|
|
||||||
<CommandItem
|
|
||||||
key={repo.uid}
|
|
||||||
value={`${repo.repo_name} ${repo.description ?? ""} repo ${activeProjectName}`}
|
|
||||||
onSelect={() => go(`/${activeProjectName}/repo/${repo.repo_name}`)}
|
|
||||||
>
|
|
||||||
<FolderGit2 />
|
|
||||||
<span className="truncate">{repo.repo_name}</span>
|
|
||||||
<CommandShortcut>repo</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
|
|
||||||
<CommandGroup heading="Boards">
|
|
||||||
{boards.map((board) => (
|
|
||||||
<CommandItem
|
|
||||||
key={board.id}
|
|
||||||
value={`${board.name} ${board.description ?? ""} board ${activeProjectName}`}
|
|
||||||
onSelect={() => go(`/${activeProjectName}/board/${board.id}`)}
|
|
||||||
>
|
|
||||||
<Kanban />
|
|
||||||
<span className="truncate">{board.name}</span>
|
|
||||||
<CommandShortcut>board</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{remoteResults.data && (
|
|
||||||
<>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup heading="Search Results">
|
|
||||||
{(remoteResults.data.projects?.items ?? []).map((project) => (
|
|
||||||
<CommandItem key={`remote-project-${project.uid}`} value={`${project.display_name} ${project.name}`} onSelect={() => go(`/${project.name}`)}>
|
|
||||||
<Boxes />
|
|
||||||
<span className="truncate">{project.display_name || project.name}</span>
|
|
||||||
<CommandShortcut>project</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
{(remoteResults.data.repos?.items ?? []).map((repo) => (
|
|
||||||
<CommandItem key={`remote-repo-${repo.uid}`} value={`${repo.name} ${repo.project_name}`} onSelect={() => go(`/${repo.project_name}/repo/${repo.name}`)}>
|
|
||||||
<FolderGit2 />
|
|
||||||
<span className="truncate">{repo.name}</span>
|
|
||||||
<CommandShortcut>{repo.project_name}</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
{(remoteResults.data.issues?.items ?? []).map((issue) => (
|
|
||||||
<CommandItem key={`remote-issue-${issue.uid}`} value={`${issue.title} ${issue.project_name} #${issue.number}`} onSelect={() => go(`/${issue.project_name}/issues/${issue.number}`)}>
|
|
||||||
<Sparkles />
|
|
||||||
<span className="truncate">#{issue.number} {issue.title}</span>
|
|
||||||
<CommandShortcut>issue</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export const OPEN_GLOBAL_SEARCH_EVENT = "app:open-global-search";
|
|
||||||
|
|
||||||
export function openGlobalSearch() {
|
|
||||||
window.dispatchEvent(new Event(OPEN_GLOBAL_SEARCH_EVENT));
|
|
||||||
}
|
|
||||||
@ -4,49 +4,6 @@ import { getAllPresets } from "@/config/theme-presets";
|
|||||||
const STORAGE_KEY = "app-theme-preset";
|
const STORAGE_KEY = "app-theme-preset";
|
||||||
const DEFAULT_PRESET = "soft-mono";
|
const DEFAULT_PRESET = "soft-mono";
|
||||||
|
|
||||||
const THEME_COLOR_ALIASES: Record<string, string> = {
|
|
||||||
"color-background": "background",
|
|
||||||
"color-foreground": "foreground",
|
|
||||||
"color-card": "card",
|
|
||||||
"color-card-foreground": "card-foreground",
|
|
||||||
"color-popover": "popover",
|
|
||||||
"color-popover-foreground": "popover-foreground",
|
|
||||||
"color-primary": "primary",
|
|
||||||
"color-primary-foreground": "primary-foreground",
|
|
||||||
"color-secondary": "secondary",
|
|
||||||
"color-secondary-foreground": "secondary-foreground",
|
|
||||||
"color-muted": "muted",
|
|
||||||
"color-muted-foreground": "muted-foreground",
|
|
||||||
"color-accent": "accent",
|
|
||||||
"color-accent-foreground": "accent-foreground",
|
|
||||||
"color-destructive": "destructive",
|
|
||||||
"color-border": "border",
|
|
||||||
"color-input": "input",
|
|
||||||
"color-ring": "ring",
|
|
||||||
"color-sidebar": "sidebar",
|
|
||||||
"color-sidebar-foreground": "sidebar-foreground",
|
|
||||||
"color-sidebar-primary": "sidebar-primary",
|
|
||||||
"color-sidebar-primary-foreground": "sidebar-primary-foreground",
|
|
||||||
"color-sidebar-accent": "sidebar-accent",
|
|
||||||
"color-sidebar-accent-foreground": "sidebar-accent-foreground",
|
|
||||||
"color-sidebar-border": "sidebar-border",
|
|
||||||
"color-sidebar-ring": "sidebar-ring",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DERIVED_THEME_TOKENS: Record<string, string> = {
|
|
||||||
"accent-bg": "color-mix(in oklch, var(--accent) 8%, transparent)",
|
|
||||||
"success-alpha10": "color-mix(in oklch, var(--success) 10%, transparent)",
|
|
||||||
"warning-alpha10": "color-mix(in oklch, var(--warning) 10%, transparent)",
|
|
||||||
"destructive-alpha10": "color-mix(in oklch, var(--destructive) 10%, transparent)",
|
|
||||||
"status-offline": "var(--text-muted)",
|
|
||||||
"text-tertiary": "color-mix(in oklch, var(--text-muted) 70%, var(--surface-ground))",
|
|
||||||
"heatmap-0": "color-mix(in oklch, var(--surface-ground) 82%, var(--border-default))",
|
|
||||||
"heatmap-1": "color-mix(in oklch, var(--success) 18%, var(--surface-ground))",
|
|
||||||
"heatmap-2": "color-mix(in oklch, var(--success) 36%, var(--surface-ground))",
|
|
||||||
"heatmap-3": "color-mix(in oklch, var(--success) 58%, var(--surface-ground))",
|
|
||||||
"heatmap-4": "color-mix(in oklch, var(--success) 78%, var(--surface-ground))",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useThemePreset() {
|
export function useThemePreset() {
|
||||||
const [presetId, setPresetIdState] = useState<string>(() => {
|
const [presetId, setPresetIdState] = useState<string>(() => {
|
||||||
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PRESET;
|
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PRESET;
|
||||||
@ -76,14 +33,6 @@ export function applyThemePreset(presetId: string) {
|
|||||||
Object.entries(vars).forEach(([key, value]) => {
|
Object.entries(vars).forEach(([key, value]) => {
|
||||||
if (value) root.style.setProperty(`--${key}`, value);
|
if (value) root.style.setProperty(`--${key}`, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.entries(THEME_COLOR_ALIASES).forEach(([alias, source]) => {
|
|
||||||
root.style.setProperty(`--${alias}`, `var(--${source})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(DERIVED_THEME_TOKENS).forEach(([key, value]) => {
|
|
||||||
root.style.setProperty(`--${key}`, value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThemePresetSelector() {
|
export function ThemePresetSelector() {
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import type { MemberRole } from "@/client/model";
|
|
||||||
|
|
||||||
export function isProjectAdminRole(role?: MemberRole | string | null) {
|
|
||||||
const normalized = String(role ?? "").toLowerCase();
|
|
||||||
return normalized === "owner" || normalized === "admin";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProjectOwnerRole(role?: MemberRole | string | null) {
|
|
||||||
return String(role ?? "").toLowerCase() === "owner";
|
|
||||||
}
|
|
||||||
@ -26,7 +26,7 @@ const numberFromEnv = (value: string | undefined, fallback: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const options: OpenObserveOptions = {
|
const options: OpenObserveOptions = {
|
||||||
clientToken: import.meta.env.VITE_OPENOBSERVE_CLIENT_TOKEN ?? 'rumZUDgk3BCRAAOKcts',
|
clientToken: import.meta.env.VITE_OPENOBSERVE_CLIENT_TOKEN ?? 'rum88J1PMrMSTcFoP0g',
|
||||||
applicationId: import.meta.env.VITE_OPENOBSERVE_APPLICATION_ID ?? 'GitDataAI',
|
applicationId: import.meta.env.VITE_OPENOBSERVE_APPLICATION_ID ?? 'GitDataAI',
|
||||||
site: import.meta.env.VITE_OPENOBSERVE_SITE ?? 'ob.gitdata.me',
|
site: import.meta.env.VITE_OPENOBSERVE_SITE ?? 'ob.gitdata.me',
|
||||||
service: import.meta.env.VITE_OPENOBSERVE_SERVICE ?? 'GitDataAIWeb',
|
service: import.meta.env.VITE_OPENOBSERVE_SERVICE ?? 'GitDataAIWeb',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user