@terrafirmagreg-team/ui 0.0.0-stage → 0.1.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 (215) hide show
  1. package/package.json +93 -3
  2. package/src/components/blocks/button/icon-button.tsx +48 -0
  3. package/src/components/blocks/canvas/chevron-edge.tsx +134 -0
  4. package/src/components/blocks/canvas/flow-canvas.tsx +177 -0
  5. package/src/components/blocks/canvas/flow-toolbar.tsx +32 -0
  6. package/src/components/blocks/canvas/flow-viewport.tsx +243 -0
  7. package/src/components/blocks/git/github-info.tsx +77 -0
  8. package/src/components/blocks/lang/content.tsx +22 -0
  9. package/src/components/blocks/lang/flag.tsx +24 -0
  10. package/src/components/blocks/lang/link.tsx +22 -0
  11. package/src/components/blocks/lang/redirect.tsx +47 -0
  12. package/src/components/blocks/lang/switcher.tsx +53 -0
  13. package/src/components/blocks/lang/untranslated-callout.tsx +28 -0
  14. package/src/components/blocks/link/link-preview.tsx +256 -0
  15. package/src/components/blocks/list/row.tsx +91 -0
  16. package/src/components/blocks/media/glb/model.tsx +243 -0
  17. package/src/components/blocks/media/glb/scene.tsx +372 -0
  18. package/src/components/blocks/media/glb/viewer.tsx +51 -0
  19. package/src/components/blocks/media/image.tsx +135 -0
  20. package/src/components/blocks/media/slideshow.tsx +177 -0
  21. package/src/components/blocks/nav/breadcrumb.tsx +50 -0
  22. package/src/components/blocks/nav/link.tsx +85 -0
  23. package/src/components/blocks/nav/site-menu.tsx +77 -0
  24. package/src/components/blocks/nav/site-title.tsx +60 -0
  25. package/src/components/blocks/nav/switcher.tsx +205 -0
  26. package/src/components/blocks/page/empty-state.tsx +31 -0
  27. package/src/components/blocks/page/heading.tsx +53 -0
  28. package/src/components/blocks/page/pager.tsx +112 -0
  29. package/src/components/blocks/readability/menu.tsx +431 -0
  30. package/src/components/blocks/readability/track.tsx +164 -0
  31. package/src/components/blocks/search/command.tsx +79 -0
  32. package/src/components/blocks/search/dialog.tsx +712 -0
  33. package/src/components/blocks/search/provider.tsx +117 -0
  34. package/src/components/blocks/sidebar.tsx +406 -0
  35. package/src/components/blocks/theme/toggle.tsx +71 -0
  36. package/src/components/blocks/tooltip/hint.tsx +27 -0
  37. package/src/components/blocks/viewer/controls.tsx +279 -0
  38. package/src/components/blocks/viewer/float-bar.tsx +50 -0
  39. package/src/components/layouts/layout.tsx +93 -0
  40. package/src/components/layouts/left-sidebar/content.tsx +62 -0
  41. package/src/components/layouts/left-sidebar/context.tsx +54 -0
  42. package/src/components/layouts/left-sidebar/expand.tsx +22 -0
  43. package/src/components/layouts/left-sidebar/footer.tsx +105 -0
  44. package/src/components/layouts/left-sidebar/index.ts +28 -0
  45. package/src/components/layouts/left-sidebar/items.tsx +102 -0
  46. package/src/components/layouts/left-sidebar/search.tsx +74 -0
  47. package/src/components/layouts/left-sidebar/sidebar.tsx +188 -0
  48. package/src/components/layouts/left-sidebar/tree.tsx +156 -0
  49. package/src/components/layouts/right-sidebar.tsx +95 -0
  50. package/src/components/layouts/tree-context.tsx +41 -0
  51. package/src/components/mdx/accordion.tsx +184 -0
  52. package/src/components/mdx/banner.tsx +154 -0
  53. package/src/components/mdx/callout.tsx +96 -0
  54. package/src/components/mdx/card.tsx +49 -0
  55. package/src/components/mdx/codeblock.tsx +249 -0
  56. package/src/components/mdx/collapsible.tsx +32 -0
  57. package/src/components/mdx/files.tsx +62 -0
  58. package/src/components/mdx/glow-header.tsx +40 -0
  59. package/src/components/mdx/gradient-text.tsx +27 -0
  60. package/src/components/mdx/image-zoom.tsx +314 -0
  61. package/src/components/mdx/mermaid.tsx +16 -0
  62. package/src/components/mdx/multiblock.tsx +18 -0
  63. package/src/components/mdx/preview.tsx +100 -0
  64. package/src/components/mdx/steps.tsx +9 -0
  65. package/src/components/mdx/table.tsx +13 -0
  66. package/src/components/mdx/tabs.tsx +353 -0
  67. package/src/components/mdx/type-table.tsx +170 -0
  68. package/src/components/ui/accordion.tsx +71 -0
  69. package/src/components/ui/alert-dialog.tsx +187 -0
  70. package/src/components/ui/alert.tsx +75 -0
  71. package/src/components/ui/aspect-ratio.tsx +22 -0
  72. package/src/components/ui/attachment.tsx +207 -0
  73. package/src/components/ui/avatar.tsx +108 -0
  74. package/src/components/ui/badge.tsx +51 -0
  75. package/src/components/ui/breadcrumb.tsx +124 -0
  76. package/src/components/ui/bubble.tsx +127 -0
  77. package/src/components/ui/button-group.tsx +87 -0
  78. package/src/components/ui/button.tsx +57 -0
  79. package/src/components/ui/calendar.tsx +221 -0
  80. package/src/components/ui/card.tsx +102 -0
  81. package/src/components/ui/carousel.tsx +242 -0
  82. package/src/components/ui/chart.tsx +371 -0
  83. package/src/components/ui/checkbox.tsx +28 -0
  84. package/src/components/ui/collapsible.tsx +21 -0
  85. package/src/components/ui/combobox.tsx +297 -0
  86. package/src/components/ui/command.tsx +198 -0
  87. package/src/components/ui/context-menu.tsx +270 -0
  88. package/src/components/ui/dialog.tsx +160 -0
  89. package/src/components/ui/direction.tsx +6 -0
  90. package/src/components/ui/drawer.tsx +227 -0
  91. package/src/components/ui/dropdown-menu.tsx +269 -0
  92. package/src/components/ui/empty.tsx +103 -0
  93. package/src/components/ui/field.tsx +238 -0
  94. package/src/components/ui/hover-card.tsx +50 -0
  95. package/src/components/ui/input-group.tsx +158 -0
  96. package/src/components/ui/input-otp.tsx +86 -0
  97. package/src/components/ui/input.tsx +19 -0
  98. package/src/components/ui/item.tsx +201 -0
  99. package/src/components/ui/kbd.tsx +26 -0
  100. package/src/components/ui/label.tsx +19 -0
  101. package/src/components/ui/marker.tsx +70 -0
  102. package/src/components/ui/menubar.tsx +280 -0
  103. package/src/components/ui/message-scroller.tsx +131 -0
  104. package/src/components/ui/message.tsx +91 -0
  105. package/src/components/ui/native-select.tsx +60 -0
  106. package/src/components/ui/navigation-menu.tsx +167 -0
  107. package/src/components/ui/pagination.tsx +130 -0
  108. package/src/components/ui/popover.tsx +91 -0
  109. package/src/components/ui/progress.tsx +82 -0
  110. package/src/components/ui/questionnaire.tsx +324 -0
  111. package/src/components/ui/radio-group.tsx +37 -0
  112. package/src/components/ui/resizable.tsx +49 -0
  113. package/src/components/ui/scroll-area.tsx +54 -0
  114. package/src/components/ui/select.tsx +200 -0
  115. package/src/components/ui/separator.tsx +24 -0
  116. package/src/components/ui/sheet.tsx +138 -0
  117. package/src/components/ui/sidebar.tsx +730 -0
  118. package/src/components/ui/skeleton.tsx +13 -0
  119. package/src/components/ui/slider.tsx +51 -0
  120. package/src/components/ui/spinner.tsx +10 -0
  121. package/src/components/ui/switch.tsx +31 -0
  122. package/src/components/ui/table.tsx +115 -0
  123. package/src/components/ui/tabs.tsx +81 -0
  124. package/src/components/ui/textarea.tsx +17 -0
  125. package/src/components/ui/toast.tsx +234 -0
  126. package/src/components/ui/toggle-group.tsx +89 -0
  127. package/src/components/ui/toggle.tsx +44 -0
  128. package/src/components/ui/tooltip.tsx +65 -0
  129. package/src/hooks/canvas/use-flow-hover.ts +18 -0
  130. package/src/hooks/canvas/use-flow-viewport-persist.ts +33 -0
  131. package/src/hooks/lang.ts +8 -0
  132. package/src/hooks/layout.ts +18 -0
  133. package/src/hooks/page/use-fit-pages.ts +91 -0
  134. package/src/hooks/page/use-location-search.ts +25 -0
  135. package/src/hooks/page/use-page-preview.ts +40 -0
  136. package/src/hooks/page/use-paged-list.ts +20 -0
  137. package/src/hooks/readability/provider.ts +86 -0
  138. package/src/hooks/search/dialog.ts +41 -0
  139. package/src/hooks/search/provider.ts +25 -0
  140. package/src/hooks/search/use-highlight-query.ts +51 -0
  141. package/src/hooks/sidebar.ts +63 -0
  142. package/src/hooks/use-async-value.ts +57 -0
  143. package/src/hooks/use-close-on-scroll.ts +23 -0
  144. package/src/hooks/use-copy-button.ts +46 -0
  145. package/src/hooks/use-cycle-index.ts +75 -0
  146. package/src/hooks/use-document-title.ts +9 -0
  147. package/src/hooks/use-element-size.ts +35 -0
  148. package/src/hooks/use-external-meta.ts +132 -0
  149. package/src/hooks/use-hot-key.ts +33 -0
  150. package/src/hooks/use-hover-follow.ts +54 -0
  151. package/src/hooks/use-in-view.ts +30 -0
  152. package/src/hooks/use-last-value.ts +15 -0
  153. package/src/hooks/use-marquee.ts +51 -0
  154. package/src/hooks/use-media-query.ts +14 -0
  155. package/src/hooks/use-mobile.ts +19 -0
  156. package/src/hooks/use-modifier-keys.ts +31 -0
  157. package/src/hooks/use-on-change.ts +35 -0
  158. package/src/hooks/use-pane-keys.ts +28 -0
  159. package/src/hooks/use-swipe.ts +26 -0
  160. package/src/hooks/use-tree.ts +102 -0
  161. package/src/hooks/viewer/use-in-float-bar.ts +6 -0
  162. package/src/hooks/viewer/use-viewer-prefs.ts +77 -0
  163. package/src/lib/canvas/edge.ts +173 -0
  164. package/src/lib/canvas/geometry.ts +53 -0
  165. package/src/lib/canvas/state.ts +47 -0
  166. package/src/lib/canvas/viewport.ts +37 -0
  167. package/src/lib/canvas/xyflow.ts +1 -0
  168. package/src/lib/config.ts +81 -0
  169. package/src/lib/mdx/callout.ts +20 -0
  170. package/src/lib/mdx/gradient.ts +15 -0
  171. package/src/lib/mdx/mermaid.ts +14 -0
  172. package/src/lib/media/crop-opaque.ts +76 -0
  173. package/src/lib/media/nearby-indexes.ts +6 -0
  174. package/src/lib/media/trim-transparent.ts +117 -0
  175. package/src/lib/readability/config.ts +10 -0
  176. package/src/lib/readability/spotlight.ts +186 -0
  177. package/src/lib/readability/store.ts +146 -0
  178. package/src/lib/tree.ts +92 -0
  179. package/src/lib/utils/async-cache.ts +32 -0
  180. package/src/lib/utils/cn.ts +1 -0
  181. package/src/lib/utils/format.ts +22 -0
  182. package/src/lib/utils/history.ts +9 -0
  183. package/src/lib/utils/merge-refs.ts +13 -0
  184. package/src/lib/utils/nav-active.ts +58 -0
  185. package/src/lib/utils/stored-value.ts +136 -0
  186. package/src/lib/utils/urls.ts +17 -0
  187. package/src/locales/en_us.json +119 -0
  188. package/src/locales/pl_pl.json +35 -0
  189. package/src/locales/ru_ru.json +119 -0
  190. package/src/locales/uk_ua.json +35 -0
  191. package/src/locales/zh_cn.json +17 -0
  192. package/src/locales/zh_tw.json +17 -0
  193. package/src/locales.ts +5 -0
  194. package/src/provider.tsx +141 -0
  195. package/src/types/canvas.ts +49 -0
  196. package/src/types/glb.ts +25 -0
  197. package/src/types/nav.ts +95 -0
  198. package/src/types/preview.ts +9 -0
  199. package/src/types/readability.ts +28 -0
  200. package/src/types/search.ts +56 -0
  201. package/src/types/tree.ts +18 -0
  202. package/src/types/viewer.ts +53 -0
  203. package/styles/content/base.css +194 -0
  204. package/styles/content/default-colors.css +51 -0
  205. package/styles/content/shiki.css +108 -0
  206. package/styles/content/typography.mjs +3941 -0
  207. package/styles/content.css +7 -0
  208. package/styles/image-zoom.css +83 -0
  209. package/styles/layout.css +28 -0
  210. package/styles/left-sidebar.css +140 -0
  211. package/styles/readability.css +50 -0
  212. package/styles/right-sidebar.css +6 -0
  213. package/styles/workspace.css +26 -0
  214. package/styles.css +413 -0
  215. package/README.md +0 -3
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, type ReactNode } from 'react';
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ Expand,
8
+ Maximize2,
9
+ Settings2,
10
+ Shrink,
11
+ ZoomIn,
12
+ ZoomOut,
13
+ } from 'lucide-react';
14
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
15
+ import { ButtonGroupText } from '@terrafirmagreg-team/ui/components/ui/button-group';
16
+ import { useInFloatBar } from '@terrafirmagreg-team/ui/hooks/viewer/use-in-float-bar';
17
+ import { FloatBar } from '@terrafirmagreg-team/ui/components/blocks/viewer/float-bar';
18
+ import {
19
+ Popover,
20
+ PopoverContent,
21
+ PopoverTrigger,
22
+ } from '@terrafirmagreg-team/ui/components/ui/popover';
23
+ import {
24
+ Field,
25
+ FieldContent,
26
+ FieldGroup,
27
+ FieldLabel,
28
+ } from '@terrafirmagreg-team/ui/components/ui/field';
29
+ import { Slider } from '@terrafirmagreg-team/ui/components/ui/slider';
30
+ import { Switch } from '@terrafirmagreg-team/ui/components/ui/switch';
31
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
32
+ import type {
33
+ ExpandButtonProps,
34
+ PagerControlsProps,
35
+ ViewerControlsProps,
36
+ ViewerSettingsProps,
37
+ ViewerSliderSetting,
38
+ ZoomControlsProps,
39
+ } from '@terrafirmagreg-team/ui/types/viewer';
40
+
41
+ export function ViewerToolbar({ children }: { children: ReactNode }) {
42
+ return (
43
+ <div className="absolute right-2 top-2 z-10">
44
+ <FloatBar>{children}</FloatBar>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export function ViewerZoomControls({
50
+ onZoomIn,
51
+ onZoomOut,
52
+ onFit,
53
+ fitLabel,
54
+ }: ZoomControlsProps) {
55
+ const t = useTranslations();
56
+ return (
57
+ <>
58
+ <IconButton overlay label={t('ui.canvas.zoom_in')} onClick={onZoomIn}>
59
+ <ZoomIn className="size-3.5" />
60
+ </IconButton>
61
+ <IconButton overlay label={t('ui.canvas.zoom_out')} onClick={onZoomOut}>
62
+ <ZoomOut className="size-3.5" />
63
+ </IconButton>
64
+ {onFit ? (
65
+ <IconButton
66
+ overlay
67
+ label={fitLabel ?? t('ui.canvas.fit')}
68
+ onClick={onFit}
69
+ >
70
+ <Maximize2 className="size-3.5" />
71
+ </IconButton>
72
+ ) : null}
73
+ </>
74
+ );
75
+ }
76
+
77
+ export function ViewerPagerControls({
78
+ index,
79
+ count,
80
+ onSelect,
81
+ }: PagerControlsProps) {
82
+ const t = useTranslations();
83
+ const inFloatBar = useInFloatBar();
84
+ return (
85
+ <>
86
+ <IconButton
87
+ overlay
88
+ label={t('ui.canvas.prev')}
89
+ disabled={index === 0}
90
+ onClick={() => onSelect(index - 1)}
91
+ >
92
+ <ChevronLeft className="size-3.5" />
93
+ </IconButton>
94
+ <ButtonGroupText
95
+ className={
96
+ inFloatBar
97
+ ? 'border-0 bg-transparent text-xs'
98
+ : 'bg-float/92 text-xs backdrop-blur-md'
99
+ }
100
+ >
101
+ {index + 1} / {count}
102
+ </ButtonGroupText>
103
+ <IconButton
104
+ overlay
105
+ label={t('ui.canvas.next')}
106
+ disabled={index === count - 1}
107
+ onClick={() => onSelect(index + 1)}
108
+ >
109
+ <ChevronRight className="size-3.5" />
110
+ </IconButton>
111
+ </>
112
+ );
113
+ }
114
+
115
+ export function ViewerExpandButton({ target }: ExpandButtonProps) {
116
+ const t = useTranslations();
117
+ const [active, setActive] = useState(false);
118
+ const [fallback, setFallback] = useState(false);
119
+
120
+ useEffect(() => {
121
+ const onChange = () =>
122
+ setActive(document.fullscreenElement === target.current);
123
+ document.addEventListener('fullscreenchange', onChange);
124
+ return () => document.removeEventListener('fullscreenchange', onChange);
125
+ }, [target]);
126
+
127
+ useEffect(() => {
128
+ const el = target.current;
129
+ if (!fallback || !el) return;
130
+ el.classList.add('viewer-fallback-fullscreen');
131
+ const previousOverflow = document.body.style.overflow;
132
+ document.body.style.overflow = 'hidden';
133
+ const onKey = (event: KeyboardEvent) => {
134
+ if (event.key === 'Escape') setFallback(false);
135
+ };
136
+ document.addEventListener('keydown', onKey);
137
+ return () => {
138
+ el.classList.remove('viewer-fallback-fullscreen');
139
+ document.body.style.overflow = previousOverflow;
140
+ document.removeEventListener('keydown', onKey);
141
+ };
142
+ }, [fallback, target]);
143
+
144
+ const expanded = active || fallback;
145
+ return (
146
+ <IconButton
147
+ overlay
148
+ aria-expanded={expanded}
149
+ label={t(expanded ? 'ui.canvas.exit_fullscreen' : 'ui.canvas.fullscreen')}
150
+ onClick={() => {
151
+ const el = target.current;
152
+ if (fallback) {
153
+ setFallback(false);
154
+ return;
155
+ }
156
+ if (!el) return;
157
+ if (document.fullscreenElement) {
158
+ void document.exitFullscreen();
159
+ } else if (el.requestFullscreen) {
160
+ void el.requestFullscreen().catch(() => setFallback(true));
161
+ } else {
162
+ setFallback(true);
163
+ }
164
+ }}
165
+ >
166
+ {expanded ? (
167
+ <Shrink className="size-3.5" />
168
+ ) : (
169
+ <Expand className="size-3.5" />
170
+ )}
171
+ </IconButton>
172
+ );
173
+ }
174
+
175
+ function ViewerSlider({
176
+ label,
177
+ icon,
178
+ value,
179
+ onChange,
180
+ min = 0,
181
+ max = 100,
182
+ step = 1,
183
+ format = String,
184
+ disabled,
185
+ live,
186
+ }: ViewerSliderSetting) {
187
+ const [draft, setDraft] = useState<{ key: number; value: number }>({
188
+ key: value,
189
+ value,
190
+ });
191
+ const shown = draft.key === value ? draft.value : value;
192
+ return (
193
+ <Field className="gap-1.5">
194
+ <FieldLabel className="text-xs text-muted-foreground">
195
+ {icon}
196
+ {label}
197
+ </FieldLabel>
198
+ <FieldContent className="rounded-lg bg-muted/70 px-3 py-2.5">
199
+ <div className="flex items-center gap-3">
200
+ <Slider
201
+ min={min}
202
+ max={max}
203
+ step={step}
204
+ value={shown}
205
+ disabled={disabled}
206
+ className="flex-1 [&_[data-slot=slider-track]]:bg-muted-foreground/25"
207
+ onValueChange={(next) => {
208
+ if (live) onChange(next as number);
209
+ else setDraft({ key: value, value: next as number });
210
+ }}
211
+ onValueCommitted={() => {
212
+ if (!live && shown !== value) onChange(shown);
213
+ }}
214
+ />
215
+ <span className="w-9 shrink-0 text-end text-xs tabular-nums text-muted-foreground">
216
+ {format(shown)}
217
+ </span>
218
+ </div>
219
+ </FieldContent>
220
+ </Field>
221
+ );
222
+ }
223
+
224
+ export function ViewerSettings({
225
+ label,
226
+ sliders,
227
+ toggles,
228
+ }: ViewerSettingsProps) {
229
+ if (!sliders?.length && !toggles?.length) return null;
230
+ return (
231
+ <Popover>
232
+ <PopoverTrigger render={<IconButton overlay label={label} />}>
233
+ <Settings2 className="size-3.5" />
234
+ </PopoverTrigger>
235
+ <PopoverContent side="bottom" align="end" className="w-60 space-y-3 p-3">
236
+ {sliders?.map((slider, index) => (
237
+ <ViewerSlider key={index} {...slider} />
238
+ ))}
239
+ {toggles?.length ? (
240
+ <FieldGroup className="gap-1.5 rounded-lg bg-muted/60 p-2">
241
+ {toggles.map((toggle, index) => (
242
+ <Field key={index} orientation="horizontal" className="w-auto gap-1.5">
243
+ <Switch
244
+ size="sm"
245
+ checked={toggle.checked}
246
+ onCheckedChange={toggle.onChange}
247
+ />
248
+ {toggle.icon}
249
+ <FieldLabel className="text-sm font-normal">
250
+ {toggle.label}
251
+ </FieldLabel>
252
+ </Field>
253
+ ))}
254
+ </FieldGroup>
255
+ ) : null}
256
+ </PopoverContent>
257
+ </Popover>
258
+ );
259
+ }
260
+
261
+ export function ViewerControls({
262
+ start,
263
+ pager,
264
+ zoom,
265
+ expand,
266
+ extra,
267
+ settings,
268
+ }: ViewerControlsProps) {
269
+ return (
270
+ <>
271
+ {start}
272
+ {pager ? <ViewerPagerControls {...pager} /> : null}
273
+ {zoom ? <ViewerZoomControls {...zoom} /> : null}
274
+ {expand ? <ViewerExpandButton {...expand} /> : null}
275
+ {extra}
276
+ {settings ? <ViewerSettings {...settings} /> : null}
277
+ </>
278
+ );
279
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { type ComponentProps, type ReactNode } from 'react';
3
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
4
+
5
+ import { FloatBarContext } from '@terrafirmagreg-team/ui/hooks/viewer/use-in-float-bar';
6
+
7
+ export function FloatBar({ className, children, ...props }: ComponentProps<'div'>) {
8
+ return (
9
+ <FloatBarContext.Provider value>
10
+ <div
11
+ role="group"
12
+ data-slot="float-bar"
13
+ className={cn(
14
+ 'flex h-7 items-stretch overflow-hidden rounded-lg bg-float/92 shadow-sm ring-1 ring-foreground/10 backdrop-blur-md',
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ {children}
20
+ </div>
21
+ </FloatBarContext.Provider>
22
+ );
23
+ }
24
+
25
+ export function FloatBarItem({
26
+ label,
27
+ className,
28
+ children,
29
+ }: {
30
+ label?: ReactNode;
31
+ className?: string;
32
+ children?: ReactNode;
33
+ }) {
34
+ return (
35
+ <span
36
+ data-slot="float-bar-item"
37
+ className={cn(
38
+ 'inline-flex items-center justify-center gap-1 px-3 font-mono text-[11.5px] leading-none whitespace-pre text-foreground',
39
+ className,
40
+ )}
41
+ >
42
+ {label != null && (
43
+ <span className="text-[9px] font-semibold uppercase tracking-[0.06em] text-foreground/65">
44
+ {label}
45
+ </span>
46
+ )}
47
+ {children}
48
+ </span>
49
+ );
50
+ }
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+ import {
3
+ useMemo,
4
+ type ComponentProps,
5
+ type ComponentType,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
9
+ import Link from '@terrafirmagreg-team/ui/components/blocks/nav/link';
10
+ import { SidebarIcon } from 'lucide-react';
11
+
12
+ import { LayoutContext, useLayout } from '@terrafirmagreg-team/ui/hooks/layout';
13
+ import { TreeContextProvider } from '@terrafirmagreg-team/ui/components/layouts/tree-context';
14
+ import {
15
+ SidebarProvider,
16
+ SidebarTrigger,
17
+ } from '@terrafirmagreg-team/ui/components/blocks/sidebar';
18
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
19
+ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
20
+ import { RightSidebar } from '@terrafirmagreg-team/ui/components/layouts/right-sidebar';
21
+ import {
22
+ LeftSidebarPortalProvider,
23
+ LeftSidebarSearch,
24
+ } from '@terrafirmagreg-team/ui/components/layouts/left-sidebar';
25
+
26
+ export type LayoutProps = {
27
+ tree: PageTree.Root;
28
+
29
+ sidebar?: ReactNode;
30
+ navTitle?: ComponentType<ComponentProps<'a'>>;
31
+ header?: ReactNode;
32
+ children?: ReactNode;
33
+ };
34
+
35
+ const EmptyTitle: ComponentType<ComponentProps<'a'>> = (props) => (
36
+ <Link href="/" {...props} />
37
+ );
38
+
39
+ export function Layout({
40
+ tree,
41
+ sidebar,
42
+ navTitle: Title = EmptyTitle,
43
+ header = <LayoutHeader />,
44
+ children,
45
+ }: LayoutProps) {
46
+ return (
47
+ <LayoutContext value={useMemo(() => ({ navTitle: Title }), [Title])}>
48
+ <TreeContextProvider tree={tree}>
49
+ <SidebarProvider>
50
+ <LeftSidebarPortalProvider>
51
+ <div
52
+ id="spacious-layout"
53
+ data-slot="layout"
54
+ className="relative grid min-h-(--layout-height) bg-card transition-[grid-template-columns] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none [--layout-height:calc(100dvh_-_var(--banner-height,0px))] [--header-height:--spacing(14)] [--page-width:100%] [--sidebar-width:0px] [--sidebar-col:var(--sidebar-width)] [--toc-width:0px] max-md:[--docs-row-3:calc(var(--banner-height,0px)+var(--header-height)+var(--toc-popover-height,0px))] md:h-(--layout-height) md:overflow-clip print:h-auto print:overflow-visible"
55
+ style={{
56
+ gridTemplate: `"sidebar header right" auto
57
+ "sidebar toc-popover right" var(--toc-popover-height, 0px)
58
+ "sidebar main right" 1fr / var(--sidebar-col) minmax(0, 1fr) auto`,
59
+ }}
60
+ >
61
+ {sidebar}
62
+ {header}
63
+ <RightSidebar>{children}</RightSidebar>
64
+ </div>
65
+ </LeftSidebarPortalProvider>
66
+ </SidebarProvider>
67
+ </TreeContextProvider>
68
+ </LayoutContext>
69
+ );
70
+ }
71
+
72
+ export function LayoutHeader({ className, ...props }: ComponentProps<'header'>) {
73
+ const { navTitle: Title } = useLayout();
74
+ return (
75
+ <header
76
+ id="subnav"
77
+ data-slot="layout-header"
78
+ className={cn(
79
+ 'sticky top-(--banner-height,0px) z-30 [grid-area:header] flex items-center h-(--header-height) ps-4 pe-2.5 bg-card md:hidden',
80
+ className,
81
+ )}
82
+ {...props}
83
+ >
84
+ <Title className="inline-flex items-center gap-2.5 me-auto font-semibold" />
85
+ <LeftSidebarSearch variant="icon" />
86
+ <SidebarTrigger
87
+ className={buttonVariants({ variant: 'ghost', size: 'icon-sm', className: 'p-2' })}
88
+ >
89
+ <SidebarIcon />
90
+ </SidebarTrigger>
91
+ </header>
92
+ );
93
+ }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import type { ComponentProps } from 'react';
4
+ import { ScrollArea } from '@base-ui/react/scroll-area';
5
+ import { useHoverFollow } from '@terrafirmagreg-team/ui/hooks/use-hover-follow';
6
+ import type { NavItemMatch } from '@terrafirmagreg-team/ui/lib/utils/nav-active';
7
+ import { LeftSidebarPortalTarget } from './context';
8
+ import { LeftSidebarTree } from './tree';
9
+
10
+ export function LeftSidebarContent({
11
+ children,
12
+ match,
13
+ navKeys,
14
+ className,
15
+ viewportClassName,
16
+ ...props
17
+ }: ComponentProps<'div'> & {
18
+ match?: NavItemMatch;
19
+ navKeys?: ReadonlySet<string>;
20
+ viewportClassName?: string;
21
+ }) {
22
+ return (
23
+ <ScrollArea.Root
24
+ data-slot="left-sidebar-content"
25
+ className={cn('relative min-h-0 flex-1', className)}
26
+ {...props}
27
+ >
28
+ <ScrollArea.Viewport
29
+ data-slot="left-sidebar-viewport"
30
+ className={cn(
31
+ 'size-full overscroll-contain px-3 py-2 mask-[linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]',
32
+ viewportClassName,
33
+ )}
34
+ >
35
+ <nav
36
+ ref={useHoverFollow()}
37
+ className="relative flex flex-col gap-1"
38
+ >
39
+ <div
40
+ aria-hidden
41
+ className="absolute top-0 left-0 py-0.5 rounded-lg bg-accent/60 bg-clip-content opacity-0 pointer-events-none transition-opacity duration-200 ease-expo motion-safe:transition-[translate,width,height,opacity]"
42
+ />
43
+ <LeftSidebarPortalTarget />
44
+ <div data-slot="left-sidebar-tree" className="flex flex-col">
45
+ {children ?? <LeftSidebarTree match={match} navKeys={navKeys} />}
46
+ </div>
47
+ </nav>
48
+ </ScrollArea.Viewport>
49
+ <ScrollArea.Scrollbar
50
+ className={(state) =>
51
+ cn(
52
+ 'flex h-full w-1.5 touch-none p-px transition-opacity select-none',
53
+ !state.hovering && 'opacity-0',
54
+ )
55
+ }
56
+ >
57
+ <ScrollArea.Thumb className="relative flex-1 rounded-full bg-border" />
58
+ </ScrollArea.Scrollbar>
59
+ <ScrollArea.Corner />
60
+ </ScrollArea.Root>
61
+ );
62
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ use,
5
+ useMemo,
6
+ useState,
7
+ type ComponentProps,
8
+ type ComponentType,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+ import type { PopoverContent } from '@terrafirmagreg-team/ui/components/ui/popover';
13
+ import type { NavItemMatch } from '@terrafirmagreg-team/ui/lib/utils/nav-active';
14
+
15
+ export type SettingsMenuComponent = ComponentType<{
16
+ anchor?: ComponentProps<typeof PopoverContent>['anchor'];
17
+ }>;
18
+
19
+ export type LeftSidebarParts = {
20
+ controls?: ReactNode;
21
+ content?: ReactNode;
22
+ footer?: ReactNode;
23
+
24
+ menu?: ReactNode;
25
+ match?: NavItemMatch;
26
+ navKeys?: ReadonlySet<string>;
27
+ settings?: SettingsMenuComponent;
28
+ };
29
+
30
+ const LeftSidebarPortalContext = createContext<{
31
+ target: HTMLElement | null;
32
+ setTarget: (el: HTMLElement | null) => void;
33
+ } | null>(null);
34
+
35
+ export function LeftSidebarPortalProvider({ children }: { children: ReactNode }) {
36
+ const [target, setTarget] = useState<HTMLElement | null>(null);
37
+ return (
38
+ <LeftSidebarPortalContext
39
+ value={useMemo(() => ({ target, setTarget }), [target])}
40
+ >
41
+ {children}
42
+ </LeftSidebarPortalContext>
43
+ );
44
+ }
45
+
46
+ export function LeftSidebarPortal({ children }: { children?: ReactNode }) {
47
+ const ctx = use(LeftSidebarPortalContext);
48
+ return ctx?.target ? createPortal(children, ctx.target) : null;
49
+ }
50
+
51
+ export function LeftSidebarPortalTarget() {
52
+ const ctx = use(LeftSidebarPortalContext);
53
+ return <div data-slot="left-sidebar-portal" ref={ctx?.setTarget} />;
54
+ }
@@ -0,0 +1,22 @@
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 { SidebarIcon } from 'lucide-react';
5
+ import { useSidebar } from '@terrafirmagreg-team/ui/hooks/sidebar';
6
+ import { SidebarCollapseTrigger } from '@terrafirmagreg-team/ui/components/blocks/sidebar';
7
+
8
+ export function LeftSidebarExpand() {
9
+ const { collapsed } = useSidebar();
10
+ if (!collapsed) return;
11
+
12
+ return (
13
+ <SidebarCollapseTrigger
14
+ className={cn(
15
+ buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
16
+ '-ms-2 size-8 text-muted-foreground',
17
+ )}
18
+ >
19
+ <SidebarIcon />
20
+ </SidebarCollapseTrigger>
21
+ );
22
+ }
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import {
4
+ isValidElement,
5
+ useRef,
6
+ type ComponentProps,
7
+ type ReactNode,
8
+ } from 'react';
9
+ import { useTheme } from 'next-themes';
10
+ import { useLang } from '@terrafirmagreg-team/translate/provider';
11
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
12
+ import { LangSwitcher } from '@terrafirmagreg-team/ui/components/blocks/lang/switcher';
13
+ import { ThemeToggle } from '@terrafirmagreg-team/ui/components/blocks/theme/toggle';
14
+ import { Separator } from '@terrafirmagreg-team/ui/components/ui/separator';
15
+ import type { SettingsMenuComponent } from './context';
16
+
17
+ export function LeftSidebarFooter({
18
+ settings,
19
+ }: {
20
+ settings?: SettingsMenuComponent;
21
+ }) {
22
+ return (
23
+ <>
24
+ <LeftSidebarLanguage />
25
+ <LeftSidebarActions settings={settings} />
26
+ </>
27
+ );
28
+ }
29
+
30
+ export function LeftSidebarLanguage() {
31
+ const t = useTranslations();
32
+ const { locale, locales, onLocaleChange } = useLang();
33
+ if (!locales?.length) return null;
34
+ return (
35
+ <LangSwitcher
36
+ locales={locales}
37
+ selected={locale}
38
+ onSelect={(value) => onLocaleChange?.(value)}
39
+ label={t('ui.choose_language')}
40
+ />
41
+ );
42
+ }
43
+
44
+ const isThemeProps = (
45
+ value: unknown,
46
+ ): value is ComponentProps<typeof ThemeToggle> =>
47
+ typeof value === 'object' && value !== null && !isValidElement(value);
48
+
49
+ export function LeftSidebarPlate({
50
+ tools,
51
+ theme,
52
+ className,
53
+ children,
54
+ ...props
55
+ }: ComponentProps<'div'> & {
56
+ tools?: ReactNode;
57
+ theme?: ComponentProps<typeof ThemeToggle> | ReactNode;
58
+ }) {
59
+ return (
60
+ <div
61
+ data-slot="left-sidebar-plate"
62
+ className={cn(
63
+ 'flex h-10 items-center gap-0.5 rounded-xl border bg-secondary/50 px-1',
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {(tools != null || theme != null) && (
70
+ <div className="ms-auto flex items-center">
71
+ {tools}
72
+ {tools != null && theme != null && (
73
+ <Separator
74
+ orientation="vertical"
75
+ className="mx-1.5 h-5 self-center! bg-muted-foreground/30"
76
+ aria-hidden
77
+ />
78
+ )}
79
+ {isThemeProps(theme) ? <ThemeToggle {...theme} /> : theme}
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ export function LeftSidebarActions({
87
+ settings: SettingsMenu,
88
+ children,
89
+ }: {
90
+ settings?: SettingsMenuComponent;
91
+
92
+ children?: ReactNode;
93
+ }) {
94
+ const { theme, setTheme } = useTheme();
95
+ const plateRef = useRef<HTMLDivElement>(null);
96
+ return (
97
+ <LeftSidebarPlate
98
+ ref={plateRef}
99
+ tools={SettingsMenu ? <SettingsMenu anchor={plateRef} /> : undefined}
100
+ theme={{ value: theme, onValueChange: setTheme }}
101
+ >
102
+ {children}
103
+ </LeftSidebarPlate>
104
+ );
105
+ }
@@ -0,0 +1,28 @@
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';