@terrafirmagreg-team/ui 0.1.0 → 1.2.1
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.
- package/package.json +14 -14
- package/src/components/blocks/button/icon-button.tsx +1 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +126 -18
- package/src/components/blocks/canvas/flow-canvas.tsx +4 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +1 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +1 -0
- package/src/components/blocks/fit-box.tsx +26 -0
- package/src/components/blocks/git/github-info.tsx +1 -0
- package/src/components/blocks/lang/content.tsx +40 -10
- package/src/components/blocks/lang/flag.tsx +1 -0
- package/src/components/blocks/lang/link.tsx +1 -0
- package/src/components/blocks/lang/redirect.tsx +2 -1
- package/src/components/blocks/lang/switcher.tsx +1 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +4 -3
- package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
- package/src/components/blocks/link/link-preview.tsx +2 -1
- package/src/components/blocks/list/row.tsx +1 -0
- package/src/components/blocks/media/glb/model.tsx +1 -0
- package/src/components/blocks/media/glb/scene.tsx +13 -12
- package/src/components/blocks/media/glb/viewer.tsx +1 -0
- package/src/components/blocks/media/image.tsx +3 -2
- package/src/components/blocks/media/slideshow.tsx +4 -3
- package/src/components/blocks/nav/breadcrumb.tsx +1 -0
- package/src/components/blocks/nav/copy-menu.tsx +37 -0
- package/src/components/blocks/nav/link.tsx +1 -0
- package/src/components/blocks/nav/site-menu.tsx +1 -0
- package/src/components/blocks/nav/site-title.tsx +1 -0
- package/src/components/blocks/nav/switcher.tsx +1 -0
- package/src/components/blocks/page/empty-state.tsx +1 -0
- package/src/components/blocks/page/heading.tsx +3 -2
- package/src/components/blocks/page/pager.tsx +110 -2
- package/src/components/blocks/readability/fields.tsx +250 -0
- package/src/components/blocks/readability/menu.tsx +74 -258
- package/src/components/blocks/readability/track.tsx +2 -1
- package/src/components/blocks/search/command.tsx +1 -0
- package/src/components/blocks/search/dialog.tsx +21 -9
- package/src/components/blocks/search/provider.tsx +2 -1
- package/src/components/blocks/sidebar.tsx +5 -5
- package/src/components/blocks/sprite.tsx +231 -0
- package/src/components/blocks/text/format-text.tsx +110 -0
- package/src/components/blocks/theme/toggle.tsx +4 -0
- package/src/components/blocks/tooltip/hint.tsx +1 -0
- package/src/components/blocks/tooltip/lazy.tsx +41 -0
- package/src/components/blocks/viewer/controls.tsx +11 -6
- package/src/components/blocks/viewer/float-bar.tsx +1 -0
- package/src/components/layouts/layout.tsx +2 -1
- package/src/components/layouts/left-sidebar/content.tsx +1 -0
- package/src/components/layouts/left-sidebar/context.tsx +1 -0
- package/src/components/layouts/left-sidebar/expand.tsx +2 -1
- package/src/components/layouts/left-sidebar/footer.tsx +10 -4
- package/src/components/layouts/left-sidebar/index.ts +1 -0
- package/src/components/layouts/left-sidebar/items.tsx +2 -1
- package/src/components/layouts/left-sidebar/search.tsx +4 -3
- package/src/components/layouts/left-sidebar/sidebar.tsx +3 -2
- package/src/components/layouts/left-sidebar/tree.tsx +1 -0
- package/src/components/layouts/right-sidebar.tsx +1 -0
- package/src/components/layouts/tree-context.tsx +1 -0
- package/src/components/mdx/accordion.tsx +3 -2
- package/src/components/mdx/banner.tsx +15 -12
- package/src/components/mdx/callout.tsx +1 -0
- package/src/components/mdx/card.tsx +12 -5
- package/src/components/mdx/codeblock.tsx +5 -4
- package/src/components/mdx/collapsible.tsx +1 -0
- package/src/components/mdx/files.tsx +1 -0
- package/src/components/mdx/glow-header.tsx +17 -3
- package/src/components/mdx/gradient-text.tsx +1 -0
- package/src/components/mdx/icon.tsx +35 -0
- package/src/components/mdx/image-zoom.tsx +5 -4
- package/src/components/mdx/math.tsx +30 -0
- package/src/components/mdx/mermaid.tsx +8 -1
- package/src/components/mdx/multiblock.tsx +1 -0
- package/src/components/mdx/preview.tsx +2 -1
- package/src/components/mdx/steps.tsx +1 -0
- package/src/components/mdx/table.tsx +1 -0
- package/src/components/mdx/tabs.tsx +2 -1
- package/src/components/mdx/type-table.tsx +7 -6
- package/src/{provider.tsx → components/provider.tsx} +1 -3
- package/src/hooks/canvas/use-flow-hover.ts +1 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +1 -0
- package/src/hooks/page/use-fit-pages.ts +1 -0
- package/src/hooks/page/use-location-search.ts +8 -2
- package/src/hooks/page/use-page-preview.ts +2 -1
- package/src/hooks/page/use-paged-list.ts +1 -0
- package/src/hooks/readability/{provider.ts → use-readability.ts} +1 -0
- package/src/hooks/search/{dialog.ts → use-dialog.ts} +1 -0
- package/src/hooks/search/use-highlight-query.ts +8 -1
- package/src/hooks/search/{provider.ts → use-provider.ts} +1 -0
- package/src/hooks/use-async-value.ts +6 -3
- package/src/hooks/use-close-on-scroll.ts +1 -0
- package/src/hooks/use-copy-button.ts +1 -0
- package/src/hooks/use-cycle-index.ts +1 -0
- package/src/hooks/use-document-title.ts +1 -0
- package/src/hooks/use-element-size.ts +1 -0
- package/src/hooks/use-external-meta.ts +2 -1
- package/src/hooks/use-hot-key.ts +1 -0
- package/src/hooks/use-hover-follow.ts +1 -0
- package/src/hooks/use-in-view.ts +1 -0
- package/src/hooks/{lang.ts → use-lang.ts} +1 -0
- package/src/hooks/use-last-value.ts +1 -0
- package/src/hooks/{layout.ts → use-layout.ts} +1 -0
- package/src/hooks/use-marquee.ts +1 -0
- package/src/hooks/use-media-query.ts +1 -0
- package/src/hooks/use-mobile.ts +1 -0
- package/src/hooks/use-modifier-keys.ts +1 -0
- package/src/hooks/use-on-change.ts +1 -0
- package/src/hooks/use-pane-keys.ts +1 -0
- package/src/hooks/{sidebar.ts → use-sidebar.ts} +1 -0
- package/src/hooks/use-swipe.ts +1 -0
- package/src/hooks/use-tree.ts +1 -3
- package/src/hooks/viewer/use-in-float-bar.ts +1 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +1 -0
- package/src/lib/canvas/edge.ts +78 -49
- package/src/lib/canvas/geometry.ts +1 -0
- package/src/lib/canvas/state.ts +1 -0
- package/src/lib/canvas/viewport.ts +1 -0
- package/src/lib/canvas/xyflow.ts +1 -0
- package/src/lib/config.ts +25 -0
- package/src/lib/mdx/callout.ts +1 -0
- package/src/lib/mdx/gradient.ts +1 -0
- package/src/lib/mdx/mermaid.ts +1 -0
- package/src/lib/media/crop-opaque.ts +1 -0
- package/src/lib/media/nearby-indexes.ts +1 -0
- package/src/lib/media/trim-transparent.ts +1 -0
- package/src/lib/readability/config.ts +1 -0
- package/src/lib/readability/spotlight.ts +1 -0
- package/src/lib/readability/store.ts +1 -0
- package/src/lib/tree.ts +1 -3
- package/src/lib/utils/cn.ts +1 -0
- package/src/lib/utils/format.ts +3 -3
- package/src/lib/utils/highlight-style.ts +10 -0
- package/src/lib/utils/history.ts +1 -0
- package/src/lib/utils/merge-refs.ts +1 -0
- package/src/lib/utils/nav-active.ts +1 -0
- package/src/lib/utils/stored-value.ts +1 -0
- package/src/lib/utils/urls.ts +2 -1
- package/src/locales/en_us.json +108 -117
- package/src/locales/pl_pl.json +33 -35
- package/src/locales/ru_ru.json +108 -117
- package/src/locales/uk_ua.json +33 -35
- package/src/locales/zh_cn.json +15 -17
- package/src/locales/zh_tw.json +15 -17
- package/src/locales.ts +1 -0
- package/src/types/canvas.ts +6 -1
- package/src/types/glb.ts +1 -0
- package/src/types/mdx-directives.ts +106 -0
- package/src/types/nav.ts +1 -0
- package/src/types/preview.ts +7 -1
- package/src/types/readability.ts +1 -0
- package/src/types/search.ts +1 -0
- package/src/types/tree.ts +1 -0
- package/src/types/viewer.ts +1 -0
- package/styles/content/base.css +198 -194
- package/styles/content/shiki.css +107 -108
- package/styles/content/typography.mjs +3637 -3941
- package/styles/content.css +5 -7
- package/styles.css +430 -413
- 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": "
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
|
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
|
|
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": "
|
|
48
|
+
"@terrafirmagreg-team/translate": "1.2.1",
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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.1"
|
|
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.
|
|
81
|
+
"@types/three": "^0.187.0"
|
|
82
82
|
},
|
|
83
83
|
"repository": {
|
|
84
84
|
"type": "git",
|
|
@@ -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
|
|
92
|
-
() =>
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
+
|
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
3
3
|
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
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
|
|
10
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
{
|
|
48
|
+
{(result ?? content).node}
|
|
20
49
|
</UntranslatedContext.Provider>
|
|
21
50
|
);
|
|
22
51
|
}
|
|
52
|
+
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|