diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-08-20 15:57:34 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-20 13:57:34 +0100 |
| commit | dd53ccb9624e719d019a8fe29fcd66415c1b1528 (patch) | |
| tree | 5788b06d4280248cf0c0f837318b0722f6d4cdd7 /apps | |
| parent | 5f07b5075dd45b4b0f4ab35ee70412f11177eff4 (diff) | |
| download | karakeep-dd53ccb9624e719d019a8fe29fcd66415c1b1528.tar.zst | |
deps: Upgrade expo & nextjs to react 19 (#1565)
* Attempt to upgrade expo 53
* Attempt upgrade nextjs
* Fix a bunch of peer deps
* upgrade some docs deps
* fix typecheck
* update the shadcn calendar component
* more fixes
* more fixes
* revert ollama upgrade
* update react version to use carets
* remove react-select from landing
* fix the typescript error caused by customFetch
* upgrade the new grid user setting to nextjs 15
* mobile: enable react canary to support react 19.1
* upgrade react native menu
* fix navigation context error
Diffstat (limited to 'apps')
25 files changed, 360 insertions, 218 deletions
diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index 4f8befd1..88e55285 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -30,8 +30,8 @@ "clsx": "^2.1.0", "cmdk": "^1.1.1", "lucide-react": "^0.501.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.22.0", "superjson": "^2.2.1", "tailwind-merge": "^2.2.1", @@ -44,8 +44,8 @@ "@karakeep/tailwind-config": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", "@types/chrome": "^0.0.260", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react-swc": "^3.11.0", "autoprefixer": "^10.4.17", "postcss": "^8.4.35", diff --git a/apps/landing/package.json b/apps/landing/package.json index e6db4d83..b8329356 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -20,10 +20,9 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.501.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router": "^7.7.1", - "react-select": "^5.8.0", "sharp": "^0.33.3", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7" @@ -33,8 +32,8 @@ "@karakeep/tailwind-config": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", "@tailwindcss/typography": "^0.5.10", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.17", "postcss": "^8.4.35", diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js index 7e8ab546..1b60c74f 100644 --- a/apps/mobile/app.config.js +++ b/apps/mobile/app.config.js @@ -9,6 +9,9 @@ export default { light: "./assets/icon.png", tinted: "./assets/icon-tinted.png", }, + experiments: { + reactCanary: true, + }, userInterfaceStyle: "automatic", assetBundlePatterns: ["**/*"], ios: { @@ -89,6 +92,7 @@ export default { }, }, ], + "expo-web-browser", ], extra: { router: { diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index e1751f1e..ca3da0cb 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -32,42 +32,47 @@ export default function RootLayout() { }, [settings.theme]); return ( - <GestureHandlerRootView style={{ flex: 1 }}> - <ShareIntentProvider> - <Providers> - <StyledStack - contentClassName={cn( - "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", - colorScheme == "dark" ? "dark" : "light", - )} - screenOptions={{ - headerTitle: "", - headerTransparent: true, - }} - > - <Stack.Screen name="index" /> - <Stack.Screen - name="signin" - options={{ - headerShown: true, - headerBackVisible: true, - headerBackTitle: "Back", - title: "", - }} - /> - <Stack.Screen name="sharing" /> - <Stack.Screen - name="test-connection" - options={{ - title: "Test Connection", - headerShown: true, - presentation: "modal", - }} - /> - </StyledStack> - <StatusBar style="auto" /> - </Providers> - </ShareIntentProvider> - </GestureHandlerRootView> + <> + <StyledStack + layout={(props) => { + return ( + <GestureHandlerRootView style={{ flex: 1 }}> + <ShareIntentProvider> + <Providers>{props.children}</Providers> + </ShareIntentProvider> + </GestureHandlerRootView> + ); + }} + contentClassName={cn( + "w-full flex-1 bg-gray-100 text-foreground dark:bg-background", + colorScheme == "dark" ? "dark" : "light", + )} + screenOptions={{ + headerTitle: "", + headerTransparent: true, + }} + > + <Stack.Screen name="index" /> + <Stack.Screen + name="signin" + options={{ + headerShown: true, + headerBackVisible: true, + headerBackTitle: "Back", + title: "", + }} + /> + <Stack.Screen name="sharing" /> + <Stack.Screen + name="test-connection" + options={{ + title: "Test Connection", + headerShown: true, + presentation: "modal", + }} + /> + </StyledStack> + <StatusBar style="auto" /> + </> ); } diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 941b4c83..506b5100 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; import { useRouter } from "expo-router"; import { useShareIntentContext } from "expo-share-intent"; @@ -83,7 +83,7 @@ export default function Sharing() { const router = useRouter(); const [mode, setMode] = useState<Mode>({ type: "idle" }); - let autoCloseTimeoutId: NodeJS.Timeout | null = null; + const autoCloseTimeoutId = useRef<number | null>(null); let comp; switch (mode.type) { @@ -102,8 +102,8 @@ export default function Sharing() { label="Manage" onPress={() => { router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`); - if (autoCloseTimeoutId) { - clearTimeout(autoCloseTimeoutId); + if (autoCloseTimeoutId.current) { + clearTimeout(autoCloseTimeoutId.current); } }} /> @@ -126,11 +126,11 @@ export default function Sharing() { return; } - autoCloseTimeoutId = setTimeout(() => { + autoCloseTimeoutId.current = setTimeout(() => { router.replace("dashboard"); }, 2000); - return () => clearTimeout(autoCloseTimeoutId!); + return () => clearTimeout(autoCloseTimeoutId.current!); }, [mode.type]); return ( diff --git a/apps/mobile/components/ui/Input.tsx b/apps/mobile/components/ui/Input.tsx index dc84f54f..2bd5e190 100644 --- a/apps/mobile/components/ui/Input.tsx +++ b/apps/mobile/components/ui/Input.tsx @@ -1,47 +1,46 @@ +import type { TextInputProps } from "react-native"; import { forwardRef } from "react"; import { ActivityIndicator, Text, TextInput, View } from "react-native"; import { cn } from "@/lib/utils"; import { TailwindResolver } from "../TailwindResolver"; -export interface InputProps - extends React.ComponentPropsWithoutRef<typeof TextInput> { +export interface InputProps extends TextInputProps { label?: string; labelClasses?: string; inputClasses?: string; + loading?: boolean; } -const Input = forwardRef< - React.ElementRef<typeof TextInput>, - InputProps & { loading?: boolean } ->( +export const Input = forwardRef<TextInput, InputProps>( ( { className, label, labelClasses, inputClasses, loading, ...props }, ref, - ) => ( - <View className={cn("flex flex-col gap-1.5", className)}> - {label && <Text className={cn("text-base", labelClasses)}>{label}</Text>} - <TailwindResolver - className="text-gray-400" - comp={(styles) => ( - <TextInput - placeholderTextColor={styles?.color?.toString()} - ref={ref} - className={cn( - "bg-background text-foreground", - inputClasses, - "rounded-lg border border-input px-4 py-2.5", - )} - {...props} - /> + ) => { + return ( + <View className={cn("flex flex-col gap-1.5", className)}> + {label && ( + <Text className={cn("text-base", labelClasses)}>{label}</Text> )} - /> - {loading && ( - <ActivityIndicator className="absolute bottom-0 right-0 p-2" /> - )} - </View> - ), + <TailwindResolver + className="text-gray-400" + comp={(styles) => ( + <TextInput + ref={ref} + placeholderTextColor={styles?.color?.toString()} + className={cn( + "bg-background text-foreground", + inputClasses, + "rounded-lg border border-input px-4 py-2.5", + )} + {...props} + /> + )} + /> + {loading && ( + <ActivityIndicator className="absolute bottom-0 right-0 p-2" /> + )} + </View> + ); + }, ); -Input.displayName = "Input"; - -export { Input }; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 80e4ffcb..0ed5668c 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -19,55 +19,55 @@ "@karakeep/shared-react": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@react-native-async-storage/async-storage": "1.23.1", - "@react-native-menu/menu": "^1.1.6", + "@react-native-menu/menu": "^1.2.4", "@tanstack/react-query": "^5.80.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", - "expo": "~52.0.46", - "expo-build-properties": "^0.13.3", - "expo-checkbox": "^4.0.1", - "expo-clipboard": "^7.0.1", - "expo-constants": "~17.0.8", - "expo-dev-client": "^5.0.20", + "expo": "~53.0.11", + "expo-build-properties": "^0.14.6", + "expo-checkbox": "^4.1.4", + "expo-clipboard": "^7.1.4", + "expo-constants": "~17.1.6", + "expo-dev-client": "^5.2.0", "expo-file-system": "~18.0.12", - "expo-haptics": "^14.0.1", - "expo-image": "^2.0.7", - "expo-image-picker": "^16.0.6", - "expo-linking": "~7.0.5", - "expo-navigation-bar": "^4.0.9", - "expo-router": "~4.0.21", - "expo-secure-store": "^14.0.1", - "expo-share-intent": "3.2.3", + "expo-haptics": "^14.1.4", + "expo-image": "^2.2.0", + "expo-image-picker": "^16.1.4", + "expo-linking": "~7.1.5", + "expo-navigation-bar": "^4.2.5", + "expo-router": "~5.0.7", + "expo-secure-store": "^14.2.3", + "expo-share-intent": "^4.0.0", "expo-sharing": "~13.0.1", - "expo-status-bar": "~2.0.1", - "expo-system-ui": "^4.0.9", - "expo-web-browser": "^14.0.2", - "lucide-react-native": "^0.354.0", + "expo-status-bar": "~2.2.3", + "expo-system-ui": "^5.0.8", + "expo-web-browser": "^14.1.6", + "lucide-react-native": "^0.513.0", "nativewind": "^4.1.23", - "react": "^18.3.1", - "react-native": "0.76.9", + "react": "^19.1.0", + "react-native": "0.79.3", "react-native-awesome-slider": "^2.5.3", "react-native-blob-util": "^0.21.2", - "react-native-gesture-handler": "~2.20.2", + "react-native-gesture-handler": "~2.24.0", "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", "react-native-pdf": "^6.7.7", - "react-native-reanimated": "^3.16.2", - "react-native-safe-area-context": "4.12.0", - "react-native-screens": "~4.4.0", - "react-native-svg": "^15.8.0", - "react-native-webview": "^13.12.5", + "react-native-reanimated": "^3.17.5", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-svg": "^15.11.2", + "react-native-webview": "^13.13.5", "tailwind-merge": "^2.2.1", "use-debounce": "^10.0.0", "zod": "^3.24.2", - "zustand": "^4.5.1" + "zustand": "^5.0.5" }, "devDependencies": { "@babel/core": "~7.26.0", "@karakeep/prettier-config": "workspace:^0.1.0", "@karakeep/tailwind-config": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", - "@types/react": "^18.3.12", + "@types/react": "^19.1.6", "ajv": "latest", "prettier": "^3.4.2", "tailwindcss": "^3.4.1", diff --git a/apps/web/app/admin/layout.tsx b/apps/web/app/admin/layout.tsx index 62a6932a..4b589712 100644 --- a/apps/web/app/admin/layout.tsx +++ b/apps/web/app/admin/layout.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { redirect } from "next/navigation"; import { AdminNotices } from "@/components/admin/AdminNotices"; import MobileSidebar from "@/components/shared/sidebar/MobileSidebar"; @@ -11,7 +12,7 @@ const adminSidebarItems = ( t: TFunction, ): { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; }[] => [ { diff --git a/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx b/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx index 432e7a6c..77d84ec5 100644 --- a/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx +++ b/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx @@ -1,15 +1,14 @@ "use client"; -import { useState } from "react"; +import { use, useState } from "react"; import { useRouter } from "next/navigation"; import BookmarkPreview from "@/components/dashboard/preview/BookmarkPreview"; import { Dialog, DialogContent } from "@/components/ui/dialog"; -export default function BookmarkPreviewPage({ - params, -}: { - params: { bookmarkId: string }; +export default function BookmarkPreviewPage(props: { + params: Promise<{ bookmarkId: string }>; }) { + const params = use(props.params); const router = useRouter(); const [open, setOpen] = useState(true); diff --git a/apps/web/app/dashboard/feeds/[feedId]/page.tsx b/apps/web/app/dashboard/feeds/[feedId]/page.tsx index ed5f9e40..a73f0f32 100644 --- a/apps/web/app/dashboard/feeds/[feedId]/page.tsx +++ b/apps/web/app/dashboard/feeds/[feedId]/page.tsx @@ -3,11 +3,10 @@ import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export default async function FeedPage({ - params, -}: { - params: { feedId: string }; +export default async function FeedPage(props: { + params: Promise<{ feedId: string }>; }) { + const params = await props.params; let feed; try { feed = await api.feeds.get({ feedId: params.feedId }); diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index de0f5054..4714a71c 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -6,15 +6,14 @@ import { TRPCError } from "@trpc/server"; import { BookmarkListContextProvider } from "@karakeep/shared-react/hooks/bookmark-list-context"; -export default async function ListPage({ - params, - searchParams, -}: { - params: { listId: string }; - searchParams?: { +export default async function ListPage(props: { + params: Promise<{ listId: string }>; + searchParams?: Promise<{ includeArchived?: string; - }; + }>; }) { + const searchParams = await props.searchParams; + const params = await props.params; const userSettings = await api.users.settings(); let list; try { diff --git a/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx index 236f5447..ea509207 100644 --- a/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx +++ b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx @@ -3,11 +3,10 @@ import BookmarkPreview from "@/components/dashboard/preview/BookmarkPreview"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export default async function BookmarkPreviewPage({ - params, -}: { - params: { bookmarkId: string }; +export default async function BookmarkPreviewPage(props: { + params: Promise<{ bookmarkId: string }>; }) { + const params = await props.params; let bookmark; try { bookmark = await api.bookmarks.getBookmark({ diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/tags/[tagId]/page.tsx index b33a351a..7971da1e 100644 --- a/apps/web/app/dashboard/tags/[tagId]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagId]/page.tsx @@ -7,15 +7,14 @@ import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; import { MoreHorizontal } from "lucide-react"; -export default async function TagPage({ - params, - searchParams, -}: { - params: { tagId: string }; - searchParams?: { +export default async function TagPage(props: { + params: Promise<{ tagId: string }>; + searchParams?: Promise<{ includeArchived?: string; - }; + }>; }) { + const searchParams = await props.searchParams; + const params = await props.params; let tag; try { tag = await api.tags.get({ tagId: params.tagId }); diff --git a/apps/web/app/public/lists/[listId]/page.tsx b/apps/web/app/public/lists/[listId]/page.tsx index 4a4ce414..17e6b947 100644 --- a/apps/web/app/public/lists/[listId]/page.tsx +++ b/apps/web/app/public/lists/[listId]/page.tsx @@ -6,11 +6,10 @@ import PublicListHeader from "@/components/public/lists/PublicListHeader"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export async function generateMetadata({ - params, -}: { - params: { listId: string }; +export async function generateMetadata(props: { + params: Promise<{ listId: string }>; }): Promise<Metadata> { + const params = await props.params; try { const resp = await api.publicBookmarks.getPublicListMetadata({ listId: params.listId, @@ -38,11 +37,10 @@ export async function generateMetadata({ }; } -export default async function PublicListPage({ - params, -}: { - params: { listId: string }; +export default async function PublicListPage(props: { + params: Promise<{ listId: string }>; }) { + const params = await props.params; try { const { list, bookmarks, nextCursor } = await api.publicBookmarks.getPublicBookmarksInList({ diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 94a5c4d7..982ac61a 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -25,7 +25,7 @@ const settingsSidebarItems = ( t: TFunction, ): { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; }[] => { return [ diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index a5966845..7ac1cade 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -24,7 +24,9 @@ import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { useUploadAsset } from "../UploadDropzone"; -function useFocusOnKeyPress(inputRef: React.RefObject<HTMLTextAreaElement>) { +function useFocusOnKeyPress( + inputRef: React.RefObject<HTMLTextAreaElement | null>, +) { useEffect(() => { function handleKeyPress(e: KeyboardEvent) { if (!inputRef.current) { diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 0de7694a..dad995e1 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -35,7 +35,7 @@ import QueryExplainerTooltip from "./QueryExplainerTooltip"; const MAX_DISPLAY_SUGGESTIONS = 5; function useFocusSearchOnKeyPress( - inputRef: React.RefObject<HTMLInputElement>, + inputRef: React.RefObject<HTMLInputElement | null>, value: string, setValue: (value: string) => void, setPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>, diff --git a/apps/web/components/shared/sidebar/TSidebarItem.ts b/apps/web/components/shared/sidebar/TSidebarItem.ts index 84cd58f5..a1ea4c97 100644 --- a/apps/web/components/shared/sidebar/TSidebarItem.ts +++ b/apps/web/components/shared/sidebar/TSidebarItem.ts @@ -1,5 +1,5 @@ export interface TSidebarItem { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; } diff --git a/apps/web/components/theme-provider.tsx b/apps/web/components/theme-provider.tsx index 737e1356..1ab9a49d 100644 --- a/apps/web/components/theme-provider.tsx +++ b/apps/web/components/theme-provider.tsx @@ -1,6 +1,6 @@ "use client"; -import type { ThemeProviderProps } from "next-themes/dist/types"; +import type { ThemeProviderProps } from "next-themes"; import * as React from "react"; import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"; diff --git a/apps/web/components/ui/calendar.tsx b/apps/web/components/ui/calendar.tsx index 99a082f6..e2a13e9e 100644 --- a/apps/web/components/ui/calendar.tsx +++ b/apps/web/components/ui/calendar.tsx @@ -1,69 +1,209 @@ "use client"; import * as React from "react"; -import { buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import { DayPicker } from "react-day-picker"; - -export type CalendarProps = React.ComponentProps<typeof DayPicker>; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react"; +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; function Calendar({ className, classNames, showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, ...props -}: CalendarProps) { +}: React.ComponentProps<typeof DayPicker> & { + buttonVariant?: React.ComponentProps<typeof Button>["variant"]; +}) { + const defaultClassNames = getDefaultClassNames(); + return ( <DayPicker showOutsideDays={showOutsideDays} - className={cn("p-3", className)} + className={cn( + "group/calendar bg-background p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", + String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: cn( - buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", - ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: - "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months, + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_next, + ), + month_caption: cn( + "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", + defaultClassNames.month_caption, + ), + dropdowns: cn( + "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + "has-focus:border-ring shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border border-input", + defaultClassNames.dropdown_root, + ), + dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", + defaultClassNames.caption_label, + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "flex-1 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground", + defaultClassNames.weekday, + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-[--cell-size] select-none", + defaultClassNames.week_number_header, + ), + week_number: cn( + "select-none text-[0.8rem] text-muted-foreground", + defaultClassNames.week_number, + ), day: cn( - buttonVariants({ variant: "ghost" }), - "h-9 w-9 p-0 font-normal aria-selected:opacity-100", - ), - day_range_end: "day-range-end", - day_selected: - "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: - "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: - "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", + "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", + defaultClassNames.day, + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start, + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none", + defaultClassNames.today, + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside, + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled, + ), + hidden: cn("invisible", defaultClassNames.hidden), ...classNames, }} components={{ - IconLeft: ({ className, ...props }) => ( - <ChevronLeft className={cn("h-4 w-4", className)} {...props} /> - ), - IconRight: ({ className, ...props }) => ( - <ChevronRight className={cn("h-4 w-4", className)} {...props} /> - ), + Root: ({ className, rootRef, ...props }) => { + return ( + <div + data-slot="calendar" + ref={rootRef} + className={cn(className)} + {...props} + /> + ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + <ChevronLeftIcon className={cn("size-4", className)} {...props} /> + ); + } + + if (orientation === "right") { + return ( + <ChevronRightIcon + className={cn("size-4", className)} + {...props} + /> + ); + } + + return ( + <ChevronDownIcon className={cn("size-4", className)} {...props} /> + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + <td {...props}> + <div className="flex size-[--cell-size] items-center justify-center text-center"> + {children} + </div> + </td> + ); + }, + ...components, }} {...props} /> ); } -Calendar.displayName = "Calendar"; -export { Calendar }; +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps<typeof DayButton>) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef<HTMLButtonElement>(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( + <Button + ref={ref} + variant="ghost" + size="icon" + data-day={day.date.toLocaleDateString()} + data-selected-single={ + modifiers.selected && + !modifiers.range_start && + !modifiers.range_end && + !modifiers.range_middle + } + data-range-start={modifiers.range_start} + data-range-end={modifiers.range_end} + data-range-middle={modifiers.range_middle} + className={cn( + "flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 [&>span]:text-xs [&>span]:opacity-70", + defaultClassNames.day, + className, + )} + {...props} + /> + ); +} + +export { Calendar, CalendarDayButton }; diff --git a/apps/web/components/ui/markdown/markdown-readonly.tsx b/apps/web/components/ui/markdown/markdown-readonly.tsx index 29077480..b945b7ab 100644 --- a/apps/web/components/ui/markdown/markdown-readonly.tsx +++ b/apps/web/components/ui/markdown/markdown-readonly.tsx @@ -34,7 +34,6 @@ export function MarkdownReadonly({ children: markdown }: { children: string }) { code({ className, children, ...props }) { const match = /language-(\w+)/.exec(className ?? ""); return match ? ( - // @ts-expect-error -- Refs are not compatible for some reason <SyntaxHighlighter PreTag="div" language={match[1]} diff --git a/apps/web/lib/userLocalSettings/userLocalSettings.ts b/apps/web/lib/userLocalSettings/userLocalSettings.ts index 85ec69a6..11bd0a84 100644 --- a/apps/web/lib/userLocalSettings/userLocalSettings.ts +++ b/apps/web/lib/userLocalSettings/userLocalSettings.ts @@ -10,16 +10,16 @@ import { } from "./types"; export async function getUserLocalSettings(): Promise<UserLocalSettings> { - const userSettings = cookies().get(USER_LOCAL_SETTINGS_COOKIE_NAME); + const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME); return ( parseUserLocalSettings(userSettings?.value) ?? defaultUserLocalSettings() ); } export async function updateBookmarksLayout(layout: BookmarksLayoutTypes) { - const userSettings = cookies().get(USER_LOCAL_SETTINGS_COOKIE_NAME); + const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME); const parsed = parseUserLocalSettings(userSettings?.value); - cookies().set({ + (await cookies()).set({ name: USER_LOCAL_SETTINGS_COOKIE_NAME, value: JSON.stringify({ ...parsed, bookmarkGridLayout: layout }), maxAge: 34560000, // Chrome caps max age to 400 days @@ -28,9 +28,9 @@ export async function updateBookmarksLayout(layout: BookmarksLayoutTypes) { } export async function updateInterfaceLang(lang: string) { - const userSettings = cookies().get(USER_LOCAL_SETTINGS_COOKIE_NAME); + const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME); const parsed = parseUserLocalSettings(userSettings?.value); - cookies().set({ + (await cookies()).set({ name: USER_LOCAL_SETTINGS_COOKIE_NAME, value: JSON.stringify({ ...parsed, lang }), maxAge: 34560000, // Chrome caps max age to 400 days @@ -39,9 +39,9 @@ export async function updateInterfaceLang(lang: string) { } export async function updateGridColumns(gridColumns: number) { - const userSettings = cookies().get(USER_LOCAL_SETTINGS_COOKIE_NAME); + const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME); const parsed = parseUserLocalSettings(userSettings?.value); - cookies().set({ + (await cookies()).set({ name: USER_LOCAL_SETTINGS_COOKIE_NAME, value: JSON.stringify({ ...parsed, gridColumns }), maxAge: 34560000, // Chrome caps max age to 400 days diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 40c3d680..1b3be084 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -2,4 +2,4 @@ /// <reference types="next/image-types/global" /> // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index 8f741e67..719dcab7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -67,25 +67,25 @@ "i18next-resources-to-backend": "^1.2.1", "lexical": "^0.20.2", "lucide-react": "^0.501.0", - "next": "14.2.25", + "next": "15.3.3", "next-auth": "^4.24.11", "next-i18next": "^15.3.1", "next-pwa": "^5.6.0", - "next-themes": "^0.3.0", + "next-themes": "^0.4.0", "nuqs": "^2.4.3", "prettier": "^3.4.2", - "react": "^18.3.1", - "react-day-picker": "8.10.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-day-picker": "^9.7.0", + "react-dom": "^19.1.0", "react-draggable": "^4.4.6", "react-dropzone": "^14.2.3", "react-error-boundary": "^5.0.0", - "react-hook-form": "^7.50.1", + "react-hook-form": "^7.57.0", "react-i18next": "^15.1.1", "react-intersection-observer": "^9.13.1", "react-markdown": "^9.0.1", "react-masonry-css": "^1.0.16", - "react-select": "^5.8.0", + "react-select": "^5.10.1", "react-syntax-highlighter": "^15.5.0", "react-tweet": "^3.2.2", "remark-breaks": "^4.0.0", @@ -95,7 +95,7 @@ "superjson": "^2.2.1", "tailwind-merge": "^2.2.1", "zod": "^3.24.2", - "zustand": "^4.5.1" + "zustand": "^5.0.5" }, "devDependencies": { "@karakeep/prettier-config": "workspace:^0.1.0", @@ -103,8 +103,8 @@ "@karakeep/tsconfig": "workspace:^0.1.0", "@types/csv-parse": "^1.2.5", "@types/emoji-mart": "^3.0.14", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.6", "@types/react-syntax-highlighter": "^15.5.13", "@types/request-ip": "^0.0.41", "autoprefixer": "^10.4.17", diff --git a/apps/web/server/api/client.ts b/apps/web/server/api/client.ts index 69a8e10a..0795d8c3 100644 --- a/apps/web/server/api/client.ts +++ b/apps/web/server/api/client.ts @@ -39,7 +39,7 @@ export const createContext = async ( ): Promise<Context> => { const session = await getServerAuthSession(); if (ip === undefined) { - const hdrs = headers(); + const hdrs = await headers(); ip = requestIp.getClientIp({ headers: Object.fromEntries(hdrs.entries()), }); |
