diff options
| -rw-r--r-- | apps/web/app/layout.tsx | 6 | ||||
| -rw-r--r-- | apps/web/app/reader/[bookmarkId]/page.tsx | 312 | ||||
| -rw-r--r-- | apps/web/components/dashboard/lists/ShareListModal.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx | 3 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/LinkContentSection.tsx | 237 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/ReaderView.tsx | 121 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/TextContentSection.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/ui/slider.tsx | 27 | ||||
| -rw-r--r-- | apps/web/package.json | 1 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 35 |
10 files changed, 583 insertions, 163 deletions
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index d5af9e35..e8673c78 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -50,11 +50,7 @@ export default async function RootLayout({ const userSettings = await getUserLocalSettings(); const isRTL = userSettings.lang === "ar"; return ( - <html - className="sm:overflow-hidden" - lang={userSettings.lang} - dir={isRTL ? "rtl" : "ltr"} - > + <html className="" lang={userSettings.lang} dir={isRTL ? "rtl" : "ltr"}> <body className={inter.className}> <NuqsAdapter> <Providers diff --git a/apps/web/app/reader/[bookmarkId]/page.tsx b/apps/web/app/reader/[bookmarkId]/page.tsx new file mode 100644 index 00000000..7c2b0c9e --- /dev/null +++ b/apps/web/app/reader/[bookmarkId]/page.tsx @@ -0,0 +1,312 @@ +"use client"; + +import { Suspense, useState } from "react"; +import { useRouter } from "next/navigation"; +import HighlightCard from "@/components/dashboard/highlights/HighlightCard"; +import ReaderView from "@/components/dashboard/preview/ReaderView"; +import { Button } from "@/components/ui/button"; +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Slider } from "@/components/ui/slider"; +import { + HighlighterIcon as Highlight, + Minus, + Plus, + Printer, + Settings, + Type, + X, +} from "lucide-react"; + +import { api } from "@karakeep/shared-react/trpc"; +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; +import { getBookmarkTitle } from "@karakeep/shared/utils/bookmarkUtils"; + +export default function ReaderViewPage({ + params, +}: { + params: { bookmarkId: string }; +}) { + const bookmarkId = params.bookmarkId; + const { data: highlights } = api.highlights.getForBookmark.useQuery({ + bookmarkId, + }); + const { data: bookmark } = api.bookmarks.getBookmark.useQuery({ + bookmarkId, + }); + + const router = useRouter(); + const [fontSize, setFontSize] = useState([18]); + const [lineHeight, setLineHeight] = useState([1.6]); + const [fontFamily, setFontFamily] = useState("serif"); + const [showHighlights, setShowHighlights] = useState(false); + const [showSettings, setShowSettings] = useState(false); + + const fontFamilies = { + serif: "ui-serif, Georgia, Cambria, serif", + sans: "ui-sans-serif, system-ui, sans-serif", + mono: "ui-monospace, Menlo, Monaco, monospace", + }; + + const onClose = () => { + if (window.history.length > 1) { + router.back(); + } else { + router.push("/dashboard"); + } + }; + + const handlePrint = () => { + window.print(); + }; + + return ( + <div className="min-h-screen bg-background"> + {/* Header */} + <header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 print:hidden"> + <div className="flex h-14 items-center justify-between px-4"> + <div className="flex items-center gap-2"> + <Button variant="ghost" size="icon" onClick={onClose}> + <X className="h-4 w-4" /> + </Button> + <span className="text-sm text-muted-foreground">Reader View</span> + </div> + + <div className="flex items-center gap-2"> + <Button variant="ghost" size="icon" onClick={handlePrint}> + <Printer className="h-4 w-4" /> + </Button> + + <Popover open={showSettings} onOpenChange={setShowSettings}> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon"> + <Settings className="h-4 w-4" /> + </Button> + </PopoverTrigger> + <PopoverContent side="bottom" align="end" className="w-80"> + <div className="space-y-4"> + <div className="flex items-center gap-2 pb-2"> + <Type className="h-4 w-4" /> + <h3 className="font-semibold">Reading Settings</h3> + </div> + + <div className="space-y-4"> + <div className="space-y-2"> + <label className="text-sm font-medium">Font Family</label> + <Select value={fontFamily} onValueChange={setFontFamily}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="serif">Serif</SelectItem> + <SelectItem value="sans">Sans Serif</SelectItem> + <SelectItem value="mono">Monospace</SelectItem> + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium">Font Size</label> + <span className="text-sm text-muted-foreground"> + {fontSize[0]}px + </span> + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + setFontSize([Math.max(12, fontSize[0] - 1)]) + } + > + <Minus className="h-3 w-3" /> + </Button> + <Slider + value={fontSize} + onValueChange={setFontSize} + max={24} + min={12} + step={1} + className="flex-1" + /> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + setFontSize([Math.min(24, fontSize[0] + 1)]) + } + > + <Plus className="h-3 w-3" /> + </Button> + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium"> + Line Height + </label> + <span className="text-sm text-muted-foreground"> + {lineHeight[0]} + </span> + </div> + <Slider + value={lineHeight} + onValueChange={setLineHeight} + max={2.5} + min={1.2} + step={0.1} + /> + </div> + </div> + </div> + </PopoverContent> + </Popover> + + <Button + variant={showHighlights ? "default" : "ghost"} + size="icon" + onClick={() => setShowHighlights(!showHighlights)} + > + <Highlight className="h-4 w-4" /> + </Button> + </div> + </div> + </header> + + <div className="flex overflow-hidden"> + {/* Mobile backdrop */} + {showHighlights && ( + <button + className="fixed inset-0 top-14 z-40 bg-black/50 lg:hidden" + onClick={() => setShowHighlights(false)} + onKeyDown={(e) => { + if (e.key === "Escape") { + setShowHighlights(false); + } + }} + aria-label="Close highlights sidebar" + /> + )} + + {/* Main Content */} + <main + className={`flex-1 overflow-x-hidden transition-all duration-300 ${showHighlights ? "lg:mr-80" : ""}`} + > + <article className="mx-auto max-w-3xl overflow-x-hidden px-4 py-8 sm:px-6"> + {bookmark ? ( + <> + {/* Article Header */} + <header className="mb-8 space-y-4"> + <h1 + className="font-bold leading-tight" + style={{ + fontFamily: + fontFamilies[fontFamily as keyof typeof fontFamilies], + fontSize: `${fontSize[0] * 1.8}px`, + lineHeight: lineHeight[0] * 0.9, + }} + > + {getBookmarkTitle(bookmark)} + </h1> + <div className="flex items-center gap-4 text-sm text-muted-foreground"> + {bookmark.content.type == BookmarkTypes.LINK && ( + <span>By {bookmark.content.author}</span> + )} + <Separator orientation="vertical" className="h-4" /> + <span>8 min</span> + </div> + </header> + + {/* Article Content */} + <Suspense fallback={<FullPageSpinner />}> + <div className="overflow-x-hidden"> + <ReaderView + className="prose prose-neutral max-w-none break-words dark:prose-invert [&_code]:break-all [&_img]:h-auto [&_img]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:overflow-x-auto" + style={{ + fontFamily: + fontFamilies[fontFamily as keyof typeof fontFamilies], + fontSize: `${fontSize[0]}px`, + lineHeight: lineHeight[0], + }} + bookmarkId={bookmarkId} + /> + </div> + </Suspense> + </> + ) : ( + <FullPageSpinner /> + )} + </article> + </main> + + {/* Mobile backdrop */} + {showHighlights && ( + <button + className="fixed inset-0 top-14 z-40 bg-black/50 lg:hidden" + onClick={() => setShowHighlights(false)} + onKeyDown={(e) => { + if (e.key === "Escape") { + setShowHighlights(false); + } + }} + aria-label="Close highlights sidebar" + /> + )} + + {/* Highlights Sidebar */} + {showHighlights && highlights && ( + <aside className="fixed right-0 top-14 z-50 h-[calc(100vh-3.5rem)] w-full border-l bg-background sm:w-80 lg:z-auto lg:bg-background/95 lg:backdrop-blur lg:supports-[backdrop-filter]:bg-background/60 print:hidden"> + <div className="flex h-full flex-col"> + <div className="border-b p-4"> + <div className="flex items-center justify-between"> + <h2 className="font-semibold">Highlights</h2> + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + {highlights.highlights.length} saved + </span> + <Button + variant="ghost" + size="icon" + className="h-6 w-6 lg:hidden" + onClick={() => setShowHighlights(false)} + > + <X className="h-4 w-4" /> + </Button> + </div> + </div> + </div> + + <div className="flex-1 overflow-auto p-4"> + <div className="space-y-4"> + {highlights.highlights.map((highlight) => ( + <HighlightCard + key={highlight.id} + highlight={highlight} + clickable={true} + /> + ))} + </div> + </div> + </div> + </aside> + )} + </div> + </div> + ); +} diff --git a/apps/web/components/dashboard/lists/ShareListModal.tsx b/apps/web/components/dashboard/lists/ShareListModal.tsx index 16668e67..4b8218a9 100644 --- a/apps/web/components/dashboard/lists/ShareListModal.tsx +++ b/apps/web/components/dashboard/lists/ShareListModal.tsx @@ -4,13 +4,13 @@ import { Dialog, DialogClose, DialogContent, + DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { useTranslation } from "@/lib/i18n/client"; -import { DialogDescription } from "@radix-ui/react-dialog"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; diff --git a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx index dc446112..19499d3e 100644 --- a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx +++ b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx @@ -92,6 +92,7 @@ export interface Highlight { interface HTMLHighlighterProps { htmlContent: string; + style?: React.CSSProperties; className?: string; highlights?: Highlight[]; onHighlight?: (highlight: Highlight) => void; @@ -102,6 +103,7 @@ interface HTMLHighlighterProps { function BookmarkHTMLHighlighter({ htmlContent, className, + style, highlights = [], onHighlight, onUpdateHighlight, @@ -345,6 +347,7 @@ function BookmarkHTMLHighlighter({ dangerouslySetInnerHTML={{ __html: htmlContent }} onPointerUp={handlePointerUp} className={className} + style={style} /> <ColorPickerMenu position={menuPosition} diff --git a/apps/web/components/dashboard/preview/LinkContentSection.tsx b/apps/web/components/dashboard/preview/LinkContentSection.tsx index e86be6b7..eefec701 100644 --- a/apps/web/components/dashboard/preview/LinkContentSection.tsx +++ b/apps/web/components/dashboard/preview/LinkContentSection.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import Image from "next/image"; -import BookmarkHTMLHighlighter from "@/components/dashboard/preview/BookmarkHtmlHighlighter"; -import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import Link from "next/link"; +import { buttonVariants } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, @@ -10,23 +11,22 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { toast } from "@/components/ui/use-toast"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; -import { ScrollArea } from "@radix-ui/react-scroll-area"; -import { Archive, BookOpen, Camera, Video } from "lucide-react"; +import { Archive, BookOpen, Camera, ExpandIcon, Video } from "lucide-react"; import { - useCreateHighlight, - useDeleteHighlight, - useUpdateHighlight, -} from "@karakeep/shared-react/hooks/highlights"; -import { BookmarkTypes, ZBookmark, ZBookmarkedLink, } from "@karakeep/shared/types/bookmarks"; +import ReaderView from "./ReaderView"; + function FullPageArchiveSection({ link }: { link: ZBookmarkedLink }) { const archiveAssetId = link.fullPageArchiveAssetId ?? link.precrawledArchiveAssetId; @@ -54,106 +54,6 @@ function ScreenshotSection({ link }: { link: ZBookmarkedLink }) { ); } -function CachedContentSection({ bookmarkId }: { bookmarkId: string }) { - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: cachedContent, isPending: isCachedContentLoading } = - api.bookmarks.getBookmark.useQuery( - { - bookmarkId, - includeContent: true, - }, - { - select: (data) => - data.content.type == BookmarkTypes.LINK - ? data.content.htmlContent - : null, - }, - ); - - const { mutate: createHighlight } = useCreateHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been created!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - const { mutate: updateHighlight } = useUpdateHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been updated!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - const { mutate: deleteHighlight } = useDeleteHighlight({ - onSuccess: () => { - toast({ - description: "Highlight has been deleted!", - }); - }, - onError: () => { - toast({ - variant: "destructive", - description: "Something went wrong", - }); - }, - }); - - let content; - if (isCachedContentLoading) { - content = <FullPageSpinner />; - } else if (!cachedContent) { - content = ( - <div className="text-destructive">Failed to fetch link content ...</div> - ); - } else { - content = ( - <BookmarkHTMLHighlighter - htmlContent={cachedContent || ""} - className="prose mx-auto dark:prose-invert" - highlights={highlights?.highlights ?? []} - onDeleteHighlight={(h) => - deleteHighlight({ - highlightId: h.id, - }) - } - onUpdateHighlight={(h) => - updateHighlight({ - highlightId: h.id, - color: h.color, - }) - } - onHighlight={(h) => - createHighlight({ - startOffset: h.startOffset, - endOffset: h.endOffset, - color: h.color, - bookmarkId, - text: h.text, - note: null, - }) - } - /> - ); - } - return <ScrollArea className="h-full">{content}</ScrollArea>; -} - function VideoSection({ link }: { link: ZBookmarkedLink }) { return ( <div className="relative h-full w-full overflow-hidden"> @@ -182,7 +82,14 @@ export default function LinkContentSection({ let content; if (section === "cached") { - content = <CachedContentSection bookmarkId={bookmark.id} />; + content = ( + <ScrollArea className="h-full"> + <ReaderView + className="prose mx-auto dark:prose-invert" + bookmarkId={bookmark.id} + /> + </ScrollArea> + ); } else if (section === "archive") { content = <FullPageArchiveSection link={bookmark.content} />; } else if (section === "video") { @@ -193,50 +100,68 @@ export default function LinkContentSection({ return ( <div className="flex h-full flex-col items-center gap-2"> - <Select onValueChange={setSection} value={section}> - <SelectTrigger className="w-fit"> - <span className="mr-2"> - <SelectValue /> - </span> - </SelectTrigger> - <SelectContent> - <SelectGroup> - <SelectItem value="cached"> - <div className="flex items-center"> - <BookOpen className="mr-2 h-4 w-4" /> - {t("preview.reader_view")} - </div> - </SelectItem> - <SelectItem - value="screenshot" - disabled={!bookmark.content.screenshotAssetId} - > - <div className="flex items-center"> - <Camera className="mr-2 h-4 w-4" /> - {t("common.screenshot")} - </div> - </SelectItem> - <SelectItem - value="archive" - disabled={ - !bookmark.content.fullPageArchiveAssetId && - !bookmark.content.precrawledArchiveAssetId - } - > - <div className="flex items-center"> - <Archive className="mr-2 h-4 w-4" /> - {t("common.archive")} - </div> - </SelectItem> - <SelectItem value="video" disabled={!bookmark.content.videoAssetId}> - <div className="flex items-center"> - <Video className="mr-2 h-4 w-4" /> - {t("common.video")} - </div> - </SelectItem> - </SelectGroup> - </SelectContent> - </Select> + <div className="flex items-center gap-2"> + <Select onValueChange={setSection} value={section}> + <SelectTrigger className="w-fit"> + <span className="mr-2"> + <SelectValue /> + </span> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value="cached"> + <div className="flex items-center"> + <BookOpen className="mr-2 h-4 w-4" /> + {t("preview.reader_view")} + </div> + </SelectItem> + <SelectItem + value="screenshot" + disabled={!bookmark.content.screenshotAssetId} + > + <div className="flex items-center"> + <Camera className="mr-2 h-4 w-4" /> + {t("common.screenshot")} + </div> + </SelectItem> + <SelectItem + value="archive" + disabled={ + !bookmark.content.fullPageArchiveAssetId && + !bookmark.content.precrawledArchiveAssetId + } + > + <div className="flex items-center"> + <Archive className="mr-2 h-4 w-4" /> + {t("common.archive")} + </div> + </SelectItem> + <SelectItem + value="video" + disabled={!bookmark.content.videoAssetId} + > + <div className="flex items-center"> + <Video className="mr-2 h-4 w-4" /> + {t("common.video")} + </div> + </SelectItem> + </SelectGroup> + </SelectContent> + </Select> + {section === "cached" && ( + <Tooltip> + <TooltipTrigger> + <Link + href={`/reader/${bookmark.id}`} + className={buttonVariants({ variant: "outline" })} + > + <ExpandIcon className="h-4 w-4" /> + </Link> + </TooltipTrigger> + <TooltipContent side="bottom">FullScreen</TooltipContent> + </Tooltip> + )} + </div> {content} </div> ); diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx new file mode 100644 index 00000000..bf4c27a5 --- /dev/null +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -0,0 +1,121 @@ +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; + +import { + useCreateHighlight, + useDeleteHighlight, + useUpdateHighlight, +} from "@karakeep/shared-react/hooks/highlights"; +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; + +import BookmarkHTMLHighlighter from "./BookmarkHtmlHighlighter"; + +export default function ReaderView({ + bookmarkId, + className, + style, +}: { + bookmarkId: string; + className?: string; + style?: React.CSSProperties; +}) { + const { data: highlights } = api.highlights.getForBookmark.useQuery({ + bookmarkId, + }); + const { data: cachedContent, isPending: isCachedContentLoading } = + api.bookmarks.getBookmark.useQuery( + { + bookmarkId, + includeContent: true, + }, + { + select: (data) => + data.content.type == BookmarkTypes.LINK + ? data.content.htmlContent + : null, + }, + ); + + const { mutate: createHighlight } = useCreateHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been created!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + const { mutate: updateHighlight } = useUpdateHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been updated!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + const { mutate: deleteHighlight } = useDeleteHighlight({ + onSuccess: () => { + toast({ + description: "Highlight has been deleted!", + }); + }, + onError: () => { + toast({ + variant: "destructive", + description: "Something went wrong", + }); + }, + }); + + let content; + if (isCachedContentLoading) { + content = <FullPageSpinner />; + } else if (!cachedContent) { + content = ( + <div className="text-destructive">Failed to fetch link content ...</div> + ); + } else { + content = ( + <BookmarkHTMLHighlighter + className={className} + style={style} + htmlContent={cachedContent || ""} + highlights={highlights?.highlights ?? []} + onDeleteHighlight={(h) => + deleteHighlight({ + highlightId: h.id, + }) + } + onUpdateHighlight={(h) => + updateHighlight({ + highlightId: h.id, + color: h.color, + }) + } + onHighlight={(h) => + createHighlight({ + startOffset: h.startOffset, + endOffset: h.endOffset, + color: h.color, + bookmarkId, + text: h.text, + note: null, + }) + } + /> + ); + } + return content; +} diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx index 4e33bb92..a4510698 100644 --- a/apps/web/components/dashboard/preview/TextContentSection.tsx +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; -import { ScrollArea } from "@radix-ui/react-scroll-area"; +import { ScrollArea } from "@/components/ui/scroll-area"; import type { ZBookmarkTypeText } from "@karakeep/shared/types/bookmarks"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; diff --git a/apps/web/components/ui/slider.tsx b/apps/web/components/ui/slider.tsx new file mode 100644 index 00000000..a789595b --- /dev/null +++ b/apps/web/components/ui/slider.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +const Slider = React.forwardRef< + React.ElementRef<typeof SliderPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> +>(({ className, ...props }, ref) => ( + <SliderPrimitive.Root + ref={ref} + className={cn( + "relative flex w-full touch-none select-none items-center", + className, + )} + {...props} + > + <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> + <SliderPrimitive.Range className="absolute h-full bg-primary" /> + </SliderPrimitive.Track> + <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" /> + </SliderPrimitive.Root> +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/web/package.json b/apps/web/package.json index bcff4f65..df88638e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e960968d..3294fc51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -546,6 +546,9 @@ importers: '@radix-ui/react-separator': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.12))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.3.5 + version: 1.3.5(@types/react-dom@18.3.7(@types/react@18.3.12))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@18.3.12)(react@18.3.1) @@ -4492,6 +4495,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.3.5': + resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.1': resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} peerDependencies: @@ -19895,6 +19911,25 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.7(@types/react@18.3.12) + '@radix-ui/react-slider@1.3.5(@types/react-dom@18.3.7(@types/react@18.3.12))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.12))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.12))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.7(@types/react@18.3.12) + '@radix-ui/react-slot@1.0.1(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 |
