@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.
- package/package.json +93 -3
- package/src/components/blocks/button/icon-button.tsx +49 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +242 -0
- package/src/components/blocks/canvas/flow-canvas.tsx +181 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +33 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +244 -0
- package/src/components/blocks/fit-box.tsx +26 -0
- package/src/components/blocks/git/github-info.tsx +78 -0
- package/src/components/blocks/lang/content.tsx +52 -0
- package/src/components/blocks/lang/flag.tsx +25 -0
- package/src/components/blocks/lang/link.tsx +23 -0
- package/src/components/blocks/lang/redirect.tsx +48 -0
- package/src/components/blocks/lang/switcher.tsx +54 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +29 -0
- package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
- package/src/components/blocks/link/link-preview.tsx +257 -0
- package/src/components/blocks/list/row.tsx +92 -0
- package/src/components/blocks/media/glb/model.tsx +244 -0
- package/src/components/blocks/media/glb/scene.tsx +373 -0
- package/src/components/blocks/media/glb/viewer.tsx +52 -0
- package/src/components/blocks/media/image.tsx +136 -0
- package/src/components/blocks/media/slideshow.tsx +178 -0
- package/src/components/blocks/nav/breadcrumb.tsx +51 -0
- package/src/components/blocks/nav/copy-menu.tsx +37 -0
- package/src/components/blocks/nav/link.tsx +86 -0
- package/src/components/blocks/nav/site-menu.tsx +78 -0
- package/src/components/blocks/nav/site-title.tsx +61 -0
- package/src/components/blocks/nav/switcher.tsx +206 -0
- package/src/components/blocks/page/empty-state.tsx +32 -0
- package/src/components/blocks/page/heading.tsx +54 -0
- package/src/components/blocks/page/pager.tsx +220 -0
- package/src/components/blocks/readability/fields.tsx +250 -0
- package/src/components/blocks/readability/menu.tsx +247 -0
- package/src/components/blocks/readability/track.tsx +165 -0
- package/src/components/blocks/search/command.tsx +80 -0
- package/src/components/blocks/search/dialog.tsx +724 -0
- package/src/components/blocks/search/provider.tsx +118 -0
- package/src/components/blocks/sidebar.tsx +406 -0
- 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 +75 -0
- package/src/components/blocks/tooltip/hint.tsx +28 -0
- package/src/components/blocks/tooltip/lazy.tsx +41 -0
- package/src/components/blocks/viewer/controls.tsx +284 -0
- package/src/components/blocks/viewer/float-bar.tsx +51 -0
- package/src/components/layouts/layout.tsx +94 -0
- package/src/components/layouts/left-sidebar/content.tsx +63 -0
- package/src/components/layouts/left-sidebar/context.tsx +55 -0
- package/src/components/layouts/left-sidebar/expand.tsx +23 -0
- package/src/components/layouts/left-sidebar/footer.tsx +111 -0
- package/src/components/layouts/left-sidebar/index.ts +29 -0
- package/src/components/layouts/left-sidebar/items.tsx +103 -0
- package/src/components/layouts/left-sidebar/search.tsx +75 -0
- package/src/components/layouts/left-sidebar/sidebar.tsx +189 -0
- package/src/components/layouts/left-sidebar/tree.tsx +157 -0
- package/src/components/layouts/right-sidebar.tsx +96 -0
- package/src/components/layouts/tree-context.tsx +42 -0
- package/src/components/mdx/accordion.tsx +185 -0
- package/src/components/mdx/banner.tsx +157 -0
- package/src/components/mdx/callout.tsx +97 -0
- package/src/components/mdx/card.tsx +56 -0
- package/src/components/mdx/codeblock.tsx +250 -0
- package/src/components/mdx/collapsible.tsx +33 -0
- package/src/components/mdx/files.tsx +63 -0
- package/src/components/mdx/glow-header.tsx +54 -0
- package/src/components/mdx/gradient-text.tsx +28 -0
- package/src/components/mdx/icon.tsx +35 -0
- package/src/components/mdx/image-zoom.tsx +315 -0
- package/src/components/mdx/math.tsx +30 -0
- package/src/components/mdx/mermaid.tsx +23 -0
- package/src/components/mdx/multiblock.tsx +19 -0
- package/src/components/mdx/preview.tsx +101 -0
- package/src/components/mdx/steps.tsx +10 -0
- package/src/components/mdx/table.tsx +14 -0
- package/src/components/mdx/tabs.tsx +354 -0
- package/src/components/mdx/type-table.tsx +171 -0
- package/src/components/provider.tsx +139 -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 +19 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +34 -0
- package/src/hooks/page/use-fit-pages.ts +92 -0
- package/src/hooks/page/use-location-search.ts +31 -0
- package/src/hooks/page/use-page-preview.ts +41 -0
- package/src/hooks/page/use-paged-list.ts +21 -0
- package/src/hooks/readability/use-readability.ts +87 -0
- package/src/hooks/search/use-dialog.ts +42 -0
- package/src/hooks/search/use-highlight-query.ts +58 -0
- package/src/hooks/search/use-provider.ts +26 -0
- package/src/hooks/use-async-value.ts +60 -0
- package/src/hooks/use-close-on-scroll.ts +24 -0
- package/src/hooks/use-copy-button.ts +47 -0
- package/src/hooks/use-cycle-index.ts +76 -0
- package/src/hooks/use-document-title.ts +10 -0
- package/src/hooks/use-element-size.ts +36 -0
- package/src/hooks/use-external-meta.ts +133 -0
- package/src/hooks/use-hot-key.ts +34 -0
- package/src/hooks/use-hover-follow.ts +55 -0
- package/src/hooks/use-in-view.ts +31 -0
- package/src/hooks/use-lang.ts +9 -0
- package/src/hooks/use-last-value.ts +16 -0
- package/src/hooks/use-layout.ts +19 -0
- package/src/hooks/use-marquee.ts +52 -0
- package/src/hooks/use-media-query.ts +15 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-modifier-keys.ts +32 -0
- package/src/hooks/use-on-change.ts +36 -0
- package/src/hooks/use-pane-keys.ts +29 -0
- package/src/hooks/use-sidebar.ts +64 -0
- package/src/hooks/use-swipe.ts +27 -0
- package/src/hooks/use-tree.ts +100 -0
- package/src/hooks/viewer/use-in-float-bar.ts +7 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +78 -0
- package/src/lib/canvas/edge.ts +202 -0
- package/src/lib/canvas/geometry.ts +54 -0
- package/src/lib/canvas/state.ts +48 -0
- package/src/lib/canvas/viewport.ts +38 -0
- package/src/lib/canvas/xyflow.ts +2 -0
- package/src/lib/config.ts +106 -0
- package/src/lib/mdx/callout.ts +21 -0
- package/src/lib/mdx/gradient.ts +16 -0
- package/src/lib/mdx/mermaid.ts +15 -0
- package/src/lib/media/crop-opaque.ts +77 -0
- package/src/lib/media/nearby-indexes.ts +7 -0
- package/src/lib/media/trim-transparent.ts +118 -0
- package/src/lib/readability/config.ts +11 -0
- package/src/lib/readability/spotlight.ts +187 -0
- package/src/lib/readability/store.ts +147 -0
- package/src/lib/tree.ts +90 -0
- package/src/lib/utils/cn.ts +2 -0
- package/src/lib/utils/format.ts +22 -0
- package/src/lib/utils/highlight-style.ts +10 -0
- package/src/lib/utils/history.ts +10 -0
- package/src/lib/utils/merge-refs.ts +14 -0
- package/src/lib/utils/nav-active.ts +59 -0
- package/src/lib/utils/stored-value.ts +137 -0
- package/src/lib/utils/urls.ts +18 -0
- package/src/locales/en_us.json +110 -0
- package/src/locales/pl_pl.json +33 -0
- package/src/locales/ru_ru.json +110 -0
- package/src/locales/uk_ua.json +33 -0
- package/src/locales/zh_cn.json +15 -0
- package/src/locales/zh_tw.json +15 -0
- package/src/locales.ts +6 -0
- package/src/types/canvas.ts +54 -0
- package/src/types/glb.ts +26 -0
- package/src/types/mdx-directives.ts +106 -0
- package/src/types/nav.ts +96 -0
- package/src/types/preview.ts +15 -0
- package/src/types/readability.ts +29 -0
- package/src/types/search.ts +57 -0
- package/src/types/tree.ts +19 -0
- package/src/types/viewer.ts +54 -0
- package/styles/content/base.css +198 -0
- package/styles/content/default-colors.css +51 -0
- package/styles/content/shiki.css +107 -0
- package/styles/content/typography.mjs +3637 -0
- package/styles/content.css +5 -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 +430 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/ui",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared UI kit",
|
|
6
|
+
"license": "LGPL-3.0-only",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./styles.css": "./styles.css",
|
|
9
|
+
"./styles/*": "./styles/*",
|
|
10
|
+
"./provider": "./src/components/provider.tsx",
|
|
11
|
+
"./components/ui/*": [
|
|
12
|
+
"./src/components/ui/*.tsx",
|
|
13
|
+
"./src/components/ui/*/index.tsx"
|
|
14
|
+
],
|
|
15
|
+
"./components/blocks/*": [
|
|
16
|
+
"./src/components/blocks/*.tsx",
|
|
17
|
+
"./src/components/blocks/*/index.tsx"
|
|
18
|
+
],
|
|
19
|
+
"./components/layouts/*": [
|
|
20
|
+
"./src/components/layouts/*.tsx",
|
|
21
|
+
"./src/components/layouts/*/index.tsx",
|
|
22
|
+
"./src/components/layouts/*/index.ts"
|
|
23
|
+
],
|
|
24
|
+
"./hooks/*": [
|
|
25
|
+
"./src/hooks/*.ts",
|
|
26
|
+
"./src/hooks/*/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"./lib/*": [
|
|
29
|
+
"./src/lib/*.ts",
|
|
30
|
+
"./src/lib/*/index.ts"
|
|
31
|
+
],
|
|
32
|
+
"./types/*": [
|
|
33
|
+
"./src/types/*.ts",
|
|
34
|
+
"./src/types/*/index.ts"
|
|
35
|
+
],
|
|
36
|
+
"./components/mdx/*": [
|
|
37
|
+
"./src/components/mdx/*.tsx",
|
|
38
|
+
"./src/components/mdx/*/index.tsx"
|
|
39
|
+
],
|
|
40
|
+
"./locales": "./src/locales.ts"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@base-ui/react": "^1.9.0",
|
|
44
|
+
"@icons-pack/react-simple-icons": "13.15.1",
|
|
45
|
+
"@react-three/drei": "10.7.9",
|
|
46
|
+
"@react-three/fiber": "9.8.2",
|
|
47
|
+
"@shadcn/react": "^0.3.1",
|
|
48
|
+
"@terrafirmagreg-team/translate": "1.2.0",
|
|
49
|
+
"@xyflow/react": "12.12.0",
|
|
50
|
+
"beautiful-mermaid": "^1.1.3",
|
|
51
|
+
"class-variance-authority": "^0.7.1",
|
|
52
|
+
"cmdk": "^1.1.1",
|
|
53
|
+
"cn": "^0.5.0",
|
|
54
|
+
"embla-carousel-react": "^8.6.0",
|
|
55
|
+
"flag-icons": "^7.6.0",
|
|
56
|
+
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
57
|
+
"input-otp": "^1.5.0",
|
|
58
|
+
"katex": "^0.19.1",
|
|
59
|
+
"lucide-react": "^0.577.0",
|
|
60
|
+
"next-themes": "^0.4.6",
|
|
61
|
+
"react-day-picker": "^10.0.2",
|
|
62
|
+
"react-medium-image-zoom": "^5.4.9",
|
|
63
|
+
"react-resizable-panels": "^4.18.0",
|
|
64
|
+
"recharts": "3.10.1",
|
|
65
|
+
"scroll-into-view-if-needed": "^3.1.0",
|
|
66
|
+
"tailwindcss": "^4.3.4",
|
|
67
|
+
"three": "^0.186.1",
|
|
68
|
+
"three-stdlib": "2.36.1",
|
|
69
|
+
"@terrafirmagreg-team/data": "1.2.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"next": ">=15",
|
|
73
|
+
"react": ">=19",
|
|
74
|
+
"react-dom": ">=19"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@types/hast": "^3.0.5",
|
|
78
|
+
"@types/node": "^26.6.5",
|
|
79
|
+
"@types/react": "^19.3.0",
|
|
80
|
+
"@types/react-dom": "^19.3.0",
|
|
81
|
+
"@types/three": "^0.187.0"
|
|
82
|
+
},
|
|
83
|
+
"repository": {
|
|
84
|
+
"type": "git",
|
|
85
|
+
"url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
|
|
86
|
+
"directory": "packages/ui"
|
|
87
|
+
},
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public"
|
|
90
|
+
},
|
|
91
|
+
"files": [
|
|
92
|
+
"src",
|
|
93
|
+
"styles",
|
|
94
|
+
"styles.css"
|
|
95
|
+
]
|
|
6
96
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { Button } from '@terrafirmagreg-team/ui/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '@terrafirmagreg-team/ui/components/ui/tooltip';
|
|
9
|
+
import { useInFloatBar } from '@terrafirmagreg-team/ui/hooks/viewer/use-in-float-bar';
|
|
10
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
11
|
+
|
|
12
|
+
export function IconButton({
|
|
13
|
+
label,
|
|
14
|
+
tooltip = true,
|
|
15
|
+
overlay,
|
|
16
|
+
variant = 'outline',
|
|
17
|
+
size = 'icon-sm',
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: ComponentProps<typeof Button> & {
|
|
21
|
+
label: string;
|
|
22
|
+
tooltip?: boolean;
|
|
23
|
+
overlay?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
const inFloatBar = useInFloatBar();
|
|
26
|
+
const button = (
|
|
27
|
+
<Button
|
|
28
|
+
variant={overlay && variant === 'outline' ? 'ghost' : variant}
|
|
29
|
+
size={size}
|
|
30
|
+
aria-label={label}
|
|
31
|
+
className={cn(
|
|
32
|
+
overlay &&
|
|
33
|
+
(inFloatBar
|
|
34
|
+
? 'rounded-none'
|
|
35
|
+
: 'bg-float/92 backdrop-blur-md hover:bg-float'),
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
if (!tooltip) return button;
|
|
42
|
+
return (
|
|
43
|
+
<Tooltip>
|
|
44
|
+
<TooltipTrigger render={button} />
|
|
45
|
+
<TooltipContent>{label}</TooltipContent>
|
|
46
|
+
</Tooltip>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
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,
|
|
15
|
+
useInternalNode,
|
|
16
|
+
useStore,
|
|
17
|
+
type EdgeProps,
|
|
18
|
+
type InternalNode,
|
|
19
|
+
} from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
|
|
20
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
21
|
+
import {
|
|
22
|
+
CHEVRON_GLYPH,
|
|
23
|
+
chevronHighlightClass,
|
|
24
|
+
chevronTicksAlongPath,
|
|
25
|
+
nearestPointOnPolyline,
|
|
26
|
+
} from '@terrafirmagreg-team/ui/lib/canvas/edge';
|
|
27
|
+
import type {
|
|
28
|
+
ChevronEdgeHighlight,
|
|
29
|
+
FlowPoint,
|
|
30
|
+
} from '@terrafirmagreg-team/ui/types/canvas';
|
|
31
|
+
|
|
32
|
+
export type ChevronEdgePathContext = EdgeProps & {
|
|
33
|
+
sourceNode: InternalNode;
|
|
34
|
+
targetNode: InternalNode;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ChevronEdgeResolvers = {
|
|
38
|
+
resolveCenter?: (node: InternalNode) => FlowPoint;
|
|
39
|
+
resolveBorder?: (node: InternalNode, toward: FlowPoint) => FlowPoint;
|
|
40
|
+
resolvePath?: (ctx: ChevronEdgePathContext) => FlowPoint[];
|
|
41
|
+
renderLabel?: (ctx: ChevronEdgePathContext) => ReactNode;
|
|
42
|
+
labelVisibility?: 'always' | 'hover';
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|
|
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
|
+
|
|
69
|
+
const CHEVRON_TICK_MIN_ZOOM = 0.25;
|
|
70
|
+
|
|
71
|
+
export function createChevronEdge({
|
|
72
|
+
resolveCenter,
|
|
73
|
+
resolveBorder,
|
|
74
|
+
resolvePath,
|
|
75
|
+
renderLabel,
|
|
76
|
+
labelVisibility = 'always',
|
|
77
|
+
className,
|
|
78
|
+
}: ChevronEdgeResolvers): FC<EdgeProps> {
|
|
79
|
+
return memo(function ChevronEdge(props: EdgeProps) {
|
|
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
|
+
}, []);
|
|
94
|
+
const sourceNode = useInternalNode(source);
|
|
95
|
+
const targetNode = useInternalNode(target);
|
|
96
|
+
const farZoom = useStore(
|
|
97
|
+
(state) => state.transform[2] < CHEVRON_TICK_MIN_ZOOM,
|
|
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]);
|
|
102
|
+
const sAbs = sourceNode?.internals.positionAbsolute;
|
|
103
|
+
const tAbs = targetNode?.internals.positionAbsolute;
|
|
104
|
+
const sSize = sourceNode?.measured;
|
|
105
|
+
const tSize = targetNode?.measured;
|
|
106
|
+
const {
|
|
107
|
+
sourceX,
|
|
108
|
+
sourceY,
|
|
109
|
+
sourcePosition,
|
|
110
|
+
targetX,
|
|
111
|
+
targetY,
|
|
112
|
+
targetPosition,
|
|
113
|
+
} = props;
|
|
114
|
+
|
|
115
|
+
const points = useMemo(() => {
|
|
116
|
+
if (!sAbs || !tAbs || !sourceNode || !targetNode) return null;
|
|
117
|
+
return resolvePath
|
|
118
|
+
? resolvePath({ ...props, sourceNode, targetNode })
|
|
119
|
+
: resolveCenter && resolveBorder
|
|
120
|
+
? [
|
|
121
|
+
resolveBorder(sourceNode, resolveCenter(targetNode)),
|
|
122
|
+
resolveBorder(targetNode, resolveCenter(sourceNode)),
|
|
123
|
+
]
|
|
124
|
+
: [
|
|
125
|
+
{ x: sourceX, y: sourceY },
|
|
126
|
+
{ x: targetX, y: targetY },
|
|
127
|
+
];
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
}, [
|
|
130
|
+
data,
|
|
131
|
+
sAbs?.x,
|
|
132
|
+
sAbs?.y,
|
|
133
|
+
sSize?.width,
|
|
134
|
+
sSize?.height,
|
|
135
|
+
tAbs?.x,
|
|
136
|
+
tAbs?.y,
|
|
137
|
+
tSize?.width,
|
|
138
|
+
tSize?.height,
|
|
139
|
+
sourceX,
|
|
140
|
+
sourceY,
|
|
141
|
+
sourcePosition,
|
|
142
|
+
targetX,
|
|
143
|
+
targetY,
|
|
144
|
+
targetPosition,
|
|
145
|
+
]);
|
|
146
|
+
const ticks = useMemo(
|
|
147
|
+
() =>
|
|
148
|
+
points && !farZoom
|
|
149
|
+
? chevronTicksAlongPath(points)
|
|
150
|
+
: { offsets: [], total: 0 },
|
|
151
|
+
[points, farZoom],
|
|
152
|
+
);
|
|
153
|
+
const highlight = data?.highlight as ChevronEdgeHighlight;
|
|
154
|
+
const mid = useMemo(
|
|
155
|
+
() => (points ? pathMidpoint(points) : null),
|
|
156
|
+
[points],
|
|
157
|
+
);
|
|
158
|
+
if (data?.hidden === true) return null;
|
|
159
|
+
if (!points || points.length < 2 || !mid) return null;
|
|
160
|
+
const base = `M${points[0]!.x} ${points[0]!.y}${points
|
|
161
|
+
.slice(1)
|
|
162
|
+
.map((p) => `L${p.x} ${p.y}`)
|
|
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;
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<g
|
|
188
|
+
className={cn(
|
|
189
|
+
'chevron-edge',
|
|
190
|
+
className,
|
|
191
|
+
chevronHighlightClass(highlight),
|
|
192
|
+
)}
|
|
193
|
+
{...hoverTrack}
|
|
194
|
+
>
|
|
195
|
+
<path
|
|
196
|
+
className="chevron-edge__interaction"
|
|
197
|
+
d={base}
|
|
198
|
+
fill="none"
|
|
199
|
+
stroke="transparent"
|
|
200
|
+
strokeWidth={props.interactionWidth ?? 20}
|
|
201
|
+
/>
|
|
202
|
+
<path
|
|
203
|
+
className="chevron-edge__stroke chevron-edge__base"
|
|
204
|
+
d={base}
|
|
205
|
+
aria-hidden="true"
|
|
206
|
+
/>
|
|
207
|
+
<g className="chevron-edge__ticks" aria-hidden="true">
|
|
208
|
+
{ticks.offsets.map((offset) => (
|
|
209
|
+
<path
|
|
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
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
))}
|
|
219
|
+
</g>
|
|
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
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type ComponentProps,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Background,
|
|
12
|
+
ReactFlow,
|
|
13
|
+
useReactFlow,
|
|
14
|
+
type Edge,
|
|
15
|
+
type Node,
|
|
16
|
+
type ReactFlowProps,
|
|
17
|
+
} from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
|
|
18
|
+
import { useTheme } from 'next-themes';
|
|
19
|
+
import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
|
|
20
|
+
import { useFlowViewportPersist } from '@terrafirmagreg-team/ui/hooks/canvas/use-flow-viewport-persist';
|
|
21
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
22
|
+
import {
|
|
23
|
+
flowContentBox,
|
|
24
|
+
zoomToFitBox,
|
|
25
|
+
} from '@terrafirmagreg-team/ui/lib/canvas/geometry';
|
|
26
|
+
import {
|
|
27
|
+
FLOW_MAX_ZOOM,
|
|
28
|
+
FLOW_MIN_ZOOM,
|
|
29
|
+
} from '@terrafirmagreg-team/ui/lib/canvas/viewport';
|
|
30
|
+
import { FlowViewport } from '@terrafirmagreg-team/ui/components/blocks/canvas/flow-viewport';
|
|
31
|
+
import type {
|
|
32
|
+
FlowCanvasViewport,
|
|
33
|
+
FlowRect,
|
|
34
|
+
} from '@terrafirmagreg-team/ui/types/canvas';
|
|
35
|
+
|
|
36
|
+
export type FlowCanvasFit = { onFit: () => void };
|
|
37
|
+
|
|
38
|
+
export type FlowCanvasProps<
|
|
39
|
+
N extends Node = Node,
|
|
40
|
+
E extends Edge = Edge,
|
|
41
|
+
> = Omit<ReactFlowProps<N, E>, 'nodes' | 'children'> & {
|
|
42
|
+
nodes: N[];
|
|
43
|
+
viewportConfig: FlowCanvasViewport;
|
|
44
|
+
|
|
45
|
+
extraRects?: FlowRect[];
|
|
46
|
+
|
|
47
|
+
fitNodes?: (node: N) => boolean;
|
|
48
|
+
backgroundGap?: number;
|
|
49
|
+
className?: string;
|
|
50
|
+
shellProps?: ComponentProps<'div'> &
|
|
51
|
+
Record<`data-${string}`, unknown>;
|
|
52
|
+
|
|
53
|
+
children?: ReactNode | ((fit: FlowCanvasFit) => ReactNode);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function FlowCanvas<N extends Node = Node, E extends Edge = Edge>({
|
|
57
|
+
nodes,
|
|
58
|
+
viewportConfig,
|
|
59
|
+
extraRects,
|
|
60
|
+
fitNodes,
|
|
61
|
+
backgroundGap = 16,
|
|
62
|
+
className,
|
|
63
|
+
shellProps,
|
|
64
|
+
children,
|
|
65
|
+
onMoveEnd,
|
|
66
|
+
...flowProps
|
|
67
|
+
}: FlowCanvasProps<N, E>) {
|
|
68
|
+
const canvasRef = useRef<HTMLDivElement>(null);
|
|
69
|
+
const viewSize = useElementSize(canvasRef);
|
|
70
|
+
const { setCenter } = useReactFlow();
|
|
71
|
+
const { resolvedTheme } = useTheme();
|
|
72
|
+
const { markReady, persist } = useFlowViewportPersist(
|
|
73
|
+
viewportConfig.storageKey,
|
|
74
|
+
viewportConfig.resetKey,
|
|
75
|
+
);
|
|
76
|
+
const contentBox = useMemo(
|
|
77
|
+
() => flowContentBox(nodes, extraRects),
|
|
78
|
+
[nodes, extraRects],
|
|
79
|
+
);
|
|
80
|
+
const scoped = useMemo(
|
|
81
|
+
() => (fitNodes ? nodes.filter(fitNodes) : nodes),
|
|
82
|
+
[nodes, fitNodes],
|
|
83
|
+
);
|
|
84
|
+
const fitBox = useMemo(() => {
|
|
85
|
+
if (!fitNodes || !scoped.length) return contentBox;
|
|
86
|
+
return flowContentBox(scoped, extraRects);
|
|
87
|
+
}, [scoped, extraRects, fitNodes, contentBox]);
|
|
88
|
+
const { focusZoom = 1, persist: _persist, ...viewportRest } = viewportConfig;
|
|
89
|
+
const onFit = useCallback(() => {
|
|
90
|
+
if (!fitBox || viewSize.w < 1 || viewSize.h < 1) return;
|
|
91
|
+
const fitZoom = zoomToFitBox(
|
|
92
|
+
fitBox,
|
|
93
|
+
viewSize.w,
|
|
94
|
+
viewSize.h,
|
|
95
|
+
flowProps.minZoom,
|
|
96
|
+
flowProps.maxZoom,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const pool = scoped.length ? scoped : nodes;
|
|
100
|
+
let anchor: { x: number; y: number } = {
|
|
101
|
+
x: fitBox.midX,
|
|
102
|
+
y: fitBox.midY,
|
|
103
|
+
};
|
|
104
|
+
let best = Infinity;
|
|
105
|
+
for (const node of pool) {
|
|
106
|
+
const dx = node.position.x - fitBox.midX;
|
|
107
|
+
const dy = node.position.y - fitBox.midY;
|
|
108
|
+
const dist = dx * dx + dy * dy;
|
|
109
|
+
if (dist < best) {
|
|
110
|
+
best = dist;
|
|
111
|
+
anchor = node.position;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
void setCenter(anchor.x, anchor.y, {
|
|
116
|
+
zoom: Math.max(fitZoom, focusZoom * 0.3),
|
|
117
|
+
duration: 200,
|
|
118
|
+
});
|
|
119
|
+
}, [
|
|
120
|
+
fitBox,
|
|
121
|
+
scoped,
|
|
122
|
+
nodes,
|
|
123
|
+
setCenter,
|
|
124
|
+
viewSize,
|
|
125
|
+
flowProps.minZoom,
|
|
126
|
+
flowProps.maxZoom,
|
|
127
|
+
focusZoom,
|
|
128
|
+
]);
|
|
129
|
+
const handleMoveEnd = useCallback<
|
|
130
|
+
NonNullable<ReactFlowProps<N, E>['onMoveEnd']>
|
|
131
|
+
>(
|
|
132
|
+
(event, vp) => {
|
|
133
|
+
if (viewportConfig.persist) persist(vp);
|
|
134
|
+
onMoveEnd?.(event, vp);
|
|
135
|
+
},
|
|
136
|
+
[viewportConfig.persist, persist, onMoveEnd],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
ref={canvasRef}
|
|
142
|
+
suppressHydrationWarning
|
|
143
|
+
{...shellProps}
|
|
144
|
+
className={cn(
|
|
145
|
+
'flex-1 min-h-0 h-full not-prose relative w-full overflow-hidden',
|
|
146
|
+
className,
|
|
147
|
+
shellProps?.className,
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{viewSize.w > 0 && viewSize.h > 0 ? (
|
|
151
|
+
<ReactFlow<N, E>
|
|
152
|
+
nodes={nodes}
|
|
153
|
+
nodeOrigin={[0.5, 0.5]}
|
|
154
|
+
nodesConnectable={false}
|
|
155
|
+
elementsSelectable
|
|
156
|
+
panOnDrag
|
|
157
|
+
zoomOnPinch
|
|
158
|
+
minZoom={FLOW_MIN_ZOOM}
|
|
159
|
+
maxZoom={FLOW_MAX_ZOOM}
|
|
160
|
+
proOptions={{ hideAttribution: true }}
|
|
161
|
+
colorMode={resolvedTheme === 'dark' ? 'dark' : 'light'}
|
|
162
|
+
className="h-full bg-card"
|
|
163
|
+
onMoveEnd={handleMoveEnd}
|
|
164
|
+
{...flowProps}
|
|
165
|
+
>
|
|
166
|
+
<Background gap={backgroundGap} size={1} />
|
|
167
|
+
{typeof children === 'function' ? children({ onFit }) : children}
|
|
168
|
+
<FlowViewport
|
|
169
|
+
{...viewportRest}
|
|
170
|
+
nodes={nodes}
|
|
171
|
+
contentBox={contentBox}
|
|
172
|
+
viewSize={viewSize}
|
|
173
|
+
focusZoom={focusZoom}
|
|
174
|
+
onReady={markReady}
|
|
175
|
+
/>
|
|
176
|
+
</ReactFlow>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useReactFlow } from '@terrafirmagreg-team/ui/lib/canvas/xyflow';
|
|
4
|
+
import { Search } from 'lucide-react';
|
|
5
|
+
import {
|
|
6
|
+
ViewerToolbar,
|
|
7
|
+
ViewerControls,
|
|
8
|
+
} from '@terrafirmagreg-team/ui/components/blocks/viewer/controls';
|
|
9
|
+
import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
|
|
10
|
+
import type { FlowToolbarProps } from '@terrafirmagreg-team/ui/types/canvas';
|
|
11
|
+
|
|
12
|
+
export function FlowToolbar({ onFit, search }: FlowToolbarProps) {
|
|
13
|
+
const { zoomIn, zoomOut } = useReactFlow();
|
|
14
|
+
return (
|
|
15
|
+
<ViewerToolbar>
|
|
16
|
+
<ViewerControls
|
|
17
|
+
start={
|
|
18
|
+
search ? (
|
|
19
|
+
<IconButton overlay label={search.label} onClick={search.onOpen}>
|
|
20
|
+
<Search className="size-3.5" />
|
|
21
|
+
</IconButton>
|
|
22
|
+
) : null
|
|
23
|
+
}
|
|
24
|
+
zoom={{
|
|
25
|
+
onZoomIn: () => void zoomIn({ duration: 150 }),
|
|
26
|
+
onZoomOut: () => void zoomOut({ duration: 150 }),
|
|
27
|
+
onFit,
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</ViewerToolbar>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|