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.

drawer

+1029 -33
+1 -1
README.md
··· 32 32 33 33 #### react-aria wrappers 34 34 35 - - [ ] Drawer 36 35 - [ ] Sheet 37 36 - [ ] Grid List 38 37 - [ ] Navigation Menu ··· 45 44 46 45 ### Done 47 46 47 + - [x] Drawer 48 48 - [x] Tabs 49 49 - [x] Menubar 50 50 - [x] Form
+1 -4
apps/docs/src/components/dialog/index.tsx
··· 88 88 > 89 89 {trigger} 90 90 91 - <ModalOverlay 92 - isKeyboardDismissDisabled 93 - {...stylex.props(dialogStyles.overlay)} 94 - > 91 + <ModalOverlay {...stylex.props(dialogStyles.overlay)} isDismissable> 95 92 <Modal {...stylex.props(dialogStyles.modal)}> 96 93 <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 97 94 {children}
+39
apps/docs/src/components/drawer/NonModalDrawer.tsx
··· 1 + import { 2 + mergeProps, 3 + useEnterAnimation, 4 + useExitAnimation, 5 + } from "@react-aria/utils"; 6 + import { use, useRef } from "react"; 7 + import { Overlay, useOverlay } from "react-aria"; 8 + import { OverlayTriggerStateContext } from "react-aria-components"; 9 + 10 + export function NonModalDrawer(props: React.ComponentProps<"div">) { 11 + const state = use(OverlayTriggerStateContext); 12 + 13 + if (!state) { 14 + throw new Error("No overlay trigger state found"); 15 + } 16 + 17 + const modalRef = useRef<HTMLDivElement>(null); 18 + const { overlayProps } = useOverlay( 19 + { isOpen: state.isOpen, onClose: state.close }, 20 + modalRef, 21 + ); 22 + const entering = useEnterAnimation(modalRef) || false; 23 + const exiting = useExitAnimation(modalRef, state.isOpen); 24 + 25 + if (!state.isOpen && !exiting) { 26 + return null; 27 + } 28 + 29 + return ( 30 + <Overlay isExiting={exiting}> 31 + <div 32 + {...mergeProps(overlayProps, props)} 33 + ref={modalRef} 34 + data-entering={entering || undefined} 35 + data-exiting={exiting || undefined} 36 + /> 37 + </Overlay> 38 + ); 39 + }
+247
apps/docs/src/components/drawer/index.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import { X } from "lucide-react"; 5 + import { 6 + DialogTrigger, 7 + DialogTriggerProps, 8 + Dialog as AriaDialog, 9 + Modal, 10 + ModalOverlay, 11 + Heading, 12 + } from "react-aria-components"; 13 + 14 + import { IconButton } from "../icon-button"; 15 + import { animationDuration, animations } from "../theme/animations.stylex"; 16 + import { uiColor } from "../theme/semantic-color.stylex"; 17 + import { spacing } from "../theme/spacing.stylex"; 18 + import { Size, StyleXComponentProps } from "../theme/types"; 19 + import { typeramp } from "../theme/typography.stylex"; 20 + import { useDialogStyles } from "../theme/useDialogStyles"; 21 + import { NonModalDrawer } from "./NonModalDrawer"; 22 + 23 + const styles = stylex.create({ 24 + overlay: { 25 + zIndex: 0, 26 + }, 27 + drawerWrapper: { 28 + bottom: { 29 + ":is([data-direction=bottom])": 0, 30 + ":is([data-direction=left])": 0, 31 + ":is([data-direction=right])": 0, 32 + }, 33 + left: { 34 + ":is([data-direction=left])": 0, 35 + ":is([data-direction=top])": 0, 36 + ":is([data-direction=bottom])": 0, 37 + }, 38 + position: "fixed", 39 + right: { 40 + ":is([data-direction=right])": 0, 41 + ":is([data-direction=top])": 0, 42 + ":is([data-direction=bottom])": 0, 43 + }, 44 + top: { 45 + ":is([data-direction=top])": 0, 46 + ":is([data-direction=left])": 0, 47 + ":is([data-direction=right])": 0, 48 + }, 49 + 50 + borderBottomWidth: { 51 + default: 0, 52 + ":is([data-direction=top])": 1, 53 + }, 54 + borderLeftWidth: { 55 + default: 0, 56 + ":is([data-direction=right])": 1, 57 + }, 58 + borderRadius: 0, 59 + borderRightWidth: { 60 + default: 0, 61 + ":is([data-direction=left])": 1, 62 + }, 63 + borderTopWidth: { 64 + default: 0, 65 + ":is([data-direction=bottom])": 1, 66 + }, 67 + height: { 68 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=sm])": 69 + "320px", 70 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=md])": 71 + "600px", 72 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=lg])": 73 + "800px", 74 + ":is([data-direction=right], [data-direction=left])": "100vh", 75 + }, 76 + maxHeight: { 77 + ":is([data-direction=top], [data-direction=bottom])": `calc(100vh - ${spacing["8"]})`, 78 + ":is([data-direction=right], [data-direction=left])": "100vh", 79 + }, 80 + maxWidth: { 81 + ":is([data-direction=top], [data-direction=bottom])": "100vw", 82 + ":is([data-direction=right], [data-direction=left])": `calc(100vw - ${spacing["8"]})`, 83 + }, 84 + translate: "unset", 85 + width: { 86 + ":is([data-direction=left], [data-direction=right]):is([data-size=sm])": 87 + "320px", 88 + ":is([data-direction=left], [data-direction=right]):is([data-size=md])": 89 + "600px", 90 + ":is([data-direction=left], [data-direction=right]):is([data-size=lg])": 91 + "800px", 92 + ":is([data-direction=top], [data-direction=bottom])": "100vw", 93 + }, 94 + 95 + animationDuration: animationDuration.default, 96 + animationName: { 97 + ":is([data-direction=right][data-entering])": animations.slideInRight, 98 + ":is([data-direction=right][data-exiting])": animations.slideOutRight, 99 + ":is([data-direction=left][data-entering])": animations.slideInLeft, 100 + ":is([data-direction=left][data-exiting])": animations.slideOutLeft, 101 + ":is([data-direction=top][data-entering])": animations.slideInTop, 102 + ":is([data-direction=top][data-exiting])": animations.slideOutTop, 103 + ":is([data-direction=bottom][data-entering])": animations.slideInBottom, 104 + ":is([data-direction=bottom][data-exiting])": animations.slideOutBottom, 105 + }, 106 + animationTimingFunction: "ease-in-out", 107 + }, 108 + dialog: { 109 + paddingBottom: spacing["2"], 110 + paddingTop: spacing["2"], 111 + }, 112 + header: { 113 + alignItems: "center", 114 + display: "flex", 115 + gap: spacing["2"], 116 + height: spacing["8"], 117 + justifyContent: "space-between", 118 + paddingBottom: spacing["2"], 119 + paddingLeft: spacing["4"], 120 + paddingRight: spacing["4"], 121 + }, 122 + description: { 123 + color: uiColor.text1, 124 + paddingBottom: spacing["4"], 125 + paddingLeft: spacing["4"], 126 + paddingRight: spacing["4"], 127 + paddingTop: spacing["4"], 128 + }, 129 + body: { 130 + flexGrow: 1, 131 + paddingBottom: spacing["4"], 132 + paddingLeft: spacing["4"], 133 + paddingRight: spacing["4"], 134 + paddingTop: { default: 0, ":first-child": spacing["4"] }, 135 + }, 136 + footer: { 137 + display: "flex", 138 + gap: spacing["2"], 139 + justifyContent: "flex-end", 140 + paddingBottom: spacing["2"], 141 + paddingLeft: spacing["4"], 142 + paddingRight: spacing["4"], 143 + paddingTop: spacing["4"], 144 + 145 + borderTopColor: uiColor.border1, 146 + borderTopStyle: "solid", 147 + borderTopWidth: 1, 148 + }, 149 + }); 150 + 151 + export interface DrawerProps extends DialogTriggerProps { 152 + trigger: React.ReactNode; 153 + children: React.ReactNode; 154 + size?: Size; 155 + direction?: "left" | "right" | "top" | "bottom"; 156 + isNonModal?: boolean; 157 + } 158 + 159 + export const Drawer = ({ 160 + trigger, 161 + children, 162 + defaultOpen, 163 + isOpen, 164 + onOpenChange, 165 + size = "md", 166 + direction = "right", 167 + isNonModal = false, 168 + }: DrawerProps) => { 169 + const dialogStyles = useDialogStyles({ size }); 170 + 171 + return ( 172 + <DialogTrigger 173 + defaultOpen={defaultOpen} 174 + isOpen={isOpen} 175 + onOpenChange={onOpenChange} 176 + > 177 + {trigger} 178 + 179 + {isNonModal ? ( 180 + <NonModalDrawer 181 + data-size={size} 182 + data-direction={direction} 183 + {...stylex.props(dialogStyles.modal, styles.drawerWrapper)} 184 + > 185 + <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 186 + {children} 187 + </AriaDialog> 188 + </NonModalDrawer> 189 + ) : ( 190 + <ModalOverlay isDismissable> 191 + <div {...stylex.props(dialogStyles.overlay, styles.overlay)} /> 192 + <Modal 193 + data-size={size} 194 + data-direction={direction} 195 + {...stylex.props(dialogStyles.modal, styles.drawerWrapper)} 196 + > 197 + <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 198 + {children} 199 + </AriaDialog> 200 + </Modal> 201 + </ModalOverlay> 202 + )} 203 + </DialogTrigger> 204 + ); 205 + }; 206 + 207 + export interface DrawerHeaderProps 208 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 209 + 210 + export const DrawerHeader = ({ children, style }: DrawerHeaderProps) => { 211 + return ( 212 + <div {...stylex.props(styles.header, typeramp.heading5, style)}> 213 + <Heading>{children}</Heading> 214 + <IconButton label="Close" size="sm" variant="tertiary" slot="close"> 215 + <X /> 216 + </IconButton> 217 + </div> 218 + ); 219 + }; 220 + 221 + export interface DrawerDescriptionProps 222 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 223 + 224 + export const DrawerDescription = ({ 225 + children, 226 + style, 227 + }: DrawerDescriptionProps) => { 228 + return ( 229 + <div {...stylex.props(styles.description, typeramp.body, style)}> 230 + {children} 231 + </div> 232 + ); 233 + }; 234 + 235 + export interface DrawerBodyProps 236 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 237 + 238 + export const DrawerBody = ({ children, style }: DrawerBodyProps) => { 239 + return <div {...stylex.props(styles.body, style)}>{children}</div>; 240 + }; 241 + 242 + export interface DrawerFooterProps 243 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 244 + 245 + export const DrawerFooter = ({ children, style }: DrawerFooterProps) => { 246 + return <div {...stylex.props(styles.footer, style)}>{children}</div>; 247 + };
+38 -8
apps/docs/src/components/tabs/index.tsx
··· 17 17 import { SizeContext } from "../context"; 18 18 import { animationDuration } from "../theme/animations.stylex"; 19 19 import { mediaQueries } from "../theme/media-queries.stylex"; 20 + import { radius } from "../theme/radius.stylex"; 20 21 import { primaryColor, uiColor } from "../theme/semantic-color.stylex"; 21 22 import { spacing } from "../theme/spacing.stylex"; 22 23 import { Size, StyleXComponentProps } from "../theme/types"; ··· 36 37 ":is([data-orientation=horizontal])": "flex-start", 37 38 ":is([data-orientation=vertical])": "stretch", 38 39 }, 39 - borderImageSlice: { 40 - ":is([data-orientation=horizontal])": "0 0 1", 41 - ":is([data-orientation=vertical])": "0 1 0 0", 40 + borderBottomColor: { 41 + ":is([data-orientation=horizontal])": uiColor.border2, 42 + ":is([data-orientation=vertical])": "transparent", 42 43 }, 43 - borderImageSource: `conic-gradient(${uiColor.border2} 0 0)`, 44 - borderImageWidth: "1px", 44 + borderBottomStyle: { 45 + ":is([data-orientation=horizontal])": "solid", 46 + ":is([data-orientation=vertical])": "none", 47 + }, 48 + borderBottomWidth: { 49 + ":is([data-orientation=horizontal])": 1, 50 + ":is([data-orientation=vertical])": 0, 51 + }, 52 + borderRightColor: { 53 + ":is([data-orientation=horizontal])": "transparent", 54 + ":is([data-orientation=vertical])": uiColor.border2, 55 + }, 56 + borderRightStyle: { 57 + ":is([data-orientation=horizontal])": "none", 58 + ":is([data-orientation=vertical])": "solid", 59 + }, 60 + borderRightWidth: { 61 + ":is([data-orientation=horizontal])": 0, 62 + ":is([data-orientation=vertical])": 1, 63 + }, 45 64 display: "flex", 46 65 flexDirection: { 47 66 ":is([data-orientation=horizontal])": "row", ··· 149 168 }, 150 169 outline: "none", 151 170 padding: { 152 - ":is([data-size=sm] *)": spacing["2.5"], 171 + ":is([data-size=sm] *)": spacing["3"], 153 172 ":is([data-size=md] *)": spacing["4"], 154 173 ":is([data-size=lg] *)": spacing["5"], 155 174 }, 175 + }, 176 + focusRing: { 177 + borderRadius: radius["sm"], 178 + outline: { 179 + default: "none", 180 + ":is([data-focus-visible] *)": `2px solid ${uiColor.solid1}`, 181 + }, 182 + outlineOffset: "2px", 156 183 }, 157 184 }); 158 185 ··· 210 237 211 238 export function Tab({ children, style, ...props }: TabProps) { 212 239 return ( 213 - <AriaTab {...props} {...stylex.props(styles.tab, style)}> 240 + <AriaTab {...props} {...stylex.props(styles.tab, styles.focusRing, style)}> 214 241 <SelectionIndicator {...stylex.props(styles.selectionIndicator)} /> 215 242 {children} 216 243 </AriaTab> ··· 224 251 225 252 export function TabPanel({ children, style, ...props }: TabPanelProps) { 226 253 return ( 227 - <AriaTabPanel {...props} {...stylex.props(styles.tabPanel, style)}> 254 + <AriaTabPanel 255 + {...props} 256 + {...stylex.props(styles.tabPanel, styles.focusRing, style)} 257 + > 228 258 {children} 229 259 </AriaTabPanel> 230 260 );
+80
apps/docs/src/components/theme/animations.stylex.tsx
··· 18 18 }, 19 19 }); 20 20 21 + const slideInRight = stylex.keyframes({ 22 + from: { 23 + transform: "translateX(100%)", 24 + }, 25 + to: { 26 + transform: "translateX(0)", 27 + }, 28 + }); 29 + 30 + const slideOutRight = stylex.keyframes({ 31 + from: { 32 + transform: "translateX(0)", 33 + }, 34 + to: { 35 + transform: "translateX(100%)", 36 + }, 37 + }); 38 + 39 + const slideInLeft = stylex.keyframes({ 40 + from: { 41 + transform: "translateX(-100%)", 42 + }, 43 + to: { 44 + transform: "translateX(0)", 45 + }, 46 + }); 47 + 48 + const slideOutLeft = stylex.keyframes({ 49 + from: { 50 + transform: "translateX(0)", 51 + }, 52 + to: { 53 + transform: "translateX(-100%)", 54 + }, 55 + }); 56 + 57 + const slideInTop = stylex.keyframes({ 58 + from: { 59 + transform: "translateY(-100%)", 60 + }, 61 + to: { 62 + transform: "translateY(0)", 63 + }, 64 + }); 65 + 66 + const slideOutTop = stylex.keyframes({ 67 + from: { 68 + transform: "translateY(0)", 69 + }, 70 + to: { 71 + transform: "translateY(-100%)", 72 + }, 73 + }); 74 + 75 + const slideInBottom = stylex.keyframes({ 76 + from: { 77 + transform: "translateY(100%)", 78 + }, 79 + to: { 80 + transform: "translateY(0)", 81 + }, 82 + }); 83 + 84 + const slideOutBottom = stylex.keyframes({ 85 + from: { 86 + transform: "translateY(0)", 87 + }, 88 + to: { 89 + transform: "translateY(100%)", 90 + }, 91 + }); 92 + 21 93 export const animations = stylex.defineVars({ 22 94 fadeIn, 23 95 zoomIn, 96 + slideInRight, 97 + slideOutRight, 98 + slideInLeft, 99 + slideOutLeft, 100 + slideInTop, 101 + slideOutTop, 102 + slideInBottom, 103 + slideOutBottom, 24 104 }); 25 105 26 106 export const animationDuration = stylex.defineVars({
+1 -8
apps/docs/src/components/theme/useDialogStyles.ts
··· 68 68 69 69 return { 70 70 overlay: styles.overlay, 71 - modal: [ 72 - styles.modal, 73 - styles.modal, 74 - ui.bg, 75 - ui.text, 76 - ui.border, 77 - styles.size(size), 78 - ], 71 + modal: [styles.modal, ui.bg, ui.text, ui.border, styles.size(size)], 79 72 dialog: styles.dialog, 80 73 }; 81 74 }
+56
apps/docs/src/docs/components/drawer.mdx
··· 1 + --- 2 + title: Drawer 3 + description: A modal drawer component that slides in from the side for displaying content in an overlay. 4 + --- 5 + 6 + import { PropDocs } from '../../lib/PropDocs' 7 + import { Example } from '../../lib/Example' 8 + import { Basic } from '../../examples/drawer/basic' 9 + import { DrawerSizes } from '../../examples/drawer/sizes' 10 + import { DrawerDirections } from '../../examples/drawer/directions' 11 + import { DrawerNonModal } from '../../examples/drawer/non-modal' 12 + 13 + <Example src={Basic} /> 14 + 15 + ## Installation 16 + 17 + Run the following command to add the drawer component to your project. 18 + 19 + ```bash 20 + pnpm hip install drawer 21 + ``` 22 + 23 + ## Props 24 + 25 + This component is built using the [React Aria Dialog](https://react-spectrum.adobe.com/react-aria/Dialog.html). 26 + 27 + <PropDocs components={["Drawer", "DrawerHeader", "DrawerDescription", "DrawerBody", "DrawerFooter"]} /> 28 + 29 + ## Features 30 + 31 + ### Sizes 32 + 33 + Drawers support different sizes to accommodate various content types. 34 + 35 + <Example src={DrawerSizes} /> 36 + 37 + ### Directions 38 + 39 + Drawers can be positioned in four different directions: left, right, top, and bottom. 40 + 41 + <Example src={DrawerDirections} /> 42 + 43 + ### Non-Modal 44 + 45 + If you need the page to be interactive while the drawer is open, you can set the `isNonModal` prop to `true`. 46 + 47 + <Example src={DrawerNonModal} /> 48 + 49 + ## Related Components 50 + 51 + - [Dialog](/docs/components/dialog) - For centered modal dialogs 52 + - [AlertDialog](/docs/components/alert-dialog) - For critical actions and confirmations 53 + - [Popover](/docs/components/popover) - For non-modal overlays 54 + - [Button](/docs/components/button) - For drawer action buttons 55 + - [Form](/docs/components/form) - For forms within drawers 56 +
+23
apps/docs/src/examples/drawer/basic.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Drawer, 4 + DrawerHeader, 5 + DrawerBody, 6 + DrawerFooter, 7 + } from "@/components/drawer"; 8 + import { Body } from "@/components/typography"; 9 + 10 + export function Basic() { 11 + return ( 12 + <Drawer trigger={<Button>Open Drawer</Button>}> 13 + <DrawerHeader>Drawer Title</DrawerHeader> 14 + <DrawerBody> 15 + <Body>This is the drawer content. You can put any content here.</Body> 16 + </DrawerBody> 17 + <DrawerFooter> 18 + <Button variant="secondary">Cancel</Button> 19 + <Button>Confirm</Button> 20 + </DrawerFooter> 21 + </Drawer> 22 + ); 23 + }
+54
apps/docs/src/examples/drawer/directions.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { Drawer, DrawerHeader, DrawerBody } from "@/components/drawer"; 3 + import { Flex } from "@/components/flex"; 4 + import { Body } from "@/components/typography"; 5 + 6 + export function DrawerDirections() { 7 + return ( 8 + <Flex gap="4" wrap> 9 + <Drawer 10 + size="sm" 11 + trigger={<Button>Open Left Drawer</Button>} 12 + direction="left" 13 + > 14 + <DrawerHeader>Left Drawer</DrawerHeader> 15 + <DrawerBody> 16 + <Body>This drawer slides in from the left side.</Body> 17 + </DrawerBody> 18 + </Drawer> 19 + 20 + <Drawer 21 + size="sm" 22 + trigger={<Button>Open Right Drawer</Button>} 23 + direction="right" 24 + > 25 + <DrawerHeader>Right Drawer</DrawerHeader> 26 + <DrawerBody> 27 + <Body>This drawer slides in from the right side.</Body> 28 + </DrawerBody> 29 + </Drawer> 30 + 31 + <Drawer 32 + size="sm" 33 + trigger={<Button>Open Top Drawer</Button>} 34 + direction="top" 35 + > 36 + <DrawerHeader>Top Drawer</DrawerHeader> 37 + <DrawerBody> 38 + <Body>This drawer slides in from the top.</Body> 39 + </DrawerBody> 40 + </Drawer> 41 + 42 + <Drawer 43 + size="sm" 44 + trigger={<Button>Open Bottom Drawer</Button>} 45 + direction="bottom" 46 + > 47 + <DrawerHeader>Bottom Drawer</DrawerHeader> 48 + <DrawerBody> 49 + <Body>This drawer slides in from the bottom.</Body> 50 + </DrawerBody> 51 + </Drawer> 52 + </Flex> 53 + ); 54 + }
+29
apps/docs/src/examples/drawer/form.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Drawer, 4 + DrawerHeader, 5 + DrawerBody, 6 + DrawerFooter, 7 + } from "@/components/drawer"; 8 + import { Flex } from "@/components/flex"; 9 + import { TextField } from "@/components/text-field"; 10 + 11 + export function DrawerForm() { 12 + return ( 13 + <Drawer trigger={<Button>Open Form Drawer</Button>}> 14 + <DrawerHeader>Create New User</DrawerHeader> 15 + <DrawerBody> 16 + <Flex direction="column" gap="4"> 17 + <TextField label="Name" placeholder="Enter full name" /> 18 + <TextField label="Email" placeholder="Enter email address" /> 19 + <TextField label="Phone" placeholder="Enter phone number" /> 20 + </Flex> 21 + </DrawerBody> 22 + <DrawerFooter> 23 + <Button variant="secondary">Cancel</Button> 24 + <Button>Create User</Button> 25 + </DrawerFooter> 26 + </Drawer> 27 + ); 28 + } 29 +
+27
apps/docs/src/examples/drawer/non-modal.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Drawer, 4 + DrawerHeader, 5 + DrawerBody, 6 + DrawerFooter, 7 + } from "@/components/drawer"; 8 + import { Body } from "@/components/typography"; 9 + 10 + export function DrawerNonModal() { 11 + return ( 12 + <Drawer trigger={<Button>Open Non-Modal Drawer</Button>} isNonModal> 13 + <DrawerHeader>Non-Modal Drawer</DrawerHeader> 14 + <DrawerBody> 15 + <Body> 16 + This drawer is non-modal, meaning you can still interact with the page 17 + behind it while it's open. Try clicking outside the drawer or 18 + interacting with other elements on the page. 19 + </Body> 20 + </DrawerBody> 21 + <DrawerFooter> 22 + <Button variant="secondary">Cancel</Button> 23 + <Button>Confirm</Button> 24 + </DrawerFooter> 25 + </Drawer> 26 + ); 27 + }
+41
apps/docs/src/examples/drawer/sizes.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Drawer, 4 + DrawerHeader, 5 + DrawerBody, 6 + } from "@/components/drawer"; 7 + import { Flex } from "@/components/flex"; 8 + import { Body } from "@/components/typography"; 9 + 10 + export function DrawerSizes() { 11 + return ( 12 + <Flex gap="4" wrap> 13 + <Drawer trigger={<Button>Small Drawer</Button>} size="sm"> 14 + <DrawerHeader>Small Drawer</DrawerHeader> 15 + <DrawerBody> 16 + <Body>This is a small drawer with minimal content.</Body> 17 + </DrawerBody> 18 + </Drawer> 19 + 20 + <Drawer trigger={<Button>Medium Drawer</Button>} size="md"> 21 + <DrawerHeader>Medium Drawer</DrawerHeader> 22 + <DrawerBody> 23 + <Body> 24 + This is a medium drawer with more space for content. 25 + </Body> 26 + </DrawerBody> 27 + </Drawer> 28 + 29 + <Drawer trigger={<Button>Large Drawer</Button>} size="lg"> 30 + <DrawerHeader>Large Drawer</DrawerHeader> 31 + <DrawerBody> 32 + <Body> 33 + This is a large drawer with even more space for content. 34 + Perfect for forms, detailed information, or complex layouts. 35 + </Body> 36 + </DrawerBody> 37 + </Drawer> 38 + </Flex> 39 + ); 40 + } 41 +
+1
packages/hip-ui/package.json
··· 31 31 "@react-aria/utils": "^3.31.0", 32 32 "@react-stately/utils": "catalog:", 33 33 "@react-types/overlays": "catalog:", 34 + "@react-types/shared": "^3.32.1", 34 35 "@stylexjs/stylex": "catalog:", 35 36 "change-case": "catalog:", 36 37 "command-line-application": "^0.10.1",
+2
packages/hip-ui/src/cli/install.tsx
··· 34 34 import { dialogConfig } from "../components/dialog/dialog-config.js"; 35 35 import { disclosureGroupConfig } from "../components/disclosure-group/disclosure-group-config.js"; 36 36 import { disclosureConfig } from "../components/disclosure/disclosure-config.js"; 37 + import { drawerConfig } from "../components/drawer/drawer-config.js"; 37 38 import { fileDropZoneConfig } from "../components/file-drop-zone/file-drop-zone-config.js"; 38 39 import { flexConfig } from "../components/flex/flex-config.js"; 39 40 import { formConfig } from "../components/form/form-config.js"; ··· 144 145 calendarConfig, 145 146 rangeCalendarConfig, 146 147 tabsConfig, 148 + drawerConfig, 147 149 ]; 148 150 149 151 function StringSetting({
+1 -4
packages/hip-ui/src/components/dialog/index.tsx
··· 88 88 > 89 89 {trigger} 90 90 91 - <ModalOverlay 92 - isKeyboardDismissDisabled 93 - {...stylex.props(dialogStyles.overlay)} 94 - > 91 + <ModalOverlay {...stylex.props(dialogStyles.overlay)} isDismissable> 95 92 <Modal {...stylex.props(dialogStyles.modal)}> 96 93 <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 97 94 {children}
+34
packages/hip-ui/src/components/drawer/NonModalDrawer.tsx
··· 1 + import { 2 + mergeProps, 3 + useEnterAnimation, 4 + useExitAnimation, 5 + } from "@react-aria/utils"; 6 + import { use, useRef } from "react"; 7 + import { Overlay, useOverlay } from "react-aria"; 8 + import { OverlayTriggerStateContext } from "react-aria-components"; 9 + 10 + export function NonModalDrawer(props: React.ComponentProps<"div">) { 11 + const state = use(OverlayTriggerStateContext); 12 + const modalRef = useRef<HTMLDivElement>(null); 13 + const { overlayProps } = useOverlay( 14 + { isOpen: state?.isOpen, onClose: state?.close }, 15 + modalRef, 16 + ); 17 + const entering = useEnterAnimation(modalRef) || false; 18 + const exiting = useExitAnimation(modalRef, state?.isOpen || false); 19 + 20 + if (!state?.isOpen && !exiting) { 21 + return null; 22 + } 23 + 24 + return ( 25 + <Overlay isExiting={exiting}> 26 + <div 27 + {...mergeProps(overlayProps, props)} 28 + ref={modalRef} 29 + data-entering={entering || undefined} 30 + data-exiting={exiting || undefined} 31 + /> 32 + </Overlay> 33 + ); 34 + }
+20
packages/hip-ui/src/components/drawer/drawer-config.ts
··· 1 + import { ComponentConfig } from "../../types"; 2 + 3 + export const drawerConfig: ComponentConfig = { 4 + name: "drawer", 5 + filepath: "./index.tsx", 6 + hipDependencies: [ 7 + "../context.ts", 8 + "../theme/useDialogStyles.ts", 9 + "../theme/spacing.stylex.tsx", 10 + "../theme/semantic-color.stylex.tsx", 11 + "../theme/typography.stylex.tsx", 12 + "../theme/types.ts", 13 + "./NonModalDrawer.tsx", 14 + ], 15 + dependencies: { 16 + "react-aria-components": "^1.13.0", 17 + "lucide-react": "^0.545.0", 18 + "@react-types/shared": "^3.32.1", 19 + }, 20 + };
+250
packages/hip-ui/src/components/drawer/index.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import { X } from "lucide-react"; 5 + import { 6 + DialogTrigger, 7 + DialogTriggerProps, 8 + Dialog as AriaDialog, 9 + Modal, 10 + ModalOverlay, 11 + Heading, 12 + } from "react-aria-components"; 13 + 14 + import { IconButton } from "../icon-button"; 15 + import { animationDuration, animations } from "../theme/animations.stylex"; 16 + import { uiColor } from "../theme/semantic-color.stylex"; 17 + import { spacing } from "../theme/spacing.stylex"; 18 + import { Size, StyleXComponentProps } from "../theme/types"; 19 + import { typeramp } from "../theme/typography.stylex"; 20 + import { useDialogStyles } from "../theme/useDialogStyles"; 21 + import { 22 + NonModalDrawer, 23 + ModalOverlay as NonModalModalOverlay, 24 + } from "./NonModalDrawer"; 25 + 26 + const styles = stylex.create({ 27 + overlay: { 28 + zIndex: 0, 29 + }, 30 + drawerWrapper: { 31 + bottom: { 32 + ":is([data-direction=bottom])": 0, 33 + ":is([data-direction=left])": 0, 34 + ":is([data-direction=right])": 0, 35 + }, 36 + left: { 37 + ":is([data-direction=left])": 0, 38 + ":is([data-direction=top])": 0, 39 + ":is([data-direction=bottom])": 0, 40 + }, 41 + position: "fixed", 42 + right: { 43 + ":is([data-direction=right])": 0, 44 + ":is([data-direction=top])": 0, 45 + ":is([data-direction=bottom])": 0, 46 + }, 47 + top: { 48 + ":is([data-direction=top])": 0, 49 + ":is([data-direction=left])": 0, 50 + ":is([data-direction=right])": 0, 51 + }, 52 + 53 + borderBottomWidth: { 54 + default: 0, 55 + ":is([data-direction=top])": 1, 56 + }, 57 + borderLeftWidth: { 58 + default: 0, 59 + ":is([data-direction=right])": 1, 60 + }, 61 + borderRadius: 0, 62 + borderRightWidth: { 63 + default: 0, 64 + ":is([data-direction=left])": 1, 65 + }, 66 + borderTopWidth: { 67 + default: 0, 68 + ":is([data-direction=bottom])": 1, 69 + }, 70 + height: { 71 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=sm])": 72 + "320px", 73 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=md])": 74 + "600px", 75 + ":is([data-direction=top], [data-direction=bottom]):is([data-size=lg])": 76 + "800px", 77 + ":is([data-direction=right], [data-direction=left])": "100vh", 78 + }, 79 + maxHeight: { 80 + ":is([data-direction=top], [data-direction=bottom])": `calc(100vh - ${spacing["8"]})`, 81 + ":is([data-direction=right], [data-direction=left])": "100vh", 82 + }, 83 + maxWidth: { 84 + ":is([data-direction=top], [data-direction=bottom])": "100vw", 85 + ":is([data-direction=right], [data-direction=left])": `calc(100vw - ${spacing["8"]})`, 86 + }, 87 + translate: "unset", 88 + width: { 89 + ":is([data-direction=left], [data-direction=right]):is([data-size=sm])": 90 + "320px", 91 + ":is([data-direction=left], [data-direction=right]):is([data-size=md])": 92 + "600px", 93 + ":is([data-direction=left], [data-direction=right]):is([data-size=lg])": 94 + "800px", 95 + ":is([data-direction=top], [data-direction=bottom])": "100vw", 96 + }, 97 + 98 + animationDuration: animationDuration.default, 99 + animationName: { 100 + ":is([data-direction=right][data-entering])": animations.slideInRight, 101 + ":is([data-direction=right][data-exiting])": animations.slideOutRight, 102 + ":is([data-direction=left][data-entering])": animations.slideInLeft, 103 + ":is([data-direction=left][data-exiting])": animations.slideOutLeft, 104 + ":is([data-direction=top][data-entering])": animations.slideInTop, 105 + ":is([data-direction=top][data-exiting])": animations.slideOutTop, 106 + ":is([data-direction=bottom][data-entering])": animations.slideInBottom, 107 + ":is([data-direction=bottom][data-exiting])": animations.slideOutBottom, 108 + }, 109 + animationTimingFunction: "ease-in-out", 110 + }, 111 + dialog: { 112 + paddingBottom: spacing["2"], 113 + paddingTop: spacing["2"], 114 + }, 115 + header: { 116 + alignItems: "center", 117 + display: "flex", 118 + gap: spacing["2"], 119 + height: spacing["8"], 120 + justifyContent: "space-between", 121 + paddingBottom: spacing["2"], 122 + paddingLeft: spacing["4"], 123 + paddingRight: spacing["4"], 124 + }, 125 + description: { 126 + color: uiColor.text1, 127 + paddingBottom: spacing["4"], 128 + paddingLeft: spacing["4"], 129 + paddingRight: spacing["4"], 130 + paddingTop: spacing["4"], 131 + }, 132 + body: { 133 + flexGrow: 1, 134 + paddingBottom: spacing["4"], 135 + paddingLeft: spacing["4"], 136 + paddingRight: spacing["4"], 137 + paddingTop: { default: 0, ":first-child": spacing["4"] }, 138 + }, 139 + footer: { 140 + display: "flex", 141 + gap: spacing["2"], 142 + justifyContent: "flex-end", 143 + paddingBottom: spacing["2"], 144 + paddingLeft: spacing["4"], 145 + paddingRight: spacing["4"], 146 + paddingTop: spacing["4"], 147 + 148 + borderTopColor: uiColor.border1, 149 + borderTopStyle: "solid", 150 + borderTopWidth: 1, 151 + }, 152 + }); 153 + 154 + export interface DrawerProps extends DialogTriggerProps { 155 + trigger: React.ReactNode; 156 + children: React.ReactNode; 157 + size?: Size; 158 + direction?: "left" | "right" | "top" | "bottom"; 159 + isNonModal?: boolean; 160 + } 161 + 162 + export const Drawer = ({ 163 + trigger, 164 + children, 165 + defaultOpen, 166 + isOpen, 167 + onOpenChange, 168 + size = "md", 169 + direction = "right", 170 + isNonModal = false, 171 + }: DrawerProps) => { 172 + const dialogStyles = useDialogStyles({ size }); 173 + 174 + return ( 175 + <DialogTrigger 176 + defaultOpen={defaultOpen} 177 + isOpen={isOpen} 178 + onOpenChange={onOpenChange} 179 + > 180 + {trigger} 181 + 182 + {isNonModal ? ( 183 + <NonModalDrawer 184 + data-size={size} 185 + data-direction={direction} 186 + {...stylex.props(dialogStyles.modal, styles.drawerWrapper)} 187 + > 188 + <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 189 + {children} 190 + </AriaDialog> 191 + </NonModalDrawer> 192 + ) : ( 193 + <ModalOverlay isDismissable> 194 + <div {...stylex.props(dialogStyles.overlay, styles.overlay)} /> 195 + <Modal 196 + data-size={size} 197 + data-direction={direction} 198 + {...stylex.props(dialogStyles.modal, styles.drawerWrapper)} 199 + > 200 + <AriaDialog {...stylex.props(dialogStyles.dialog, styles.dialog)}> 201 + {children} 202 + </AriaDialog> 203 + </Modal> 204 + </ModalOverlay> 205 + )} 206 + </DialogTrigger> 207 + ); 208 + }; 209 + 210 + export interface DrawerHeaderProps 211 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 212 + 213 + export const DrawerHeader = ({ children, style }: DrawerHeaderProps) => { 214 + return ( 215 + <div {...stylex.props(styles.header, typeramp.heading5, style)}> 216 + <Heading>{children}</Heading> 217 + <IconButton label="Close" size="sm" variant="tertiary" slot="close"> 218 + <X /> 219 + </IconButton> 220 + </div> 221 + ); 222 + }; 223 + 224 + export interface DrawerDescriptionProps 225 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 226 + 227 + export const DrawerDescription = ({ 228 + children, 229 + style, 230 + }: DrawerDescriptionProps) => { 231 + return ( 232 + <div {...stylex.props(styles.description, typeramp.body, style)}> 233 + {children} 234 + </div> 235 + ); 236 + }; 237 + 238 + export interface DrawerBodyProps 239 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 240 + 241 + export const DrawerBody = ({ children, style }: DrawerBodyProps) => { 242 + return <div {...stylex.props(styles.body, style)}>{children}</div>; 243 + }; 244 + 245 + export interface DrawerFooterProps 246 + extends StyleXComponentProps<React.ComponentProps<"div">> {} 247 + 248 + export const DrawerFooter = ({ children, style }: DrawerFooterProps) => { 249 + return <div {...stylex.props(styles.footer, style)}>{children}</div>; 250 + };
+80
packages/hip-ui/src/components/theme/animations.stylex.tsx
··· 18 18 }, 19 19 }); 20 20 21 + const slideInRight = stylex.keyframes({ 22 + from: { 23 + transform: "translateX(100%)", 24 + }, 25 + to: { 26 + transform: "translateX(0)", 27 + }, 28 + }); 29 + 30 + const slideOutRight = stylex.keyframes({ 31 + from: { 32 + transform: "translateX(0)", 33 + }, 34 + to: { 35 + transform: "translateX(100%)", 36 + }, 37 + }); 38 + 39 + const slideInLeft = stylex.keyframes({ 40 + from: { 41 + transform: "translateX(-100%)", 42 + }, 43 + to: { 44 + transform: "translateX(0)", 45 + }, 46 + }); 47 + 48 + const slideOutLeft = stylex.keyframes({ 49 + from: { 50 + transform: "translateX(0)", 51 + }, 52 + to: { 53 + transform: "translateX(-100%)", 54 + }, 55 + }); 56 + 57 + const slideInTop = stylex.keyframes({ 58 + from: { 59 + transform: "translateY(-100%)", 60 + }, 61 + to: { 62 + transform: "translateY(0)", 63 + }, 64 + }); 65 + 66 + const slideOutTop = stylex.keyframes({ 67 + from: { 68 + transform: "translateY(0)", 69 + }, 70 + to: { 71 + transform: "translateY(-100%)", 72 + }, 73 + }); 74 + 75 + const slideInBottom = stylex.keyframes({ 76 + from: { 77 + transform: "translateY(100%)", 78 + }, 79 + to: { 80 + transform: "translateY(0)", 81 + }, 82 + }); 83 + 84 + const slideOutBottom = stylex.keyframes({ 85 + from: { 86 + transform: "translateY(0)", 87 + }, 88 + to: { 89 + transform: "translateY(100%)", 90 + }, 91 + }); 92 + 21 93 export const animations = stylex.defineVars({ 22 94 fadeIn, 23 95 zoomIn, 96 + slideInRight, 97 + slideOutRight, 98 + slideInLeft, 99 + slideOutLeft, 100 + slideInTop, 101 + slideOutTop, 102 + slideInBottom, 103 + slideOutBottom, 24 104 }); 25 105 26 106 export const animationDuration = stylex.defineVars({
+1 -8
packages/hip-ui/src/components/theme/useDialogStyles.ts
··· 68 68 69 69 return { 70 70 overlay: styles.overlay, 71 - modal: [ 72 - styles.modal, 73 - styles.modal, 74 - ui.bg, 75 - ui.text, 76 - ui.border, 77 - styles.size(size), 78 - ], 71 + modal: [styles.modal, ui.bg, ui.text, ui.border, styles.size(size)], 79 72 dialog: styles.dialog, 80 73 }; 81 74 }
+3
pnpm-lock.yaml
··· 377 377 '@react-types/overlays': 378 378 specifier: 'catalog:' 379 379 version: 3.9.2(react@19.2.0) 380 + '@react-types/shared': 381 + specifier: ^3.32.1 382 + version: 3.32.1(react@19.2.0) 380 383 '@stylexjs/stylex': 381 384 specifier: 'catalog:' 382 385 version: 0.16.2