mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
refactor: replace button elements with Button component for improved consistency and styling across multiple UI components
This commit is contained in:
parent
23e05acc7c
commit
3d42712b3f
27 changed files with 401 additions and 424 deletions
|
|
@ -96,13 +96,14 @@ export function CommunityPromptsContent() {
|
||||||
{prompt.prompt}
|
{prompt.prompt}
|
||||||
</p>
|
</p>
|
||||||
{prompt.prompt.length > 100 && (
|
{prompt.prompt.length > 100 && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="link"
|
||||||
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)}
|
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)}
|
||||||
className="mt-1 text-[11px] text-primary hover:underline cursor-pointer"
|
className="mt-1 h-auto cursor-pointer px-0 py-0 text-[11px] text-primary"
|
||||||
>
|
>
|
||||||
{expandedId === prompt.id ? "See less" : "See more"}
|
{expandedId === prompt.id ? "See less" : "See more"}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -97,17 +97,18 @@ function HotkeyRow({
|
||||||
<RotateCcw className="size-3" />
|
<RotateCcw className="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
title={recording ? "Press shortcut keys" : "Click to edit shortcut"}
|
title={recording ? "Press shortcut keys" : "Click to edit shortcut"}
|
||||||
onClick={() => setRecording(true)}
|
onClick={() => setRecording(true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={() => setRecording(false)}
|
onBlur={() => setRecording(false)}
|
||||||
className={
|
className={
|
||||||
recording
|
recording
|
||||||
? "flex h-7 items-center rounded-md border border-transparent bg-primary/5 outline-none ring-0 focus:outline-none focus-visible:outline-none focus-visible:ring-0"
|
? "h-7 border border-transparent bg-primary/5 px-0 outline-none ring-0 focus:outline-none focus-visible:outline-none focus-visible:ring-0"
|
||||||
: "flex h-7 cursor-pointer items-center rounded-md border border-transparent bg-transparent outline-none ring-0 transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-0"
|
: "h-7 cursor-pointer border border-transparent bg-transparent px-0 outline-none ring-0 transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-0"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{recording ? (
|
{recording ? (
|
||||||
|
|
@ -115,7 +116,7 @@ function HotkeyRow({
|
||||||
) : (
|
) : (
|
||||||
<ShortcutKbd keys={displayKeys} className="ml-0 px-1.5 text-foreground/85" />
|
<ShortcutKbd keys={displayKeys} className="ml-0 px-1.5 text-foreground/85" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -277,22 +277,25 @@ export function PromptsContent() {
|
||||||
{prompt.prompt}
|
{prompt.prompt}
|
||||||
</p>
|
</p>
|
||||||
{prompt.prompt.length > 100 && (
|
{prompt.prompt.length > 100 && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="link"
|
||||||
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)}
|
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)}
|
||||||
className="mt-1 text-[11px] text-primary hover:underline cursor-pointer"
|
className="mt-1 h-auto cursor-pointer px-0 py-0 text-[11px] text-primary"
|
||||||
>
|
>
|
||||||
{expandedId === prompt.id ? "See less" : "See more"}
|
{expandedId === prompt.id ? "See less" : "See more"}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden group-hover:flex items-center gap-1 shrink-0">
|
<div className="hidden group-hover:flex items-center gap-1 shrink-0">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
title={prompt.is_public ? "Make private" : "Share with community"}
|
title={prompt.is_public ? "Make private" : "Share with community"}
|
||||||
onClick={() => handleTogglePublic(prompt)}
|
onClick={() => handleTogglePublic(prompt)}
|
||||||
disabled={togglingPublicIds.has(prompt.id)}
|
disabled={togglingPublicIds.has(prompt.id)}
|
||||||
className="flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-accent-foreground hover:bg-accent transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
className="size-7 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
{togglingPublicIds.has(prompt.id) ? (
|
{togglingPublicIds.has(prompt.id) ? (
|
||||||
<Spinner className="size-3.5" />
|
<Spinner className="size-3.5" />
|
||||||
|
|
@ -301,7 +304,7 @@ export function PromptsContent() {
|
||||||
) : (
|
) : (
|
||||||
<Globe className="size-3.5" />
|
<Globe className="size-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
|
||||||
|
|
@ -207,13 +207,14 @@ export default function DesktopPermissionsPage() {
|
||||||
<Button disabled className="text-sm h-9 min-w-[180px]">
|
<Button disabled className="text-sm h-9 min-w-[180px]">
|
||||||
Grant permissions to continue
|
Grant permissions to continue
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="link"
|
||||||
onClick={handleSkip}
|
onClick={handleSkip}
|
||||||
className="block mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
|
className="mx-auto h-auto px-0 py-0 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,11 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
isAlreadyReverted && "opacity-70"
|
isAlreadyReverted && "opacity-70"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setIsExpanded((v) => !v)}
|
onClick={() => setIsExpanded((v) => !v)}
|
||||||
className="flex w-full items-start gap-3 p-3 text-left hover:bg-accent hover:text-accent-foreground"
|
className="h-auto w-full items-start justify-start gap-3 p-3 text-left hover:bg-accent hover:text-accent-foreground"
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
>
|
>
|
||||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
|
||||||
|
|
@ -119,7 +120,7 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
isExpanded && "rotate-90"
|
isExpanded && "rotate-90"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="flex flex-col gap-3 border-t bg-muted/20 p-3">
|
<div className="flex flex-col gap-3 border-t bg-muted/20 p-3">
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import type { ImageMessagePartComponent } from "@assistant-ui/react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { ImageIcon, ImageOffIcon } from "lucide-react";
|
import { ImageIcon, ImageOffIcon } from "lucide-react";
|
||||||
import NextImage from "next/image";
|
import NextImage from "next/image";
|
||||||
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
import { memo, type PropsWithChildren, useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
||||||
|
|
@ -44,8 +45,14 @@ function ImageRoot({ className, variant, size, children, ...props }: ImageRootPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImagePreviewProps = Omit<React.ComponentProps<"img">, "children"> & {
|
type ImagePreviewProps = Omit<
|
||||||
|
React.ComponentProps<"img">,
|
||||||
|
"children" | "height" | "onError" | "onLoad" | "src" | "width"
|
||||||
|
> & {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
onError?: React.ReactEventHandler<HTMLImageElement>;
|
||||||
|
onLoad?: React.ReactEventHandler<HTMLImageElement>;
|
||||||
|
src?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ImagePreview({
|
function ImagePreview({
|
||||||
|
|
@ -57,18 +64,17 @@ function ImagePreview({
|
||||||
src,
|
src,
|
||||||
...props
|
...props
|
||||||
}: ImagePreviewProps) {
|
}: ImagePreviewProps) {
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
|
||||||
const [loadedSrc, setLoadedSrc] = useState<string | undefined>(undefined);
|
const [loadedSrc, setLoadedSrc] = useState<string | undefined>(undefined);
|
||||||
const [errorSrc, setErrorSrc] = useState<string | undefined>(undefined);
|
const [errorSrc, setErrorSrc] = useState<string | undefined>(undefined);
|
||||||
|
const imageSrc = src ?? "";
|
||||||
|
|
||||||
const loaded = loadedSrc === src;
|
const loaded = imageSrc !== "" && loadedSrc === imageSrc;
|
||||||
const error = errorSrc === src;
|
const error = imageSrc === "" || errorSrc === imageSrc;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof src === "string" && imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
|
setLoadedSrc((current) => (current === imageSrc ? current : undefined));
|
||||||
setLoadedSrc(src);
|
setErrorSrc((current) => (current === imageSrc ? current : undefined));
|
||||||
}
|
}, [imageSrc]);
|
||||||
}, [src]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-slot="image-preview" className={cn("relative min-h-32", containerClassName)}>
|
<div data-slot="image-preview" className={cn("relative min-h-32", containerClassName)}>
|
||||||
|
|
@ -87,55 +93,22 @@ function ImagePreview({
|
||||||
>
|
>
|
||||||
<ImageOffIcon className="size-8 text-muted-foreground" />
|
<ImageOffIcon className="size-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : isDataOrBlobUrl(src) ? (
|
|
||||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
|
||||||
<img
|
|
||||||
ref={imgRef}
|
|
||||||
src={src}
|
|
||||||
alt={alt}
|
|
||||||
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
|
||||||
onLoad={(e) => {
|
|
||||||
if (typeof src === "string") setLoadedSrc(src);
|
|
||||||
onLoad?.(e);
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
if (typeof src === "string") setErrorSrc(src);
|
|
||||||
onError?.(e);
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
|
|
||||||
// <img
|
|
||||||
// ref={imgRef}
|
|
||||||
// src={src}
|
|
||||||
// alt={alt}
|
|
||||||
// className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
|
||||||
// onLoad={(e) => {
|
|
||||||
// if (typeof src === "string") setLoadedSrc(src);
|
|
||||||
// onLoad?.(e);
|
|
||||||
// }}
|
|
||||||
// onError={(e) => {
|
|
||||||
// if (typeof src === "string") setErrorSrc(src);
|
|
||||||
// onError?.(e);
|
|
||||||
// }}
|
|
||||||
// {...props}
|
|
||||||
// />
|
|
||||||
<NextImage
|
<NextImage
|
||||||
fill
|
fill
|
||||||
src={src || ""}
|
src={imageSrc}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
||||||
className={cn("block object-contain", !loaded && "invisible", className)}
|
className={cn("block object-contain", !loaded && "invisible", className)}
|
||||||
onLoad={() => {
|
onLoad={(event) => {
|
||||||
if (typeof src === "string") setLoadedSrc(src);
|
setLoadedSrc(imageSrc);
|
||||||
onLoad?.();
|
onLoad?.(event);
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={(event) => {
|
||||||
if (typeof src === "string") setErrorSrc(src);
|
setErrorSrc(imageSrc);
|
||||||
onError?.();
|
onError?.(event);
|
||||||
}}
|
}}
|
||||||
unoptimized={false}
|
unoptimized={isDataOrBlobUrl(imageSrc)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -196,59 +169,40 @@ function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
className="aui-image-zoom-trigger cursor-zoom-in border-0 bg-transparent p-0 text-left"
|
className="aui-image-zoom-trigger h-auto cursor-zoom-in border-0 bg-transparent p-0 text-left hover:bg-transparent"
|
||||||
aria-label="Click to zoom image"
|
aria-label="Click to zoom image"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</Button>
|
||||||
{isMounted &&
|
{isMounted &&
|
||||||
isOpen &&
|
isOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
data-slot="image-zoom-overlay"
|
data-slot="image-zoom-overlay"
|
||||||
className="aui-image-zoom-overlay fade-in fixed inset-0 z-50 flex animate-in cursor-zoom-out items-center justify-center border-0 bg-black/80 p-0 duration-200"
|
className="aui-image-zoom-overlay fade-in fixed inset-0 z-50 h-auto w-auto animate-in cursor-zoom-out items-center justify-center rounded-none border-0 bg-black/80 p-0 duration-200 hover:bg-black/80 focus-visible:ring-0"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
aria-label="Close zoomed image"
|
aria-label="Close zoomed image"
|
||||||
>
|
>
|
||||||
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
|
<NextImage
|
||||||
{isDataOrBlobUrl(src) ? (
|
data-slot="image-zoom-content"
|
||||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
fill
|
||||||
<img
|
src={src}
|
||||||
data-slot="image-zoom-content"
|
alt={alt}
|
||||||
src={src}
|
sizes="90vw"
|
||||||
alt={alt}
|
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
||||||
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
handleClose();
|
||||||
handleClose();
|
}}
|
||||||
}}
|
unoptimized={isDataOrBlobUrl(src)}
|
||||||
onKeyDown={(e) => {
|
/>
|
||||||
if (e.key === "Enter") {
|
</Button>,
|
||||||
e.stopPropagation();
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NextImage
|
|
||||||
data-slot="image-zoom-content"
|
|
||||||
fill
|
|
||||||
src={src}
|
|
||||||
alt={alt}
|
|
||||||
sizes="90vw"
|
|
||||||
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleClose();
|
|
||||||
}}
|
|
||||||
unoptimized={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>,
|
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Presentation,
|
Presentation,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
@ -243,9 +244,11 @@ export function GoogleDriveFolderTree({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isFolder ? (
|
{isFolder ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center justify-center w-3 h-3 sm:w-4 sm:h-4 shrink-0 bg-transparent border-0 p-0 cursor-pointer"
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-3 w-3 shrink-0 cursor-pointer bg-transparent p-0 hover:bg-transparent sm:h-4 sm:w-4"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleFolder(item);
|
toggleFolder(item);
|
||||||
|
|
@ -259,7 +262,7 @@ export function GoogleDriveFolderTree({
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
<ChevronRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<span className="w-3 h-3 sm:w-4 sm:h-4 shrink-0" />
|
<span className="w-3 h-3 sm:w-4 sm:h-4 shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -290,13 +293,14 @@ export function GoogleDriveFolderTree({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFolder ? (
|
{isFolder ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="truncate flex-1 text-left text-xs sm:text-sm min-w-0 bg-transparent border-0 p-0 cursor-pointer"
|
variant="ghost"
|
||||||
|
className="h-auto min-w-0 flex-1 cursor-pointer justify-start truncate bg-transparent p-0 text-left text-xs hover:bg-transparent sm:text-sm"
|
||||||
onClick={() => toggleFolder(item)}
|
onClick={() => toggleFolder(item)}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<span className="truncate flex-1 text-left text-xs sm:text-sm min-w-0">
|
<span className="truncate flex-1 text-left text-xs sm:text-sm min-w-0">
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|
@ -332,13 +336,14 @@ export function GoogleDriveFolderTree({
|
||||||
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20"
|
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20"
|
||||||
/>
|
/>
|
||||||
<HardDrive className="h-3 w-3 sm:h-4 sm:w-4 text-muted-foreground shrink-0" />
|
<HardDrive className="h-3 w-3 sm:h-4 sm:w-4 text-muted-foreground shrink-0" />
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="font-semibold truncate text-xs sm:text-sm cursor-pointer bg-transparent border-0 p-0 text-left"
|
variant="ghost"
|
||||||
|
className="h-auto cursor-pointer truncate bg-transparent p-0 text-left text-xs font-semibold hover:bg-transparent sm:text-sm"
|
||||||
onClick={() => toggleFolderSelection("root", "My Drive")}
|
onClick={() => toggleFolderSelection("root", "My Drive")}
|
||||||
>
|
>
|
||||||
My Drive
|
My Drive
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,9 +241,11 @@ export function DocumentsFilters({
|
||||||
aria-label={t("filter_placeholder")}
|
aria-label={t("filter_placeholder")}
|
||||||
/>
|
/>
|
||||||
{Boolean(searchValue) && (
|
{Boolean(searchValue) && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-1 top-1/2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
aria-label="Clear filter"
|
aria-label="Clear filter"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSearch("");
|
onSearch("");
|
||||||
|
|
@ -251,7 +253,7 @@ export function DocumentsFilters({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size={14} strokeWidth={2} aria-hidden="true" />
|
<X size={14} strokeWidth={2} aria-hidden="true" />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,42 +85,47 @@ export function FolderPickerDialog({
|
||||||
const FolderIcon = isExpanded ? FolderOpen : Folder;
|
const FolderIcon = isExpanded ? FolderOpen : Folder;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<button
|
<div key={f.id} className="relative w-full">
|
||||||
key={f.id}
|
{hasChildren && (
|
||||||
type="button"
|
<Button
|
||||||
disabled={isDisabled}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors",
|
|
||||||
isSelected && "bg-accent text-accent-foreground",
|
|
||||||
!isSelected && !isDisabled && "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
isDisabled && "cursor-not-allowed opacity-40"
|
|
||||||
)}
|
|
||||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isDisabled) setSelectedId(f.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hasChildren ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-4 w-4 shrink-0 items-center justify-center"
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={isDisabled}
|
||||||
|
className="absolute top-1/2 z-10 size-4 -translate-y-1/2 p-0"
|
||||||
|
style={{ left: `${depth * 16 + 8}px` }}
|
||||||
|
aria-label={isExpanded ? `Collapse ${f.name}` : `Expand ${f.name}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleExpand(f.id);
|
toggleExpand(f.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-3.5 w-3.5" />
|
<ChevronDown data-icon="inline-start" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-3.5 w-3.5" />
|
<ChevronRight data-icon="inline-start" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
|
||||||
<span className="h-4 w-4 shrink-0" />
|
|
||||||
)}
|
)}
|
||||||
<FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Button
|
||||||
<span className="truncate">{f.name}</span>
|
type="button"
|
||||||
</button>,
|
variant="ghost"
|
||||||
|
disabled={isDisabled}
|
||||||
|
className={cn(
|
||||||
|
"h-auto w-full justify-start gap-1.5 px-2 py-1.5 text-sm font-normal",
|
||||||
|
isSelected && "bg-accent text-accent-foreground",
|
||||||
|
isDisabled && "cursor-not-allowed opacity-40"
|
||||||
|
)}
|
||||||
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isDisabled) setSelectedId(f.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="size-4 shrink-0" />
|
||||||
|
<FolderIcon data-icon="inline-start" className="text-muted-foreground" />
|
||||||
|
<span className="truncate">{f.name}</span>
|
||||||
|
</Button>
|
||||||
|
</div>,
|
||||||
...(isExpanded ? renderPickerLevel(f.id, depth + 1) : []),
|
...(isExpanded ? renderPickerLevel(f.id, depth + 1) : []),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
@ -143,19 +148,19 @@ export function FolderPickerDialog({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="max-h-[300px] overflow-y-auto rounded-md border p-1">
|
<div className="max-h-[300px] overflow-y-auto rounded-md border p-1">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors",
|
"h-auto w-full justify-start gap-1.5 px-2 py-1.5 text-sm font-normal",
|
||||||
selectedId === null && "bg-accent text-accent-foreground",
|
selectedId === null && "bg-accent text-accent-foreground"
|
||||||
selectedId !== null && "hover:bg-accent hover:text-accent-foreground"
|
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedId(null)}
|
onClick={() => setSelectedId(null)}
|
||||||
>
|
>
|
||||||
<span className="h-4 w-4 shrink-0" />
|
<span className="size-4 shrink-0" />
|
||||||
<Home className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Home data-icon="inline-start" className="text-muted-foreground" />
|
||||||
<span>Root</span>
|
<span>Root</span>
|
||||||
</button>
|
</Button>
|
||||||
{renderPickerLevel(null, 1)}
|
{renderPickerLevel(null, 1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,12 +177,13 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
||||||
<div className="flex-1 overflow-y-auto p-2">
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
{versions.map((v) => (
|
{versions.map((v) => (
|
||||||
<button
|
<Button
|
||||||
key={v.version_number}
|
key={v.version_number}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => handleSelectVersion(v.version_number)}
|
onClick={() => handleSelectVersion(v.version_number)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors focus:outline-none focus-visible:outline-none w-full",
|
"h-auto w-full justify-start gap-2 rounded-lg px-3 py-2.5 text-left transition-colors focus:outline-none focus-visible:outline-none",
|
||||||
selectedVersion === v.version_number
|
selectedVersion === v.version_number
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
|
|
@ -197,7 +198,7 @@ function VersionHistoryPanel({ documentId }: { documentId: number }) {
|
||||||
{v.title && <p className="text-xs text-muted-foreground truncate">{v.title}</p>}
|
{v.title && <p className="text-xs text-muted-foreground truncate">{v.title}</p>}
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
<ChevronRight className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,16 @@ import {
|
||||||
} from "@/lib/citations/citation-parser";
|
} from "@/lib/citations/citation-parser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plate inline-void node modeling a single `[citation:...]` reference.
|
* Plate inline-void node for one `[citation:...]` reference.
|
||||||
*
|
* Inline voids keep the citation chip atomic while preserving caret behavior
|
||||||
* Modeled after the existing `MentionPlugin` pattern in
|
* around the surrounding text.
|
||||||
* `inline-mention-editor.tsx` — the only confirmed pattern in this repo
|
|
||||||
* for non-text inline UI. Inline-void elements satisfy Slate's invariant
|
|
||||||
* that the editor renders both atomic widgets and surrounding text
|
|
||||||
* cleanly without breaking selection / caret semantics.
|
|
||||||
*/
|
*/
|
||||||
export type CitationElementNode = {
|
export type CitationElementNode = {
|
||||||
type: "citation";
|
type: "citation";
|
||||||
kind: "chunk" | "doc" | "url";
|
kind: "chunk" | "doc" | "url";
|
||||||
chunkId?: number;
|
chunkId?: number;
|
||||||
url?: string;
|
url?: string;
|
||||||
/** Original `[citation:...]` substring for traceability/debugging. */
|
/** Original literal token that produced this citation node. */
|
||||||
rawText: string;
|
rawText: string;
|
||||||
children: [{ text: "" }];
|
children: [{ text: "" }];
|
||||||
};
|
};
|
||||||
|
|
@ -62,17 +58,14 @@ const CitationPlugin = createPlatePlugin({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Plugin kit shape used elsewhere in the editor. */
|
|
||||||
export const CitationKit = [CitationPlugin];
|
export const CitationKit = [CitationPlugin];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slate value transform — runs after MarkdownPlugin.deserialize
|
// Slate value transform
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Structural shapes used by the value transform. We cannot use Plate's
|
// Local structural shapes keep the recursive walker readable without forcing
|
||||||
// generic Element / Text type predicates directly because `Descendant` is a
|
// Plate's broad Descendant union into narrower generic predicates.
|
||||||
// constrained union and our predicates would over-narrow. Casting through
|
|
||||||
// these row types keeps the walker readable without fighting the types.
|
|
||||||
type SlateText = { text: string } & Record<string, unknown>;
|
type SlateText = { text: string } & Record<string, unknown>;
|
||||||
type SlateElement = { type?: string; children: Descendant[] } & Record<string, unknown>;
|
type SlateElement = { type?: string; children: Descendant[] } & Record<string, unknown>;
|
||||||
|
|
||||||
|
|
@ -89,19 +82,15 @@ function asElement(node: Descendant): SlateElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element types whose subtrees we MUST NOT inject citation void elements
|
* Subtrees that should keep citation tokens as text:
|
||||||
* into. Each rationale documented in the citation plan:
|
* - Code nodes preserve source text and reject inline void children.
|
||||||
* - `KEYS.codeBlock` / `code_line` — Plate's schema rejects inline elements
|
* - Link nodes already render as anchors; citation chips are interactive
|
||||||
* inside code containers; the user expects literal text inside code.
|
* shadcn Button-based controls, so injecting them would nest interactions.
|
||||||
* - `KEYS.link` — `<button>` inside `<a>` is invalid HTML and the link
|
|
||||||
* swallows the citation click. Mirrors the `<a>` skip in
|
|
||||||
* `MarkdownViewer`.
|
|
||||||
*/
|
*/
|
||||||
const SKIP_SUBTREE_TYPES = new Set<string>([KEYS.codeBlock, "code_line", KEYS.link]);
|
const SKIP_SUBTREE_TYPES = new Set<string>([KEYS.codeBlock, "code_line", KEYS.link]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the marks portion of a Slate text node so we can preserve formatting
|
* Preserve text marks such as bold and italic when splitting around citations.
|
||||||
* (bold/italic/etc.) on the surrounding text fragments after we split.
|
|
||||||
*/
|
*/
|
||||||
function copyMarks(textNode: SlateText): Record<string, unknown> {
|
function copyMarks(textNode: SlateText): Record<string, unknown> {
|
||||||
const { text: _text, ...marks } = textNode;
|
const { text: _text, ...marks } = textNode;
|
||||||
|
|
@ -131,9 +120,7 @@ function makeCitationElement(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-extract the raw `[citation:...]` substrings that produced each parsed
|
* Keep each original citation token on the generated node for diagnostics.
|
||||||
* segment, in source order. Lets us preserve the original literal for
|
|
||||||
* `rawText` on the inline-void element.
|
|
||||||
*/
|
*/
|
||||||
function extractRawCitationMatches(text: string): string[] {
|
function extractRawCitationMatches(text: string): string[] {
|
||||||
const matches: string[] = [];
|
const matches: string[] = [];
|
||||||
|
|
@ -159,9 +146,7 @@ function transformTextNode(node: SlateText, urlMap: CitationUrlMap): Descendant[
|
||||||
let pendingText: string | null = null;
|
let pendingText: string | null = null;
|
||||||
|
|
||||||
const flushText = () => {
|
const flushText = () => {
|
||||||
// Slate inline-void adjacency: emit an empty text node (with copied
|
// Inline voids need text siblings, even at text boundaries.
|
||||||
// marks) when the citation appears at the very start/end of the text
|
|
||||||
// node so neighbours of the void always have a text sibling.
|
|
||||||
out.push({ ...marks, text: pendingText ?? "" } as unknown as Descendant);
|
out.push({ ...marks, text: pendingText ?? "" } as unknown as Descendant);
|
||||||
pendingText = null;
|
pendingText = null;
|
||||||
};
|
};
|
||||||
|
|
@ -174,8 +159,7 @@ function transformTextNode(node: SlateText, urlMap: CitationUrlMap): Descendant[
|
||||||
const raw = rawMatches[citationIdx] ?? "";
|
const raw = rawMatches[citationIdx] ?? "";
|
||||||
out.push(makeCitationElement(raw, segment) as unknown as Descendant);
|
out.push(makeCitationElement(raw, segment) as unknown as Descendant);
|
||||||
citationIdx += 1;
|
citationIdx += 1;
|
||||||
// Always reset pendingText so the next loop iteration emits a
|
// Ensure a trailing text sibling if the citation ends the node.
|
||||||
// trailing empty text node if no further plain text follows.
|
|
||||||
pendingText = "";
|
pendingText = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,12 +190,9 @@ function transformChildren(children: Descendant[], urlMap: CitationUrlMap): Desc
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk a deserialized Slate value and replace every `[citation:...]`
|
* Replace citation tokens in a deserialized Slate tree with citation inline
|
||||||
* substring with a `citation` inline-void element. URL placeholders
|
* void nodes. URL placeholders from `preprocessCitationMarkdown` are resolved
|
||||||
* created by `preprocessCitationMarkdown` are resolved through `urlMap`.
|
* through `urlMap`; skipped subtrees are returned unchanged.
|
||||||
*
|
|
||||||
* Subtrees of `code_block`, `code_line`, and `link` are returned as-is —
|
|
||||||
* see `SKIP_SUBTREE_TYPES` above.
|
|
||||||
*/
|
*/
|
||||||
export function injectCitationNodes(value: Descendant[], urlMap: CitationUrlMap): Descendant[] {
|
export function injectCitationNodes(value: Descendant[], urlMap: CitationUrlMap): Descendant[] {
|
||||||
return transformChildren(value, urlMap);
|
return transformChildren(value, urlMap);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ArrowUpIcon, Globe, Paperclip, SquareIcon } from "lucide-react";
|
||||||
import { type FC, useCallback, useRef, useState } from "react";
|
import { type FC, useCallback, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useAnonymousMode } from "@/contexts/anonymous-mode";
|
import { useAnonymousMode } from "@/contexts/anonymous-mode";
|
||||||
|
|
@ -185,18 +186,19 @@ export const FreeComposer: FC = () => {
|
||||||
/>
|
/>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleUploadClick}
|
onClick={handleUploadClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-md px-2 py-1 text-xs transition-colors",
|
"h-auto gap-1.5 rounded-md px-2 py-1 text-xs transition-colors",
|
||||||
"text-muted-foreground hover:text-accent-foreground hover:bg-accent",
|
"text-muted-foreground hover:text-accent-foreground hover:bg-accent",
|
||||||
hasUploadedDoc && "text-primary"
|
hasUploadedDoc && "text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Paperclip className="size-3.5" />
|
<Paperclip className="size-3.5" />
|
||||||
{hasUploadedDoc ? "1/1" : "Upload"}
|
{hasUploadedDoc ? "1/1" : "Upload"}
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{hasUploadedDoc
|
{hasUploadedDoc
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { OctagonAlert, Orbit, X } from "lucide-react";
|
import { OctagonAlert, Orbit, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface QuotaWarningBannerProps {
|
interface QuotaWarningBannerProps {
|
||||||
|
|
@ -71,13 +72,15 @@ export function QuotaWarningBanner({
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
for $5 of premium credit.
|
for $5 of premium credit.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={() => setDismissed(true)}
|
onClick={() => setDismissed(true)}
|
||||||
className="text-amber-400 hover:text-amber-600 dark:hover:text-amber-200"
|
className="size-6 text-amber-400 hover:bg-transparent hover:text-amber-600 dark:hover:text-amber-200"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { IconMessageCircleQuestion } from "@tabler/icons-react";
|
import { IconMessageCircleQuestion } from "@tabler/icons-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function CTAHomepage() {
|
export function CTAHomepage() {
|
||||||
|
|
@ -22,15 +23,16 @@ export function CTAHomepage() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-start sm:items-center flex-col sm:flex-row sm:gap-4">
|
<div className="flex items-start sm:items-center flex-col sm:flex-row sm:gap-4">
|
||||||
<Link href="/contact">
|
<Button
|
||||||
<button
|
asChild
|
||||||
type="button"
|
variant="ghost"
|
||||||
className="mt-8 flex space-x-2 items-center group text-base px-4 py-2 rounded-lg text-black dark:text-white border border-neutral-200 dark:border-neutral-800 shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]"
|
className="mt-8 h-auto gap-2 rounded-lg border border-neutral-200 px-4 py-2 text-base text-black shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset] dark:border-neutral-800 dark:text-white"
|
||||||
>
|
>
|
||||||
|
<Link href="/contact" className="group">
|
||||||
<span>Talk to us</span>
|
<span>Talk to us</span>
|
||||||
<IconMessageCircleQuestion className="text-black dark:text-white group-hover:translate-x-1 stroke-[1px] h-3 w-3 mt-0.5 transition-transform duration-200" />
|
<IconMessageCircleQuestion className="text-black dark:text-white group-hover:translate-x-1 stroke-[1px] h-3 w-3 mt-0.5 transition-transform duration-200" />
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="border-t md:border-t-0 md:border-l border-dashed p-8 md:p-14">
|
{/* <div className="border-t md:border-t-0 md:border-l border-dashed p-8 md:p-14">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { AnimatePresence, motion } from "motion/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
|
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Balancer from "react-wrap-balancer";
|
import Balancer from "react-wrap-balancer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -184,24 +185,26 @@ function GetStartedButton() {
|
||||||
|
|
||||||
if (isGoogleAuth) {
|
if (isGoogleAuth) {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleGoogleLogin}
|
onClick={handleGoogleLogin}
|
||||||
className="flex h-14 w-full cursor-pointer items-center justify-center gap-3 rounded-lg bg-white text-center text-base font-medium text-neutral-700 shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-neutral-50 sm:w-56 dark:bg-neutral-900 dark:text-neutral-200 dark:ring-neutral-700/50 dark:hover:bg-neutral-800"
|
className="h-14 w-full cursor-pointer gap-3 rounded-lg bg-white text-center text-base font-medium text-neutral-700 shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-neutral-50 sm:w-56 dark:bg-neutral-900 dark:text-neutral-200 dark:ring-neutral-700/50 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<GoogleLogo className="h-5 w-5" />
|
<GoogleLogo className="h-5 w-5" />
|
||||||
<span>Continue with Google</span>
|
<span>Continue with Google</span>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Button
|
||||||
href="/login"
|
asChild
|
||||||
className="flex h-14 w-full items-center justify-center rounded-lg bg-black text-center text-base font-medium text-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 sm:w-52 dark:bg-white dark:text-black"
|
variant="ghost"
|
||||||
|
className="h-14 w-full rounded-lg bg-black text-center text-base font-medium text-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-black sm:w-52 dark:bg-white dark:text-black dark:hover:bg-white"
|
||||||
>
|
>
|
||||||
Get Started
|
<Link href="/login">Get Started</Link>
|
||||||
</Link>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,35 +215,40 @@ function DownloadButton() {
|
||||||
|
|
||||||
if (!primary) {
|
if (!primary) {
|
||||||
return (
|
return (
|
||||||
<a
|
<Button
|
||||||
href={fallbackUrl}
|
asChild
|
||||||
target="_blank"
|
variant="ghost"
|
||||||
rel="noopener noreferrer"
|
className="h-14 w-full gap-2 rounded-lg border border-neutral-200 bg-white text-center text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-98 hover:bg-neutral-50 sm:w-auto sm:px-6 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
||||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-lg border border-neutral-200 bg-white text-center text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-98 hover:bg-neutral-50 sm:w-auto sm:px-6 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
|
||||||
>
|
>
|
||||||
<Download className="size-4" />
|
<a href={fallbackUrl} target="_blank" rel="noopener noreferrer">
|
||||||
Download for {os}
|
<Download className="size-4" />
|
||||||
</a>
|
Download for {os}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-14 w-full items-stretch sm:w-auto">
|
<div className="flex h-14 w-full items-stretch sm:w-auto">
|
||||||
<a
|
<Button
|
||||||
href={primary.url}
|
asChild
|
||||||
className="flex flex-1 items-center justify-center gap-2 rounded-l-lg border border-r-0 border-neutral-200 bg-white px-5 text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-[0.99] hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
variant="ghost"
|
||||||
|
className="h-auto flex-1 gap-2 rounded-l-lg rounded-r-none border border-r-0 border-neutral-200 bg-white px-5 text-base font-medium text-neutral-700 shadow-sm transition duration-150 active:scale-[0.99] hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<Download className="size-4 shrink-0" />
|
<a href={primary.url}>
|
||||||
Download for {os}
|
<Download className="size-4 shrink-0" />
|
||||||
</a>
|
Download for {os}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center justify-center rounded-r-lg border border-neutral-200 bg-white px-2.5 text-neutral-500 shadow-sm transition duration-150 hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800"
|
variant="ghost"
|
||||||
|
className="h-auto rounded-l-none rounded-r-lg border border-neutral-200 bg-white px-2.5 text-neutral-500 shadow-sm transition duration-150 hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<ChevronDown className="size-4" />
|
<ChevronDown className="size-4" />
|
||||||
</button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-64">
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
{alternatives.map((asset) => (
|
{alternatives.map((asset) => (
|
||||||
|
|
@ -284,11 +292,12 @@ const BrowserWindow = () => {
|
||||||
<div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4">
|
<div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4">
|
||||||
{TAB_ITEMS.map((item, index) => (
|
{TAB_ITEMS.map((item, index) => (
|
||||||
<React.Fragment key={item.title}>
|
<React.Fragment key={item.title}>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => setSelectedIndex(index)}
|
onClick={() => setSelectedIndex(index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
|
"h-auto shrink-0 gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
|
||||||
selectedIndex === index &&
|
selectedIndex === index &&
|
||||||
!item.featured &&
|
!item.featured &&
|
||||||
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
|
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
|
||||||
|
|
@ -311,7 +320,7 @@ const BrowserWindow = () => {
|
||||||
<TooltipContent side="bottom">Desktop app only</TooltipContent>
|
<TooltipContent side="bottom">Desktop app only</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
{index !== TAB_ITEMS.length - 1 && (
|
{index !== TAB_ITEMS.length - 1 && (
|
||||||
<div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
|
<div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -354,13 +363,14 @@ const BrowserWindow = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950 w-full"
|
variant="ghost"
|
||||||
|
className="h-auto w-full cursor-pointer rounded-none bg-neutral-50 p-2 hover:bg-neutral-50 sm:p-3 dark:bg-neutral-950 dark:hover:bg-neutral-950"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
>
|
>
|
||||||
<TabVideo src={selectedItem.src} />
|
<TabVideo key={selectedItem.src} src={selectedItem.src} />
|
||||||
</button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -385,7 +395,7 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
video.currentTime = 0;
|
video.currentTime = 0;
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
}, [src]);
|
}, []);
|
||||||
|
|
||||||
const handleCanPlay = useCallback(() => {
|
const handleCanPlay = useCallback(() => {
|
||||||
setHasLoaded(true);
|
setHasLoaded(true);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { SignInButton } from "@/components/auth/sign-in-button";
|
||||||
import { NavbarGitHubStars } from "@/components/homepage/github-stars-badge";
|
import { NavbarGitHubStars } from "@/components/homepage/github-stars-badge";
|
||||||
import { Logo } from "@/components/Logo";
|
import { Logo } from "@/components/Logo";
|
||||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
|
|
@ -99,7 +100,7 @@ const DesktopNav = ({ navItems, isScrolled, scrolledBgClassName }: DesktopNavPro
|
||||||
onMouseEnter={() => setHovered(idx)}
|
onMouseEnter={() => setHovered(idx)}
|
||||||
onMouseLeave={() => setHovered(null)}
|
onMouseLeave={() => setHovered(null)}
|
||||||
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
|
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
|
||||||
key={`link=${idx}`}
|
key={navItem.link}
|
||||||
href={navItem.link}
|
href={navItem.link}
|
||||||
>
|
>
|
||||||
{hovered === idx && (
|
{hovered === idx && (
|
||||||
|
|
@ -179,10 +180,12 @@ const MobileNav = ({ navItems, isScrolled, scrolledBgClassName }: MobileNavProps
|
||||||
<Logo className="h-8 w-8 rounded-md" disableLink />
|
<Logo className="h-8 w-8 rounded-md" disableLink />
|
||||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
className="relative z-50 flex items-center justify-center p-2 -mr-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
className="relative z-50 -mr-2 rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 touch-manipulation"
|
||||||
aria-label={open ? "Close menu" : "Open menu"}
|
aria-label={open ? "Close menu" : "Open menu"}
|
||||||
>
|
>
|
||||||
{open ? (
|
{open ? (
|
||||||
|
|
@ -190,7 +193,7 @@ const MobileNav = ({ navItems, isScrolled, scrolledBgClassName }: MobileNavProps
|
||||||
) : (
|
) : (
|
||||||
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
|
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
@ -202,9 +205,9 @@ const MobileNav = ({ navItems, isScrolled, scrolledBgClassName }: MobileNavProps
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
className="absolute inset-x-0 top-full mt-1 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-xl bg-white/90 backdrop-blur-xl border border-white/20 shadow-2xl px-4 py-6 dark:bg-neutral-950/90 dark:border-neutral-800/50"
|
className="absolute inset-x-0 top-full mt-1 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-xl bg-white/90 backdrop-blur-xl border border-white/20 shadow-2xl px-4 py-6 dark:bg-neutral-950/90 dark:border-neutral-800/50"
|
||||||
>
|
>
|
||||||
{navItems.map((navItem: NavItem, idx: number) => (
|
{navItems.map((navItem: NavItem) => (
|
||||||
<Link
|
<Link
|
||||||
key={`link=${idx}`}
|
key={navItem.link}
|
||||||
href={navItem.link}
|
href={navItem.link}
|
||||||
className="relative text-neutral-600 dark:text-neutral-300"
|
className="relative text-neutral-600 dark:text-neutral-300"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { Settings, Trash2, Users } from "lucide-react";
|
import { Settings, Trash2, Users } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
|
|
@ -120,11 +121,13 @@ export function SearchSpaceAvatar({
|
||||||
);
|
);
|
||||||
|
|
||||||
const avatarButton = (
|
const avatarButton = (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-center justify-center rounded-lg font-semibold text-white transition-all select-none",
|
"relative rounded-lg font-semibold text-white transition-all select-none",
|
||||||
"hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
"hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
isActive && "ring-2 ring-primary ring-offset-1 ring-offset-background"
|
isActive && "ring-2 ring-primary ring-offset-1 ring-offset-background"
|
||||||
|
|
@ -144,7 +147,7 @@ export function SearchSpaceAvatar({
|
||||||
<Users className={cn(size === "sm" ? "h-2 w-2" : "h-2.5 w-2.5")} />
|
<Users className={cn(size === "sm" ? "h-2 w-2" : "h-2.5 w-2.5")} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const menuItems = (
|
const menuItems = (
|
||||||
|
|
|
||||||
|
|
@ -347,8 +347,9 @@ export function AllSharedChatsSidebarContent({
|
||||||
return (
|
return (
|
||||||
<div key={thread.id} className="group/item relative w-full">
|
<div key={thread.id} className="group/item relative w-full">
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (wasLongPress()) return;
|
if (wasLongPress()) return;
|
||||||
handleThreadClick(thread.id);
|
handleThreadClick(thread.id);
|
||||||
|
|
@ -361,7 +362,7 @@ export function AllSharedChatsSidebarContent({
|
||||||
onTouchMove={longPressHandlers.onTouchMove}
|
onTouchMove={longPressHandlers.onTouchMove}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
||||||
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||||
isActive && "bg-accent text-accent-foreground",
|
isActive && "bg-accent text-accent-foreground",
|
||||||
|
|
@ -369,16 +370,17 @@ export function AllSharedChatsSidebarContent({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip delayDuration={600}>
|
<Tooltip delayDuration={600}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => handleThreadClick(thread.id)}
|
onClick={() => handleThreadClick(thread.id)}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
||||||
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||||
isActive && "bg-accent text-accent-foreground",
|
isActive && "bg-accent text-accent-foreground",
|
||||||
|
|
@ -386,7 +388,7 @@ export function AllSharedChatsSidebarContent({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{thread.title || "New Chat"}</span>
|
<span className="truncate">{thread.title || "New Chat"}</span>
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="start">
|
<TooltipContent side="bottom" align="start">
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -56,19 +56,20 @@ export function ChatListItem({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group/item relative w-full">
|
<div className="group/item relative w-full">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...(isMobile ? longPressHandlers : {})}
|
{...(isMobile ? longPressHandlers : {})}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
|
"h-auto w-full justify-start gap-2 overflow-hidden px-2 py-1.5 text-left font-normal",
|
||||||
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||||
isActive && "bg-accent text-accent-foreground"
|
isActive && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{animatedName}</span>
|
<span className="truncate">{animatedName}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -575,14 +575,15 @@ export function InboxSidebarContent({
|
||||||
{t("filter") || "Filter"}
|
{t("filter") || "Filter"}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveFilter("all");
|
setActiveFilter("all");
|
||||||
setFilterDrawerOpen(false);
|
setFilterDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||||
activeFilter === "all"
|
activeFilter === "all"
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
|
|
@ -593,15 +594,16 @@ export function InboxSidebarContent({
|
||||||
<span>{t("all") || "All"}</span>
|
<span>{t("all") || "All"}</span>
|
||||||
</span>
|
</span>
|
||||||
{activeFilter === "all" && <Check className="h-4 w-4" />}
|
{activeFilter === "all" && <Check className="h-4 w-4" />}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveFilter("unread");
|
setActiveFilter("unread");
|
||||||
setFilterDrawerOpen(false);
|
setFilterDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||||
activeFilter === "unread"
|
activeFilter === "unread"
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
|
|
@ -612,16 +614,17 @@ export function InboxSidebarContent({
|
||||||
<span>{t("unread") || "Unread"}</span>
|
<span>{t("unread") || "Unread"}</span>
|
||||||
</span>
|
</span>
|
||||||
{activeFilter === "unread" && <Check className="h-4 w-4" />}
|
{activeFilter === "unread" && <Check className="h-4 w-4" />}
|
||||||
</button>
|
</Button>
|
||||||
{activeTab === "status" && (
|
{activeTab === "status" && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveFilter("errors");
|
setActiveFilter("errors");
|
||||||
setFilterDrawerOpen(false);
|
setFilterDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||||
activeFilter === "errors"
|
activeFilter === "errors"
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
|
|
@ -632,7 +635,7 @@ export function InboxSidebarContent({
|
||||||
<span>{t("errors_only") || "Errors only"}</span>
|
<span>{t("errors_only") || "Errors only"}</span>
|
||||||
</span>
|
</span>
|
||||||
{activeFilter === "errors" && <Check className="h-4 w-4" />}
|
{activeFilter === "errors" && <Check className="h-4 w-4" />}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -642,14 +645,15 @@ export function InboxSidebarContent({
|
||||||
{t("sources") || "Sources"}
|
{t("sources") || "Sources"}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSource(null);
|
setSelectedSource(null);
|
||||||
setFilterDrawerOpen(false);
|
setFilterDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||||
selectedSource === null
|
selectedSource === null
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
|
|
@ -660,17 +664,18 @@ export function InboxSidebarContent({
|
||||||
<span>{t("all_sources") || "All sources"}</span>
|
<span>{t("all_sources") || "All sources"}</span>
|
||||||
</span>
|
</span>
|
||||||
{selectedSource === null && <Check className="h-4 w-4" />}
|
{selectedSource === null && <Check className="h-4 w-4" />}
|
||||||
</button>
|
</Button>
|
||||||
{statusSourceOptions.map((source) => (
|
{statusSourceOptions.map((source) => (
|
||||||
<button
|
<Button
|
||||||
key={source.key}
|
key={source.key}
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSource(source.key);
|
setSelectedSource(source.key);
|
||||||
setFilterDrawerOpen(false);
|
setFilterDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
"h-auto w-full justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||||
selectedSource === source.key
|
selectedSource === source.key
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
|
|
@ -681,7 +686,7 @@ export function InboxSidebarContent({
|
||||||
<span>{source.displayName}</span>
|
<span>{source.displayName}</span>
|
||||||
</span>
|
</span>
|
||||||
{selectedSource === source.key && <Check className="h-4 w-4" />}
|
{selectedSource === source.key && <Check className="h-4 w-4" />}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -922,11 +927,12 @@ export function InboxSidebarContent({
|
||||||
{activeTab === "status" ? (
|
{activeTab === "status" ? (
|
||||||
<Tooltip delayDuration={600}>
|
<Tooltip delayDuration={600}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
disabled={isMarkingAsRead}
|
disabled={isMarkingAsRead}
|
||||||
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
|
className="h-auto flex-1 justify-start gap-3 overflow-hidden bg-transparent p-0 text-left hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<div className="shrink-0">{getStatusIcon(item)}</div>
|
<div className="shrink-0">{getStatusIcon(item)}</div>
|
||||||
<div className="flex-1 min-w-0 overflow-hidden">
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
|
|
@ -942,7 +948,7 @@ export function InboxSidebarContent({
|
||||||
{convertRenderedToDisplay(item.message)}
|
{convertRenderedToDisplay(item.message)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="start" className="max-w-[250px]">
|
<TooltipContent side="bottom" align="start" className="max-w-[250px]">
|
||||||
<p className="font-medium">{item.title}</p>
|
<p className="font-medium">{item.title}</p>
|
||||||
|
|
@ -952,11 +958,12 @@ export function InboxSidebarContent({
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
disabled={isMarkingAsRead}
|
disabled={isMarkingAsRead}
|
||||||
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
|
className="h-auto flex-1 justify-start gap-3 overflow-hidden bg-transparent p-0 text-left hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<div className="shrink-0">{getStatusIcon(item)}</div>
|
<div className="shrink-0">{getStatusIcon(item)}</div>
|
||||||
<div className="flex-1 min-w-0 overflow-hidden">
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
|
|
@ -972,7 +979,7 @@ export function InboxSidebarContent({
|
||||||
{convertRenderedToDisplay(item.message)}
|
{convertRenderedToDisplay(item.message)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">
|
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useIsAnonymous } from "@/contexts/anonymous-mode";
|
import { useIsAnonymous } from "@/contexts/anonymous-mode";
|
||||||
|
|
@ -204,13 +205,14 @@ export function Sidebar({
|
||||||
alwaysShowAction={!disableTooltips && isSharedChatsPanelOpen}
|
alwaysShowAction={!disableTooltips && isSharedChatsPanelOpen}
|
||||||
action={
|
action={
|
||||||
onViewAllSharedChats ? (
|
onViewAllSharedChats ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={onViewAllSharedChats}
|
onClick={onViewAllSharedChats}
|
||||||
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
|
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
|
||||||
</button>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -260,13 +262,14 @@ export function Sidebar({
|
||||||
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen}
|
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen}
|
||||||
action={
|
action={
|
||||||
onViewAllPrivateChats ? (
|
onViewAllPrivateChats ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={onViewAllPrivateChats}
|
onClick={onViewAllPrivateChats}
|
||||||
className="text-xs font-medium text-muted-foreground/60 hover:text-muted-foreground transition-colors whitespace-nowrap cursor-pointer bg-transparent border-none p-0 focus:outline-none"
|
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
|
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
|
||||||
</button>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,12 @@ interface SidebarButtonProps {
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
badge?: React.ReactNode;
|
badge?: React.ReactNode;
|
||||||
/** Overlay in the top-right corner of the collapsed icon (e.g. status badge) */
|
|
||||||
collapsedOverlay?: React.ReactNode;
|
collapsedOverlay?: React.ReactNode;
|
||||||
/** Custom icon node for collapsed mode — overrides the default <Icon> rendering */
|
|
||||||
collapsedIconNode?: React.ReactNode;
|
collapsedIconNode?: React.ReactNode;
|
||||||
/** Custom icon node for expanded mode — overrides the default <Icon> rendering */
|
|
||||||
expandedIconNode?: React.ReactNode;
|
expandedIconNode?: React.ReactNode;
|
||||||
/** Optional inline trailing content shown in expanded mode */
|
|
||||||
trailingContent?: React.ReactNode;
|
trailingContent?: React.ReactNode;
|
||||||
/** Optional tooltip content that replaces the default label tooltip */
|
|
||||||
tooltipContent?: React.ReactNode;
|
tooltipContent?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Extra attributes spread onto the inner <button> (e.g. data-joyride) */
|
|
||||||
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useLocaleContext } from "@/contexts/LocaleContext";
|
import { useLocaleContext } from "@/contexts/LocaleContext";
|
||||||
import { usePlatform } from "@/hooks/use-platform";
|
import { usePlatform } from "@/hooks/use-platform";
|
||||||
|
|
@ -204,11 +205,12 @@ export function SidebarUserProfile({
|
||||||
<div className="border-t px-1.5 py-2">
|
<div className="border-t px-1.5 py-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-auto flex h-9 w-9 items-center justify-center rounded-full",
|
"mx-auto h-9 w-9 rounded-full p-0",
|
||||||
"transition-opacity hover:opacity-90",
|
"transition-opacity hover:bg-transparent hover:opacity-90",
|
||||||
"focus:outline-none focus-visible:outline-none",
|
"focus:outline-none focus-visible:outline-none",
|
||||||
"data-[state=open]:opacity-90"
|
"data-[state=open]:opacity-90"
|
||||||
)}
|
)}
|
||||||
|
|
@ -220,7 +222,7 @@ export function SidebarUserProfile({
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">{displayName}</span>
|
<span className="sr-only">{displayName}</span>
|
||||||
</button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
||||||
|
|
@ -367,10 +369,11 @@ export function SidebarUserProfile({
|
||||||
<div className="border-t">
|
<div className="border-t">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 px-2 py-3 text-left",
|
"h-auto w-full justify-start gap-2 rounded-none px-2 py-3 text-left",
|
||||||
"hover:bg-accent transition-colors",
|
"hover:bg-accent transition-colors",
|
||||||
"focus:outline-none focus-visible:outline-none",
|
"focus:outline-none focus-visible:outline-none",
|
||||||
"data-[state=open]:bg-transparent"
|
"data-[state=open]:bg-transparent"
|
||||||
|
|
@ -386,7 +389,7 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
{/* Chevron icon */}
|
{/* Chevron icon */}
|
||||||
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
|
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type Tab,
|
type Tab,
|
||||||
tabsAtom,
|
tabsAtom,
|
||||||
} from "@/atoms/tabs/tabs.atom";
|
} from "@/atoms/tabs/tabs.atom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface TabBarProps {
|
interface TabBarProps {
|
||||||
|
|
@ -189,22 +190,25 @@ export function TabBar({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
data-tab-id={tab.id}
|
data-tab-id={tab.id}
|
||||||
onClick={() => handleTabClick(tab)}
|
className="group relative h-full w-[180px] shrink-0"
|
||||||
onMouseEnter={() => setHoveredTabIndex(index)}
|
|
||||||
onMouseLeave={() => setHoveredTabIndex(null)}
|
|
||||||
className={cn(
|
|
||||||
"group relative flex h-full items-center px-3 w-[180px] min-h-0 overflow-hidden text-[13px] font-medium rounded-md transition-colors duration-150 shrink-0",
|
|
||||||
isActive
|
|
||||||
? "bg-accent text-accent-foreground"
|
|
||||||
: "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span className="block min-w-0 flex-1 whitespace-nowrap overflow-hidden text-left">
|
<Button
|
||||||
{tab.title}
|
type="button"
|
||||||
</span>
|
variant="ghost"
|
||||||
|
onClick={() => handleTabClick(tab)}
|
||||||
|
onMouseEnter={() => setHoveredTabIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredTabIndex(null)}
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full justify-start overflow-hidden px-3 text-left text-[13px] font-medium transition-colors duration-150",
|
||||||
|
isActive
|
||||||
|
? "bg-accent text-accent-foreground"
|
||||||
|
: "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground group-hover:bg-accent group-hover:text-accent-foreground group-focus-within:bg-accent group-focus-within:text-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="block min-w-0 flex-1 truncate text-left">{tab.title}</span>
|
||||||
|
</Button>
|
||||||
{/* Hover-only gradient + close overlay (sidebar pattern) — keeps pill width fixed and avoids ellipsis shift. */}
|
{/* Hover-only gradient + close overlay (sidebar pattern) — keeps pill width fixed and avoids ellipsis shift. */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -213,23 +217,19 @@ export function TabBar({
|
||||||
"bg-gradient-to-l from-accent from-60% to-transparent"
|
"bg-gradient-to-l from-accent from-60% to-transparent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */}
|
<Button
|
||||||
<span
|
type="button"
|
||||||
role="button"
|
variant="ghost"
|
||||||
tabIndex={0}
|
size="icon"
|
||||||
onClick={(e) => handleTabClose(e, tab.id)}
|
onClick={(e) => handleTabClose(e, tab.id)}
|
||||||
onKeyDown={(e) => {
|
onMouseEnter={() => setHoveredTabIndex(index)}
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
onMouseLeave={() => setHoveredTabIndex(null)}
|
||||||
e.preventDefault();
|
className="pointer-events-auto size-4 rounded-full p-0.5 hover:bg-accent hover:text-accent-foreground"
|
||||||
handleTabClose(e as unknown as React.MouseEvent, tab.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="pointer-events-auto rounded-full p-0.5 transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X data-icon="inline-start" />
|
||||||
</span>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -243,14 +243,16 @@ export function TabBar({
|
||||||
"before:bg-gradient-to-r before:from-transparent before:to-panel"
|
"before:bg-gradient-to-r before:from-transparent before:to-panel"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={onNewChat}
|
onClick={onNewChat}
|
||||||
className="flex h-8 w-8 items-center justify-center shrink-0 rounded-md text-muted-foreground transition-all duration-150 hover:bg-accent hover:text-accent-foreground"
|
className="size-8 shrink-0 text-muted-foreground transition-all duration-150 hover:bg-accent hover:text-accent-foreground"
|
||||||
title="New Chat"
|
title="New Chat"
|
||||||
>
|
>
|
||||||
<Plus className="size-4" />
|
<Plus data-icon="inline-start" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -143,18 +143,20 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
{hasPublicSnapshots && (
|
{hasPublicSnapshots && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSearchSpaceSettingsDialog({
|
setSearchSpaceSettingsDialog({
|
||||||
open: true,
|
open: true,
|
||||||
initialTab: "public-links",
|
initialTab: "public-links",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex items-center justify-center h-8 w-8 rounded-md bg-muted/50 hover:bg-accent hover:text-accent-foreground transition-colors"
|
className="size-8 bg-muted/50 hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<Earth className="h-4 w-4 text-muted-foreground" />
|
<Earth data-icon="inline-start" className="text-muted-foreground" />
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Manage public links</TooltipContent>
|
<TooltipContent>Manage public links</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -185,14 +187,13 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
const Icon = option.icon;
|
const Icon = option.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => handleVisibilityChange(option.value)}
|
onClick={() => handleVisibilityChange(option.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-colors",
|
"h-auto w-full justify-start gap-2.5 whitespace-normal px-2.5 py-2 font-normal",
|
||||||
"hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
|
||||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
isSelected && "bg-accent text-accent-foreground"
|
isSelected && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -204,7 +205,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 block",
|
"block",
|
||||||
isSelected ? "text-primary dark:text-white" : "text-muted-foreground"
|
isSelected ? "text-primary dark:text-white" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -224,7 +225,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
{option.description}
|
{option.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
@ -234,19 +235,18 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
<div className="border-t border-popover-border my-1" />
|
<div className="border-t border-popover-border my-1" />
|
||||||
|
|
||||||
{/* Public Link Option */}
|
{/* Public Link Option */}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
onClick={handleCreatePublicLink}
|
onClick={handleCreatePublicLink}
|
||||||
disabled={isCreatingSnapshot}
|
disabled={isCreatingSnapshot}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-colors",
|
"h-auto w-full justify-start gap-2.5 whitespace-normal px-2.5 py-2 font-normal",
|
||||||
"hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
"disabled:cursor-not-allowed"
|
||||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted dark:bg-white/5">
|
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted dark:bg-white/5">
|
||||||
<Earth className="size-4 block text-muted-foreground" />
|
<Earth className="block text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-left min-w-0">
|
<div className="flex-1 text-left min-w-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|
@ -258,7 +258,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
Creates a shareable snapshot of this chat
|
Creates a shareable snapshot of this chat
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -115,13 +115,15 @@ function buildFolderTree(entries: FolderEntry[]): FolderTreeNode[] {
|
||||||
|
|
||||||
function flattenTree(
|
function flattenTree(
|
||||||
nodes: FolderTreeNode[],
|
nodes: FolderTreeNode[],
|
||||||
depth = 0
|
depth = 0,
|
||||||
): { name: string; isFolder: boolean; depth: number; size?: number }[] {
|
parentPath = ""
|
||||||
const items: { name: string; isFolder: boolean; depth: number; size?: number }[] = [];
|
): { name: string; isFolder: boolean; depth: number; path: string; size?: number }[] {
|
||||||
|
const items: { name: string; isFolder: boolean; depth: number; path: string; size?: number }[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
items.push({ name: node.name, isFolder: node.isFolder, depth, size: node.size });
|
const path = parentPath ? `${parentPath}/${node.name}` : node.name;
|
||||||
|
items.push({ name: node.name, isFolder: node.isFolder, depth, path, size: node.size });
|
||||||
if (node.isFolder && node.children.length > 0) {
|
if (node.isFolder && node.children.length > 0) {
|
||||||
items.push(...flattenTree(node.children, depth + 1));
|
items.push(...flattenTree(node.children, depth + 1, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|
@ -537,37 +539,39 @@ export function DocumentUploadTab({
|
||||||
isElectron ? (
|
isElectron ? (
|
||||||
<div className="w-full">{renderBrowseButton({ compact: true, fullWidth: true })}</div>
|
<div className="w-full">{renderBrowseButton({ compact: true, fullWidth: true })}</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full text-xs h-8 flex items-center justify-center gap-1.5 rounded-md border border-dashed border-muted-foreground/30 text-muted-foreground hover:text-accent-foreground hover:border-foreground/50 transition-colors"
|
variant="ghost"
|
||||||
|
className="h-8 w-full gap-1.5 rounded-md border border-dashed border-muted-foreground/30 px-0 text-xs text-muted-foreground transition-colors hover:border-foreground/50 hover:bg-transparent hover:text-accent-foreground"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
Add more files
|
Add more files
|
||||||
</button>
|
</Button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
// biome-ignore lint/a11y/useSemanticElements: cannot use <button> here because the contents include nested interactive elements (renderBrowseButton renders a Button), which would be invalid HTML.
|
<div className="flex w-full flex-col items-center gap-4 bg-transparent px-4 py-12 select-none">
|
||||||
<div
|
{isElectron ? (
|
||||||
role="button"
|
<div className="flex w-full flex-col items-center gap-4">
|
||||||
tabIndex={0}
|
<Upload className="h-10 w-10 text-muted-foreground" />
|
||||||
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
|
<div className="text-center space-y-1.5">
|
||||||
onClick={() => {
|
<p className="text-base font-medium">{t("select_files_or_folder")}</p>
|
||||||
if (!isElectron) fileInputRef.current?.click();
|
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
||||||
}}
|
</div>
|
||||||
onKeyDown={(e) => {
|
</div>
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
) : (
|
||||||
e.preventDefault();
|
<Button
|
||||||
if (!isElectron) fileInputRef.current?.click();
|
type="button"
|
||||||
}
|
variant="ghost"
|
||||||
}}
|
className="h-auto w-full flex-col gap-4 whitespace-normal bg-transparent p-0 text-foreground hover:bg-transparent hover:text-foreground"
|
||||||
>
|
onClick={() => fileInputRef.current?.click()}
|
||||||
<Upload className="h-10 w-10 text-muted-foreground" />
|
>
|
||||||
<div className="text-center space-y-1.5">
|
<Upload className="h-10 w-10 text-muted-foreground" />
|
||||||
<p className="text-base font-medium">
|
<div className="text-center space-y-1.5">
|
||||||
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
|
<p className="text-base font-medium">{t("tap_select_files_or_folder")}</p>
|
||||||
</p>
|
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
||||||
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
|
)}
|
||||||
<fieldset
|
<fieldset
|
||||||
className="w-full mt-1 border-none p-0 m-0"
|
className="w-full mt-1 border-none p-0 m-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|
@ -649,9 +653,9 @@ export function DocumentUploadTab({
|
||||||
|
|
||||||
<div className="max-h-[160px] sm:max-h-[200px] overflow-y-auto -mx-1">
|
<div className="max-h-[160px] sm:max-h-[200px] overflow-y-auto -mx-1">
|
||||||
{folderUpload
|
{folderUpload
|
||||||
? folderTreeItems.map((item, i) => (
|
? folderTreeItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={`${item.depth}-${i}-${item.name}`}
|
key={item.path}
|
||||||
className="flex items-center gap-1.5 py-0.5 px-2"
|
className="flex items-center gap-1.5 py-0.5 px-2"
|
||||||
style={{ paddingLeft: `${item.depth * 16 + 8}px` }}
|
style={{ paddingLeft: `${item.depth * 16 + 8}px` }}
|
||||||
>
|
>
|
||||||
|
|
@ -726,10 +730,11 @@ export function DocumentUploadTab({
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="font-medium text-sm px-1">{t("processing_mode")}</p>
|
<p className="font-medium text-sm px-1">{t("processing_mode")}</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setProcessingMode("basic")}
|
onClick={() => setProcessingMode("basic")}
|
||||||
className={`flex items-start gap-2.5 rounded-lg border p-3 text-left transition-colors ${
|
variant="ghost"
|
||||||
|
className={`h-auto w-full items-start justify-start whitespace-normal rounded-lg border p-3 text-left transition-colors hover:bg-transparent hover:text-foreground ${
|
||||||
processingMode === "basic"
|
processingMode === "basic"
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border hover:border-muted-foreground/50"
|
: "border-border hover:border-muted-foreground/50"
|
||||||
|
|
@ -742,11 +747,12 @@ export function DocumentUploadTab({
|
||||||
<p className="font-medium text-sm">{t("basic_mode")}</p>
|
<p className="font-medium text-sm">{t("basic_mode")}</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("basic_mode_desc")}</p>
|
<p className="text-xs text-muted-foreground">{t("basic_mode_desc")}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setProcessingMode("premium")}
|
onClick={() => setProcessingMode("premium")}
|
||||||
className={`flex items-start gap-2.5 rounded-lg border p-3 text-left transition-colors ${
|
variant="ghost"
|
||||||
|
className={`h-auto w-full items-start justify-start whitespace-normal rounded-lg border p-3 text-left transition-colors hover:bg-transparent hover:text-foreground ${
|
||||||
processingMode === "premium"
|
processingMode === "premium"
|
||||||
? "border-amber-500 bg-amber-500/5"
|
? "border-amber-500 bg-amber-500/5"
|
||||||
: "border-border hover:border-muted-foreground/50"
|
: "border-border hover:border-muted-foreground/50"
|
||||||
|
|
@ -759,7 +765,7 @@ export function DocumentUploadTab({
|
||||||
<p className="font-medium text-sm">{t("premium_mode")}</p>
|
<p className="font-medium text-sm">{t("premium_mode")}</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("premium_mode_desc")}</p>
|
<p className="text-xs text-muted-foreground">{t("premium_mode_desc")}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
} from "platejs/react";
|
} from "platejs/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
const popoverVariants = cva(
|
const popoverVariants = cva(
|
||||||
|
|
@ -116,13 +116,9 @@ export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarStat
|
||||||
input
|
input
|
||||||
) : (
|
) : (
|
||||||
<div className="box-content flex items-center">
|
<div className="box-content flex items-center">
|
||||||
<button
|
<Button size="sm" variant="ghost" type="button" {...editButtonProps}>
|
||||||
className={buttonVariants({ size: "sm", variant: "ghost" })}
|
|
||||||
type="button"
|
|
||||||
{...editButtonProps}
|
|
||||||
>
|
|
||||||
Edit link
|
Edit link
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
|
|
||||||
|
|
@ -130,16 +126,9 @@ export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarStat
|
||||||
|
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
|
|
||||||
<button
|
<Button size="sm" variant="ghost" type="button" {...unlinkButtonProps}>
|
||||||
className={buttonVariants({
|
|
||||||
size: "sm",
|
|
||||||
variant: "ghost",
|
|
||||||
})}
|
|
||||||
type="button"
|
|
||||||
{...unlinkButtonProps}
|
|
||||||
>
|
|
||||||
<Unlink width={18} />
|
<Unlink width={18} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -158,29 +147,22 @@ export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarStat
|
||||||
|
|
||||||
function LinkOpenButton() {
|
function LinkOpenButton() {
|
||||||
const editor = useEditorRef();
|
const editor = useEditorRef();
|
||||||
const selection = useEditorSelection();
|
useEditorSelection();
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: selection triggers recalculation of link attributes
|
const entry = editor.api.node<TLinkElement>({
|
||||||
const attributes = React.useMemo(() => {
|
match: { type: editor.getType(KEYS.link) },
|
||||||
const entry = editor.api.node<TLinkElement>({
|
});
|
||||||
match: { type: editor.getType(KEYS.link) },
|
const attributes = entry ? getLinkAttributes(editor, entry[0]) : {};
|
||||||
});
|
const href = typeof attributes.href === "string" ? attributes.href : undefined;
|
||||||
if (!entry) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const [element] = entry;
|
|
||||||
return getLinkAttributes(editor, element);
|
|
||||||
}, [editor, selection]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/noStaticElementInteractions: <a> with spread attributes has dynamic href
|
<Button
|
||||||
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label needed for icon-only link
|
size="sm"
|
||||||
<a
|
variant="ghost"
|
||||||
{...attributes}
|
type="button"
|
||||||
className={buttonVariants({
|
onClick={() => {
|
||||||
size: "sm",
|
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
||||||
variant: "ghost",
|
}}
|
||||||
})}
|
|
||||||
onMouseOver={(e) => {
|
onMouseOver={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
|
@ -188,10 +170,9 @@ function LinkOpenButton() {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
aria-label="Open link in a new tab"
|
aria-label="Open link in a new tab"
|
||||||
target="_blank"
|
disabled={!href}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<ExternalLink width={18} />
|
<ExternalLink width={18} />
|
||||||
</a>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue