feat(navbar): in progress

This commit is contained in:
Junior Garcia 2023-04-18 00:40:04 -03:00
parent 7d3289eb52
commit 5be0df4ab3
22 changed files with 868 additions and 5 deletions

View File

@ -147,7 +147,7 @@ const InsideScrollTemplate: ComponentStory<typeof Modal> = (args: ModalProps) =>
<>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
<Lorem size={5} />
<Lorem count={5} />
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>Close</Button>
@ -168,7 +168,7 @@ const OutsideScrollTemplate: ComponentStory<typeof Modal> = (args: ModalProps) =
<>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
<Lorem size={5} />
<Lorem count={5} />
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>Close</Button>
@ -193,7 +193,7 @@ const OpenChangeTemplate: ComponentStory<typeof Modal> = (args: ModalProps) => {
<>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
<Lorem size={5} />
<Lorem count={5} />
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>Close</Button>

View File

@ -0,0 +1,24 @@
# @nextui-org/navbar
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/navbar
# or
npm i @nextui-org/navbar
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,19 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Navbar} from "../src";
describe("Navbar", () => {
it("should render correctly", () => {
const wrapper = render(<Navbar />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Navbar ref={ref} />);
expect(ref.current).not.toBeNull();
});
});

View File

@ -0,0 +1,66 @@
{
"name": "@nextui-org/navbar",
"version": "2.0.0-beta.1",
"description": "A responsive navigation header positioned on top side of your page that includes support for branding, links, navigation, collapse and more.",
"keywords": [
"navbar"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/navbar"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18"
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/use-scroll-position": "workspace:*",
"@react-aria/utils": "^3.16.0"
},
"devDependencies": {
"@nextui-org/link": "workspace:*",
"@nextui-org/avatar": "workspace:*",
"@nextui-org/dropdown": "workspace:*",
"@nextui-org/button": "workspace:*",
"@nextui-org/input": "workspace:*",
"react-lorem-component": "0.13.0",
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,17 @@
// export types
export type {NavbarProps} from "./navbar";
export type {NavbarBrandProps} from "./navbar-brand";
export type {NavbarContentProps} from "./navbar-content";
export type {NavbarItemProps} from "./navbar-item";
// export hooks
export {useNavbar} from "./use-navbar";
// export context
export * from "./navbar-context";
// export components
export {default as Navbar} from "./navbar";
export {default as NavbarBrand} from "./navbar-brand";
export {default as NavbarContent} from "./navbar-content";
export {default as NavbarItem} from "./navbar-item";

View File

@ -0,0 +1,30 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useNavbarContext} from "./navbar-context";
export interface NavbarBrandProps extends HTMLNextUIProps<"div"> {
children?: React.ReactNode | React.ReactNode[];
}
const NavbarBrand = forwardRef<NavbarBrandProps, "div">((props, ref) => {
const {as, className, children, ...otherProps} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
const {slots, classNames} = useNavbarContext();
const styles = clsx(classNames?.brand, className);
return (
<Component ref={domRef} className={slots.brand?.({class: styles})} {...otherProps}>
{children}
</Component>
);
});
NavbarBrand.displayName = "NextUI.NavbarBrand";
export default NavbarBrand;

View File

@ -0,0 +1,33 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {useNavbarContext} from "./navbar-context";
export interface NavbarContentProps extends HTMLNextUIProps<"ul"> {
/**
* The content of the Navbar.Content. It is usually the `NavbarItem`,
*/
children?: React.ReactNode | React.ReactNode[];
}
const NavbarContent = forwardRef<NavbarContentProps, "ul">((props, ref) => {
const {as, className, children, ...otherProps} = props;
const Component = as || "ul";
const domRef = useDOMRef(ref);
const {slots, classNames} = useNavbarContext();
const styles = clsx(classNames?.content, className);
return (
<Component ref={domRef} className={slots.content?.({class: styles})} {...otherProps}>
{children}
</Component>
);
});
NavbarContent.displayName = "NextUI.NavbarContent";
export default NavbarContent;

View File

@ -0,0 +1,10 @@
import {createContext} from "@nextui-org/shared-utils";
import {ContextType} from "./use-navbar";
export const [NavbarProvider, useNavbarContext] = createContext<ContextType>({
name: "NavbarContext",
strict: true,
errorMessage:
"useNavbarContext: `context` is undefined. Seems you forgot to wrap component within <Navbar />",
});

View File

@ -0,0 +1,44 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useId} from "react";
import {useNavbarContext} from "./navbar-context";
export interface NavbarItemProps extends HTMLNextUIProps<"li"> {
children?: React.ReactNode;
/**
* Whether the item is active or not.
* @default false
*/
isActive?: boolean;
}
const NavbarItem = forwardRef<NavbarItemProps, "li">((props, ref) => {
const {as, className, children, isActive, ...otherProps} = props;
const Component = as || "li";
const domRef = useDOMRef(ref);
const itemId = useId();
const {slots, classNames} = useNavbarContext();
const styles = clsx(classNames?.item, className);
return (
<Component
ref={domRef}
className={slots.item?.({class: styles})}
data-active={dataAttr(isActive)}
id={itemId}
{...otherProps}
>
{children}
</Component>
);
});
NavbarItem.displayName = "NextUI.NavbarItem";
export default NavbarItem;

