@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,354 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ type ComponentProps,
6
+ createContext,
7
+ type ReactNode,
8
+ use,
9
+ useContext,
10
+ useEffect,
11
+ useEffectEvent,
12
+ useId,
13
+ useLayoutEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from 'react';
18
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
19
+ import { mergeRefs } from '@terrafirmagreg-team/ui/lib/utils/merge-refs';
20
+ import { Tabs as Primitive } from '@base-ui/react/tabs';
21
+
22
+ type ChangeListener = (v: string) => void;
23
+ const listeners = new Map<string, Set<ChangeListener>>();
24
+
25
+ export interface BaseTabsProps extends ComponentProps<typeof Primitive.Root> {
26
+
27
+ groupId?: string;
28
+
29
+ persist?: boolean;
30
+
31
+ updateAnchor?: boolean;
32
+
33
+ onValueChange?: (value: string) => void;
34
+ }
35
+
36
+ const BaseTabsContext = createContext<{
37
+ valueToIdMap: Map<string, string>;
38
+
39
+ panels: Map<string, HTMLElement>;
40
+
41
+ triggers: Set<string>;
42
+ } | null>(null);
43
+
44
+ function useBaseTabContext() {
45
+ const ctx = use(BaseTabsContext);
46
+ if (!ctx) throw new Error('You must wrap your component in <BaseTabs>');
47
+ return ctx;
48
+ }
49
+
50
+ export const BaseTabsList = Primitive.List;
51
+
52
+ export function BaseTabsTrigger({ value, ref, ...props }: ComponentProps<typeof Primitive.Tab>) {
53
+ const { triggers } = useBaseTabContext();
54
+
55
+ return (
56
+ <Primitive.Tab
57
+ ref={mergeRefs(ref, (element) => {
58
+ if (element) triggers.add(value);
59
+ else triggers.delete(value);
60
+ })}
61
+ value={value}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ export function BaseTabs({
68
+ ref,
69
+ groupId,
70
+ persist = false,
71
+ updateAnchor = false,
72
+ defaultValue,
73
+ value: _value,
74
+ onValueChange: _onValueChange,
75
+ ...props
76
+ }: BaseTabsProps) {
77
+ const tabsRef = useRef<HTMLDivElement>(null);
78
+ const valueToIdMap = useMemo(() => new Map<string, string>(), []);
79
+ const panels = useMemo(() => new Map<string, HTMLElement>(), []);
80
+ const triggers = useMemo(() => new Set<string>(), []);
81
+ const [value, setValue] =
82
+ _value === undefined
83
+ ? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
84
+ useState(defaultValue)
85
+ : // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
86
+ [_value, (v: string) => _onValueChange?.(v)];
87
+
88
+ const onChange = useEffectEvent((v: string) => {
89
+ if (triggers.has(v)) setValue(v);
90
+ });
91
+
92
+ const openFromHash = useEffectEvent(() => {
93
+ const hash = window.location.hash.slice(1);
94
+ if (!hash) return;
95
+
96
+ for (const [value, id] of valueToIdMap.entries()) {
97
+ if (id === hash) {
98
+ setValue(value);
99
+ tabsRef.current?.scrollIntoView();
100
+ return;
101
+ }
102
+ }
103
+
104
+ const target = document.getElementById(hash);
105
+ if (!target) return;
106
+
107
+ for (const [value, panel] of panels.entries()) {
108
+ if (!panel.contains(target)) continue;
109
+
110
+ setValue(value);
111
+ requestAnimationFrame(() => target.scrollIntoView());
112
+ return;
113
+ }
114
+ });
115
+
116
+ useLayoutEffect(() => {
117
+ if (!groupId) return;
118
+ let previous = sessionStorage.getItem(groupId);
119
+ if (persist) previous ??= localStorage.getItem(groupId);
120
+ if (previous) onChange(previous);
121
+
122
+ const groupListeners = listeners.get(groupId) ?? new Set();
123
+ groupListeners.add(onChange);
124
+ listeners.set(groupId, groupListeners);
125
+ return () => {
126
+ groupListeners.delete(onChange);
127
+ };
128
+ }, [groupId, persist]);
129
+
130
+ useLayoutEffect(() => {
131
+ openFromHash();
132
+ window.addEventListener('hashchange', openFromHash);
133
+ return () => window.removeEventListener('hashchange', openFromHash);
134
+ }, []);
135
+
136
+ return (
137
+ <Primitive.Root
138
+ ref={mergeRefs(ref, tabsRef)}
139
+ value={value}
140
+ onValueChange={(v: string) => {
141
+ if (updateAnchor) {
142
+ const id = valueToIdMap.get(v);
143
+
144
+ if (id) {
145
+ window.history.replaceState(null, '', `#${id}`);
146
+ }
147
+ }
148
+
149
+ if (groupId) {
150
+ const groupListeners = listeners.get(groupId);
151
+ if (groupListeners) {
152
+ for (const listener of groupListeners) listener(v);
153
+ }
154
+
155
+ sessionStorage.setItem(groupId, v);
156
+ if (persist) localStorage.setItem(groupId, v);
157
+ } else {
158
+ setValue(v);
159
+ }
160
+ }}
161
+ {...props}
162
+ >
163
+ <BaseTabsContext
164
+ value={useMemo(
165
+ () => ({ valueToIdMap, panels, triggers }),
166
+ [valueToIdMap, panels, triggers],
167
+ )}
168
+ >
169
+ {props.children}
170
+ </BaseTabsContext>
171
+ </Primitive.Root>
172
+ );
173
+ }
174
+
175
+ export function BaseTabsContent({ value, ref, ...props }: ComponentProps<typeof Primitive.Panel>) {
176
+ const { valueToIdMap, panels } = useBaseTabContext();
177
+
178
+ if (props.id) {
179
+ valueToIdMap.set(value, props.id);
180
+ }
181
+
182
+ return (
183
+ <Primitive.Panel
184
+ ref={mergeRefs(ref, (element) => {
185
+ if (element) panels.set(value, element);
186
+ else panels.delete(value);
187
+ })}
188
+ value={value}
189
+ {...props}
190
+ >
191
+ {props.children}
192
+ </Primitive.Panel>
193
+ );
194
+ }
195
+
196
+ type CollectionKey = string | symbol;
197
+
198
+ export interface TabsProps extends Omit<
199
+ ComponentProps<typeof BaseTabs>,
200
+ 'value' | 'onValueChange'
201
+ > {
202
+
203
+ items?: string[];
204
+
205
+ defaultIndex?: number;
206
+
207
+ label?: ReactNode;
208
+ }
209
+
210
+ const TabsContext = createContext<{
211
+ items?: string[];
212
+ collection: CollectionKey[];
213
+ } | null>(null);
214
+
215
+ function useTabContext() {
216
+ const ctx = useContext(TabsContext);
217
+ if (!ctx) throw new Error('You must wrap your component in <Tabs>');
218
+ return ctx;
219
+ }
220
+
221
+ export function TabsList({ className, ...props }: React.ComponentProps<typeof BaseTabsList>) {
222
+ return (
223
+ <BaseTabsList
224
+ {...props}
225
+ className={(s) =>
226
+ cn(
227
+ 'flex gap-3.5 text-secondary-foreground overflow-x-auto px-4 not-prose border-b border-border',
228
+ typeof className === 'function' ? className(s) : className,
229
+ )
230
+ }
231
+ />
232
+ );
233
+ }
234
+
235
+ export function TabsTrigger({
236
+ className,
237
+ ...props
238
+ }: React.ComponentProps<typeof BaseTabsTrigger>) {
239
+ return (
240
+ <BaseTabsTrigger
241
+ {...props}
242
+ className={(s) =>
243
+ cn(
244
+ 'inline-flex items-center gap-2 whitespace-nowrap text-muted-foreground border-b border-transparent py-2 text-sm font-medium transition-colors [&_svg]:size-4 hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active]:border-primary data-[active]:text-primary',
245
+ typeof className === 'function' ? className(s) : className,
246
+ )
247
+ }
248
+ />
249
+ );
250
+ }
251
+
252
+ export function Tabs({
253
+ ref,
254
+ className,
255
+ items,
256
+ label,
257
+ defaultIndex = 0,
258
+ defaultValue = items ? escapeValue(items[defaultIndex]) : undefined,
259
+ ...props
260
+ }: TabsProps) {
261
+ const collection = useMemo<CollectionKey[]>(() => [], []);
262
+
263
+ return (
264
+ <BaseTabs
265
+ ref={ref}
266
+ className={(s) =>
267
+ cn(
268
+ 'flex flex-col overflow-hidden rounded-xl border bg-secondary my-4',
269
+ typeof className === 'function' ? className(s) : className,
270
+ )
271
+ }
272
+ defaultValue={defaultValue}
273
+ {...props}
274
+ >
275
+ {items && (
276
+ <TabsList>
277
+ {label && <span className="text-sm font-medium my-auto me-auto">{label}</span>}
278
+ {items.map((item) => (
279
+ <TabsTrigger key={item} value={escapeValue(item)}>
280
+ {item}
281
+ </TabsTrigger>
282
+ ))}
283
+ </TabsList>
284
+ )}
285
+ <TabsContext.Provider value={useMemo(() => ({ items, collection }), [collection, items])}>
286
+ {props.children}
287
+ </TabsContext.Provider>
288
+ </BaseTabs>
289
+ );
290
+ }
291
+
292
+ export interface TabProps extends Omit<ComponentProps<typeof BaseTabsContent>, 'value'> {
293
+
294
+ value?: string;
295
+ }
296
+
297
+ export function Tab({ value, ...props }: TabProps) {
298
+ const { items } = useTabContext();
299
+ const resolved =
300
+ value ??
301
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change
302
+ items?.at(useCollectionIndex());
303
+ if (!resolved)
304
+ throw new Error(
305
+ 'Failed to resolve tab `value`, please pass a `value` prop to the Tab component.',
306
+ );
307
+
308
+ return (
309
+ <TabsContent value={escapeValue(resolved)} {...props}>
310
+ {props.children}
311
+ </TabsContent>
312
+ );
313
+ }
314
+
315
+ export function TabsContent({
316
+ value,
317
+ className,
318
+ ...props
319
+ }: ComponentProps<typeof BaseTabsContent>) {
320
+ return (
321
+ <BaseTabsContent
322
+ value={value}
323
+ className={(s) =>
324
+ cn(
325
+ 'p-4 text-[0.9375rem] bg-background rounded-xl outline-none prose-no-margin data-[inactive]:hidden [&>figure:only-child]:-m-4 [&>figure:only-child]:border-none',
326
+ typeof className === 'function' ? className(s) : className,
327
+ )
328
+ }
329
+ {...props}
330
+ >
331
+ {props.children}
332
+ </BaseTabsContent>
333
+ );
334
+ }
335
+
336
+ function useCollectionIndex() {
337
+ const key = useId();
338
+ const { collection } = useTabContext();
339
+
340
+ useEffect(() => {
341
+ return () => {
342
+ const idx = collection.indexOf(key);
343
+ if (idx !== -1) collection.splice(idx, 1);
344
+ };
345
+ }, [key, collection]);
346
+
347
+ if (!collection.includes(key)) collection.push(key);
348
+ return collection.indexOf(key);
349
+ }
350
+
351
+ function escapeValue(v: string): string {
352
+ return v.toLowerCase().replace(/\s/, '-');
353
+ }
354
+
@@ -0,0 +1,171 @@
1
+ 'use client';
2
+
3
+ import { ChevronDown } from 'lucide-react';
4
+ import { useUiComponents } from '@terrafirmagreg-team/ui/provider';
5
+ import { cva } from 'class-variance-authority';
6
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
+ import { type ComponentProps, type ReactNode, useEffect, useState } from 'react';
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './collapsible';
9
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
10
+
11
+ export interface ParameterNode {
12
+ name: string;
13
+ description: ReactNode;
14
+ }
15
+
16
+ export interface TypeNode {
17
+
18
+ description?: ReactNode;
19
+
20
+ type: ReactNode;
21
+
22
+ typeDescription?: ReactNode;
23
+
24
+ typeDescriptionLink?: string;
25
+
26
+ default?: ReactNode;
27
+
28
+ required?: boolean;
29
+ deprecated?: boolean;
30
+
31
+ parameters?: ParameterNode[];
32
+
33
+ returns?: ReactNode;
34
+ }
35
+
36
+ const fieldVariants = cva('text-muted-foreground not-prose pe-2');
37
+
38
+ export function TypeTable({
39
+ id,
40
+ type,
41
+ className,
42
+ ...props
43
+ }: { type: Record<string, TypeNode> } & ComponentProps<'div'>) {
44
+ const t = useTranslations();
45
+
46
+ return (
47
+ <div
48
+ id={id}
49
+ className={cn(
50
+ '@container flex flex-col p-1 bg-card text-card-foreground rounded-2xl border my-6 text-sm overflow-hidden',
51
+ className,
52
+ )}
53
+ {...props}
54
+ >
55
+ <div className="flex font-medium items-center px-3 py-1 not-prose text-muted-foreground">
56
+ <p className="w-1/4">{t('Prop', { note: 'ui.prop' })}</p>
57
+ <p className="@max-xl:hidden">{t('Type', { note: 'ui.type' })}</p>
58
+ </div>
59
+ {Object.entries(type).map(([key, value]) => (
60
+ <Item key={key} parentId={id} name={key} item={value} />
61
+ ))}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ function Item({
67
+ parentId,
68
+ name,
69
+ item: {
70
+ parameters = [],
71
+ description,
72
+ required = false,
73
+ deprecated,
74
+ typeDescription,
75
+ default: defaultValue,
76
+ type,
77
+ typeDescriptionLink,
78
+ returns,
79
+ },
80
+ }: {
81
+ parentId?: string;
82
+ name: string;
83
+ item: TypeNode;
84
+ }) {
85
+ const t = useTranslations();
86
+ const { Link } = useUiComponents();
87
+ const [open, setOpen] = useState(false);
88
+ const id = parentId ? `${parentId}-${name}` : undefined;
89
+
90
+ useEffect(() => {
91
+ const hash = window.location.hash;
92
+ if (!id || !hash) return;
93
+ if (`#${id}` === hash) setOpen(true);
94
+ }, [id]);
95
+
96
+ return (
97
+ <Collapsible
98
+ id={id}
99
+ open={open}
100
+ onOpenChange={(v) => {
101
+ if (v && id) {
102
+ window.history.replaceState(null, '', `#${id}`);
103
+ }
104
+ setOpen(v);
105
+ }}
106
+ className={cn(
107
+ 'rounded-xl border overflow-hidden scroll-m-20 transition-all',
108
+ open ? 'shadow-sm bg-background not-last:mb-2' : 'border-transparent',
109
+ )}
110
+ >
111
+ <CollapsibleTrigger className="relative flex flex-row items-center w-full group text-start px-3 py-2 not-prose hover:bg-accent">
112
+ <code
113
+ className={cn(
114
+ 'text-primary min-w-fit w-1/4 font-mono font-medium pe-2',
115
+ deprecated && 'line-through text-primary/50',
116
+ )}
117
+ >
118
+ {name}
119
+ {!required && '?'}
120
+ </code>
121
+ {typeDescriptionLink ? (
122
+ <Link href={typeDescriptionLink} className="underline @max-xl:hidden">
123
+ {type}
124
+ </Link>
125
+ ) : (
126
+ <span className="@max-xl:hidden">{type}</span>
127
+ )}
128
+ <ChevronDown className="absolute inset-e-2 size-4 text-muted-foreground transition-transform group-data-[open]:rotate-180" />
129
+ </CollapsibleTrigger>
130
+ <CollapsibleContent>
131
+ <div className="grid grid-cols-[1fr_3fr] gap-y-4 text-sm p-3 overflow-auto scroll-container border-t">
132
+ <div className="prose prose-sm col-span-full prose-no-margin empty:hidden">
133
+ {description}
134
+ </div>
135
+ {typeDescription && (
136
+ <>
137
+ <p className={cn(fieldVariants())}>{t('Type', { note: 'ui.type' })}</p>
138
+ <p className="my-auto not-prose">{typeDescription}</p>
139
+ </>
140
+ )}
141
+ {defaultValue && (
142
+ <>
143
+ <p className={cn(fieldVariants())}>{t('Default', { note: 'ui.default' })}</p>
144
+ <p className="my-auto not-prose">{defaultValue}</p>
145
+ </>
146
+ )}
147
+ {parameters.length > 0 && (
148
+ <>
149
+ <p className={cn(fieldVariants())}>{t('Parameters', { note: 'ui.parameters' })}</p>
150
+ <div className="flex flex-col gap-2">
151
+ {parameters.map((param) => (
152
+ <div key={param.name} className="inline-flex items-center flex-wrap gap-1">
153
+ <p className="font-medium not-prose text-nowrap">{param.name} -</p>
154
+ <div className="prose prose-sm prose-no-margin">{param.description}</div>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ </>
159
+ )}
160
+ {returns && (
161
+ <>
162
+ <p className={cn(fieldVariants())}>{t('Returns', { note: 'ui.returns' })}</p>
163
+ <div className="my-auto prose prose-sm prose-no-margin">{returns}</div>
164
+ </>
165
+ )}
166
+ </div>
167
+ </CollapsibleContent>
168
+ </Collapsible>
169
+ );
170
+ }
171
+
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+ import Link from 'next/link';
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useMemo,
7
+ type ComponentProps,
8
+ type ComponentType,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { flushSync } from 'react-dom';
12
+ import { DirectionProvider } from '@base-ui/react/direction-provider';
13
+ import {
14
+ ThemeProvider,
15
+ useTheme,
16
+ type ThemeProviderProps,
17
+ } from 'next-themes';
18
+ import { TranslationProvider } from '@terrafirmagreg-team/translate';
19
+ import { useHotKey } from '@terrafirmagreg-team/ui/hooks/use-hot-key';
20
+
21
+ export type UiLinkProps = { href: string; prefetch?: boolean } & Omit<
22
+ ComponentProps<'a'>,
23
+ 'href'
24
+ >;
25
+
26
+ export type UiComponents = {
27
+ Link?: ComponentType<UiLinkProps>;
28
+ };
29
+
30
+ const defaults: Required<UiComponents> = {
31
+ Link: Link as ComponentType<UiLinkProps>,
32
+ };
33
+
34
+ const UiComponentsContext = createContext<UiComponents | null>(null);
35
+
36
+ export function UiProvider({
37
+ components,
38
+ translations,
39
+ children,
40
+ }: {
41
+ components?: UiComponents;
42
+ translations?: Record<string, string>;
43
+ children: ReactNode;
44
+ }) {
45
+ const parent = useContext(UiComponentsContext);
46
+ const merged = useMemo(
47
+ () => ({ ...parent, ...components }),
48
+ [parent, components],
49
+ );
50
+ const inner = (
51
+ <UiComponentsContext.Provider value={merged}>
52
+ {children}
53
+ </UiComponentsContext.Provider>
54
+ );
55
+ if (!translations) return inner;
56
+ return (
57
+ <TranslationProvider translations={translations}>
58
+ {inner}
59
+ </TranslationProvider>
60
+ );
61
+ }
62
+
63
+ export function useUiComponents(): Required<UiComponents> {
64
+ return { ...defaults, ...useContext(UiComponentsContext) };
65
+ }
66
+
67
+ export interface ThemeOptions extends ThemeProviderProps {
68
+
69
+ enabled?: boolean;
70
+
71
+ hotKey?: string | ((e: KeyboardEvent) => boolean) | false;
72
+ }
73
+
74
+ function ThemeHotKey({
75
+ hotKey,
76
+ }: {
77
+ hotKey: Exclude<ThemeOptions['hotKey'], false | undefined>;
78
+ }) {
79
+ const { setTheme, resolvedTheme } = useTheme();
80
+
81
+ useHotKey(
82
+ (e) => {
83
+
84
+ const matched =
85
+ typeof hotKey === 'string'
86
+ ? !e.metaKey &&
87
+ !e.ctrlKey &&
88
+ !e.altKey &&
89
+ e.key.toLowerCase() === hotKey.toLowerCase()
90
+ : hotKey(e);
91
+ if (!matched) return;
92
+
93
+ e.preventDefault();
94
+ const next = resolvedTheme === 'dark' ? 'light' : 'dark';
95
+ if (document?.startViewTransition) {
96
+ document.startViewTransition(() => flushSync(() => setTheme(next)));
97
+ } else {
98
+ setTheme(next);
99
+ }
100
+ },
101
+ { ignoreTyping: true },
102
+ );
103
+
104
+ return null;
105
+ }
106
+
107
+ export function UiRootProvider({
108
+ children,
109
+ dir = 'ltr',
110
+ theme = {},
111
+ }: {
112
+ children?: ReactNode;
113
+
114
+ dir?: 'rtl' | 'ltr';
115
+
116
+ theme?: ThemeOptions;
117
+ }) {
118
+ let body = children;
119
+
120
+ if (theme?.enabled !== false) {
121
+ const { enabled: _, hotKey = 'd', ...themeProps } = theme;
122
+
123
+ body = (
124
+ <ThemeProvider
125
+ attribute="class"
126
+ defaultTheme="system"
127
+ enableSystem
128
+ disableTransitionOnChange
129
+ {...themeProps}
130
+ >
131
+ {hotKey !== false && <ThemeHotKey hotKey={hotKey} />}
132
+ {body}
133
+ </ThemeProvider>
134
+ );
135
+ }
136
+
137
+ return <DirectionProvider direction={dir}>{body}</DirectionProvider>;
138
+ }
139
+