mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
iteration
This commit is contained in:
@@ -4,9 +4,15 @@ import type { View } from '../App';
|
||||
import Stop from './ui/Stop';
|
||||
import { Attach, Send, Close } from './icons';
|
||||
import { debounce } from 'lodash';
|
||||
import BottomMenu from './bottom_menu/BottomMenu';
|
||||
import { LocalMessageStorage } from '../utils/localMessageStorage';
|
||||
import { Message } from '../types/message';
|
||||
import { DirSwitcher } from './bottom_menu/DirSwitcher';
|
||||
import BottomMenuAlertPopover from './bottom_menu/BottomMenuAlertPopover';
|
||||
import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar';
|
||||
import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection';
|
||||
import { ManualSummarizeButton } from './context_management/ManualSummaryButton';
|
||||
import { useAlerts } from './alerts';
|
||||
import { useToolCount } from './alerts/useToolCount';
|
||||
|
||||
interface PastedImage {
|
||||
id: string;
|
||||
@@ -41,11 +47,19 @@ export default function ChatInput({
|
||||
commandHistory = [],
|
||||
initialValue = '',
|
||||
droppedFiles = [],
|
||||
setView,
|
||||
numTokens,
|
||||
hasMessages,
|
||||
messages = [],
|
||||
setMessages,
|
||||
}: ChatInputProps) {
|
||||
const [_value, setValue] = useState(initialValue);
|
||||
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [pastedImages, setPastedImages] = useState<PastedImage[]>([]);
|
||||
const { alerts, addAlert, clearAlerts } = useAlerts();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const toolCount = useToolCount();
|
||||
|
||||
// Update internal value when initialValue changes
|
||||
useEffect(() => {
|
||||
@@ -434,7 +448,11 @@ export default function ChatInput({
|
||||
: 'border-borderSubtle hover:border-borderStandard'
|
||||
} z-10`}
|
||||
>
|
||||
<form onSubmit={onFormSubmit}>
|
||||
{/* DirSwitcher at the top */}
|
||||
<div className="p-2 pb-0">
|
||||
<DirSwitcher hasMessages={messages.length > 0} />
|
||||
</div>
|
||||
<form onSubmit={onFormSubmit} className="flex flex-col">
|
||||
<textarea
|
||||
data-testid="chat-input"
|
||||
autoFocus
|
||||
@@ -455,38 +473,16 @@ export default function ChatInput({
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
className="w-full pl-4 pr-[68px] outline-none border-none focus:ring-0 bg-transparent pt-3 pb-1.5 text-sm resize-none text-textStandard placeholder:text-textPlaceholder"
|
||||
className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 text-sm resize-none text-textStandard placeholder:text-textPlaceholder"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1 absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<Button type="button" shape="round" size="sm" variant="ghost" onClick={handleFileSelect}>
|
||||
<Attach />
|
||||
</Button>
|
||||
|
||||
{isLoading ? (
|
||||
<Button type="button" onClick={onStop} shape="round" size="sm" variant="ghost">
|
||||
<Stop />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
// disabled={!displayValue.trim() && pastedImages.length === 0}
|
||||
>
|
||||
<Send />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pastedImages.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border-t border-borderSubtle">
|
||||
{pastedImages.map((img) => (
|
||||
<div key={img.id} className="relative group w-20 h-20">
|
||||
{img.dataUrl && (
|
||||
<img
|
||||
src={img.dataUrl} // Use dataUrl for instant preview
|
||||
src={img.dataUrl}
|
||||
alt={`Pasted image ${img.id}`}
|
||||
className={`w-full h-full object-cover rounded border ${img.error ? 'border-red-500' : 'border-borderStandard'}`}
|
||||
/>
|
||||
@@ -506,6 +502,8 @@ export default function ChatInput({
|
||||
type="button"
|
||||
onClick={() => handleRetryImageSave(img.id)}
|
||||
title="Retry saving image"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
@@ -519,6 +517,8 @@ export default function ChatInput({
|
||||
onClick={() => handleRemovePastedImage(img.id)}
|
||||
className="absolute -top-1 -right-1 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity z-10"
|
||||
aria-label="Remove image"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
>
|
||||
<Close />
|
||||
</Button>
|
||||
@@ -527,6 +527,37 @@ export default function ChatInput({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions and model/mode/alerts row below input */}
|
||||
<div className="flex flex-row items-center gap-1 p-2">
|
||||
{/* Send/Attach/Stop actions */}
|
||||
<Button type="button" size="xs" variant="outline" onClick={handleFileSelect}>
|
||||
<Attach />
|
||||
</Button>
|
||||
{isLoading ? (
|
||||
<Button type="button" onClick={onStop} size="xs" variant="outline">
|
||||
<Stop />
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="submit" size="xs" variant="outline">
|
||||
<Send />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Model selector, mode selector, alerts, summarize button */}
|
||||
<div className="flex flex-row items-center gap-2 ml-4">
|
||||
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
|
||||
<BottomMenuModeSelection setView={setView} />
|
||||
<BottomMenuAlertPopover alerts={alerts} />
|
||||
{messages.length > 0 && (
|
||||
<ManualSummarizeButton
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
} from '../types/message';
|
||||
import SessionsSidebar from './GooseSidebar/AppSidebar';
|
||||
import { SidebarTrigger } from './ui/sidebar';
|
||||
import BottomMenu from './bottom_menu/BottomMenu';
|
||||
import { SessionInsights } from './sessions/SessionsInsights';
|
||||
import { useSidebar } from './ui/sidebar';
|
||||
import { Button } from './ui/button';
|
||||
@@ -418,6 +417,11 @@ function ChatContentWithSidebar({
|
||||
// Handle stopping the message stream
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Check if there are any messages before proceeding
|
||||
if (!lastMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the last user message has any tool response(s)
|
||||
const isToolResponse = lastMessage.content.some(
|
||||
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
|
||||
@@ -847,14 +851,6 @@ function ChatContentWithSidebar({
|
||||
</div>
|
||||
</MainPanelLayout>
|
||||
|
||||
<BottomMenu
|
||||
setView={setView}
|
||||
numTokens={sessionTokenCount}
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
|
||||
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
||||
|
||||
<SessionSummaryModal
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Folder, FileText, Clock, MessageCircle, Home, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Folder,
|
||||
FileText,
|
||||
Clock,
|
||||
MessageCircle,
|
||||
Home,
|
||||
Loader2,
|
||||
AppWindowMac,
|
||||
AppWindow,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
@@ -204,7 +213,11 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
tooltip="Start a new session in a new window"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<ChatSmart className="w-4 h-4" />
|
||||
{safeIsMacOS ? (
|
||||
<AppWindowMac className="w-4 h-4" />
|
||||
) : (
|
||||
<AppWindow className="w-4 h-4" />
|
||||
)}
|
||||
<span>New window</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export const MainPanelLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-w-0 h-[calc(100dvh-80px)] shadow-default bg-background-default mr-2 mb-2 rounded-2xl animate-in fade-in slide-in-from-right-8 duration-500">
|
||||
<div className="flex flex-col flex-1 min-w-0 h-[calc(100dvh-56px)] shadow-default bg-background-default mr-2 mb-2 rounded-2xl animate-in fade-in slide-in-from-right-8 duration-500">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Document } from '../icons';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
interface DirSwitcherProps {
|
||||
hasMessages?: boolean;
|
||||
@@ -25,15 +26,17 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
|
||||
<TooltipProvider>
|
||||
<Tooltip open={isTooltipOpen} onOpenChange={setIsTooltipOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
<Button
|
||||
className={`z-[100] hover:cursor-pointer text-text-default/70 hover:text-text-default text-xs flex items-center transition-colors pl-1 [&>svg]:size-4 ${className}`}
|
||||
onClick={handleDirectoryChange}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
>
|
||||
<Document className="mr-1" />
|
||||
<div className="max-w-[200px] truncate [direction:rtl]">
|
||||
{String(window.appConfig.get('GOOSE_WORKING_DIR'))}
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
|
||||
{window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
|
||||
@@ -419,6 +419,11 @@ function HubContentWithSidebar({
|
||||
// Handle stopping the message stream
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Check if there are any messages before proceeding
|
||||
if (!lastMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the last user message has any tool response(s)
|
||||
const isToolResponse = lastMessage.content.some(
|
||||
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
|
||||
@@ -585,10 +590,7 @@ function HubContentWithSidebar({
|
||||
{/* Loader when generating recipe */}
|
||||
{isGeneratingRecipe && <LayingEggLoader />}
|
||||
|
||||
<div className="h-12 flex items-center justify-between">
|
||||
{/* <div className={`flex items-center ${headerPadding}`}>
|
||||
<SidebarTrigger className="no-drag" />
|
||||
</div> */}
|
||||
<div className="h-12 flex items-center justify-between absolute">
|
||||
<div className="flex items-center pr-4">
|
||||
{messages.length > 0 && (
|
||||
<>
|
||||
@@ -693,7 +695,7 @@ function HubContentWithSidebar({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16 pt-2"
|
||||
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
|
||||
@@ -417,6 +417,11 @@ function PairContentWithSidebar({
|
||||
// Handle stopping the message stream
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Check if there are any messages before proceeding
|
||||
if (!lastMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the last user message has any tool response(s)
|
||||
const isToolResponse = lastMessage.content.some(
|
||||
(content): content is ToolResponseMessageContent => content.type == 'toolResponse'
|
||||
@@ -584,9 +589,6 @@ function PairContentWithSidebar({
|
||||
{isGeneratingRecipe && <LayingEggLoader />}
|
||||
|
||||
<div className="h-12 flex items-center justify-between">
|
||||
<div className={`flex items-center ${headerPadding}`}>
|
||||
<SidebarTrigger className="no-drag" />
|
||||
</div>
|
||||
<div className="flex items-center pr-4">
|
||||
{messages.length > 0 && (
|
||||
<>
|
||||
@@ -811,16 +813,6 @@ function PairContentWithSidebar({
|
||||
</div>
|
||||
</MainPanelLayout>
|
||||
|
||||
<BottomMenu
|
||||
setView={setView}
|
||||
numTokens={sessionTokenCount}
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
|
||||
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
||||
|
||||
<SessionSummaryModal
|
||||
isOpen={isSummaryModalOpen}
|
||||
onClose={closeSummaryModal}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { SidebarTrigger, useSidebar } from '../ui/sidebar';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/vertical-tabs';
|
||||
import type { View, ViewOptions } from '../../App';
|
||||
import ExtensionsSection from './extensions/ExtensionsSection';
|
||||
import ModelsSection from './models/ModelsSection';
|
||||
@@ -11,6 +12,8 @@ import AppSettingsSection from './app/AppSettingsSection';
|
||||
import { ExtensionConfig } from '../../api';
|
||||
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
|
||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||
import { Bot, Puzzle, Settings, Share2, Palette, Wrench, Monitor, Target } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export type SettingsViewOptions = {
|
||||
deepLinkConfig?: ExtensionConfig;
|
||||
@@ -28,12 +31,35 @@ export default function SettingsView({
|
||||
viewOptions: SettingsViewOptions;
|
||||
}) {
|
||||
const { open: isSidebarOpen } = useSidebar();
|
||||
const [activeTab, setActiveTab] = useState('models');
|
||||
|
||||
const safeIsMacOS = (window?.electron?.platform || 'darwin') === 'darwin';
|
||||
|
||||
// Calculate padding based on sidebar state and macOS
|
||||
const headerPadding = !isSidebarOpen ? (safeIsMacOS ? 'pl-20' : 'pl-12') : 'pl-4';
|
||||
|
||||
// Determine initial tab based on section prop
|
||||
useEffect(() => {
|
||||
if (viewOptions.section) {
|
||||
// Map section names to tab values
|
||||
const sectionToTab: Record<string, string> = {
|
||||
update: 'app',
|
||||
models: 'models',
|
||||
extensions: 'extensions',
|
||||
modes: 'modes',
|
||||
sharing: 'sharing',
|
||||
styles: 'styles',
|
||||
tools: 'tools',
|
||||
app: 'app',
|
||||
};
|
||||
|
||||
const targetTab = sectionToTab[viewOptions.section];
|
||||
if (targetTab) {
|
||||
setActiveTab(targetTab);
|
||||
}
|
||||
}
|
||||
}, [viewOptions.section]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainPanelLayout>
|
||||
@@ -53,29 +79,73 @@ export default function SettingsView({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 relative px-6">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="h-full relative">
|
||||
<div className="space-y-8 pt-4">
|
||||
{/* Models Section */}
|
||||
<ModelsSection setView={setView} />
|
||||
{/* Extensions Section */}
|
||||
<ExtensionsSection
|
||||
deepLinkConfig={viewOptions.deepLinkConfig}
|
||||
showEnvVars={viewOptions.showEnvVars}
|
||||
/>
|
||||
{/* Goose Modes */}
|
||||
<ModeSection setView={setView} />
|
||||
{/*Session sharing*/}
|
||||
<SessionSharingSection />
|
||||
{/* Response Styles */}
|
||||
<ResponseStylesSection />
|
||||
{/* Tool Selection Strategy */}
|
||||
<ToolSelectionStrategySection setView={setView} />
|
||||
{/* App Settings */}
|
||||
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex">
|
||||
<TabsList className="w-48 h-full">
|
||||
<TabsTrigger value="models" className="flex gap-2">
|
||||
<Bot className="h-4 w-4" />
|
||||
Models
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="extensions" className="flex gap-2">
|
||||
<Puzzle className="h-4 w-4" />
|
||||
Extensions
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="modes" className="flex gap-2">
|
||||
<Wrench className="h-4 w-4" />
|
||||
Modes
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sharing" className="flex gap-2">
|
||||
<Share2 className="h-4 w-4" />
|
||||
Session Sharing
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="styles" className="flex gap-2">
|
||||
<Palette className="h-4 w-4" />
|
||||
Response Styles
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools" className="flex gap-2">
|
||||
<Target className="h-4 w-4" />
|
||||
Tool Selection
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="app" className="flex gap-2">
|
||||
<Monitor className="h-4 w-4" />
|
||||
App Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-1 ml-6">
|
||||
<ScrollArea className="h-full">
|
||||
<TabsContent value="models" className="mt-0">
|
||||
<ModelsSection setView={setView} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="extensions" className="mt-0">
|
||||
<ExtensionsSection
|
||||
deepLinkConfig={viewOptions.deepLinkConfig}
|
||||
showEnvVars={viewOptions.showEnvVars}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="modes" className="mt-0">
|
||||
<ModeSection setView={setView} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sharing" className="mt-0">
|
||||
<SessionSharingSection />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="styles" className="mt-0">
|
||||
<ResponseStylesSection />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools" className="mt-0">
|
||||
<ToolSelectionStrategySection setView={setView} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="app" className="mt-0">
|
||||
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</MainPanelLayout>
|
||||
|
||||
@@ -11,22 +11,28 @@ const buttonVariants = cva(
|
||||
default: 'bg-background-accent text-text-on-accent hover:bg-background-accent/90 shadow-xs',
|
||||
destructive:
|
||||
'bg-background-danger text-white hover:bg-background-danger/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-background-danger/60 shadow-xs',
|
||||
outline: 'border hover:bg-background-default hover:shadow-default hover:border-transparent',
|
||||
outline: 'border hover:bg-background-muted',
|
||||
secondary: 'bg-background-muted text-text-default hover:bg-background-muted/80 shadow-xs',
|
||||
ghost: 'hover:bg-background-muted dark:hover:bg-background-muted/50',
|
||||
link: 'text-text-accent underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
xs: 'h-6 gap-1 ![&_svg:not([class*="size-"])]:size-3',
|
||||
default: 'h-9',
|
||||
sm: 'h-8 gap-1.5',
|
||||
lg: 'h-10',
|
||||
},
|
||||
shape: {
|
||||
pill: 'rounded-full',
|
||||
pill: 'rounded-lg',
|
||||
round: '',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
shape: 'pill',
|
||||
size: 'xs',
|
||||
className: 'px-2 has-[>svg]:px-2',
|
||||
},
|
||||
{
|
||||
shape: 'pill',
|
||||
size: 'default',
|
||||
@@ -42,6 +48,11 @@ const buttonVariants = cva(
|
||||
size: 'lg',
|
||||
className: 'px-6 has-[>svg]:px-6',
|
||||
},
|
||||
{
|
||||
shape: 'round',
|
||||
size: 'xs',
|
||||
className: 'w-6 h-6 p-0 rounded-full',
|
||||
},
|
||||
{
|
||||
shape: 'round',
|
||||
size: 'default',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
const Tabs = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Root
|
||||
orientation="vertical"
|
||||
ref={ref}
|
||||
className={cn('flex rounded-md text-text-muted gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Tabs.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-col h-auto justify-start rounded-md bg-background-default p-1 text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-start whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'ml-4 mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
Reference in New Issue
Block a user