View File

@ -0,0 +1,26 @@
import {forwardRef} from "@nextui-org/system";
import {UseNavbarProps, useNavbar} from "./use-navbar";
import {NavbarProvider} from "./navbar-context";
export interface NavbarProps extends Omit<UseNavbarProps, "ref"> {
children?: React.ReactNode | React.ReactNode[];
}
const Navbar = forwardRef<NavbarProps, "div">((props, ref) => {
const {children, ...otherProps} = props;
const {Component, getBaseProps, getWrapperProps, context} = useNavbar({ref, ...otherProps});
return (
<NavbarProvider value={context}>
<Component {...getBaseProps()}>
<header {...getWrapperProps()}>{children}</header>
</Component>
</NavbarProvider>
);
});
Navbar.displayName = "NextUI.Navbar";
export default Navbar;

View File

@ -0,0 +1,124 @@
import type {NavbarVariantProps, SlotsToClasses, NavbarSlots} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {navbar} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, ReactRef} from "@nextui-org/shared-utils";
import {useMemo, useState} from "react";
import {mergeProps} from "@react-aria/utils";
import {useScrollPosition} from "@nextui-org/use-scroll-position";
export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProps> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
/**
* The parent element where the navbar is placed within.
* This is used to determine the scroll position and whether the navbar should be hidden or not.
* @default `window`
*/
parentRef?: React.RefObject<HTMLElement>;
/**
* Whether the navbar should hide on scroll or not.
* @default false
*/
shouldHideOnScroll?: boolean;
/**
* Whether the navbar parent scroll event should be listened to or not.
* @default false
*/
disableScrollHandler?: boolean;
/**
* The scroll event handler for the navbar. The event fires when the navbar parent element is scrolled.
* it only works if `disableScrollHandler` is set to `false` or `shouldHideOnScroll` is set to `true`.
*/
onScrollPositionChange?: (scrollPosition: number) => void;
/**
* Classname or List of classes to change the classNames of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Navbar classNames={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* brand: "brand-classes",
* }} />
* ```
*/
classNames?: SlotsToClasses<NavbarSlots>;
}
export type ContextType = {
slots: ReturnType<typeof navbar>;
classNames?: SlotsToClasses<NavbarSlots>;
};
export function useNavbar(originalProps: UseNavbarProps) {
const [props, variantProps] = mapPropsVariants(originalProps, navbar.variantKeys);
const {
ref,
as,
parentRef,
shouldHideOnScroll = false,
disableScrollHandler = false,
onScrollPositionChange,
className,
classNames,
...otherProps
} = props;
const Component = as || "nav";
const domRef = useDOMRef(ref);
const [isSticky, setIsSticky] = useState(false);
const slots = useMemo(
() =>
navbar({
...variantProps,
position: isSticky ? "sticky" : originalProps?.position,
}),
[...Object.values(variantProps), isSticky],
);
const context: ContextType = {
slots,
classNames,
};
const baseStyles = clsx(classNames?.base, className);
useScrollPosition({
elementRef: parentRef,
isEnabled: shouldHideOnScroll || !disableScrollHandler,
callback: ({prevPos, currPos}) => {
onScrollPositionChange?.(currPos.y);
if (shouldHideOnScroll) {
setIsSticky((prev) => {
const next = currPos.y > prevPos.y;
return next !== prev ? next : prev;
});
}
},
});
const getBaseProps: PropGetter = (props = {}) => ({
...mergeProps(otherProps, props),
ref: domRef,
className: slots.base({class: clsx(baseStyles, props?.className)}),
});
const getWrapperProps: PropGetter = (props = {}) => ({
...props,
className: slots.wrapper({class: clsx(classNames?.wrapper, props?.className)}),
});
return {Component, slots, domRef, context, getBaseProps, getWrapperProps};
}
export type UseNavbarReturn = ReturnType<typeof useNavbar>;

View File

