shapes-ui 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +1 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
  4. package/.github/pull_request_template.md +14 -0
  5. package/.github/workflows/pr-preview.yml +75 -0
  6. package/.github/workflows/release.yml +8 -0
  7. package/CHANGELOG.md +30 -0
  8. package/CODE_OF_CONDUCT.md +41 -0
  9. package/CONTRIBUTING.md +52 -0
  10. package/README.md +18 -0
  11. package/SECURITY.md +0 -0
  12. package/content/components/accordion.mdx +13 -0
  13. package/content/components/alert-dialog.mdx +34 -0
  14. package/content/components/autocomplete.mdx +62 -0
  15. package/content/components/avatar.mdx +11 -0
  16. package/content/components/button.mdx +8 -0
  17. package/content/components/checkbox.mdx +11 -0
  18. package/content/components/collapsible.mdx +11 -0
  19. package/content/components/combobox.mdx +33 -0
  20. package/content/components/context-menu.mdx +29 -0
  21. package/content/components/dialog.mdx +33 -0
  22. package/content/components/drawer.mdx +38 -0
  23. package/content/components/field.mdx +23 -2
  24. package/content/components/fieldset.mdx +11 -1
  25. package/content/components/form.mdx +8 -0
  26. package/content/components/input.mdx +4 -0
  27. package/content/components/menu.mdx +27 -0
  28. package/content/components/menubar.mdx +31 -0
  29. package/content/components/meter.mdx +14 -0
  30. package/content/components/navigation-menu.mdx +28 -0
  31. package/content/components/number-field.mdx +25 -0
  32. package/content/components/popover.mdx +22 -0
  33. package/content/components/preview-card.mdx +14 -2
  34. package/content/components/progress.mdx +15 -1
  35. package/content/components/radio.mdx +31 -0
  36. package/content/components/scroll-area.mdx +23 -0
  37. package/content/components/select.mdx +57 -0
  38. package/content/components/separator.mdx +29 -0
  39. package/content/components/slider.mdx +52 -0
  40. package/content/components/switch.mdx +30 -0
  41. package/content/components/tabs.mdx +47 -0
  42. package/content/components/toast.mdx +70 -0
  43. package/content/components/toggle-group.mdx +37 -0
  44. package/content/components/toggle.mdx +46 -2
  45. package/content/components/toolbar.mdx +48 -0
  46. package/content/components/tooltip.mdx +38 -0
  47. package/content/docs/installation.mdx +30 -0
  48. package/content-collections.ts +65 -1
  49. package/dist/cli.js +947 -101
  50. package/examples/__index.tsx +320 -66
  51. package/examples/autocomplete-align.tsx +39 -0
  52. package/examples/autocomplete-controlled.tsx +44 -0
  53. package/examples/autocomplete-groups.tsx +65 -0
  54. package/examples/autocomplete-no-clear.tsx +40 -0
  55. package/examples/avatar-demo.tsx +3 -3
  56. package/examples/checkbox-demo.tsx +1 -1
  57. package/examples/checkbox-form.tsx +3 -3
  58. package/examples/field-custom-control.tsx +33 -9
  59. package/examples/form-demo.tsx +5 -10
  60. package/examples/input-group-with-button.tsx +1 -1
  61. package/examples/menu-advanced.tsx +1 -3
  62. package/examples/menu-align.tsx +19 -16
  63. package/examples/menu-checkbox.tsx +2 -3
  64. package/examples/menu-demo.tsx +1 -3
  65. package/examples/menu-group.tsx +1 -3
  66. package/examples/menu-radio.tsx +1 -3
  67. package/examples/menu-submenu.tsx +2 -3
  68. package/examples/meter-demo.tsx +10 -2
  69. package/examples/meter-flip.tsx +8 -8
  70. package/examples/meter-no-label.tsx +9 -2
  71. package/examples/meter-no-value.tsx +7 -8
  72. package/examples/radio-card.tsx +28 -0
  73. package/examples/radio-demo.tsx +19 -1
  74. package/examples/radio-description.tsx +26 -0
  75. package/examples/radio-orientation.tsx +21 -0
  76. package/examples/select-alignment.tsx +51 -0
  77. package/examples/select-demo.tsx +36 -1
  78. package/examples/select-disabled.tsx +38 -0
  79. package/examples/select-groups.tsx +54 -0
  80. package/examples/select-invalid.tsx +41 -0
  81. package/examples/select-scrollable.tsx +112 -0
  82. package/examples/separator-demo.tsx +13 -0
  83. package/examples/separator-horizontal.tsx +18 -0
  84. package/examples/slider-controlled.tsx +28 -0
  85. package/examples/slider-demo.tsx +3 -1
  86. package/examples/slider-disabled.tsx +7 -0
  87. package/examples/slider-edge.tsx +13 -0
  88. package/examples/slider-multiple.tsx +7 -0
  89. package/examples/slider-range.tsx +5 -0
  90. package/examples/slider-vertical.tsx +10 -0
  91. package/examples/switch-demo.tsx +19 -1
  92. package/examples/switch-disabled.tsx +20 -0
  93. package/examples/switch-sizes.tsx +24 -0
  94. package/examples/switch-with-label.tsx +16 -0
  95. package/examples/tabs-demo.tsx +14 -1
  96. package/examples/tabs-disabled.tsx +21 -0
  97. package/examples/tabs-line.tsx +18 -0
  98. package/examples/tabs-vertical.tsx +13 -0
  99. package/examples/toast-action.tsx +39 -0
  100. package/examples/toast-anchored.tsx +36 -0
  101. package/examples/toast-demo.tsx +27 -1
  102. package/examples/toast-positions.tsx +54 -0
  103. package/examples/toast-promise.tsx +51 -0
  104. package/examples/toast-stacked.tsx +30 -0
  105. package/examples/toast-timeout.tsx +43 -0
  106. package/examples/toast-update.tsx +38 -0
  107. package/examples/toast-variants.tsx +54 -0
  108. package/examples/toggle-controlled.tsx +20 -0
  109. package/examples/toggle-demo.tsx +7 -51
  110. package/examples/toggle-group-demo.tsx +19 -0
  111. package/examples/toggle-group-multiple.tsx +19 -0
  112. package/examples/toggle-icon-fill.tsx +12 -0
  113. package/examples/toolbar-demo.tsx +45 -21
  114. package/examples/toolbar-input-link.tsx +35 -0
  115. package/examples/toolbar-menu.tsx +53 -0
  116. package/examples/tooltip-demo.tsx +48 -0
  117. package/examples/tooltip-positions.tsx +60 -0
  118. package/package.json +19 -18
  119. package/public/base-ui.svg +1 -0
  120. package/public/r/drawer.json +1 -1
  121. package/public/r/field.json +1 -1
  122. package/public/r/meter.json +1 -1
  123. package/public/r/number-field.json +1 -1
  124. package/public/r/progress.json +1 -1
  125. package/public/r/radio.json +1 -1
  126. package/public/r/select.json +1 -1
  127. package/public/r/slider.json +1 -1
  128. package/public/r/switch.json +1 -1
  129. package/public/r/tabs.json +1 -1
  130. package/public/r/toast.json +2 -1
  131. package/public/r/toggle.json +1 -1
  132. package/public/r/toolbar.json +1 -1
  133. package/public/r/tooltip.json +15 -0
  134. package/src/assets/base-ui.svg +1 -0
  135. package/src/commands/add.ts +79 -38
  136. package/src/commands/cli.ts +50 -3
  137. package/src/commands/create.ts +262 -0
  138. package/src/commands/init.ts +45 -12
  139. package/src/commands/palette.ts +55 -0
  140. package/src/components/docs/layout/footer.tsx +2 -2
  141. package/src/components/docs/layout/header.tsx +7 -19
  142. package/src/components/docs/layout/mobile-menu.tsx +26 -78
  143. package/src/components/docs/layout/nav-list.tsx +27 -21
  144. package/src/components/docs/layout/page-header.tsx +52 -7
  145. package/src/components/docs/layout/split-layout.tsx +11 -9
  146. package/src/components/docs/layout/table-of-content.tsx +145 -0
  147. package/src/components/docs/markdown/components.tsx +142 -29
  148. package/src/components/docs/markdown/copy-button.tsx +41 -0
  149. package/src/components/docs/markdown/installation-block.tsx +5 -24
  150. package/src/components/docs/markdown/render-preview.tsx +1 -1
  151. package/src/components/ui/badge.tsx +1 -1
  152. package/src/components/ui/button-group.tsx +1 -1
  153. package/src/components/ui/checkbox.tsx +1 -1
  154. package/src/components/ui/drawer.tsx +1 -1
  155. package/src/components/ui/field.tsx +9 -28
  156. package/src/components/ui/form.tsx +1 -1
  157. package/src/components/ui/meter.tsx +12 -26
  158. package/src/components/ui/number-field.tsx +1 -1
  159. package/src/components/ui/radio.tsx +32 -19
  160. package/src/components/ui/scroll-area.tsx +11 -2
  161. package/src/components/ui/select.tsx +6 -6
  162. package/src/components/ui/slider.tsx +8 -5
  163. package/src/components/ui/switch.tsx +13 -17
  164. package/src/components/ui/tabs.tsx +23 -10
  165. package/src/components/ui/toast.tsx +190 -29
  166. package/src/components/ui/toggle.tsx +1 -1
  167. package/src/components/ui/toolbar.tsx +17 -4
  168. package/src/components/ui/tooltip.tsx +54 -0
  169. package/src/lib/docs-headings.ts +72 -0
  170. package/src/routeTree.gen.ts +60 -3
  171. package/src/routes/__root.tsx +3 -5
  172. package/src/routes/components.$slug.tsx +20 -4
  173. package/src/routes/docs.$slug.tsx +78 -0
  174. package/src/routes/docs.tsx +15 -0
  175. package/src/styles/styles.css +1 -1
  176. package/src/utils/cli-utils.ts +8 -8
  177. package/src/utils/dependency-installer.ts +30 -0
  178. package/src/utils/package-manager.ts +4 -4
  179. package/src/utils/palette.ts +666 -0
  180. package/src/utils/schema.ts +6 -0
