@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,315 @@
1
+ 'use client';
2
+
3
+ import Image, { type ImageProps } from 'next/image';
4
+ import {
5
+ type ComponentProps,
6
+ createContext,
7
+ type CSSProperties,
8
+ type ReactNode,
9
+ use,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import { createPortal, flushSync } from 'react-dom';
14
+ import Zoom, { type UncontrolledProps } from 'react-medium-image-zoom';
15
+ import { ChevronLeft, ChevronRight, X } from 'lucide-react';
16
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
17
+ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
18
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
19
+ import '../../../styles/image-zoom.css';
20
+
21
+ export type ImageZoomProps = ImageProps & {
22
+
23
+ zoomInProps?: ComponentProps<'img'>;
24
+
25
+ rmiz?: UncontrolledProps;
26
+
27
+ caption?: ReactNode;
28
+ };
29
+
30
+ interface Item {
31
+ element: HTMLElement;
32
+ image: ComponentProps<'img'>;
33
+ caption?: ReactNode;
34
+ }
35
+
36
+ interface Slide extends Item {
37
+ style?: CSSProperties;
38
+ }
39
+
40
+ const LightboxContext = createContext<{
41
+ register: (item: Item) => () => void;
42
+ open: (element: HTMLElement) => void;
43
+ } | null>(null);
44
+
45
+ function getImageSrc(src: ImageProps['src']): string {
46
+ if (typeof src === 'string') return src;
47
+
48
+ if (typeof src === 'object') {
49
+
50
+ if ('default' in src) return (src as { default: { src: string } }).default.src;
51
+ return src.src;
52
+ }
53
+
54
+ return '';
55
+ }
56
+
57
+ export function ImageZoom({ zoomInProps, children, rmiz, caption, ...props }: ImageZoomProps) {
58
+ const lightbox = use(LightboxContext);
59
+ const zoomImg = { src: getImageSrc(props.src), sizes: undefined, ...zoomInProps };
60
+ const image = children ?? (
61
+ <Image sizes="(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 900px" {...props} />
62
+ );
63
+
64
+ if (lightbox) {
65
+ return (
66
+ <span
67
+ role="button"
68
+ tabIndex={0}
69
+ aria-haspopup="dialog"
70
+ ref={(element) => {
71
+ if (!element) return;
72
+ return lightbox.register({
73
+ element,
74
+ image: { alt: props.alt, ...zoomImg },
75
+ caption: caption ?? props.title,
76
+ });
77
+ }}
78
+ onClick={(e) => {
79
+ if (e.target !== e.currentTarget) lightbox.open(e.currentTarget);
80
+ }}
81
+ onKeyDown={(e) => {
82
+ if (e.key !== 'Enter' && e.key !== ' ') return;
83
+ e.preventDefault();
84
+ lightbox.open(e.currentTarget);
85
+ }}
86
+ className="block outline-none [&_img]:cursor-zoom-in [&:focus-visible_img]:ring-2 [&:focus-visible_img]:ring-ring"
87
+ >
88
+ {image}
89
+ </span>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <Zoom zoomMargin={20} wrapElement="span" {...rmiz} zoomImg={zoomImg}>
95
+ {image}
96
+ </Zoom>
97
+ );
98
+ }
99
+
100
+ export function Lightbox({ children }: { children: ReactNode }) {
101
+ const t = useTranslations();
102
+ const [slides, setSlides] = useState<Slide[] | null>(null);
103
+ const scrollerRef = useRef<HTMLDivElement>(null);
104
+ const [context] = useState(() => {
105
+ const items = new Set<Item>();
106
+
107
+ return {
108
+ register(item: Item) {
109
+ items.add(item);
110
+ return () => void items.delete(item);
111
+ },
112
+ open(element: HTMLElement) {
113
+ const list: Slide[] = [];
114
+ for (const item of items) {
115
+ if (!item.element.checkVisibility()) continue;
116
+ const img = item.element.querySelector('img');
117
+ list.push({ ...item, style: img ? getSlideStyle(img) : undefined });
118
+ }
119
+ list.sort((a, b) =>
120
+ a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,
121
+ );
122
+ const index = list.findIndex((slide) => slide.element === element);
123
+
124
+ transition(getImage(element), () => {
125
+ flushSync(() => setSlides(list));
126
+ const slide = scrollerRef.current!.children[index];
127
+ slide.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'instant' });
128
+ return slide.querySelector('img')!;
129
+ });
130
+ },
131
+ };
132
+ });
133
+
134
+ function close() {
135
+ const scroller = scrollerRef.current;
136
+ if (!scroller || !slides) return;
137
+ const index = getIndex(scroller);
138
+ const { element } = slides[index];
139
+
140
+ transition(scroller.children[index].querySelector('img')!, () => {
141
+ flushSync(() => setSlides(null));
142
+ element.focus({ preventScroll: true });
143
+ const image = getImage(element);
144
+ image.scrollIntoView({ block: 'nearest', behavior: 'instant' });
145
+ return image;
146
+ });
147
+ }
148
+
149
+ function navigate(step: number) {
150
+ const scroller = scrollerRef.current!;
151
+ scroller.children[getIndex(scroller) + step]?.scrollIntoView({
152
+ block: 'nearest',
153
+ inline: 'start',
154
+ behavior: 'smooth',
155
+ });
156
+ }
157
+
158
+ return (
159
+ <LightboxContext value={context}>
160
+ {children}
161
+ {slides &&
162
+ createPortal(
163
+ <dialog
164
+ ref={initDialog}
165
+ tabIndex={-1}
166
+ aria-label={t('Image Viewer', { note: 'ui.image_viewer' })}
167
+ onCancel={(e) => {
168
+ e.preventDefault();
169
+ close();
170
+ }}
171
+ onClose={() => setSlides(null)}
172
+ onKeyDown={(e) => {
173
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
174
+ e.preventDefault();
175
+ const rtl = getComputedStyle(e.currentTarget).direction === 'rtl';
176
+ navigate((e.key === 'ArrowRight') !== rtl ? 1 : -1);
177
+ }}
178
+ className="m-0 size-full max-h-none max-w-none overflow-hidden border-none bg-background p-0 text-foreground outline-none"
179
+ >
180
+ <button
181
+ type="button"
182
+ aria-label={t('Close', { note: 'ui.close' })}
183
+ onClick={close}
184
+ className={cn(
185
+ buttonVariants({
186
+ variant: 'ghost',
187
+ size: 'icon-sm',
188
+ className: 'absolute top-3.25 inset-e-2.5',
189
+ }),
190
+ )}
191
+ >
192
+ <X />
193
+ </button>
194
+ <div
195
+ ref={scrollerRef}
196
+ onClick={(e) => {
197
+ if (!(e.target as Element).closest('figcaption')) close();
198
+ }}
199
+ className="flex size-full snap-x snap-mandatory overflow-x-auto overscroll-x-contain [scrollbar-width:none]"
200
+ >
201
+ {slides.map((slide, i) => (
202
+ <figure
203
+ key={i}
204
+ className="flex w-full shrink-0 snap-start snap-always flex-col items-center pb-6"
205
+ >
206
+ <p className="flex h-14 w-full items-center px-4 text-sm text-muted-foreground tabular-nums">
207
+ {slides.length > 1 && `${i + 1} / ${slides.length}`}
208
+ </p>
209
+ <div className="flex min-h-0 w-full flex-1 items-center justify-center px-4 [container-type:size] md:px-16">
210
+ <img
211
+ loading="lazy"
212
+ {...slide.image}
213
+ style={slide.style}
214
+ className={cn('max-h-full cursor-zoom-out bg-cover', slide.image.className)}
215
+ />
216
+ </div>
217
+ {slide.caption && (
218
+ <figcaption className="prose prose-sm mt-4 px-4 text-center text-muted-foreground md:px-16">
219
+ {slide.caption}
220
+ </figcaption>
221
+ )}
222
+ </figure>
223
+ ))}
224
+ </div>
225
+ {slides.length > 1 && (
226
+ <>
227
+ <button
228
+ type="button"
229
+ aria-label={t('Previous Image', { note: 'ui.prev_image' })}
230
+ onClick={() => navigate(-1)}
231
+ className={cn(
232
+ buttonVariants({
233
+ variant: 'ghost',
234
+ size: 'icon',
235
+ className: 'absolute top-1/2 inset-s-2.5 -translate-y-1/2 max-md:hidden',
236
+ }),
237
+ )}
238
+ >
239
+ <ChevronLeft className="rtl:-scale-x-100" />
240
+ </button>
241
+ <button
242
+ type="button"
243
+ aria-label={t('Next Image', { note: 'ui.next_image' })}
244
+ onClick={() => navigate(1)}
245
+ className={cn(
246
+ buttonVariants({
247
+ variant: 'ghost',
248
+ size: 'icon',
249
+ className: 'absolute top-1/2 inset-e-2.5 -translate-y-1/2 max-md:hidden',
250
+ }),
251
+ )}
252
+ >
253
+ <ChevronRight className="rtl:-scale-x-100" />
254
+ </button>
255
+ </>
256
+ )}
257
+ </dialog>,
258
+ document.body,
259
+ )}
260
+ </LightboxContext>
261
+ );
262
+ }
263
+
264
+ function initDialog(element: HTMLDialogElement) {
265
+
266
+ if (!element.open) element.showModal();
267
+ element.focus();
268
+ const { style } = document.documentElement;
269
+ const overflow = style.overflow;
270
+ style.overflow = 'hidden';
271
+ return () => {
272
+ style.overflow = overflow;
273
+ };
274
+ }
275
+
276
+ function getIndex(scroller: HTMLElement) {
277
+ return Math.round(Math.abs(scroller.scrollLeft) / scroller.clientWidth);
278
+ }
279
+
280
+ function getImage(element: HTMLElement): HTMLElement {
281
+ return element.querySelector('img') ?? element;
282
+ }
283
+
284
+ function getSlideStyle(img: HTMLImageElement): CSSProperties {
285
+ const ratio = (img.naturalWidth || img.width) / (img.naturalHeight || img.height);
286
+
287
+ return {
288
+ width: ratio ? `min(100cqw, ${ratio} * 100cqh)` : undefined,
289
+ aspectRatio: ratio ? `auto ${ratio}` : undefined,
290
+ backgroundImage: img.currentSrc ? `url(${JSON.stringify(img.currentSrc)})` : undefined,
291
+ };
292
+ }
293
+
294
+ function transition(from: HTMLElement, update: () => HTMLElement) {
295
+ if (
296
+ !('startViewTransition' in document) ||
297
+ matchMedia('(prefers-reduced-motion: reduce)').matches
298
+ ) {
299
+ update();
300
+ return;
301
+ }
302
+
303
+ let to: HTMLElement | undefined;
304
+ from.style.viewTransitionName = 'lightbox';
305
+ document
306
+ .startViewTransition(() => {
307
+ from.style.viewTransitionName = '';
308
+ to = update();
309
+ to.style.viewTransitionName = 'lightbox';
310
+ })
311
+ .finished.finally(() => {
312
+ if (to) to.style.viewTransitionName = '';
313
+ });
314
+ }
315
+
@@ -0,0 +1,30 @@
1
+ import katex from 'katex';
2
+ import { useMemo } from 'react';
3
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
4
+
5
+ export function Math({
6
+ children,
7
+ block = false,
8
+ className,
9
+ }: {
10
+ children?: string;
11
+ block?: boolean;
12
+ className?: string;
13
+ }) {
14
+ const html = useMemo(
15
+ () =>
16
+ katex.renderToString(children ?? '', {
17
+ displayMode: block,
18
+ throwOnError: false,
19
+ }),
20
+ [children, block],
21
+ );
22
+ const Tag = block ? 'div' : 'span';
23
+ return (
24
+ <Tag
25
+ className={cn(block && 'my-4 overflow-x-auto text-center', className)}
26
+ dangerouslySetInnerHTML={{ __html: html }}
27
+ />
28
+ );
29
+ }
30
+
@@ -0,0 +1,23 @@
1
+ import { CodeBlock, Pre } from '@terrafirmagreg-team/ui/components/mdx/codeblock';
2
+ import { renderMermaidChart } from '@terrafirmagreg-team/ui/lib/mdx/mermaid';
3
+
4
+ export function Mermaid({ chart }: { chart: string }) {
5
+ const svg = renderMermaidChart(chart);
6
+
7
+ if (svg === null) {
8
+ return (
9
+ <CodeBlock title="Mermaid">
10
+ <Pre>
11
+ {chart.split('\n').map((line, i) => (
12
+ <span key={i} className="line">
13
+ {line}
14
+ </span>
15
+ ))}
16
+ </Pre>
17
+ </CodeBlock>
18
+ );
19
+ }
20
+
21
+ return <div dangerouslySetInnerHTML={{ __html: svg }} />;
22
+ }
23
+
@@ -0,0 +1,19 @@
1
+ export function Multiblock({
2
+ id,
3
+ children,
4
+ }: {
5
+ id?: string;
6
+ children?: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <div className="not-prose my-4 rounded-xl border border-dashed border-border bg-secondary/40 p-4">
10
+ <div className="text-xs uppercase tracking-wide text-muted-foreground mb-2">
11
+ Multiblock{id ? ` · ${id}` : ''}
12
+ </div>
13
+ <div className="text-sm text-muted-foreground [&_pre]:overflow-x-auto [&_code]:font-mono">
14
+ {children ?? 'Multiblock pattern will render here.'}
15
+ </div>
16
+ </div>
17
+ );
18
+ }
19
+
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
5
+ import type { ReactNode } from 'react';
6
+
7
+ function Pane({
8
+ kind,
9
+ label,
10
+ children,
11
+ className,
12
+ }: {
13
+ kind: 'input' | 'output';
14
+ label?: string;
15
+ children?: ReactNode;
16
+ className?: string;
17
+ }) {
18
+ const t = useTranslations();
19
+ const text =
20
+ label ?? (kind === 'input' ? t('Input', { note: 'ui.input' }) : t('Output', { note: 'ui.output' }));
21
+
22
+ return (
23
+ <div className={cn('min-w-0', className)}>
24
+ <div className="mb-3 flex items-center gap-2.5">
25
+ <span
26
+ className={cn(
27
+ 'inline-flex shrink-0 items-center rounded-md border px-2 py-0.5 font-mono text-2xs font-semibold uppercase tracking-wider',
28
+ kind === 'input'
29
+ ? 'border-border bg-secondary text-muted-foreground'
30
+ : 'border-primary/30 bg-primary/10 text-primary',
31
+ )}
32
+ >
33
+ {text}
34
+ </span>
35
+ <span className="h-px min-w-4 flex-1 bg-border" aria-hidden />
36
+ </div>
37
+ <div
38
+ className={cn(
39
+ kind === 'input' &&
40
+ '[&_.codeblock]:my-0 [&_figure]:my-0 [&_pre]:my-0',
41
+ )}
42
+ >
43
+ {children}
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ function Preview({
50
+ children,
51
+ className,
52
+ }: {
53
+ children?: ReactNode;
54
+ className?: string;
55
+ }) {
56
+ return (
57
+ <section
58
+ className={cn(
59
+ 'dev-doc-preview my-6 space-y-5 rounded-lg border border-dashed border-border bg-transparent p-4',
60
+ className,
61
+ )}
62
+ >
63
+ {children}
64
+ </section>
65
+ );
66
+ }
67
+
68
+ function Input({
69
+ children,
70
+ label,
71
+ className,
72
+ }: {
73
+ children?: ReactNode;
74
+ label?: string;
75
+ className?: string;
76
+ }) {
77
+ return (
78
+ <Pane kind="input" label={label} className={className}>
79
+ {children}
80
+ </Pane>
81
+ );
82
+ }
83
+
84
+ function Output({
85
+ children,
86
+ label,
87
+ className,
88
+ }: {
89
+ children?: ReactNode;
90
+ label?: string;
91
+ className?: string;
92
+ }) {
93
+ return (
94
+ <Pane kind="output" label={label} className={className}>
95
+ {children}
96
+ </Pane>
97
+ );
98
+ }
99
+
100
+ export { Preview, Input, Output };
101
+
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export function Steps({ children }: { children: ReactNode }) {
4
+ return <div className="fd-steps">{children}</div>;
5
+ }
6
+
7
+ export function Step({ children }: { children: ReactNode }) {
8
+ return <div className="fd-step">{children}</div>;
9
+ }
10
+
@@ -0,0 +1,14 @@
1
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
2
+ import type { ComponentProps } from 'react';
3
+
4
+ export function Table(props: ComponentProps<'table'>) {
5
+ return (
6
+ <div className="relative overflow-auto prose-no-margin my-6" tabIndex={0}>
7
+ <table
8
+ {...props}
9
+ className={cn('w-full', props.className)}
10
+ />
11
+ </div>
12
+ );
13
+ }
14
+