@ -0,0 +1,116 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {navbar} from "@nextui-org/theme";
import {Link} from "@nextui-org/link";
import {Button} from "@nextui-org/button";
import Lorem from "react-lorem-component";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarProps} from "../src";
export default {
title: "Components/Navbar",
component: Navbar,
argTypes: {
position: {
control: {
type: "select",
options: ["static", "sticky", "floating"],
},
},
maxWidth: {
control: {
type: "select",
options: ["sm", "md", "lg", "xl", "2xl", "full"],
},
},
isBlurred: {
control: {
type: "boolean",
},
},
},
decorators: [
(Story) => (
<div className="flex justify-center w-screen h-screen">
<Story />
</div>
),
],
} as ComponentMeta<typeof Navbar>;
const defaultProps = {
...navbar.defaultVariants,
};
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
const App = React.forwardRef(({children}: any, ref: any) => {
return (
<div
ref={ref}
className="max-w-[920px] max-h-[600px] overflow-x-hidden overflow-y-scroll shadow-md relative border border-neutral"
>
{children}
<div className="flex flex-col gap-4 px-10 mt-8">
<h1>Lorem ipsum dolor sit ame</h1>
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
<Lorem key={i} className="mb-5 text-lg" count={1} sentenceUpperBound={40} />
))}
</div>
</div>
);
});
App.displayName = "App";
const Template: ComponentStory<typeof Navbar> = (args: NavbarProps) => (
<App>
<Navbar {...args}>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold hidden sm:block text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex">
<NavbarItem as={Link} color="foreground" href="#">
Features
</NavbarItem>
<NavbarItem isActive as={Link} href="#">
Customers
</NavbarItem>
<NavbarItem as={Link} color="foreground" href="#">
Integrations
</NavbarItem>
<NavbarItem as={Link} color="foreground" href="#">
Pricing
</NavbarItem>
<NavbarItem as={Link} color="foreground" href="#">
Company
</NavbarItem>
</NavbarContent>
<NavbarContent>
<NavbarItem as={Link} href="#">
Login
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
</App>
);
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};

