@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,27 @@
1
+ 'use client';
2
+
3
+ import { useRef, type TouchEvent } from 'react';
4
+
5
+ const MIN_DX = 48;
6
+
7
+ export function useSwipe(onSwipe: (dir: -1 | 1) => void) {
8
+ const start = useRef<{ x: number; y: number } | null>(null);
9
+ return {
10
+ onTouchStart: (event: TouchEvent) => {
11
+ const touch = event.touches[0];
12
+ if (!touch) return;
13
+ start.current = { x: touch.clientX, y: touch.clientY };
14
+ },
15
+ onTouchEnd: (event: TouchEvent) => {
16
+ const from = start.current;
17
+ start.current = null;
18
+ const touch = event.changedTouches[0];
19
+ if (!from || !touch) return;
20
+ const dx = touch.clientX - from.x;
21
+ const dy = touch.clientY - from.y;
22
+ if (Math.abs(dx) < MIN_DX || Math.abs(dx) < Math.abs(dy) * 1.5) return;
23
+ onSwipe(dx < 0 ? 1 : -1);
24
+ },
25
+ };
26
+ }
27
+
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+ import { useMemo, createContext, use } from 'react';
3
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
4
+ import type { TabOption, TreeContextValue } from '@terrafirmagreg-team/ui/types/tree';
5
+ import { findProjection } from '@terrafirmagreg-team/ui/lib/tree';
6
+
7
+ export const TreeContext = createContext<TreeContextValue | null>(null);
8
+ export const PathContext = createContext<PageTree.Node[]>([]);
9
+
10
+ export function useTreePath(): PageTree.Node[] {
11
+ return use(PathContext);
12
+ }
13
+
14
+ export function useTreeContext(): TreeContextValue {
15
+ const ctx = use(TreeContext);
16
+ if (!ctx)
17
+ throw new Error('You must wrap this component under <DocsLayout />');
18
+ return ctx;
19
+ }
20
+
21
+ export function useTabsGroups(tabs: TabOption[]): {
22
+ active?: PageTree.Folder;
23
+ options: TabOption[];
24
+ }[] {
25
+ const { full: tree } = useTreeContext();
26
+ const path = use(PathContext);
27
+
28
+ return useMemo(() => {
29
+ const out: { active?: PageTree.Folder; options: TabOption[] }[] = [];
30
+ const last = path[path.length - 1];
31
+ const page = last?.type === 'page' ? last : undefined;
32
+ let scope = tree.fallback
33
+ ? [...tree.children, ...tree.fallback.children]
34
+ : tree.children;
35
+
36
+ for (const node of path) {
37
+ if (node.type !== 'folder' || !node.root) continue;
38
+ const group = { active: node, options: [] as TabOption[] };
39
+ collectTabs(scope, node, page, tabs, group.options);
40
+ if (group.options.length > 0) out.push(group);
41
+ scope = node.children;
42
+ }
43
+
44
+ const custom = tabs.filter((tab) => !tab.$folder);
45
+ if (custom.length > 0) {
46
+ const group = out.findLast((group) => group.active?.root === true);
47
+ if (group) group.options.push(...custom);
48
+ else out.push({ options: custom });
49
+ }
50
+ return out;
51
+ }, [tabs, tree, path]);
52
+ }
53
+
54
+ function collectTabs(
55
+ scope: PageTree.Node[],
56
+ active: PageTree.Folder,
57
+ page: PageTree.Item | undefined,
58
+ tabs: TabOption[],
59
+ out: TabOption[],
60
+ ): void {
61
+ for (const node of scope) {
62
+ if (node.type !== 'folder') continue;
63
+ if (node.root === active.root) {
64
+ const tab = tabs.find(
65
+ (tab) =>
66
+ tab.$folder &&
67
+ (tab.$folder === node || tab.$folder.$id === node.$id),
68
+ );
69
+ if (!tab) continue;
70
+ const projection =
71
+ page && findProjection(active, node, page);
72
+ out.push(projection ? { ...tab, url: projection.url } : tab);
73
+ } else if (!node.root) {
74
+ collectTabs(node.children, active, page, tabs, out);
75
+ }
76
+ }
77
+ }
78
+
79
+ const footerCache = new WeakMap<PageTree.Root | PageTree.Folder, PageTree.Item[]>();
80
+
81
+ export function useFooterItems(): PageTree.Item[] {
82
+ const { root } = useTreeContext();
83
+ const cached = footerCache.get(root);
84
+ if (cached) return cached;
85
+
86
+ const list: PageTree.Item[] = [];
87
+ function onNode(node: PageTree.Node): void {
88
+ if (node.type === 'folder') {
89
+ if (node.index) onNode(node.index);
90
+ for (const child of node.children) onNode(child);
91
+ } else if (node.type === 'page' && !node.external) {
92
+ list.push(node);
93
+ }
94
+ }
95
+ for (const child of root.children) onNode(child);
96
+
97
+ footerCache.set(root, list);
98
+ return list;
99
+ }
100
+
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ export const FloatBarContext = createContext(false);
5
+
6
+ export const useInFloatBar = () => useContext(FloatBarContext);
7
+
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import {
4
+ boolCodec,
5
+ createStoredValue,
6
+ numberCodec,
7
+ } from '@terrafirmagreg-team/ui/lib/utils/stored-value';
8
+
9
+ export const AUTOPLAY_SPEED_MIN = 1;
10
+ export const AUTOPLAY_SPEED_MAX = 10;
11
+ export const ROTATE_MIN = 0;
12
+ export const ROTATE_MAX = 4;
13
+ export const SCENE_WIDTH_MIN = 50;
14
+ export const SCENE_WIDTH_MAX = 100;
15
+ export const SCENE_HEIGHT_MIN = 160;
16
+ export const SCENE_HEIGHT_MAX = 640;
17
+
18
+ export const useAutoplay = createStoredValue(
19
+ 'wiki.viewer.autoplay',
20
+ boolCodec(true),
21
+ );
22
+
23
+ export const useAutoplaySpeed = createStoredValue(
24
+ 'wiki.viewer.autoplaySpeed',
25
+ numberCodec(4, AUTOPLAY_SPEED_MIN, AUTOPLAY_SPEED_MAX),
26
+ );
27
+
28
+ const useAutoRotate = createStoredValue(
29
+ 'wiki.viewer.glb.autoRotate',
30
+ boolCodec(true),
31
+ );
32
+
33
+ const useRotateSpeed = createStoredValue(
34
+ 'wiki.viewer.glb.rotateSpeed',
35
+ numberCodec(1, ROTATE_MIN, ROTATE_MAX),
36
+ );
37
+
38
+ const useDotGrid = createStoredValue(
39
+ 'wiki.viewer.glb.dotGrid',
40
+ boolCodec(true),
41
+ );
42
+
43
+ const useSceneWidth = createStoredValue(
44
+ 'wiki.viewer.glb.width',
45
+ numberCodec(SCENE_WIDTH_MAX, SCENE_WIDTH_MIN, SCENE_WIDTH_MAX),
46
+ );
47
+
48
+ const useSceneHeight = createStoredValue(
49
+ 'wiki.viewer.glb.height',
50
+ numberCodec(288, SCENE_HEIGHT_MIN, SCENE_HEIGHT_MAX),
51
+ );
52
+
53
+ export function useViewerPrefs() {
54
+ const [autoplay, setAutoplay] = useAutoplay();
55
+ const [autoplaySpeed, setAutoplaySpeed] = useAutoplaySpeed();
56
+ const [autoRotate, setAutoRotate] = useAutoRotate();
57
+ const [rotateSpeed, setRotateSpeed] = useRotateSpeed();
58
+ const [dotGrid, setDotGrid] = useDotGrid();
59
+ const [sceneWidth, setSceneWidth] = useSceneWidth();
60
+ const [sceneHeight, setSceneHeight] = useSceneHeight();
61
+ return {
62
+ autoplay,
63
+ setAutoplay,
64
+ autoplaySpeed,
65
+ setAutoplaySpeed,
66
+ autoRotate,
67
+ setAutoRotate,
68
+ rotateSpeed,
69
+ setRotateSpeed,
70
+ dotGrid,
71
+ setDotGrid,
72
+ sceneWidth,
73
+ setSceneWidth,
74
+ sceneHeight,
75
+ setSceneHeight,
76
+ };
77
+ }
78
+
@@ -0,0 +1,202 @@
1
+ import type {
2
+ ChevronEdgeHighlight,
3
+ FlowPoint,
4
+ } from '@terrafirmagreg-team/ui/types/canvas';
5
+
6
+ export const CHEVRON_SPACING = 10;
7
+ const CHEVRON_SIZE = 3.5;
8
+
9
+ export const CHEVRON_GLYPH = `M${-CHEVRON_SIZE * 0.45} ${-CHEVRON_SIZE * 0.9}L${CHEVRON_SIZE} 0L${-CHEVRON_SIZE * 0.45} ${CHEVRON_SIZE * 0.9}`;
10
+
11
+ export function chevronTicksAlongPath(points: readonly FlowPoint[]): {
12
+ offsets: number[];
13
+ total: number;
14
+ } {
15
+ let total = 0;
16
+ for (let i = 0; i + 1 < points.length; i += 1) {
17
+ total += Math.hypot(
18
+ points[i + 1]!.x - points[i]!.x,
19
+ points[i + 1]!.y - points[i]!.y,
20
+ );
21
+ }
22
+ const offsets: number[] = [];
23
+ for (
24
+ let d = CHEVRON_SPACING;
25
+ d <= total - CHEVRON_SPACING / 2;
26
+ d += CHEVRON_SPACING
27
+ ) {
28
+ offsets.push(d);
29
+ }
30
+ return { offsets, total };
31
+ }
32
+
33
+ export function svgPathToPoints(d: string, samples = 12): FlowPoint[] {
34
+ const out: FlowPoint[] = [];
35
+ const re = /([MLCQ])([^MLCQ]*)/g;
36
+ let match: RegExpExecArray | null;
37
+ let cur: FlowPoint = { x: 0, y: 0 };
38
+ while ((match = re.exec(d))) {
39
+ const nums = match[2]!.trim().split(/[\s,]+/).map(Number);
40
+ if (match[1] === 'M' || match[1] === 'L') {
41
+ for (let i = 0; i + 1 < nums.length; i += 2) {
42
+ cur = { x: nums[i]!, y: nums[i + 1]! };
43
+ out.push(cur);
44
+ }
45
+ } else if (match[1] === 'C') {
46
+ for (let i = 0; i + 5 < nums.length; i += 6) {
47
+ const p0 = cur;
48
+ const c1 = { x: nums[i]!, y: nums[i + 1]! };
49
+ const c2 = { x: nums[i + 2]!, y: nums[i + 3]! };
50
+ const p1 = { x: nums[i + 4]!, y: nums[i + 5]! };
51
+ for (let s = 1; s <= samples; s += 1) {
52
+ const t = s / samples;
53
+ const mt = 1 - t;
54
+ out.push({
55
+ x:
56
+ mt * mt * mt * p0.x +
57
+ 3 * mt * mt * t * c1.x +
58
+ 3 * mt * t * t * c2.x +
59
+ t * t * t * p1.x,
60
+ y:
61
+ mt * mt * mt * p0.y +
62
+ 3 * mt * mt * t * c1.y +
63
+ 3 * mt * t * t * c2.y +
64
+ t * t * t * p1.y,
65
+ });
66
+ }
67
+ cur = p1;
68
+ }
69
+ } else if (match[1] === 'Q') {
70
+ for (let i = 0; i + 3 < nums.length; i += 4) {
71
+ const p0 = cur;
72
+ const c = { x: nums[i]!, y: nums[i + 1]! };
73
+ const p1 = { x: nums[i + 2]!, y: nums[i + 3]! };
74
+ for (let s = 1; s <= samples; s += 1) {
75
+ const t = s / samples;
76
+ const mt = 1 - t;
77
+ out.push({
78
+ x: mt * mt * p0.x + 2 * mt * t * c.x + t * t * p1.x,
79
+ y: mt * mt * p0.y + 2 * mt * t * c.y + t * t * p1.y,
80
+ });
81
+ }
82
+ cur = p1;
83
+ }
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ export function chamferedPolylinePath(
90
+ points: readonly FlowPoint[],
91
+ cut = 8,
92
+ ): string {
93
+ if (!points.length) return '';
94
+ if (points.length < 3) {
95
+ return `M${points[0]!.x} ${points[0]!.y}${points
96
+ .slice(1)
97
+ .map((p) => `L${p.x} ${p.y}`)
98
+ .join('')}`;
99
+ }
100
+ let d = `M${points[0]!.x} ${points[0]!.y}`;
101
+ let cursor = points[0]!;
102
+ for (let i = 1; i + 1 < points.length; i += 1) {
103
+ const corner = points[i]!;
104
+ const next = points[i + 1]!;
105
+ const inLen = Math.hypot(corner.x - cursor.x, corner.y - cursor.y);
106
+ const outLen = Math.hypot(next.x - corner.x, next.y - corner.y);
107
+ const take = Math.min(cut, inLen / 2, outLen / 2);
108
+ if (take < 0.5) {
109
+ d += `L${corner.x} ${corner.y}`;
110
+ cursor = corner;
111
+ continue;
112
+ }
113
+ const ax = corner.x - ((corner.x - cursor.x) / inLen) * take;
114
+ const ay = corner.y - ((corner.y - cursor.y) / inLen) * take;
115
+ const bx = corner.x + ((next.x - corner.x) / outLen) * take;
116
+ const by = corner.y + ((next.y - corner.y) / outLen) * take;
117
+ d += `L${ax} ${ay}L${bx} ${by}`;
118
+ cursor = { x: bx, y: by };
119
+ }
120
+ const last = points[points.length - 1]!;
121
+ return `${d}L${last.x} ${last.y}`;
122
+ }
123
+
124
+ export function roundedPolylinePath(
125
+ points: readonly FlowPoint[],
126
+ radius = 10,
127
+ ): string {
128
+ if (!points.length) return '';
129
+ if (points.length < 3) {
130
+ return `M${points[0]!.x} ${points[0]!.y}${points
131
+ .slice(1)
132
+ .map((p) => `L${p.x} ${p.y}`)
133
+ .join('')}`;
134
+ }
135
+ let d = `M${points[0]!.x} ${points[0]!.y}`;
136
+ let cursor = points[0]!;
137
+ for (let i = 1; i + 1 < points.length; i += 1) {
138
+ const corner = points[i]!;
139
+ const next = points[i + 1]!;
140
+ const inLen = Math.hypot(corner.x - cursor.x, corner.y - cursor.y);
141
+ const outLen = Math.hypot(next.x - corner.x, next.y - corner.y);
142
+ const take = Math.min(radius, inLen / 2, outLen / 2);
143
+ if (take < 0.5) {
144
+ d += `L${corner.x} ${corner.y}`;
145
+ cursor = corner;
146
+ continue;
147
+ }
148
+ const ax = corner.x - ((corner.x - cursor.x) / inLen) * take;
149
+ const ay = corner.y - ((corner.y - cursor.y) / inLen) * take;
150
+ const bx = corner.x + ((next.x - corner.x) / outLen) * take;
151
+ const by = corner.y + ((next.y - corner.y) / outLen) * take;
152
+ d += `L${ax} ${ay}Q${corner.x} ${corner.y} ${bx} ${by}`;
153
+ cursor = { x: bx, y: by };
154
+ }
155
+ const last = points[points.length - 1]!;
156
+ return `${d}L${last.x} ${last.y}`;
157
+ }
158
+
159
+ export function nearestPointOnPolyline(
160
+ points: readonly FlowPoint[],
161
+ p: FlowPoint,
162
+ ): FlowPoint {
163
+ let best = points[0]!;
164
+ let bestD = Infinity;
165
+ for (let i = 1; i < points.length; i += 1) {
166
+ const a = points[i - 1]!;
167
+ const b = points[i]!;
168
+ const dx = b.x - a.x;
169
+ const dy = b.y - a.y;
170
+ const len2 = dx * dx + dy * dy;
171
+ const t = len2
172
+ ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2))
173
+ : 0;
174
+ const q = { x: a.x + dx * t, y: a.y + dy * t };
175
+ const d = (q.x - p.x) ** 2 + (q.y - p.y) ** 2;
176
+ if (d < bestD) {
177
+ bestD = d;
178
+ best = q;
179
+ }
180
+ }
181
+ return best;
182
+ }
183
+
184
+ export function chevronHighlightClass(
185
+ highlight: ChevronEdgeHighlight,
186
+ ): string {
187
+ switch (highlight) {
188
+ case 'incoming':
189
+ return 'chevron-edge--incoming';
190
+ case 'outgoing':
191
+ return 'chevron-edge--outgoing';
192
+ case 'dim':
193
+ return 'chevron-edge--dim';
194
+ case undefined:
195
+ return '';
196
+ default: {
197
+ const _never: never = highlight;
198
+ return _never;
199
+ }
200
+ }
201
+ }
202
+
@@ -0,0 +1,54 @@
1
+ import { FLOW_MAX_ZOOM, FLOW_MIN_ZOOM } from '@terrafirmagreg-team/ui/lib/canvas/viewport';
2
+ import type {
3
+ FlowContentBox,
4
+ FlowRect,
5
+ FlowSizedNode,
6
+ } from '@terrafirmagreg-team/ui/types/canvas';
7
+
8
+ const FIT_VIEW_PAD = 0.12;
9
+
10
+ export function flowContentBox(
11
+ nodes: FlowSizedNode[],
12
+ rects: FlowRect[] = [],
13
+ ): FlowContentBox | null {
14
+ let minX = Number.POSITIVE_INFINITY;
15
+ let minY = Number.POSITIVE_INFINITY;
16
+ let maxX = Number.NEGATIVE_INFINITY;
17
+ let maxY = Number.NEGATIVE_INFINITY;
18
+ for (const node of nodes) {
19
+ const halfW = (node.width ?? 0) / 2;
20
+ const halfH = (node.height ?? 0) / 2;
21
+ minX = Math.min(minX, node.position.x - halfW);
22
+ minY = Math.min(minY, node.position.y - halfH);
23
+ maxX = Math.max(maxX, node.position.x + halfW);
24
+ maxY = Math.max(maxY, node.position.y + halfH);
25
+ }
26
+ for (const rect of rects) {
27
+ minX = Math.min(minX, rect.x - rect.width / 2);
28
+ minY = Math.min(minY, rect.y - rect.height / 2);
29
+ maxX = Math.max(maxX, rect.x + rect.width / 2);
30
+ maxY = Math.max(maxY, rect.y + rect.height / 2);
31
+ }
32
+ if (!Number.isFinite(minX)) return null;
33
+ return {
34
+ midX: (minX + maxX) / 2,
35
+ midY: (minY + maxY) / 2,
36
+ width: Math.max(1, maxX - minX),
37
+ height: Math.max(1, maxY - minY),
38
+ };
39
+ }
40
+
41
+ export function zoomToFitBox(
42
+ box: { width: number; height: number },
43
+ viewW: number,
44
+ viewH: number,
45
+ minZoom = FLOW_MIN_ZOOM,
46
+ maxZoom = FLOW_MAX_ZOOM,
47
+ ): number {
48
+ const zoom = Math.min(
49
+ (viewW * (1 - 2 * FIT_VIEW_PAD)) / box.width,
50
+ (viewH * (1 - 2 * FIT_VIEW_PAD)) / box.height,
51
+ );
52
+ return Math.min(maxZoom, Math.max(minZoom, zoom));
53
+ }
54
+
@@ -0,0 +1,48 @@
1
+ import type { Edge, Node } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
2
+ import type { ChevronEdgeHighlight } from '@terrafirmagreg-team/ui/types/canvas';
3
+
4
+ export function applyNodeSelection<N extends Node>(
5
+ nodes: N[],
6
+ selectedId: string | null,
7
+ type?: string,
8
+ ): N[] {
9
+ let changed = false;
10
+ const next = nodes.map((node) => {
11
+ if (type && node.type !== type) return node;
12
+ const selected = selectedId != null && node.id === selectedId;
13
+ if (node.selected === selected) return node;
14
+ changed = true;
15
+ return { ...node, selected };
16
+ });
17
+ return changed ? next : nodes;
18
+ }
19
+
20
+ export function decorateFlowEdges<E extends Edge>(
21
+ edges: E[],
22
+ hoverId: string | null,
23
+ selectedId: string | null,
24
+ ): E[] {
25
+ return edges.map((edge) => {
26
+ const outgoing = hoverId != null && edge.source === hoverId;
27
+ const incoming = hoverId != null && edge.target === hoverId;
28
+ const selected =
29
+ selectedId != null &&
30
+ (edge.source === selectedId || edge.target === selectedId);
31
+ const sourceEngaged = outgoing || selectedId === edge.source;
32
+ const targetEngaged = incoming || selectedId === edge.target;
33
+ const flags = edge.data as
34
+ | { hideTarget?: boolean; hideSource?: boolean }
35
+ | undefined;
36
+ const hidden =
37
+ (flags?.hideTarget === true && !targetEngaged) ||
38
+ (flags?.hideSource === true && !sourceEngaged);
39
+ const highlight: ChevronEdgeHighlight =
40
+ outgoing || (selected && edge.source === selectedId)
41
+ ? 'outgoing'
42
+ : incoming || (selected && edge.target === selectedId)
43
+ ? 'incoming'
44
+ : undefined;
45
+ return { ...edge, data: { hidden, highlight } };
46
+ });
47
+ }
48
+
@@ -0,0 +1,38 @@
1
+ import type { StoredViewport } from '@terrafirmagreg-team/ui/types/canvas';
2
+
3
+ export const VIEWPORT_SAVE_MS = 300;
4
+ export const FLOW_MIN_ZOOM = 0.25;
5
+ export const FLOW_MAX_ZOOM = 2;
6
+
7
+ export function readStoredViewport(key: string): StoredViewport | null {
8
+ try {
9
+ const raw = localStorage.getItem(key);
10
+ if (!raw) return null;
11
+ const parsed = JSON.parse(raw) as Partial<StoredViewport>;
12
+ if (
13
+ typeof parsed.x !== 'number' ||
14
+ typeof parsed.y !== 'number' ||
15
+ typeof parsed.zoom !== 'number' ||
16
+ !Number.isFinite(parsed.x) ||
17
+ !Number.isFinite(parsed.y) ||
18
+ !Number.isFinite(parsed.zoom)
19
+ ) {
20
+ return null;
21
+ }
22
+ return { x: parsed.x, y: parsed.y, zoom: parsed.zoom };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function writeStoredViewport(
29
+ key: string,
30
+ viewport: StoredViewport,
31
+ ): void {
32
+ try {
33
+ localStorage.setItem(key, JSON.stringify(viewport));
34
+ } catch {
35
+
36
+ }
37
+ }
38
+
@@ -0,0 +1,2 @@
1
+ export * from '@xyflow/react';
2
+
@@ -0,0 +1,106 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
3
+ import type { PagePreviewData, PreviewIcon } from '@terrafirmagreg-team/ui/types/preview';
4
+
5
+ export type ExternalMetaProvider = {
6
+ host: RegExp;
7
+ path?: RegExp;
8
+ url: string;
9
+ kind?: 'json' | 'html';
10
+ title?: string;
11
+ description?: string;
12
+ replace?: [RegExp, string][];
13
+ };
14
+
15
+ export const DEFAULT_EXTERNAL_META_PROVIDERS: ExternalMetaProvider[] = [
16
+ {
17
+ host: /(^|\.)wikipedia\.org$/i,
18
+ path: /^\/wiki\/(.+)$/,
19
+ url: 'https://{host}/api/rest_v1/page/summary/{1}',
20
+ kind: 'json',
21
+ title: 'titles.normalized',
22
+ description: 'description',
23
+ replace: [[/_/g, ' ']],
24
+ },
25
+ {
26
+ host: /^github\.com$/i,
27
+ path: /^\/([^/]+)\/([^/?#]+)\/releases\/tag\/([^/?#]+)\/?$/,
28
+ url: 'https://api.github.com/repos/{1}/{2}/releases/tags/{3}',
29
+ kind: 'json',
30
+ title: 'name',
31
+ description: 'body',
32
+ },
33
+ {
34
+ host: /^github\.com$/i,
35
+ path: /^\/([^/]+)\/([^/?#]+)\/(?:issues|pull)\/([^/?#]+)\/?$/,
36
+ url: 'https://api.github.com/repos/{1}/{2}/issues/{3}',
37
+ kind: 'json',
38
+ title: 'title',
39
+ description: 'body',
40
+ },
41
+ {
42
+ host: /^github\.com$/i,
43
+ path: /^\/([^/]+)\/([^/?#]+)\/?$/,
44
+ url: 'https://api.github.com/repos/{1}/{2}',
45
+ kind: 'json',
46
+ title: 'full_name',
47
+ description: 'description',
48
+ },
49
+ {
50
+ host: /^github\.com$/i,
51
+ path: /^\/([^/?#]+)\/?$/,
52
+ url: 'https://api.github.com/users/{1}',
53
+ kind: 'json',
54
+ title: 'name',
55
+ description: 'bio',
56
+ },
57
+ {
58
+ host: /.*/,
59
+ url: 'https://api.allorigins.win/raw?url={href}',
60
+ kind: 'html',
61
+ },
62
+ ];
63
+
64
+ export const DEFAULT_EXTERNAL_FAVICON = 'https://{host}/favicon.ico';
65
+
66
+ export type UiConfig = {
67
+ basePath?: string;
68
+
69
+ brand?: { name: string; icon?: string };
70
+
71
+ components?: Record<string, ComponentType<any>>;
72
+ externalMetaProviders?: ExternalMetaProvider[];
73
+ externalFavicon?: string;
74
+
75
+ loadPagePreview?: (
76
+ href: string,
77
+ ctx?: { lang?: string },
78
+ ) => Promise<PagePreviewData | undefined>;
79
+
80
+ loadPagePreviews?: (
81
+ lang: Lang,
82
+ langMap: Record<string, string>,
83
+ hrefs: Iterable<string>,
84
+ ) => Promise<Record<string, PagePreviewData>>;
85
+ pagePreviewIcon?: ComponentType<{ icon: PreviewIcon }>;
86
+ };
87
+
88
+ export function createConfigRegistry<I extends object, R = I>(
89
+ resolve: (overrides: I) => R = (overrides) => overrides as unknown as R,
90
+ ) {
91
+ let overrides = {} as I;
92
+ return {
93
+ register(input: I): void {
94
+ overrides = { ...overrides, ...input };
95
+ },
96
+ get(): R {
97
+ return resolve(overrides);
98
+ },
99
+ };
100
+ }
101
+
102
+ const { register: registerUiConfig, get: uiConfig } =
103
+ createConfigRegistry<UiConfig>();
104
+
105
+ export { registerUiConfig, uiConfig };
106
+
@@ -0,0 +1,21 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ export const COLORS: Record<string, string> = {
4
+ info: 'var(--color-info)',
5
+ note: 'var(--color-note)',
6
+ tip: 'var(--color-tip)',
7
+ warn: 'var(--color-warning)',
8
+ warning: 'var(--color-warning)',
9
+ error: 'var(--color-error)',
10
+ danger: 'var(--color-error)',
11
+ success: 'var(--color-success)',
12
+ idea: 'var(--color-idea)',
13
+ };
14
+
15
+ export function calloutStyle(
16
+ type: string,
17
+ style: CSSProperties | undefined,
18
+ ): CSSProperties {
19
+ return { '--callout-color': COLORS[type] ?? COLORS.info, ...style } as CSSProperties;
20
+ }
21
+