@terrafirmagreg-team/ui 0.0.0-stage → 0.1.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.
- package/package.json +93 -3
- package/src/components/blocks/button/icon-button.tsx +48 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +134 -0
- package/src/components/blocks/canvas/flow-canvas.tsx +177 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +32 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +243 -0
- package/src/components/blocks/git/github-info.tsx +77 -0
- package/src/components/blocks/lang/content.tsx +22 -0
- package/src/components/blocks/lang/flag.tsx +24 -0
- package/src/components/blocks/lang/link.tsx +22 -0
- package/src/components/blocks/lang/redirect.tsx +47 -0
- package/src/components/blocks/lang/switcher.tsx +53 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +28 -0
- package/src/components/blocks/link/link-preview.tsx +256 -0
- package/src/components/blocks/list/row.tsx +91 -0
- package/src/components/blocks/media/glb/model.tsx +243 -0
- package/src/components/blocks/media/glb/scene.tsx +372 -0
- package/src/components/blocks/media/glb/viewer.tsx +51 -0
- package/src/components/blocks/media/image.tsx +135 -0
- package/src/components/blocks/media/slideshow.tsx +177 -0
- package/src/components/blocks/nav/breadcrumb.tsx +50 -0
- package/src/components/blocks/nav/link.tsx +85 -0
- package/src/components/blocks/nav/site-menu.tsx +77 -0
- package/src/components/blocks/nav/site-title.tsx +60 -0
- package/src/components/blocks/nav/switcher.tsx +205 -0
- package/src/components/blocks/page/empty-state.tsx +31 -0
- package/src/components/blocks/page/heading.tsx +53 -0
- package/src/components/blocks/page/pager.tsx +112 -0
- package/src/components/blocks/readability/menu.tsx +431 -0
- package/src/components/blocks/readability/track.tsx +164 -0
- package/src/components/blocks/search/command.tsx +79 -0
- package/src/components/blocks/search/dialog.tsx +712 -0
- package/src/components/blocks/search/provider.tsx +117 -0
- package/src/components/blocks/sidebar.tsx +406 -0
- package/src/components/blocks/theme/toggle.tsx +71 -0
- package/src/components/blocks/tooltip/hint.tsx +27 -0
- package/src/components/blocks/viewer/controls.tsx +279 -0
- package/src/components/blocks/viewer/float-bar.tsx +50 -0
- package/src/components/layouts/layout.tsx +93 -0
- package/src/components/layouts/left-sidebar/content.tsx +62 -0
- package/src/components/layouts/left-sidebar/context.tsx +54 -0
- package/src/components/layouts/left-sidebar/expand.tsx +22 -0
- package/src/components/layouts/left-sidebar/footer.tsx +105 -0
- package/src/components/layouts/left-sidebar/index.ts +28 -0
- package/src/components/layouts/left-sidebar/items.tsx +102 -0
- package/src/components/layouts/left-sidebar/search.tsx +74 -0
- package/src/components/layouts/left-sidebar/sidebar.tsx +188 -0
- package/src/components/layouts/left-sidebar/tree.tsx +156 -0
- package/src/components/layouts/right-sidebar.tsx +95 -0
- package/src/components/layouts/tree-context.tsx +41 -0
- package/src/components/mdx/accordion.tsx +184 -0
- package/src/components/mdx/banner.tsx +154 -0
- package/src/components/mdx/callout.tsx +96 -0
- package/src/components/mdx/card.tsx +49 -0
- package/src/components/mdx/codeblock.tsx +249 -0
- package/src/components/mdx/collapsible.tsx +32 -0
- package/src/components/mdx/files.tsx +62 -0
- package/src/components/mdx/glow-header.tsx +40 -0
- package/src/components/mdx/gradient-text.tsx +27 -0
- package/src/components/mdx/image-zoom.tsx +314 -0
- package/src/components/mdx/mermaid.tsx +16 -0
- package/src/components/mdx/multiblock.tsx +18 -0
- package/src/components/mdx/preview.tsx +100 -0
- package/src/components/mdx/steps.tsx +9 -0
- package/src/components/mdx/table.tsx +13 -0
- package/src/components/mdx/tabs.tsx +353 -0
- package/src/components/mdx/type-table.tsx +170 -0
- package/src/components/ui/accordion.tsx +71 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/alert.tsx +75 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +207 -0
- package/src/components/ui/avatar.tsx +108 -0
- package/src/components/ui/badge.tsx +51 -0
- package/src/components/ui/breadcrumb.tsx +124 -0
- package/src/components/ui/bubble.tsx +127 -0
- package/src/components/ui/button-group.tsx +87 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +221 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +242 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/combobox.tsx +297 -0
- package/src/components/ui/command.tsx +198 -0
- package/src/components/ui/context-menu.tsx +270 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/direction.tsx +6 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +103 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +158 -0
- package/src/components/ui/input-otp.tsx +86 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +70 -0
- package/src/components/ui/menubar.tsx +280 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/native-select.tsx +60 -0
- package/src/components/ui/navigation-menu.tsx +167 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/popover.tsx +91 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +324 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +115 -0
- package/src/components/ui/tabs.tsx +81 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +234 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/hooks/canvas/use-flow-hover.ts +18 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +33 -0
- package/src/hooks/lang.ts +8 -0
- package/src/hooks/layout.ts +18 -0
- package/src/hooks/page/use-fit-pages.ts +91 -0
- package/src/hooks/page/use-location-search.ts +25 -0
- package/src/hooks/page/use-page-preview.ts +40 -0
- package/src/hooks/page/use-paged-list.ts +20 -0
- package/src/hooks/readability/provider.ts +86 -0
- package/src/hooks/search/dialog.ts +41 -0
- package/src/hooks/search/provider.ts +25 -0
- package/src/hooks/search/use-highlight-query.ts +51 -0
- package/src/hooks/sidebar.ts +63 -0
- package/src/hooks/use-async-value.ts +57 -0
- package/src/hooks/use-close-on-scroll.ts +23 -0
- package/src/hooks/use-copy-button.ts +46 -0
- package/src/hooks/use-cycle-index.ts +75 -0
- package/src/hooks/use-document-title.ts +9 -0
- package/src/hooks/use-element-size.ts +35 -0
- package/src/hooks/use-external-meta.ts +132 -0
- package/src/hooks/use-hot-key.ts +33 -0
- package/src/hooks/use-hover-follow.ts +54 -0
- package/src/hooks/use-in-view.ts +30 -0
- package/src/hooks/use-last-value.ts +15 -0
- package/src/hooks/use-marquee.ts +51 -0
- package/src/hooks/use-media-query.ts +14 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-modifier-keys.ts +31 -0
- package/src/hooks/use-on-change.ts +35 -0
- package/src/hooks/use-pane-keys.ts +28 -0
- package/src/hooks/use-swipe.ts +26 -0
- package/src/hooks/use-tree.ts +102 -0
- package/src/hooks/viewer/use-in-float-bar.ts +6 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +77 -0
- package/src/lib/canvas/edge.ts +173 -0
- package/src/lib/canvas/geometry.ts +53 -0
- package/src/lib/canvas/state.ts +47 -0
- package/src/lib/canvas/viewport.ts +37 -0
- package/src/lib/canvas/xyflow.ts +1 -0
- package/src/lib/config.ts +81 -0
- package/src/lib/mdx/callout.ts +20 -0
- package/src/lib/mdx/gradient.ts +15 -0
- package/src/lib/mdx/mermaid.ts +14 -0
- package/src/lib/media/crop-opaque.ts +76 -0
- package/src/lib/media/nearby-indexes.ts +6 -0
- package/src/lib/media/trim-transparent.ts +117 -0
- package/src/lib/readability/config.ts +10 -0
- package/src/lib/readability/spotlight.ts +186 -0
- package/src/lib/readability/store.ts +146 -0
- package/src/lib/tree.ts +92 -0
- package/src/lib/utils/async-cache.ts +32 -0
- package/src/lib/utils/cn.ts +1 -0
- package/src/lib/utils/format.ts +22 -0
- package/src/lib/utils/history.ts +9 -0
- package/src/lib/utils/merge-refs.ts +13 -0
- package/src/lib/utils/nav-active.ts +58 -0
- package/src/lib/utils/stored-value.ts +136 -0
- package/src/lib/utils/urls.ts +17 -0
- package/src/locales/en_us.json +119 -0
- package/src/locales/pl_pl.json +35 -0
- package/src/locales/ru_ru.json +119 -0
- package/src/locales/uk_ua.json +35 -0
- package/src/locales/zh_cn.json +17 -0
- package/src/locales/zh_tw.json +17 -0
- package/src/locales.ts +5 -0
- package/src/provider.tsx +141 -0
- package/src/types/canvas.ts +49 -0
- package/src/types/glb.ts +25 -0
- package/src/types/nav.ts +95 -0
- package/src/types/preview.ts +9 -0
- package/src/types/readability.ts +28 -0
- package/src/types/search.ts +56 -0
- package/src/types/tree.ts +18 -0
- package/src/types/viewer.ts +53 -0
- package/styles/content/base.css +194 -0
- package/styles/content/default-colors.css +51 -0
- package/styles/content/shiki.css +108 -0
- package/styles/content/typography.mjs +3941 -0
- package/styles/content.css +7 -0
- package/styles/image-zoom.css +83 -0
- package/styles/layout.css +28 -0
- package/styles/left-sidebar.css +140 -0
- package/styles/readability.css +50 -0
- package/styles/right-sidebar.css +6 -0
- package/styles/workspace.css +26 -0
- package/styles.css +413 -0
- package/README.md +0 -3
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
|
|
22
|
+
export function useTabsGroups(tabs: TabOption[]): {
|
|
23
|
+
active?: PageTree.Folder;
|
|
24
|
+
options: TabOption[];
|
|
25
|
+
}[] {
|
|
26
|
+
const { full: tree } = useTreeContext();
|
|
27
|
+
const path = use(PathContext);
|
|
28
|
+
|
|
29
|
+
return useMemo(() => {
|
|
30
|
+
const out: { active?: PageTree.Folder; options: TabOption[] }[] = [];
|
|
31
|
+
const last = path[path.length - 1];
|
|
32
|
+
const page = last?.type === 'page' ? last : undefined;
|
|
33
|
+
let scope = tree.fallback
|
|
34
|
+
? [...tree.children, ...tree.fallback.children]
|
|
35
|
+
: tree.children;
|
|
36
|
+
|
|
37
|
+
for (const node of path) {
|
|
38
|
+
if (node.type !== 'folder' || !node.root) continue;
|
|
39
|
+
const group = { active: node, options: [] as TabOption[] };
|
|
40
|
+
collectTabs(scope, node, page, tabs, group.options);
|
|
41
|
+
if (group.options.length > 0) out.push(group);
|
|
42
|
+
scope = node.children;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const custom = tabs.filter((tab) => !tab.$folder);
|
|
46
|
+
if (custom.length > 0) {
|
|
47
|
+
const group = out.findLast((group) => group.active?.root === true);
|
|
48
|
+
if (group) group.options.push(...custom);
|
|
49
|
+
else out.push({ options: custom });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}, [tabs, tree, path]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
function collectTabs(
|
|
57
|
+
scope: PageTree.Node[],
|
|
58
|
+
active: PageTree.Folder,
|
|
59
|
+
page: PageTree.Item | undefined,
|
|
60
|
+
tabs: TabOption[],
|
|
61
|
+
out: TabOption[],
|
|
62
|
+
): void {
|
|
63
|
+
for (const node of scope) {
|
|
64
|
+
if (node.type !== 'folder') continue;
|
|
65
|
+
if (node.root === active.root) {
|
|
66
|
+
const tab = tabs.find(
|
|
67
|
+
(tab) =>
|
|
68
|
+
tab.$folder &&
|
|
69
|
+
(tab.$folder === node || tab.$folder.$id === node.$id),
|
|
70
|
+
);
|
|
71
|
+
if (!tab) continue;
|
|
72
|
+
const projection =
|
|
73
|
+
page && findProjection(active, node, page);
|
|
74
|
+
out.push(projection ? { ...tab, url: projection.url } : tab);
|
|
75
|
+
} else if (!node.root) {
|
|
76
|
+
collectTabs(node.children, active, page, tabs, out);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const footerCache = new WeakMap<PageTree.Root | PageTree.Folder, PageTree.Item[]>();
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
export function useFooterItems(): PageTree.Item[] {
|
|
85
|
+
const { root } = useTreeContext();
|
|
86
|
+
const cached = footerCache.get(root);
|
|
87
|
+
if (cached) return cached;
|
|
88
|
+
|
|
89
|
+
const list: PageTree.Item[] = [];
|
|
90
|
+
function onNode(node: PageTree.Node): void {
|
|
91
|
+
if (node.type === 'folder') {
|
|
92
|
+
if (node.index) onNode(node.index);
|
|
93
|
+
for (const child of node.children) onNode(child);
|
|
94
|
+
} else if (node.type === 'page' && !node.external) {
|
|
95
|
+
list.push(node);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const child of root.children) onNode(child);
|
|
99
|
+
|
|
100
|
+
footerCache.set(root, list);
|
|
101
|
+
return list;
|
|
102
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChevronEdgeHighlight,
|
|
3
|
+
FlowPoint,
|
|
4
|
+
} from '@terrafirmagreg-team/ui/types/canvas';
|
|
5
|
+
|
|
6
|
+
const CHEVRON_SPACING = 10;
|
|
7
|
+
const CHEVRON_SIZE = 3.5;
|
|
8
|
+
|
|
9
|
+
const RUN_COS = Math.cos(Math.PI / 7);
|
|
10
|
+
|
|
11
|
+
export type ChevronRun = { d: string; shiftX: number; shiftY: number };
|
|
12
|
+
|
|
13
|
+
export function chevronTicksAlongPath(
|
|
14
|
+
points: readonly FlowPoint[],
|
|
15
|
+
): ChevronRun[] {
|
|
16
|
+
if (points.length < 2) return [];
|
|
17
|
+
const lens: number[] = [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (let i = 0; i + 1 < points.length; i += 1) {
|
|
20
|
+
const len = Math.hypot(
|
|
21
|
+
points[i + 1]!.x - points[i]!.x,
|
|
22
|
+
points[i + 1]!.y - points[i]!.y,
|
|
23
|
+
);
|
|
24
|
+
lens.push(len);
|
|
25
|
+
total += len;
|
|
26
|
+
}
|
|
27
|
+
if (total < 2) return [];
|
|
28
|
+
const size = CHEVRON_SIZE;
|
|
29
|
+
const back = size * 0.45;
|
|
30
|
+
const wing = size * 0.9;
|
|
31
|
+
const runs: { ux: number; uy: number; parts: string[] }[] = [];
|
|
32
|
+
let run: (typeof runs)[0] | null = null;
|
|
33
|
+
let seg = 0;
|
|
34
|
+
let segStart = 0;
|
|
35
|
+
for (let d = CHEVRON_SPACING; d <= total; d += CHEVRON_SPACING) {
|
|
36
|
+
while (seg < lens.length && segStart + lens[seg]! < d) {
|
|
37
|
+
segStart += lens[seg]!;
|
|
38
|
+
seg += 1;
|
|
39
|
+
}
|
|
40
|
+
if (seg >= lens.length) break;
|
|
41
|
+
const len = lens[seg]!;
|
|
42
|
+
if (len < 0.001) continue;
|
|
43
|
+
const p0 = points[seg]!;
|
|
44
|
+
const p1 = points[seg + 1]!;
|
|
45
|
+
const t = (d - segStart) / len;
|
|
46
|
+
const ux = (p1.x - p0.x) / len;
|
|
47
|
+
const uy = (p1.y - p0.y) / len;
|
|
48
|
+
const cx = p0.x + (p1.x - p0.x) * t;
|
|
49
|
+
const cy = p0.y + (p1.y - p0.y) * t;
|
|
50
|
+
if (!run || ux * run.ux + uy * run.uy < RUN_COS) {
|
|
51
|
+
run = { ux, uy, parts: [] };
|
|
52
|
+
runs.push(run);
|
|
53
|
+
}
|
|
54
|
+
const px = -uy;
|
|
55
|
+
const py = ux;
|
|
56
|
+
run.parts.push(
|
|
57
|
+
`M${cx - ux * back + px * wing} ${cy - uy * back + py * wing}L${cx + ux * size} ${cy + uy * size}L${cx - ux * back - px * wing} ${cy - uy * back - py * wing}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return runs.map((entry) => ({
|
|
61
|
+
d: entry.parts.join(''),
|
|
62
|
+
shiftX: entry.ux * CHEVRON_SPACING,
|
|
63
|
+
shiftY: entry.uy * CHEVRON_SPACING,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function svgPathToPoints(d: string, samples = 12): FlowPoint[] {
|
|
68
|
+
const out: FlowPoint[] = [];
|
|
69
|
+
const re = /([MLCQ])([^MLCQ]*)/g;
|
|
70
|
+
let match: RegExpExecArray | null;
|
|
71
|
+
let cur: FlowPoint = { x: 0, y: 0 };
|
|
72
|
+
while ((match = re.exec(d))) {
|
|
73
|
+
const nums = match[2]!.trim().split(/[\s,]+/).map(Number);
|
|
74
|
+
if (match[1] === 'M' || match[1] === 'L') {
|
|
75
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
76
|
+
cur = { x: nums[i]!, y: nums[i + 1]! };
|
|
77
|
+
out.push(cur);
|
|
78
|
+
}
|
|
79
|
+
} else if (match[1] === 'C') {
|
|
80
|
+
for (let i = 0; i + 5 < nums.length; i += 6) {
|
|
81
|
+
const p0 = cur;
|
|
82
|
+
const c1 = { x: nums[i]!, y: nums[i + 1]! };
|
|
83
|
+
const c2 = { x: nums[i + 2]!, y: nums[i + 3]! };
|
|
84
|
+
const p1 = { x: nums[i + 4]!, y: nums[i + 5]! };
|
|
85
|
+
for (let s = 1; s <= samples; s += 1) {
|
|
86
|
+
const t = s / samples;
|
|
87
|
+
const mt = 1 - t;
|
|
88
|
+
out.push({
|
|
89
|
+
x:
|
|
90
|
+
mt * mt * mt * p0.x +
|
|
91
|
+
3 * mt * mt * t * c1.x +
|
|
92
|
+
3 * mt * t * t * c2.x +
|
|
93
|
+
t * t * t * p1.x,
|
|
94
|
+
y:
|
|
95
|
+
mt * mt * mt * p0.y +
|
|
96
|
+
3 * mt * mt * t * c1.y +
|
|
97
|
+
3 * mt * t * t * c2.y +
|
|
98
|
+
t * t * t * p1.y,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
cur = p1;
|
|
102
|
+
}
|
|
103
|
+
} else if (match[1] === 'Q') {
|
|
104
|
+
for (let i = 0; i + 3 < nums.length; i += 4) {
|
|
105
|
+
const p0 = cur;
|
|
106
|
+
const c = { x: nums[i]!, y: nums[i + 1]! };
|
|
107
|
+
const p1 = { x: nums[i + 2]!, y: nums[i + 3]! };
|
|
108
|
+
for (let s = 1; s <= samples; s += 1) {
|
|
109
|
+
const t = s / samples;
|
|
110
|
+
const mt = 1 - t;
|
|
111
|
+
out.push({
|
|
112
|
+
x: mt * mt * p0.x + 2 * mt * t * c.x + t * t * p1.x,
|
|
113
|
+
y: mt * mt * p0.y + 2 * mt * t * c.y + t * t * p1.y,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
cur = p1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function chamferedPolylinePath(
|
|
124
|
+
points: readonly FlowPoint[],
|
|
125
|
+
cut = 8,
|
|
126
|
+
): string {
|
|
127
|
+
if (!points.length) return '';
|
|
128
|
+
if (points.length < 3) {
|
|
129
|
+
return `M${points[0]!.x} ${points[0]!.y}${points
|
|
130
|
+
.slice(1)
|
|
131
|
+
.map((p) => `L${p.x} ${p.y}`)
|
|
132
|
+
.join('')}`;
|
|
133
|
+
}
|
|
134
|
+
let d = `M${points[0]!.x} ${points[0]!.y}`;
|
|
135
|
+
let cursor = points[0]!;
|
|
136
|
+
for (let i = 1; i + 1 < points.length; i += 1) {
|
|
137
|
+
const corner = points[i]!;
|
|
138
|
+
const next = points[i + 1]!;
|
|
139
|
+
const inLen = Math.hypot(corner.x - cursor.x, corner.y - cursor.y);
|
|
140
|
+
const outLen = Math.hypot(next.x - corner.x, next.y - corner.y);
|
|
141
|
+
const take = Math.min(cut, inLen / 2, outLen / 2);
|
|
142
|
+
if (take < 0.5) {
|
|
143
|
+
d += `L${corner.x} ${corner.y}`;
|
|
144
|
+
cursor = corner;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const ax = corner.x - ((corner.x - cursor.x) / inLen) * take;
|
|
148
|
+
const ay = corner.y - ((corner.y - cursor.y) / inLen) * take;
|
|
149
|
+
const bx = corner.x + ((next.x - corner.x) / outLen) * take;
|
|
150
|
+
const by = corner.y + ((next.y - corner.y) / outLen) * take;
|
|
151
|
+
d += `L${ax} ${ay}L${bx} ${by}`;
|
|
152
|
+
cursor = { x: bx, y: by };
|
|
153
|
+
}
|
|
154
|
+
const last = points[points.length - 1]!;
|
|
155
|
+
return `${d}L${last.x} ${last.y}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function chevronHighlightClass(
|
|
159
|
+
highlight: ChevronEdgeHighlight,
|
|
160
|
+
): string {
|
|
161
|
+
switch (highlight) {
|
|
162
|
+
case 'incoming':
|
|
163
|
+
return 'chevron-edge--incoming';
|
|
164
|
+
case 'outgoing':
|
|
165
|
+
return 'chevron-edge--outgoing';
|
|
166
|
+
case undefined:
|
|
167
|
+
return '';
|
|
168
|
+
default: {
|
|
169
|
+
const _never: never = highlight;
|
|
170
|
+
return _never;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@xyflow/react';
|
|
@@ -0,0 +1,81 @@
|
|
|
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: /^\/([^/]+)\/([^/?#]+)\/?$/,
|
|
28
|
+
url: 'https://api.github.com/repos/{1}/{2}',
|
|
29
|
+
kind: 'json',
|
|
30
|
+
title: 'full_name',
|
|
31
|
+
description: 'description',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
host: /.*/,
|
|
35
|
+
url: 'https://api.allorigins.win/raw?url={href}',
|
|
36
|
+
kind: 'html',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_EXTERNAL_FAVICON = 'https://{host}/favicon.ico';
|
|
41
|
+
|
|
42
|
+
export type UiConfig = {
|
|
43
|
+
basePath?: string;
|
|
44
|
+
|
|
45
|
+
brand?: { name: string; icon?: string };
|
|
46
|
+
|
|
47
|
+
components?: Record<string, ComponentType<any>>;
|
|
48
|
+
externalMetaProviders?: ExternalMetaProvider[];
|
|
49
|
+
externalFavicon?: string;
|
|
50
|
+
|
|
51
|
+
loadPagePreview?: (
|
|
52
|
+
href: string,
|
|
53
|
+
ctx?: { lang?: string },
|
|
54
|
+
) => Promise<PagePreviewData | undefined>;
|
|
55
|
+
|
|
56
|
+
loadPagePreviews?: (
|
|
57
|
+
lang: Lang,
|
|
58
|
+
langMap: Record<string, string>,
|
|
59
|
+
hrefs: Iterable<string>,
|
|
60
|
+
) => Promise<Record<string, PagePreviewData>>;
|
|
61
|
+
pagePreviewIcon?: ComponentType<{ icon: PreviewIcon }>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function createConfigRegistry<I extends object, R = I>(
|
|
65
|
+
resolve: (overrides: I) => R = (overrides) => overrides as unknown as R,
|
|
66
|
+
) {
|
|
67
|
+
let overrides = {} as I;
|
|
68
|
+
return {
|
|
69
|
+
register(input: I): void {
|
|
70
|
+
overrides = { ...overrides, ...input };
|
|
71
|
+
},
|
|
72
|
+
get(): R {
|
|
73
|
+
return resolve(overrides);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { register: registerUiConfig, get: uiConfig } =
|
|
79
|
+
createConfigRegistry<UiConfig>();
|
|
80
|
+
|
|
81
|
+
export { registerUiConfig, uiConfig };
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type GradientProps = {
|
|
2
|
+
from?: string;
|
|
3
|
+
to?: string;
|
|
4
|
+
dir?: string;
|
|
5
|
+
image?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function gradientImage({
|
|
9
|
+
from = '#fa18cf',
|
|
10
|
+
to = '#ff7967',
|
|
11
|
+
dir = 'to right',
|
|
12
|
+
image,
|
|
13
|
+
}: GradientProps): string {
|
|
14
|
+
return image ?? `linear-gradient(${dir}, ${from}, ${to})`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { renderMermaidSVG } from 'beautiful-mermaid';
|
|
2
|
+
|
|
3
|
+
export function renderMermaidChart(chart: string): string | null {
|
|
4
|
+
try {
|
|
5
|
+
return renderMermaidSVG(chart, {
|
|
6
|
+
bg: 'var(--color-background)',
|
|
7
|
+
fg: 'var(--color-foreground)',
|
|
8
|
+
interactive: true,
|
|
9
|
+
transparent: true,
|
|
10
|
+
});
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|