View File

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -245,7 +245,7 @@ const RadioCard = (props: RadioProps) => {
<Component
{...getBaseProps()}
className={clsx(
"inline-flex items-center justify-between hover:bg-content2 flex-row-reverse max-w-[300px] cursor-pointer border-2 border-neutral rounded-lg gap-4 p-4",
"group inline-flex items-center justify-between hover:bg-content2 flex-row-reverse max-w-[300px] cursor-pointer border-2 border-neutral rounded-lg gap-4 p-4",
{
"border-primary": isSelected,
},

View File

@ -29,3 +29,4 @@ export * from "./dropdown-section";
export * from "./dropdown-menu";
export * from "./image";
export * from "./modal";
export * from "./navbar";

View File

@ -0,0 +1,130 @@
import type {VariantProps} from "tailwind-variants";
import {tv} from "tailwind-variants";
/**
* Card **Tailwind Variants** component
*
* @example
* ```js
* const {base, wrapper, brand, content, item} = navbar({...})
*
* <nav className={base()}>
* <div className={wrapper()}>
* <div className={brand()}>Brand</div>
* <ul className={content()}>
* <li className={item()}>Item 1</li>
* <li className={item()}>Item 2</li>
* <li className={item()}>Item 3</li>
* </ul>
* </div>
* </nav>
* ```
*/
const navbar = tv({
slots: {
base: [
"relative",
"z-20",
"w-full",
"h-auto",
"flex",
"items-center",
"justify-center",
"border-b",
"border-neutral-100",
"shadow-lg",
],
wrapper: [
"flex",
"flex-row",
"flex-nowrap",
"items-center",
"justify-between",
"w-full",
"h-16",
"px-6",
],
brand: [
"flex",
"flex-row",
"flex-nowrap",
"justify-start",
"bg-transparent",
"items-center",
"no-underline",
"text-base",
"whitespace-nowrap",
"box-border",
],
content: ["flex", "h-full", "flex-row", "flex-nowrap", "items-center", "gap-4"],
item: [
"text-base",
"whitespace-nowrap",
"box-border",
// active
"data-[active=true]:font-semibold",
],
},
variants: {
position: {
static: {
base: "static",
},
sticky: {},
floating: {
base: "shadow-none border-b-0",
wrapper: "mt-4 mx-8 shadow-lg border border-neutral-100 rounded-xl",
},
},
maxWidth: {
sm: {
wrapper: "max-w-[640px]",
},
md: {
wrapper: "max-w-[768px]",
},
lg: {
wrapper: "max-w-[1024px]",
},
xl: {
wrapper: "max-w-[1280px]",
},
"2xl": {
wrapper: "max-w-[1536px]",
},
full: {
wrapper: "max-w-full",
},
},
isBordered: {
true: {},
},
isBlurred: {
false: {
base: "bg-background",
},
true: {
base: "backdrop-blur-xl backdrop-saturate-200 bg-background/50",
},
},
},
defaultVariants: {
maxWidth: "lg",
position: "sticky",
isBlurred: true,
},
compoundVariants: [
{
position: ["sticky", "floating"],
class: {
base: "sticky top-0 inset-x-0",
},
},
],
});
export type NavbarVariantProps = VariantProps<typeof navbar>;
export type NavbarSlots = keyof ReturnType<typeof navbar>;
export {navbar};

View File

@ -0,0 +1,24 @@
# @nextui-org/use-scroll-position
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/use-scroll-position
# or
npm i @nextui-org/use-scroll-position
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,52 @@
{
"name": "@nextui-org/use-scroll-position",
"version": "2.0.0-beta.1",
"description": "Provides the logic to control the scroll over an element",
"keywords": [
"use-scroll-position"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/hooks/use-scroll-position"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,78 @@
import {useRef, useEffect} from "react";
const isBrowser = typeof window !== "undefined";
export type ScrollValue = {x: any; y: any};
function getScrollPosition(element: HTMLElement | undefined | null): ScrollValue {
if (!isBrowser) return {x: 0, y: 0};
if (!element) {
return {x: window.scrollX, y: window.scrollY};
}
return {x: element.scrollLeft, y: element.scrollTop};
}
export interface UseScrollPositionOptions {
/**
* The wait time in milliseconds before triggering the callback.
* @default 30
*/
delay?: number;
/**
* Whether the scroll position should be tracked or not.
* @default true
*/
isEnabled?: boolean;
/**
* The element to track the scroll position for.
*/
elementRef?: React.RefObject<HTMLElement> | null;
/**
* The callback function to be called when the scroll position changes.
*/
callback?: ({prevPos, currPos}: {prevPos: ScrollValue; currPos: ScrollValue}) => void;
}
export const useScrollPosition = (props: UseScrollPositionOptions): ScrollValue => {
const {elementRef, delay = 30, callback, isEnabled} = props;
const position = useRef<ScrollValue>(
isEnabled ? getScrollPosition(elementRef?.current) : {x: 0, y: 0},
);
let throttleTimeout: ReturnType<typeof setTimeout> | null = null;
const handler = () => {
const currPos = getScrollPosition(elementRef?.current);
if (typeof callback === "function") {
callback({prevPos: position.current, currPos});
}
position.current = currPos;
throttleTimeout = null;
};
useEffect(() => {
if (!isEnabled) return;
const handleScroll = () => {
if (delay) {
if (throttleTimeout === null) {
throttleTimeout = setTimeout(handler, delay);
}
} else {
handler();
}
};
const target = elementRef?.current || window;
target.addEventListener("scroll", handleScroll);
return () => target.removeEventListener("scroll", handleScroll);
}, [elementRef?.current, delay, isEnabled]);
return position.current;
};

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"include": ["src", "index.ts"]
}

View File

@ -136,5 +136,5 @@ import {link, button} from "@nextui-org/theme";
<br />
<div class="block text-xs text-neutral-400">
Last updated on <time datetime="2023-03-07">April 11, 2023</time>
Last updated on <time datetime="2023-03-07">April 16, 2023</time>
</div>

55
pnpm-lock.yaml generated
View File

@ -1077,6 +1077,52 @@ importers:
specifier: 0.13.0
version: 0.13.0(react@18.2.0)
packages/components/navbar:
dependencies:
'@nextui-org/dom-utils':
specifier: workspace:*
version: link:../../utilities/dom-utils
'@nextui-org/shared-utils':
specifier: workspace:*
version: link:../../utilities/shared-utils
'@nextui-org/system':
specifier: workspace:*
version: link:../../core/system
'@nextui-org/theme':
specifier: workspace:*
version: link:../../core/theme
'@nextui-org/use-scroll-position':
specifier: workspace:*
version: link:../../hooks/use-scroll-position
'@react-aria/utils':
specifier: ^3.16.0
version: 3.16.0(react@18.2.0)
devDependencies:
'@nextui-org/avatar':
specifier: workspace:*
version: link:../avatar
'@nextui-org/button':
specifier: workspace:*
version: link:../button
'@nextui-org/dropdown':
specifier: workspace:*
version: link:../dropdown
'@nextui-org/input':
specifier: workspace:*
version: link:../input
'@nextui-org/link':
specifier: workspace:*
version: link:../link
clean-package:
specifier: 2.2.0
version: 2.2.0
react:
specifier: ^18.2.0
version: 18.2.0
react-lorem-component:
specifier: 0.13.0
version: 0.13.0(react@18.2.0)
packages/components/pagination:
dependencies:
'@nextui-org/dom-utils':
@ -1816,6 +1862,15 @@ importers:
specifier: ^18.2.0
version: 18.2.0
packages/hooks/use-scroll-position:
devDependencies:
clean-package:
specifier: 2.2.0
version: 2.2.0
react:
specifier: ^18.2.0
version: 18.2.0
packages/hooks/use-ssr:
devDependencies:
clean-package: