@terrafirmagreg-team/ui 0.0.0-stage → 1.2.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 (225) hide show
  1. package/package.json +93 -3
  2. package/src/components/blocks/button/icon-button.tsx +49 -0
  3. package/src/components/blocks/canvas/chevron-edge.tsx +242 -0
  4. package/src/components/blocks/canvas/flow-canvas.tsx +181 -0
  5. package/src/components/blocks/canvas/flow-toolbar.tsx +33 -0
  6. package/src/components/blocks/canvas/flow-viewport.tsx +244 -0
  7. package/src/components/blocks/fit-box.tsx +26 -0
  8. package/src/components/blocks/git/github-info.tsx +78 -0
  9. package/src/components/blocks/lang/content.tsx +52 -0
  10. package/src/components/blocks/lang/flag.tsx +25 -0
  11. package/src/components/blocks/lang/link.tsx +23 -0
  12. package/src/components/blocks/lang/redirect.tsx +48 -0
  13. package/src/components/blocks/lang/switcher.tsx +54 -0
  14. package/src/components/blocks/lang/untranslated-callout.tsx +29 -0
  15. package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
  16. package/src/components/blocks/link/link-preview.tsx +257 -0
  17. package/src/components/blocks/list/row.tsx +92 -0
  18. package/src/components/blocks/media/glb/model.tsx +244 -0
  19. package/src/components/blocks/media/glb/scene.tsx +373 -0
  20. package/src/components/blocks/media/glb/viewer.tsx +52 -0
  21. package/src/components/blocks/media/image.tsx +136 -0
  22. package/src/components/blocks/media/slideshow.tsx +178 -0
  23. package/src/components/blocks/nav/breadcrumb.tsx +51 -0
  24. package/src/components/blocks/nav/copy-menu.tsx +37 -0
  25. package/src/components/blocks/nav/link.tsx +86 -0
  26. package/src/components/blocks/nav/site-menu.tsx +78 -0
  27. package/src/components/blocks/nav/site-title.tsx +61 -0
  28. package/src/components/blocks/nav/switcher.tsx +206 -0
  29. package/src/components/blocks/page/empty-state.tsx +32 -0
  30. package/src/components/blocks/page/heading.tsx +54 -0
  31. package/src/components/blocks/page/pager.tsx +220 -0
  32. package/src/components/blocks/readability/fields.tsx +250 -0
  33. package/src/components/blocks/readability/menu.tsx +247 -0
  34. package/src/components/blocks/readability/track.tsx +165 -0
  35. package/src/components/blocks/search/command.tsx +80 -0
  36. package/src/components/blocks/search/dialog.tsx +724 -0
  37. package/src/components/blocks/search/provider.tsx +118 -0
  38. package/src/components/blocks/sidebar.tsx +406 -0
  39. package/src/components/blocks/sprite.tsx +231 -0
  40. package/src/components/blocks/text/format-text.tsx +110 -0
  41. package/src/components/blocks/theme/toggle.tsx +75 -0
  42. package/src/components/blocks/tooltip/hint.tsx +28 -0
  43. package/src/components/blocks/tooltip/lazy.tsx +41 -0
  44. package/src/components/blocks/viewer/controls.tsx +284 -0
  45. package/src/components/blocks/viewer/float-bar.tsx +51 -0
  46. package/src/components/layouts/layout.tsx +94 -0
  47. package/src/components/layouts/left-sidebar/content.tsx +63 -0
  48. package/src/components/layouts/left-sidebar/context.tsx +55 -0
  49. package/src/components/layouts/left-sidebar/expand.tsx +23 -0
  50. package/src/components/layouts/left-sidebar/footer.tsx +111 -0
  51. package/src/components/layouts/left-sidebar/index.ts +29 -0
  52. package/src/components/layouts/left-sidebar/items.tsx +103 -0
  53. package/src/components/layouts/left-sidebar/search.tsx +75 -0
  54. package/src/components/layouts/left-sidebar/sidebar.tsx +189 -0
  55. package/src/components/layouts/left-sidebar/tree.tsx +157 -0
  56. package/src/components/layouts/right-sidebar.tsx +96 -0
  57. package/src/components/layouts/tree-context.tsx +42 -0
  58. package/src/components/mdx/accordion.tsx +185 -0
  59. package/src/components/mdx/banner.tsx +157 -0
  60. package/src/components/mdx/callout.tsx +97 -0
  61. package/src/components/mdx/card.tsx +56 -0
  62. package/src/components/mdx/codeblock.tsx +250 -0
  63. package/src/components/mdx/collapsible.tsx +33 -0
  64. package/src/components/mdx/files.tsx +63 -0
  65. package/src/components/mdx/glow-header.tsx +54 -0
  66. package/src/components/mdx/gradient-text.tsx +28 -0
  67. package/src/components/mdx/icon.tsx +35 -0
  68. package/src/components/mdx/image-zoom.tsx +315 -0
  69. package/src/components/mdx/math.tsx +30 -0
  70. package/src/components/mdx/mermaid.tsx +23 -0
  71. package/src/components/mdx/multiblock.tsx +19 -0
  72. package/src/components/mdx/preview.tsx +101 -0
  73. package/src/components/mdx/steps.tsx +10 -0
  74. package/src/components/mdx/table.tsx +14 -0
  75. package/src/components/mdx/tabs.tsx +354 -0
  76. package/src/components/mdx/type-table.tsx +171 -0
  77. package/src/components/provider.tsx +139 -0
  78. package/src/components/ui/accordion.tsx +71 -0
  79. package/src/components/ui/alert-dialog.tsx +187 -0
  80. package/src/components/ui/alert.tsx +75 -0
  81. package/src/components/ui/aspect-ratio.tsx +22 -0
  82. package/src/components/ui/attachment.tsx +207 -0
  83. package/src/components/ui/avatar.tsx +108 -0
  84. package/src/components/ui/badge.tsx +51 -0
  85. package/src/components/ui/breadcrumb.tsx +124 -0
  86. package/src/components/ui/bubble.tsx +127 -0
  87. package/src/components/ui/button-group.tsx +87 -0
  88. package/src/components/ui/button.tsx +57 -0
  89. package/src/components/ui/calendar.tsx +221 -0
  90. package/src/components/ui/card.tsx +102 -0
  91. package/src/components/ui/carousel.tsx +242 -0
  92. package/src/components/ui/chart.tsx +371 -0
  93. package/src/components/ui/checkbox.tsx +28 -0
  94. package/src/components/ui/collapsible.tsx +21 -0
  95. package/src/components/ui/combobox.tsx +297 -0
  96. package/src/components/ui/command.tsx +198 -0
  97. package/src/components/ui/context-menu.tsx +270 -0
  98. package/src/components/ui/dialog.tsx +160 -0
  99. package/src/components/ui/direction.tsx +6 -0
  100. package/src/components/ui/drawer.tsx +227 -0
  101. package/src/components/ui/dropdown-menu.tsx +269 -0
  102. package/src/components/ui/empty.tsx +103 -0
  103. package/src/components/ui/field.tsx +238 -0
  104. package/src/components/ui/hover-card.tsx +50 -0
  105. package/src/components/ui/input-group.tsx +158 -0
  106. package/src/components/ui/input-otp.tsx +86 -0
  107. package/src/components/ui/input.tsx +19 -0
  108. package/src/components/ui/item.tsx +201 -0
  109. package/src/components/ui/kbd.tsx +26 -0
  110. package/src/components/ui/label.tsx +19 -0
  111. package/src/components/ui/marker.tsx +70 -0
  112. package/src/components/ui/menubar.tsx +280 -0
  113. package/src/components/ui/message-scroller.tsx +131 -0
  114. package/src/components/ui/message.tsx +91 -0
  115. package/src/components/ui/native-select.tsx +60 -0
  116. package/src/components/ui/navigation-menu.tsx +167 -0
  117. package/src/components/ui/pagination.tsx +130 -0
  118. package/src/components/ui/popover.tsx +91 -0
  119. package/src/components/ui/progress.tsx +82 -0
  120. package/src/components/ui/questionnaire.tsx +324 -0
  121. package/src/components/ui/radio-group.tsx +37 -0
  122. package/src/components/ui/resizable.tsx +49 -0
  123. package/src/components/ui/scroll-area.tsx +54 -0
  124. package/src/components/ui/select.tsx +200 -0
  125. package/src/components/ui/separator.tsx +24 -0
  126. package/src/components/ui/sheet.tsx +138 -0
  127. package/src/components/ui/sidebar.tsx +730 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +51 -0
  130. package/src/components/ui/spinner.tsx +10 -0
  131. package/src/components/ui/switch.tsx +31 -0
  132. package/src/components/ui/table.tsx +115 -0
  133. package/src/components/ui/tabs.tsx +81 -0
  134. package/src/components/ui/textarea.tsx +17 -0
  135. package/src/components/ui/toast.tsx +234 -0
  136. package/src/components/ui/toggle-group.tsx +89 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +65 -0
  139. package/src/hooks/canvas/use-flow-hover.ts +19 -0
  140. package/src/hooks/canvas/use-flow-viewport-persist.ts +34 -0
  141. package/src/hooks/page/use-fit-pages.ts +92 -0
  142. package/src/hooks/page/use-location-search.ts +31 -0
  143. package/src/hooks/page/use-page-preview.ts +41 -0
  144. package/src/hooks/page/use-paged-list.ts +21 -0
  145. package/src/hooks/readability/use-readability.ts +87 -0
  146. package/src/hooks/search/use-dialog.ts +42 -0
  147. package/src/hooks/search/use-highlight-query.ts +58 -0
  148. package/src/hooks/search/use-provider.ts +26 -0
  149. package/src/hooks/use-async-value.ts +60 -0
  150. package/src/hooks/use-close-on-scroll.ts +24 -0
  151. package/src/hooks/use-copy-button.ts +47 -0
  152. package/src/hooks/use-cycle-index.ts +76 -0
  153. package/src/hooks/use-document-title.ts +10 -0
  154. package/src/hooks/use-element-size.ts +36 -0
  155. package/src/hooks/use-external-meta.ts +133 -0
  156. package/src/hooks/use-hot-key.ts +34 -0
  157. package/src/hooks/use-hover-follow.ts +55 -0
  158. package/src/hooks/use-in-view.ts +31 -0
  159. package/src/hooks/use-lang.ts +9 -0
  160. package/src/hooks/use-last-value.ts +16 -0
  161. package/src/hooks/use-layout.ts +19 -0
  162. package/src/hooks/use-marquee.ts +52 -0
  163. package/src/hooks/use-media-query.ts +15 -0
  164. package/src/hooks/use-mobile.ts +20 -0
  165. package/src/hooks/use-modifier-keys.ts +32 -0
  166. package/src/hooks/use-on-change.ts +36 -0
  167. package/src/hooks/use-pane-keys.ts +29 -0
  168. package/src/hooks/use-sidebar.ts +64 -0
  169. package/src/hooks/use-swipe.ts +27 -0
  170. package/src/hooks/use-tree.ts +100 -0
  171. package/src/hooks/viewer/use-in-float-bar.ts +7 -0
  172. package/src/hooks/viewer/use-viewer-prefs.ts +78 -0
  173. package/src/lib/canvas/edge.ts +202 -0
  174. package/src/lib/canvas/geometry.ts +54 -0
  175. package/src/lib/canvas/state.ts +48 -0
  176. package/src/lib/canvas/viewport.ts +38 -0
  177. package/src/lib/canvas/xyflow.ts +2 -0
  178. package/src/lib/config.ts +106 -0
  179. package/src/lib/mdx/callout.ts +21 -0
  180. package/src/lib/mdx/gradient.ts +16 -0
  181. package/src/lib/mdx/mermaid.ts +15 -0
  182. package/src/lib/media/crop-opaque.ts +77 -0
  183. package/src/lib/media/nearby-indexes.ts +7 -0
  184. package/src/lib/media/trim-transparent.ts +118 -0
  185. package/src/lib/readability/config.ts +11 -0
  186. package/src/lib/readability/spotlight.ts +187 -0
  187. package/src/lib/readability/store.ts +147 -0
  188. package/src/lib/tree.ts +90 -0
  189. package/src/lib/utils/cn.ts +2 -0
  190. package/src/lib/utils/format.ts +22 -0
  191. package/src/lib/utils/highlight-style.ts +10 -0
  192. package/src/lib/utils/history.ts +10 -0
  193. package/src/lib/utils/merge-refs.ts +14 -0
  194. package/src/lib/utils/nav-active.ts +59 -0
  195. package/src/lib/utils/stored-value.ts +137 -0
  196. package/src/lib/utils/urls.ts +18 -0
  197. package/src/locales/en_us.json +110 -0
  198. package/src/locales/pl_pl.json +33 -0
  199. package/src/locales/ru_ru.json +110 -0
  200. package/src/locales/uk_ua.json +33 -0
  201. package/src/locales/zh_cn.json +15 -0
  202. package/src/locales/zh_tw.json +15 -0
  203. package/src/locales.ts +6 -0
  204. package/src/types/canvas.ts +54 -0
  205. package/src/types/glb.ts +26 -0
  206. package/src/types/mdx-directives.ts +106 -0
  207. package/src/types/nav.ts +96 -0
  208. package/src/types/preview.ts +15 -0
  209. package/src/types/readability.ts +29 -0
  210. package/src/types/search.ts +57 -0
  211. package/src/types/tree.ts +19 -0
  212. package/src/types/viewer.ts +54 -0
  213. package/styles/content/base.css +198 -0
  214. package/styles/content/default-colors.css +51 -0
  215. package/styles/content/shiki.css +107 -0
  216. package/styles/content/typography.mjs +3637 -0
  217. package/styles/content.css +5 -0
  218. package/styles/image-zoom.css +83 -0
  219. package/styles/layout.css +28 -0
  220. package/styles/left-sidebar.css +140 -0
  221. package/styles/readability.css +50 -0
  222. package/styles/right-sidebar.css +6 -0
  223. package/styles/workspace.css +26 -0
  224. package/styles.css +430 -0
  225. package/README.md +0 -3
@@ -0,0 +1,29 @@
1
+ export type { SettingsMenuComponent } from './context';
2
+ export {
3
+ LeftSidebarPortalProvider,
4
+ LeftSidebarPortal,
5
+ } from './context';
6
+ export { LeftSidebar } from './sidebar';
7
+ export { LeftSidebarExpand } from './expand';
8
+ export {
9
+ LeftSidebarSearchTrigger,
10
+ LeftSidebarSearch,
11
+ } from './search';
12
+ export { LeftSidebarContent } from './content';
13
+ export {
14
+ LeftSidebarFooter,
15
+ LeftSidebarLanguage,
16
+ LeftSidebarPlate,
17
+ LeftSidebarActions,
18
+ } from './footer';
19
+ export {
20
+ LeftSidebarFolder,
21
+ LeftSidebarSeparator,
22
+ LeftSidebarItem,
23
+ LeftSidebarFolderTrigger,
24
+ LeftSidebarFolderLink,
25
+ LeftSidebarFolderContent,
26
+ LeftSidebarLabel,
27
+ } from './items';
28
+ export { LeftSidebarTree, LeftSidebarTreeItem } from './tree';
29
+
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+ import type { ComponentProps } from 'react';
3
+ import { useFolderDepth, useSidebar } from '@terrafirmagreg-team/ui/hooks/use-sidebar';
4
+ import { SidebarFolder, SidebarFolderContent as BaseFolderContent, SidebarFolderLink as BaseFolderLink, SidebarFolderTrigger as BaseFolderTrigger, SidebarItem as BaseItem } from '@terrafirmagreg-team/ui/components/blocks/sidebar';
5
+ import { Separator } from '@terrafirmagreg-team/ui/components/ui/separator';
6
+
7
+ export { SidebarFolder as LeftSidebarFolder };
8
+
9
+ export function LeftSidebarSeparator({
10
+ orientation = 'horizontal',
11
+ ...props
12
+ }: ComponentProps<typeof Separator>) {
13
+ return (
14
+ <Separator
15
+ orientation={orientation}
16
+ data-slot="left-sidebar-separator"
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ export function LeftSidebarItem({
23
+ className,
24
+ onClick,
25
+ ...props
26
+ }: ComponentProps<typeof BaseItem>) {
27
+ const { setOpen } = useSidebar();
28
+ return (
29
+ <BaseItem
30
+ data-slot="left-sidebar-item"
31
+ onClick={(event) => {
32
+ setOpen(false);
33
+ onClick?.(event);
34
+ }}
35
+ className={className}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ export function LeftSidebarFolderTrigger({
42
+ className,
43
+ ...props
44
+ }: ComponentProps<typeof BaseFolderTrigger>) {
45
+ return (
46
+ <BaseFolderTrigger
47
+ data-slot="left-sidebar-folder-trigger"
48
+ className={className}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export function LeftSidebarFolderLink({
55
+ className,
56
+ onClick,
57
+ ...props
58
+ }: ComponentProps<typeof BaseFolderLink>) {
59
+ const { setOpen } = useSidebar();
60
+ return (
61
+ <BaseFolderLink
62
+ data-slot="left-sidebar-folder-link"
63
+ onClick={(event) => {
64
+ setOpen(false);
65
+ onClick?.(event);
66
+ }}
67
+ className={className}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ export function LeftSidebarFolderContent({
74
+ children,
75
+ className,
76
+ ...props
77
+ }: ComponentProps<typeof BaseFolderContent>) {
78
+ return (
79
+ <BaseFolderContent
80
+ data-slot="left-sidebar-folder-content"
81
+ className={className}
82
+ {...props}
83
+ >
84
+ {children}
85
+ </BaseFolderContent>
86
+ );
87
+ }
88
+
89
+ export function LeftSidebarLabel({
90
+ className,
91
+ ...props
92
+ }: ComponentProps<'p'>) {
93
+ const depth = useFolderDepth();
94
+ return (
95
+ <p
96
+ data-slot="left-sidebar-label"
97
+ data-nested={depth > 0 || undefined}
98
+ className={className}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import type { ComponentProps, ReactNode } from 'react';
4
+ import { Search } from 'lucide-react';
5
+ import { useSearchContext } from '@terrafirmagreg-team/ui/hooks/search/use-provider';
6
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
7
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
8
+
9
+ export function LeftSidebarSearchTrigger({
10
+ label,
11
+ hint,
12
+ keys = ['Ctrl', 'K'],
13
+ icon,
14
+ className,
15
+ ...props
16
+ }: ComponentProps<'button'> & {
17
+ label?: ReactNode;
18
+ hint?: ReactNode;
19
+ keys?: readonly ReactNode[];
20
+ icon?: ReactNode;
21
+ }) {
22
+ return (
23
+ <button
24
+ type="button"
25
+ data-slot="left-sidebar-search-trigger"
26
+ className={cn(
27
+ 'flex h-10 w-full items-center gap-3 rounded-xl border bg-secondary/50 ps-2.5 pe-2 text-sm text-muted-foreground outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0',
28
+ className,
29
+ )}
30
+ {...props}
31
+ >
32
+ {icon ?? <Search className="size-4" aria-hidden />}
33
+ <span className="flex-1 truncate text-start">{label}</span>
34
+ {hint ?? (
35
+ <span className="ms-auto inline-flex gap-0.5" aria-hidden>
36
+ {keys.map((k, i) => (
37
+ <kbd key={i} className="rounded-md border bg-background px-1.5">
38
+ {k}
39
+ </kbd>
40
+ ))}
41
+ </span>
42
+ )}
43
+ </button>
44
+ );
45
+ }
46
+
47
+ export function LeftSidebarSearch({
48
+ variant = 'full',
49
+ }: {
50
+ variant?: 'full' | 'icon';
51
+ }) {
52
+ const { enabled, hotKey, setOpenSearch } = useSearchContext();
53
+ const t = useTranslations();
54
+ if (!enabled) return null;
55
+ if (variant === 'icon')
56
+ return (
57
+ <IconButton
58
+ label={t('Search', { note: 'ui.search_trigger' })}
59
+ variant="ghost"
60
+ size="icon-sm"
61
+ tooltip={false}
62
+ onClick={() => setOpenSearch(true)}
63
+ >
64
+ <Search />
65
+ </IconButton>
66
+ );
67
+ return (
68
+ <LeftSidebarSearchTrigger
69
+ label={t('Search', { note: 'ui.search_trigger' })}
70
+ onClick={() => setOpenSearch(true)}
71
+ keys={hotKey?.map((k) => k.display)}
72
+ />
73
+ );
74
+ }
75
+
@@ -0,0 +1,189 @@
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
4
+ import type { ComponentProps, ReactNode } from 'react';
5
+ import { SidebarIcon } from 'lucide-react';
6
+ import { useSidebar } from '@terrafirmagreg-team/ui/hooks/use-sidebar';
7
+ import { SidebarCollapseTrigger, SidebarDrawerContent, SidebarDrawerOverlay, SidebarTrigger } from '@terrafirmagreg-team/ui/components/blocks/sidebar';
8
+ import { useLayout } from '@terrafirmagreg-team/ui/hooks/use-layout';
9
+ import { LeftSidebarContent } from './content';
10
+ import { LeftSidebarFooter } from './footer';
11
+ import type { LeftSidebarParts } from './context';
12
+
13
+ export function LeftSidebar({
14
+ className,
15
+ controls,
16
+ content,
17
+ footer,
18
+ menu,
19
+ match,
20
+ navKeys,
21
+ settings,
22
+ ...props
23
+ }: LeftSidebarParts & Omit<ComponentProps<'aside'>, 'content'>) {
24
+ const { mode } = useSidebar();
25
+ const parts = { controls, content, footer, menu, match, navKeys, settings };
26
+ return (
27
+ <>
28
+ {mode === 'full' && (
29
+ <LeftSidebarAside className={className} {...parts} {...props} />
30
+ )}
31
+ <LeftSidebarDrawer {...parts} />
32
+ </>
33
+ );
34
+ }
35
+
36
+ function LeftSidebarAside({
37
+ className,
38
+ controls,
39
+ content,
40
+ footer,
41
+ menu,
42
+ match,
43
+ navKeys,
44
+ settings,
45
+ ...props
46
+ }: LeftSidebarParts & Omit<ComponentProps<'aside'>, 'content'>) {
47
+ const { collapsed } = useSidebar();
48
+ const { navTitle: Title } = useLayout();
49
+ return (
50
+ <aside
51
+ id="sidebar"
52
+ data-slot="left-sidebar"
53
+ data-collapsed={collapsed || undefined}
54
+ inert={collapsed}
55
+ className={cn(
56
+ 'flex min-h-0 w-67 flex-col overflow-hidden text-sm transition-[width,translate,opacity] duration-300 ease-expo *:transition-[opacity,translate] *:duration-200 data-[collapsed=true]:w-2 data-[collapsed=true]:*:pointer-events-none data-[collapsed=true]:*:opacity-0 data-[collapsed=true]:*:-translate-x-3 [grid-area:sidebar] md:layout:[--sidebar-width:268px] max-md:hidden',
57
+ collapsed && 'md:layout:[--sidebar-col:--spacing(2)]',
58
+ className,
59
+ )}
60
+ {...props}
61
+ >
62
+ <div className="flex min-h-0 flex-1 flex-col w-(--sidebar-width)">
63
+ <LeftSidebarRegions
64
+ title={
65
+ <Title className="inline-flex items-center gap-2 min-w-0 me-auto text-nav-title font-semibold" />
66
+ }
67
+ collapse={
68
+ <SidebarCollapseTrigger
69
+ className={cn(
70
+ buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
71
+ 'size-8 text-muted-foreground! aria-expanded:bg-transparent',
72
+ )}
73
+ >
74
+ <SidebarIcon />
75
+ </SidebarCollapseTrigger>
76
+ }
77
+ controls={controls}
78
+ content={content}
79
+ footer={footer}
80
+ menu={menu}
81
+ match={match}
82
+ navKeys={navKeys}
83
+ settings={settings}
84
+ />
85
+ </div>
86
+ </aside>
87
+ );
88
+ }
89
+
90
+ function LeftSidebarDrawer(props: LeftSidebarParts) {
91
+ const { navTitle: Title } = useLayout();
92
+ return (
93
+ <>
94
+ <SidebarDrawerOverlay className="fixed z-40 inset-0 backdrop-blur-xs data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out" />
95
+ <SidebarDrawerContent className="fixed z-40 inset-e-0 inset-y-0 flex flex-col w-[85%] max-w-95 text-nav-title bg-background border-s shadow-lg data-[state=open]:animate-sidebar-in data-[state=closed]:animate-sidebar-out">
96
+ <LeftSidebarRegions
97
+ headerClassName="h-(--header-height) my-2 ps-5.5 pe-3"
98
+ title={
99
+ <Title className="inline-flex items-center gap-2 min-w-0 me-auto text-nav-title font-semibold" />
100
+ }
101
+ collapse={
102
+ <SidebarTrigger
103
+ className={cn(
104
+ buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
105
+ 'size-8 text-muted-foreground',
106
+ )}
107
+ >
108
+ <SidebarIcon />
109
+ </SidebarTrigger>
110
+ }
111
+ contentClassName="mt-2"
112
+ {...props}
113
+ />
114
+ </SidebarDrawerContent>
115
+ </>
116
+ );
117
+ }
118
+
119
+ function LeftSidebarRegions({
120
+ title,
121
+ collapse,
122
+ controls,
123
+ content,
124
+ footer,
125
+ menu,
126
+ match,
127
+ navKeys,
128
+ settings,
129
+ headerClassName,
130
+ controlsClassName,
131
+ contentClassName,
132
+ viewportClassName,
133
+ footerClassName,
134
+ }: LeftSidebarParts & {
135
+ title?: ReactNode;
136
+ collapse?: ReactNode;
137
+ headerClassName?: string;
138
+ controlsClassName?: string;
139
+ contentClassName?: string;
140
+ viewportClassName?: string;
141
+ footerClassName?: string;
142
+ }) {
143
+ return (
144
+ <>
145
+ {(title != null || collapse != null || menu != null) && (
146
+ <header
147
+ data-slot="left-sidebar-header"
148
+ className={cn(
149
+ 'my-2 flex h-14 shrink-0 items-center gap-2 ps-5.5 pe-3',
150
+ headerClassName,
151
+ )}
152
+ >
153
+ {title}
154
+ {menu}
155
+ {collapse}
156
+ </header>
157
+ )}
158
+ <div
159
+ data-slot="left-sidebar-controls"
160
+ className={cn(
161
+ 'flex shrink-0 flex-col gap-2 px-3 empty:hidden',
162
+ controlsClassName,
163
+ )}
164
+ >
165
+ {controls}
166
+ </div>
167
+ <LeftSidebarContent
168
+ className={contentClassName}
169
+ viewportClassName={viewportClassName}
170
+ match={match}
171
+ navKeys={navKeys}
172
+ >
173
+ {content}
174
+ </LeftSidebarContent>
175
+ {(footer ?? <LeftSidebarFooter settings={settings} />) != null && (
176
+ <footer
177
+ data-slot="left-sidebar-footer"
178
+ className={cn(
179
+ 'flex shrink-0 flex-col gap-1.5 px-3 py-2.5 empty:hidden',
180
+ footerClassName,
181
+ )}
182
+ >
183
+ {footer ?? <LeftSidebarFooter settings={settings} />}
184
+ </footer>
185
+ )}
186
+ </>
187
+ );
188
+ }
189
+
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ use,
5
+ useMemo,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import { Fragment } from 'react';
9
+ import { usePathname } from 'next/navigation';
10
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
11
+ import { useTreeContext, useTreePath } from '@terrafirmagreg-team/ui/hooks/use-tree';
12
+ import { useLocaleRegistry } from '@terrafirmagreg-team/translate/hooks/use-locale-registry';
13
+ import { useLocationSearch } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
14
+ import {
15
+ hasActiveDescendant,
16
+ isNavItemActive,
17
+ langScopeKey,
18
+ type NavItemMatch,
19
+ } from '@terrafirmagreg-team/ui/lib/utils/nav-active';
20
+ import {
21
+ LeftSidebarFolder,
22
+ LeftSidebarFolderContent,
23
+ LeftSidebarFolderLink,
24
+ LeftSidebarFolderTrigger,
25
+ LeftSidebarItem,
26
+ LeftSidebarLabel,
27
+ } from './items';
28
+
29
+ type TreeRenderer = (props: {
30
+ item: PageTree.Node;
31
+ children: ReactNode;
32
+ }) => ReactNode;
33
+
34
+ const RendererContext = createContext<{
35
+ Separator?: TreeRenderer;
36
+ Item?: TreeRenderer;
37
+ Folder?: TreeRenderer;
38
+ pathname: string;
39
+ search: string;
40
+ match?: NavItemMatch;
41
+ navKeys?: ReadonlySet<string>;
42
+ } | null>(null);
43
+
44
+ export function LeftSidebarTree({
45
+ tree,
46
+ match,
47
+ navKeys,
48
+ Separator,
49
+ Item,
50
+ Folder,
51
+ }: {
52
+ tree?: PageTree.Root;
53
+ match?: NavItemMatch;
54
+ navKeys?: ReadonlySet<string>;
55
+ Separator?: TreeRenderer;
56
+ Item?: TreeRenderer;
57
+ Folder?: TreeRenderer;
58
+ }) {
59
+ const { root } = useTreeContext();
60
+ const pathname = usePathname();
61
+ const search = useLocationSearch();
62
+ const { languages } = useLocaleRegistry();
63
+ const resolved = tree ?? root;
64
+ return (
65
+ <RendererContext
66
+ value={useMemo(
67
+ () => ({ Separator, Item, Folder, pathname, search, match, navKeys }),
68
+ [Separator, Item, Folder, pathname, search, match, navKeys],
69
+ )}
70
+ >
71
+ <Fragment key={langScopeKey(resolved.$id, languages)}>
72
+ {resolved.children.map((node, i) => (
73
+ <LeftSidebarTreeItem key={i} node={node} />
74
+ ))}
75
+ </Fragment>
76
+ </RendererContext>
77
+ );
78
+ }
79
+
80
+ export function LeftSidebarTreeItem({ node }: { node: PageTree.Node }) {
81
+ const ctx = use(RendererContext);
82
+ const path = useTreePath();
83
+ if (!ctx) return null;
84
+ const {
85
+ Separator: CustomSeparator,
86
+ Item: CustomItem,
87
+ Folder: CustomFolder,
88
+ pathname,
89
+ search,
90
+ match,
91
+ navKeys,
92
+ } = ctx;
93
+ const isActive: NavItemMatch =
94
+ match ?? ((url) => isNavItemActive(url, pathname, search, navKeys));
95
+ if (node.type === 'separator') {
96
+ if (CustomSeparator)
97
+ return <CustomSeparator item={node}>{null}</CustomSeparator>;
98
+ return (
99
+ <LeftSidebarLabel>
100
+ {node.icon}
101
+ {node.name}
102
+ </LeftSidebarLabel>
103
+ );
104
+ }
105
+ if (node.type === 'folder') {
106
+ if (CustomFolder)
107
+ return (
108
+ <CustomFolder item={node}>
109
+ {node.children.map((child, i) => (
110
+ <LeftSidebarTreeItem key={i} node={child} />
111
+ ))}
112
+ </CustomFolder>
113
+ );
114
+ return (
115
+ <LeftSidebarFolder
116
+ collapsible={node.collapsible}
117
+ active={
118
+ path.includes(node) || hasActiveDescendant(node, isActive)
119
+ }
120
+ defaultOpen={node.defaultOpen}
121
+ >
122
+ {node.index ? (
123
+ <LeftSidebarFolderLink
124
+ href={node.index.url}
125
+ active={isActive(node.index.url)}
126
+ external={node.index.external}
127
+ >
128
+ {node.icon}
129
+ {node.name}
130
+ </LeftSidebarFolderLink>
131
+ ) : (
132
+ <LeftSidebarFolderTrigger>
133
+ {node.icon}
134
+ {node.name}
135
+ </LeftSidebarFolderTrigger>
136
+ )}
137
+ <LeftSidebarFolderContent>
138
+ {node.children.map((child, i) => (
139
+ <LeftSidebarTreeItem key={i} node={child} />
140
+ ))}
141
+ </LeftSidebarFolderContent>
142
+ </LeftSidebarFolder>
143
+ );
144
+ }
145
+ if (CustomItem) return <CustomItem item={node}>{null}</CustomItem>;
146
+ return (
147
+ <LeftSidebarItem
148
+ href={node.url}
149
+ external={node.external}
150
+ active={isActive(node.url)}
151
+ icon={node.icon}
152
+ >
153
+ {node.name}
154
+ </LeftSidebarItem>
155
+ );
156
+ }
157
+
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useState,
7
+ type ComponentProps,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import { createPortal } from 'react-dom';
11
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
12
+
13
+ const RightSidebarContext = createContext<HTMLElement | null>(null);
14
+
15
+ export function RightSidebar({ children, className, ...props }: ComponentProps<'div'>) {
16
+ const [el, setEl] = useState<HTMLElement | null>(null);
17
+ useEffect(() => {
18
+ if (!el) return;
19
+ const onWheel = (event: WheelEvent) => {
20
+ let node = event.target as HTMLElement | null;
21
+ while (node && node !== el) {
22
+ const style = getComputedStyle(node);
23
+ if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
24
+ const canScroll = event.deltaY > 0
25
+ ? node.scrollTop + node.clientHeight < node.scrollHeight - 1
26
+ : node.scrollTop > 0;
27
+ if (canScroll) return;
28
+ }
29
+ node = node.parentElement;
30
+ }
31
+ const page = document.getElementById('page')?.parentElement;
32
+ if (!page || page.scrollHeight <= page.clientHeight) return;
33
+ event.preventDefault();
34
+ page.scrollBy({ top: event.deltaY });
35
+ };
36
+ el.addEventListener('wheel', onWheel, { passive: false });
37
+ return () => el.removeEventListener('wheel', onWheel);
38
+ }, [el]);
39
+ return (
40
+ <RightSidebarContext.Provider value={el}>
41
+ {children}
42
+ <div
43
+ ref={setEl}
44
+ data-slot="right-sidebar"
45
+ className={cn('flex min-h-0 flex-col gap-2 lg:my-2 lg:w-96 lg:px-3 [grid-area:right] [grid-row:main]', className)}
46
+ {...props}
47
+ />
48
+ </RightSidebarContext.Provider>
49
+ );
50
+ }
51
+
52
+ export function RightSidebarPortal({ children }: ComponentProps<'div'>) {
53
+ const el = useContext(RightSidebarContext);
54
+ return el ? createPortal(children, el) : null;
55
+ }
56
+
57
+ export function RightSidebarPanel({
58
+ header,
59
+ footer,
60
+ children,
61
+ className,
62
+ ...props
63
+ }: ComponentProps<'div'> & { header?: ReactNode; footer?: ReactNode }) {
64
+ return (
65
+ <div
66
+ data-slot="panel"
67
+ className={cn('hidden h-full min-h-0 w-90 min-w-0 flex-col lg:flex', className)}
68
+ {...props}
69
+ >
70
+ {header}
71
+ {children}
72
+ {footer}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function RightSidebarPanelHeader({ className, ...props }: ComponentProps<'div'>) {
78
+ return (
79
+ <div
80
+ data-slot="panel-header"
81
+ className={cn('flex shrink-0 items-center gap-1.5', className)}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+
87
+ export function RightSidebarPanelBody({ className, ...props }: ComponentProps<'div'>) {
88
+ return (
89
+ <div
90
+ data-slot="panel-body"
91
+ className={cn('flex min-h-0 flex-1 flex-col', className)}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { usePathname } from 'next/navigation';
3
+ import { useMemo, useRef, type ReactNode } from 'react';
4
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
5
+ import { PathContext, TreeContext } from '@terrafirmagreg-team/ui/hooks/use-tree';
6
+ import { searchPath } from '@terrafirmagreg-team/ui/lib/tree';
7
+
8
+ export function TreeContextProvider({
9
+ tree: rawTree,
10
+ children,
11
+ }: {
12
+ tree: PageTree.Root;
13
+ children: ReactNode;
14
+ }) {
15
+ const nextIdRef = useRef(0);
16
+ const pathname = usePathname();
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: reset memoized tree only when its $id changes
18
+ const tree = useMemo(() => rawTree, [rawTree.$id]);
19
+
20
+ const path = useMemo(() => {
21
+ return (
22
+ searchPath(tree.children, pathname) ??
23
+ (tree.fallback ? searchPath(tree.fallback.children, pathname) : null) ??
24
+ []
25
+ );
26
+ }, [tree, pathname]);
27
+
28
+ const root =
29
+ (path.findLast(
30
+ (item) => item.type === 'folder' && item.root,
31
+ ) as PageTree.Folder | undefined) ?? tree;
32
+ root.$id ??= String(nextIdRef.current++);
33
+
34
+ return (
35
+ <TreeContext
36
+ value={useMemo(() => ({ root, full: tree }), [root, tree])}
37
+ >
38
+ <PathContext value={path}>{children}</PathContext>
39
+ </TreeContext>
40
+ );
41
+ }
42
+