@terrafirmagreg-team/ui 0.1.0 → 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 (157) hide show
  1. package/package.json +14 -14
  2. package/src/components/blocks/button/icon-button.tsx +1 -0
  3. package/src/components/blocks/canvas/chevron-edge.tsx +126 -18
  4. package/src/components/blocks/canvas/flow-canvas.tsx +4 -0
  5. package/src/components/blocks/canvas/flow-toolbar.tsx +1 -0
  6. package/src/components/blocks/canvas/flow-viewport.tsx +1 -0
  7. package/src/components/blocks/fit-box.tsx +26 -0
  8. package/src/components/blocks/git/github-info.tsx +1 -0
  9. package/src/components/blocks/lang/content.tsx +40 -10
  10. package/src/components/blocks/lang/flag.tsx +1 -0
  11. package/src/components/blocks/lang/link.tsx +1 -0
  12. package/src/components/blocks/lang/redirect.tsx +2 -1
  13. package/src/components/blocks/lang/switcher.tsx +1 -0
  14. package/src/components/blocks/lang/untranslated-callout.tsx +4 -3
  15. package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
  16. package/src/components/blocks/link/link-preview.tsx +2 -1
  17. package/src/components/blocks/list/row.tsx +1 -0
  18. package/src/components/blocks/media/glb/model.tsx +1 -0
  19. package/src/components/blocks/media/glb/scene.tsx +13 -12
  20. package/src/components/blocks/media/glb/viewer.tsx +1 -0
  21. package/src/components/blocks/media/image.tsx +3 -2
  22. package/src/components/blocks/media/slideshow.tsx +4 -3
  23. package/src/components/blocks/nav/breadcrumb.tsx +1 -0
  24. package/src/components/blocks/nav/copy-menu.tsx +37 -0
  25. package/src/components/blocks/nav/link.tsx +1 -0
  26. package/src/components/blocks/nav/site-menu.tsx +1 -0
  27. package/src/components/blocks/nav/site-title.tsx +1 -0
  28. package/src/components/blocks/nav/switcher.tsx +1 -0
  29. package/src/components/blocks/page/empty-state.tsx +1 -0
  30. package/src/components/blocks/page/heading.tsx +3 -2
  31. package/src/components/blocks/page/pager.tsx +110 -2
  32. package/src/components/blocks/readability/fields.tsx +250 -0
  33. package/src/components/blocks/readability/menu.tsx +74 -258
  34. package/src/components/blocks/readability/track.tsx +2 -1
  35. package/src/components/blocks/search/command.tsx +1 -0
  36. package/src/components/blocks/search/dialog.tsx +21 -9
  37. package/src/components/blocks/search/provider.tsx +2 -1
  38. package/src/components/blocks/sidebar.tsx +5 -5
  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 +4 -0
  42. package/src/components/blocks/tooltip/hint.tsx +1 -0
  43. package/src/components/blocks/tooltip/lazy.tsx +41 -0
  44. package/src/components/blocks/viewer/controls.tsx +11 -6
  45. package/src/components/blocks/viewer/float-bar.tsx +1 -0
  46. package/src/components/layouts/layout.tsx +2 -1
  47. package/src/components/layouts/left-sidebar/content.tsx +1 -0
  48. package/src/components/layouts/left-sidebar/context.tsx +1 -0
  49. package/src/components/layouts/left-sidebar/expand.tsx +2 -1
  50. package/src/components/layouts/left-sidebar/footer.tsx +10 -4
  51. package/src/components/layouts/left-sidebar/index.ts +1 -0
  52. package/src/components/layouts/left-sidebar/items.tsx +2 -1
  53. package/src/components/layouts/left-sidebar/search.tsx +4 -3
  54. package/src/components/layouts/left-sidebar/sidebar.tsx +3 -2
  55. package/src/components/layouts/left-sidebar/tree.tsx +1 -0
  56. package/src/components/layouts/right-sidebar.tsx +1 -0
  57. package/src/components/layouts/tree-context.tsx +1 -0
  58. package/src/components/mdx/accordion.tsx +3 -2
  59. package/src/components/mdx/banner.tsx +15 -12
  60. package/src/components/mdx/callout.tsx +1 -0
  61. package/src/components/mdx/card.tsx +12 -5
  62. package/src/components/mdx/codeblock.tsx +5 -4
  63. package/src/components/mdx/collapsible.tsx +1 -0
  64. package/src/components/mdx/files.tsx +1 -0
  65. package/src/components/mdx/glow-header.tsx +17 -3
  66. package/src/components/mdx/gradient-text.tsx +1 -0
  67. package/src/components/mdx/icon.tsx +35 -0
  68. package/src/components/mdx/image-zoom.tsx +5 -4
  69. package/src/components/mdx/math.tsx +30 -0
  70. package/src/components/mdx/mermaid.tsx +8 -1
  71. package/src/components/mdx/multiblock.tsx +1 -0
  72. package/src/components/mdx/preview.tsx +2 -1
  73. package/src/components/mdx/steps.tsx +1 -0
  74. package/src/components/mdx/table.tsx +1 -0
  75. package/src/components/mdx/tabs.tsx +2 -1
  76. package/src/components/mdx/type-table.tsx +7 -6
  77. package/src/{provider.tsx → components/provider.tsx} +1 -3
  78. package/src/hooks/canvas/use-flow-hover.ts +1 -0
  79. package/src/hooks/canvas/use-flow-viewport-persist.ts +1 -0
  80. package/src/hooks/page/use-fit-pages.ts +1 -0
  81. package/src/hooks/page/use-location-search.ts +8 -2
  82. package/src/hooks/page/use-page-preview.ts +2 -1
  83. package/src/hooks/page/use-paged-list.ts +1 -0
  84. package/src/hooks/readability/{provider.ts → use-readability.ts} +1 -0
  85. package/src/hooks/search/{dialog.ts → use-dialog.ts} +1 -0
  86. package/src/hooks/search/use-highlight-query.ts +8 -1
  87. package/src/hooks/search/{provider.ts → use-provider.ts} +1 -0
  88. package/src/hooks/use-async-value.ts +6 -3
  89. package/src/hooks/use-close-on-scroll.ts +1 -0
  90. package/src/hooks/use-copy-button.ts +1 -0
  91. package/src/hooks/use-cycle-index.ts +1 -0
  92. package/src/hooks/use-document-title.ts +1 -0
  93. package/src/hooks/use-element-size.ts +1 -0
  94. package/src/hooks/use-external-meta.ts +2 -1
  95. package/src/hooks/use-hot-key.ts +1 -0
  96. package/src/hooks/use-hover-follow.ts +1 -0
  97. package/src/hooks/use-in-view.ts +1 -0
  98. package/src/hooks/{lang.ts → use-lang.ts} +1 -0
  99. package/src/hooks/use-last-value.ts +1 -0
  100. package/src/hooks/{layout.ts → use-layout.ts} +1 -0
  101. package/src/hooks/use-marquee.ts +1 -0
  102. package/src/hooks/use-media-query.ts +1 -0
  103. package/src/hooks/use-mobile.ts +1 -0
  104. package/src/hooks/use-modifier-keys.ts +1 -0
  105. package/src/hooks/use-on-change.ts +1 -0
  106. package/src/hooks/use-pane-keys.ts +1 -0
  107. package/src/hooks/{sidebar.ts → use-sidebar.ts} +1 -0
  108. package/src/hooks/use-swipe.ts +1 -0
  109. package/src/hooks/use-tree.ts +1 -3
  110. package/src/hooks/viewer/use-in-float-bar.ts +1 -0
  111. package/src/hooks/viewer/use-viewer-prefs.ts +1 -0
  112. package/src/lib/canvas/edge.ts +78 -49
  113. package/src/lib/canvas/geometry.ts +1 -0
  114. package/src/lib/canvas/state.ts +1 -0
  115. package/src/lib/canvas/viewport.ts +1 -0
  116. package/src/lib/canvas/xyflow.ts +1 -0
  117. package/src/lib/config.ts +25 -0
  118. package/src/lib/mdx/callout.ts +1 -0
  119. package/src/lib/mdx/gradient.ts +1 -0
  120. package/src/lib/mdx/mermaid.ts +1 -0
  121. package/src/lib/media/crop-opaque.ts +1 -0
  122. package/src/lib/media/nearby-indexes.ts +1 -0
  123. package/src/lib/media/trim-transparent.ts +1 -0
  124. package/src/lib/readability/config.ts +1 -0
  125. package/src/lib/readability/spotlight.ts +1 -0
  126. package/src/lib/readability/store.ts +1 -0
  127. package/src/lib/tree.ts +1 -3
  128. package/src/lib/utils/cn.ts +1 -0
  129. package/src/lib/utils/format.ts +3 -3
  130. package/src/lib/utils/highlight-style.ts +10 -0
  131. package/src/lib/utils/history.ts +1 -0
  132. package/src/lib/utils/merge-refs.ts +1 -0
  133. package/src/lib/utils/nav-active.ts +1 -0
  134. package/src/lib/utils/stored-value.ts +1 -0
  135. package/src/lib/utils/urls.ts +2 -1
  136. package/src/locales/en_us.json +108 -117
  137. package/src/locales/pl_pl.json +33 -35
  138. package/src/locales/ru_ru.json +108 -117
  139. package/src/locales/uk_ua.json +33 -35
  140. package/src/locales/zh_cn.json +15 -17
  141. package/src/locales/zh_tw.json +15 -17
  142. package/src/locales.ts +1 -0
  143. package/src/types/canvas.ts +6 -1
  144. package/src/types/glb.ts +1 -0
  145. package/src/types/mdx-directives.ts +106 -0
  146. package/src/types/nav.ts +1 -0
  147. package/src/types/preview.ts +7 -1
  148. package/src/types/readability.ts +1 -0
  149. package/src/types/search.ts +1 -0
  150. package/src/types/tree.ts +1 -0
  151. package/src/types/viewer.ts +1 -0
  152. package/styles/content/base.css +198 -194
  153. package/styles/content/shiki.css +107 -108
  154. package/styles/content/typography.mjs +3637 -3941
  155. package/styles/content.css +5 -7
  156. package/styles.css +430 -413
  157. package/src/lib/utils/async-cache.ts +0 -32
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/ui",
3
- "version": "0.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
+ "description": "Shared UI kit",
6
+ "license": "LGPL-3.0-only",
5
7
  "exports": {
6
8
  "./styles.css": "./styles.css",
7
9
  "./styles/*": "./styles/*",
8
- "./provider": "./src/provider.tsx",
10
+ "./provider": "./src/components/provider.tsx",
9
11
  "./components/ui/*": [
10
12
  "./src/components/ui/*.tsx",
11
13
  "./src/components/ui/*/index.tsx"
@@ -21,15 +23,11 @@
21
23
  ],
22
24
  "./hooks/*": [
23
25
  "./src/hooks/*.ts",
24
- "./src/hooks/*.tsx",
25
- "./src/hooks/*/index.ts",
26
- "./src/hooks/*/index.tsx"
26
+ "./src/hooks/*/index.ts"
27
27
  ],
28
28
  "./lib/*": [
29
29
  "./src/lib/*.ts",
30
- "./src/lib/*.tsx",
31
- "./src/lib/*/index.ts",
32
- "./src/lib/*/index.tsx"
30
+ "./src/lib/*/index.ts"
33
31
  ],
34
32
  "./types/*": [
35
33
  "./src/types/*.ts",
@@ -47,7 +45,7 @@
47
45
  "@react-three/drei": "10.7.9",
48
46
  "@react-three/fiber": "9.8.2",
49
47
  "@shadcn/react": "^0.3.1",
50
- "@terrafirmagreg-team/translate": "0.1.0",
48
+ "@terrafirmagreg-team/translate": "1.2.0",
51
49
  "@xyflow/react": "12.12.0",
52
50
  "beautiful-mermaid": "^1.1.3",
53
51
  "class-variance-authority": "^0.7.1",
@@ -57,16 +55,18 @@
57
55
  "flag-icons": "^7.6.0",
58
56
  "hast-util-to-jsx-runtime": "^2.3.6",
59
57
  "input-otp": "^1.5.0",
60
- "lucide-react": "^1.63.0",
58
+ "katex": "^0.19.1",
59
+ "lucide-react": "^0.577.0",
61
60
  "next-themes": "^0.4.6",
62
61
  "react-day-picker": "^10.0.2",
63
62
  "react-medium-image-zoom": "^5.4.9",
64
- "react-resizable-panels": "^4.15.0",
63
+ "react-resizable-panels": "^4.18.0",
65
64
  "recharts": "3.10.1",
66
65
  "scroll-into-view-if-needed": "^3.1.0",
67
- "tailwindcss": "^4.3.3",
66
+ "tailwindcss": "^4.3.4",
68
67
  "three": "^0.186.1",
69
- "three-stdlib": "2.36.1"
68
+ "three-stdlib": "2.36.1",
69
+ "@terrafirmagreg-team/data": "1.2.0"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "next": ">=15",
@@ -78,7 +78,7 @@
78
78
  "@types/node": "^26.6.5",
79
79
  "@types/react": "^19.3.0",
80
80
  "@types/react-dom": "^19.3.0",
81
- "@types/three": "^0.186.0"
81
+ "@types/three": "^0.187.0"
82
82
  },
83
83
  "repository": {
84
84
  "type": "git",
@@ -46,3 +46,4 @@ export function IconButton({
46
46
  </Tooltip>
47
47
  );
48
48
  }
49
+
@@ -1,7 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { memo, useMemo, type FC } from 'react';
4
3
  import {
4
+ memo,
5
+ useCallback,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type FC,
10
+ type MouseEvent as ReactMouseEvent,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import {
14
+ EdgeLabelRenderer,
5
15
  useInternalNode,
6
16
  useStore,
7
17
  type EdgeProps,
@@ -9,8 +19,10 @@ import {
9
19
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
10
20
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
11
21
  import {
22
+ CHEVRON_GLYPH,
12
23
  chevronHighlightClass,
13
24
  chevronTicksAlongPath,
25
+ nearestPointOnPolyline,
14
26
  } from '@terrafirmagreg-team/ui/lib/canvas/edge';
15
27
  import type {
16
28
  ChevronEdgeHighlight,
@@ -26,24 +38,67 @@ type ChevronEdgeResolvers = {
26
38
  resolveCenter?: (node: InternalNode) => FlowPoint;
27
39
  resolveBorder?: (node: InternalNode, toward: FlowPoint) => FlowPoint;
28
40
  resolvePath?: (ctx: ChevronEdgePathContext) => FlowPoint[];
41
+ renderLabel?: (ctx: ChevronEdgePathContext) => ReactNode;
42
+ labelVisibility?: 'always' | 'hover';
29
43
  className?: string;
30
44
  };
31
45
 
46
+ function pathMidpoint(points: readonly FlowPoint[]): FlowPoint {
47
+ let total = 0;
48
+ for (let i = 1; i < points.length; i += 1) {
49
+ total += Math.hypot(
50
+ points[i]!.x - points[i - 1]!.x,
51
+ points[i]!.y - points[i - 1]!.y,
52
+ );
53
+ }
54
+ const half = total / 2;
55
+ let acc = 0;
56
+ for (let i = 1; i < points.length; i += 1) {
57
+ const a = points[i - 1]!;
58
+ const b = points[i]!;
59
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
60
+ if (acc + len >= half && len > 0) {
61
+ const t = (half - acc) / len;
62
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
63
+ }
64
+ acc += len;
65
+ }
66
+ return points[points.length - 1]!;
67
+ }
68
+
32
69
  const CHEVRON_TICK_MIN_ZOOM = 0.25;
33
70
 
34
71
  export function createChevronEdge({
35
72
  resolveCenter,
36
73
  resolveBorder,
37
74
  resolvePath,
75
+ renderLabel,
76
+ labelVisibility = 'always',
38
77
  className,
39
78
  }: ChevronEdgeResolvers): FC<EdgeProps> {
40
79
  return memo(function ChevronEdge(props: EdgeProps) {
41
80
  const { source, target, data } = props;
81
+ const [hover, setHover] = useState(false);
82
+ const [hoverPt, setHoverPt] = useState<FlowPoint | null>(null);
83
+ const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
84
+ const enter = useCallback(() => {
85
+ if (hoverTimer.current) clearTimeout(hoverTimer.current);
86
+ setHover(true);
87
+ }, []);
88
+ const leave = useCallback(() => {
89
+ hoverTimer.current = setTimeout(() => {
90
+ setHover(false);
91
+ setHoverPt(null);
92
+ }, 120);
93
+ }, []);
42
94
  const sourceNode = useInternalNode(source);
43
95
  const targetNode = useInternalNode(target);
44
96
  const farZoom = useStore(
45
97
  (state) => state.transform[2] < CHEVRON_TICK_MIN_ZOOM,
46
98
  );
99
+ const tx = useStore((state) => state.transform[0]);
100
+ const ty = useStore((state) => state.transform[1]);
101
+ const tz = useStore((state) => state.transform[2]);
47
102
  const sAbs = sourceNode?.internals.positionAbsolute;
48
103
  const tAbs = targetNode?.internals.positionAbsolute;
49
104
  const sSize = sourceNode?.measured;
@@ -88,47 +143,100 @@ export function createChevronEdge({
88
143
  targetY,
89
144
  targetPosition,
90
145
  ]);
91
- const runs = useMemo(
92
- () => (points && !farZoom ? chevronTicksAlongPath(points) : []),
146
+ const ticks = useMemo(
147
+ () =>
148
+ points && !farZoom
149
+ ? chevronTicksAlongPath(points)
150
+ : { offsets: [], total: 0 },
93
151
  [points, farZoom],
94
152
  );
95
153
  const highlight = data?.highlight as ChevronEdgeHighlight;
154
+ const mid = useMemo(
155
+ () => (points ? pathMidpoint(points) : null),
156
+ [points],
157
+ );
96
158
  if (data?.hidden === true) return null;
97
- if (!points || points.length < 2) return null;
159
+ if (!points || points.length < 2 || !mid) return null;
98
160
  const base = `M${points[0]!.x} ${points[0]!.y}${points
99
161
  .slice(1)
100
162
  .map((p) => `L${p.x} ${p.y}`)
101
163
  .join('')}`;
164
+ const move = useCallback(
165
+ (e: ReactMouseEvent<Element>) => {
166
+ if (!points || tz === 0) return;
167
+ const pane = e.currentTarget.closest('.react-flow');
168
+ if (!pane) return;
169
+ const rect = pane.getBoundingClientRect();
170
+ const p = {
171
+ x: (e.clientX - rect.left - tx) / tz,
172
+ y: (e.clientY - rect.top - ty) / tz,
173
+ };
174
+ setHoverPt(nearestPointOnPolyline(points, p));
175
+ },
176
+ [points, tx, ty, tz],
177
+ );
178
+ const hoverTrack =
179
+ labelVisibility === 'hover'
180
+ ? { onMouseEnter: enter, onMouseLeave: leave, onMouseMove: move }
181
+ : {};
182
+ const labelOn =
183
+ labelVisibility !== 'hover' || hover || props.selected === true;
184
+ const labelAt = hoverPt ?? mid;
102
185
  return (
186
+ <>
103
187
  <g
104
188
  className={cn(
105
189
  'chevron-edge',
106
190
  className,
107
191
  chevronHighlightClass(highlight),
108
192
  )}
193
+ {...hoverTrack}
109
194
  >
195
+ <path
196
+ className="chevron-edge__interaction"
197
+ d={base}
198
+ fill="none"
199
+ stroke="transparent"
200
+ strokeWidth={props.interactionWidth ?? 20}
201
+ />
110
202
  <path
111
203
  className="chevron-edge__stroke chevron-edge__base"
112
204
  d={base}
113
205
  aria-hidden="true"
114
206
  />
115
- {runs.map((run, i) => (
116
- <g
117
- key={i}
118
- className="chevron-edge__ticks"
119
- style={{
120
- ['--chevron-edge-shift-x' as string]: `${run.shiftX}px`,
121
- ['--chevron-edge-shift-y' as string]: `${run.shiftY}px`,
122
- }}
123
- >
207
+ <g className="chevron-edge__ticks" aria-hidden="true">
208
+ {ticks.offsets.map((offset) => (
124
209
  <path
125
- className="chevron-edge__stroke chevron-edge__stroke--main"
126
- d={run.d}
127
- aria-hidden="true"
210
+ key={offset}
211
+ className="chevron-edge__stroke chevron-edge__stroke--main chevron-edge__tick"
212
+ d={CHEVRON_GLYPH}
213
+ style={{
214
+ offsetPath: `path("${base}")`,
215
+ ['--chevron-o' as string]: `${offset}px`,
216
+ }}
128
217
  />
129
- </g>
130
- ))}
218
+ ))}
219
+ </g>
131
220
  </g>
221
+ {renderLabel && labelOn ? (
222
+ <EdgeLabelRenderer>
223
+ <div
224
+ className="chevron-edge__label nodrag nopan"
225
+ {...hoverTrack}
226
+ style={{
227
+ transform: `translate(-50%, -50%) translate(${labelAt.x}px, ${labelAt.y}px)`,
228
+ }}
229
+ >
230
+ {renderLabel({
231
+ ...props,
232
+ sourceNode: sourceNode!,
233
+ targetNode: targetNode!,
234
+ })}
235
+ </div>
236
+ </EdgeLabelRenderer>
237
+ ) : null}
238
+ </>
132
239
  );
133
240
  });
134
241
  }
242
+
@@ -15,6 +15,7 @@ import {
15
15
  type Node,
16
16
  type ReactFlowProps,
17
17
  } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
18
+ import { useTheme } from 'next-themes';
18
19
  import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
19
20
  import { useFlowViewportPersist } from '@terrafirmagreg-team/ui/hooks/canvas/use-flow-viewport-persist';
20
21
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
@@ -67,6 +68,7 @@ export function FlowCanvas<N extends Node = Node, E extends Edge = Edge>({
67
68
  const canvasRef = useRef<HTMLDivElement>(null);
68
69
  const viewSize = useElementSize(canvasRef);
69
70
  const { setCenter } = useReactFlow();
71
+ const { resolvedTheme } = useTheme();
70
72
  const { markReady, persist } = useFlowViewportPersist(
71
73
  viewportConfig.storageKey,
72
74
  viewportConfig.resetKey,
@@ -156,6 +158,7 @@ export function FlowCanvas<N extends Node = Node, E extends Edge = Edge>({
156
158
  minZoom={FLOW_MIN_ZOOM}
157
159
  maxZoom={FLOW_MAX_ZOOM}
158
160
  proOptions={{ hideAttribution: true }}
161
+ colorMode={resolvedTheme === 'dark' ? 'dark' : 'light'}
159
162
  className="h-full bg-card"
160
163
  onMoveEnd={handleMoveEnd}
161
164
  {...flowProps}
@@ -175,3 +178,4 @@ export function FlowCanvas<N extends Node = Node, E extends Edge = Edge>({
175
178
  </div>
176
179
  );
177
180
  }
181
+
@@ -30,3 +30,4 @@ export function FlowToolbar({ onFit, search }: FlowToolbarProps) {
30
30
  </ViewerToolbar>
31
31
  );
32
32
  }
33
+
@@ -241,3 +241,4 @@ export function FlowViewport({
241
241
 
242
242
  return null;
243
243
  }
244
+
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { useRef, type ReactNode } from 'react';
4
+ import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
5
+
6
+ export function FitBox({
7
+ w,
8
+ children,
9
+ }: {
10
+ w: number;
11
+ children: ReactNode;
12
+ }) {
13
+ const ref = useRef<HTMLDivElement>(null);
14
+ const { w: avail } = useElementSize(ref);
15
+ const fit = w > 0 && avail > 0 ? Math.min(1, avail / w) : 1;
16
+ return (
17
+ <div ref={ref} className="w-full">
18
+ <div
19
+ className="mx-auto w-fit"
20
+ style={fit < 1 ? { zoom: fit } : undefined}
21
+ >
22
+ {children}
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -75,3 +75,4 @@ export async function GithubInfo({
75
75
  />
76
76
  );
77
77
  }
78
+
@@ -1,22 +1,52 @@
1
1
  'use client';
2
- import { useLocaleRegistry } from '@terrafirmagreg-team/translate/hooks/use-locale-registry';
2
+ import { useEffect, useState, type ReactNode } from 'react';
3
3
  import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
4
- import type { LangContent as LangContentMap } from '@terrafirmagreg-team/translate/types/content';
5
- import { UntranslatedContext } from '@terrafirmagreg-team/ui/hooks/lang';
4
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
5
+ import type { LangContent } from '@terrafirmagreg-team/translate/types/content';
6
+ import { UntranslatedContext } from '@terrafirmagreg-team/ui/hooks/use-lang';
6
7
 
7
8
  export { UntranslatedContext };
8
9
 
9
- export function LangContentSwitch({ content }: { content: LangContentMap }) {
10
- const { defaultLanguage } = useLocaleRegistry();
10
+ export type LocaleNodeResult = { node: ReactNode; missing: boolean } | null;
11
+ export type LoadLocaleNode = (lang: Lang) => Promise<LocaleNodeResult>;
12
+
13
+ export function LangContentSwitch({
14
+ content,
15
+ loadLang,
16
+ }: {
17
+ content: LangContent;
18
+ loadLang?: LoadLocaleNode;
19
+ }) {
11
20
  const lang = useResolvedLang();
12
- const own = content[lang];
13
- const entry =
14
- own ?? content[defaultLanguage] ?? Object.values(content).find(Boolean);
15
- const missing = !own && lang !== defaultLanguage && Boolean(entry);
21
+ const [loaded, setLoaded] = useState<
22
+ Partial<Record<Lang, LocaleNodeResult>>
23
+ >({});
24
+ const result =
25
+ lang === content.lang
26
+ ? { node: content.node, missing: false }
27
+ : loaded[lang];
28
+
29
+ useEffect(() => {
30
+ if (!loadLang || lang === content.lang || lang in loaded) return;
31
+ let live = true;
32
+ void loadLang(lang).then((next) => {
33
+ if (live) {
34
+ setLoaded((map) => (lang in map ? map : { ...map, [lang]: next }));
35
+ }
36
+ });
37
+ return () => {
38
+ live = false;
39
+ };
40
+ }, [lang, content.lang, loadLang, loaded]);
16
41
 
42
+ const missing =
43
+ result === undefined
44
+ ? !loadLang && lang !== content.lang
45
+ : (result?.missing ?? lang !== content.lang);
17
46
  return (
18
47
  <UntranslatedContext.Provider value={missing}>
19
- {entry?.node ?? null}
48
+ {(result ?? content).node}
20
49
  </UntranslatedContext.Provider>
21
50
  );
22
51
  }
52
+
@@ -22,3 +22,4 @@ export function LangFlag({
22
22
  />
23
23
  );
24
24
  }
25
+
@@ -20,3 +20,4 @@ export function LangLink({
20
20
 
21
21
  return <NextLink href={nextHref ?? href ?? '#'} {...props} />;
22
22
  }
23
+
@@ -14,7 +14,7 @@ function RedirectLink({ href }: { href: string }) {
14
14
  href={href}
15
15
  className="text-sm text-muted-foreground underline underline-offset-4 transition-colors hover:text-accent-foreground"
16
16
  >
17
- {t('ui.redirecting')}
17
+ {t('Redirecting…', { note: 'ui.redirecting' })}
18
18
  </a>
19
19
  </div>
20
20
  );
@@ -45,3 +45,4 @@ export function LangRedirect({ to }: { to: string }) {
45
45
  </Suspense>
46
46
  );
47
47
  }
48
+
@@ -51,3 +51,4 @@ export function LangSwitcher({
51
51
  />
52
52
  );
53
53
  }
54
+
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { Callout } from '@terrafirmagreg-team/ui/components/mdx/callout';
3
- import { useUntranslated } from '@terrafirmagreg-team/ui/hooks/lang';
3
+ import { useUntranslated } from '@terrafirmagreg-team/ui/hooks/use-lang';
4
4
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
5
  import type { TranslationState } from '@terrafirmagreg-team/translate/server';
6
6
 
@@ -18,11 +18,12 @@ export function UntranslatedCallout({
18
18
  <Callout
19
19
  type="warn"
20
20
  title={
21
- full ? t('untranslated.title') : t('untranslated.partial_title')
21
+ full ? t('This page is not translated', { note: 'untranslated.title' }) : t('This page is not fully translated', { note: 'untranslated.partial_title' })
22
22
  }
23
23
  className="shrink-0"
24
24
  >
25
- <p>{full ? t('untranslated.body') : t('untranslated.partial_body')}</p>
25
+ <p>{full ? t('Showing the English version. A translation is not available yet.', { note: 'untranslated.body' }) : t('Some content is still shown in English.', { note: 'untranslated.partial_body' })}</p>
26
26
  </Callout>
27
27
  );
28
28
  }
29
+
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
3
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+
6
+ function LanguagesIcon({ className }: { className?: string }) {
7
+ return (
8
+ <svg
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ className={className}
16
+ aria-hidden
17
+ >
18
+ <path d="m5 8 6 6" />
19
+ <path d="m4 14 6-6 2-3" />
20
+ <path d="M2 5h12" />
21
+ <path d="M7 2h1" />
22
+ <path d="m22 22-5-10-5 10" />
23
+ <path d="M14 18h6" />
24
+ </svg>
25
+ );
26
+ }
27
+
28
+ export function UntranslatedChip({ className }: { className?: string }) {
29
+ const t = useTranslations();
30
+ return (
31
+ <Hint
32
+ side="top"
33
+ label={
34
+ <span className="block max-w-56">
35
+ <span className="mb-0.5 block font-semibold">
36
+ {t('This page is not translated', { note: 'untranslated.title' })}
37
+ </span>
38
+ {t('Showing the English version. A translation is not available yet.', { note: 'untranslated.body' })}
39
+ </span>
40
+ }
41
+ >
42
+ <span
43
+ className={cn(
44
+ 'inline-flex cursor-help items-center gap-1 rounded-md bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400',
45
+ className,
46
+ )}
47
+ >
48
+ <LanguagesIcon className="size-3" />
49
+ {t('Untranslated', { note: 'untranslated.chip' })}
50
+ </span>
51
+ </Hint>
52
+ );
53
+ }
@@ -70,7 +70,7 @@ function ExternalAnchor({
70
70
  const title = meta?.title || host;
71
71
  const detail = [host && host !== title ? host : '', meta?.description]
72
72
  .filter(Boolean)
73
- .join(' — ');
73
+ .join(' · ');
74
74
  return (
75
75
  <HoverCard open={open} onOpenChange={setOpen}>
76
76
  <HoverCardTrigger
@@ -254,3 +254,4 @@ export function LinkPreview({
254
254
  </HoverCard>
255
255
  );
256
256
  }
257
+
@@ -89,3 +89,4 @@ export function ItemRow({
89
89
  </Item>
90
90
  );
91
91
  }
92
+
@@ -241,3 +241,4 @@ export const GlbSceneApi = forwardRef<
241
241
  );
242
242
  return null;
243
243
  });
244
+
@@ -119,7 +119,7 @@ export function GlbScene({
119
119
  const homeRef = useRef<GlbHomeView | null>(null);
120
120
  const initializedRef = useRef(false);
121
121
  const rendererRef = useRef<WebGLRenderer | null>(null);
122
- const label = title || t('ui.viewer.label');
122
+ const label = title || t('3D model', { note: 'ui.viewer.label' });
123
123
  const url = urls[Math.min(selected, urls.length - 1)];
124
124
  const [statusEntry, setStatusEntry] = useState<{
125
125
  url: string;
@@ -250,15 +250,15 @@ export function GlbScene({
250
250
  onZoomIn: () => apiRef.current?.zoomBy(1 / GLB_ZOOM_STEP),
251
251
  onZoomOut: () => apiRef.current?.zoomBy(GLB_ZOOM_STEP),
252
252
  onFit: () => apiRef.current?.reset(),
253
- fitLabel: t('ui.viewer.reset_view'),
253
+ fitLabel: t('Reset view', { note: 'ui.viewer.reset_view' }),
254
254
  }}
255
255
  expand={{ target: surfaceRef }}
256
256
  settings={{
257
- label: t('ui.viewer.settings'),
257
+ label: t('Viewer settings', { note: 'ui.viewer.settings' }),
258
258
  sliders: [
259
259
  {
260
260
  icon: <MoveHorizontal />,
261
- label: t('ui.viewer.width'),
261
+ label: t('Width', { note: 'ui.viewer.width' }),
262
262
  min: SCENE_WIDTH_MIN,
263
263
  max: SCENE_WIDTH_MAX,
264
264
  step: 5,
@@ -268,7 +268,7 @@ export function GlbScene({
268
268
  },
269
269
  {
270
270
  icon: <MoveVertical />,
271
- label: t('ui.viewer.height'),
271
+ label: t('Height', { note: 'ui.viewer.height' }),
272
272
  min: SCENE_HEIGHT_MIN,
273
273
  max: SCENE_HEIGHT_MAX,
274
274
  step: 16,
@@ -278,7 +278,7 @@ export function GlbScene({
278
278
  },
279
279
  {
280
280
  icon: <Gauge />,
281
- label: t('ui.viewer.rotate_speed'),
281
+ label: t('Rotate speed', { note: 'ui.viewer.rotate_speed' }),
282
282
  min: ROTATE_MIN,
283
283
  max: ROTATE_MAX,
284
284
  step: 0.1,
@@ -290,7 +290,7 @@ export function GlbScene({
290
290
  ? [
291
291
  {
292
292
  icon: <Timer />,
293
- label: t('ui.viewer.autoplay_speed'),
293
+ label: t('Autoplay speed', { note: 'ui.viewer.autoplay_speed' }),
294
294
  min: AUTOPLAY_SPEED_MIN,
295
295
  max: AUTOPLAY_SPEED_MAX,
296
296
  step: 0.5,
@@ -305,7 +305,7 @@ export function GlbScene({
305
305
  toggles: [
306
306
  {
307
307
  icon: <RotateCw />,
308
- label: t('ui.viewer.spin'),
308
+ label: t('Spin', { note: 'ui.viewer.spin' }),
309
309
  checked: autoRotate,
310
310
  onChange: setAutoRotate,
311
311
  },
@@ -313,7 +313,7 @@ export function GlbScene({
313
313
  ? [
314
314
  {
315
315
  icon: <Play />,
316
- label: t('ui.viewer.autoplay'),
316
+ label: t('Autoplay', { note: 'ui.viewer.autoplay' }),
317
317
  checked: autoplay,
318
318
  onChange: setAutoplay,
319
319
  },
@@ -321,7 +321,7 @@ export function GlbScene({
321
321
  : []),
322
322
  {
323
323
  icon: <Grid3x3 />,
324
- label: t('ui.viewer.grid'),
324
+ label: t('Grid', { note: 'ui.viewer.grid' }),
325
325
  checked: dotGrid,
326
326
  onChange: setDotGrid,
327
327
  },
@@ -332,11 +332,11 @@ export function GlbScene({
332
332
  )}
333
333
  {status === 'loading' ? (
334
334
  <p className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
335
- {t('ui.viewer.loading')}
335
+ {t('Loading…', { note: 'ui.viewer.loading' })}
336
336
  </p>
337
337
  ) : status === 'error' ? (
338
338
  <p className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
339
- {t('ui.image_unavailable')}
339
+ {t('Image unavailable', { note: 'ui.image_unavailable' })}
340
340
  </p>
341
341
  ) : null}
342
342
  </div>
@@ -370,3 +370,4 @@ export function GlbSession({
370
370
  />
371
371
  );
372
372
  }
373
+
@@ -49,3 +49,4 @@ export function GlbPlaceholder({
49
49
  </div>
50
50
  );
51
51
  }
52
+