@@ -1,6 +1,5 @@
1
1
  import { Link } from "@tanstack/react-router";
2
- import { MenuIcon, SunMoonIcon } from "lucide-react";
3
- import { useState } from "react";
2
+ import { SunMoonIcon } from "lucide-react";
4
3
 
5
4
  import { DocsButton } from "@/components/docs/docs-button";
6
5
  import { useTheme } from "@/components/docs/theme-provider";
@@ -9,17 +8,17 @@ import { MobileMenu } from "./mobile-menu";
9
8
 
10
9
  export function Header() {
11
10
  const { toggleTheme, theme } = useTheme();
12
- const [showMobileMenu, setShowMobileMenu] = useState(false);
13
11
 
14
12
  return (
15
- <header className=" sticky top-0 z-10 h-12 border-b bg-background">
16
- <div className=" container mx-auto flex h-full items-center justify-between border-x px-4">
17
- <Link to="/">
13
+ <header className=" sticky top-0 z-10 h-12">
14
+ <div className=" container mx-auto flex h-full items-center justify-between px-4">
15
+ <Link to="/" className="flex items-center gap-2 font-medium font-heading tracking-wide">
18
16
  <img
19
17
  src={theme === "dark" ? "/shps_white.svg" : "/shps_black.svg"}
20
18
  alt="Shapes UI Logo"
21
- className=" size-6"
19
+ className=" size-5"
22
20
  />
21
+ shapes ui
23
22
  </Link>
24
23
 
25
24
  <div className=" mx-auto hidden gap-4 lg:flex">
@@ -31,19 +30,8 @@ export function Header() {
31
30
  <DocsButton onClick={toggleTheme} variant={"ghost"} className={"hidden lg:inline-flex"}>
32
31
  <SunMoonIcon className=" size-4" />
33
32
  </DocsButton>
34
-
35
- <DocsButton
36
- onClick={() => setShowMobileMenu(!showMobileMenu)}
37
- variant={"ghost"}
38
- size={"sm"}
39
- className={"lg:hidden"}
40
- >
41
- <MenuIcon data-icon="start" />
42
- Navigation
43
- </DocsButton>
33
+ <MobileMenu />
44
34
  </div>
45
-
46
- <MobileMenu open={showMobileMenu} onClose={() => setShowMobileMenu(false)} />
47
35
  </header>
48
36
  );
49
37
  }
@@ -1,86 +1,34 @@
1
1
  import { Link } from "@tanstack/react-router";
2
- import { clsx } from "clsx";
3
2
  import { allComponents } from "content-collections";
4
- import { Suspense, useEffect, useRef } from "react";
5
-
3
+ import { Suspense, useState } from "react";
6
4
  import { SuspenseFallback } from "./suspense-fallback";
5
+ import { Drawer, DrawerPopup, DrawerTrigger } from "@/components/ui/drawer";
6
+ import { Button } from "@/components/ui/button";
7
+ import { MenuIcon } from "lucide-react";
7
8
 
8
- export function MobileMenu({ open, onClose }: { open: boolean; onClose: () => void }) {
9
- const panelRef = useRef<HTMLDivElement | null>(null);
10
-
11
- useEffect(() => {
12
- function onKey(e: KeyboardEvent) {
13
- if (e.key === "Escape") onClose();
14
- }
15
-
16
- if (open) {
17
- document.addEventListener("keydown", onKey);
18
- document.body.style.overflow = "hidden";
19
- // Move focus into the panel for accessibility
20
- setTimeout(() => panelRef.current?.focus(), 0);
21
- } else {
22
- document.body.style.overflow = "";
23
- }
24
-
25
- return () => {
26
- document.removeEventListener("keydown", onKey);
27
- document.body.style.overflow = "";
28
- };
29
- }, [open, onClose]);
9
+ export function MobileMenu() {
30
10
 
31
- return (
32
- <div aria-hidden={!open} className={clsx("fixed inset-0 z-50", !open && "pointer-events-none")}>
33
- <div
34
- className={clsx(
35
- "fixed inset-0 bg-black/40 transition-opacity duration-200",
36
- open ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
37
- "motion-reduce:transition-none",
38
- )}
39
- onClick={onClose}
40
- aria-hidden
41
- />
11
+ const [open, setOpen] = useState(false);
42
12
 
43
- <div
44
- ref={panelRef}
45
- role="dialog"
46
- aria-modal
47
- tabIndex={-1}
48
- className={clsx(
49
- "fixed right-0 bottom-0 left-0 transform border-t bg-popup p-4 transition-transform duration-300 ease-[cubic-bezier(.22,1,.36,1)]",
50
- open ? "translate-y-0" : "translate-y-full",
51
- "motion-reduce:transform-none motion-reduce:transition-none",
52
- )}
53
- >
54
- <nav className="mt-4 flex flex-col gap-2">
55
- <Link to="/components" className="text-sm text-muted-foreground">
56
- Components
57
- </Link>
58
- <Link to="/" className="text-base">
59
- Home
60
- </Link>
61
- <div>
62
- <Link to="/components" className="text-base">
63
- Components
13
+ return <Drawer open={open} onOpenChange={setOpen} >
14
+ <DrawerTrigger className={'flex md:hidden'} render={<Button size={'xs'} variant={'secondary'} >
15
+ <MenuIcon />
16
+ </Button>} />
17
+ <DrawerPopup position="bottom">
18
+ <nav className="mt-4 flex flex-col gap-2">
19
+ <span className="text-sm font-medium text-muted-foreground">Components</span>
20
+ <Suspense fallback={<SuspenseFallback />}>
21
+ {allComponents.map((component) => (
22
+ <Link key={component.slug} to={'/components/$slug'} params={{
23
+ slug: component.slug
24
+ }} onClick={() => setOpen(false)}>
25
+ <Button variant={'link'}>
26
+ {component.title}
27
+ </Button>
64
28
  </Link>
65
- <div className=" ml-4 flex flex-col">
66
- <Suspense fallback={<SuspenseFallback />}>
67
- {[...allComponents]
68
- .sort((a, b) => a.title.localeCompare(b.title))
69
- .map((component) => (
70
- <Link
71
- key={component.slug}
72
- to="/components/$slug"
73
- params={{ slug: component.slug }}
74
- className="text-base"
75
- >
76
- {component.title}
77
- </Link>
78
- ))}
79
- </Suspense>
80
- </div>
81
- </div>
82
- </nav>
83
- </div>
84
- </div>
85
- );
29
+ ))}
30
+ </Suspense>
31
+ </nav>
32
+ </DrawerPopup>
33
+ </Drawer>
86
34
  }
@@ -3,11 +3,12 @@ import { allComponents } from "content-collections";
3
3
  import { ComponentProps } from "react";
4
4
 
5
5
  import { DocsButton } from "@/components/docs/docs-button";
6
+ import { ScrollArea } from "@/components/ui/scroll-area";
6
7
  import { cn } from "@/lib/utils";
7
8
 
8
9
  function NavSection({ title, children }: { title: string; children: React.ReactNode }) {
9
10
  return (
10
- <div className="flex flex-col gap-2">
11
+ <div className="flex min-h-0 flex-1 flex-col gap-2">
11
12
  <h4 className=" pl-2 font-heading text-xs font-medium text-muted-foreground">{title}</h4>
12
13
  {children}
13
14
  </div>
@@ -16,27 +17,32 @@ function NavSection({ title, children }: { title: string; children: React.ReactN
16
17
 
17
18
  export function NavSidebar({ className, ...props }: ComponentProps<"nav">) {
18
19
  return (
19
- <nav className={cn("hidden h-full w-64 flex-col p-4 lg:flex", className)} {...props}>
20
+ <nav className={cn("hidden h-full min-h-0 w-64 flex-col p-4 bg-card lg:flex", className)} {...props}>
20
21
  <NavSection title="Components">
21
- <ul>
22
- {[...allComponents]
23
- .sort((a, b) => a.title.localeCompare(b.title))
24
- .map((component) => (
25
- <li key={component.slug}>
26
- <Link to="/components/$slug" params={{ slug: component.slug }}>
27
- {({ isActive }) => (
28
- <DocsButton
29
- variant={"ghost"}
30
- size={"sm"}
31
- className={cn("w-fit justify-start rounded-none", isActive ? "bg-muted" : "")}
32
- >
33
- {component.title}
34
- </DocsButton>
35
- )}
36
- </Link>
37
- </li>
38
- ))}
39
- </ul>
22
+ <ScrollArea className="h-[98%]">
23
+ <ul>
24
+ {[...allComponents]
25
+ .sort((a, b) => a.title.localeCompare(b.title))
26
+ .map((component) => (
27
+ <li key={component.slug}>
28
+ <Link to="/components/$slug" params={{ slug: component.slug }}>
29
+ {({ isActive }) => (
30
+ <DocsButton
31
+ variant={"ghost"}
32
+ size={"sm"}
33
+ className={cn(
34
+ "w-fit justify-start rounded-none",
35
+ isActive ? "bg-muted" : "",
36
+ )}
37
+ >
38
+ {component.title}
39
+ </DocsButton>
40
+ )}
41
+ </Link>
42
+ </li>
43
+ ))}
44
+ </ul>
45
+ </ScrollArea>
40
46
  </NavSection>
41
47
  </nav>
42
48
  );
@@ -1,33 +1,78 @@
1
+ import { IconMarkdown } from "@tabler/icons-react";
2
+ import { ChevronDownIcon } from "lucide-react";
1
3
  import { ComponentProps } from "react";
2
4
 
5
+ import BaseUI from "@/assets/base-ui.svg?url";
6
+ import { Button } from "@/components/ui/button";
7
+ import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group";
8
+ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@/components/ui/menu";
3
9
  import { cn } from "@/lib/utils";
4
10
 
5
- import CircuitBoard from "./circuit-board";
6
-
7
11
  export function PageHeader({
8
12
  title,
9
13
  subtitle,
10
14
  children,
15
+ baseUILink,
16
+ markdownLink,
11
17
  className,
12
18
  ...props
13
19
  }: ComponentProps<"div"> & {
14
20
  title: string;
15
21
  subtitle?: string;
22
+ baseUILink?: string;
23
+ markdownLink?: string;
16
24
  }) {
25
+ const handleOpenMarkdown = () => {
26
+ if (!markdownLink) {
27
+ return;
28
+ }
29
+
30
+ window.open(markdownLink, "_blank", "noopener,noreferrer");
31
+ };
32
+
17
33
  return (
18
34
  <div
19
35
  className={cn(
20
- "flex h-32 items-center overflow-hidden border-b bg-background md:h-60",
36
+ "flex items-start justify-between overflow-hidden bg-background max-w-7xl mx-auto p-4 lg:px-0 ",
21
37
  className,
22
38
  )}
23
39
  {...props}
24
40
  >
25
- <div className=" flex flex-col gap-1 p-6">
26
- <h1 className="font-heading text-3xl font-bold xl:text-5xl">{title}</h1>
27
- {subtitle && <p className="text-xs text-muted-foreground md:text-sm">{subtitle}</p>}
41
+ <div className=" flex flex-col">
42
+ <h1 className="font-heading text-2xl font-bold xl:text-3xl">{title}</h1>
43
+ {subtitle && <p className="text-xs w-2/3 text-muted-foreground md:text-sm">{subtitle}</p>}
28
44
  {children}
29
45
  </div>
30
- <CircuitBoard className="mask-rtl ml-auto h-full max-w-70 text-foreground/40" />
46
+
47
+ <div className="flex flex-col">
48
+ <ButtonGroup>
49
+ <ButtonGroupText>
50
+ View
51
+ </ButtonGroupText>
52
+ <ButtonGroupSeparator />
53
+ <Menu>
54
+ <MenuTrigger
55
+ render={
56
+ <Button variant={"secondary"} size={"icon"}>
57
+ <ChevronDownIcon />
58
+ </Button>
59
+ }
60
+ />
61
+ <MenuPopup align="end">
62
+ <MenuItem onClick={handleOpenMarkdown} disabled={!markdownLink}>
63
+ <IconMarkdown />
64
+ Markdown
65
+ </MenuItem>
66
+ {baseUILink && (
67
+ <MenuItem>
68
+ <img className="size-4" alt="Base UI" src={BaseUI} />
69
+ Base UI
70
+ </MenuItem>
71
+ )}
72
+ </MenuPopup>
73
+ </Menu>
74
+ </ButtonGroup>
75
+ </div>
31
76
  </div>
32
77
  );
33
78
  }
@@ -1,21 +1,23 @@
1
- import { ComponentProps } from "react";
1
+ import { ComponentProps, useState } from "react";
2
2
 
3
+ import { ScrollArea } from "@/components/ui/scroll-area";
3
4
  import { cn } from "@/lib/utils";
4
5
 
5
6
  import { NavSidebar } from "./nav-list";
7
+ import { TableOfContent } from "./table-of-content";
6
8
 
7
- export function SplitLayout({
8
- children,
9
- className,
9
+ export function SplitLayout({ children, className, ...props }: ComponentProps<"div">) {
10
+ const [contentViewport, setContentViewport] = useState<HTMLDivElement | null>(null);
10
11
 
11
- ...props
12
- }: ComponentProps<"div">) {
13
12
  return (
14
- <div className={cn("flex h-full w-full", className)} {...props}>
13
+ <div className={cn("flex h-full min-h-0 w-full border", className)} {...props}>
15
14
  <NavSidebar />
16
- <main className="min-w-0 flex-1 overflow-auto">
17
- <div className="min-h-full lg:border-l">{children}</div>
15
+ <main className="min-w-0 flex-1 bg-card ">
16
+ <ScrollArea viewportRef={setContentViewport} className="h-full">
17
+ <div className="min-h-full">{children}</div>
18
+ </ScrollArea>
18
19
  </main>
20
+ <TableOfContent contentViewport={contentViewport} />
19
21
  </div>
20
22
  );
21
23
  }
@@ -0,0 +1,145 @@
1
+ import { Link, useRouterState } from "@tanstack/react-router";
2
+ import { allComponents, allDocs } from "content-collections";
3
+ import { useEffect, useMemo, useState } from "react";
4
+
5
+ import { ScrollArea } from "@/components/ui/scroll-area";
6
+ import type { TocHeading } from "@/lib/docs-headings";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ function getRouteContent(pathname: string): TocHeading[] {
10
+ const [section, slug] = pathname.split("/").filter(Boolean);
11
+
12
+ if (!slug) {
13
+ return [];
14
+ }
15
+
16
+ if (section === "components") {
17
+ const current = allComponents.find((component) => component.slug === slug) as
18
+ | { toc?: TocHeading[] }
19
+ | undefined;
20
+
21
+ return current?.toc ?? [];
22
+ }
23
+
24
+ if (section === "docs") {
25
+ const current = allDocs.find((doc) => doc.slug === slug) as { toc?: TocHeading[] } | undefined;
26
+
27
+ return current?.toc ?? [];
28
+ }
29
+
30
+ return [];
31
+ }
32
+
33
+ type TableOfContentProps = {
34
+ contentViewport: HTMLDivElement | null;
35
+ };
36
+
37
+ export const TableOfContent = ({ contentViewport }: TableOfContentProps) => {
38
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
39
+ const toc = useMemo(() => getRouteContent(pathname), [pathname]);
40
+ const [activeId, setActiveId] = useState<string>(toc[0]?.id ?? "");
41
+
42
+ useEffect(() => {
43
+ if (!toc.length) {
44
+ setActiveId("");
45
+ return;
46
+ }
47
+
48
+ let frameId = 0;
49
+ let warmupInterval: number | undefined;
50
+
51
+ const computeActiveHeading = () => {
52
+ const topOffset = contentViewport ? contentViewport.getBoundingClientRect().top + 96 : 120;
53
+
54
+ const renderedHeadings = toc
55
+ .map((heading) => ({
56
+ id: heading.id,
57
+ element: document.getElementById(heading.id),
58
+ }))
59
+ .filter((item) => item.element);
60
+
61
+ if (!renderedHeadings.length) {
62
+ return;
63
+ }
64
+
65
+ let nextActive = renderedHeadings[0]?.id ?? "";
66
+
67
+ for (const item of renderedHeadings) {
68
+ if ((item.element?.getBoundingClientRect().top ?? Number.POSITIVE_INFINITY) <= topOffset) {
69
+ nextActive = item.id;
70
+ continue;
71
+ }
72
+
73
+ break;
74
+ }
75
+
76
+ setActiveId((prev) => (prev === nextActive ? prev : nextActive));
77
+ };
78
+
79
+ const scheduleActiveUpdate = () => {
80
+ if (frameId) {
81
+ cancelAnimationFrame(frameId);
82
+ }
83
+
84
+ frameId = requestAnimationFrame(() => {
85
+ computeActiveHeading();
86
+ frameId = 0;
87
+ });
88
+ };
89
+
90
+ contentViewport?.addEventListener("scroll", scheduleActiveUpdate, { passive: true });
91
+ window.addEventListener("resize", scheduleActiveUpdate);
92
+
93
+ scheduleActiveUpdate();
94
+ warmupInterval = window.setInterval(scheduleActiveUpdate, 80);
95
+ window.setTimeout(() => {
96
+ if (warmupInterval) {
97
+ window.clearInterval(warmupInterval);
98
+ }
99
+ }, 1500);
100
+
101
+ return () => {
102
+ if (frameId) {
103
+ cancelAnimationFrame(frameId);
104
+ }
105
+
106
+ if (warmupInterval) {
107
+ window.clearInterval(warmupInterval);
108
+ }
109
+
110
+ contentViewport?.removeEventListener("scroll", scheduleActiveUpdate);
111
+ window.removeEventListener("resize", scheduleActiveUpdate);
112
+ };
113
+ }, [contentViewport, pathname, toc]);
114
+
115
+ return (
116
+ <aside className="hidden h-full min-h-0 w-64 flex-col p-4 xl:flex">
117
+ <h4 className="pl-2 font-heading text-xs font-medium text-muted-foreground">On this page</h4>
118
+ <ScrollArea className="mt-2 h-[98%]">
119
+ <ul className="space-y-1">
120
+ {toc.map((heading) => {
121
+ const isActive = activeId === heading.id;
122
+
123
+ return (
124
+ <li key={heading.id}>
125
+ <Link
126
+ to="."
127
+ hash={heading.id}
128
+ style={{ paddingLeft: `${(heading.level - 1) * 10}px` }}
129
+ className={cn(
130
+ "block truncate rounded px-2 py-1.5 text-xs transition-colors",
131
+ isActive
132
+ ? "bg-muted text-foreground"
133
+ : "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
134
+ )}
135
+ >
136
+ {heading.title}
137
+ </Link>
138
+ </li>
139
+ );
140
+ })}
141
+ </ul>
142
+ </ScrollArea>
143
+ </aside>
144
+ );
145
+ };