A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Navbar Animation

+1685 -1038
+7
.cursor/rules/js.mdc
··· 1 + --- 2 + description: Typescript authoring rules 3 + globs: ["**/*.js", "**/*.ts", "**/*.js"] 4 + alwaysApply: true 5 + --- 6 + 7 + - Avoid nesting functions if you dont need to
+8
.cursor/rules/tsx.mdc
··· 1 + --- 2 + description: Typescript authoring rules 3 + globs: ["**/*.tsx"] 4 + alwaysApply: true 5 + --- 6 + 7 + - When there are linter errors first run eslint --fix 8 + - Make sure to never cause reflow if you can. Check this resource to determine what does cause reflow https://gist.github.com/paulirish/5d52fb081b3570c81e3a
+2
apps/docs/package.json
··· 38 38 "lightningcss": "^1.30.2", 39 39 "lucide-react": "^0.554.0", 40 40 "magic-string": "^0.30.21", 41 + "match-container": "^0.1.0", 42 + "raf-throttle": "^2.0.6", 41 43 "react": "catalog:", 42 44 "react-aria": "^3.44.0", 43 45 "react-aria-components": "^1.13.0",
+13 -13
apps/docs/src/components/link/index.tsx
··· 10 10 import { StyleXComponentProps } from "../theme/types"; 11 11 import { fontFamily, fontWeight } from "../theme/typography.stylex"; 12 12 import { LinkContext } from "./link-context"; 13 - import { pointInPolygon } from "tldraw"; 14 13 15 14 const styles = stylex.create({ 16 15 link: { 17 - textDecoration: "none", 18 - position: "relative", 19 - display: "inline-flex", 20 - alignItems: "center", 21 - gap: spacing["2"], 22 16 "--underline-opacity": { 23 17 default: 0, 18 + ":is([aria-expanded=true])": 1, 24 19 ":is([data-breadcrumb] *)": 0, 25 20 ":is([data-hovered])": 1, 26 - ":is([aria-expanded=true])": 1, 27 21 }, 22 + gap: spacing["2"], 23 + textDecoration: "none", 24 + alignItems: "center", 28 25 color: { 29 26 default: primaryColor.text2, 30 27 ":is([data-breadcrumb] *)": uiColor.text1, 31 28 ":is([data-breadcrumb][data-current] *)": uiColor.text2, 32 29 }, 33 30 cursor: "pointer", 31 + display: "inline-flex", 34 32 fontFamily: fontFamily["sans"], 35 33 fontWeight: fontWeight["normal"], 34 + position: "relative", 36 35 36 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 37 37 ":is(*) svg": { 38 - width: "1.2em", 39 38 height: "1.2em", 39 + width: "1.2em", 40 40 }, 41 41 42 42 "::after": { 43 - opacity: "var(--underline-opacity)", 43 + backgroundColor: "currentColor", 44 44 content: '""', 45 45 display: "block", 46 - width: "100%", 47 - height: "2px", 48 - backgroundColor: "currentColor", 46 + opacity: "var(--underline-opacity)", 47 + pointerEvents: "none", 49 48 position: "absolute", 50 49 bottom: `calc(${spacing["1"]} * -1)`, 50 + height: "2px", 51 51 left: 0, 52 52 right: 0, 53 - pointerEvents: "none", 53 + width: "100%", 54 54 }, 55 55 }, 56 56 });
+319
apps/docs/src/components/navbar/Navbar.tsx
··· 1 + "use client"; 2 + import * as stylex from "@stylexjs/stylex"; 3 + import { Menu, X } from "lucide-react"; 4 + import * as React from "react"; 5 + import { use, useState } from "react"; 6 + import { Link, LinkProps } from "react-aria-components"; 7 + 8 + import { SizeContext } from "../context"; 9 + import { IconButton } from "../icon-button"; 10 + import { Separator } from "../separator"; 11 + import { primaryColor, uiColor } from "../theme/color.stylex"; 12 + import { containerBreakpoints } from "../theme/media-queries.stylex"; 13 + import { ui } from "../theme/semantic-color.stylex"; 14 + import { spacing } from "../theme/spacing.stylex"; 15 + import { Size, StyleXComponentProps } from "../theme/types"; 16 + import { fontFamily, fontWeight } from "../theme/typography.stylex"; 17 + 18 + const styles = stylex.create({ 19 + navbar: { 20 + "--separator-visibility": { 21 + default: "none", 22 + ":is([data-navbar-open])": "flex", 23 + ":has([data-always-visible])": "none", 24 + [containerBreakpoints.sm]: "none", 25 + }, 26 + "--visibility": { 27 + ":is([data-navbar-open])": "flex", 28 + [containerBreakpoints.sm]: "none", 29 + }, 30 + gridTemplateAreas: { 31 + default: ` 32 + "logo hamburger" 33 + `, 34 + ":is([data-navbar-open])": ` 35 + "logo hamburger" 36 + "navigation navigation" 37 + "separator separator" 38 + "action action" 39 + `, 40 + ":has([data-always-visible])": ` 41 + "logo action hamburger" 42 + `, 43 + ":has([data-always-visible]):is([data-navbar-open])": ` 44 + "logo action hamburger" 45 + "navigation navigation navigation" 46 + "separator separator separator" 47 + `, 48 + [containerBreakpoints.sm]: ` 49 + "logo navigation action" 50 + `, 51 + }, 52 + overflow: { 53 + ":is([data-navbar-open])": "auto", 54 + }, 55 + alignItems: "center", 56 + boxSizing: "border-box", 57 + columnGap: { 58 + default: spacing["4"], 59 + [containerBreakpoints.sm]: spacing["8"], 60 + }, 61 + display: "grid", 62 + gridTemplateColumns: { 63 + default: "1fr auto", 64 + ":has([data-always-visible])": "1fr min-content min-content", 65 + [containerBreakpoints.sm]: "auto 1fr auto", 66 + }, 67 + gridTemplateRows: { 68 + ":is([data-navbar-open])": `min-content min-content min-content min-content`, 69 + }, 70 + rowGap: spacing["8"], 71 + zIndex: 1000, 72 + borderBottomColor: uiColor.border1, 73 + borderBottomStyle: "solid", 74 + borderBottomWidth: 1, 75 + height: { 76 + default: spacing["14"], 77 + ":is([data-navbar-open])": "100%", 78 + [containerBreakpoints.sm]: spacing["14"], 79 + }, 80 + paddingBottom: spacing["3"], 81 + paddingLeft: spacing["4"], 82 + paddingRight: spacing["4"], 83 + paddingTop: spacing["3"], 84 + top: 0, 85 + width: "100%", 86 + }, 87 + logo: { 88 + alignItems: "center", 89 + display: "flex", 90 + }, 91 + separator: { 92 + gridArea: "separator", 93 + // eslint-disable-next-line @stylexjs/valid-styles 94 + display: "var(--separator-visibility, none)", 95 + }, 96 + navigation: { 97 + gridArea: "navigation", 98 + flex: "1", 99 + gap: { 100 + default: spacing["6"], 101 + [containerBreakpoints.sm]: spacing["8"], 102 + }, 103 + alignItems: { 104 + default: "start", 105 + [containerBreakpoints.sm]: "stretch", 106 + }, 107 + display: { 108 + // eslint-disable-next-line @stylexjs/valid-styles 109 + default: "var(--visibility, none)", 110 + [containerBreakpoints.sm]: "flex", 111 + }, 112 + flexDirection: { 113 + default: "column", 114 + [containerBreakpoints.sm]: "row", 115 + }, 116 + }, 117 + navigationJustifyLeft: { 118 + justifyContent: "flex-start", 119 + }, 120 + navigationJustifyCenter: { 121 + justifyContent: "center", 122 + }, 123 + navigationJustifyRight: { 124 + justifyContent: "flex-end", 125 + }, 126 + action: { 127 + gridArea: "action", 128 + gap: spacing["2"], 129 + alignItems: "center", 130 + display: { 131 + // eslint-disable-next-line @stylexjs/valid-styles 132 + default: "var(--visibility, none)", 133 + [containerBreakpoints.sm]: "flex", 134 + ":is([data-always-visible])": "flex", 135 + }, 136 + }, 137 + hamburgerButton: { 138 + gridArea: "hamburger", 139 + alignItems: "center", 140 + display: { 141 + default: "flex", 142 + [containerBreakpoints.sm]: "none", 143 + }, 144 + }, 145 + link: { 146 + "--underline-opacity": { 147 + default: 0, 148 + ":is([aria-expanded=true])": 1, 149 + ":is([data-active])": 1, 150 + ":is([data-breadcrumb] *)": 0, 151 + ":is([data-hovered])": 1, 152 + }, 153 + gap: spacing["2"], 154 + textDecoration: "none", 155 + alignItems: "center", 156 + color: { 157 + default: primaryColor.text2, 158 + ":is([data-breadcrumb] *)": uiColor.text1, 159 + ":is([data-breadcrumb][data-current] *)": uiColor.text2, 160 + }, 161 + cursor: "pointer", 162 + display: "inline-flex", 163 + fontFamily: fontFamily["sans"], 164 + fontWeight: fontWeight["normal"], 165 + position: "relative", 166 + 167 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 168 + ":is(*) svg": { 169 + height: "1.2em", 170 + width: "1.2em", 171 + }, 172 + 173 + "::after": { 174 + backgroundColor: "currentColor", 175 + content: '""', 176 + display: "block", 177 + opacity: "var(--underline-opacity)", 178 + pointerEvents: "none", 179 + position: "absolute", 180 + bottom: `calc(${spacing["1"]} * -1)`, 181 + height: "2px", 182 + left: 0, 183 + right: 0, 184 + width: "100%", 185 + }, 186 + }, 187 + }); 188 + 189 + // Define subcomponents first so they can be referenced in Navbar 190 + export interface NavbarLogoProps 191 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 192 + 193 + /** 194 + * NavbarLogo component for displaying the logo in the navbar. 195 + */ 196 + export const NavbarLogo = ({ style, ...props }: NavbarLogoProps) => { 197 + return ( 198 + <div {...props} {...stylex.props(styles.logo, style)}> 199 + {props.children} 200 + </div> 201 + ); 202 + }; 203 + 204 + export interface NavbarNavigationProps 205 + extends StyleXComponentProps<React.ComponentProps<"div">> { 206 + /** 207 + * Justify content alignment for the navigation items. 208 + * @default "left" 209 + */ 210 + justify?: "left" | "right" | "center"; 211 + } 212 + 213 + /** 214 + * NavbarNavigation component for displaying navigation items. 215 + * On mobile, this is hidden and shown in the hamburger menu. 216 + */ 217 + export const NavbarNavigation = ({ 218 + style, 219 + justify = "left", 220 + ...props 221 + }: NavbarNavigationProps) => { 222 + return ( 223 + <div 224 + {...props} 225 + {...stylex.props( 226 + styles.navigation, 227 + justify === "left" && styles.navigationJustifyLeft, 228 + justify === "center" && styles.navigationJustifyCenter, 229 + justify === "right" && styles.navigationJustifyRight, 230 + style, 231 + )} 232 + > 233 + {props.children} 234 + </div> 235 + ); 236 + }; 237 + 238 + export interface NavbarActionProps 239 + extends StyleXComponentProps<React.ComponentProps<"div">> { 240 + /** 241 + * Whether the action should be always visible on mobile. 242 + * @default false 243 + */ 244 + alwaysVisible?: boolean; 245 + } 246 + 247 + /** 248 + * NavbarAction component for displaying action buttons. 249 + * On mobile, this is hidden and shown in the hamburger menu. 250 + */ 251 + export const NavbarAction = ({ 252 + style, 253 + alwaysVisible = false, 254 + ...props 255 + }: NavbarActionProps) => { 256 + return ( 257 + <div 258 + {...props} 259 + data-always-visible={alwaysVisible || undefined} 260 + {...stylex.props(styles.action, style)} 261 + > 262 + {props.children} 263 + </div> 264 + ); 265 + }; 266 + 267 + export interface NavbarLinkProps extends StyleXComponentProps<LinkProps> { 268 + isActive?: boolean; 269 + } 270 + 271 + export function NavbarLink({ style, isActive, ...props }: NavbarLinkProps) { 272 + return ( 273 + <Link 274 + data-active={isActive} 275 + {...stylex.props(styles.link, style)} 276 + {...props} 277 + /> 278 + ); 279 + } 280 + 281 + export interface NavbarProps 282 + extends StyleXComponentProps<React.ComponentProps<"nav">> { 283 + size?: Size; 284 + } 285 + 286 + /** 287 + * Navbar component that provides a responsive navigation bar with logo, navigation, and action sections. 288 + * On mobile, navigation and actions are automatically contained in a hamburger menu overlay. 289 + */ 290 + export const Navbar = ({ 291 + style, 292 + size: sizeProp, 293 + children, 294 + ...props 295 + }: NavbarProps) => { 296 + const size = sizeProp || use(SizeContext); 297 + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 298 + 299 + return ( 300 + <SizeContext value={size}> 301 + <nav 302 + {...props} 303 + data-navbar-open={isMobileMenuOpen || undefined} 304 + {...stylex.props(styles.navbar, ui.bg, style)} 305 + > 306 + {children} 307 + <Separator style={styles.separator as unknown as stylex.StyleXStyles} /> 308 + <IconButton 309 + aria-label="Open menu" 310 + variant="tertiary" 311 + style={styles.hamburgerButton} 312 + onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)} 313 + > 314 + {isMobileMenuOpen ? <X /> : <Menu />} 315 + </IconButton> 316 + </nav> 317 + </SizeContext> 318 + ); 319 + };
+239
apps/docs/src/components/navbar/NavbarMenu.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import * as React from "react"; 5 + import { mergeProps, useHover, usePress } from "react-aria"; 6 + import { Button, Disclosure, DisclosurePanel } from "react-aria-components"; 7 + 8 + import { HoverCard, HoverCardProps } from "../hover-card"; 9 + import { animationDuration } from "../theme/animations.stylex"; 10 + import { primaryColor, uiColor } from "../theme/color.stylex"; 11 + import { 12 + containerBreakpoints, 13 + mediaQueries, 14 + } from "../theme/media-queries.stylex"; 15 + import { radius } from "../theme/radius.stylex"; 16 + import { spacing } from "../theme/spacing.stylex"; 17 + import { StyleXComponentProps } from "../theme/types"; 18 + import { fontFamily, fontSize, fontWeight } from "../theme/typography.stylex"; 19 + 20 + const styles = stylex.create({ 21 + menuItem: { 22 + padding: spacing["2"], 23 + borderRadius: { 24 + default: radius["sm"], 25 + [mediaQueries.supportsSquircle]: radius["lg"], 26 + }, 27 + textDecoration: "none", 28 + alignItems: "center", 29 + backgroundColor: { 30 + ":is([data-hovered=true]):not([data-pressed=true])": uiColor.component2, 31 + ":is([data-pressed=true])": uiColor.component3, 32 + }, 33 + columnGap: spacing["3"], 34 + display: "grid", 35 + rowGap: spacing["1.5"], 36 + transitionDuration: animationDuration.fast, 37 + transitionProperty: "background-color", 38 + transitionTimingFunction: "ease-in-out", 39 + userSelect: "none", 40 + 41 + gridTemplateAreas: { 42 + default: '"title"', 43 + ":has([data-description])": ` 44 + "title" 45 + "description" 46 + `, 47 + ":has([data-icon])": ` 48 + "icon title" 49 + `, 50 + ":has([data-icon]):has([data-description])": ` 51 + "icon title" 52 + "icon description" 53 + `, 54 + }, 55 + gridTemplateColumns: { 56 + ":has([data-icon])": "min-content 1fr", 57 + ":has([data-icon]):has([data-description])": "min-content 1fr", 58 + }, 59 + }, 60 + menuItemIcon: { 61 + gridArea: "icon", 62 + padding: spacing["2"], 63 + borderRadius: { 64 + default: radius["sm"], 65 + [mediaQueries.supportsSquircle]: radius["lg"], 66 + }, 67 + alignItems: "center", 68 + backgroundColor: { 69 + default: uiColor.component2, 70 + [stylex.when.ancestor(":hover")]: uiColor.component1, 71 + }, 72 + color: uiColor.text1, 73 + display: "flex", 74 + justifyContent: "center", 75 + height: spacing["8"], 76 + width: spacing["8"], 77 + 78 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 79 + ":is(*) svg": { 80 + height: spacing["6"], 81 + width: spacing["6"], 82 + }, 83 + }, 84 + menuItemLabel: { 85 + gridArea: "title", 86 + color: uiColor.text2, 87 + fontWeight: fontWeight["medium"], 88 + }, 89 + menuItemDescription: { 90 + gridArea: "description", 91 + color: uiColor.text1, 92 + fontSize: fontSize["sm"], 93 + }, 94 + menuItemDisabled: { 95 + opacity: 0.5, 96 + }, 97 + link: { 98 + "--underline-opacity": { 99 + default: 0, 100 + ":is([aria-expanded=true])": 1, 101 + ":is([data-active])": 1, 102 + ":is([data-breadcrumb] *)": 0, 103 + ":is([data-hovered])": 1, 104 + }, 105 + gap: spacing["2"], 106 + textDecoration: "none", 107 + alignItems: "center", 108 + color: { 109 + default: primaryColor.text2, 110 + ":is([data-breadcrumb] *)": uiColor.text1, 111 + ":is([data-breadcrumb][data-current] *)": uiColor.text2, 112 + }, 113 + cursor: "pointer", 114 + display: "inline-flex", 115 + fontFamily: fontFamily["sans"], 116 + fontWeight: fontWeight["normal"], 117 + position: "relative", 118 + 119 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 120 + ":is(*) svg": { 121 + height: "1.2em", 122 + width: "1.2em", 123 + }, 124 + 125 + "::after": { 126 + backgroundColor: "currentColor", 127 + content: '""', 128 + display: "block", 129 + opacity: "var(--underline-opacity)", 130 + pointerEvents: "none", 131 + position: "absolute", 132 + bottom: `calc(${spacing["1"]} * -1)`, 133 + height: "2px", 134 + left: 0, 135 + right: 0, 136 + width: "100%", 137 + }, 138 + }, 139 + desktopMenu: { 140 + display: { 141 + default: "none", 142 + [containerBreakpoints.sm]: "block", 143 + }, 144 + }, 145 + mobileMenu: { 146 + display: { 147 + default: "block", 148 + [containerBreakpoints.sm]: "none", 149 + }, 150 + }, 151 + menuTriggerButton: { 152 + display: "contents", 153 + fontSize: "inherit", 154 + }, 155 + menuDisclosurePanel: { 156 + marginLeft: `calc(${spacing["2"]} * -1)`, 157 + paddingTop: spacing["2"], 158 + }, 159 + }); 160 + 161 + interface NavbarMenuProps extends HoverCardProps {} 162 + 163 + export function NavbarMenu({ trigger, children, ...props }: NavbarMenuProps) { 164 + return ( 165 + <> 166 + <div {...stylex.props(styles.desktopMenu)}> 167 + <HoverCard {...props} offset={24} trigger={trigger}> 168 + {children} 169 + </HoverCard> 170 + </div> 171 + <Disclosure {...stylex.props(styles.mobileMenu)}> 172 + <Button slot="trigger" {...stylex.props(styles.menuTriggerButton)}> 173 + {trigger} 174 + </Button> 175 + <DisclosurePanel> 176 + <div {...stylex.props(styles.menuDisclosurePanel)}>{children}</div> 177 + </DisclosurePanel> 178 + </Disclosure> 179 + </> 180 + ); 181 + } 182 + 183 + export interface NavbarMenuTriggerProps 184 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 185 + 186 + export function NavbarMenuTrigger({ style, ...props }: NavbarMenuTriggerProps) { 187 + return <div {...stylex.props(styles.link, style)} {...props} />; 188 + } 189 + 190 + interface NavbarMenuItemProps 191 + extends StyleXComponentProps<Omit<React.ComponentProps<"div">, "children">> { 192 + icon?: React.ReactNode; 193 + label: string; 194 + description?: string; 195 + isDisabled?: boolean; 196 + } 197 + 198 + export function NavbarMenuItem({ 199 + style, 200 + icon, 201 + label, 202 + description, 203 + isDisabled, 204 + ...props 205 + }: NavbarMenuItemProps) { 206 + const { hoverProps, isHovered } = useHover({ isDisabled }); 207 + const { pressProps, isPressed } = usePress({ isDisabled }); 208 + const Component = "href" in props ? "a" : "button"; 209 + 210 + return ( 211 + <Component 212 + {...mergeProps( 213 + props as React.ComponentProps<typeof Component>, 214 + hoverProps, 215 + pressProps, 216 + )} 217 + data-hovered={isHovered} 218 + data-pressed={isPressed} 219 + {...stylex.props( 220 + stylex.defaultMarker(), 221 + styles.menuItem, 222 + isDisabled && styles.menuItemDisabled, 223 + style, 224 + )} 225 + > 226 + {Boolean(icon) && ( 227 + <div data-icon {...stylex.props(styles.menuItemIcon)}> 228 + {icon} 229 + </div> 230 + )} 231 + {label && <div {...stylex.props(styles.menuItemLabel)}>{label}</div>} 232 + {description && ( 233 + <div data-description {...stylex.props(styles.menuItemDescription)}> 234 + {description} 235 + </div> 236 + )} 237 + </Component> 238 + ); 239 + }
+4 -503
apps/docs/src/components/navbar/index.tsx
··· 1 - "use client"; 2 - 3 - import * as stylex from "@stylexjs/stylex"; 4 - import { Menu, X } from "lucide-react"; 5 - import * as React from "react"; 6 - import { use, useState } from "react"; 7 - 8 - import { SizeContext } from "../context"; 9 - import { IconButton } from "../icon-button"; 10 - import { Separator } from "../separator"; 11 - import { primaryColor, uiColor } from "../theme/color.stylex"; 12 - import { 13 - containerBreakpoints, 14 - mediaQueries, 15 - } from "../theme/media-queries.stylex"; 16 - import { ui } from "../theme/semantic-color.stylex"; 17 - import { spacing } from "../theme/spacing.stylex"; 18 - import { Size, StyleXComponentProps } from "../theme/types"; 19 - import { HoverCard, HoverCardProps } from "../hover-card"; 20 - import { radius } from "../theme/radius.stylex"; 21 - import { mergeProps, useHover, usePress } from "react-aria"; 22 - import { animationDuration } from "../theme/animations.stylex"; 23 - import { fontFamily, fontSize, fontWeight } from "../theme/typography.stylex"; 24 - import { 25 - Button, 26 - Disclosure, 27 - DisclosurePanel, 28 - Link, 29 - LinkProps, 30 - } from "react-aria-components"; 31 - 32 - const styles = stylex.create({ 33 - navbar: { 34 - "--separator-visibility": { 35 - default: "none", 36 - ":is([data-navbar-open])": "flex", 37 - ":has([data-always-visible])": "none", 38 - [containerBreakpoints.sm]: "none", 39 - }, 40 - "--visibility": { 41 - ":is([data-navbar-open])": "flex", 42 - [containerBreakpoints.sm]: "none", 43 - }, 44 - gridTemplateAreas: { 45 - default: ` 46 - "logo hamburger" 47 - `, 48 - ":is([data-navbar-open])": ` 49 - "logo hamburger" 50 - "navigation navigation" 51 - "separator separator" 52 - "action action" 53 - `, 54 - ":has([data-always-visible])": ` 55 - "logo action hamburger" 56 - `, 57 - ":has([data-always-visible]):is([data-navbar-open])": ` 58 - "logo action hamburger" 59 - "navigation navigation navigation" 60 - "separator separator separator" 61 - `, 62 - [containerBreakpoints.sm]: ` 63 - "logo navigation action" 64 - `, 65 - }, 66 - alignItems: "center", 67 - boxSizing: "border-box", 68 - columnGap: { 69 - default: spacing["4"], 70 - [containerBreakpoints.sm]: spacing["8"], 71 - }, 72 - display: "grid", 73 - gridTemplateColumns: { 74 - default: "1fr auto", 75 - ":has([data-always-visible])": "1fr min-content min-content", 76 - [containerBreakpoints.sm]: "auto 1fr auto", 77 - }, 78 - gridTemplateRows: { 79 - ":is([data-navbar-open])": `min-content min-content min-content min-content`, 80 - }, 81 - rowGap: spacing["8"], 82 - borderBottomColor: uiColor.border1, 83 - borderBottomStyle: "solid", 84 - borderBottomWidth: 1, 85 - height: { 86 - default: spacing["14"], 87 - ":is([data-navbar-open])": "100%", 88 - [containerBreakpoints.sm]: spacing["14"], 89 - }, 90 - overflow: { 91 - ":is([data-navbar-open])": "auto", 92 - }, 93 - paddingBottom: spacing["3"], 94 - paddingLeft: spacing["4"], 95 - paddingRight: spacing["4"], 96 - paddingTop: spacing["3"], 97 - width: "100%", 98 - }, 99 - logo: { 100 - alignItems: "center", 101 - display: "flex", 102 - }, 103 - separator: { 104 - gridArea: "separator", 105 - // eslint-disable-next-line @stylexjs/valid-styles 106 - display: "var(--separator-visibility, none)", 107 - }, 108 - navigation: { 109 - gridArea: "navigation", 110 - flex: "1", 111 - gap: { 112 - default: spacing["6"], 113 - [containerBreakpoints.sm]: spacing["8"], 114 - }, 115 - display: { 116 - // eslint-disable-next-line @stylexjs/valid-styles 117 - default: "var(--visibility, none)", 118 - [containerBreakpoints.sm]: "flex", 119 - }, 120 - flexDirection: { 121 - default: "column", 122 - [containerBreakpoints.sm]: "row", 123 - }, 124 - alignItems: { 125 - default: "start", 126 - [containerBreakpoints.sm]: "stretch", 127 - }, 128 - }, 129 - navigationJustifyLeft: { 130 - justifyContent: "flex-start", 131 - }, 132 - navigationJustifyCenter: { 133 - justifyContent: "center", 134 - }, 135 - navigationJustifyRight: { 136 - justifyContent: "flex-end", 137 - }, 138 - action: { 139 - gridArea: "action", 140 - gap: spacing["2"], 141 - alignItems: "center", 142 - display: { 143 - // eslint-disable-next-line @stylexjs/valid-styles 144 - default: "var(--visibility, none)", 145 - [containerBreakpoints.sm]: "flex", 146 - ":is([data-always-visible])": "flex", 147 - }, 148 - }, 149 - hamburgerButton: { 150 - gridArea: "hamburger", 151 - alignItems: "center", 152 - display: { 153 - default: "flex", 154 - [containerBreakpoints.sm]: "none", 155 - }, 156 - }, 157 - menuItem: { 158 - textDecoration: "none", 159 - display: "grid", 160 - alignItems: "center", 161 - columnGap: spacing["3"], 162 - rowGap: spacing["1.5"], 163 - padding: spacing["2"], 164 - borderRadius: { 165 - default: radius["sm"], 166 - [mediaQueries.supportsSquircle]: radius["lg"], 167 - }, 168 - backgroundColor: { 169 - ":is([data-hovered=true]):not([data-pressed=true])": uiColor.component2, 170 - ":is([data-pressed=true])": uiColor.component3, 171 - }, 172 - transitionDuration: animationDuration.fast, 173 - transitionProperty: "background-color", 174 - transitionTimingFunction: "ease-in-out", 175 - userSelect: "none", 1 + /* eslint-disable react-refresh/only-export-components */ 176 2 177 - gridTemplateColumns: { 178 - ":has([data-icon])": "min-content 1fr", 179 - ":has([data-icon]):has([data-description])": "min-content 1fr", 180 - }, 181 - gridTemplateAreas: { 182 - default: '"title"', 183 - ":has([data-description])": ` 184 - "title" 185 - "description" 186 - `, 187 - ":has([data-icon])": ` 188 - "icon title" 189 - `, 190 - ":has([data-icon]):has([data-description])": ` 191 - "icon title" 192 - "icon description" 193 - `, 194 - }, 195 - }, 196 - menuItemIcon: { 197 - gridArea: "icon", 198 - color: uiColor.text1, 199 - display: "flex", 200 - alignItems: "center", 201 - justifyContent: "center", 202 - backgroundColor: { 203 - default: uiColor.component2, 204 - [stylex.when.ancestor(":hover")]: uiColor.component1, 205 - }, 206 - padding: spacing["2"], 207 - height: spacing["8"], 208 - width: spacing["8"], 209 - borderRadius: { 210 - default: radius["sm"], 211 - [mediaQueries.supportsSquircle]: radius["lg"], 212 - }, 3 + export * from "./Navbar"; 4 + export * from "./NavbarMenu"; 213 5 214 - ":is(*) svg": { 215 - width: spacing["6"], 216 - height: spacing["6"], 217 - }, 218 - }, 219 - menuItemLabel: { 220 - gridArea: "title", 221 - fontWeight: fontWeight["medium"], 222 - color: uiColor.text2, 223 - }, 224 - menuItemDescription: { 225 - gridArea: "description", 226 - fontSize: fontSize["sm"], 227 - color: uiColor.text1, 228 - }, 229 - menuItemDisabled: { 230 - opacity: 0.5, 231 - }, 232 - link: { 233 - textDecoration: "none", 234 - position: "relative", 235 - display: "inline-flex", 236 - alignItems: "center", 237 - gap: spacing["2"], 238 - "--underline-opacity": { 239 - default: 0, 240 - ":is([data-breadcrumb] *)": 0, 241 - ":is([data-hovered])": 1, 242 - ":is([aria-expanded=true])": 1, 243 - ":is([data-active])": 1, 244 - }, 245 - color: { 246 - default: primaryColor.text2, 247 - ":is([data-breadcrumb] *)": uiColor.text1, 248 - ":is([data-breadcrumb][data-current] *)": uiColor.text2, 249 - }, 250 - cursor: "pointer", 251 - fontFamily: fontFamily["sans"], 252 - fontWeight: fontWeight["normal"], 253 - 254 - ":is(*) svg": { 255 - width: "1.2em", 256 - height: "1.2em", 257 - }, 258 - 259 - "::after": { 260 - opacity: "var(--underline-opacity)", 261 - content: '""', 262 - display: "block", 263 - width: "100%", 264 - height: "2px", 265 - backgroundColor: "currentColor", 266 - position: "absolute", 267 - bottom: `calc(${spacing["1"]} * -1)`, 268 - left: 0, 269 - right: 0, 270 - pointerEvents: "none", 271 - }, 272 - }, 273 - desktopMenu: { 274 - display: { 275 - default: "none", 276 - [containerBreakpoints.sm]: "block", 277 - }, 278 - }, 279 - mobileMenu: { 280 - display: { 281 - default: "block", 282 - [containerBreakpoints.sm]: "none", 283 - }, 284 - }, 285 - menuTriggerButton: { 286 - display: "contents", 287 - fontSize: "inherit", 288 - }, 289 - menuDisclosurePanel: { 290 - paddingTop: spacing["2"], 291 - marginLeft: `calc(${spacing["2"]} * -1)`, 292 - }, 293 - }); 294 - 295 - // Define subcomponents first so they can be referenced in Navbar 296 - export interface NavbarLogoProps 297 - extends StyleXComponentProps<React.ComponentProps<"div">> {} 298 - 299 - /** 300 - * NavbarLogo component for displaying the logo in the navbar. 301 - */ 302 - export const NavbarLogo = ({ style, ...props }: NavbarLogoProps) => { 303 - return ( 304 - <div {...props} {...stylex.props(styles.logo, style)}> 305 - {props.children} 306 - </div> 307 - ); 308 - }; 309 - 310 - export interface NavbarNavigationProps 311 - extends StyleXComponentProps<React.ComponentProps<"div">> { 312 - /** 313 - * Justify content alignment for the navigation items. 314 - * @default "left" 315 - */ 316 - justify?: "left" | "right" | "center"; 317 - } 318 - 319 - /** 320 - * NavbarNavigation component for displaying navigation items. 321 - * On mobile, this is hidden and shown in the hamburger menu. 322 - */ 323 - export const NavbarNavigation = ({ 324 - style, 325 - justify = "left", 326 - ...props 327 - }: NavbarNavigationProps) => { 328 - return ( 329 - <div 330 - {...props} 331 - {...stylex.props( 332 - styles.navigation, 333 - justify === "left" && styles.navigationJustifyLeft, 334 - justify === "center" && styles.navigationJustifyCenter, 335 - justify === "right" && styles.navigationJustifyRight, 336 - style, 337 - )} 338 - > 339 - {props.children} 340 - </div> 341 - ); 342 - }; 343 - 344 - export interface NavbarActionProps 345 - extends StyleXComponentProps<React.ComponentProps<"div">> { 346 - /** 347 - * Whether the action should be always visible on mobile. 348 - * @default false 349 - */ 350 - alwaysVisible?: boolean; 351 - } 352 - 353 - /** 354 - * NavbarAction component for displaying action buttons. 355 - * On mobile, this is hidden and shown in the hamburger menu. 356 - */ 357 - export const NavbarAction = ({ 358 - style, 359 - alwaysVisible = false, 360 - ...props 361 - }: NavbarActionProps) => { 362 - return ( 363 - <div 364 - {...props} 365 - data-always-visible={alwaysVisible || undefined} 366 - {...stylex.props(styles.action, style)} 367 - > 368 - {props.children} 369 - </div> 370 - ); 371 - }; 372 - 373 - interface NavbarMenuProps extends HoverCardProps {} 374 - 375 - export function NavbarMenu({ trigger, children, ...props }: NavbarMenuProps) { 376 - return ( 377 - <> 378 - <div {...stylex.props(styles.desktopMenu)}> 379 - <HoverCard {...props} offset={24} trigger={trigger}> 380 - {children} 381 - </HoverCard> 382 - </div> 383 - <Disclosure {...stylex.props(styles.mobileMenu)}> 384 - <Button slot="trigger" {...stylex.props(styles.menuTriggerButton)}> 385 - {trigger} 386 - </Button> 387 - <DisclosurePanel> 388 - <div {...stylex.props(styles.menuDisclosurePanel)}>{children}</div> 389 - </DisclosurePanel> 390 - </Disclosure> 391 - </> 392 - ); 393 - } 394 - 395 - export interface NavbarLinkProps extends StyleXComponentProps<LinkProps> { 396 - isActive?: boolean; 397 - } 398 - 399 - export function NavbarLink({ style, isActive, ...props }: NavbarLinkProps) { 400 - return ( 401 - <Link 402 - data-active={isActive} 403 - {...stylex.props(styles.link, style)} 404 - {...props} 405 - /> 406 - ); 407 - } 408 - 409 - export interface NavbarMenuTriggerProps 410 - extends StyleXComponentProps<React.ComponentProps<"div">> {} 411 - 412 - export function NavbarMenuTrigger({ style, ...props }: NavbarMenuTriggerProps) { 413 - return <div {...stylex.props(styles.link, style)} {...props} />; 414 - } 415 - 416 - interface NavbarMenuItemProps 417 - extends StyleXComponentProps<Omit<React.ComponentProps<"div">, "children">> { 418 - icon?: React.ReactNode; 419 - label: string; 420 - description?: string; 421 - isDisabled?: boolean; 422 - } 423 - 424 - export function NavbarMenuItem({ 425 - style, 426 - icon, 427 - label, 428 - description, 429 - isDisabled, 430 - ...props 431 - }: NavbarMenuItemProps) { 432 - const { hoverProps, isHovered } = useHover({ isDisabled }); 433 - const { pressProps, isPressed } = usePress({ isDisabled }); 434 - const Component = "href" in props ? "a" : "button"; 435 - 436 - return ( 437 - <Component 438 - {...mergeProps( 439 - props as React.ComponentProps<typeof Component>, 440 - hoverProps, 441 - pressProps, 442 - )} 443 - data-hovered={isHovered} 444 - data-pressed={isPressed} 445 - {...stylex.props( 446 - stylex.defaultMarker(), 447 - styles.menuItem, 448 - isDisabled && styles.menuItemDisabled, 449 - style, 450 - )} 451 - > 452 - {icon && ( 453 - <div data-icon {...stylex.props(styles.menuItemIcon)}> 454 - {icon} 455 - </div> 456 - )} 457 - {label && <div {...stylex.props(styles.menuItemLabel)}>{label}</div>} 458 - {description && ( 459 - <div data-description {...stylex.props(styles.menuItemDescription)}> 460 - {description} 461 - </div> 462 - )} 463 - </Component> 464 - ); 465 - } 466 - 467 - export interface NavbarProps 468 - extends StyleXComponentProps<React.ComponentProps<"nav">> { 469 - size?: Size; 470 - } 471 - 472 - /** 473 - * Navbar component that provides a responsive navigation bar with logo, navigation, and action sections. 474 - * On mobile, navigation and actions are automatically contained in a hamburger menu overlay. 475 - */ 476 - export const Navbar = ({ 477 - style, 478 - size: sizeProp, 479 - children, 480 - ...props 481 - }: NavbarProps) => { 482 - const size = sizeProp || use(SizeContext); 483 - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 484 - 485 - return ( 486 - <SizeContext value={size}> 487 - <nav 488 - {...props} 489 - data-navbar-open={isMobileMenuOpen || undefined} 490 - {...stylex.props(styles.navbar, ui.bg, style)} 491 - > 492 - {children} 493 - <Separator style={styles.separator as unknown as stylex.StyleXStyles} /> 494 - <IconButton 495 - aria-label="Open menu" 496 - variant="tertiary" 497 - style={styles.hamburgerButton} 498 - onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)} 499 - > 500 - {isMobileMenuOpen ? <X /> : <Menu />} 501 - </IconButton> 502 - </nav> 503 - </SizeContext> 504 - ); 505 - }; 6 + /* eslint-enable react-refresh/only-export-components */
+156
apps/docs/src/components/navbar/useAnimatedNavbar.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import rafThrottle from "raf-throttle"; 5 + import { useEffect, useRef, useState } from "react"; 6 + 7 + import { 8 + animationDuration, 9 + animationTimingFunction, 10 + } from "../theme/animations.stylex"; 11 + 12 + const SCROLL_THRESHOLD = 16; 13 + 14 + const styles = stylex.create({ 15 + navbarOutOfViewport: { 16 + position: "sticky", 17 + transform: "translateY(-100%)", 18 + top: 0, 19 + }, 20 + navbarRevealed: { 21 + position: "sticky", 22 + transform: "translateY(0)", 23 + transitionDuration: animationDuration.slow, 24 + transitionProperty: "transform", 25 + transitionTimingFunction: animationTimingFunction.easeInOut, 26 + top: 0, 27 + }, 28 + navbarAnimatedOut: { 29 + transitionDuration: animationDuration.slow, 30 + transitionProperty: "transform", 31 + transitionTimingFunction: animationTimingFunction.easeInOut, 32 + }, 33 + }); 34 + 35 + /** 36 + * A hook that animates the navbar into view and stick to the top when the user scrolls down. 37 + */ 38 + export const useAnimatedNavbar = ({ 39 + scrollContainer: scrollContainerProp, 40 + }: { 41 + scrollContainer?: React.RefObject<HTMLElement | null>; 42 + }) => { 43 + const lastScrollY = useRef(0); 44 + const [hasScrollNavbarOutOfView, setHasScrollNavbarOutOfView] = 45 + useState(false); 46 + const [shouldAnimateOut, setShouldAnimateOut] = useState(false); 47 + const [shouldAnimateIn, setShouldAnimateIn] = useState(false); 48 + const navRef = useRef<HTMLElement>(null); 49 + const topSentinelRef = useRef<HTMLDivElement>(null); 50 + 51 + // Use intersection observer to detect when navbar is out of viewport 52 + useEffect(() => { 53 + if (!navRef.current) return; 54 + 55 + const observer = new IntersectionObserver(([entry]) => { 56 + if (!entry || entry.isIntersecting) return; 57 + setHasScrollNavbarOutOfView(true); 58 + }); 59 + 60 + observer.observe(navRef.current); 61 + 62 + return () => { 63 + observer.disconnect(); 64 + }; 65 + }, []); 66 + 67 + // Animate the navbar into view and stick to the top 68 + useEffect(() => { 69 + if (!hasScrollNavbarOutOfView || !scrollContainerProp?.current) return; 70 + 71 + const handleScroll = rafThrottle((e: Event) => { 72 + if (!(e.target instanceof HTMLElement)) return; 73 + 74 + const currentScrollY = e.target.scrollTop; 75 + const scrollDirection = 76 + currentScrollY > lastScrollY.current ? "down" : "up"; 77 + 78 + if (scrollDirection === "up") { 79 + // Only hide/show if scrolled past threshold 80 + if (Math.abs(currentScrollY - lastScrollY.current) < SCROLL_THRESHOLD) { 81 + return; 82 + } 83 + 84 + // Show navbar when scrolling up or at the top 85 + if ( 86 + currentScrollY < lastScrollY.current || 87 + currentScrollY <= SCROLL_THRESHOLD 88 + ) { 89 + setShouldAnimateIn(true); 90 + } 91 + } 92 + // Animate navbar out when scrolling down past threshold 93 + else if ( 94 + currentScrollY > lastScrollY.current && 95 + currentScrollY > SCROLL_THRESHOLD 96 + ) { 97 + setShouldAnimateIn(false); 98 + setShouldAnimateOut(true); 99 + } 100 + 101 + lastScrollY.current = currentScrollY; 102 + }); 103 + 104 + const scrollContainer = scrollContainerProp.current; 105 + 106 + scrollContainer.addEventListener("scroll", handleScroll, { 107 + passive: true, 108 + }); 109 + 110 + return () => { 111 + scrollContainer.removeEventListener("scroll", handleScroll); 112 + }; 113 + }, [hasScrollNavbarOutOfView, scrollContainerProp]); 114 + 115 + // Use IntersectionObserver to detect if scrolled to top (most performant) 116 + useEffect(() => { 117 + if (!topSentinelRef.current || !scrollContainerProp?.current) return; 118 + 119 + const observer = new IntersectionObserver( 120 + ([entry]) => { 121 + if (!entry) return; 122 + 123 + const atTop = entry.isIntersecting; 124 + 125 + setHasScrollNavbarOutOfView((has) => { 126 + if (!has) return has; 127 + if (atTop) { 128 + setShouldAnimateIn(false); 129 + setShouldAnimateOut(false); 130 + return false; 131 + } 132 + return true; 133 + }); 134 + }, 135 + { root: scrollContainerProp.current }, 136 + ); 137 + 138 + observer.observe(topSentinelRef.current); 139 + 140 + return () => { 141 + observer.disconnect(); 142 + }; 143 + }, [scrollContainerProp]); 144 + 145 + return { 146 + sentinel: <div ref={topSentinelRef} />, 147 + navBarProps: { 148 + ref: navRef, 149 + style: [ 150 + hasScrollNavbarOutOfView && styles.navbarOutOfViewport, 151 + shouldAnimateIn && styles.navbarRevealed, 152 + shouldAnimateOut && styles.navbarAnimatedOut, 153 + ], 154 + }, 155 + }; 156 + };
+7
apps/docs/src/docs/components/navigation/navbar.mdx
··· 11 11 import { AlwaysVisibleActions } from '../../../examples/navbar/always-visible-actions' 12 12 import { WithMenus } from '../../../examples/navbar/with-menus' 13 13 import { ActiveLink } from '../../../examples/navbar/active-link' 14 + import { Sticky } from '../../../examples/navbar/sticky' 14 15 15 16 <Example src={Basic} noPadding /> 16 17 ··· 29 30 <PropDocs components={["Navbar", "NavbarLogo", "NavbarNavigation", "NavbarAction"]} /> 30 31 31 32 ## Features 33 + 34 + ### Sticky Behavior 35 + 36 + The `Navbar` component can be made sticky by using the `useAnimatedNavbar` hook. 37 + 38 + <Example src={Sticky} /> 32 39 33 40 ### Active Link 34 41
+51 -1
apps/docs/src/examples/navbar/basic.tsx
··· 20 20 borderWidth: 1, 21 21 borderColor: uiColor.border1, 22 22 borderRadius: radius["lg"], 23 - overflow: "hidden", 23 + overflow: "auto", 24 24 margin: spacing["4"], 25 + }, 26 + content: { 27 + padding: spacing["4"], 25 28 }, 26 29 }); 27 30 ··· 55 58 <Button variant="primary">Sign In</Button> 56 59 </NavbarAction> 57 60 </Navbar> 61 + <div {...stylex.props(styles.content)}> 62 + <h2>Lorem Ipsum</h2> 63 + <p> 64 + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do 65 + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 66 + minim veniam, quis nostrud exercitation ullamco laboris nisi ut 67 + aliquip ex ea commodo consequat. 68 + </p> 69 + <p> 70 + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 71 + dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 72 + proident, sunt in culpa qui officia deserunt mollit anim id est 73 + laborum. 74 + </p> 75 + <p> 76 + Sed ut perspiciatis unde omnis iste natus error sit voluptatem 77 + accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae 78 + ab illo inventore veritatis et quasi architecto beatae vitae dicta 79 + sunt explicabo. 80 + </p> 81 + <p> 82 + Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut 83 + fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem 84 + sequi nesciunt. 85 + </p> 86 + <p> 87 + Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, 88 + consectetur, adipisci velit, sed quia non numquam eius modi tempora 89 + incidunt ut labore et dolore magnam aliquam quaerat voluptatem. 90 + </p> 91 + <p> 92 + Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis 93 + suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis 94 + autem vel eum iure reprehenderit qui in ea voluptate velit esse quam 95 + nihil molestiae consequatur. 96 + </p> 97 + <p> 98 + At vero eos et accusamus et iusto odio dignissimos ducimus qui 99 + blanditiis praesentium voluptatum deleniti atque corrupti quos dolores 100 + et quas molestias excepturi sint occaecati cupiditate non provident. 101 + </p> 102 + <p> 103 + Similique sunt in culpa qui officia deserunt mollitia animi, id est 104 + laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita 105 + distinctio. 106 + </p> 107 + </div> 58 108 </div> 59 109 ); 60 110 }
+118
apps/docs/src/examples/navbar/sticky.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Navbar, 4 + NavbarAction, 5 + NavbarLink, 6 + NavbarLogo, 7 + NavbarNavigation, 8 + } from "@/components/navbar"; 9 + import { uiColor } from "../../components/theme/color.stylex"; 10 + import * as stylex from "@stylexjs/stylex"; 11 + import { radius } from "../../components/theme/radius.stylex"; 12 + import { spacing } from "../../components/theme/spacing.stylex"; 13 + import { useRef } from "react"; 14 + import { useAnimatedNavbar } from "@/components/navbar/useAnimatedNavbar"; 15 + 16 + const styles = stylex.create({ 17 + wrapper: { 18 + containerType: "inline-size", 19 + height: "400px", 20 + width: "90%", 21 + borderStyle: "solid", 22 + borderWidth: 1, 23 + borderColor: uiColor.border1, 24 + borderRadius: radius["lg"], 25 + overflow: "auto", 26 + margin: spacing["4"], 27 + }, 28 + content: { 29 + padding: spacing["4"], 30 + }, 31 + }); 32 + 33 + function Logo() { 34 + return ( 35 + <svg 36 + width="32" 37 + height="32" 38 + viewBox="0 0 120 120" 39 + fill="currentColor" 40 + xmlns="http://www.w3.org/2000/svg" 41 + > 42 + <circle cx="60" cy="60" r="50" stroke="currentColor" strokeWidth="2" /> 43 + </svg> 44 + ); 45 + } 46 + 47 + export function Sticky() { 48 + const scrollContainerRef = useRef<HTMLDivElement>(null); 49 + const { sentinel, navBarProps } = useAnimatedNavbar({ 50 + scrollContainer: scrollContainerRef, 51 + }); 52 + 53 + return ( 54 + <div ref={scrollContainerRef} {...stylex.props(styles.wrapper)}> 55 + {sentinel} 56 + <Navbar {...navBarProps}> 57 + <NavbarLogo> 58 + <Logo /> 59 + </NavbarLogo> 60 + <NavbarNavigation justify="right"> 61 + <NavbarLink href="/dashboard">Dashboard</NavbarLink> 62 + <NavbarLink href="/projects">Projects</NavbarLink> 63 + <NavbarLink href="/settings">Settings</NavbarLink> 64 + </NavbarNavigation> 65 + <NavbarAction> 66 + <Button variant="primary">Sign In</Button> 67 + </NavbarAction> 68 + </Navbar> 69 + <div {...stylex.props(styles.content)}> 70 + <h2>Lorem Ipsum</h2> 71 + <p> 72 + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do 73 + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 74 + minim veniam, quis nostrud exercitation ullamco laboris nisi ut 75 + aliquip ex ea commodo consequat. 76 + </p> 77 + <p> 78 + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 79 + dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 80 + proident, sunt in culpa qui officia deserunt mollit anim id est 81 + laborum. 82 + </p> 83 + <p> 84 + Sed ut perspiciatis unde omnis iste natus error sit voluptatem 85 + accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae 86 + ab illo inventore veritatis et quasi architecto beatae vitae dicta 87 + sunt explicabo. 88 + </p> 89 + <p> 90 + Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut 91 + fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem 92 + sequi nesciunt. 93 + </p> 94 + <p> 95 + Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, 96 + consectetur, adipisci velit, sed quia non numquam eius modi tempora 97 + incidunt ut labore et dolore magnam aliquam quaerat voluptatem. 98 + </p> 99 + <p> 100 + Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis 101 + suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis 102 + autem vel eum iure reprehenderit qui in ea voluptate velit esse quam 103 + nihil molestiae consequatur. 104 + </p> 105 + <p> 106 + At vero eos et accusamus et iusto odio dignissimos ducimus qui 107 + blanditiis praesentium voluptatum deleniti atque corrupti quos dolores 108 + et quas molestias excepturi sint occaecati cupiditate non provident. 109 + </p> 110 + <p> 111 + Similique sunt in culpa qui officia deserunt mollitia animi, id est 112 + laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita 113 + distinctio. 114 + </p> 115 + </div> 116 + </div> 117 + ); 118 + }
+19 -15
apps/docs/stylex.css
··· 70 70 @property --x---xepqutl { syntax: "*"; inherits: false;} 71 71 @property --x---x1umfgwa { syntax: "*"; inherits: false;} 72 72 @property --x---x1l5nls6 { syntax: "*"; inherits: false;} 73 + @property --x-boxShadow { syntax: "*"; inherits: false;} 73 74 @property --x-animationDuration { syntax: "*"; inherits: false;} 74 75 @property --x-animationName { syntax: "*"; inherits: false;} 75 76 @property --x-animationTimingFunction { syntax: "*"; inherits: false;} 76 - @property --x-boxShadow { syntax: "*"; inherits: false;} 77 77 @property --x-gridTemplateRows { syntax: "*"; inherits: false;} 78 78 @property --x-gridTemplateColumns { syntax: "*"; inherits: false;} 79 79 @property --x-gridColumnStart { syntax: "*"; inherits: false;} ··· 469 469 .xvruivg:is([data-size=lg] *){--progress-size:var(--xyoqvup)} 470 470 .xho0a14:is([data-navbar-open]){--separator-visibility:flex} 471 471 .xfg5xm8:is([data-breadcrumb] *){--underline-opacity:0} 472 - .xdd3hwi:is([data-hovered]){--underline-opacity:1} 473 472 .xtj37it:is([aria-expanded=true]){--underline-opacity:1} 473 + .xdd3hwi:is([data-hovered]){--underline-opacity:1} 474 474 .xief1pu:is([data-active]){--underline-opacity:1} 475 475 .xtbn0nk:is([data-navbar-open]){--visibility:flex} 476 476 .x1xi42hd:has([data-always-visible]){--separator-visibility:none} ··· 947 947 .x11lhmoz:not(#\#):not(#\#):not(#\#){transform:translate(-50%,-50%)} 948 948 .x5i6ehr:not(#\#):not(#\#):not(#\#){transform:translateX(-100%)} 949 949 .xuuh30:not(#\#):not(#\#):not(#\#){transform:translateX(-50%)} 950 + .x105ttfm:not(#\#):not(#\#):not(#\#){transform:translateY(-100%)} 950 951 .x1cb1t30:not(#\#):not(#\#):not(#\#){transform:translateY(-50%)} 952 + .xnn1q72:not(#\#):not(#\#):not(#\#){transform:translateY(0)} 951 953 .xsqj5wx:not(#\#):not(#\#):not(#\#){transform:var(--x-transform)} 952 954 .x13zvrfm:not(#\#):not(#\#):not(#\#){transition-duration:100ms} 953 955 .xndv0t3:not(#\#):not(#\#):not(#\#){transition-duration:150ms} ··· 966 968 .x1mnhl27:not(#\#):not(#\#):not(#\#){transition-property:translate,width} 967 969 .xz4gly6:not(#\#):not(#\#):not(#\#){transition-timing-function:ease-in-out} 968 970 .xcj1dhv:not(#\#):not(#\#):not(#\#){transition-timing-function:linear} 971 + .x1d661q8:not(#\#):not(#\#):not(#\#){transition-timing-function:cubic-bezier(0.5, 0, 0.5, 1)} 969 972 .x1eoxazy:not(#\#):not(#\#):not(#\#){transition-timing-function: 970 973 linear( 971 974 0, ··· 997 1000 .x1ja2u2z:not(#\#):not(#\#):not(#\#){z-index:0} 998 1001 .x1vjfegm:not(#\#):not(#\#):not(#\#){z-index:1} 999 1002 .x11uqc5h:not(#\#):not(#\#):not(#\#){z-index:100} 1003 + .xfo81ep:not(#\#):not(#\#):not(#\#){z-index:1000} 1000 1004 .x1q2oy4v:not(#\#):not(#\#):not(#\#){z-index:9999} 1001 1005 @container (min-width: 40rem){.x1x9uo7l.x1x9uo7l:not(#\#):not(#\#):not(#\#){--separator-visibility:none}} 1002 1006 @container (min-width: 40rem){.x1y37myp.x1y37myp:not(#\#):not(#\#):not(#\#){--visibility:none}} ··· 1075 1079 .xgdln6g:is([data-hovered=true]):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1076 1080 .x1kag6iq:is([data-react-aria-pressable=true]:hover:not([data-disabled]) *):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1077 1081 .x1n5h65z:is([data-hovered]):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1078 - .x3o6k81:is([data-hovered=true]):not([data-pressed=true]):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1079 1082 .xvqskg:is([data-variant=secondary] *):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1080 1083 .xb2a7i8:is([data-pressed]):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1081 1084 .x1i8lhow:is([data-hovered]):not([data-unavailable]):not(#\#):not(#\#):not(#\#)::before{background-color:var(--xx7wvuk)} 1085 + .x3o6k81:is([data-hovered=true]):not([data-pressed=true]):not(#\#):not(#\#):not(#\#){background-color:var(--xx7wvuk)} 1082 1086 .xq2ffnt:is([data-hovered=true] *):not(#\#):not(#\#):not(#\#){background-color:var(--xxsd9i2)} 1083 1087 .x1kpz582:is(:active,[data-pressed=true]):not(:disabled):not(#\#):not(#\#):not(#\#){background-color:var(--xzgtcz2)} 1084 1088 .x1advicj:hover:not(:has(* button:hover)):not(:disabled):not(#\#):not(#\#):not(#\#){background-color:var(--xzgtcz2)} ··· 1098 1102 .x13m65lo:is([data-variant=critical] *):not(#\#):not(#\#):not(#\#){color:var(--x1umfgwa)} 1099 1103 .x178ek7g:is([data-selected]):not(#\#):not(#\#):not(#\#){color:var(--x1xyma3f)} 1100 1104 .x1pis5b5:is([data-selection-start],[data-selection-end]):not(#\#):not(#\#):not(#\#){color:var(--x1xyma3f)} 1105 + .xx3mchb:is([data-breadcrumb] *):not(#\#):not(#\#):not(#\#){color:var(--x45q57k)} 1101 1106 .x1lydh9o:is(:not(#\#):not(#\#):not(#\#)::placeholder,[data-placeholder]){color:var(--x45q57k)} 1102 - .x1t5mdh1:is([data-current]):not(#\#):not(#\#):not(#\#){color:var(--x45q57k)} 1103 - .xx3mchb:is([data-breadcrumb] *):not(#\#):not(#\#):not(#\#){color:var(--x45q57k)} 1104 1107 .xuk2py2:is([data-placeholder]):not(#\#):not(#\#):not(#\#){color:var(--x45q57k)} 1108 + .x1t5mdh1:is([data-current]):not(#\#):not(#\#):not(#\#){color:var(--x45q57k)} 1109 + .xnprwmv:is([data-breadcrumb][data-current] *):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1105 1110 .xlu6prn:is([data-hovered]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1106 1111 .xkzp63n:is([data-selected]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1107 1112 .x1xwj67i:is([data-hovered],[data-focused],[data-selected]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1108 - .xnprwmv:is([data-breadcrumb][data-current] *):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1109 1113 .x8b72bf:is([data-hovered]):not([data-unavailable]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1110 1114 .xf5mls2:is([data-variant=warning] *):not(#\#):not(#\#):not(#\#){color:var(--xw4mcn1)} 1111 1115 .x15057wy:is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){column-gap:var(--xgvn2um)} ··· 1125 1129 .xmzpm4q:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#){flex-grow:1} 1126 1130 .x1vnanun:is(*) svg:not(#\#):not(#\#):not(#\#){flex-shrink:0} 1127 1131 .xn4f9nh:is([data-card-size='sm'] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1hevvyd)} 1128 - .xs02ue2:is([data-size=lg] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1hevvyd)} 1129 1132 .x121stge:is([data-empty-state-size='sm'] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1hevvyd)} 1133 + .xs02ue2:is([data-size=lg] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1hevvyd)} 1130 1134 .xcuzl93:is([data-size=sm]):not(#\#):not(#\#):not(#\#){font-size:var(--x1vaen13)} 1131 1135 .x1kk4rht:is([data-size=sm] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1vaen13)} 1132 1136 .xw6vg6:is([data-card-size='md'] *):not(#\#):not(#\#):not(#\#){font-size:var(--x1vzl7l6)} ··· 1158 1162 .xz45kkc:is([aria-disabled=true] *):not(#\#):not(#\#):not(#\#){opacity:.5} 1159 1163 .x78oxnk:is([data-disabled=true] *):not(#\#):not(#\#):not(#\#){opacity:.5} 1160 1164 .xc6n1n6:is([data-outside-visible-range],[data-unavailable]):not(#\#):not(#\#):not(#\#){opacity:.5} 1165 + .x1xlq6hu:is([data-entering], [data-entering] > *):not(#\#):not(#\#):not(#\#){opacity:0} 1166 + .x15h3uk9:is([data-exiting], [data-exiting] > *):not(#\#):not(#\#):not(#\#){opacity:0} 1161 1167 .xo3lgda:is([data-entering]):not(#\#):not(#\#):not(#\#){opacity:0} 1162 1168 .x1y3ytew:is([data-exiting]):not(#\#):not(#\#):not(#\#){opacity:0} 1163 - .x1xlq6hu:is([data-entering], [data-entering] > *):not(#\#):not(#\#):not(#\#){opacity:0} 1164 - .x15h3uk9:is([data-exiting], [data-exiting] > *):not(#\#):not(#\#):not(#\#){opacity:0} 1165 1169 .xdwziqs:is([data-heading-link]:hover *):not(#\#):not(#\#):not(#\#){opacity:1} 1166 1170 .x127iuxr:is([data-focus-visible]):not(#\#):not(#\#):not(#\#){opacity:1} 1167 1171 .x1f9km8m:is([data-react-aria-pressable=true]:hover:not([data-disabled]) *):not(#\#):not(#\#):not(#\#){opacity:1} ··· 1512 1516 .xfajk90:is([aria-orientation=vertical]):not(#\#):not(#\#):not(#\#):not(#\#){height:100%} 1513 1517 .x15y5qw9:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#){height:100%} 1514 1518 .x1e29dhg:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#)::before{height:100%} 1515 - .xugiiin:is([data-navbar-open]):not(#\#):not(#\#):not(#\#):not(#\#){height:100%} 1516 1519 .x1ujwaxl:is([data-handle-orientation='horizontal'] *):not(#\#):not(#\#):not(#\#):not(#\#){height:100%} 1520 + .xugiiin:is([data-navbar-open]):not(#\#):not(#\#):not(#\#):not(#\#){height:100%} 1517 1521 .xa6wk7n:is([data-direction=right], [data-direction=left]):not(#\#):not(#\#):not(#\#):not(#\#){height:100vh} 1518 1522 .xa90ixn:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#){height:1px} 1519 1523 .xgfdyuw:is([data-direction=top], [data-direction=bottom]):is([data-size=sm]):not(#\#):not(#\#):not(#\#):not(#\#){height:320px} ··· 1521 1525 .x1i0w0n8:is([data-direction=top], [data-direction=bottom]):is([data-size=lg]):not(#\#):not(#\#):not(#\#):not(#\#){height:800px} 1522 1526 .x4isadb:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#){height:calc(attr(data-progress number) * 1%)} 1523 1527 .xqr7z5w:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#){height:calc(attr(data-progress-end number) * 1% - attr(data-progress-start number) * 1%)} 1524 - .x10pomc3:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x109877l)} 1525 1528 .x1s93bjk:is([data-size=lg] *):not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x109877l)} 1526 1529 .xtv84u5:is([data-focus-visible]):not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x109877l)} 1527 1530 .x1f7uwq8:is([data-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x109877l)} 1531 + .x10pomc3:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x109877l)} 1528 1532 .x2l7m0n:is([data-size=lg]):not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x11x3va4)} 1529 1533 .xyzkuza:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x1a1riub)} 1530 1534 .x1ra6d0n:is([data-size=lg] *):not(#\#):not(#\#):not(#\#):not(#\#){height:var(--x1a1riub)} ··· 1615 1619 .x1olbyz:is([data-table-size=lg] *:not(:last-child)):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xgvn2um)} 1616 1620 .x1srgp0q:is([data-size=md] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xmuc480)} 1617 1621 .x1z0vii2:is([data-size=md]):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xsow7ju)} 1618 - .x1hrrdhv:is([data-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xsow7ju)} 1619 1622 .x1e6kius:is([data-size=md] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xsow7ju)} 1623 + .x1hrrdhv:is([data-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-right:var(--xsow7ju)} 1620 1624 .xl9nm1a:is([data-table-size=lg] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--x1a1riub)} 1621 1625 .xoxkb97:is([data-size=lg]):not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--x1a1riub)} 1622 1626 .xnb2w68:is([data-size=sm]):not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--x1plbop)} ··· 1654 1658 .x16ouo39:is([data-orientation=vertical] *):not(#\#):not(#\#):not(#\#):not(#\#){width:auto} 1655 1659 .x19s274e:is([data-orientation=horizontal] *):not(#\#):not(#\#):not(#\#):not(#\#){width:calc(attr(data-progress number) * 1%)} 1656 1660 .xbu4jqu:is([data-orientation=horizontal] *):not(#\#):not(#\#):not(#\#):not(#\#){width:calc(attr(data-progress-end number) * 1% - attr(data-progress-start number) * 1%)} 1657 - .x1mcq3fe:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x109877l)} 1658 1661 .xi8t3kn:is([data-focus-visible]):not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x109877l)} 1659 1662 .x5clsaj:is([data-size=lg] *):not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x109877l)} 1663 + .x1mcq3fe:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x109877l)} 1660 1664 .x19m7d8w:is(*) svg:not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x1a1riub)} 1661 1665 .xn6lw00:is([data-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x1a1riub)} 1662 1666 .x1xm5iqf:is([data-size=md]):not(#\#):not(#\#):not(#\#):not(#\#){width:var(--x1do95gr)} ··· 1747 1751 .xhkezso:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{display:block} 1748 1752 .x1eokf9:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{opacity:var(--underline-opacity)} 1749 1753 .x2q1x1w:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{pointer-events:none} 1750 - .x1hmns74:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{position:absolute} 1751 1754 .x1j6awrg:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{position:absolute} 1755 + .x1hmns74:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{position:absolute} 1752 1756 .xka30rq:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{transition-duration:100ms} 1753 1757 .xxcwgru:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{transition-property:background-color} 1754 1758 .xn05597:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{transition-timing-function:ease-in-out} ··· 1756 1760 .xhq5o37:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{bottom:0} 1757 1761 .x1bw9o38:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{bottom:calc(var(--x1plbop) * -1)} 1758 1762 .x3cntg4:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{height:2px} 1759 - .x1wlytlt:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{left:0} 1760 1763 .x17cx49:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{left:0} 1764 + .x1wlytlt:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{left:0} 1761 1765 .xnbfe2x:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{right:0} 1762 1766 .x1y3wzot:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{top:0} 1763 1767 .x4eaejv:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{width:100%}
+1
packages/hip-ui/package.json
··· 41 41 "ink": "^6.3.1", 42 42 "lilconfig": "^3.1.3", 43 43 "lucide-react": "catalog:", 44 + "raf-throttle": "^2.0.6", 44 45 "react": "catalog:", 45 46 "react-aria": "catalog:", 46 47 "react-aria-components": "catalog:",
+319
packages/hip-ui/src/components/navbar/Navbar.tsx
··· 1 + "use client"; 2 + import * as stylex from "@stylexjs/stylex"; 3 + import { Menu, X } from "lucide-react"; 4 + import * as React from "react"; 5 + import { use, useState } from "react"; 6 + import { Link, LinkProps } from "react-aria-components"; 7 + 8 + import { SizeContext } from "../context"; 9 + import { IconButton } from "../icon-button"; 10 + import { Separator } from "../separator"; 11 + import { primaryColor, uiColor } from "../theme/color.stylex"; 12 + import { containerBreakpoints } from "../theme/media-queries.stylex"; 13 + import { ui } from "../theme/semantic-color.stylex"; 14 + import { spacing } from "../theme/spacing.stylex"; 15 + import { Size, StyleXComponentProps } from "../theme/types"; 16 + import { fontFamily, fontWeight } from "../theme/typography.stylex"; 17 + 18 + const styles = stylex.create({ 19 + navbar: { 20 + "--separator-visibility": { 21 + default: "none", 22 + ":is([data-navbar-open])": "flex", 23 + ":has([data-always-visible])": "none", 24 + [containerBreakpoints.sm]: "none", 25 + }, 26 + "--visibility": { 27 + ":is([data-navbar-open])": "flex", 28 + [containerBreakpoints.sm]: "none", 29 + }, 30 + gridTemplateAreas: { 31 + default: ` 32 + "logo hamburger" 33 + `, 34 + ":is([data-navbar-open])": ` 35 + "logo hamburger" 36 + "navigation navigation" 37 + "separator separator" 38 + "action action" 39 + `, 40 + ":has([data-always-visible])": ` 41 + "logo action hamburger" 42 + `, 43 + ":has([data-always-visible]):is([data-navbar-open])": ` 44 + "logo action hamburger" 45 + "navigation navigation navigation" 46 + "separator separator separator" 47 + `, 48 + [containerBreakpoints.sm]: ` 49 + "logo navigation action" 50 + `, 51 + }, 52 + overflow: { 53 + ":is([data-navbar-open])": "auto", 54 + }, 55 + alignItems: "center", 56 + boxSizing: "border-box", 57 + columnGap: { 58 + default: spacing["4"], 59 + [containerBreakpoints.sm]: spacing["8"], 60 + }, 61 + display: "grid", 62 + gridTemplateColumns: { 63 + default: "1fr auto", 64 + ":has([data-always-visible])": "1fr min-content min-content", 65 + [containerBreakpoints.sm]: "auto 1fr auto", 66 + }, 67 + gridTemplateRows: { 68 + ":is([data-navbar-open])": `min-content min-content min-content min-content`, 69 + }, 70 + rowGap: spacing["8"], 71 + zIndex: 1000, 72 + borderBottomColor: uiColor.border1, 73 + borderBottomStyle: "solid", 74 + borderBottomWidth: 1, 75 + height: { 76 + default: spacing["14"], 77 + ":is([data-navbar-open])": "100%", 78 + [containerBreakpoints.sm]: spacing["14"], 79 + }, 80 + paddingBottom: spacing["3"], 81 + paddingLeft: spacing["4"], 82 + paddingRight: spacing["4"], 83 + paddingTop: spacing["3"], 84 + top: 0, 85 + width: "100%", 86 + }, 87 + logo: { 88 + alignItems: "center", 89 + display: "flex", 90 + }, 91 + separator: { 92 + gridArea: "separator", 93 + // eslint-disable-next-line @stylexjs/valid-styles 94 + display: "var(--separator-visibility, none)", 95 + }, 96 + navigation: { 97 + gridArea: "navigation", 98 + flex: "1", 99 + gap: { 100 + default: spacing["6"], 101 + [containerBreakpoints.sm]: spacing["8"], 102 + }, 103 + alignItems: { 104 + default: "start", 105 + [containerBreakpoints.sm]: "stretch", 106 + }, 107 + display: { 108 + // eslint-disable-next-line @stylexjs/valid-styles 109 + default: "var(--visibility, none)", 110 + [containerBreakpoints.sm]: "flex", 111 + }, 112 + flexDirection: { 113 + default: "column", 114 + [containerBreakpoints.sm]: "row", 115 + }, 116 + }, 117 + navigationJustifyLeft: { 118 + justifyContent: "flex-start", 119 + }, 120 + navigationJustifyCenter: { 121 + justifyContent: "center", 122 + }, 123 + navigationJustifyRight: { 124 + justifyContent: "flex-end", 125 + }, 126 + action: { 127 + gridArea: "action", 128 + gap: spacing["2"], 129 + alignItems: "center", 130 + display: { 131 + // eslint-disable-next-line @stylexjs/valid-styles 132 + default: "var(--visibility, none)", 133 + [containerBreakpoints.sm]: "flex", 134 + ":is([data-always-visible])": "flex", 135 + }, 136 + }, 137 + hamburgerButton: { 138 + gridArea: "hamburger", 139 + alignItems: "center", 140 + display: { 141 + default: "flex", 142 + [containerBreakpoints.sm]: "none", 143 + }, 144 + }, 145 + link: { 146 + "--underline-opacity": { 147 + default: 0, 148 + ":is([aria-expanded=true])": 1, 149 + ":is([data-active])": 1, 150 + ":is([data-breadcrumb] *)": 0, 151 + ":is([data-hovered])": 1, 152 + }, 153 + gap: spacing["2"], 154 + textDecoration: "none", 155 + alignItems: "center", 156 + color: { 157 + default: primaryColor.text2, 158 + ":is([data-breadcrumb] *)": uiColor.text1, 159 + ":is([data-breadcrumb][data-current] *)": uiColor.text2, 160 + }, 161 + cursor: "pointer", 162 + display: "inline-flex", 163 + fontFamily: fontFamily["sans"], 164 + fontWeight: fontWeight["normal"], 165 + position: "relative", 166 + 167 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 168 + ":is(*) svg": { 169 + height: "1.2em", 170 + width: "1.2em", 171 + }, 172 + 173 + "::after": { 174 + backgroundColor: "currentColor", 175 + content: '""', 176 + display: "block", 177 + opacity: "var(--underline-opacity)", 178 + pointerEvents: "none", 179 + position: "absolute", 180 + bottom: `calc(${spacing["1"]} * -1)`, 181 + height: "2px", 182 + left: 0, 183 + right: 0, 184 + width: "100%", 185 + }, 186 + }, 187 + }); 188 + 189 + // Define subcomponents first so they can be referenced in Navbar 190 + export interface NavbarLogoProps 191 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 192 + 193 + /** 194 + * NavbarLogo component for displaying the logo in the navbar. 195 + */ 196 + export const NavbarLogo = ({ style, ...props }: NavbarLogoProps) => { 197 + return ( 198 + <div {...props} {...stylex.props(styles.logo, style)}> 199 + {props.children} 200 + </div> 201 + ); 202 + }; 203 + 204 + export interface NavbarNavigationProps 205 + extends StyleXComponentProps<React.ComponentProps<"div">> { 206 + /** 207 + * Justify content alignment for the navigation items. 208 + * @default "left" 209 + */ 210 + justify?: "left" | "right" | "center"; 211 + } 212 + 213 + /** 214 + * NavbarNavigation component for displaying navigation items. 215 + * On mobile, this is hidden and shown in the hamburger menu. 216 + */ 217 + export const NavbarNavigation = ({ 218 + style, 219 + justify = "left", 220 + ...props 221 + }: NavbarNavigationProps) => { 222 + return ( 223 + <div 224 + {...props} 225 + {...stylex.props( 226 + styles.navigation, 227 + justify === "left" && styles.navigationJustifyLeft, 228 + justify === "center" && styles.navigationJustifyCenter, 229 + justify === "right" && styles.navigationJustifyRight, 230 + style, 231 + )} 232 + > 233 + {props.children} 234 + </div> 235 + ); 236 + }; 237 + 238 + export interface NavbarActionProps 239 + extends StyleXComponentProps<React.ComponentProps<"div">> { 240 + /** 241 + * Whether the action should be always visible on mobile. 242 + * @default false 243 + */ 244 + alwaysVisible?: boolean; 245 + } 246 + 247 + /** 248 + * NavbarAction component for displaying action buttons. 249 + * On mobile, this is hidden and shown in the hamburger menu. 250 + */ 251 + export const NavbarAction = ({ 252 + style, 253 + alwaysVisible = false, 254 + ...props 255 + }: NavbarActionProps) => { 256 + return ( 257 + <div 258 + {...props} 259 + data-always-visible={alwaysVisible || undefined} 260 + {...stylex.props(styles.action, style)} 261 + > 262 + {props.children} 263 + </div> 264 + ); 265 + }; 266 + 267 + export interface NavbarLinkProps extends StyleXComponentProps<LinkProps> { 268 + isActive?: boolean; 269 + } 270 + 271 + export function NavbarLink({ style, isActive, ...props }: NavbarLinkProps) { 272 + return ( 273 + <Link 274 + data-active={isActive} 275 + {...stylex.props(styles.link, style)} 276 + {...props} 277 + /> 278 + ); 279 + } 280 + 281 + export interface NavbarProps 282 + extends StyleXComponentProps<React.ComponentProps<"nav">> { 283 + size?: Size; 284 + } 285 + 286 + /** 287 + * Navbar component that provides a responsive navigation bar with logo, navigation, and action sections. 288 + * On mobile, navigation and actions are automatically contained in a hamburger menu overlay. 289 + */ 290 + export const Navbar = ({ 291 + style, 292 + size: sizeProp, 293 + children, 294 + ...props 295 + }: NavbarProps) => { 296 + const size = sizeProp || use(SizeContext); 297 + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 298 + 299 + return ( 300 + <SizeContext value={size}> 301 + <nav 302 + {...props} 303 + data-navbar-open={isMobileMenuOpen || undefined} 304 + {...stylex.props(styles.navbar, ui.bg, style)} 305 + > 306 + {children} 307 + <Separator style={styles.separator as unknown as stylex.StyleXStyles} /> 308 + <IconButton 309 + aria-label="Open menu" 310 + variant="tertiary" 311 + style={styles.hamburgerButton} 312 + onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)} 313 + > 314 + {isMobileMenuOpen ? <X /> : <Menu />} 315 + </IconButton> 316 + </nav> 317 + </SizeContext> 318 + ); 319 + };
+239
packages/hip-ui/src/components/navbar/NavbarMenu.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import * as React from "react"; 5 + import { mergeProps, useHover, usePress } from "react-aria"; 6 + import { Button, Disclosure, DisclosurePanel } from "react-aria-components"; 7 + 8 + import { HoverCard, HoverCardProps } from "../hover-card"; 9 + import { animationDuration } from "../theme/animations.stylex"; 10 + import { primaryColor, uiColor } from "../theme/color.stylex"; 11 + import { 12 + containerBreakpoints, 13 + mediaQueries, 14 + } from "../theme/media-queries.stylex"; 15 + import { radius } from "../theme/radius.stylex"; 16 + import { spacing } from "../theme/spacing.stylex"; 17 + import { StyleXComponentProps } from "../theme/types"; 18 + import { fontFamily, fontSize, fontWeight } from "../theme/typography.stylex"; 19 + 20 + const styles = stylex.create({ 21 + menuItem: { 22 + padding: spacing["2"], 23 + borderRadius: { 24 + default: radius["sm"], 25 + [mediaQueries.supportsSquircle]: radius["lg"], 26 + }, 27 + textDecoration: "none", 28 + alignItems: "center", 29 + backgroundColor: { 30 + ":is([data-hovered=true]):not([data-pressed=true])": uiColor.component2, 31 + ":is([data-pressed=true])": uiColor.component3, 32 + }, 33 + columnGap: spacing["3"], 34 + display: "grid", 35 + rowGap: spacing["1.5"], 36 + transitionDuration: animationDuration.fast, 37 + transitionProperty: "background-color", 38 + transitionTimingFunction: "ease-in-out", 39 + userSelect: "none", 40 + 41 + gridTemplateAreas: { 42 + default: '"title"', 43 + ":has([data-description])": ` 44 + "title" 45 + "description" 46 + `, 47 + ":has([data-icon])": ` 48 + "icon title" 49 + `, 50 + ":has([data-icon]):has([data-description])": ` 51 + "icon title" 52 + "icon description" 53 + `, 54 + }, 55 + gridTemplateColumns: { 56 + ":has([data-icon])": "min-content 1fr", 57 + ":has([data-icon]):has([data-description])": "min-content 1fr", 58 + }, 59 + }, 60 + menuItemIcon: { 61 + gridArea: "icon", 62 + padding: spacing["2"], 63 + borderRadius: { 64 + default: radius["sm"], 65 + [mediaQueries.supportsSquircle]: radius["lg"], 66 + }, 67 + alignItems: "center", 68 + backgroundColor: { 69 + default: uiColor.component2, 70 + [stylex.when.ancestor(":hover")]: uiColor.component1, 71 + }, 72 + color: uiColor.text1, 73 + display: "flex", 74 + justifyContent: "center", 75 + height: spacing["8"], 76 + width: spacing["8"], 77 + 78 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 79 + ":is(*) svg": { 80 + height: spacing["6"], 81 + width: spacing["6"], 82 + }, 83 + }, 84 + menuItemLabel: { 85 + gridArea: "title", 86 + color: uiColor.text2, 87 + fontWeight: fontWeight["medium"], 88 + }, 89 + menuItemDescription: { 90 + gridArea: "description", 91 + color: uiColor.text1, 92 + fontSize: fontSize["sm"], 93 + }, 94 + menuItemDisabled: { 95 + opacity: 0.5, 96 + }, 97 + link: { 98 + "--underline-opacity": { 99 + default: 0, 100 + ":is([aria-expanded=true])": 1, 101 + ":is([data-active])": 1, 102 + ":is([data-breadcrumb] *)": 0, 103 + ":is([data-hovered])": 1, 104 + }, 105 + gap: spacing["2"], 106 + textDecoration: "none", 107 + alignItems: "center", 108 + color: { 109 + default: primaryColor.text2, 110 + ":is([data-breadcrumb] *)": uiColor.text1, 111 + ":is([data-breadcrumb][data-current] *)": uiColor.text2, 112 + }, 113 + cursor: "pointer", 114 + display: "inline-flex", 115 + fontFamily: fontFamily["sans"], 116 + fontWeight: fontWeight["normal"], 117 + position: "relative", 118 + 119 + // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 120 + ":is(*) svg": { 121 + height: "1.2em", 122 + width: "1.2em", 123 + }, 124 + 125 + "::after": { 126 + backgroundColor: "currentColor", 127 + content: '""', 128 + display: "block", 129 + opacity: "var(--underline-opacity)", 130 + pointerEvents: "none", 131 + position: "absolute", 132 + bottom: `calc(${spacing["1"]} * -1)`, 133 + height: "2px", 134 + left: 0, 135 + right: 0, 136 + width: "100%", 137 + }, 138 + }, 139 + desktopMenu: { 140 + display: { 141 + default: "none", 142 + [containerBreakpoints.sm]: "block", 143 + }, 144 + }, 145 + mobileMenu: { 146 + display: { 147 + default: "block", 148 + [containerBreakpoints.sm]: "none", 149 + }, 150 + }, 151 + menuTriggerButton: { 152 + display: "contents", 153 + fontSize: "inherit", 154 + }, 155 + menuDisclosurePanel: { 156 + marginLeft: `calc(${spacing["2"]} * -1)`, 157 + paddingTop: spacing["2"], 158 + }, 159 + }); 160 + 161 + interface NavbarMenuProps extends HoverCardProps {} 162 + 163 + export function NavbarMenu({ trigger, children, ...props }: NavbarMenuProps) { 164 + return ( 165 + <> 166 + <div {...stylex.props(styles.desktopMenu)}> 167 + <HoverCard {...props} offset={24} trigger={trigger}> 168 + {children} 169 + </HoverCard> 170 + </div> 171 + <Disclosure {...stylex.props(styles.mobileMenu)}> 172 + <Button slot="trigger" {...stylex.props(styles.menuTriggerButton)}> 173 + {trigger} 174 + </Button> 175 + <DisclosurePanel> 176 + <div {...stylex.props(styles.menuDisclosurePanel)}>{children}</div> 177 + </DisclosurePanel> 178 + </Disclosure> 179 + </> 180 + ); 181 + } 182 + 183 + export interface NavbarMenuTriggerProps 184 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 185 + 186 + export function NavbarMenuTrigger({ style, ...props }: NavbarMenuTriggerProps) { 187 + return <div {...stylex.props(styles.link, style)} {...props} />; 188 + } 189 + 190 + interface NavbarMenuItemProps 191 + extends StyleXComponentProps<Omit<React.ComponentProps<"div">, "children">> { 192 + icon?: React.ReactNode; 193 + label: string; 194 + description?: string; 195 + isDisabled?: boolean; 196 + } 197 + 198 + export function NavbarMenuItem({ 199 + style, 200 + icon, 201 + label, 202 + description, 203 + isDisabled, 204 + ...props 205 + }: NavbarMenuItemProps) { 206 + const { hoverProps, isHovered } = useHover({ isDisabled }); 207 + const { pressProps, isPressed } = usePress({ isDisabled }); 208 + const Component = "href" in props ? "a" : "button"; 209 + 210 + return ( 211 + <Component 212 + {...mergeProps( 213 + props as React.ComponentProps<typeof Component>, 214 + hoverProps, 215 + pressProps, 216 + )} 217 + data-hovered={isHovered} 218 + data-pressed={isPressed} 219 + {...stylex.props( 220 + stylex.defaultMarker(), 221 + styles.menuItem, 222 + isDisabled && styles.menuItemDisabled, 223 + style, 224 + )} 225 + > 226 + {Boolean(icon) && ( 227 + <div data-icon {...stylex.props(styles.menuItemIcon)}> 228 + {icon} 229 + </div> 230 + )} 231 + {label && <div {...stylex.props(styles.menuItemLabel)}>{label}</div>} 232 + {description && ( 233 + <div data-description {...stylex.props(styles.menuItemDescription)}> 234 + {description} 235 + </div> 236 + )} 237 + </Component> 238 + ); 239 + }
+4 -505
packages/hip-ui/src/components/navbar/index.tsx
··· 1 - "use client"; 2 - 3 - import * as stylex from "@stylexjs/stylex"; 4 - import { Menu, X } from "lucide-react"; 5 - import * as React from "react"; 6 - import { use, useState } from "react"; 7 - import { mergeProps, useHover, usePress } from "react-aria"; 8 - import { 9 - Button, 10 - Disclosure, 11 - DisclosurePanel, 12 - Link, 13 - LinkProps, 14 - } from "react-aria-components"; 1 + /* eslint-disable react-refresh/only-export-components */ 15 2 16 - import { SizeContext } from "../context"; 17 - import { HoverCard, HoverCardProps } from "../hover-card"; 18 - import { IconButton } from "../icon-button"; 19 - import { Separator } from "../separator"; 20 - import { animationDuration } from "../theme/animations.stylex"; 21 - import { primaryColor, uiColor } from "../theme/color.stylex"; 22 - import { 23 - containerBreakpoints, 24 - mediaQueries, 25 - } from "../theme/media-queries.stylex"; 26 - import { radius } from "../theme/radius.stylex"; 27 - import { ui } from "../theme/semantic-color.stylex"; 28 - import { spacing } from "../theme/spacing.stylex"; 29 - import { Size, StyleXComponentProps } from "../theme/types"; 30 - import { fontFamily, fontSize, fontWeight } from "../theme/typography.stylex"; 3 + export * from "./Navbar"; 4 + export * from "./NavbarMenu"; 31 5 32 - const styles = stylex.create({ 33 - navbar: { 34 - "--separator-visibility": { 35 - default: "none", 36 - ":is([data-navbar-open])": "flex", 37 - ":has([data-always-visible])": "none", 38 - [containerBreakpoints.sm]: "none", 39 - }, 40 - "--visibility": { 41 - ":is([data-navbar-open])": "flex", 42 - [containerBreakpoints.sm]: "none", 43 - }, 44 - gridTemplateAreas: { 45 - default: ` 46 - "logo hamburger" 47 - `, 48 - ":is([data-navbar-open])": ` 49 - "logo hamburger" 50 - "navigation navigation" 51 - "separator separator" 52 - "action action" 53 - `, 54 - ":has([data-always-visible])": ` 55 - "logo action hamburger" 56 - `, 57 - ":has([data-always-visible]):is([data-navbar-open])": ` 58 - "logo action hamburger" 59 - "navigation navigation navigation" 60 - "separator separator separator" 61 - `, 62 - [containerBreakpoints.sm]: ` 63 - "logo navigation action" 64 - `, 65 - }, 66 - overflow: { 67 - ":is([data-navbar-open])": "auto", 68 - }, 69 - alignItems: "center", 70 - boxSizing: "border-box", 71 - columnGap: { 72 - default: spacing["4"], 73 - [containerBreakpoints.sm]: spacing["8"], 74 - }, 75 - display: "grid", 76 - gridTemplateColumns: { 77 - default: "1fr auto", 78 - ":has([data-always-visible])": "1fr min-content min-content", 79 - [containerBreakpoints.sm]: "auto 1fr auto", 80 - }, 81 - gridTemplateRows: { 82 - ":is([data-navbar-open])": `min-content min-content min-content min-content`, 83 - }, 84 - rowGap: spacing["8"], 85 - borderBottomColor: uiColor.border1, 86 - borderBottomStyle: "solid", 87 - borderBottomWidth: 1, 88 - height: { 89 - default: spacing["14"], 90 - ":is([data-navbar-open])": "100%", 91 - [containerBreakpoints.sm]: spacing["14"], 92 - }, 93 - paddingBottom: spacing["3"], 94 - paddingLeft: spacing["4"], 95 - paddingRight: spacing["4"], 96 - paddingTop: spacing["3"], 97 - width: "100%", 98 - }, 99 - logo: { 100 - alignItems: "center", 101 - display: "flex", 102 - }, 103 - separator: { 104 - gridArea: "separator", 105 - // eslint-disable-next-line @stylexjs/valid-styles 106 - display: "var(--separator-visibility, none)", 107 - }, 108 - navigation: { 109 - gridArea: "navigation", 110 - flex: "1", 111 - gap: { 112 - default: spacing["6"], 113 - [containerBreakpoints.sm]: spacing["8"], 114 - }, 115 - alignItems: { 116 - default: "start", 117 - [containerBreakpoints.sm]: "stretch", 118 - }, 119 - display: { 120 - // eslint-disable-next-line @stylexjs/valid-styles 121 - default: "var(--visibility, none)", 122 - [containerBreakpoints.sm]: "flex", 123 - }, 124 - flexDirection: { 125 - default: "column", 126 - [containerBreakpoints.sm]: "row", 127 - }, 128 - }, 129 - navigationJustifyLeft: { 130 - justifyContent: "flex-start", 131 - }, 132 - navigationJustifyCenter: { 133 - justifyContent: "center", 134 - }, 135 - navigationJustifyRight: { 136 - justifyContent: "flex-end", 137 - }, 138 - action: { 139 - gridArea: "action", 140 - gap: spacing["2"], 141 - alignItems: "center", 142 - display: { 143 - // eslint-disable-next-line @stylexjs/valid-styles 144 - default: "var(--visibility, none)", 145 - [containerBreakpoints.sm]: "flex", 146 - ":is([data-always-visible])": "flex", 147 - }, 148 - }, 149 - hamburgerButton: { 150 - gridArea: "hamburger", 151 - alignItems: "center", 152 - display: { 153 - default: "flex", 154 - [containerBreakpoints.sm]: "none", 155 - }, 156 - }, 157 - menuItem: { 158 - padding: spacing["2"], 159 - borderRadius: { 160 - default: radius["sm"], 161 - [mediaQueries.supportsSquircle]: radius["lg"], 162 - }, 163 - textDecoration: "none", 164 - alignItems: "center", 165 - backgroundColor: { 166 - ":is([data-hovered=true]):not([data-pressed=true])": uiColor.component2, 167 - ":is([data-pressed=true])": uiColor.component3, 168 - }, 169 - columnGap: spacing["3"], 170 - display: "grid", 171 - rowGap: spacing["1.5"], 172 - transitionDuration: animationDuration.fast, 173 - transitionProperty: "background-color", 174 - transitionTimingFunction: "ease-in-out", 175 - userSelect: "none", 176 - 177 - gridTemplateAreas: { 178 - default: '"title"', 179 - ":has([data-description])": ` 180 - "title" 181 - "description" 182 - `, 183 - ":has([data-icon])": ` 184 - "icon title" 185 - `, 186 - ":has([data-icon]):has([data-description])": ` 187 - "icon title" 188 - "icon description" 189 - `, 190 - }, 191 - gridTemplateColumns: { 192 - ":has([data-icon])": "min-content 1fr", 193 - ":has([data-icon]):has([data-description])": "min-content 1fr", 194 - }, 195 - }, 196 - menuItemIcon: { 197 - gridArea: "icon", 198 - padding: spacing["2"], 199 - borderRadius: { 200 - default: radius["sm"], 201 - [mediaQueries.supportsSquircle]: radius["lg"], 202 - }, 203 - alignItems: "center", 204 - backgroundColor: { 205 - default: uiColor.component2, 206 - [stylex.when.ancestor(":hover")]: uiColor.component1, 207 - }, 208 - color: uiColor.text1, 209 - display: "flex", 210 - justifyContent: "center", 211 - height: spacing["8"], 212 - width: spacing["8"], 213 - 214 - // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 215 - ":is(*) svg": { 216 - height: spacing["6"], 217 - width: spacing["6"], 218 - }, 219 - }, 220 - menuItemLabel: { 221 - gridArea: "title", 222 - color: uiColor.text2, 223 - fontWeight: fontWeight["medium"], 224 - }, 225 - menuItemDescription: { 226 - gridArea: "description", 227 - color: uiColor.text1, 228 - fontSize: fontSize["sm"], 229 - }, 230 - menuItemDisabled: { 231 - opacity: 0.5, 232 - }, 233 - link: { 234 - "--underline-opacity": { 235 - default: 0, 236 - ":is([aria-expanded=true])": 1, 237 - ":is([data-active])": 1, 238 - ":is([data-breadcrumb] *)": 0, 239 - ":is([data-hovered])": 1, 240 - }, 241 - gap: spacing["2"], 242 - textDecoration: "none", 243 - alignItems: "center", 244 - color: { 245 - default: primaryColor.text2, 246 - ":is([data-breadcrumb] *)": uiColor.text1, 247 - ":is([data-breadcrumb][data-current] *)": uiColor.text2, 248 - }, 249 - cursor: "pointer", 250 - display: "inline-flex", 251 - fontFamily: fontFamily["sans"], 252 - fontWeight: fontWeight["normal"], 253 - position: "relative", 254 - 255 - // eslint-disable-next-line @stylexjs/no-legacy-contextual-styles, @stylexjs/valid-styles 256 - ":is(*) svg": { 257 - height: "1.2em", 258 - width: "1.2em", 259 - }, 260 - 261 - "::after": { 262 - backgroundColor: "currentColor", 263 - content: '""', 264 - display: "block", 265 - opacity: "var(--underline-opacity)", 266 - pointerEvents: "none", 267 - position: "absolute", 268 - bottom: `calc(${spacing["1"]} * -1)`, 269 - height: "2px", 270 - left: 0, 271 - right: 0, 272 - width: "100%", 273 - }, 274 - }, 275 - desktopMenu: { 276 - display: { 277 - default: "none", 278 - [containerBreakpoints.sm]: "block", 279 - }, 280 - }, 281 - mobileMenu: { 282 - display: { 283 - default: "block", 284 - [containerBreakpoints.sm]: "none", 285 - }, 286 - }, 287 - menuTriggerButton: { 288 - display: "contents", 289 - fontSize: "inherit", 290 - }, 291 - menuDisclosurePanel: { 292 - marginLeft: `calc(${spacing["2"]} * -1)`, 293 - paddingTop: spacing["2"], 294 - }, 295 - }); 296 - 297 - // Define subcomponents first so they can be referenced in Navbar 298 - export interface NavbarLogoProps 299 - extends StyleXComponentProps<React.ComponentProps<"div">> {} 300 - 301 - /** 302 - * NavbarLogo component for displaying the logo in the navbar. 303 - */ 304 - export const NavbarLogo = ({ style, ...props }: NavbarLogoProps) => { 305 - return ( 306 - <div {...props} {...stylex.props(styles.logo, style)}> 307 - {props.children} 308 - </div> 309 - ); 310 - }; 311 - 312 - export interface NavbarNavigationProps 313 - extends StyleXComponentProps<React.ComponentProps<"div">> { 314 - /** 315 - * Justify content alignment for the navigation items. 316 - * @default "left" 317 - */ 318 - justify?: "left" | "right" | "center"; 319 - } 320 - 321 - /** 322 - * NavbarNavigation component for displaying navigation items. 323 - * On mobile, this is hidden and shown in the hamburger menu. 324 - */ 325 - export const NavbarNavigation = ({ 326 - style, 327 - justify = "left", 328 - ...props 329 - }: NavbarNavigationProps) => { 330 - return ( 331 - <div 332 - {...props} 333 - {...stylex.props( 334 - styles.navigation, 335 - justify === "left" && styles.navigationJustifyLeft, 336 - justify === "center" && styles.navigationJustifyCenter, 337 - justify === "right" && styles.navigationJustifyRight, 338 - style, 339 - )} 340 - > 341 - {props.children} 342 - </div> 343 - ); 344 - }; 345 - 346 - export interface NavbarActionProps 347 - extends StyleXComponentProps<React.ComponentProps<"div">> { 348 - /** 349 - * Whether the action should be always visible on mobile. 350 - * @default false 351 - */ 352 - alwaysVisible?: boolean; 353 - } 354 - 355 - /** 356 - * NavbarAction component for displaying action buttons. 357 - * On mobile, this is hidden and shown in the hamburger menu. 358 - */ 359 - export const NavbarAction = ({ 360 - style, 361 - alwaysVisible = false, 362 - ...props 363 - }: NavbarActionProps) => { 364 - return ( 365 - <div 366 - {...props} 367 - data-always-visible={alwaysVisible || undefined} 368 - {...stylex.props(styles.action, style)} 369 - > 370 - {props.children} 371 - </div> 372 - ); 373 - }; 374 - 375 - interface NavbarMenuProps extends HoverCardProps {} 376 - 377 - export function NavbarMenu({ trigger, children, ...props }: NavbarMenuProps) { 378 - return ( 379 - <> 380 - <div {...stylex.props(styles.desktopMenu)}> 381 - <HoverCard {...props} offset={24} trigger={trigger}> 382 - {children} 383 - </HoverCard> 384 - </div> 385 - <Disclosure {...stylex.props(styles.mobileMenu)}> 386 - <Button slot="trigger" {...stylex.props(styles.menuTriggerButton)}> 387 - {trigger} 388 - </Button> 389 - <DisclosurePanel> 390 - <div {...stylex.props(styles.menuDisclosurePanel)}>{children}</div> 391 - </DisclosurePanel> 392 - </Disclosure> 393 - </> 394 - ); 395 - } 396 - 397 - export interface NavbarLinkProps extends StyleXComponentProps<LinkProps> { 398 - isActive?: boolean; 399 - } 400 - 401 - export function NavbarLink({ style, isActive, ...props }: NavbarLinkProps) { 402 - return ( 403 - <Link 404 - data-active={isActive} 405 - {...stylex.props(styles.link, style)} 406 - {...props} 407 - /> 408 - ); 409 - } 410 - 411 - export interface NavbarMenuTriggerProps 412 - extends StyleXComponentProps<React.ComponentProps<"div">> {} 413 - 414 - export function NavbarMenuTrigger({ style, ...props }: NavbarMenuTriggerProps) { 415 - return <div {...stylex.props(styles.link, style)} {...props} />; 416 - } 417 - 418 - interface NavbarMenuItemProps 419 - extends StyleXComponentProps<Omit<React.ComponentProps<"div">, "children">> { 420 - icon?: React.ReactNode; 421 - label: string; 422 - description?: string; 423 - isDisabled?: boolean; 424 - } 425 - 426 - export function NavbarMenuItem({ 427 - style, 428 - icon, 429 - label, 430 - description, 431 - isDisabled, 432 - ...props 433 - }: NavbarMenuItemProps) { 434 - const { hoverProps, isHovered } = useHover({ isDisabled }); 435 - const { pressProps, isPressed } = usePress({ isDisabled }); 436 - const Component = "href" in props ? "a" : "button"; 437 - 438 - return ( 439 - <Component 440 - {...mergeProps( 441 - props as React.ComponentProps<typeof Component>, 442 - hoverProps, 443 - pressProps, 444 - )} 445 - data-hovered={isHovered} 446 - data-pressed={isPressed} 447 - {...stylex.props( 448 - stylex.defaultMarker(), 449 - styles.menuItem, 450 - isDisabled && styles.menuItemDisabled, 451 - style, 452 - )} 453 - > 454 - {Boolean(icon) && ( 455 - <div data-icon {...stylex.props(styles.menuItemIcon)}> 456 - {icon} 457 - </div> 458 - )} 459 - {label && <div {...stylex.props(styles.menuItemLabel)}>{label}</div>} 460 - {description && ( 461 - <div data-description {...stylex.props(styles.menuItemDescription)}> 462 - {description} 463 - </div> 464 - )} 465 - </Component> 466 - ); 467 - } 468 - 469 - export interface NavbarProps 470 - extends StyleXComponentProps<React.ComponentProps<"nav">> { 471 - size?: Size; 472 - } 473 - 474 - /** 475 - * Navbar component that provides a responsive navigation bar with logo, navigation, and action sections. 476 - * On mobile, navigation and actions are automatically contained in a hamburger menu overlay. 477 - */ 478 - export const Navbar = ({ 479 - style, 480 - size: sizeProp, 481 - children, 482 - ...props 483 - }: NavbarProps) => { 484 - const size = sizeProp || use(SizeContext); 485 - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 486 - 487 - return ( 488 - <SizeContext value={size}> 489 - <nav 490 - {...props} 491 - data-navbar-open={isMobileMenuOpen || undefined} 492 - {...stylex.props(styles.navbar, ui.bg, style)} 493 - > 494 - {children} 495 - <Separator style={styles.separator as unknown as stylex.StyleXStyles} /> 496 - <IconButton 497 - aria-label="Open menu" 498 - variant="tertiary" 499 - style={styles.hamburgerButton} 500 - onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)} 501 - > 502 - {isMobileMenuOpen ? <X /> : <Menu />} 503 - </IconButton> 504 - </nav> 505 - </SizeContext> 506 - ); 507 - }; 6 + /* eslint-enable react-refresh/only-export-components */
+4 -1
packages/hip-ui/src/components/navbar/navbar-config.ts
··· 12 12 "../icon-button/index.tsx", 13 13 "../flex/index.tsx", 14 14 "../separator/index.tsx", 15 + "./useAnimatedNavbar.tsx", 16 + "./Navbar.tsx", 17 + "./NavbarMenu.tsx", 15 18 ], 16 19 dependencies: { 17 20 "react-aria-components": "^1.13.0", 18 21 "lucide-react": "^0.263.1", 22 + "raf-throttle": "^2.0.6", 19 23 }, 20 24 }; 21 -
+156
packages/hip-ui/src/components/navbar/useAnimatedNavbar.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import rafThrottle from "raf-throttle"; 5 + import { useEffect, useRef, useState } from "react"; 6 + 7 + import { 8 + animationDuration, 9 + animationTimingFunction, 10 + } from "../theme/animations.stylex"; 11 + 12 + const SCROLL_THRESHOLD = 16; 13 + 14 + const styles = stylex.create({ 15 + navbarOutOfViewport: { 16 + position: "sticky", 17 + transform: "translateY(-100%)", 18 + top: 0, 19 + }, 20 + navbarRevealed: { 21 + position: "sticky", 22 + transform: "translateY(0)", 23 + transitionDuration: animationDuration.slow, 24 + transitionProperty: "transform", 25 + transitionTimingFunction: animationTimingFunction.easeInOut, 26 + top: 0, 27 + }, 28 + navbarAnimatedOut: { 29 + transitionDuration: animationDuration.slow, 30 + transitionProperty: "transform", 31 + transitionTimingFunction: animationTimingFunction.easeInOut, 32 + }, 33 + }); 34 + 35 + /** 36 + * A hook that animates the navbar into view and stick to the top when the user scrolls down. 37 + */ 38 + export const useAnimatedNavbar = ({ 39 + scrollContainer: scrollContainerProp, 40 + }: { 41 + scrollContainer?: React.RefObject<HTMLElement | null>; 42 + }) => { 43 + const lastScrollY = useRef(0); 44 + const [hasScrollNavbarOutOfView, setHasScrollNavbarOutOfView] = 45 + useState(false); 46 + const [shouldAnimateOut, setShouldAnimateOut] = useState(false); 47 + const [shouldAnimateIn, setShouldAnimateIn] = useState(false); 48 + const navRef = useRef<HTMLElement>(null); 49 + const topSentinelRef = useRef<HTMLDivElement>(null); 50 + 51 + // Use intersection observer to detect when navbar is out of viewport 52 + useEffect(() => { 53 + if (!navRef.current) return; 54 + 55 + const observer = new IntersectionObserver(([entry]) => { 56 + if (!entry || entry.isIntersecting) return; 57 + setHasScrollNavbarOutOfView(true); 58 + }); 59 + 60 + observer.observe(navRef.current); 61 + 62 + return () => { 63 + observer.disconnect(); 64 + }; 65 + }, []); 66 + 67 + // Animate the navbar into view and stick to the top 68 + useEffect(() => { 69 + if (!hasScrollNavbarOutOfView || !scrollContainerProp?.current) return; 70 + 71 + const handleScroll = rafThrottle((e: Event) => { 72 + if (!(e.target instanceof HTMLElement)) return; 73 + 74 + const currentScrollY = e.target.scrollTop; 75 + const scrollDirection = 76 + currentScrollY > lastScrollY.current ? "down" : "up"; 77 + 78 + if (scrollDirection === "up") { 79 + // Only hide/show if scrolled past threshold 80 + if (Math.abs(currentScrollY - lastScrollY.current) < SCROLL_THRESHOLD) { 81 + return; 82 + } 83 + 84 + // Show navbar when scrolling up or at the top 85 + if ( 86 + currentScrollY < lastScrollY.current || 87 + currentScrollY <= SCROLL_THRESHOLD 88 + ) { 89 + setShouldAnimateIn(true); 90 + } 91 + } 92 + // Animate navbar out when scrolling down past threshold 93 + else if ( 94 + currentScrollY > lastScrollY.current && 95 + currentScrollY > SCROLL_THRESHOLD 96 + ) { 97 + setShouldAnimateIn(false); 98 + setShouldAnimateOut(true); 99 + } 100 + 101 + lastScrollY.current = currentScrollY; 102 + }); 103 + 104 + const scrollContainer = scrollContainerProp.current; 105 + 106 + scrollContainer.addEventListener("scroll", handleScroll, { 107 + passive: true, 108 + }); 109 + 110 + return () => { 111 + scrollContainer.removeEventListener("scroll", handleScroll); 112 + }; 113 + }, [hasScrollNavbarOutOfView, scrollContainerProp]); 114 + 115 + // Use IntersectionObserver to detect if scrolled to top (most performant) 116 + useEffect(() => { 117 + if (!topSentinelRef.current || !scrollContainerProp?.current) return; 118 + 119 + const observer = new IntersectionObserver( 120 + ([entry]) => { 121 + if (!entry) return; 122 + 123 + const atTop = entry.isIntersecting; 124 + 125 + setHasScrollNavbarOutOfView((has) => { 126 + if (!has) return has; 127 + if (atTop) { 128 + setShouldAnimateIn(false); 129 + setShouldAnimateOut(false); 130 + return false; 131 + } 132 + return true; 133 + }); 134 + }, 135 + { root: scrollContainerProp.current }, 136 + ); 137 + 138 + observer.observe(topSentinelRef.current); 139 + 140 + return () => { 141 + observer.disconnect(); 142 + }; 143 + }, [scrollContainerProp]); 144 + 145 + return { 146 + sentinel: <div ref={topSentinelRef} />, 147 + navBarProps: { 148 + ref: navRef, 149 + style: [ 150 + hasScrollNavbarOutOfView && styles.navbarOutOfViewport, 151 + shouldAnimateIn && styles.navbarRevealed, 152 + shouldAnimateOut && styles.navbarAnimatedOut, 153 + ], 154 + }, 155 + }; 156 + };
+19
pnpm-lock.yaml
··· 161 161 magic-string: 162 162 specifier: ^0.30.21 163 163 version: 0.30.21 164 + match-container: 165 + specifier: ^0.1.0 166 + version: 0.1.0 167 + raf-throttle: 168 + specifier: ^2.0.6 169 + version: 2.0.6 164 170 react: 165 171 specifier: 'catalog:' 166 172 version: 19.2.0 ··· 428 434 lucide-react: 429 435 specifier: 'catalog:' 430 436 version: 0.548.0(react@19.2.0) 437 + raf-throttle: 438 + specifier: ^2.0.6 439 + version: 2.0.6 431 440 react: 432 441 specifier: 'catalog:' 433 442 version: 19.2.0 ··· 5849 5858 resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} 5850 5859 hasBin: true 5851 5860 5861 + match-container@0.1.0: 5862 + resolution: {integrity: sha512-ZVPQUtnh/8Gx0mwwlnDfnl224A9GxVoV5LRlxF9fWs6AnBQ0TsFSaGVwSAwj6aD7LFOGuVTeI8KuBGBDTtbX4g==} 5863 + 5852 5864 math-intrinsics@1.1.0: 5853 5865 resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 5854 5866 engines: {node: '>= 0.4'} ··· 6443 6455 6444 6456 radix3@1.1.2: 6445 6457 resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} 6458 + 6459 + raf-throttle@2.0.6: 6460 + resolution: {integrity: sha512-C7W6hy78A+vMmk5a/B6C5szjBHrUzWJkVyakjKCK59Uy2CcA7KhO1JUvvH32IXYFIcyJ3FMKP3ZzCc2/71I6Vg==} 6446 6461 6447 6462 randombytes@2.1.0: 6448 6463 resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} ··· 14372 14387 punycode.js: 2.3.1 14373 14388 uc.micro: 2.1.0 14374 14389 14390 + match-container@0.1.0: {} 14391 + 14375 14392 math-intrinsics@1.1.0: {} 14376 14393 14377 14394 mdast-util-from-markdown@2.0.2: ··· 15329 15346 '@types/react-dom': 19.2.0(@types/react@19.2.0) 15330 15347 15331 15348 radix3@1.1.2: {} 15349 + 15350 + raf-throttle@2.0.6: {} 15332 15351 15333 15352 randombytes@2.1.0: 15334 15353 dependencies: