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.

Virtualization

+715 -1805
+13 -3
README.md
··· 4 4 5 5 ### Imprrovements 6 6 7 - - [ ] Add Virtualizer 8 - - [ ] add borderless variant for input fields 9 7 - [ ] perfect inset border radii 10 8 - [ ] Number field validation state styling 11 9 ··· 19 17 20 18 - [ ] Carousel 21 19 - [ ] Input OTP 22 - - [ ] Resizable 20 + - [ ] Stepper 21 + - [ ] Lightbox 22 + - [ ] Video 23 + - [ ] [Skip Link](https://component.gallery/components/skip-link/) 24 + - [ ] Page 25 + - [ ] Footer 26 + - [ ] NavBar 27 + - [ ] Hero 23 28 24 29 #### OTher Wrappers 25 30 ··· 28 33 #### react-aria wrappers 29 34 30 35 - [ ] Grid List 36 + - [ ] Virtual Grid 37 + - [ ] Waterfall Layout 31 38 - [ ] Navigation Menu 39 + - [ ] Rating 32 40 33 41 ### Maybe 34 42 ··· 38 46 39 47 ### Done 40 48 49 + - [x] add borderless variant for input fields 50 + - [x] Add Virtualizer 41 51 - [x] Alert / Callout 42 52 - [x] Empty 43 53 - [x] Skeleton
+7 -7
apps/docs/src/components/alert/index.tsx
··· 12 12 13 13 import { SizeContext } from "../context"; 14 14 import { IconButton } from "../icon-button"; 15 + import { 16 + criticalColor, 17 + primaryColor, 18 + successColor, 19 + uiColor, 20 + warningColor, 21 + } from "../theme/color.stylex"; 15 22 import { maxBreakpoints } from "../theme/media-queries.stylex"; 16 23 import { mediaQueries } from "../theme/media-queries.stylex"; 17 24 import { radius } from "../theme/radius.stylex"; ··· 25 32 import { Size, StyleXComponentProps } from "../theme/types"; 26 33 import { fontFamily } from "../theme/typography.stylex"; 27 34 import { Text } from "../typography/text"; 28 - import { 29 - criticalColor, 30 - primaryColor, 31 - successColor, 32 - uiColor, 33 - warningColor, 34 - } from "../theme/color.stylex"; 35 35 36 36 const styles = stylex.create({ 37 37 alert: {
+26 -6
apps/docs/src/components/combobox/index.tsx
··· 10 10 ComboBox as AriaComboBox, 11 11 ComboBoxProps as AriaComboBoxProps, 12 12 Input, 13 + Virtualizer, 14 + ListLayout, 13 15 } from "react-aria-components"; 14 16 15 17 import { SizeContext } from "../context"; ··· 24 26 StyleXComponentProps, 25 27 } from "../theme/types"; 26 28 import { useInputStyles } from "../theme/useInputStyles"; 29 + import { estimatedRowHeights } from "../theme/useListBoxItemStyles"; 27 30 import { usePopoverStyles } from "../theme/usePopoverStyles"; 28 31 import { SmallBody } from "../typography"; 29 32 ··· 64 67 shouldUpdatePosition?: boolean; 65 68 placement?: PopoverProps["placement"]; 66 69 renderEmptyState?: ListBoxProps<T>["renderEmptyState"]; 70 + isVirtualized?: boolean; 67 71 } 68 72 69 73 function ComboBoxContent<T extends object>({ ··· 84 88 shouldUpdatePosition, 85 89 placement, 86 90 renderEmptyState, 91 + isVirtualized, 87 92 }: ComboBoxContentProps<T>) { 88 93 const inputStyles = useInputStyles({ 89 94 size, ··· 92 97 }); 93 98 const popoverStyles = usePopoverStyles(); 94 99 100 + let listbox = ( 101 + <ListBox items={items} renderEmptyState={renderEmptyState || EmptyState}> 102 + {children} 103 + </ListBox> 104 + ); 105 + 106 + if (isVirtualized) { 107 + listbox = ( 108 + <Virtualizer 109 + layout={ListLayout} 110 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 111 + > 112 + {listbox} 113 + </Virtualizer> 114 + ); 115 + } 116 + 95 117 return ( 96 118 <> 97 119 <Label>{label}</Label> ··· 126 148 styles.matchWidth, 127 149 )} 128 150 > 129 - <ListBox 130 - items={items} 131 - renderEmptyState={renderEmptyState || EmptyState} 132 - > 133 - {children} 134 - </ListBox> 151 + {listbox} 135 152 </Popover> 136 153 </> 137 154 ); ··· 160 177 placeholder?: string; 161 178 prefix?: React.ReactNode; 162 179 suffix?: React.ReactNode; 180 + isVirtualized?: boolean; 163 181 } 164 182 165 183 export function ComboBox<T extends object>({ ··· 180 198 prefix, 181 199 suffix, 182 200 renderEmptyState, 201 + isVirtualized = false, 183 202 ...props 184 203 }: ComboBoxProps<T>) { 185 204 const size = sizeProp || use(SizeContext); ··· 210 229 shouldUpdatePosition={shouldUpdatePosition} 211 230 placement={placement} 212 231 renderEmptyState={renderEmptyState} 232 + isVirtualized={isVirtualized} 213 233 > 214 234 {children} 215 235 </ComboBoxContent>
+77 -30
apps/docs/src/components/empty-state/index.tsx
··· 11 11 12 12 const styles = stylex.create({ 13 13 emptyState: { 14 - "--empty-state-gap": { 15 - ":is([data-empty-state-size=lg])": spacing["8"], 16 - ":is([data-empty-state-size=md])": spacing["6"], 17 - ":is([data-empty-state-size=sm])": spacing["4"], 14 + display: "grid", 15 + gridTemplateColumns: { 16 + ":is([data-empty-state-size=sm])": { 17 + ":has([data-empty-state-actions])": "min-content 1fr max-content", 18 + default: "min-content 1fr", 19 + }, 18 20 }, 19 - "--empty-state-image-size": { 20 - ":is([data-empty-state-size=lg])": "240px", 21 - ":is([data-empty-state-size=md])": "180px", 22 - ":is([data-empty-state-size=sm])": "120px", 21 + gridTemplateAreas: { 22 + ":is([data-empty-state-size=md],[data-empty-state-size=lg])": { 23 + default: ` 24 + "image" 25 + "title" 26 + "description" 27 + `, 28 + ":has([data-empty-state-actions])": ` 29 + "image" 30 + "title" 31 + "description" 32 + "actions" 33 + `, 34 + }, 35 + ":is([data-empty-state-size=sm])": { 36 + default: ` 37 + "image title" 38 + "image description" 39 + `, 40 + ":has([data-empty-state-actions])": ` 41 + "image title actions" 42 + "image description actions" 43 + `, 44 + }, 23 45 }, 24 - gap: "var(--empty-state-gap)", 25 46 alignItems: "center", 26 - display: "flex", 27 - flexDirection: "column", 47 + justifyItems: { 48 + ":is([data-empty-state-size=md],[data-empty-state-size=lg])": "center", 49 + ":is([data-empty-state-size=sm])": "start", 50 + }, 28 51 fontFamily: fontFamily["sans"], 29 - justifyContent: "center", 30 52 textAlign: "center", 53 + columnGap: { 54 + ":is([data-empty-state-size=sm])": spacing["4"], 55 + }, 56 + rowGap: { 57 + ":is([data-empty-state-size=lg])": spacing["6"], 58 + ":is([data-empty-state-size=md])": spacing["4"], 59 + ":is([data-empty-state-size=sm])": spacing["2"], 60 + }, 61 + "--empty-state-image-size": { 62 + ":is([data-empty-state-size=lg])": spacing["20"], 63 + ":is([data-empty-state-size=md])": spacing["14"], 64 + ":is([data-empty-state-size=sm])": spacing["10"], 65 + }, 31 66 }, 32 67 image: { 33 - alignItems: "center", 68 + gridArea: "image", 69 + width: "var(--empty-state-image-size)", 70 + height: "var(--empty-state-image-size)", 71 + objectFit: "contain", 34 72 display: "flex", 73 + alignItems: "center", 35 74 justifyContent: "center", 36 - objectFit: "contain", 37 - height: "var(--empty-state-image-size)", 38 - width: "var(--empty-state-image-size)", 39 75 }, 40 76 title: { 41 - margin: 0, 77 + gridArea: "title", 42 78 fontSize: { 43 79 ":is([data-empty-state-size='lg'] *)": fontSize["2xl"], 44 80 ":is([data-empty-state-size='md'] *)": fontSize["xl"], 45 81 ":is([data-empty-state-size='sm'] *)": fontSize["lg"], 46 82 }, 47 83 fontWeight: fontWeight["semibold"], 84 + margin: 0, 48 85 }, 49 86 description: { 50 - margin: 0, 87 + gridArea: "description", 51 88 fontSize: fontSize["sm"], 52 89 fontWeight: fontWeight["normal"], 90 + margin: 0, 53 91 maxWidth: { 54 92 ":is([data-empty-state-size=lg])": "480px", 55 93 ":is([data-empty-state-size=md])": "400px", ··· 57 95 }, 58 96 }, 59 97 actions: { 60 - gap: spacing["2"], 61 - alignItems: "center", 98 + gridArea: "actions", 62 99 display: "flex", 63 100 flexDirection: "row", 101 + gap: spacing["2"], 102 + alignItems: "center", 103 + justifyContent: "center", 64 104 flexWrap: "wrap", 65 - justifyContent: "center", 105 + 106 + paddingLeft: { 107 + ":is([data-empty-state-size=sm] *)": spacing["4"], 108 + }, 66 109 }, 67 110 }); 68 111 ··· 83 126 const size = sizeProp || use(SizeContext); 84 127 85 128 return ( 86 - <SizeContext value={size}> 87 - <div 88 - {...props} 89 - data-empty-state-size={size} 90 - {...stylex.props(styles.emptyState, style)} 91 - /> 92 - </SizeContext> 129 + <div 130 + {...props} 131 + data-empty-state-size={size} 132 + {...stylex.props(styles.emptyState, style)} 133 + /> 93 134 ); 94 135 }; 95 136 ··· 131 172 extends StyleXComponentProps<React.ComponentProps<"h2">> {} 132 173 133 174 export const EmptyStateTitle = ({ style, ...props }: EmptyStateTitleProps) => { 134 - return <div {...props} {...stylex.props(styles.title, ui.text, style)} />; 175 + return <h2 {...props} {...stylex.props(styles.title, ui.text, style)} />; 135 176 }; 136 177 137 178 export interface EmptyStateDescriptionProps ··· 153 194 style, 154 195 ...props 155 196 }: EmptyStateActionsProps) => { 156 - return <div {...props} {...stylex.props(styles.actions, style)} />; 197 + return ( 198 + <div 199 + {...props} 200 + data-empty-state-actions 201 + {...stylex.props(styles.actions, style)} 202 + /> 203 + ); 157 204 };
+21 -2
apps/docs/src/components/listbox/index.tsx
··· 11 11 Header, 12 12 SeparatorProps, 13 13 ListStateContext, 14 + Virtualizer, 15 + ListLayout, 14 16 } from "react-aria-components"; 15 17 16 18 import { Checkbox, CheckboxProps } from "../checkbox"; ··· 20 22 import { spacing } from "../theme/spacing.stylex"; 21 23 import { Size, StyleXComponentProps } from "../theme/types"; 22 24 import { typeramp } from "../theme/typography.stylex"; 23 - import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; 25 + import { 26 + estimatedRowHeights, 27 + useListBoxItemStyles, 28 + } from "../theme/useListBoxItemStyles"; 24 29 25 30 const styles = stylex.create({ 26 31 listBox: { ··· 57 62 items?: Iterable<T>; 58 63 children: React.ReactNode | ((item: T) => React.ReactNode); 59 64 variant?: ListBoxVariant; 65 + isVirtualized?: boolean; 60 66 } 61 67 62 68 export function ListBox<T extends object>({ 63 69 size: sizeProp, 64 70 style, 65 71 variant = "default", 72 + isVirtualized = false, 66 73 ...props 67 74 }: ListBoxProps<T>) { 68 75 const size = sizeProp || use(SizeContext); 76 + const listbox = ( 77 + <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 78 + ); 69 79 70 80 return ( 71 81 <ListboxVariantContext value={variant}> 72 82 <SizeContext value={size}> 73 - <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 83 + {isVirtualized ? ( 84 + <Virtualizer 85 + layout={ListLayout} 86 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 87 + > 88 + {listbox} 89 + </Virtualizer> 90 + ) : ( 91 + listbox 92 + )} 74 93 </SizeContext> 75 94 </ListboxVariantContext> 76 95 );
+34 -11
apps/docs/src/components/select/index.tsx
··· 12 12 Select as AriaSelect, 13 13 Autocomplete, 14 14 useFilter, 15 + ListLayout, 16 + Virtualizer, 15 17 } from "react-aria-components"; 16 18 17 19 import { SizeContext } from "../context"; ··· 27 29 StyleXComponentProps, 28 30 } from "../theme/types"; 29 31 import { useInputStyles } from "../theme/useInputStyles"; 32 + import { estimatedRowHeights } from "../theme/useListBoxItemStyles"; 30 33 import { usePopoverStyles } from "../theme/usePopoverStyles"; 31 34 32 35 const styles = stylex.create({ ··· 57 60 shouldFlip?: boolean; 58 61 shouldUpdatePosition?: boolean; 59 62 placement?: PopoverProps["placement"]; 63 + isVirtualized?: boolean; 60 64 } 61 65 62 66 function SelectContent<T extends object>({ 67 + isVirtualized, 63 68 label, 64 69 description, 65 70 errorMessage, ··· 86 91 const popoverStyles = usePopoverStyles(); 87 92 const { contains } = useFilter({ sensitivity: "base" }); 88 93 94 + let listbox = <ListBox items={items}>{children}</ListBox>; 95 + 96 + if (isVirtualized) { 97 + listbox = ( 98 + <Virtualizer 99 + layout={ListLayout} 100 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 101 + > 102 + {listbox} 103 + </Virtualizer> 104 + ); 105 + } 106 + 107 + if (isSearchable) { 108 + listbox = ( 109 + <Autocomplete filter={contains}> 110 + <div {...stylex.props(styles.searchField)}> 111 + <SearchField placeholder="Search" variant="secondary" /> 112 + </div> 113 + <ListBoxSeparator /> 114 + {listbox} 115 + </Autocomplete> 116 + ); 117 + } 118 + 89 119 return ( 90 120 <> 91 121 <Label>{label}</Label> ··· 127 157 styles.matchWidth, 128 158 )} 129 159 > 130 - {isSearchable ? ( 131 - <Autocomplete filter={contains}> 132 - <div {...stylex.props(styles.searchField)}> 133 - <SearchField placeholder="Search" variant="secondary" /> 134 - </div> 135 - <ListBoxSeparator /> 136 - <ListBox items={items}>{children}</ListBox> 137 - </Autocomplete> 138 - ) : ( 139 - <ListBox items={items}>{children}</ListBox> 140 - )} 160 + {listbox} 141 161 </Popover> 142 162 </> 143 163 ); ··· 166 186 prefix?: React.ReactNode; 167 187 suffix?: React.ReactNode; 168 188 isSearchable?: boolean; 189 + isVirtualized?: boolean; 169 190 } 170 191 171 192 export function Select< ··· 189 210 prefix, 190 211 suffix, 191 212 isSearchable = false, 213 + isVirtualized = false, 192 214 ...props 193 215 }: SelectProps<T, M>) { 194 216 const size = sizeProp || use(SizeContext); ··· 204 226 > 205 227 {({ isInvalid }) => ( 206 228 <SelectContent 229 + isVirtualized={isVirtualized} 207 230 label={label} 208 231 description={description} 209 232 errorMessage={errorMessage}
+31 -10
apps/docs/src/components/table/index.tsx
··· 19 19 ColumnResizer, 20 20 DropIndicatorProps, 21 21 DropIndicator, 22 + TableLayout, 23 + Virtualizer, 22 24 } from "react-aria-components"; 23 25 24 26 import { Checkbox } from "../checkbox"; ··· 155 157 }, 156 158 }); 157 159 160 + const estimatedRowHeights: Record<Size, number> = { 161 + sm: 24, 162 + md: 32, 163 + lg: 40, 164 + }; 165 + 158 166 export interface TableProps extends StyleXComponentProps<AriaTableProps> { 159 167 size?: Size; 168 + isVirtualized?: boolean; 160 169 } 161 170 162 - export const Table = ({ style, size: sizeProp, ...props }: TableProps) => { 171 + export const Table = ({ 172 + style, 173 + size: sizeProp, 174 + isVirtualized = false, 175 + ...props 176 + }: TableProps) => { 163 177 const size = sizeProp || use(SizeContext); 178 + let table = <AriaTable {...props} {...stylex.props(styles.table, style)} />; 164 179 165 - return ( 166 - <SizeContext value={size}> 167 - <AriaTable 168 - data-table-size={size} 169 - {...stylex.props(styles.table, style)} 170 - {...props} 171 - /> 172 - </SizeContext> 173 - ); 180 + if (isVirtualized) { 181 + table = ( 182 + <Virtualizer 183 + layout={TableLayout} 184 + layoutOptions={{ 185 + estimatedRowHeight: estimatedRowHeights[size], 186 + headingHeight: estimatedRowHeights[size], 187 + }} 188 + > 189 + {table} 190 + </Virtualizer> 191 + ); 192 + } 193 + 194 + return <SizeContext value={size}>{table}</SizeContext>; 174 195 }; 175 196 176 197 export interface TableColumnProps
+7
apps/docs/src/components/theme/useListBoxItemStyles.ts
··· 13 13 import { animationDuration } from "./animations.stylex"; 14 14 import { criticalColor, primaryColor, uiColor } from "./color.stylex"; 15 15 import { mediaQueries } from "./media-queries.stylex"; 16 + import { Size } from "./types"; 16 17 17 18 const styles = stylex.create({ 18 19 item: { ··· 108 109 flexGrow: 1, 109 110 }, 110 111 }); 112 + 113 + export const estimatedRowHeights: Record<Size, number> = { 114 + sm: 24, 115 + md: 32, 116 + lg: 40, 117 + }; 111 118 112 119 export function useListBoxItemStyles() { 113 120 const size = use(SizeContext);
+21 -6
apps/docs/src/components/tree/index.tsx
··· 3 3 import { use } from "react"; 4 4 import { 5 5 Button, 6 + Virtualizer, 6 7 Tree as AriaTree, 7 8 TreeProps as AriaTreeProps, 8 9 TreeItemContent as AriaTreeItemContent, 9 10 TreeItem as AriaTreeItem, 10 11 TreeItemProps as AriaTreeItemProps, 11 12 TreeItemContentProps as AriaTreeItemContentProps, 13 + ListLayout, 12 14 } from "react-aria-components"; 13 15 14 16 import { Checkbox } from "../checkbox"; ··· 20 22 import { ui } from "../theme/semantic-color.stylex"; 21 23 import { spacing } from "../theme/spacing.stylex"; 22 24 import { Size, StyleXComponentProps } from "../theme/types"; 23 - import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; 25 + import { 26 + estimatedRowHeights, 27 + useListBoxItemStyles, 28 + } from "../theme/useListBoxItemStyles"; 24 29 25 30 const styles = stylex.create({ 26 31 wrapper: { ··· 225 230 extends StyleXComponentProps<Omit<AriaTreeProps<T>, "children">> { 226 231 children: React.ReactNode | ((item: T) => React.ReactNode); 227 232 size?: Size; 233 + isVirtualized?: boolean; 228 234 } 229 235 230 236 export function Tree<T extends object>({ 231 237 style, 232 238 size: sizeProp, 239 + isVirtualized = false, 233 240 ...props 234 241 }: TreeProps<T>) { 235 242 const size = sizeProp || use(SizeContext); 243 + let tree = <AriaTree {...props} {...stylex.props(style)} />; 236 244 237 - return ( 238 - <SizeContext value={size}> 239 - <AriaTree {...props} {...stylex.props(style)} /> 240 - </SizeContext> 241 - ); 245 + if (isVirtualized) { 246 + tree = ( 247 + <Virtualizer 248 + layout={ListLayout} 249 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 250 + > 251 + {tree} 252 + </Virtualizer> 253 + ); 254 + } 255 + 256 + return <SizeContext value={size}>{tree}</SizeContext>; 242 257 }
+10
apps/docs/src/docs/components/collections/listbox.mdx
··· 6 6 import { PropDocs } from '../../../lib/PropDocs' 7 7 import { Example } from '../../../lib/Example' 8 8 import { Basic } from '../../../examples/listbox/basic' 9 + import { Virtualization } from '../../../examples/listbox/virtualization' 9 10 10 11 <Example src={Basic} /> 11 12 ··· 22 23 This component is built using the [React Aria ListBox](https://react-spectrum.adobe.com/react-aria/ListBox.html). 23 24 24 25 <PropDocs components={["ListBox", "ListBoxItem", "ListBoxSection", "ListBoxSeparator", "ListBoxSectionHeader"]} /> 26 + 27 + ## Features 28 + 29 + ### Virtualization 30 + 31 + The list box supports virtualization by providing the `isVirtualized` prop. 32 + The container must have a fixed height and `overflow: auto`. 33 + 34 + <Example src={Virtualization} /> 25 35 26 36 ## Related Components 27 37
+8
apps/docs/src/docs/components/collections/table.mdx
··· 11 11 import { DisabledRows } from '../../../examples/table/disabled-rows' 12 12 import { Sorting } from '../../../examples/table/sorting' 13 13 import { DragAndDrop } from '../../../examples/table/drag-and-drop' 14 + import { Virtualization } from '../../../examples/table/virtualization' 14 15 15 16 <Example src={Basic} /> 16 17 ··· 83 84 A row can be disabled by setting the `isDisabled` prop to true. 84 85 85 86 <Example src={DisabledRows} /> 87 + 88 + ### Virtualization 89 + 90 + The table supports virtualization by providing the `isVirtualized` prop. 91 + The container must have a fixed height and `overflow: auto`. 92 + 93 + <Example src={Virtualization} /> 86 94 87 95 ## Related Components 88 96
+10 -2
apps/docs/src/docs/components/collections/tree.mdx
··· 7 7 import { Example } from '../../../lib/Example' 8 8 import { Basic } from '../../../examples/tree/basic' 9 9 import { DragAndDrop } from '../../../examples/tree/drag-and-drop' 10 + import { Virtualization } from '../../../examples/tree/virtualization' 10 11 11 12 <Example src={Basic} /> 12 13 ··· 24 25 25 26 <PropDocs components={["Tree", "TreeItem"]} /> 26 27 27 - ### Features 28 + ## Features 28 29 29 - #### Drag and Drop 30 + ### Drag and Drop 30 31 31 32 The tree supports drag and drop by providing the `dragAndDropHooks` prop to the `Tree` component. 32 33 React more about it [here](https://react-spectrum.adobe.com/react-aria/Tree.html#drag-and-drop). 33 34 34 35 <Example src={DragAndDrop} /> 36 + 37 + ### Virtualization 38 + 39 + The tree supports virtualization by providing the `isVirtualized` prop. 40 + The container must have a fixed height and `overflow: auto`. 41 + 42 + <Example src={Virtualization} /> 35 43 36 44 ## Related Components 37 45
+8
apps/docs/src/docs/components/form/combobox.mdx
··· 9 9 import { Validation } from '../../../examples/combobox/validation' 10 10 import { ValidationWarning } from '../../../examples/combobox/validation-warning' 11 11 import { ValidationSuccess } from '../../../examples/combobox/validation-success' 12 + import { Virtualization } from '../../../examples/combobox/virtualization' 12 13 13 14 <Example src={Basic} /> 14 15 ··· 42 43 Set `validationState="valid"` to show a success state with a description. 43 44 44 45 <Example src={ValidationSuccess} /> 46 + 47 + ### Virtualization 48 + 49 + The combo box supports virtualization by providing the `isVirtualized` prop. 50 + The container must have a fixed height and `overflow: auto`. 51 + 52 + <Example src={Virtualization} /> 45 53 46 54 ## Related Components 47 55
+8
apps/docs/src/docs/components/form/select.mdx
··· 11 11 import { Validation } from '../../../examples/select/validation' 12 12 import { ValidationWarning } from '../../../examples/select/validation-warning' 13 13 import { ValidationSuccess } from '../../../examples/select/validation-success' 14 + import { Virtualization } from '../../../examples/select/virtualization' 14 15 15 16 <Example src={Basic} /> 16 17 ··· 56 57 Set `validationState="valid"` to show a success state with a description. 57 58 58 59 <Example src={ValidationSuccess} /> 60 + 61 + ### Virtualization 62 + 63 + The select supports virtualization by providing the `isVirtualized` prop. 64 + The container must have a fixed height and `overflow: auto`. 65 + 66 + <Example src={Virtualization} /> 59 67 60 68 ## Related Components 61 69
+24
apps/docs/src/examples/combobox/virtualization.tsx
··· 1 + import { ComboBox, ComboBoxItem } from "@/components/combobox"; 2 + 3 + const options = Array.from({ length: 1000 }, (_, i) => ({ 4 + id: `option-${i + 1}`, 5 + name: `Option ${i + 1}`, 6 + })); 7 + 8 + export function Virtualization() { 9 + return ( 10 + <ComboBox 11 + label="Choose an option" 12 + placeholder="Type to search..." 13 + items={options} 14 + isVirtualized 15 + > 16 + {(item) => ( 17 + <ComboBoxItem key={item.id} id={item.id}> 18 + {item.name} 19 + </ComboBoxItem> 20 + )} 21 + </ComboBox> 22 + ); 23 + } 24 +
+31
apps/docs/src/examples/listbox/virtualization.tsx
··· 1 + import { ListBox, ListBoxItem } from "@/components/listbox"; 2 + import { uiColor } from "../../components/theme/color.stylex"; 3 + import * as stylex from "@stylexjs/stylex"; 4 + 5 + const styles = stylex.create({ 6 + listBox: { 7 + height: "300px", 8 + width: "300px", 9 + borderWidth: 1, 10 + borderColor: uiColor.border1, 11 + borderStyle: "solid", 12 + overflow: "auto", 13 + }, 14 + }); 15 + 16 + const items = Array.from({ length: 1000 }, (_, i) => ({ 17 + id: `item-${i + 1}`, 18 + name: `Item ${i + 1}`, 19 + })); 20 + 21 + export function Virtualization() { 22 + return ( 23 + <ListBox items={items} isVirtualized style={styles.listBox}> 24 + {(item) => ( 25 + <ListBoxItem key={item.id} id={item.id}> 26 + {item.name} 27 + </ListBoxItem> 28 + )} 29 + </ListBox> 30 + ); 31 + }
+19
apps/docs/src/examples/select/virtualization.tsx
··· 1 + import { Select, SelectItem } from "@/components/select"; 2 + 3 + const options = Array.from({ length: 1000 }, (_, i) => ({ 4 + id: `option-${i + 1}`, 5 + name: `Option ${i + 1}`, 6 + })); 7 + 8 + export function Virtualization() { 9 + return ( 10 + <Select 11 + label="Choose an option" 12 + placeholder="Select an option" 13 + items={options} 14 + isVirtualized 15 + > 16 + {(item) => <SelectItem id={item.id}>{item.name}</SelectItem>} 17 + </Select> 18 + ); 19 + }
+65
apps/docs/src/examples/table/virtualization.tsx
··· 1 + import * as stylex from "@stylexjs/stylex"; 2 + 3 + import { 4 + Table, 5 + TableHeader, 6 + TableBody, 7 + TableColumn, 8 + TableRow, 9 + TableCell, 10 + } from "@/components/table"; 11 + import { uiColor } from "../../components/theme/color.stylex"; 12 + 13 + const styles = stylex.create({ 14 + wrapper: { 15 + height: "400px", 16 + width: "100%", 17 + borderWidth: 1, 18 + borderColor: uiColor.border1, 19 + borderStyle: "solid", 20 + overflow: "auto", 21 + }, 22 + table: { 23 + width: "100%", 24 + }, 25 + }); 26 + 27 + const columns = [ 28 + { id: "id", name: "ID" }, 29 + { id: "name", name: "Name" }, 30 + { id: "email", name: "Email" }, 31 + { id: "role", name: "Role" }, 32 + ]; 33 + 34 + const rows = Array.from({ length: 1000 }, (_, i) => ({ 35 + id: i + 1, 36 + name: `User ${i + 1}`, 37 + email: `user${i + 1}@example.com`, 38 + role: i % 3 === 0 ? "Admin" : i % 3 === 1 ? "Moderator" : "User", 39 + })); 40 + 41 + export function Virtualization() { 42 + return ( 43 + <div {...stylex.props(styles.wrapper)}> 44 + <Table isVirtualized style={styles.table}> 45 + <TableHeader> 46 + {columns.map((column) => ( 47 + <TableColumn isRowHeader key={column.id} id={column.id}> 48 + {column.name} 49 + </TableColumn> 50 + ))} 51 + </TableHeader> 52 + <TableBody items={rows}> 53 + {(item) => ( 54 + <TableRow key={item.id} id={item.id}> 55 + <TableCell>{item.id}</TableCell> 56 + <TableCell>{item.name}</TableCell> 57 + <TableCell>{item.email}</TableCell> 58 + <TableCell>{item.role}</TableCell> 59 + </TableRow> 60 + )} 61 + </TableBody> 62 + </Table> 63 + </div> 64 + ); 65 + }
+40
apps/docs/src/examples/tree/virtualization.tsx
··· 1 + import { Tree, TreeItem } from "@/components/tree"; 2 + import { uiColor } from "../../components/theme/color.stylex"; 3 + import * as stylex from "@stylexjs/stylex"; 4 + 5 + const styles = stylex.create({ 6 + tree: { 7 + height: "300px", 8 + width: "300px", 9 + borderWidth: 1, 10 + borderColor: uiColor.border1, 11 + borderStyle: "solid", 12 + overflow: "auto", 13 + }, 14 + }); 15 + 16 + const treeData = Array.from({ length: 100 }, (_, i) => ({ 17 + id: `folder-${i + 1}`, 18 + name: `Folder ${i + 1}`, 19 + children: Array.from({ length: 20 }, (_, j) => ({ 20 + id: `folder-${i + 1}-file-${j + 1}`, 21 + name: `File ${j + 1}`, 22 + })), 23 + })); 24 + 25 + export function Virtualization() { 26 + return ( 27 + <Tree items={treeData} isVirtualized style={styles.tree}> 28 + {function renderTreeItem(item) { 29 + return ( 30 + <TreeItem key={item.id} id={item.id} title={item.name}> 31 + {item.children.map((child) => ( 32 + <TreeItem key={child.id} id={child.id} title={child.name} /> 33 + ))} 34 + </TreeItem> 35 + ); 36 + }} 37 + </Tree> 38 + ); 39 + } 40 +
+3 -45
apps/docs/src/routeTree.gen.ts
··· 12 12 import { Route as DocsRouteImport } from './routes/docs' 13 13 import { Route as IndexRouteImport } from './routes/index' 14 14 import { Route as DocsSplatRouteImport } from './routes/docs.$' 15 - import { Route as DocsInvoiceAppRouteImport } from './routes/_docs.invoice-app' 16 - import { Route as DocsEcommerceAppRouteImport } from './routes/_docs.ecommerce-app' 17 15 18 16 const DocsRoute = DocsRouteImport.update({ 19 17 id: '/docs', ··· 29 27 id: '/$', 30 28 path: '/$', 31 29 getParentRoute: () => DocsRoute, 32 - } as any) 33 - const DocsInvoiceAppRoute = DocsInvoiceAppRouteImport.update({ 34 - id: '/_docs/invoice-app', 35 - path: '/invoice-app', 36 - getParentRoute: () => rootRouteImport, 37 - } as any) 38 - const DocsEcommerceAppRoute = DocsEcommerceAppRouteImport.update({ 39 - id: '/_docs/ecommerce-app', 40 - path: '/ecommerce-app', 41 - getParentRoute: () => rootRouteImport, 42 30 } as any) 43 31 44 32 export interface FileRoutesByFullPath { 45 33 '/': typeof IndexRoute 46 34 '/docs': typeof DocsRouteWithChildren 47 - '/ecommerce-app': typeof DocsEcommerceAppRoute 48 - '/invoice-app': typeof DocsInvoiceAppRoute 49 35 '/docs/$': typeof DocsSplatRoute 50 36 } 51 37 export interface FileRoutesByTo { 52 38 '/': typeof IndexRoute 53 39 '/docs': typeof DocsRouteWithChildren 54 - '/ecommerce-app': typeof DocsEcommerceAppRoute 55 - '/invoice-app': typeof DocsInvoiceAppRoute 56 40 '/docs/$': typeof DocsSplatRoute 57 41 } 58 42 export interface FileRoutesById { 59 43 __root__: typeof rootRouteImport 60 44 '/': typeof IndexRoute 61 45 '/docs': typeof DocsRouteWithChildren 62 - '/_docs/ecommerce-app': typeof DocsEcommerceAppRoute 63 - '/_docs/invoice-app': typeof DocsInvoiceAppRoute 64 46 '/docs/$': typeof DocsSplatRoute 65 47 } 66 48 export interface FileRouteTypes { 67 49 fileRoutesByFullPath: FileRoutesByFullPath 68 - fullPaths: '/' | '/docs' | '/ecommerce-app' | '/invoice-app' | '/docs/$' 50 + fullPaths: '/' | '/docs' | '/docs/$' 69 51 fileRoutesByTo: FileRoutesByTo 70 - to: '/' | '/docs' | '/ecommerce-app' | '/invoice-app' | '/docs/$' 71 - id: 72 - | '__root__' 73 - | '/' 74 - | '/docs' 75 - | '/_docs/ecommerce-app' 76 - | '/_docs/invoice-app' 77 - | '/docs/$' 52 + to: '/' | '/docs' | '/docs/$' 53 + id: '__root__' | '/' | '/docs' | '/docs/$' 78 54 fileRoutesById: FileRoutesById 79 55 } 80 56 export interface RootRouteChildren { 81 57 IndexRoute: typeof IndexRoute 82 58 DocsRoute: typeof DocsRouteWithChildren 83 - DocsEcommerceAppRoute: typeof DocsEcommerceAppRoute 84 - DocsInvoiceAppRoute: typeof DocsInvoiceAppRoute 85 59 } 86 60 87 61 declare module '@tanstack/react-router' { ··· 107 81 preLoaderRoute: typeof DocsSplatRouteImport 108 82 parentRoute: typeof DocsRoute 109 83 } 110 - '/_docs/invoice-app': { 111 - id: '/_docs/invoice-app' 112 - path: '/invoice-app' 113 - fullPath: '/invoice-app' 114 - preLoaderRoute: typeof DocsInvoiceAppRouteImport 115 - parentRoute: typeof rootRouteImport 116 - } 117 - '/_docs/ecommerce-app': { 118 - id: '/_docs/ecommerce-app' 119 - path: '/ecommerce-app' 120 - fullPath: '/ecommerce-app' 121 - preLoaderRoute: typeof DocsEcommerceAppRouteImport 122 - parentRoute: typeof rootRouteImport 123 - } 124 84 } 125 85 } 126 86 ··· 137 97 const rootRouteChildren: RootRouteChildren = { 138 98 IndexRoute: IndexRoute, 139 99 DocsRoute: DocsRouteWithChildren, 140 - DocsEcommerceAppRoute: DocsEcommerceAppRoute, 141 - DocsInvoiceAppRoute: DocsInvoiceAppRoute, 142 100 } 143 101 export const routeTree = rootRouteImport 144 102 ._addFileChildren(rootRouteChildren)
-663
apps/docs/src/routes/_docs.ecommerce-app.tsx
··· 1 - import * as stylex from "@stylexjs/stylex"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 - import { Bookmark, Shredder, Upload } from "lucide-react"; 4 - import { Fragment } from "react/jsx-runtime"; 5 - 6 - import { AspectRatio, AspectRatioImage } from "@/components/aspect-ratio"; 7 - import { Avatar } from "@/components/avatar"; 8 - import { Badge } from "@/components/badge"; 9 - import { Button } from "@/components/button"; 10 - import { 11 - Card, 12 - CardBody, 13 - CardDescription, 14 - CardFooter, 15 - CardHeader, 16 - CardHeaderAction, 17 - CardImage, 18 - CardTitle, 19 - } from "@/components/card"; 20 - import { ColorSwatch } from "@/components/color-swatch"; 21 - import { FileDropZone } from "@/components/file-drop-zone"; 22 - import { Flex } from "@/components/flex"; 23 - import { Grid } from "@/components/grid"; 24 - import { IconButton } from "@/components/icon-button"; 25 - import { Link } from "@/components/link"; 26 - import { NumberField } from "@/components/number-field"; 27 - import { Select, SelectItem } from "@/components/select"; 28 - import { Separator } from "@/components/separator"; 29 - import { TextArea } from "@/components/text-area"; 30 - import { TextField } from "@/components/text-field"; 31 - import { ToggleButton } from "@/components/toggle-button"; 32 - import { ToggleButtonGroup } from "@/components/toggle-button-group"; 33 - import { LabelText, SmallBody } from "@/components/typography"; 34 - import { Text } from "@/components/typography/text"; 35 - 36 - import { spacing } from "../components/theme/spacing.stylex"; 37 - 38 - const styles = stylex.create({ 39 - heightFull: { 40 - height: "100%", 41 - }, 42 - grow: { 43 - flexBasis: "0%", 44 - flexGrow: 1, 45 - flexShrink: 0, 46 - minWidth: 0, 47 - }, 48 - medium: { 49 - flexBasis: "0%", 50 - flexGrow: 0.75, 51 - flexShrink: 0, 52 - }, 53 - skinny: { 54 - flexBasis: "0%", 55 - flexGrow: 0.5, 56 - flexShrink: 0, 57 - }, 58 - relative: { 59 - position: "relative", 60 - }, 61 - bottomRight: { 62 - bottom: 0, 63 - marginBottom: spacing["4"], 64 - marginRight: spacing["4"], 65 - position: "absolute", 66 - right: 0, 67 - }, 68 - }); 69 - 70 - export const Route = createFileRoute("/_docs/ecommerce-app")({ 71 - component: RouteComponent, 72 - }); 73 - 74 - function SmallProductCard() { 75 - return ( 76 - <Card size="sm"> 77 - <CardImage src="https://images.unsplash.com/photo-1620799140408-edc6dcb6d633?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=560&h=424&q=80" /> 78 - <CardHeader> 79 - <CardTitle>Back to basics</CardTitle> 80 - <CardDescription>Simple and versatile</CardDescription> 81 - <CardHeaderAction> 82 - <Button variant="secondary">Show now</Button> 83 - </CardHeaderAction> 84 - </CardHeader> 85 - </Card> 86 - ); 87 - } 88 - 89 - function SmallProductCardWithBuying() { 90 - return ( 91 - <Card size="sm"> 92 - <div {...stylex.props(styles.relative)}> 93 - <CardImage src="https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=560&h=540&q=80" /> 94 - <div {...stylex.props(styles.bottomRight)}> 95 - <IconButton label="Bookmark" variant="secondary"> 96 - <Bookmark /> 97 - </IconButton> 98 - </div> 99 - </div> 100 - <CardBody> 101 - <Flex direction="column" gap="4"> 102 - <Flex direction="column" gap="2"> 103 - <Text size="sm">Footwear</Text> 104 - <Text weight="semibold">Sneakers #12</Text> 105 - <Text size="sm" variant="secondary"> 106 - Love at the first sight for enthusiasts seeking a fresh and 107 - whimsical style. 108 - </Text> 109 - </Flex> 110 - <Separator /> 111 - <Flex align="end" gap="2"> 112 - <Select 113 - label="Color" 114 - defaultValue="red" 115 - style={styles.grow} 116 - variant="secondary" 117 - > 118 - <SelectItem id="red">Red</SelectItem> 119 - <SelectItem id="blue">Blue</SelectItem> 120 - <SelectItem id="green">Green</SelectItem> 121 - <SelectItem id="yellow">Yellow</SelectItem> 122 - <SelectItem id="purple">Purple</SelectItem> 123 - <SelectItem id="orange">Orange</SelectItem> 124 - <SelectItem id="pink">Pink</SelectItem> 125 - <SelectItem id="brown">Brown</SelectItem> 126 - <SelectItem id="gray">Gray</SelectItem> 127 - </Select> 128 - <Select 129 - label="Size" 130 - defaultValue="8" 131 - style={styles.grow} 132 - variant="secondary" 133 - > 134 - <SelectItem id="8">8</SelectItem> 135 - <SelectItem id="9">9</SelectItem> 136 - <SelectItem id="10">10</SelectItem> 137 - <SelectItem id="11">11</SelectItem> 138 - <SelectItem id="12">12</SelectItem> 139 - <SelectItem id="13">13</SelectItem> 140 - <SelectItem id="14">14</SelectItem> 141 - <SelectItem id="15">15</SelectItem> 142 - <SelectItem id="16">16</SelectItem> 143 - </Select> 144 - <Button>Buy</Button> 145 - </Flex> 146 - </Flex> 147 - </CardBody> 148 - </Card> 149 - ); 150 - } 151 - 152 - function ProductOptionsCard() { 153 - return ( 154 - <Card size="sm"> 155 - <CardBody> 156 - <Flex direction="column" gap="5"> 157 - <Flex direction="column" gap="2"> 158 - <Text weight="semibold">Delivery</Text> 159 - <ToggleButtonGroup 160 - variant="separate" 161 - itemsPerRow={2} 162 - selectionMode="single" 163 - > 164 - <ToggleButton id="tomorrow" variant="secondary"> 165 - Tomorrow 166 - </ToggleButton> 167 - <ToggleButton id="within-3-days" variant="secondary"> 168 - Within 3 days 169 - </ToggleButton> 170 - </ToggleButtonGroup> 171 - </Flex> 172 - <Flex direction="column" gap="2"> 173 - <Text weight="semibold">Size</Text> 174 - <ToggleButtonGroup 175 - variant="separate" 176 - itemsPerRow={5} 177 - selectionMode="single" 178 - > 179 - <ToggleButton id="5.5" variant="secondary"> 180 - 5.5 181 - </ToggleButton> 182 - <ToggleButton id="6" variant="secondary"> 183 - 6 184 - </ToggleButton> 185 - <ToggleButton id="6.5" variant="secondary"> 186 - 6.5 187 - </ToggleButton> 188 - <ToggleButton id="7" variant="secondary"> 189 - 7 190 - </ToggleButton> 191 - <ToggleButton id="7.5" variant="secondary"> 192 - 7.5 193 - </ToggleButton> 194 - <ToggleButton id="8" variant="secondary"> 195 - 8 196 - </ToggleButton> 197 - <ToggleButton id="8.5" variant="secondary"> 198 - 8.5 199 - </ToggleButton> 200 - <ToggleButton id="9" variant="secondary"> 201 - 9 202 - </ToggleButton> 203 - <ToggleButton id="9.5" variant="secondary"> 204 - 9.5 205 - </ToggleButton> 206 - <ToggleButton id="10" variant="secondary"> 207 - 10 208 - </ToggleButton> 209 - </ToggleButtonGroup> 210 - </Flex> 211 - <Flex direction="column" gap="2"> 212 - <Text weight="semibold">Material</Text> 213 - <ToggleButtonGroup 214 - variant="separate" 215 - itemsPerRow={5} 216 - selectionMode="single" 217 - > 218 - <ToggleButton id="leather" variant="secondary"> 219 - Leather 220 - </ToggleButton> 221 - <ToggleButton id="suede" variant="secondary"> 222 - Suede 223 - </ToggleButton> 224 - <ToggleButton id="mesh" variant="secondary"> 225 - Mesh 226 - </ToggleButton> 227 - <ToggleButton id="canvas" variant="secondary"> 228 - Canvas 229 - </ToggleButton> 230 - </ToggleButtonGroup> 231 - </Flex> 232 - <Flex direction="column" gap="2"> 233 - <Text weight="semibold">Color</Text> 234 - <ToggleButtonGroup 235 - variant="separate" 236 - itemsPerRow={3} 237 - selectionMode="single" 238 - > 239 - <ToggleButton id="white" variant="secondary"> 240 - <ColorSwatch color="#fff" size="sm" /> 241 - White 242 - </ToggleButton> 243 - <ToggleButton id="grey" variant="secondary"> 244 - <ColorSwatch color="#808080" size="sm" /> 245 - Grey 246 - </ToggleButton> 247 - <ToggleButton id="black" variant="secondary"> 248 - <ColorSwatch color="#000" size="sm" /> 249 - Black 250 - </ToggleButton> 251 - <ToggleButton id="red" variant="secondary"> 252 - <ColorSwatch color="#f00" size="sm" /> 253 - Red 254 - </ToggleButton> 255 - <ToggleButton id="pink" variant="secondary"> 256 - <ColorSwatch color="#f0f" size="sm" /> 257 - Pink 258 - </ToggleButton> 259 - <ToggleButton id="violet" variant="secondary"> 260 - <ColorSwatch color="#800080" size="sm" /> 261 - Violet 262 - </ToggleButton> 263 - <ToggleButton id="blue" variant="secondary"> 264 - <ColorSwatch color="#00f" size="sm" /> 265 - Blue 266 - </ToggleButton> 267 - <ToggleButton id="green" variant="secondary"> 268 - <ColorSwatch color="#0f0" size="sm" /> 269 - Green 270 - </ToggleButton> 271 - <ToggleButton id="beige" variant="secondary"> 272 - <ColorSwatch color="#f5f5dc" size="sm" /> 273 - Beige 274 - </ToggleButton> 275 - </ToggleButtonGroup> 276 - </Flex> 277 - </Flex> 278 - </CardBody> 279 - </Card> 280 - ); 281 - } 282 - 283 - function ShoppingCartCard() { 284 - const cart = [ 285 - { 286 - title: "Poncho #4", 287 - image: 288 - "https://images.unsplash.com/photo-1434389677669-e08b4cac3105?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=80&h=80&q=80&crop=entropy", 289 - price: 79, 290 - size: "M", 291 - count: 1, 292 - }, 293 - { 294 - title: "Jeans #8", 295 - image: 296 - "https://images.unsplash.com/photo-1602293589930-45aad59ba3ab?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=80&h=80&q=80&crop=entropy", 297 - price: 59, 298 - size: "30", 299 - count: 2, 300 - }, 301 - { 302 - title: "Sneakers #14", 303 - image: 304 - "https://images.unsplash.com/photo-1549298916-b41d501d3772?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=80&h=80&q=80&crop=center", 305 - price: 116, 306 - size: "8", 307 - count: 1, 308 - }, 309 - ]; 310 - 311 - return ( 312 - <Card size="sm"> 313 - <CardHeader> 314 - <CardTitle>Shopping Cart</CardTitle> 315 - </CardHeader> 316 - <CardBody> 317 - <Flex direction="column" gap="4"> 318 - <Grid columns="auto 1fr auto auto" columnGap="3" alignItems="center"> 319 - {cart.map((item) => ( 320 - <Fragment key={item.title}> 321 - <Avatar 322 - src={item.image} 323 - alt={item.title} 324 - fallback={item.title.charAt(0)} 325 - size="lg" 326 - /> 327 - <Flex direction="column" gap="1"> 328 - <Text weight="medium">{item.title}</Text> 329 - <Text variant="secondary" size="sm"> 330 - Size {item.size} 331 - </Text> 332 - </Flex> 333 - <Select defaultValue={item.count.toString()}> 334 - <SelectItem id="1">1</SelectItem> 335 - <SelectItem id="2">2</SelectItem> 336 - <SelectItem id="3">3</SelectItem> 337 - <SelectItem id="4">4</SelectItem> 338 - <SelectItem id="5">5</SelectItem> 339 - <SelectItem id="6">6</SelectItem> 340 - <SelectItem id="7">7</SelectItem> 341 - <SelectItem id="8">8</SelectItem> 342 - <SelectItem id="9">9</SelectItem> 343 - <SelectItem id="10">10</SelectItem> 344 - </Select> 345 - <Text size="sm" variant="secondary"> 346 - {Intl.NumberFormat("en-US", { 347 - style: "currency", 348 - currency: "USD", 349 - }).format(item.price * item.count)} 350 - </Text> 351 - </Fragment> 352 - ))} 353 - </Grid> 354 - <Separator /> 355 - <Flex gap="2" justify="between"> 356 - <div></div> 357 - <Button>Go to checkout</Button> 358 - </Flex> 359 - </Flex> 360 - </CardBody> 361 - </Card> 362 - ); 363 - } 364 - 365 - function Delivery() { 366 - return ( 367 - <Card size="sm"> 368 - <Flex direction="column" gap="5"> 369 - <CardHeader> 370 - <CardTitle>Delivery</CardTitle> 371 - <CardHeaderAction> 372 - <Badge variant="warning" size="sm"> 373 - Guaranteed 374 - </Badge> 375 - </CardHeaderAction> 376 - </CardHeader> 377 - <CardBody> 378 - <Flex direction="column" gap="1.5"> 379 - <Text weight="semibold">Tomorrow</Text> 380 - <Text variant="secondary" size="sm"> 381 - 12:00 pm - 2:00 pm 382 - </Text> 383 - </Flex> 384 - 385 - <Flex direction="column" gap="1.5"> 386 - <Text weight="semibold">Luna Rodriguez</Text> 387 - <Text variant="secondary" size="sm"> 388 - 9876 Maple Avenue 389 - <br /> 390 - Cityville, WA 54321 391 - </Text> 392 - </Flex> 393 - </CardBody> 394 - <CardImage src="https://workos.imgix.net/images/bc04b345-f225-488d-8a46-1811096d0c3b.png?auto=format&fit=clip&q=90&w=840&h=654" /> 395 - <CardFooter> 396 - <Button variant="secondary">Edit</Button> 397 - <Button>Confirm</Button> 398 - </CardFooter> 399 - </Flex> 400 - </Card> 401 - ); 402 - } 403 - 404 - function Bookmarks() { 405 - const bookmarks = [ 406 - { 407 - title: "Jeans #8", 408 - image: 409 - "https://images.unsplash.com/photo-1602293589930-45aad59ba3ab?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=272&h=272&q=80&crop=entropy", 410 - price: 118, 411 - }, 412 - { 413 - title: "Jacket #3", 414 - image: 415 - "https://images.unsplash.com/photo-1591047139829-d91aecb6caea?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&crop=entropy&w=272&h=272&q=80", 416 - price: 49, 417 - }, 418 - { 419 - title: "Pants #10", 420 - image: 421 - "https://images.unsplash.com/photo-1506629082955-511b1aa562c8?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=272&h=272&q=80", 422 - price: 32, 423 - }, 424 - { 425 - title: "Shirt #11", 426 - image: 427 - "https://images.unsplash.com/photo-1611312449412-6cefac5dc3e4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=272&h=272&q=80", 428 - price: 39, 429 - }, 430 - ]; 431 - 432 - return ( 433 - <Card size="sm"> 434 - <CardHeader> 435 - <CardTitle>Bookmarks</CardTitle> 436 - <CardHeaderAction> 437 - <Button variant="tertiary">Buy all</Button> 438 - </CardHeaderAction> 439 - </CardHeader> 440 - <CardBody> 441 - <Grid columns="1fr 1fr" columnGap="2"> 442 - {bookmarks.map((bookmark) => ( 443 - <Flex direction="column" gap="2" key={bookmark.title}> 444 - <AspectRatio aspectRatio={1}> 445 - <AspectRatioImage src={bookmark.image} /> 446 - </AspectRatio> 447 - <div> 448 - <Text weight="medium" size="sm"> 449 - {bookmark.title} 450 - </Text> 451 - <Text variant="secondary" size="sm"> 452 - {", "} 453 - {Intl.NumberFormat("en-US", { 454 - style: "currency", 455 - currency: "USD", 456 - }).format(bookmark.price)} 457 - </Text> 458 - </div> 459 - </Flex> 460 - ))} 461 - </Grid> 462 - </CardBody> 463 - </Card> 464 - ); 465 - } 466 - 467 - function DiscardedCard() { 468 - return ( 469 - <Card> 470 - <CardBody> 471 - <Flex direction="column" gap="4" align="center"> 472 - <Shredder size={48} /> 473 - <Text weight="semibold" size="lg"> 474 - Product discarded 475 - </Text> 476 - <SmallBody variant="secondary"> 477 - It's still available in the <Link>archive.</Link> 478 - </SmallBody> 479 - <Flex gap="2"> 480 - <Button variant="secondary">Undo</Button> 481 - <Button>Done</Button> 482 - </Flex> 483 - </Flex> 484 - </CardBody> 485 - </Card> 486 - ); 487 - } 488 - 489 - function EditProductCard() { 490 - return ( 491 - <Card> 492 - <CardHeader> 493 - <CardTitle>Edit product</CardTitle> 494 - </CardHeader> 495 - <CardBody> 496 - <Grid columns="1fr 140px" columnGap="2"> 497 - <TextField label="Title" defaultValue="Sneakers #12" /> 498 - <NumberField 499 - label="Price" 500 - defaultValue={116} 501 - formatOptions={{ 502 - style: "currency", 503 - currency: "USD", 504 - }} 505 - /> 506 - </Grid> 507 - <Flex direction="column" gap="2"> 508 - <LabelText>Media</LabelText> 509 - <Grid columns="1fr 1fr 1fr" columnGap="2"> 510 - <AspectRatio> 511 - <AspectRatioImage src="https://images.unsplash.com/photo-1551163943-3f6a855d1153?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=400&h=400&q=80&crop=bottom" /> 512 - </AspectRatio> 513 - <AspectRatio> 514 - <AspectRatioImage src="https://workos.imgix.net/images/c773ee38-9136-49d1-804c-6d166dad9c65.png?auto=format&fit=clip&q=80w=400&h=400" /> 515 - </AspectRatio> 516 - <AspectRatio> 517 - <FileDropZone style={styles.heightFull}> 518 - <IconButton variant="secondary" label="Upload image"> 519 - <Upload /> 520 - </IconButton> 521 - </FileDropZone> 522 - </AspectRatio> 523 - </Grid> 524 - </Flex> 525 - <TextArea 526 - label="Description" 527 - defaultValue="This is a description of the product." 528 - rows={4} 529 - /> 530 - <Flex direction="column" gap="2"> 531 - <Text weight="semibold">Material</Text> 532 - <ToggleButtonGroup 533 - variant="separate" 534 - itemsPerRow={3} 535 - selectionMode="single" 536 - > 537 - <ToggleButton id="synthetic" variant="secondary"> 538 - Synthetic 539 - </ToggleButton> 540 - <ToggleButton id="wool" variant="secondary"> 541 - Wool 542 - </ToggleButton> 543 - <ToggleButton id="cotton" variant="secondary"> 544 - Cotton 545 - </ToggleButton> 546 - <ToggleButton id="linen" variant="secondary"> 547 - Linen 548 - </ToggleButton> 549 - <ToggleButton id="denim" variant="secondary"> 550 - Denim 551 - </ToggleButton> 552 - <ToggleButton id="leather" variant="secondary"> 553 - Leather 554 - </ToggleButton> 555 - <ToggleButton id="silk" variant="secondary"> 556 - Silk 557 - </ToggleButton> 558 - <ToggleButton id="chiffon" variant="secondary"> 559 - Chiffon 560 - </ToggleButton> 561 - <ToggleButton id="other" variant="secondary"> 562 - Other 563 - </ToggleButton> 564 - </ToggleButtonGroup> 565 - </Flex> 566 - <Flex direction="column" gap="2"> 567 - <Text weight="semibold">Main Color</Text> 568 - <ToggleButtonGroup 569 - variant="separate" 570 - itemsPerRow={3} 571 - selectionMode="single" 572 - > 573 - <ToggleButton id="white" variant="secondary"> 574 - <ColorSwatch color="#fff" size="sm" /> 575 - White 576 - </ToggleButton> 577 - <ToggleButton id="grey" variant="secondary"> 578 - <ColorSwatch color="#808080" size="sm" /> 579 - Grey 580 - </ToggleButton> 581 - <ToggleButton id="black" variant="secondary"> 582 - <ColorSwatch color="#000" size="sm" /> 583 - Black 584 - </ToggleButton> 585 - <ToggleButton id="red" variant="secondary"> 586 - <ColorSwatch color="#f00" size="sm" /> 587 - Red 588 - </ToggleButton> 589 - <ToggleButton id="pink" variant="secondary"> 590 - <ColorSwatch color="#f0f" size="sm" /> 591 - Pink 592 - </ToggleButton> 593 - <ToggleButton id="violet" variant="secondary"> 594 - <ColorSwatch color="#800080" size="sm" /> 595 - Violet 596 - </ToggleButton> 597 - <ToggleButton id="blue" variant="secondary"> 598 - <ColorSwatch color="#00f" size="sm" /> 599 - Blue 600 - </ToggleButton> 601 - <ToggleButton id="green" variant="secondary"> 602 - <ColorSwatch color="#0f0" size="sm" /> 603 - Green 604 - </ToggleButton> 605 - <ToggleButton id="beige" variant="secondary"> 606 - <ColorSwatch color="#f5f5dc" size="sm" /> 607 - Beige 608 - </ToggleButton> 609 - </ToggleButtonGroup> 610 - </Flex> 611 - <Flex direction="column" gap="2"> 612 - <Text weight="semibold">Size</Text> 613 - <ToggleButtonGroup 614 - variant="separate" 615 - itemsPerRow={3} 616 - selectionMode="single" 617 - > 618 - <ToggleButton id="xs" variant="secondary"> 619 - XS 620 - </ToggleButton> 621 - <ToggleButton id="s" variant="secondary"> 622 - S 623 - </ToggleButton> 624 - <ToggleButton id="m" variant="secondary"> 625 - M 626 - </ToggleButton> 627 - <ToggleButton id="l" variant="secondary"> 628 - L 629 - </ToggleButton> 630 - <ToggleButton id="xl" variant="secondary"> 631 - XL 632 - </ToggleButton> 633 - <ToggleButton id="xxl" variant="secondary"> 634 - XXL 635 - </ToggleButton> 636 - </ToggleButtonGroup> 637 - </Flex> 638 - </CardBody> 639 - </Card> 640 - ); 641 - } 642 - 643 - function RouteComponent() { 644 - return ( 645 - <Flex gap="4"> 646 - <Flex direction="column" gap="4" style={styles.skinny}> 647 - <SmallProductCard /> 648 - <SmallProductCardWithBuying /> 649 - <ProductOptionsCard /> 650 - </Flex> 651 - <Flex direction="column" gap="4" style={styles.skinny}> 652 - <Delivery /> 653 - <Bookmarks /> 654 - <ShoppingCartCard /> 655 - </Flex> 656 - <Flex direction="column" gap="4" style={styles.medium}> 657 - <DiscardedCard /> 658 - <EditProductCard /> 659 - </Flex> 660 - <Flex direction="column" gap="4" style={styles.grow}></Flex> 661 - </Flex> 662 - ); 663 - }
-938
apps/docs/src/routes/_docs.invoice-app.tsx
··· 1 - import * as stylex from "@stylexjs/stylex"; 2 - import { createFileRoute } from "@tanstack/react-router"; 3 - import { 4 - ArrowDown, 5 - ArrowUp, 6 - CheckCircle, 7 - CheckCircle2, 8 - Copy, 9 - ExternalLink, 10 - MoreHorizontal, 11 - Pin, 12 - Plus, 13 - Share, 14 - X, 15 - } from "lucide-react"; 16 - import { Fragment } from "react/jsx-runtime"; 17 - 18 - import { Avatar } from "@/components/avatar"; 19 - import { Badge } from "@/components/badge"; 20 - import { Button } from "@/components/button"; 21 - import { 22 - Card, 23 - CardBody, 24 - CardDescription, 25 - CardFooter, 26 - CardHeader, 27 - CardTitle, 28 - } from "@/components/card"; 29 - import { Checkbox } from "@/components/checkbox"; 30 - import { Flex } from "@/components/flex"; 31 - import { Grid, GridItem } from "@/components/grid"; 32 - import { IconButton } from "@/components/icon-button"; 33 - import { Link } from "@/components/link"; 34 - import { Menu, MenuItem, MenuSeparator } from "@/components/menu"; 35 - import { Separator } from "@/components/separator"; 36 - import { Switch } from "@/components/switch"; 37 - import { TextField } from "@/components/text-field"; 38 - import { ToggleButton } from "@/components/toggle-button"; 39 - import { Body, Heading5, SmallBody } from "@/components/typography"; 40 - import { Text } from "@/components/typography/text"; 41 - 42 - import { radius } from "../components/theme/radius.stylex"; 43 - import { ui, primary } from "../components/theme/semantic-color.stylex"; 44 - import { primaryColor, successColor } from "../components/theme/color.stylex"; 45 - import { spacing } from "../components/theme/spacing.stylex"; 46 - import { fontFamily, typeramp } from "../components/theme/typography.stylex"; 47 - 48 - const styles = stylex.create({ 49 - relative: { 50 - position: "relative", 51 - }, 52 - textCenter: { 53 - textAlign: "center", 54 - }, 55 - grow: { 56 - flexBasis: "0%", 57 - flexGrow: 1, 58 - flexShrink: 0, 59 - minWidth: 0, 60 - }, 61 - skinny: { 62 - flexBasis: "0%", 63 - flexGrow: 0.75, 64 - flexShrink: 0, 65 - }, 66 - pinnedButtons: { 67 - marginRight: spacing["4"], 68 - marginTop: spacing["4"], 69 - position: "absolute", 70 - right: 0, 71 - top: 0, 72 - }, 73 - invoiceCard: { 74 - padding: spacing["6"], 75 - }, 76 - check: { 77 - color: successColor.solid2, 78 - }, 79 - creditCardWrapper: { 80 - borderRadius: { 81 - default: radius["lg"], 82 - "@supports (corner-shape: squircle)": radius["4xl"], 83 - }, 84 - cornerShape: "squircle", 85 - padding: spacing["8"], 86 - }, 87 - creditCard: { 88 - backgroundImage: `linear-gradient(135deg, ${primaryColor.solid2} 0%, ${primaryColor.text1} 100%)`, 89 - borderRadius: { 90 - default: radius["lg"], 91 - "@supports (corner-shape: squircle)": radius["4xl"], 92 - }, 93 - cornerShape: "squircle", 94 - fontFamily: fontFamily["mono"], 95 - height: spacing["40"], 96 - padding: spacing["4"], 97 - }, 98 - copyCardNumber: { 99 - backgroundColor: { 100 - default: "transparent", 101 - ":hover": "rgba(255, 255, 255, 0.3)", 102 - }, 103 - color: "white", 104 - }, 105 - }); 106 - 107 - export const Route = createFileRoute("/_docs/invoice-app")({ 108 - component: RouteComponent, 109 - }); 110 - 111 - function YourTeam() { 112 - const members = [ 113 - { 114 - name: "Oliver Chen", 115 - email: "oliver.chen@example.com", 116 - avatar: 117 - "https://images.unsplash.com/photo-1544005313-94ddf0286df2?&w=64&h=64&dpr=2&q=70&crop=faces&fit=crop", 118 - }, 119 - { 120 - name: "Amelia Rodriguez", 121 - email: "amelia.r@example.com", 122 - avatar: 123 - "https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?&w=64&h=64&dpr=2&q=70&crop=faces&fit=crop", 124 - }, 125 - { 126 - name: "Theodore Kim", 127 - email: "theo.kim@example.com", 128 - avatar: 129 - "https://images.unsplash.com/photo-1526510747491-58f928ec870f?&w=64&h=64&dpr=2&q=70&crop=focalpoint&fp-x=0.48&fp-y=0.48&fp-z=1.3&fit=crop", 130 - }, 131 - { 132 - name: "Sofia Patel", 133 - email: "sofia.patel@example.com", 134 - avatar: 135 - "https://images.unsplash.com/photo-1541823709867-1b206113eafd?&w=64&h=64&dpr=2&q=70&crop=focalpoint&fp-x=0.5&fp-y=0.3&fp-z=1.5&fit=crop", 136 - }, 137 - { 138 - name: "Mateo Garcia", 139 - email: "m.garcia@example.com", 140 - avatar: 141 - "https://images.unsplash.com/photo-1532073150508-0c1df022bdd1?&w=64&h=64&dpr=2&q=70&crop=focalpoint&fp-x=0.48&fp-y=0.35&fp-z=2&fit=crop", 142 - }, 143 - ]; 144 - 145 - return ( 146 - <Card> 147 - <CardHeader> 148 - <CardTitle>Your Team</CardTitle> 149 - <CardDescription>Invite and manage your team members.</CardDescription> 150 - </CardHeader> 151 - <CardBody> 152 - <Flex direction="column" gap="8"> 153 - <Flex align="center" gap="2"> 154 - <TextField 155 - aria-label="Email" 156 - placeholder="you@example.com" 157 - style={styles.grow} 158 - /> 159 - <Button>Invite</Button> 160 - </Flex> 161 - 162 - <Grid 163 - columns="auto max-content 1fr auto" 164 - columnGap="4" 165 - rowGap="5" 166 - alignItems="center" 167 - > 168 - {members.map((member, index) => ( 169 - <Fragment key={member.email}> 170 - <Avatar 171 - size="lg" 172 - src={member.avatar} 173 - fallback={member.name.charAt(0)} 174 - /> 175 - <Link href={`mailto:${member.email}`}>{member.name}</Link> 176 - <SmallBody variant="secondary">{member.email}</SmallBody> 177 - <Menu 178 - trigger={ 179 - <IconButton label="More" variant="tertiary"> 180 - <MoreHorizontal /> 181 - </IconButton> 182 - } 183 - > 184 - <MenuItem>View profile</MenuItem> 185 - <MenuItem>Change role</MenuItem> 186 - <MenuSeparator /> 187 - <MenuItem variant="destructive">Remove</MenuItem> 188 - </Menu> 189 - {index < members.length - 1 && ( 190 - <GridItem columnStart={1} columnEnd={-1}> 191 - <Separator /> 192 - </GridItem> 193 - )} 194 - </Fragment> 195 - ))} 196 - </Grid> 197 - </Flex> 198 - </CardBody> 199 - </Card> 200 - ); 201 - } 202 - 203 - function Pricing() { 204 - return ( 205 - <Card> 206 - <CardHeader> 207 - <CardTitle>Pricing</CardTitle> 208 - <CardDescription> 209 - No credit card required. Every plan includes a 30-day trial of all Pro 210 - features. 211 - </CardDescription> 212 - </CardHeader> 213 - <CardBody> 214 - <Flex gap="8"> 215 - <Flex direction="column" gap="5" style={styles.grow}> 216 - <Flex direction="column" gap="2"> 217 - <Text weight="semibold" size="xl"> 218 - Basic 219 - </Text> 220 - <Text size="sm" variant="secondary"> 221 - 3 team members 222 - </Text> 223 - </Flex> 224 - <Text weight="semibold" size="xl"> 225 - $0<Text variant="secondary">{" / mo"}</Text> 226 - </Text> 227 - <Flex direction="column" gap="2"> 228 - <Flex align="center" gap="2"> 229 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 230 - <Text size="sm">Expense tracking</Text> 231 - </Flex> 232 - <Flex align="center" gap="2"> 233 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 234 - <Text size="sm">Invoicing</Text> 235 - </Flex> 236 - <Flex align="center" gap="2"> 237 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 238 - <Text size="sm">Payment tracking</Text> 239 - </Flex> 240 - <Flex align="center" gap="2"> 241 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 242 - <Text size="sm">Transaction recording</Text> 243 - </Flex> 244 - <Flex align="center" gap="2"> 245 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 246 - <Text size="sm">Basic reports</Text> 247 - </Flex> 248 - <Flex align="center" gap="2"> 249 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 250 - <Text size="sm">Email support</Text> 251 - </Flex> 252 - </Flex> 253 - <Button variant="outline">Downgrade</Button> 254 - </Flex> 255 - <Flex direction="column" gap="5" style={styles.grow}> 256 - <Flex direction="column" gap="2"> 257 - <Text weight="semibold" size="xl"> 258 - Growth 259 - </Text> 260 - <Text size="sm" variant="secondary"> 261 - 10 team members 262 - </Text> 263 - </Flex> 264 - <Text weight="semibold" size="xl"> 265 - $49<Text variant="secondary">{" / mo"}</Text> 266 - </Text> 267 - <Flex direction="column" gap="2"> 268 - <Flex align="center" gap="2"> 269 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 270 - <Text size="sm">Online payments</Text> 271 - </Flex> 272 - <Flex align="center" gap="2"> 273 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 274 - <Text size="sm">Recurring invoices</Text> 275 - </Flex> 276 - <Flex align="center" gap="2"> 277 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 278 - <Text size="sm">Bill management</Text> 279 - </Flex> 280 - <Flex align="center" gap="2"> 281 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 282 - <Text size="sm">Inventory tracking</Text> 283 - </Flex> 284 - <Flex align="center" gap="2"> 285 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 286 - <Text size="sm">Detailed reports</Text> 287 - </Flex> 288 - <Flex align="center" gap="2"> 289 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 290 - <Text size="sm">Phone support</Text> 291 - </Flex> 292 - </Flex> 293 - <Button variant="outline">Go to billing</Button> 294 - </Flex> 295 - <Flex direction="column" gap="5" style={styles.grow}> 296 - <Flex direction="column" gap="2"> 297 - <Text weight="semibold" size="xl"> 298 - Pro 299 - </Text> 300 - <Text size="sm" variant="secondary"> 301 - Unlimited team members 302 - </Text> 303 - </Flex> 304 - <Text weight="semibold" size="xl"> 305 - $99<Text variant="secondary">{" / mo"}</Text> 306 - </Text> 307 - <Flex direction="column" gap="2"> 308 - <Flex align="center" gap="2"> 309 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 310 - <Text size="sm">Custom invoices</Text> 311 - </Flex> 312 - <Flex align="center" gap="2"> 313 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 314 - <Text size="sm">Multi-business</Text> 315 - </Flex> 316 - <Flex align="center" gap="2"> 317 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 318 - <Text size="sm">Team collaboration</Text> 319 - </Flex> 320 - <Flex align="center" gap="2"> 321 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 322 - <Text size="sm">App integrations</Text> 323 - </Flex> 324 - <Flex align="center" gap="2"> 325 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 326 - <Text size="sm">Advanced security</Text> 327 - </Flex> 328 - <Flex align="center" gap="2"> 329 - <CheckCircle {...stylex.props(styles.check)} size={16} /> 330 - <Text size="sm">Priority support</Text> 331 - </Flex> 332 - </Flex> 333 - <Button>Upgrade</Button> 334 - </Flex> 335 - </Flex> 336 - </CardBody> 337 - </Card> 338 - ); 339 - } 340 - 341 - function Notifications() { 342 - const sections = [ 343 - { 344 - title: "Comments", 345 - description: 346 - "Receive notifications when someone comments on your documents or mentions you.", 347 - }, 348 - { 349 - title: "Favorites", 350 - description: 351 - "Receive notifications when there is activity related to your favorited items.", 352 - }, 353 - { 354 - title: "New documents", 355 - description: 356 - "Receive notifications whenever people on your team create new documents.", 357 - }, 358 - ]; 359 - 360 - return ( 361 - <Card> 362 - <CardHeader> 363 - <CardTitle>Notifications</CardTitle> 364 - <CardDescription>Manage your notification settings.</CardDescription> 365 - </CardHeader> 366 - <CardBody> 367 - <Grid columns="1fr auto" rowGap="6" alignItems="start"> 368 - {sections.map((section, index) => ( 369 - <Fragment key={section.title}> 370 - <Flex direction="column" gap="1"> 371 - <Heading5>{section.title}</Heading5> 372 - <SmallBody variant="secondary">{section.description}</SmallBody> 373 - </Flex> 374 - <Flex direction="column" gap="2"> 375 - <Switch>Push</Switch> 376 - <Switch>Email</Switch> 377 - <Switch isDisabled>Slack</Switch> 378 - </Flex> 379 - {index < sections.length - 1 && ( 380 - <GridItem columnStart={1} columnEnd={-1}> 381 - <Separator /> 382 - </GridItem> 383 - )} 384 - </Fragment> 385 - ))} 386 - </Grid> 387 - </CardBody> 388 - </Card> 389 - ); 390 - } 391 - 392 - function RecentActivity() { 393 - const activities = [ 394 - { 395 - name: "Oliver Chen", 396 - timestamp: "06-08-2025 10:00 AM", 397 - description: ( 398 - <> 399 - Approved invoice <Link href="#">#3461</Link>{" "} 400 - </> 401 - ), 402 - avatar: 403 - "https://images.unsplash.com/photo-1521119989659-a83eee488004?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.45&amp;fp-y=0.37&amp;fp-z=3.5&amp;fit=crop", 404 - }, 405 - { 406 - name: "Amelia Rodriguez", 407 - timestamp: "06-08-2025 10:00 AM", 408 - description: ( 409 - <> 410 - Purchased <Link href="#">15 office chairs</Link>{" "} 411 - </> 412 - ), 413 - avatar: 414 - "https://images.unsplash.com/photo-1632765854612-9b02b6ec2b15?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.4&amp;fp-y=0.35&amp;fp-z=1.05&amp;fit=crop", 415 - }, 416 - { 417 - name: "Theodore Kim", 418 - timestamp: "06-08-2025 10:00 AM", 419 - avatar: 420 - "https://images.unsplash.com/photo-1632765854612-9b02b6ec2b15?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.4&amp;fp-y=0.35&amp;fp-z=1.05&amp;fit=crop", 421 - description: ( 422 - <> 423 - Responded to your commente <Link href="#">#7514</Link>{" "} 424 - </> 425 - ), 426 - }, 427 - { 428 - name: "Jasper Eriksson", 429 - timestamp: "06-08-2025 10:00 AM", 430 - avatar: 431 - "https://images.unsplash.com/photo-1586822339087-80cc375ac083?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.5&amp;fp-y=0.6&amp;fp-z=1&amp;fit=crop", 432 - description: ( 433 - <> 434 - Created <Link href="#">4 invoices</Link>{" "} 435 - </> 436 - ), 437 - }, 438 - { 439 - name: "Travis Ross", 440 - timestamp: "06-08-2025 10:00 AM", 441 - avatar: 442 - "https://images.unsplash.com/photo-1564564321837-a57b7070ac4f?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.52&amp;fp-y=0.47&amp;fp-z=1.3&amp;fit=crop", 443 - description: ( 444 - <> 445 - Updated client details for <Link href="#">Acme Co.</Link>{" "} 446 - </> 447 - ), 448 - }, 449 - { 450 - name: "Gizela Kavková", 451 - timestamp: "06-08-2025 10:00 AM", 452 - avatar: 453 - "https://images.unsplash.com/photo-1525304937537-4d586f394674?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=faces&amp;fit=crop", 454 - description: ( 455 - <> 456 - Created <Link href="#">4 invoices</Link>{" "} 457 - </> 458 - ), 459 - }, 460 - { 461 - name: "Gizela Kavková", 462 - timestamp: "06-08-2025 8:00 AM", 463 - avatar: 464 - "https://images.unsplash.com/photo-1525304937537-4d586f394674?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=faces&amp;fit=crop", 465 - description: ( 466 - <> 467 - Deleted report <Link href="#">#1234</Link>{" "} 468 - </> 469 - ), 470 - }, 471 - { 472 - name: "Da-Xia Wu", 473 - timestamp: "06-08-2025 8:00 AM", 474 - avatar: 475 - "https://images.unsplash.com/photo-1541823709867-1b206113eafd?&amp;w=64&amp;h=64&amp;dpr=2&amp;q=70&amp;crop=focalpoint&amp;fp-x=0.5&amp;fp-y=0.3&amp;fp-z=1.5&amp;fit=crop", 476 - description: <>Joined the team</>, 477 - }, 478 - ]; 479 - 480 - return ( 481 - <Card style={styles.relative}> 482 - <CardHeader> 483 - <CardTitle>Recent activity</CardTitle> 484 - <CardDescription> 485 - Review what has happened over the past days. 486 - </CardDescription> 487 - </CardHeader> 488 - <CardBody> 489 - <Grid 490 - columns="auto 1fr auto" 491 - columnGap="4" 492 - rowGap="5" 493 - alignItems="center" 494 - > 495 - {activities.map((activity, index) => ( 496 - <Fragment key={activity.timestamp}> 497 - <Avatar 498 - size="lg" 499 - src={activity.avatar} 500 - fallback={activity.name.charAt(0)} 501 - /> 502 - <Flex direction="column" gap="2"> 503 - <Text weight="medium">{activity.name}</Text> 504 - <SmallBody variant="secondary"> 505 - {activity.description} 506 - </SmallBody> 507 - </Flex> 508 - <SmallBody variant="secondary"> 509 - {Intl.DateTimeFormat("en-US", { 510 - dateStyle: "short", 511 - timeStyle: "short", 512 - }).format(new Date(activity.timestamp))} 513 - </SmallBody> 514 - {index < activities.length - 1 && ( 515 - <GridItem columnStart={1} columnEnd={-1}> 516 - <Separator /> 517 - </GridItem> 518 - )} 519 - </Fragment> 520 - ))} 521 - </Grid> 522 - <Flex gap="1" style={styles.pinnedButtons}> 523 - <IconButton variant="tertiary" label="Open in new tab"> 524 - <ExternalLink /> 525 - </IconButton> 526 - <ToggleButton defaultSelected variant="tertiary"> 527 - <Pin /> 528 - </ToggleButton> 529 - </Flex> 530 - </CardBody> 531 - </Card> 532 - ); 533 - } 534 - 535 - function FinancialPerformance() { 536 - const data = [ 537 - { 538 - label: "MRR", 539 - change: "3.2%", 540 - value: "$350K", 541 - trend: "positive", 542 - direction: "up", 543 - }, 544 - { 545 - label: "OpEx", 546 - change: "12.8%", 547 - value: "$211K", 548 - trend: "negative", 549 - direction: "up", 550 - }, 551 - { 552 - label: "CapEx", 553 - change: "8.8%", 554 - value: "$94K", 555 - trend: "positive", 556 - direction: "down", 557 - }, 558 - { 559 - label: "GPM", 560 - change: "1.2%", 561 - value: "44.6%", 562 - trend: "negative", 563 - direction: "down", 564 - }, 565 - { 566 - label: "NPM", 567 - change: "0.0%", 568 - value: "9.1%", 569 - trend: "neutral", 570 - direction: "none", 571 - }, 572 - { 573 - label: "EBITDA", 574 - change: "4.1%", 575 - value: "$443K", 576 - trend: "positive", 577 - direction: "up", 578 - }, 579 - { 580 - label: "CAC", 581 - change: "11.0%", 582 - value: "$146", 583 - trend: "positive", 584 - direction: "down", 585 - }, 586 - { 587 - label: "LTV", 588 - change: "3%", 589 - value: "$1,849", 590 - trend: "positive", 591 - direction: "up", 592 - }, 593 - { 594 - label: "Churn", 595 - change: "1.1%", 596 - value: "12.4%", 597 - trend: "negative", 598 - direction: "up", 599 - }, 600 - ]; 601 - 602 - return ( 603 - <Card style={styles.relative}> 604 - <CardHeader> 605 - <CardTitle>Financial performance</CardTitle> 606 - <CardDescription> 607 - Review your company's KPIs compared to the month before. . 608 - </CardDescription> 609 - </CardHeader> 610 - <CardBody> 611 - <Grid 612 - columns="1fr 1fr 1fr" 613 - columnGap="10" 614 - rowGap="10" 615 - alignItems="center" 616 - > 617 - {data.map((item) => ( 618 - <Flex 619 - key={`${item.label}-${item.change}`} 620 - direction="column" 621 - gap="4" 622 - > 623 - <Flex gap="2" align="center"> 624 - <Text variant="secondary">{item.label}</Text> 625 - <Badge 626 - size="sm" 627 - variant={ 628 - item.trend === "positive" 629 - ? "success" 630 - : item.trend === "negative" 631 - ? "critical" 632 - : "default" 633 - } 634 - > 635 - {item.direction === "up" && <ArrowUp />} 636 - {item.direction === "down" && <ArrowDown />} 637 - {item.change} 638 - </Badge> 639 - </Flex> 640 - <Text size="3xl" weight="semibold"> 641 - {item.value} 642 - </Text> 643 - </Flex> 644 - ))} 645 - </Grid> 646 - <Flex gap="1" style={styles.pinnedButtons}> 647 - <IconButton variant="tertiary" label="Open in new tab"> 648 - <ExternalLink /> 649 - </IconButton> 650 - <ToggleButton defaultSelected variant="tertiary"> 651 - <Pin /> 652 - </ToggleButton> 653 - </Flex> 654 - </CardBody> 655 - </Card> 656 - ); 657 - } 658 - 659 - function Todos() { 660 - const todos = [ 661 - { 662 - id: "1", 663 - title: ( 664 - <span> 665 - Respond to comment <Link>#384</Link> from Travis Ross 666 - </span> 667 - ), 668 - completed: false, 669 - }, 670 - { 671 - id: "2", 672 - title: <span>Invite Acme Co. team to Slack</span>, 673 - completed: false, 674 - }, 675 - { 676 - id: "3", 677 - title: <span>Create a report requested by Danilo Sousa</span>, 678 - completed: false, 679 - }, 680 - { 681 - id: "4", 682 - title: ( 683 - <span> 684 - Review support request <Link>#85</Link> 685 - </span> 686 - ), 687 - completed: false, 688 - }, 689 - { id: "5", title: <span>Close Q2 finances</span>, completed: true }, 690 - { 691 - id: "6", 692 - title: ( 693 - <span> 694 - Review invoice <Link>#3456</Link> 695 - </span> 696 - ), 697 - completed: true, 698 - }, 699 - ]; 700 - 701 - return ( 702 - <Card style={styles.relative}> 703 - <CardHeader> 704 - <CardTitle>To-do</CardTitle> 705 - <CardDescription>Stay on top of your daily tasks.</CardDescription> 706 - </CardHeader> 707 - <CardBody> 708 - <Flex direction="column" gap="4"> 709 - {todos.map((todo) => ( 710 - <Checkbox isSelected={todo.completed} key={todo.id}> 711 - <Text strikethrough={todo.completed}>{todo.title}</Text> 712 - </Checkbox> 713 - ))} 714 - </Flex> 715 - <Flex gap="1" style={styles.pinnedButtons}> 716 - <IconButton variant="tertiary" label="Share tasks"> 717 - <Share /> 718 - </IconButton> 719 - <IconButton variant="tertiary" label="Add new task"> 720 - <Plus /> 721 - </IconButton> 722 - </Flex> 723 - </CardBody> 724 - </Card> 725 - ); 726 - } 727 - 728 - function SignUpForm() { 729 - return ( 730 - <Card> 731 - <CardHeader> 732 - <CardTitle>Sign Up</CardTitle> 733 - </CardHeader> 734 - <CardBody> 735 - <Flex direction="column" gap="4"> 736 - <TextField label="Email Address" type="email" /> 737 - <TextField 738 - type="password" 739 - label={ 740 - <Flex justify="between" align="center"> 741 - <span>Password</span> 742 - <Link style={typeramp.sublabel}>Forgot Password?</Link> 743 - </Flex> 744 - } 745 - /> 746 - </Flex> 747 - </CardBody> 748 - <CardFooter> 749 - <Button variant="secondary">Create Account</Button> 750 - <Button>Sign In</Button> 751 - </CardFooter> 752 - </Card> 753 - ); 754 - } 755 - 756 - function SampleInvoice() { 757 - return ( 758 - <Card style={styles.relative}> 759 - <CardHeader> 760 - <CardTitle> 761 - Invoice <Link>#3461</Link> 762 - </CardTitle> 763 - </CardHeader> 764 - <CardBody> 765 - <Flex direction="column" gap="8"> 766 - <Grid columns="1fr 1fr" columnGap="6"> 767 - <Flex direction="column" gap="2"> 768 - <Text size="sm" variant="secondary"> 769 - Issued 770 - </Text> 771 - <Text weight="medium">June 21, 2023</Text> 772 - </Flex> 773 - <Flex direction="column" gap="2"> 774 - <Text size="sm" variant="secondary"> 775 - Due 776 - </Text> 777 - <Text weight="medium">July 21, 2023</Text> 778 - </Flex> 779 - <Flex direction="column" gap="2"> 780 - <Text size="sm" variant="secondary"> 781 - To 782 - </Text> 783 - <Text weight="medium">Paradise Ventures</Text> 784 - <Text size="sm"> 785 - 742 Evergreen Terrace, Springfield, IL 62704 786 - </Text> 787 - </Flex> 788 - <Flex direction="column" gap="2"> 789 - <Text size="sm" variant="secondary"> 790 - From 791 - </Text> 792 - <Text weight="medium">Rogue Widgets</Text> 793 - <Text size="sm">1600 Baker Street NW, Washington, DC 20500</Text> 794 - </Flex> 795 - </Grid> 796 - 797 - <Flex direction="column" gap="4"> 798 - <Flex justify="between" align="center"> 799 - <Text size="sm" variant="secondary"> 800 - Services 801 - </Text> 802 - <Text size="sm" variant="secondary"> 803 - Price 804 - </Text> 805 - </Flex> 806 - <Flex justify="between" align="center"> 807 - <Text weight="medium" size="lg"> 808 - Branding 809 - </Text> 810 - <Text>$20,000</Text> 811 - </Flex> 812 - <Flex justify="between" align="center"> 813 - <Text weight="medium" size="lg"> 814 - Marketing website 815 - </Text> 816 - <Text>$17,500</Text> 817 - </Flex> 818 - <Separator /> 819 - <Flex justify="between" align="center"> 820 - <Text>Total</Text> 821 - <Text>$37,500</Text> 822 - </Flex> 823 - </Flex> 824 - </Flex> 825 - <Flex gap="1" style={styles.pinnedButtons}> 826 - <IconButton variant="tertiary" label="Close"> 827 - <X /> 828 - </IconButton> 829 - </Flex> 830 - </CardBody> 831 - <CardFooter> 832 - <Button variant="critical-outline">Reject</Button> 833 - <Button>Approve</Button> 834 - </CardFooter> 835 - </Card> 836 - ); 837 - } 838 - 839 - function InvoicePaid() { 840 - return ( 841 - <Card style={[styles.relative, styles.invoiceCard]}> 842 - <Flex direction="column" gap="6" style={styles.textCenter}> 843 - <Flex align="center" justify="center"> 844 - <CheckCircle2 size={48} {...stylex.props(styles.check)} /> 845 - </Flex> 846 - <Text size="xl" weight="medium"> 847 - Invoice paid 848 - </Text> 849 - <Body> 850 - You paid $17,975.30. A receipt copy was sent to{" "} 851 - <Text weight="semibold">accounting@example.com</Text> 852 - </Body> 853 - <Flex direction="column" gap="2"> 854 - <Button>Next invoice</Button> 855 - <Button variant="outline">Done</Button> 856 - </Flex> 857 - </Flex> 858 - <Flex gap="1" style={styles.pinnedButtons}> 859 - <IconButton variant="tertiary" label="Close"> 860 - <X /> 861 - </IconButton> 862 - </Flex> 863 - </Card> 864 - ); 865 - } 866 - 867 - function YourCompanyCard() { 868 - return ( 869 - <Card> 870 - <CardHeader> 871 - <CardTitle>Your Company Card</CardTitle> 872 - <CardDescription>View and manage your corporate card.</CardDescription> 873 - </CardHeader> 874 - <CardBody> 875 - <div 876 - {...stylex.props( 877 - styles.creditCardWrapper, 878 - ui.bgDim, 879 - primary.textContrast, 880 - )} 881 - > 882 - <Flex 883 - direction="column" 884 - justify="between" 885 - gap="2" 886 - style={styles.creditCard} 887 - > 888 - <Text>Shane Goodall</Text> 889 - <Flex direction="column" gap="2"> 890 - <Flex align="center" gap="2"> 891 - <Text>1234 5678 9012 3456</Text> 892 - <IconButton 893 - size="sm" 894 - variant="tertiary" 895 - label="Copy card number" 896 - style={styles.copyCardNumber} 897 - > 898 - <Copy /> 899 - </IconButton> 900 - </Flex> 901 - <Flex align="center" gap="2"> 902 - <Text>01/27</Text> 903 - <Text>999</Text> 904 - </Flex> 905 - </Flex> 906 - </Flex> 907 - </div> 908 - </CardBody> 909 - <CardFooter> 910 - <Button variant="critical-outline">Freeze</Button> 911 - <Button>Done</Button> 912 - </CardFooter> 913 - </Card> 914 - ); 915 - } 916 - 917 - function RouteComponent() { 918 - return ( 919 - <Flex gap="6"> 920 - <Flex direction="column" gap="6" style={styles.grow}> 921 - <YourTeam /> 922 - <Notifications /> 923 - <Pricing /> 924 - </Flex> 925 - <Flex direction="column" gap="6" style={styles.skinny}> 926 - <SignUpForm /> 927 - <YourCompanyCard /> 928 - <InvoicePaid /> 929 - <SampleInvoice /> 930 - </Flex> 931 - <Flex direction="column" gap="6" style={styles.grow}> 932 - <FinancialPerformance /> 933 - <RecentActivity /> 934 - <Todos /> 935 - </Flex> 936 - </Flex> 937 - ); 938 - }
+18 -11
apps/docs/src/showcases/ecommerce.tsx
··· 16 16 CardImage, 17 17 CardTitle, 18 18 } from "@/components/card"; 19 + import { 20 + EmptyState, 21 + EmptyStateImage, 22 + EmptyStateTitle, 23 + EmptyStateDescription, 24 + EmptyStateActions, 25 + } from "@/components/empty-state"; 19 26 import { ColorSwatch } from "@/components/color-swatch"; 20 27 import { FileDropZone } from "@/components/file-drop-zone"; 21 28 import { Flex } from "@/components/flex"; ··· 30 37 import { TextField } from "@/components/text-field"; 31 38 import { ToggleButton } from "@/components/toggle-button"; 32 39 import { ToggleButtonGroup } from "@/components/toggle-button-group"; 33 - import { LabelText, SmallBody } from "@/components/typography"; 40 + import { LabelText } from "@/components/typography"; 34 41 import { Text } from "@/components/typography/text"; 35 42 36 43 import { spacing } from "../components/theme/spacing.stylex"; ··· 512 519 return ( 513 520 <Card> 514 521 <CardBody> 515 - <Flex direction="column" gap="4" align="center"> 516 - <Shredder size={48} /> 517 - <Text weight="semibold" size="lg"> 518 - Product discarded 519 - </Text> 520 - <SmallBody variant="secondary"> 522 + <EmptyState> 523 + <EmptyStateImage> 524 + <Shredder /> 525 + </EmptyStateImage> 526 + <EmptyStateTitle>Product discarded</EmptyStateTitle> 527 + <EmptyStateDescription> 521 528 It's still available in the <Link>archive.</Link> 522 - </SmallBody> 523 - <Flex gap="2"> 529 + </EmptyStateDescription> 530 + <EmptyStateActions> 524 531 <Button variant="secondary">Undo</Button> 525 532 <Button>Done</Button> 526 - </Flex> 527 - </Flex> 533 + </EmptyStateActions> 534 + </EmptyState> 528 535 </CardBody> 529 536 </Card> 530 537 );
+18 -7
apps/docs/stylex.css
··· 454 454 .xs5yzll:is([data-card-size=md]){--card-y-padding:var(--x3ogwq2)} 455 455 .x13452rq:is([data-card-size=sm]){--card-y-padding:var(--xsow7ju)} 456 456 .x895p9r:is([data-card-size=lg]){--card-y-padding:var(--xyoqvup)} 457 - .x1b2b8rc:is([data-empty-state-size=md]){--empty-state-gap:var(--x109877l)} 458 - .x106abaa:is([data-empty-state-size=lg]){--empty-state-gap:var(--x1do95gr)} 459 - .x1mazroy:is([data-empty-state-size=sm]){--empty-state-gap:var(--xgvn2um)} 460 - .xc1b8y5:is([data-empty-state-size=sm]){--empty-state-image-size:120px} 461 - .x10zm317:is([data-empty-state-size=md]){--empty-state-image-size:180px} 462 - .x13qpv5z:is([data-empty-state-size=lg]){--empty-state-image-size:240px} 457 + .x17il39p:is([data-empty-state-size=md]){--empty-state-image-size:var(--x18pvp2c)} 458 + .xzhsimv:is([data-empty-state-size=lg]){--empty-state-image-size:var(--xbgtkw8)} 459 + .x7j7gah:is([data-empty-state-size=sm]){--empty-state-image-size:var(--xyoqvup)} 463 460 .xraprjz:is([data-placement=right],[data-placement=right] > *){--origin-x:calc(var(--xgvn2um) * -1)} 464 461 .xl9dh7p:is([data-placement=left],[data-placement=left] > *){--origin-x:var(--xgvn2um)} 465 462 .xkllo3s:is([data-placement=bottom],[data-placement=bottom] > *){--origin-y:calc(var(--xgvn2um) * -1)} ··· 472 469 .x10z2afa:is([data-size=sm] *){--progress-size:var(--xgvn2um)} 473 470 .xvruivg:is([data-size=lg] *){--progress-size:var(--xyoqvup)} 474 471 .x19dv8yz:not(#\#){grid-area:action} 472 + .x1cfmmib:not(#\#){grid-area:actions} 475 473 .x1iynvfu:not(#\#){grid-area:bar} 476 474 .x1vu5sea:not(#\#){grid-area:close} 477 475 .x1fdo2jl:not(#\#){grid-area:content} 478 476 .x19tbii3:not(#\#){grid-area:description} 479 477 .x1cnu6j2:not(#\#){grid-area:icon} 478 + .x5jscpo:not(#\#){grid-area:image} 480 479 .xxwd7sb:not(#\#){grid-area:label} 481 480 .xnyuz70:not(#\#){grid-area:title} 482 481 .xo3lhmp:not(#\#){grid-area:track} ··· 547 546 .x1rzw5jd:not(#\#):not(#\#){gap:32px} 548 547 .x167g77z:not(#\#):not(#\#){gap:8px} 549 548 .x8fwt5u:not(#\#):not(#\#){gap:var(--card-gap)} 550 - .x1dp1mka:not(#\#):not(#\#){gap:var(--empty-state-gap)} 551 549 .x19e0sr3:not(#\#):not(#\#){gap:var(--toggle-button-group-gap)} 552 550 .xzqe5kx:not(#\#):not(#\#){gap:var(--x109877l)} 553 551 .xp9mjrc:not(#\#):not(#\#){gap:var(--x11x3va4)} ··· 632 630 .xqwsx7m:is([data-selected]):not(#\#):not(#\#)::after{border-width:1px} 633 631 .xgq8a5n:is([data-size=sm]):not(#\#):not(#\#){gap:var(--x1plbop)} 634 632 .x1qovttw:is([data-orientation=vertical]):not(#\#):not(#\#){gap:var(--xsow7ju)} 633 + .x1d8lqcm:is([data-empty-state-size=sm]):not(#\#):not(#\#){grid-template-areas:"image title" "image description"} 634 + .x4jzqnn:is([data-empty-state-size=md],[data-empty-state-size=lg]):not(#\#):not(#\#){grid-template-areas:"image" "title" "description"} 635 635 .x1gph5tg:is([data-focus-visible] *):not(#\#):not(#\#){outline:2px solid var(--x1qgqpcr)} 636 636 .x1meiurb:is([data-unavailable]):not(#\#):not(#\#){text-decoration:line-through} 637 637 .x17pbne7:is([data-breadcrumb] *):not(#\#):not(#\#){text-decoration:none} ··· 642 642 .xw9agv0:has([data-hovered]):not(#\#):not(#\#){border-color:var(--x1wagwp)} 643 643 .xx842gn:has(label,[data-slider-output]):not(#\#):not(#\#){grid-template-areas:'label value-label' 'track track'} 644 644 @supports (corner-shape: squircle){.x151gzyt.x151gzyt:is(*) pre:not(#\#):not(#\#){border-radius:var(--x1bwjc12)}} 645 + .x1ljrgqn:has([data-empty-state-actions]):is([data-empty-state-size=sm]):not(#\#):not(#\#){grid-template-areas:"image title actions" "image description actions"} 646 + .x9cfk05:has([data-empty-state-actions]):is([data-empty-state-size=md],[data-empty-state-size=lg]):not(#\#):not(#\#){grid-template-areas:"image" "title" "description" "actions"} 645 647 .x17yyxzj:disabled:not(#\#):not(#\#){border-color:transparent} 646 648 .x1yyog5s:disabled:not(#\#):not(#\#){border-color:var(--x1ojhc7k)} 647 649 .x1lqbkrs:hover:not(#\#):not(#\#){border-color:var(--x1be1b4q)} ··· 1056 1058 .x1xwj67i:is([data-hovered],[data-focused],[data-selected]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1057 1059 .x8b72bf:is([data-hovered]):not([data-unavailable]):not(#\#):not(#\#):not(#\#){color:var(--xelq0bc)} 1058 1060 .xf5mls2:is([data-variant=warning] *):not(#\#):not(#\#):not(#\#){color:var(--xw4mcn1)} 1061 + .x15057wy:is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){column-gap:var(--xgvn2um)} 1059 1062 .x12tbmpm:is(*) pre:not(#\#):not(#\#):not(#\#){corner-shape:squircle} 1060 1063 .x1w11t6e:is([data-resizable-direction=left]):not(#\#):not(#\#):not(#\#){cursor:e-resize} 1061 1064 .x1oooc6o:is([data-resizable-direction=both]):not(#\#):not(#\#):not(#\#){cursor:ew-resize} ··· 1088 1091 .xwbnkov:is([data-empty-state-size='lg'] *):not(#\#):not(#\#):not(#\#){font-size:var(--xq985z6)} 1089 1092 .x10ef3jg:is([data-react-aria-pressable=true][data-selected=true]):not(#\#):not(#\#):not(#\#){font-weight:var(--x1tobmye)} 1090 1093 .x1e1yh2g:is([data-current]):not(#\#):not(#\#):not(#\#){font-weight:var(--x1tobmye)} 1094 + .xl84o51:is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){grid-template-columns:min-content 1fr} 1095 + .x14u8no6:is([data-empty-state-size=md],[data-empty-state-size=lg]):not(#\#):not(#\#):not(#\#){justify-items:center} 1096 + .x1h912m2:is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){justify-items:start} 1091 1097 .xqreftt:is(li *):not(#\#):not(#\#):not(#\#){line-height:var(--x17a3wz2)} 1092 1098 .xeccy5s:is(blockquote *):not(#\#):not(#\#):not(#\#){line-height:var(--x17a3wz2)} 1093 1099 .x5srk72:is([data-size=lg]):not(#\#):not(#\#):not(#\#){line-height:var(--x17a3wz2)} ··· 1116 1122 .x19hqv7w:is([data-exiting], [data-exiting] > *):not(#\#):not(#\#):not(#\#){pointer-events:none} 1117 1123 .xufvh4u:is([aria-expanded=true] *):not(#\#):not(#\#):not(#\#){rotate:180deg} 1118 1124 .x18ellfx:is([aria-expanded=true] *):not(#\#):not(#\#):not(#\#){rotate:90deg} 1125 + .xjsw4yf:is([data-empty-state-size=lg]):not(#\#):not(#\#):not(#\#){row-gap:var(--x109877l)} 1126 + .x15icp3p:is([data-empty-state-size=md]):not(#\#):not(#\#):not(#\#){row-gap:var(--xgvn2um)} 1127 + .xrbp8k3:is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){row-gap:var(--xsow7ju)} 1119 1128 .x140extk:is([data-placement=right] *):not(#\#):not(#\#):not(#\#){transform:rotate(-90deg)} 1120 1129 .x1s861hk:is([data-placement=top] *):not(#\#):not(#\#):not(#\#){transform:rotate(0deg)} 1121 1130 .xr9p1a1:is([data-placement=bottom] *):not(#\#):not(#\#):not(#\#){transform:rotate(180deg)} ··· 1141 1150 .x1yg37rf:is([data-variant=warning] *):is(*) svg:not(#\#):not(#\#):not(#\#){color:var(--x14k0c5u)} 1142 1151 .x1e1l22d:is([data-variant=critical] *):is(*) svg:not(#\#):not(#\#):not(#\#){color:var(--x1uwop3o)} 1143 1152 .xpw51zl:is([data-variant=success] *):is(*) svg:not(#\#):not(#\#):not(#\#){color:var(--x6wzzll)} 1153 + .xyvwb2k:has([data-empty-state-actions]):is([data-empty-state-size=sm]):not(#\#):not(#\#):not(#\#){grid-template-columns:min-content 1fr max-content} 1144 1154 .x1oqui1x:disabled:not(#\#):not(#\#):not(#\#){background-color:transparent} 1145 1155 .x1n9027r:disabled:not(#\#):not(#\#):not(#\#){background-color:var(--x16ucms1)} 1146 1156 .x16f0nht:disabled:not(#\#):not(#\#):not(#\#){background-color:var(--x17z0c80)} ··· 1531 1541 .xkz5e6c:is([data-size=sm]):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--x1plbop)} 1532 1542 .xsabt53:is(*) pre:not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xgvn2um)} 1533 1543 .x1yq5ceo:is([data-table-size=lg] *:not(:first-child)):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xgvn2um)} 1544 + .x1pp8d3o:is([data-empty-state-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xgvn2um)} 1534 1545 .x8pw2nx:is([data-size=md] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xmuc480)} 1535 1546 .x1dd6wxn:is([data-size=md]):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xsow7ju)} 1536 1547 .x8300ou:is([data-size=sm] *):not(#\#):not(#\#):not(#\#):not(#\#){padding-left:var(--xsow7ju)}
+26 -6
packages/hip-ui/src/components/combobox/index.tsx
··· 10 10 ComboBox as AriaComboBox, 11 11 ComboBoxProps as AriaComboBoxProps, 12 12 Input, 13 + Virtualizer, 14 + ListLayout, 13 15 } from "react-aria-components"; 14 16 15 17 import { SizeContext } from "../context"; ··· 24 26 StyleXComponentProps, 25 27 } from "../theme/types"; 26 28 import { useInputStyles } from "../theme/useInputStyles"; 29 + import { estimatedRowHeights } from "../theme/useListBoxItemStyles"; 27 30 import { usePopoverStyles } from "../theme/usePopoverStyles"; 28 31 import { SmallBody } from "../typography"; 29 32 ··· 64 67 shouldUpdatePosition?: boolean; 65 68 placement?: PopoverProps["placement"]; 66 69 renderEmptyState?: ListBoxProps<T>["renderEmptyState"]; 70 + isVirtualized?: boolean; 67 71 } 68 72 69 73 function ComboBoxContent<T extends object>({ ··· 84 88 shouldUpdatePosition, 85 89 placement, 86 90 renderEmptyState, 91 + isVirtualized, 87 92 }: ComboBoxContentProps<T>) { 88 93 const inputStyles = useInputStyles({ 89 94 size, ··· 92 97 }); 93 98 const popoverStyles = usePopoverStyles(); 94 99 100 + let listbox = ( 101 + <ListBox items={items} renderEmptyState={renderEmptyState || EmptyState}> 102 + {children} 103 + </ListBox> 104 + ); 105 + 106 + if (isVirtualized) { 107 + listbox = ( 108 + <Virtualizer 109 + layout={ListLayout} 110 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 111 + > 112 + {listbox} 113 + </Virtualizer> 114 + ); 115 + } 116 + 95 117 return ( 96 118 <> 97 119 <Label>{label}</Label> ··· 126 148 styles.matchWidth, 127 149 )} 128 150 > 129 - <ListBox 130 - items={items} 131 - renderEmptyState={renderEmptyState || EmptyState} 132 - > 133 - {children} 134 - </ListBox> 151 + {listbox} 135 152 </Popover> 136 153 </> 137 154 ); ··· 160 177 placeholder?: string; 161 178 prefix?: React.ReactNode; 162 179 suffix?: React.ReactNode; 180 + isVirtualized?: boolean; 163 181 } 164 182 165 183 export function ComboBox<T extends object>({ ··· 180 198 prefix, 181 199 suffix, 182 200 renderEmptyState, 201 + isVirtualized = false, 183 202 ...props 184 203 }: ComboBoxProps<T>) { 185 204 const size = sizeProp || use(SizeContext); ··· 210 229 shouldUpdatePosition={shouldUpdatePosition} 211 230 placement={placement} 212 231 renderEmptyState={renderEmptyState} 232 + isVirtualized={isVirtualized} 213 233 > 214 234 {children} 215 235 </ComboBoxContent>
+76 -29
packages/hip-ui/src/components/empty-state/index.tsx
··· 11 11 12 12 const styles = stylex.create({ 13 13 emptyState: { 14 - "--empty-state-gap": { 15 - ":is([data-empty-state-size=lg])": spacing["8"], 16 - ":is([data-empty-state-size=md])": spacing["6"], 17 - ":is([data-empty-state-size=sm])": spacing["4"], 14 + display: "grid", 15 + gridTemplateAreas: { 16 + ":is([data-empty-state-size=md],[data-empty-state-size=lg])": { 17 + default: ` 18 + "image" 19 + "title" 20 + "description" 21 + `, 22 + ":has([data-empty-state-actions])": ` 23 + "image" 24 + "title" 25 + "description" 26 + "actions" 27 + `, 28 + }, 29 + ":is([data-empty-state-size=sm])": { 30 + default: ` 31 + "image title" 32 + "image description" 33 + `, 34 + ":has([data-empty-state-actions])": ` 35 + "image title actions" 36 + "image description actions" 37 + `, 38 + }, 18 39 }, 19 - "--empty-state-image-size": { 20 - ":is([data-empty-state-size=lg])": "240px", 21 - ":is([data-empty-state-size=md])": "180px", 22 - ":is([data-empty-state-size=sm])": "120px", 40 + gridTemplateColumns: { 41 + ":is([data-empty-state-size=sm])": { 42 + ":has([data-empty-state-actions])": "min-content 1fr max-content", 43 + default: "min-content 1fr", 44 + }, 23 45 }, 24 - gap: "var(--empty-state-gap)", 25 46 alignItems: "center", 26 - display: "flex", 27 - flexDirection: "column", 28 47 fontFamily: fontFamily["sans"], 29 - justifyContent: "center", 48 + justifyItems: { 49 + ":is([data-empty-state-size=md],[data-empty-state-size=lg])": "center", 50 + ":is([data-empty-state-size=sm])": "start", 51 + }, 52 + columnGap: { 53 + ":is([data-empty-state-size=sm])": spacing["4"], 54 + }, 30 55 textAlign: "center", 56 + "--empty-state-image-size": { 57 + ":is([data-empty-state-size=lg])": spacing["20"], 58 + ":is([data-empty-state-size=md])": spacing["14"], 59 + ":is([data-empty-state-size=sm])": spacing["10"], 60 + }, 61 + rowGap: { 62 + ":is([data-empty-state-size=lg])": spacing["6"], 63 + ":is([data-empty-state-size=md])": spacing["4"], 64 + ":is([data-empty-state-size=sm])": spacing["2"], 65 + }, 31 66 }, 32 67 image: { 33 - alignItems: "center", 34 - display: "flex", 35 - justifyContent: "center", 36 - objectFit: "contain", 68 + gridArea: "image", 37 69 height: "var(--empty-state-image-size)", 38 70 width: "var(--empty-state-image-size)", 71 + display: "flex", 72 + objectFit: "contain", 73 + alignItems: "center", 74 + justifyContent: "center", 39 75 }, 40 76 title: { 41 - margin: 0, 77 + gridArea: "title", 42 78 fontSize: { 43 79 ":is([data-empty-state-size='lg'] *)": fontSize["2xl"], 44 80 ":is([data-empty-state-size='md'] *)": fontSize["xl"], 45 81 ":is([data-empty-state-size='sm'] *)": fontSize["lg"], 46 82 }, 83 + margin: 0, 47 84 fontWeight: fontWeight["semibold"], 48 85 }, 49 86 description: { 87 + gridArea: "description", 88 + fontSize: fontSize["sm"], 50 89 margin: 0, 51 - fontSize: fontSize["sm"], 52 90 fontWeight: fontWeight["normal"], 53 91 maxWidth: { 54 92 ":is([data-empty-state-size=lg])": "480px", ··· 57 95 }, 58 96 }, 59 97 actions: { 98 + gridArea: "actions", 99 + display: "flex", 60 100 gap: spacing["2"], 101 + flexDirection: "row", 61 102 alignItems: "center", 62 - display: "flex", 63 - flexDirection: "row", 64 103 flexWrap: "wrap", 65 104 justifyContent: "center", 105 + 106 + paddingLeft: { 107 + ":is([data-empty-state-size=sm] *)": spacing["4"], 108 + }, 66 109 }, 67 110 }); 68 111 ··· 83 126 const size = sizeProp || use(SizeContext); 84 127 85 128 return ( 86 - <SizeContext value={size}> 87 - <div 88 - {...props} 89 - data-empty-state-size={size} 90 - {...stylex.props(styles.emptyState, style)} 91 - /> 92 - </SizeContext> 129 + <div 130 + {...props} 131 + data-empty-state-size={size} 132 + {...stylex.props(styles.emptyState, style)} 133 + /> 93 134 ); 94 135 }; 95 136 ··· 131 172 extends StyleXComponentProps<React.ComponentProps<"h2">> {} 132 173 133 174 export const EmptyStateTitle = ({ style, ...props }: EmptyStateTitleProps) => { 134 - return <div {...props} {...stylex.props(styles.title, ui.text, style)} />; 175 + return <h2 {...props} {...stylex.props(styles.title, ui.text, style)} />; 135 176 }; 136 177 137 178 export interface EmptyStateDescriptionProps ··· 153 194 style, 154 195 ...props 155 196 }: EmptyStateActionsProps) => { 156 - return <div {...props} {...stylex.props(styles.actions, style)} />; 197 + return ( 198 + <div 199 + {...props} 200 + data-empty-state-actions 201 + {...stylex.props(styles.actions, style)} 202 + /> 203 + ); 157 204 };
+21 -2
packages/hip-ui/src/components/listbox/index.tsx
··· 11 11 Header, 12 12 SeparatorProps, 13 13 ListStateContext, 14 + Virtualizer, 15 + ListLayout, 14 16 } from "react-aria-components"; 15 17 16 18 import { Checkbox, CheckboxProps } from "../checkbox"; ··· 20 22 import { spacing } from "../theme/spacing.stylex"; 21 23 import { Size, StyleXComponentProps } from "../theme/types"; 22 24 import { typeramp } from "../theme/typography.stylex"; 23 - import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; 25 + import { 26 + estimatedRowHeights, 27 + useListBoxItemStyles, 28 + } from "../theme/useListBoxItemStyles"; 24 29 25 30 const styles = stylex.create({ 26 31 listBox: { ··· 57 62 items?: Iterable<T>; 58 63 children: React.ReactNode | ((item: T) => React.ReactNode); 59 64 variant?: ListBoxVariant; 65 + isVirtualized?: boolean; 60 66 } 61 67 62 68 export function ListBox<T extends object>({ 63 69 size: sizeProp, 64 70 style, 65 71 variant = "default", 72 + isVirtualized = false, 66 73 ...props 67 74 }: ListBoxProps<T>) { 68 75 const size = sizeProp || use(SizeContext); 76 + const listbox = ( 77 + <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 78 + ); 69 79 70 80 return ( 71 81 <ListboxVariantContext value={variant}> 72 82 <SizeContext value={size}> 73 - <AriaListBox {...stylex.props(styles.listBox, style)} {...props} /> 83 + {isVirtualized ? ( 84 + <Virtualizer 85 + layout={ListLayout} 86 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 87 + > 88 + {listbox} 89 + </Virtualizer> 90 + ) : ( 91 + listbox 92 + )} 74 93 </SizeContext> 75 94 </ListboxVariantContext> 76 95 );
+34 -11
packages/hip-ui/src/components/select/index.tsx
··· 12 12 Select as AriaSelect, 13 13 Autocomplete, 14 14 useFilter, 15 + ListLayout, 16 + Virtualizer, 15 17 } from "react-aria-components"; 16 18 17 19 import { SizeContext } from "../context"; ··· 27 29 StyleXComponentProps, 28 30 } from "../theme/types"; 29 31 import { useInputStyles } from "../theme/useInputStyles"; 32 + import { estimatedRowHeights } from "../theme/useListBoxItemStyles"; 30 33 import { usePopoverStyles } from "../theme/usePopoverStyles"; 31 34 32 35 const styles = stylex.create({ ··· 57 60 shouldFlip?: boolean; 58 61 shouldUpdatePosition?: boolean; 59 62 placement?: PopoverProps["placement"]; 63 + isVirtualized?: boolean; 60 64 } 61 65 62 66 function SelectContent<T extends object>({ 67 + isVirtualized, 63 68 label, 64 69 description, 65 70 errorMessage, ··· 86 91 const popoverStyles = usePopoverStyles(); 87 92 const { contains } = useFilter({ sensitivity: "base" }); 88 93 94 + let listbox = <ListBox items={items}>{children}</ListBox>; 95 + 96 + if (isVirtualized) { 97 + listbox = ( 98 + <Virtualizer 99 + layout={ListLayout} 100 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 101 + > 102 + {listbox} 103 + </Virtualizer> 104 + ); 105 + } 106 + 107 + if (isSearchable) { 108 + listbox = ( 109 + <Autocomplete filter={contains}> 110 + <div {...stylex.props(styles.searchField)}> 111 + <SearchField placeholder="Search" variant="secondary" /> 112 + </div> 113 + <ListBoxSeparator /> 114 + {listbox} 115 + </Autocomplete> 116 + ); 117 + } 118 + 89 119 return ( 90 120 <> 91 121 <Label>{label}</Label> ··· 127 157 styles.matchWidth, 128 158 )} 129 159 > 130 - {isSearchable ? ( 131 - <Autocomplete filter={contains}> 132 - <div {...stylex.props(styles.searchField)}> 133 - <SearchField placeholder="Search" variant="secondary" /> 134 - </div> 135 - <ListBoxSeparator /> 136 - <ListBox items={items}>{children}</ListBox> 137 - </Autocomplete> 138 - ) : ( 139 - <ListBox items={items}>{children}</ListBox> 140 - )} 160 + {listbox} 141 161 </Popover> 142 162 </> 143 163 ); ··· 166 186 prefix?: React.ReactNode; 167 187 suffix?: React.ReactNode; 168 188 isSearchable?: boolean; 189 + isVirtualized?: boolean; 169 190 } 170 191 171 192 export function Select< ··· 189 210 prefix, 190 211 suffix, 191 212 isSearchable = false, 213 + isVirtualized = false, 192 214 ...props 193 215 }: SelectProps<T, M>) { 194 216 const size = sizeProp || use(SizeContext); ··· 204 226 > 205 227 {({ isInvalid }) => ( 206 228 <SelectContent 229 + isVirtualized={isVirtualized} 207 230 label={label} 208 231 description={description} 209 232 errorMessage={errorMessage}
+31 -10
packages/hip-ui/src/components/table/index.tsx
··· 19 19 ColumnResizer, 20 20 DropIndicatorProps, 21 21 DropIndicator, 22 + TableLayout, 23 + Virtualizer, 22 24 } from "react-aria-components"; 23 25 24 26 import { Checkbox } from "../checkbox"; ··· 155 157 }, 156 158 }); 157 159 160 + const estimatedRowHeights: Record<Size, number> = { 161 + sm: 24, 162 + md: 32, 163 + lg: 40, 164 + }; 165 + 158 166 export interface TableProps extends StyleXComponentProps<AriaTableProps> { 159 167 size?: Size; 168 + isVirtualized?: boolean; 160 169 } 161 170 162 - export const Table = ({ style, size: sizeProp, ...props }: TableProps) => { 171 + export const Table = ({ 172 + style, 173 + size: sizeProp, 174 + isVirtualized = false, 175 + ...props 176 + }: TableProps) => { 163 177 const size = sizeProp || use(SizeContext); 178 + let table = <AriaTable {...props} {...stylex.props(styles.table, style)} />; 164 179 165 - return ( 166 - <SizeContext value={size}> 167 - <AriaTable 168 - data-table-size={size} 169 - {...stylex.props(styles.table, style)} 170 - {...props} 171 - /> 172 - </SizeContext> 173 - ); 180 + if (isVirtualized) { 181 + table = ( 182 + <Virtualizer 183 + layout={TableLayout} 184 + layoutOptions={{ 185 + estimatedRowHeight: estimatedRowHeights[size], 186 + headingHeight: estimatedRowHeights[size], 187 + }} 188 + > 189 + {table} 190 + </Virtualizer> 191 + ); 192 + } 193 + 194 + return <SizeContext value={size}>{table}</SizeContext>; 174 195 }; 175 196 176 197 export interface TableColumnProps
+7
packages/hip-ui/src/components/theme/useListBoxItemStyles.ts
··· 13 13 import { animationDuration } from "./animations.stylex"; 14 14 import { criticalColor, primaryColor, uiColor } from "./color.stylex"; 15 15 import { mediaQueries } from "./media-queries.stylex"; 16 + import { Size } from "./types"; 16 17 17 18 const styles = stylex.create({ 18 19 item: { ··· 108 109 flexGrow: 1, 109 110 }, 110 111 }); 112 + 113 + export const estimatedRowHeights: Record<Size, number> = { 114 + sm: 24, 115 + md: 32, 116 + lg: 40, 117 + }; 111 118 112 119 export function useListBoxItemStyles() { 113 120 const size = use(SizeContext);
+21 -6
packages/hip-ui/src/components/tree/index.tsx
··· 3 3 import { use } from "react"; 4 4 import { 5 5 Button, 6 + Virtualizer, 6 7 Tree as AriaTree, 7 8 TreeProps as AriaTreeProps, 8 9 TreeItemContent as AriaTreeItemContent, 9 10 TreeItem as AriaTreeItem, 10 11 TreeItemProps as AriaTreeItemProps, 11 12 TreeItemContentProps as AriaTreeItemContentProps, 13 + ListLayout, 12 14 } from "react-aria-components"; 13 15 14 16 import { Checkbox } from "../checkbox"; ··· 20 22 import { ui } from "../theme/semantic-color.stylex"; 21 23 import { spacing } from "../theme/spacing.stylex"; 22 24 import { Size, StyleXComponentProps } from "../theme/types"; 23 - import { useListBoxItemStyles } from "../theme/useListBoxItemStyles"; 25 + import { 26 + estimatedRowHeights, 27 + useListBoxItemStyles, 28 + } from "../theme/useListBoxItemStyles"; 24 29 25 30 const styles = stylex.create({ 26 31 wrapper: { ··· 225 230 extends StyleXComponentProps<Omit<AriaTreeProps<T>, "children">> { 226 231 children: React.ReactNode | ((item: T) => React.ReactNode); 227 232 size?: Size; 233 + isVirtualized?: boolean; 228 234 } 229 235 230 236 export function Tree<T extends object>({ 231 237 style, 232 238 size: sizeProp, 239 + isVirtualized = false, 233 240 ...props 234 241 }: TreeProps<T>) { 235 242 const size = sizeProp || use(SizeContext); 243 + let tree = <AriaTree {...props} {...stylex.props(style)} />; 236 244 237 - return ( 238 - <SizeContext value={size}> 239 - <AriaTree {...props} {...stylex.props(style)} /> 240 - </SizeContext> 241 - ); 245 + if (isVirtualized) { 246 + tree = ( 247 + <Virtualizer 248 + layout={ListLayout} 249 + layoutOptions={{ estimatedRowHeight: estimatedRowHeights[size] }} 250 + > 251 + {tree} 252 + </Virtualizer> 253 + ); 254 + } 255 + 256 + return <SizeContext value={size}>{tree}</SizeContext>; 242 257 }