diff --git a/packages/client/package.json b/packages/client/package.json index dd8e595..f6f8f3c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.7.0", diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 790f223..539f446 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -19,19 +19,20 @@ import { TooltipProvider } from "./components/ui/tooltip"; import Gallery from "./page/galleryPage"; import Layout from "./components/layout/layout"; import NotFoundPage from "./page/404"; +import LoginPage from "./page/loginPage"; +import ProfilePage from "./page/profilesPage"; const App = () => { return ( - } /> + + {/* }> }> - } /> - }> }> }> }>*/} diff --git a/packages/client/src/component/headline.tsx b/packages/client/src/component/headline.tsx deleted file mode 100644 index 712fe81..0000000 --- a/packages/client/src/component/headline.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { AccountCircle, ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon } from "@mui/icons-material"; -import { - AppBar, - Button, - CssBaseline, - Divider, - Drawer, - Hidden, - IconButton, - InputBase, - Link, - List, - ListItem, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - styled, - Toolbar, - Tooltip, - Typography, -} from "@mui/material"; -import { alpha, Theme, useTheme } from "@mui/material/styles"; -import React, { useContext, useState } from "react"; - -import { Link as RouterLink, useNavigate } from "react-router-dom"; -import { doLogout, UserContext } from "../state"; - -const drawerWidth = 270; - -const DrawerHeader = styled("div")(({ theme }) => ({ - ...theme.mixins.toolbar, -})); - -const StyledDrawer = styled(Drawer)(({ theme }) => ({ - flexShrink: 0, - whiteSpace: "nowrap", - [theme.breakpoints.up("sm")]: { - width: drawerWidth, - }, -})); -const StyledSearchBar = styled("div")(({ theme }) => ({ - position: "relative", - borderRadius: theme.shape.borderRadius, - backgroundColor: alpha(theme.palette.common.white, 0.15), - "&:hover": { - backgroundColor: alpha(theme.palette.common.white, 0.25), - }, - marginLeft: 0, - width: "100%", - [theme.breakpoints.up("sm")]: { - marginLeft: theme.spacing(1), - width: "auto", - }, -})); -const StyledInputBase = styled(InputBase)(({ theme }) => ({ - color: "inherit", - "& .MuiInputBase-input": { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(4)})`, - transition: theme.transitions.create("width"), - width: "100%", - [theme.breakpoints.up("sm")]: { - width: "12ch", - "&:focus": { - width: "20ch", - }, - }, - }, -})); - -const StyledNav = styled("nav")(({ theme }) => ({ - [theme.breakpoints.up("sm")]: { - width: theme.spacing(7), - }, -})); - -const closedMixin = (theme: Theme) => ({ - overflowX: "hidden", - width: `calc(${theme.spacing(7)} + 1px)`, -}); - -export const Headline = (prop: { - children?: React.ReactNode; - classes?: { - content?: string; - toolbar?: string; - }; - rightAppbar?: React.ReactNode; - menu: React.ReactNode; -}) => { - const [v, setv] = useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); - const theme = useTheme(); - const toggleV = () => setv(!v); - const handleProfileMenuOpen = (e: React.MouseEvent) => setAnchorEl(e.currentTarget); - const handleProfileMenuClose = () => setAnchorEl(null); - const isProfileMenuOpened = Boolean(anchorEl); - const menuId = "primary-search-account-menu"; - const user_ctx = useContext(UserContext); - const isLogin = user_ctx.username !== ""; - const navigate = useNavigate(); - const [search, setSearch] = useState(""); - - const renderProfileMenu = ( - - - Profile - - { - handleProfileMenuClose(); - await doLogout(); - user_ctx.setUsername(""); - }} - > - Logout - - - ); - const drawer_contents = ( - <> - - {theme.direction === "ltr" ? : } - - - {prop.menu} - - ); - - return ( -
- - - - - - - - Ionian - -
- {prop.rightAppbar} - -
- navSearch(search)} /> -
- setSearch(e.target.value)} - onKeyUp={(e) => { - if (e.key === "Enter") { - navSearch(search); - } - }} - value={search} - /> -
- {isLogin ? ( - - - - ) : ( - - )} -
-
- {renderProfileMenu} - - - - {drawer_contents} - - - - - {drawer_contents} - - - -
- {prop.children} -
-
- ); - function navSearch(search: string) { - let words = search.includes("&") ? search.split("&") : [search]; - words = words - .map((w) => w.trim()) - .map((w) => (w.includes(":") ? `allow_tag=${w}` : `word=${encodeURIComponent(w)}`)); - navigate(`/search?${words.join("&")}`); - } -}; - -export default Headline; diff --git a/packages/client/src/component/loading.tsx b/packages/client/src/component/loading.tsx deleted file mode 100644 index 93b69ed..0000000 --- a/packages/client/src/component/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Box, CircularProgress } from "@mui/material"; -import React from "react"; - -export const LoadingCircle = () => { - return ( - - - - ); -}; diff --git a/packages/client/src/component/navlist.tsx b/packages/client/src/component/navlist.tsx deleted file mode 100644 index a78b0f5..0000000 --- a/packages/client/src/component/navlist.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - ArrowBack as ArrowBackIcon, - Collections as CollectionIcon, - Folder as FolderIcon, - Home as HomeIcon, - List as ListIcon, - Settings as SettingIcon, - VideoLibrary as VideoIcon, -} from "@mui/icons-material"; -import { Divider, List, ListItem, ListItemIcon, ListItemText, Tooltip } from "@mui/material"; -import React from "react"; -import { Link as RouterLink } from "react-router-dom"; - -export const NavItem = (props: { name: string; to: string; icon: React.ReactElement }) => { - return ( - - - - {props.icon} - - - - - ); -}; - -export const NavList = (props: { children?: React.ReactNode }) => { - return {props.children}; -}; - -export const BackItem = (props: { to?: string }) => { - return } />; -}; - -export function CommonMenuList(props?: { url?: string }) { - let url = props?.url ?? ""; - return ( - - {url !== "" && ( - <> - - - )} - } /> - }> - } /> - - } /> - - }> - } /> - - ); -} diff --git a/packages/client/src/component/pagepad.tsx b/packages/client/src/component/pagepad.tsx deleted file mode 100644 index 8726512..0000000 --- a/packages/client/src/component/pagepad.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { styled } from "@mui/material"; - -export const PagePad = styled("div")(({ theme }) => ({ - padding: theme.spacing(3), -})); diff --git a/packages/client/src/component/tagchip.tsx b/packages/client/src/component/tagchip.tsx deleted file mode 100644 index dfb90e6..0000000 --- a/packages/client/src/component/tagchip.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as colors from "@mui/material/colors"; -import Chip, { ChipTypeMap } from "@mui/material/Chip"; -import { emphasize, styled, Theme, useTheme } from "@mui/material/styles"; -import React from "react"; -import { Link as RouterLink } from "react-router-dom"; - -type TagChipStyleProp = { - color: `rgba(${number},${number},${number},${number})` | `#${string}` | "default"; -}; - -const { blue, pink } = colors; -const getTagColorName = (tagname: string): TagChipStyleProp["color"] => { - if (tagname.startsWith("female")) { - return pink[600]; - } else if (tagname.startsWith("male")) { - return blue[600]; - } else return "default"; -}; - -type ColorChipProp = Omit & - TagChipStyleProp & { - component?: React.ElementType; - to?: string; - }; - -export const ColorChip = (props: ColorChipProp) => { - const { color, ...rest } = props; - const theme = useTheme(); - - let newcolor = color; - if (color === "default") { - newcolor = "#ebebeb"; - } - return ( - - ); -}; - -type TagChipProp = Omit & { - tagname: string; -}; - -export const TagChip = (props: TagChipProp) => { - const { tagname, label, clickable, ...rest } = props; - const colorName = getTagColorName(tagname); - - let newlabel: React.ReactNode = label; - if (typeof label === "string") { - const female = "female:"; - const male = "male:"; - if (label.startsWith(female)) { - newlabel = "♀ " + label.slice(female.length); - } else if (label.startsWith(male)) { - newlabel = "♂ " + label.slice(male.length); - } - } - - const inner = clickable ? ( - - ) : ( - - ); - return inner; -}; diff --git a/packages/client/src/components/gallery/GalleryCard.tsx b/packages/client/src/components/gallery/GalleryCard.tsx index 797c075..30f6bcc 100644 --- a/packages/client/src/components/gallery/GalleryCard.tsx +++ b/packages/client/src/components/gallery/GalleryCard.tsx @@ -15,6 +15,43 @@ function clipTagsWhenOverflow(tags: string[], limit: number) { return tags; } +function LazyImage({ src, alt, className }: { src: string; alt: string; className?: string; }) { + const ref = useRef(null); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + if (ref.current) { + let toggle = false; + const observer = new IntersectionObserver((entries) => { + if (entries.some(x => x.isIntersecting)) { + setLoaded(true); + toggle = !toggle; + ref.current?.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500, easing: "ease-in-out" }); + observer.disconnect(); + } + else { + if (toggle) { + console.log("fade out"); + ref.current?.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 500, easing: "ease-in-out" }); + } + } + }); + observer.observe(ref.current); + return () => { + observer.disconnect(); + }; + } + }, []); + + return {alt}; +} + export function GalleryCard({ doc: x }: { doc: Document; }) { @@ -42,11 +79,12 @@ export function GalleryCard({ }; }, []); - return + return
- {x.title} + className="max-h-full max-w-full object-cover object-center" + />
@@ -56,10 +94,10 @@ export function GalleryCard({ -
  • +
      {clippedTags.map(tag => )} - {clippedTags.length < originalTags.length && } - + {clippedTags.length < originalTags.length && } +
  • ; diff --git a/packages/client/src/components/gallery/TagBadge.tsx b/packages/client/src/components/gallery/TagBadge.tsx index 36f9a81..19c0c7d 100644 --- a/packages/client/src/components/gallery/TagBadge.tsx +++ b/packages/client/src/components/gallery/TagBadge.tsx @@ -1,4 +1,5 @@ -import { Badge, badgeVariants } from "@/components/ui/badge"; +import { badgeVariants } from "@/components/ui/badge"; +import { Link } from "wouter"; import { cn } from "@/lib/utils"; const femaleTagPrefix = "female:"; @@ -26,7 +27,7 @@ function toPrettyTagname(tagname: string): string { } } -export default function TagBadge(props: { tagname: string, className?: string}) { +export default function TagBadge(props: { tagname: string, className?: string; disabled?: boolean;}) { const { tagname } = props; const kind = getTagKind(tagname); return
  • {toPrettyTagname(tagname)}
  • ; + }>{toPrettyTagname(tagname)}; } \ No newline at end of file diff --git a/packages/client/src/components/layout/nav.tsx b/packages/client/src/components/layout/nav.tsx index 4ddb9be..3c86c34 100644 --- a/packages/client/src/components/layout/nav.tsx +++ b/packages/client/src/components/layout/nav.tsx @@ -2,6 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip" import { Link } from "wouter" import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons" import { buttonVariants } from "../ui/button" +import { useLogin } from "@/state/user"; interface NavItemProps { icon: React.ReactNode; @@ -29,6 +30,8 @@ export function NavItem({ } export function NavList() { + const loginInfo = useLogin(); + return diff --git a/packages/client/src/components/ui/label.tsx b/packages/client/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/packages/client/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/packages/client/src/page/galleryPage.tsx b/packages/client/src/page/galleryPage.tsx index 1c44988..4142100 100644 --- a/packages/client/src/page/galleryPage.tsx +++ b/packages/client/src/page/galleryPage.tsx @@ -54,7 +54,7 @@ export default function Gallery() { return
    Error: {String(error)}
    } - return (
    + return (
    @@ -66,12 +66,12 @@ export default function Gallery() {
    } { - data?.length === 0 &&
    No results
    + data?.length === 0 &&
    No results
    } { data?.map((x) => { return ( - + ); }) } diff --git a/packages/client/src/page/loginPage.tsx b/packages/client/src/page/loginPage.tsx new file mode 100644 index 0000000..36f0c00 --- /dev/null +++ b/packages/client/src/page/loginPage.tsx @@ -0,0 +1,58 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { doLogin } from "@/state/user"; +import { useState } from "react"; +import { useLocation } from "wouter"; + +export function LoginForm() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [, setLocation] = useLocation(); + + return ( + + + Login + + Enter your email below to login to your account. + + + +
    + + setUsername(e.target.value)}/> +
    +
    + + setPassword(e.target.value)}/> +
    +
    + + + +
    + ) +} + +export function LoginPage() { + return ( +
    + +
    + ) +} + +export default LoginPage; \ No newline at end of file diff --git a/packages/client/src/page/mod.ts b/packages/client/src/page/mod.ts deleted file mode 100644 index 47167f2..0000000 --- a/packages/client/src/page/mod.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./404"; -export * from "./contentinfo"; -export * from "./difference"; -export * from "./gallery"; -export * from "./login"; -export * from "./profile"; -export * from "./setting"; -export * from "./tags"; diff --git a/packages/client/src/page/profilesPage.tsx b/packages/client/src/page/profilesPage.tsx new file mode 100644 index 0000000..d19f7b4 --- /dev/null +++ b/packages/client/src/page/profilesPage.tsx @@ -0,0 +1,32 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useLogin } from "@/state/user"; +import { Redirect } from "wouter"; + +export function ProfilePage() { + const userInfo = useLogin(); + if (!userInfo) { + console.error("User session expired. Redirecting to login page."); + return ; + } + return ( +
    + + + Profile + + +
    + Username + {userInfo.username} +
    +
    + Permission + {userInfo.permission.length > 1 ? userInfo.permission.join(",") : "N/A"} +
    +
    +
    +
    + ) +} + +export default ProfilePage; \ No newline at end of file diff --git a/packages/client/src/state.tsx b/packages/client/src/state.tsx deleted file mode 100644 index da6c0fb..0000000 --- a/packages/client/src/state.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { createContext, useRef, useState } from "react"; - -export const UserContext = createContext({ - username: "", - permission: [] as string[], - setUsername: (s: string) => {}, - setPermission: (permission: string[]) => {}, -}); - -type LoginLocalStorage = { - username: string; - permission: string[]; - accessExpired: number; -}; - -let localObj: LoginLocalStorage | null = null; - -export const getInitialValue = async () => { - if (localObj === null) { - const storagestr = window.localStorage.getItem("UserLoginContext") as string | null; - const storage = storagestr !== null ? (JSON.parse(storagestr) as LoginLocalStorage | null) : null; - localObj = storage; - } - if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) { - return { - username: localObj.username, - permission: localObj.permission, - }; - } - const res = await fetch("/user/refresh", { - method: "POST", - }); - if (res.status !== 200) throw new Error("Maybe Network Error"); - const r = (await res.json()) as LoginLocalStorage & { refresh: boolean }; - if (r.refresh) { - localObj = { - username: r.username, - permission: r.permission, - accessExpired: r.accessExpired, - }; - } else { - localObj = { - accessExpired: 0, - username: "", - permission: r.permission, - }; - } - window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); - return { - username: r.username, - permission: r.permission, - }; -}; -export const doLogout = async () => { - const req = await fetch("/user/logout", { - method: "POST", - }); - try { - const res = await req.json(); - localObj = { - accessExpired: 0, - username: "", - permission: res["permission"], - }; - window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); - return { - username: localObj.username, - permission: localObj.permission, - }; - } catch (error) { - console.error(`Server Error ${error}`); - return { - username: "", - permission: [], - }; - } -}; -export const doLogin = async (userLoginInfo: { - username: string; - password: string; -}): Promise => { - const res = await fetch("/user/login", { - method: "POST", - body: JSON.stringify(userLoginInfo), - headers: { "content-type": "application/json" }, - }); - const b = await res.json(); - if (res.status !== 200) { - return b.detail as string; - } - localObj = b; - window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); - return b; -}; diff --git a/packages/client/src/state/user.ts b/packages/client/src/state/user.ts index 0b82a5f..5c8c82a 100644 --- a/packages/client/src/state/user.ts +++ b/packages/client/src/state/user.ts @@ -47,7 +47,7 @@ async function refresh() { } export const doLogout = async () => { - const req = await fetch("/user/logout", { + const req = await fetch("/api/user/logout", { method: "POST", }); const setVal = setAtomValue(userLoginStateAtom); @@ -76,7 +76,7 @@ export const doLogin = async (userLoginInfo: { username: string; password: string; }): Promise => { - const res = await fetch("/user/login", { + const res = await fetch("/api/user/login", { method: "POST", body: JSON.stringify(userLoginInfo), headers: { "content-type": "application/json" }, diff --git a/packages/server/src/route/comic.ts b/packages/server/src/route/comic.ts index bac0012..2dd34ea 100644 --- a/packages/server/src/route/comic.ts +++ b/packages/server/src/route/comic.ts @@ -10,46 +10,67 @@ import { Readable, Writable } from "node:stream"; /** * zip stream cache. */ -const ZipStreamCache: { - [path: string]: [{ - reader: ZipReader, - handle: FileHandle - }, number] -} = {}; +const ZipStreamCache = new Map, + handle: FileHandle, + refCount: number, +}>(); -async function acquireZip(path: string) { - if (!(path in ZipStreamCache)) { + +function markUseZip(path: string) { + const ret = ZipStreamCache.get(path); + if (ret) { + ret.refCount++; + } + return ret !== undefined; +} + +async function acquireZip(path: string, marked = false) { + const ret = ZipStreamCache.get(path); + if (!ret) { const obj = await readZip(path); - ZipStreamCache[path] = [obj, 1]; - // console.log(`acquire ${path} 1`); + const check = ZipStreamCache.get(path); + if (check) { + check.refCount++; + // if the cache is updated, release the previous one. + releaseZip(path); + return check.reader; + } + // if the cache is not updated, set the new one. + ZipStreamCache.set(path, { + reader: obj.reader, + handle: obj.handle, + refCount: 1, + }); return obj.reader; } - const [ret, refCount] = ZipStreamCache[path]; - ZipStreamCache[path] = [ret, refCount + 1]; - // console.log(`acquire ${path} ${refCount + 1}`); + if (!marked) { + ret.refCount++; + } return ret.reader; } function releaseZip(path: string) { - const obj = ZipStreamCache[path]; - if (obj === undefined) throw new Error("error! key invalid"); - const [ref, refCount] = obj; - // console.log(`release ${path} : ${refCount}`); - if (refCount === 1) { - const { reader, handle } = ref; + const obj = ZipStreamCache.get(path); + if (obj === undefined) { + console.warn(`warning! duplicate release at ${path}`); + return; + } + if (obj.refCount === 1) { + const { reader, handle } = obj; reader.close().then(() => { handle.close(); }); - delete ZipStreamCache[path]; + ZipStreamCache.delete(path); } else { - ZipStreamCache[path] = [ref, refCount - 1]; + obj.refCount--; } } async function renderZipImage(ctx: Context, path: string, page: number) { const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"]; - // console.log(`opened ${page}`); - const zip = await acquireZip(path); + const marked = markUseZip(path); + const zip = await acquireZip(path, marked); const entries = (await entriesByNaturalOrder(zip)).filter((x) => { const ext = x.filename.split(".").pop(); return ext !== undefined && image_ext.includes(ext); @@ -70,9 +91,9 @@ async function renderZipImage(ctx: Context, path: string, page: number) { }, close() { nodeReadableStream.push(null); - setTimeout(() => { - releaseZip(path); - }, 100); + releaseZip(path); + // setTimeout(() => { + // }, 500); }, })); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 2b11d9e..bf5bd08 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -92,8 +92,8 @@ class ServerApplication { this.serve_static_file(router); const login_router = createLoginRouter(this.userController); - router.use("/user", login_router.routes()); - router.use("/user", login_router.allowedMethods()); + router.use("/api/user", login_router.routes()); + router.use("/api/user", login_router.allowedMethods()); if (setting.mode === "development") { let mm_count = 0; diff --git a/packages/server/src/util/zipwrap.ts b/packages/server/src/util/zipwrap.ts index ac6c5a4..8699d84 100644 --- a/packages/server/src/util/zipwrap.ts +++ b/packages/server/src/util/zipwrap.ts @@ -34,10 +34,10 @@ export async function readZip(path: string): Promise<{ reader: ZipReader handle: FileHandle }> { - const fd = await open(path); + const fd = await open(path, "r"); const reader = new ZipReader(new FileReader(fd), { useCompressionStream: true, - preventClose: false, + preventClose: true, }); return { reader, handle: fd }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f695d5..6c57ff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.71)(react@18.2.0) @@ -30,7 +33,7 @@ importers: specifier: ^2.1.0 version: 2.1.0 dbtype: - specifier: link:..\dbtype + specifier: workspace:* version: link:../dbtype react: specifier: ^18.2.0 @@ -1060,6 +1063,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.71 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: