From 76039bdd0eed8adec1ea052fa8ebda2bc5773a4e Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 9 Mar 2026 13:42:03 +0100 Subject: [PATCH] help --- package-lock.json | 56 +++++++++++++ package.json | 1 + .../graph/NodeFooterEdgeIndicators.tsx | 4 + src/components/graph/NodeHelpPopover.tsx | 35 ++++++++ src/components/ui/popover.tsx | 31 +++++++ src/lib/nodeHelp.tsx | 84 +++++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100644 src/components/graph/NodeHelpPopover.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/lib/nodeHelp.tsx diff --git a/package-lock.json b/package-lock.json index a2b5f76..73d9935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -2047,6 +2048,61 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", diff --git a/package.json b/package.json index 73f26ba..4b3ac4a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", diff --git a/src/components/graph/NodeFooterEdgeIndicators.tsx b/src/components/graph/NodeFooterEdgeIndicators.tsx index d2c3648..039603a 100644 --- a/src/components/graph/NodeFooterEdgeIndicators.tsx +++ b/src/components/graph/NodeFooterEdgeIndicators.tsx @@ -1,6 +1,7 @@ import React, { useContext, useMemo } from 'react' import FlowContext from '../../lib/flowContext' import { ArrowDownLeft, ArrowUpRight } from 'lucide-react' +import { NodeHelpPopover } from './NodeHelpPopover' const NODE_HAS_INPUT: Record = { config: true, @@ -58,6 +59,9 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props) {children} )} + + + ) } diff --git a/src/components/graph/NodeHelpPopover.tsx b/src/components/graph/NodeHelpPopover.tsx new file mode 100644 index 0000000..be49ceb --- /dev/null +++ b/src/components/graph/NodeHelpPopover.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { HelpCircle } from 'lucide-react' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' +import { getNodeHelp, type NodeType } from '../../lib/nodeHelp' +import { cn } from '../../lib/utils' + +type Props = { + nodeType: NodeType + className?: string +} + +export function NodeHelpPopover({ nodeType, className }: Props) { + const { title, content } = getNodeHelp(nodeType) + + return ( + + + + + +

{title}

+
{content}
+
+
+ ) +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..fdcb511 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/lib/nodeHelp.tsx b/src/lib/nodeHelp.tsx new file mode 100644 index 0000000..c0d94f8 --- /dev/null +++ b/src/lib/nodeHelp.tsx @@ -0,0 +1,84 @@ +import type React from 'react' + +export type NodeType = 'config' | 'render' | 'variable' | 'function' + +export type NodeHelpEntry = { + title: string + content: React.ReactNode +} + +const Code = ({ children }: { children: React.ReactNode }) => ( + {children} +) + +const Section = ({ title, children }: { title: string; children: React.ReactNode }) => ( +
+

{title}

+
{children}
+
+) + +export const NODE_HELP: Record = { + config: { + title: 'Config node', + content: ( + <> +
+

Config nodes hold PlantUML + Nunjucks template content. Connect one config to a Render node to display the diagram. Use the editor to write @startuml blocks and Nunjucks tags ({'{{ }}'}, {'{% %}'}).

+
+
+

From another config, reference this template:

+
    +
  • {'{% extends "configId" %}'} — inherit layout
  • +
  • {'{% include "configId" %}'} — inline content
  • +
  • {'{% import "configId" as alias %}'} — use as macro namespace
  • +
+

Replace configId with this node’s id or its title. Connect variables/functions to this config; they are available as {'{{ varId }}'} and {'{{ x | fnId }}'} in the template.

+
+ + ), + }, + render: { + title: 'Renderer node', + content: ( + <> +
+

Connect a single Config node (input) to this Render node. It resolves the config’s PlantUML + Nunjucks (variables, function filters, extends/include), sends the result to the diagram service, and shows the SVG here.

+
+
+

Renderer nodes are terminal: they only consume configs. They are not referenced from other nodes. To reuse a diagram, reference the Config node from another Config (extends/include), then connect that config to a Render node.

+
+ + ), + }, + variable: { + title: 'Variable node', + content: ( + <> +
+

Variables hold a value (string, number, or boolean). Connect a Variable node to a Config node to expose it in that config’s template.

+
+
+

In a Config template connected to this variable, use {'{{ '}nodeId{' }}'} where nodeId is this node’s id. Example: if the variable node id is var_001, write {'{{ var_001 }}'} in the config.

+
+ + ), + }, + function: { + title: 'Function node', + content: ( + <> +
+

Functions are Nunjucks custom filters. Write a body using either named parameters (function(num, x, kwargs) { ... }) or the args array. Connect this node to a Config to use the filter in that config’s template.

+
+
+

In a Config template, use the filter syntax: {'{{ value | '}nodeId{' }}'} or {'{{ value | '}nodeId{'(arg1, key=val) }}'}. The first argument is the value before |; extra arguments and keyword args are passed as in Nunjucks. Return a value or a Promise for async filters.

+
+ + ), + }, +} + +export function getNodeHelp(nodeType: NodeType): NodeHelpEntry { + return NODE_HELP[nodeType] ?? { title: nodeType, content: null } +}