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.

Factor out TOC

+355 -31
+179
apps/docs/src/components/table-of-contents/index.tsx
··· 1 + "use client"; 2 + 3 + import * as stylex from "@stylexjs/stylex"; 4 + import { createContext, use, useEffect, useState } from "react"; 5 + 6 + import { animationDuration } from "../theme/animations.stylex"; 7 + import { primaryColor, uiColor } from "../theme/color.stylex"; 8 + import { spacing } from "../theme/spacing.stylex"; 9 + import { StyleXComponentProps } from "../theme/types"; 10 + import { fontSize } from "../theme/typography.stylex"; 11 + 12 + export interface TocEntry { 13 + value: string; 14 + depth: number; 15 + id?: string; 16 + children?: Array<TocEntry>; 17 + } 18 + 19 + export type Toc = Array<TocEntry>; 20 + 21 + const ActiveHeaderIdContext = createContext<string | null>(null); 22 + const LevelContext = createContext(1); 23 + 24 + const styles = stylex.create({ 25 + wrapper: { 26 + gap: spacing["2"], 27 + overflow: "auto", 28 + boxSizing: "border-box", 29 + display: "flex", 30 + flexDirection: "column", 31 + flexShrink: 0, 32 + paddingBottom: spacing["20"], 33 + paddingTop: spacing["12"], 34 + }, 35 + sticky: { 36 + position: "sticky", 37 + height: "100vh", 38 + marginTop: spacing["12"], 39 + top: 0, 40 + }, 41 + itemList: { 42 + margin: 0, 43 + listStyle: "none", 44 + paddingLeft: 0, 45 + }, 46 + item: { 47 + textDecoration: "none", 48 + alignItems: "center", 49 + backgroundColor: { 50 + default: "transparent", 51 + ":hover::before": primaryColor.solid1, 52 + ":hover": uiColor.component1, 53 + }, 54 + color: { 55 + default: uiColor.text1, 56 + }, 57 + display: "flex", 58 + fontSize: fontSize["sm"], 59 + position: "relative", 60 + transitionDuration: animationDuration.fast, 61 + transitionProperty: { 62 + default: "color, border-left-color", 63 + "@media (prefers-reduced-motion: reduce)": "none", 64 + }, 65 + transitionTimingFunction: "ease-in-out", 66 + borderLeftColor: { 67 + default: uiColor.border1, 68 + }, 69 + borderLeftStyle: "solid", 70 + borderLeftWidth: 1, 71 + height: spacing[8], 72 + 73 + "::before": { 74 + content: "''", 75 + position: "absolute", 76 + bottom: 0, 77 + left: 0, 78 + top: 0, 79 + width: 1, 80 + }, 81 + }, 82 + level: (level: number) => ({ 83 + paddingLeft: `calc(${spacing[4]} * ${level.toString()})`, 84 + }), 85 + active: { 86 + color: primaryColor.solid2, 87 + borderLeftColor: primaryColor.solid1, 88 + 89 + "::before": { 90 + backgroundColor: primaryColor.solid1, 91 + content: "''", 92 + position: "absolute", 93 + bottom: 0, 94 + left: 0, 95 + top: 0, 96 + width: 1, 97 + }, 98 + }, 99 + }); 100 + 101 + function TocItem({ id, value, children }: TocEntry) { 102 + const level = use(LevelContext); 103 + const activeHeaderId = use(ActiveHeaderIdContext); 104 + 105 + return ( 106 + <li key={id}> 107 + <a 108 + href={`#${id ?? ""}`} 109 + {...stylex.props( 110 + styles.item, 111 + styles.level(level), 112 + activeHeaderId === id && styles.active, 113 + )} 114 + > 115 + {value} 116 + </a> 117 + {children && ( 118 + <LevelContext value={level + 1}> 119 + <ul {...stylex.props(styles.itemList)}> 120 + {children.map((child) => ( 121 + <TocItem key={child.id} {...child} /> 122 + ))} 123 + </ul> 124 + </LevelContext> 125 + )} 126 + </li> 127 + ); 128 + } 129 + 130 + /** 131 + * TableOfContents component props. 132 + */ 133 + export interface TableOfContentsProps extends StyleXComponentProps<{ 134 + toc: Toc; 135 + }> { 136 + sticky?: boolean; 137 + } 138 + 139 + /** 140 + * A table of contents component that displays a navigation tree based on document headings. 141 + * Automatically highlights the currently visible heading using IntersectionObserver. 142 + */ 143 + export function TableOfContents({ toc, style, sticky }: TableOfContentsProps) { 144 + const [activeHeaderId, setActiveHeaderId] = useState<string | null>(null); 145 + 146 + useEffect(() => { 147 + const observer = new IntersectionObserver((entries) => { 148 + for (const entry of entries) { 149 + if (entry.isIntersecting && entry.target.id) { 150 + setActiveHeaderId(entry.target.id); 151 + } 152 + } 153 + }); 154 + 155 + const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); 156 + 157 + for (const heading of headings) { 158 + observer.observe(heading); 159 + } 160 + 161 + return () => { 162 + observer.disconnect(); 163 + }; 164 + }, []); 165 + 166 + return ( 167 + <ActiveHeaderIdContext value={activeHeaderId}> 168 + <nav {...stylex.props(styles.wrapper, sticky && styles.sticky, style)}> 169 + <LevelContext value={1}> 170 + <ul {...stylex.props(styles.itemList)}> 171 + {toc.map((item) => ( 172 + <TocItem key={item.id} {...item} /> 173 + ))} 174 + </ul> 175 + </LevelContext> 176 + </nav> 177 + </ActiveHeaderIdContext> 178 + ); 179 + }
+30
apps/docs/src/docs/components/navigation/table-of-contents.mdx
··· 1 + --- 2 + title: TableOfContents 3 + description: A table of contents component that displays a navigation tree based on document headings and automatically highlights the currently visible section. 4 + --- 5 + 6 + import { PropDocs } from '../../../lib/PropDocs' 7 + import { Example } from '../../../lib/Example' 8 + import { Basic } from '../../../examples/table-of-contents/basic' 9 + 10 + <Example src={Basic} noPadding /> 11 + 12 + ## Installation 13 + 14 + Run the following command to add the table of contents component to your project. 15 + 16 + ```bash 17 + pnpm hip install table-of-contents 18 + ``` 19 + 20 + ## Props 21 + 22 + <PropDocs components={["TableOfContents"]} /> 23 + 24 + ## Related Components 25 + 26 + - [Sidebar](/docs/components/navigation/sidebar) - For navigation menus 27 + - [Breadcrumbs](/docs/components/navigation/breadcrumbs) - For showing navigation path 28 + - [Link](/docs/components/navigation/link) - For navigation links 29 + - [Content](/docs/components/content/content) - For document content layout 30 +
+72
apps/docs/src/examples/table-of-contents/basic.tsx
··· 1 + import { TableOfContents, Toc } from "@/components/table-of-contents"; 2 + 3 + // Mock table of contents data 4 + const toc: Toc = [ 5 + { 6 + id: "introduction", 7 + value: "Introduction", 8 + depth: 1, 9 + children: [], 10 + }, 11 + { 12 + id: "getting-started", 13 + value: "Getting Started", 14 + depth: 1, 15 + children: [ 16 + { 17 + id: "installation", 18 + value: "Installation", 19 + depth: 2, 20 + children: [], 21 + }, 22 + { 23 + id: "configuration", 24 + value: "Configuration", 25 + depth: 2, 26 + children: [], 27 + }, 28 + ], 29 + }, 30 + { 31 + id: "components", 32 + value: "Components", 33 + depth: 1, 34 + children: [ 35 + { 36 + id: "button", 37 + value: "Button", 38 + depth: 2, 39 + children: [ 40 + { 41 + id: "button-props", 42 + value: "Props", 43 + depth: 3, 44 + children: [], 45 + }, 46 + { 47 + id: "button-examples", 48 + value: "Examples", 49 + depth: 3, 50 + children: [], 51 + }, 52 + ], 53 + }, 54 + { 55 + id: "card", 56 + value: "Card", 57 + depth: 2, 58 + children: [], 59 + }, 60 + ], 61 + }, 62 + { 63 + id: "advanced", 64 + value: "Advanced", 65 + depth: 1, 66 + children: [], 67 + }, 68 + ]; 69 + 70 + export function Basic() { 71 + return <TableOfContents toc={toc} />; 72 + }
+51 -29
apps/docs/src/lib/TableOfContents.tsx packages/hip-ui/src/components/table-of-contents/index.tsx
··· 1 - import { Toc, TocEntry } from "@stefanprobst/rehype-extract-toc"; 1 + "use client"; 2 + 2 3 import * as stylex from "@stylexjs/stylex"; 3 4 import { createContext, use, useEffect, useState } from "react"; 4 5 5 - import { animationDuration } from "../components/theme/animations.stylex"; 6 - import { primaryColor, uiColor } from "../components/theme/color.stylex"; 7 - import { spacing } from "../components/theme/spacing.stylex"; 8 - import { fontSize } from "../components/theme/typography.stylex"; 9 - import { StyleXComponentProps } from "@/components/theme/types"; 6 + import { animationDuration } from "../theme/animations.stylex"; 7 + import { primaryColor, uiColor } from "../theme/color.stylex"; 8 + import { spacing } from "../theme/spacing.stylex"; 9 + import { StyleXComponentProps } from "../theme/types"; 10 + import { fontSize } from "../theme/typography.stylex"; 11 + 12 + export interface TocEntry { 13 + value: string; 14 + depth: number; 15 + id?: string; 16 + children?: Array<TocEntry>; 17 + } 18 + 19 + export type Toc = Array<TocEntry>; 10 20 11 21 const ActiveHeaderIdContext = createContext<string | null>(null); 12 22 const LevelContext = createContext(1); 13 23 14 24 const styles = stylex.create({ 15 25 wrapper: { 26 + gap: spacing["2"], 27 + overflow: "auto", 16 28 boxSizing: "border-box", 17 29 display: "flex", 18 30 flexDirection: "column", 19 31 flexShrink: 0, 20 - gap: spacing["2"], 21 - height: "100vh", 22 - marginTop: spacing["12"], 23 - overflow: "auto", 24 32 paddingBottom: spacing["20"], 25 33 paddingTop: spacing["12"], 34 + }, 35 + sticky: { 26 36 position: "sticky", 37 + height: "100vh", 38 + marginTop: spacing["12"], 27 39 top: 0, 28 40 }, 29 41 itemList: { 42 + margin: 0, 30 43 listStyle: "none", 31 - margin: 0, 32 44 paddingLeft: 0, 33 45 }, 34 46 item: { 47 + textDecoration: "none", 35 48 alignItems: "center", 36 49 backgroundColor: { 37 50 default: "transparent", 38 - ":hover": uiColor.component1, 39 51 ":hover::before": primaryColor.solid1, 40 - }, 41 - borderLeftColor: { 42 - default: uiColor.border1, 52 + ":hover": uiColor.component1, 43 53 }, 44 - borderLeftStyle: "solid", 45 - borderLeftWidth: 1, 46 54 color: { 47 55 default: uiColor.text1, 48 56 }, 49 57 display: "flex", 50 58 fontSize: fontSize["sm"], 51 - height: spacing[8], 52 59 position: "relative", 53 - textDecoration: "none", 54 60 transitionDuration: animationDuration.fast, 55 61 transitionProperty: { 56 62 default: "color, border-left-color", 57 63 "@media (prefers-reduced-motion: reduce)": "none", 58 64 }, 59 65 transitionTimingFunction: "ease-in-out", 66 + borderLeftColor: { 67 + default: uiColor.border1, 68 + }, 69 + borderLeftStyle: "solid", 70 + borderLeftWidth: 1, 71 + height: spacing[8], 60 72 61 73 "::before": { 62 - bottom: 0, 63 74 content: "''", 64 - left: 0, 65 75 position: "absolute", 76 + bottom: 0, 77 + left: 0, 66 78 top: 0, 67 79 width: 1, 68 80 }, ··· 75 87 borderLeftColor: primaryColor.solid1, 76 88 77 89 "::before": { 90 + backgroundColor: primaryColor.solid1, 78 91 content: "''", 79 - bottom: 0, 80 92 position: "absolute", 81 - top: 0, 93 + bottom: 0, 82 94 left: 0, 95 + top: 0, 83 96 width: 1, 84 - backgroundColor: primaryColor.solid1, 85 97 }, 86 98 }, 87 99 }); ··· 115 127 ); 116 128 } 117 129 118 - export function TableOfContents({ 119 - toc, 120 - style, 121 - }: StyleXComponentProps<{ toc: Toc }>) { 130 + /** 131 + * TableOfContents component props. 132 + */ 133 + export interface TableOfContentsProps extends StyleXComponentProps<{ 134 + toc: Toc; 135 + }> { 136 + sticky?: boolean; 137 + } 138 + 139 + /** 140 + * A table of contents component that displays a navigation tree based on document headings. 141 + * Automatically highlights the currently visible heading using IntersectionObserver. 142 + */ 143 + export function TableOfContents({ toc, style, sticky }: TableOfContentsProps) { 122 144 const [activeHeaderId, setActiveHeaderId] = useState<string | null>(null); 123 145 124 146 useEffect(() => { ··· 143 165 144 166 return ( 145 167 <ActiveHeaderIdContext value={activeHeaderId}> 146 - <nav {...stylex.props(styles.wrapper, style)}> 168 + <nav {...stylex.props(styles.wrapper, sticky && styles.sticky, style)}> 147 169 <LevelContext value={1}> 148 170 <ul {...stylex.props(styles.itemList)}> 149 171 {toc.map((item) => (
+4 -2
apps/docs/src/routes/docs.$.tsx
··· 46 46 UnorderedList, 47 47 } from "@/components/typography"; 48 48 import { Text } from "@/components/typography/text"; 49 + import { TableOfContents } from "@/components/table-of-contents"; 49 50 import { CopyToClipboardButton } from "@/lib/CopyToClipboardButton"; 50 - import { TableOfContents } from "@/lib/TableOfContents"; 51 51 52 52 import { animationDuration } from "../components/theme/animations.stylex"; 53 53 import { radius } from "../components/theme/radius.stylex"; ··· 307 307 <Page components={components} /> 308 308 </Suspense> 309 309 </Content> 310 - {toc && <TableOfContents toc={toc} style={styles.tableOfContents} />} 310 + {toc && ( 311 + <TableOfContents toc={toc} style={styles.tableOfContents} sticky /> 312 + )} 311 313 </div> 312 314 ); 313 315 }
+2
packages/hip-ui/src/cli/install.tsx
··· 73 73 import { sliderConfig } from "../components/slider/slider-config.js"; 74 74 import { switchConfig } from "../components/switch/switch-config.js"; 75 75 import { tableConfig } from "../components/table/table-config.js"; 76 + import { tableOfContentsConfig } from "../components/table-of-contents/table-of-contents-config.js"; 76 77 import { tabsConfig } from "../components/tabs/tabs-config.js"; 77 78 import { tagGroupConfig } from "../components/tag-group/tag-group-config.js"; 78 79 import { textAreaConfig } from "../components/text-area/text-area-config.js"; ··· 146 147 headerLayoutConfig, 147 148 sidebarLayoutConfig, 148 149 tableConfig, 150 + tableOfContentsConfig, 149 151 sliderConfig, 150 152 tagGroupConfig, 151 153 progressBarConfig,
+17
packages/hip-ui/src/components/table-of-contents/table-of-contents-config.ts
··· 1 + import { ComponentConfig } from "../../types"; 2 + 3 + export const tableOfContentsConfig: ComponentConfig = { 4 + name: "table-of-contents", 5 + filepath: "./index.tsx", 6 + hipDependencies: [ 7 + "../theme/animations.stylex.tsx", 8 + "../theme/color.stylex.tsx", 9 + "../theme/spacing.stylex.tsx", 10 + "../theme/typography.stylex.tsx", 11 + "../theme/types.ts", 12 + ], 13 + dependencies: { 14 + "@stefanprobst/rehype-extract-toc": "^3.0.0", 15 + }, 16 + }; 17 +