1use serde_json::json;
2
3use super::{
4 NodeKitContentDensity, NodeKitFixture, NodeKitFixtureEdge, NodeKitFixtureNode,
5 NodeKitLayoutHints, NodeKitManifest, NodeKitRegistry,
6};
7use crate::schema::{
8 ActionIntent, ActionTarget, BlackboardDescriptor, InspectorDescriptor, InspectorTarget,
9 MenuDescriptor, MenuSurface, NodeActionDescriptor, NodeChromeDescriptor, NodeChromePlacement,
10 NodeControlBinding, NodeControlDescriptor, NodeControlEditability, NodeControlOption,
11 NodeControlOptionSource, NodeControlPresentation, NodeControlValidation,
12 NodeControlValidationRule, NodeRepeatableAnchorRule, NodeRepeatableCollectionDescriptor,
13 NodeSchema, NodeSurfaceLayoutBudget, NodeSurfaceOverflowIndicator, NodeSurfaceSlotDescriptor,
14 PortDecl, PortViewDescriptor,
15};
16use jellyflow_core::core::{
17 CanvasPoint, CanvasSize, EdgeKind, EdgeLabelAnchor, EdgeViewDescriptor, PortCapacity,
18 PortDirection, PortKind,
19};
20use jellyflow_core::types::TypeDesc;
21
22pub fn builtin_node_kits() -> NodeKitRegistry {
23 let mut registry = NodeKitRegistry::new();
24 registry.register(workflow_automation_manifest());
25 registry.register(shader_blueprint_manifest());
26 registry.register(erd_table_manifest());
27 registry.register(mind_map_knowledge_canvas_manifest());
28 registry
29}
30
31pub fn workflow_automation_manifest() -> NodeKitManifest {
32 NodeKitManifest::new("workflow.automation", "Workflow automation")
33 .with_supported_adapter("egui")
34 .with_supported_adapter("proof")
35 .with_supported_adapter("gpui")
36 .with_capability("workflow")
37 .with_capability("automation")
38 .with_layout_hints(
39 NodeKitLayoutHints::default()
40 .with_zoom_range(0.72, 0.9)
41 .with_field_spacing(10.0)
42 .with_action_spacing(8.0)
43 .with_measurement_note(
44 "Compact action rows and badge blocks should collapse at low zoom.",
45 ),
46 )
47 .recipe(workflow_start_schema())
48 .recipe(workflow_task_schema())
49 .recipe(workflow_trigger_schema())
50 .recipe(workflow_tool_schema())
51 .recipe(workflow_llm_schema())
52 .recipe(workflow_decision_schema())
53 .recipe(workflow_switch_schema())
54 .recipe(workflow_output_schema())
55 .recipe(workflow_return_schema())
56 .recipe(workflow_error_schema())
57 .fixture(workflow_fixture())
58}
59
60pub fn erd_table_manifest() -> NodeKitManifest {
61 NodeKitManifest::new("erd.table", "ERD table")
62 .with_supported_adapter("egui")
63 .with_supported_adapter("proof")
64 .with_supported_adapter("gpui")
65 .with_capability("erd")
66 .with_capability("table")
67 .with_layout_hints(
68 NodeKitLayoutHints::default()
69 .with_zoom_range(0.72, 0.88)
70 .with_field_spacing(6.0)
71 .with_measurement_note("Field rows should stay aligned to anchored ports."),
72 )
73 .recipe(erd_table_schema())
74 .fixture(erd_fixture())
75}
76
77pub fn shader_blueprint_manifest() -> NodeKitManifest {
78 NodeKitManifest::new("shader.blueprint", "Shader and blueprint")
79 .with_supported_adapter("egui")
80 .with_supported_adapter("proof")
81 .with_supported_adapter("gpui")
82 .with_capability("shader")
83 .with_capability("blueprint")
84 .with_layout_hints(
85 NodeKitLayoutHints::default()
86 .with_zoom_range(0.74, 0.92)
87 .with_field_spacing(6.0)
88 .with_measurement_note(
89 "Typed port rails and preview regions should preserve handle alignment.",
90 ),
91 )
92 .recipe(shader_texture_sample_schema())
93 .recipe(shader_mix_schema())
94 .fixture(shader_fixture())
95}
96
97pub fn mind_map_knowledge_canvas_manifest() -> NodeKitManifest {
98 NodeKitManifest::new("mind-map.knowledge-canvas", "Mind map and knowledge canvas")
99 .with_supported_adapter("egui")
100 .with_supported_adapter("proof")
101 .with_supported_adapter("gpui")
102 .with_capability("mind-map")
103 .with_capability("knowledge-canvas")
104 .with_layout_hints(
105 NodeKitLayoutHints::default()
106 .with_zoom_range(0.68, 0.88)
107 .with_measurement_note(
108 "Topic, summary, and source nodes should degrade to shells with stable anchors.",
109 ),
110 )
111 .recipe(mind_topic_schema())
112 .recipe(mind_idea_schema())
113 .recipe(source_card_schema())
114 .fixture(mind_map_fixture())
115}
116
117fn product_layout_budget(
118 min_readable_size: CanvasSize,
119 preferred_size: CanvasSize,
120 slot_line_budget: usize,
121 control_line_budget: usize,
122 repeatable_visible_items: Option<usize>,
123) -> NodeSurfaceLayoutBudget {
124 let budget = NodeSurfaceLayoutBudget::default()
125 .with_min_readable_size(min_readable_size)
126 .with_preferred_size(preferred_size)
127 .with_slot_line_budget(slot_line_budget)
128 .with_control_line_budget(control_line_budget)
129 .with_overflow_indicator(NodeSurfaceOverflowIndicator::Count)
130 .with_density_priority([
131 NodeKitContentDensity::Full,
132 NodeKitContentDensity::Regular,
133 NodeKitContentDensity::Compact,
134 ]);
135
136 if let Some(items) = repeatable_visible_items {
137 budget.with_repeatable_visible_items(items)
138 } else {
139 budget
140 }
141}
142
143fn workflow_trigger_schema() -> NodeSchema {
144 NodeSchema::builder("demo.trigger", "Trigger")
145 .category(["Automation", "Workflow"])
146 .keywords(["webhook", "schedule", "event"])
147 .renderer_key("data-card")
148 .default_size(CanvasSize {
149 width: 208.0,
150 height: 96.0,
151 })
152 .port(exec_output("event").on_right().with_view_group("exec"))
153 .port(
154 data_output("payload")
155 .on_bottom()
156 .with_view_group("data")
157 .with_view_order(1),
158 )
159 .default_data(json!({ "title": "Trigger", "summary": "Starts an automation" }))
160 .build()
161}
162
163fn workflow_start_schema() -> NodeSchema {
164 NodeSchema::builder("demo.start", "Start")
165 .category(["Workflow"])
166 .renderer_key("data-card")
167 .default_size(CanvasSize {
168 width: 176.0,
169 height: 80.0,
170 })
171 .port(output_port("out").on_right())
172 .default_data(json!({ "title": "Start", "summary": "Entry point" }))
173 .build()
174}
175
176fn workflow_task_schema() -> NodeSchema {
177 NodeSchema::builder("demo.task", "Task")
178 .category(["Workflow"])
179 .renderer_key("task-card")
180 .default_size(CanvasSize {
181 width: 208.0,
182 height: 96.0,
183 })
184 .port(PortDecl::data_input("in").with_label("in").on_left())
185 .port(PortDecl::data_output("out").with_label("out").on_right())
186 .default_data(json!({ "title": "Task", "summary": "Run a unit of work" }))
187 .build()
188}
189
190fn workflow_tool_schema() -> NodeSchema {
191 NodeSchema::builder("demo.tool", "Tool")
192 .category(["Automation", "Workflow"])
193 .keywords(["api", "function", "action"])
194 .renderer_key("task-card")
195 .default_size(CanvasSize {
196 width: 208.0,
197 height: 104.0,
198 })
199 .port(exec_input("in").on_left().with_view_group("exec"))
200 .port(exec_output("out").on_right().with_view_group("exec"))
201 .port(
202 data_input("args")
203 .on_top()
204 .with_view_group("data")
205 .with_view_order(0),
206 )
207 .port(
208 data_output("result")
209 .on_bottom()
210 .with_view_group("data")
211 .with_view_order(0),
212 )
213 .default_data(json!({ "title": "Tool", "summary": "Runs an external action" }))
214 .build()
215}
216
217fn workflow_llm_schema() -> NodeSchema {
218 NodeSchema::builder("demo.llm", "LLM")
219 .category(["Automation", "AI"])
220 .keywords(["prompt", "model", "dify"])
221 .renderer_key("decision-card")
222 .default_size(CanvasSize {
223 width: 228.0,
224 height: 196.0,
225 })
226 .layout_budget(product_layout_budget(
227 CanvasSize {
228 width: 340.0,
229 height: 288.0,
230 },
231 CanvasSize {
232 width: 364.0,
233 height: 312.0,
234 },
235 4,
236 3,
237 Some(3),
238 ))
239 .port(exec_input("in").on_left().with_view_group("exec"))
240 .port(exec_output("out").on_right().with_view_group("exec"))
241 .port(
242 data_input("prompt")
243 .on_top()
244 .with_view_anchor("field.prompt")
245 .with_view_group("parameters")
246 .with_view_order(0),
247 )
248 .port(
249 data_output("completion")
250 .on_bottom()
251 .with_view_anchor("field.completion")
252 .with_view_group("outputs")
253 .with_view_order(0),
254 )
255 .surface_slot(
256 NodeSurfaceSlotDescriptor::field_row("field.prompt")
257 .with_label("Prompt")
258 .with_slot("prompt")
259 .with_anchor("field.prompt")
260 .with_lane("parameters")
261 .with_order(0)
262 .with_control(
263 NodeControlDescriptor::text_area("control.prompt")
264 .with_label("Prompt")
265 .with_binding(NodeControlBinding::slot("prompt"))
266 .required()
267 .with_placeholder("Use {{ variable }} references"),
268 )
269 .with_control(
270 NodeControlDescriptor::variable_picker("control.prompt_variable")
271 .with_label("Variable")
272 .with_binding(NodeControlBinding::graph_symbol("workflow.inputs"))
273 .with_option_source(NodeControlOptionSource::Variables),
274 ),
275 )
276 .surface_slot(
277 NodeSurfaceSlotDescriptor::field_row("field.completion")
278 .with_label("Completion")
279 .with_slot("completion")
280 .with_anchor("field.completion")
281 .with_lane("outputs")
282 .with_order(1)
283 .with_control(
284 NodeControlDescriptor::text_area("control.completion")
285 .with_label("Completion")
286 .with_binding(NodeControlBinding::slot("completion"))
287 .read_only(),
288 ),
289 )
290 .surface_slot(
291 NodeSurfaceSlotDescriptor::badge("badge.model")
292 .with_label("Model")
293 .with_slot("meta.model")
294 .with_anchor("meta.model")
295 .with_order(0)
296 .with_control(llm_model_select_control()),
297 )
298 .surface_slot(
299 NodeSurfaceSlotDescriptor::metric_badge("metric.latency")
300 .with_label("Latency")
301 .with_slot("metrics.latency")
302 .with_anchor("metric.latency")
303 .with_order(1),
304 )
305 .surface_slot(
306 NodeSurfaceSlotDescriptor::config_group("config.model")
307 .with_label("Config")
308 .with_slot("config.model")
309 .with_anchor("config.model")
310 .with_order(1)
311 .with_controls([
312 llm_model_select_control(),
313 NodeControlDescriptor::slider("control.temperature")
314 .with_label("Temperature")
315 .with_binding(NodeControlBinding::data_path("config.model.temperature"))
316 .with_validation_rule(NodeControlValidationRule::Range {
317 min: Some(0.0),
318 max: Some(2.0),
319 })
320 .with_presentation(
321 NodeControlPresentation::default()
322 .with_unit("temp")
323 .compact_label(),
324 ),
325 NodeControlDescriptor::toggle("control.stream")
326 .with_label("Stream")
327 .with_binding(NodeControlBinding::data_path("config.model.stream")),
328 NodeControlDescriptor::asset("control.knowledge_base")
329 .with_label("Knowledge base")
330 .with_binding(NodeControlBinding::data_path("config.model.knowledge_base"))
331 .with_option_source(NodeControlOptionSource::Assets),
332 ]),
333 )
334 .surface_slot(
335 NodeSurfaceSlotDescriptor::nested_region("nested.policy")
336 .with_label("Policy")
337 .with_slot("nested.policy")
338 .with_anchor("nested.policy")
339 .with_order(2),
340 )
341 .surface_slot(
342 NodeSurfaceSlotDescriptor::status_banner("status.validation")
343 .with_label("Status")
344 .with_slot("status.validation")
345 .with_anchor("status.validation")
346 .with_order(3),
347 )
348 .surface_slot(
349 NodeSurfaceSlotDescriptor::action_row("actions.primary")
350 .with_label("Actions")
351 .with_slot("actions.primary")
352 .with_anchor("actions.primary")
353 .with_order(4)
354 .with_control(
355 NodeControlDescriptor::toggle("control.enable_run")
356 .with_label("Run enabled")
357 .with_binding(NodeControlBinding::data_path("actions.primary.enabled"))
358 .with_editability(
359 NodeControlEditability::default()
360 .disabled("Requires a valid model and prompt"),
361 ),
362 ),
363 )
364 .action(
365 NodeActionDescriptor::new(
366 "action.llm.run",
367 "Run",
368 ActionTarget::Node {
369 node_kind: "demo.llm".to_owned(),
370 },
371 ActionIntent::RunNode,
372 )
373 .with_group("primary")
374 .with_order(0)
375 .with_icon_key("play"),
376 )
377 .action(
378 NodeActionDescriptor::new(
379 "action.insert.llm",
380 "Insert LLM",
381 ActionTarget::DroppedWire {
382 source_port_key: Some("completion".to_owned()),
383 },
384 ActionIntent::InsertNode {
385 node_kind: "demo.llm".to_owned(),
386 },
387 )
388 .with_group("create")
389 .with_order(0),
390 )
391 .menu(
392 MenuDescriptor::new("menu.dropped_wire.llm", MenuSurface::DroppedWire)
393 .with_label("Insert compatible node")
394 .with_action_key("action.insert.llm"),
395 )
396 .menu(
397 MenuDescriptor::new("menu.node.llm", MenuSurface::Node)
398 .with_label("LLM")
399 .with_action_key("action.llm.run"),
400 )
401 .inspector(
402 InspectorDescriptor::new(
403 "inspector.llm",
404 InspectorTarget::Node {
405 node_kind: "demo.llm".to_owned(),
406 },
407 )
408 .with_label("LLM")
409 .with_control(
410 NodeControlDescriptor::select("inspector.model")
411 .with_label("Model")
412 .with_binding(NodeControlBinding::data_path("meta.model"))
413 .with_options([
414 NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
415 NodeControlOption::new("gpt-4.1", "GPT 4.1"),
416 ]),
417 )
418 .with_action_key("action.llm.run"),
419 )
420 .repeatable_collection(
421 NodeRepeatableCollectionDescriptor::new("llm.params", "params", "id")
422 .with_label("Parameters")
423 .with_empty_label("No parameters")
424 .with_item_template_slot(
425 NodeSurfaceSlotDescriptor::field_row("param")
426 .with_label("Parameter")
427 .with_slot("params")
428 .with_control(
429 NodeControlDescriptor::text_input("control.param.name")
430 .with_label("Name")
431 .with_binding(NodeControlBinding::data_path("name")),
432 )
433 .with_control(
434 NodeControlDescriptor::expression("control.param.value")
435 .with_label("Value")
436 .with_binding(NodeControlBinding::data_path("value"))
437 .with_validation_rule(NodeControlValidationRule::ExpressionShape {
438 language: Some("jinja".to_owned()),
439 }),
440 ),
441 )
442 .with_anchor_rule(NodeRepeatableAnchorRule::new("param", "param"))
443 .with_min_items(0)
444 .with_max_items(12)
445 .reorderable()
446 .with_add_action("action.param.add")
447 .with_remove_action("action.param.remove")
448 .with_reorder_action("action.param.reorder"),
449 )
450 .action(
451 NodeActionDescriptor::new(
452 "action.param.add",
453 "Add parameter",
454 ActionTarget::Node {
455 node_kind: "demo.llm".to_owned(),
456 },
457 ActionIntent::AddRepeatableItem {
458 collection_key: "llm.params".to_owned(),
459 },
460 )
461 .with_group("parameters"),
462 )
463 .action(
464 NodeActionDescriptor::new(
465 "action.param.remove",
466 "Remove parameter",
467 ActionTarget::Node {
468 node_kind: "demo.llm".to_owned(),
469 },
470 ActionIntent::RemoveRepeatableItem {
471 collection_key: "llm.params".to_owned(),
472 item_id: String::new(),
473 },
474 )
475 .with_group("parameters")
476 .danger(),
477 )
478 .action(
479 NodeActionDescriptor::new(
480 "action.param.reorder",
481 "Reorder parameter",
482 ActionTarget::Node {
483 node_kind: "demo.llm".to_owned(),
484 },
485 ActionIntent::ReorderRepeatableItem {
486 collection_key: "llm.params".to_owned(),
487 item_id: String::new(),
488 },
489 )
490 .with_group("parameters"),
491 )
492 .inspector(
493 InspectorDescriptor::new(
494 "inspector.param.topic",
495 InspectorTarget::RepeatableItem {
496 collection_key: "llm.params".to_owned(),
497 item_id: "topic".to_owned(),
498 },
499 )
500 .with_label("Parameter")
501 .with_control(
502 NodeControlDescriptor::expression("inspector.param.value")
503 .with_label("Value")
504 .with_binding(NodeControlBinding::data_path("value")),
505 ),
506 )
507 .chrome(NodeChromeDescriptor::resizer("resize.corner").with_order(0))
508 .chrome(
509 NodeChromeDescriptor::toolbar("toolbar.primary", NodeChromePlacement::TopRight)
510 .with_label("Node tools")
511 .with_renderer_key("node-toolbar")
512 .with_icon_key("settings")
513 .with_order(10),
514 )
515 .chrome(
516 NodeChromeDescriptor::status_strip("status.run", NodeChromePlacement::InsideFooter)
517 .with_label("Ready")
518 .with_renderer_key("run-status")
519 .with_icon_key("activity")
520 .with_order(20),
521 )
522 .chrome(
523 NodeChromeDescriptor::run_action_strip("actions.run", NodeChromePlacement::Bottom)
524 .with_label("Run")
525 .with_renderer_key("run-actions")
526 .with_icon_key("play")
527 .with_order(30),
528 )
529 .default_data(json!({
530 "title": "LLM",
531 "summary": "Prompt, model, tools, and variables",
532 "meta": {
533 "model": "gpt-4.1-mini"
534 },
535 "metrics": {
536 "latency": "420ms"
537 },
538 "config": {
539 "model": {
540 "temperature": 0.2,
541 "tools": "retrieval",
542 "stream": true,
543 "knowledge_base": "product-docs"
544 }
545 },
546 "status": {
547 "validation": "Ready"
548 },
549 "nested": {
550 "policy": {
551 "guardrails": "Block PII",
552 "response": "Return structured route"
553 }
554 },
555 "actions": {
556 "primary": {
557 "enabled": false,
558 "items": ["Test prompt", "Open trace", "Copy config"]
559 }
560 },
561 "fields": {
562 "prompt": "Customer intake + policy",
563 "completion": "Priority and route"
564 },
565 "params": [
566 { "id": "topic", "name": "topic", "value": "{{ customer.topic }}" },
567 { "id": "priority", "name": "priority", "value": "{{ intake.priority }}" }
568 ]
569 }))
570 .build()
571}
572
573fn llm_model_select_control() -> NodeControlDescriptor {
574 let model_values = vec![
575 json!("gpt-4.1-mini"),
576 json!("gpt-4.1"),
577 json!("local-llama"),
578 ];
579
580 NodeControlDescriptor::select("control.model")
581 .with_label("Model")
582 .with_binding(NodeControlBinding::data_path("meta.model"))
583 .with_options([
584 NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
585 NodeControlOption::new("gpt-4.1", "GPT 4.1"),
586 NodeControlOption::new("local-llama", "Local Llama"),
587 ])
588 .with_validation(NodeControlValidation::default().required().with_rule(
589 NodeControlValidationRule::EnumValues {
590 values: model_values,
591 },
592 ))
593}
594
595fn workflow_decision_schema() -> NodeSchema {
596 NodeSchema::builder("demo.decision", "Decision")
597 .category(["Workflow"])
598 .renderer_key("decision-card")
599 .default_size(CanvasSize {
600 width: 208.0,
601 height: 104.0,
602 })
603 .port(input_port("in"))
604 .port(output_port("yes").on_top().with_view_order(0))
605 .port(output_port("no").on_bottom().with_view_order(1))
606 .surface_slot(
607 NodeSurfaceSlotDescriptor::badge("badge.branch")
608 .with_label("Branch")
609 .with_slot("meta.branch")
610 .with_anchor("meta.branch")
611 .with_order(0),
612 )
613 .default_data(json!({ "title": "Decision", "summary": "Branch the flow" }))
614 .build()
615}
616
617fn workflow_switch_schema() -> NodeSchema {
618 NodeSchema::builder("demo.switch", "Switch")
619 .category(["Automation", "Workflow"])
620 .keywords(["branch", "condition", "router"])
621 .renderer_key("decision-card")
622 .default_size(CanvasSize {
623 width: 208.0,
624 height: 104.0,
625 })
626 .port(exec_input("in").on_left().with_view_group("exec"))
627 .port(exec_output("yes").on_top().with_view_order(0))
628 .port(exec_output("no").on_bottom().with_view_order(1))
629 .default_data(json!({ "title": "Switch", "summary": "Branch execution" }))
630 .build()
631}
632
633fn workflow_output_schema() -> NodeSchema {
634 NodeSchema::builder("demo.output", "Output")
635 .alias("demo.workflow_output")
636 .category(["Workflow"])
637 .renderer_key("output-card")
638 .default_size(CanvasSize {
639 width: 208.0,
640 height: 96.0,
641 })
642 .port(PortDecl::data_input("in").with_label("in").on_left())
643 .default_data(json!({ "title": "Output", "summary": "Publish the result" }))
644 .build()
645}
646
647fn workflow_return_schema() -> NodeSchema {
648 NodeSchema::builder("demo.workflow_output", "Workflow output")
649 .category(["Automation", "Workflow"])
650 .keywords(["return", "response", "result"])
651 .renderer_key("output-card")
652 .default_size(CanvasSize {
653 width: 208.0,
654 height: 96.0,
655 })
656 .port(exec_input("in").on_left().with_view_group("exec"))
657 .port(
658 data_input("result")
659 .on_top()
660 .with_view_group("data")
661 .with_view_order(0),
662 )
663 .default_data(json!({ "title": "Workflow output", "summary": "Returns data to caller" }))
664 .build()
665}
666
667fn workflow_error_schema() -> NodeSchema {
668 NodeSchema::builder("demo.error", "Error")
669 .category(["Automation", "Workflow"])
670 .keywords(["failure", "retry", "fallback"])
671 .renderer_key("output-card")
672 .default_size(CanvasSize {
673 width: 208.0,
674 height: 96.0,
675 })
676 .port(exec_input("error").on_left().with_view_group("exec"))
677 .port(exec_output("out").on_right().with_view_group("exec"))
678 .default_data(json!({ "title": "Error path", "summary": "Retry or recover" }))
679 .build()
680}
681
682fn erd_table_schema() -> NodeSchema {
683 NodeSchema::builder("demo.table", "Table")
684 .category(["ERD"])
685 .keywords(["database", "schema", "relation"])
686 .renderer_key("table-card")
687 .default_size(CanvasSize {
688 width: 226.0,
689 height: 186.0,
690 })
691 .layout_budget(product_layout_budget(
692 CanvasSize {
693 width: 420.0,
694 height: 330.0,
695 },
696 CanvasSize {
697 width: 448.0,
698 height: 356.0,
699 },
700 4,
701 2,
702 Some(4),
703 ))
704 .port(
705 data_input("fk")
706 .with_label("foreign key")
707 .on_left()
708 .with_view_anchor("field.foreign_key")
709 .with_view_group("fields")
710 .with_view_order(0),
711 )
712 .port(
713 data_output("pk")
714 .with_label("primary key")
715 .on_right()
716 .with_view_anchor("field.primary_key")
717 .with_view_group("fields")
718 .with_view_order(0),
719 )
720 .surface_slot(
721 NodeSurfaceSlotDescriptor::field_row("field.primary_key")
722 .with_label("Primary key")
723 .with_slot("primary_key")
724 .with_anchor("field.primary_key")
725 .with_lane("fields")
726 .with_order(0)
727 .with_control(
728 NodeControlDescriptor::text_input("control.primary_key.name")
729 .with_label("Name")
730 .with_binding(NodeControlBinding::slot("primary_key"))
731 .required(),
732 ),
733 )
734 .surface_slot(
735 NodeSurfaceSlotDescriptor::field_row("field.field")
736 .with_label("Field")
737 .with_slot("field")
738 .with_anchor("field.field")
739 .with_lane("fields")
740 .with_order(1)
741 .with_controls([
742 NodeControlDescriptor::text_input("control.field.name")
743 .with_label("Name")
744 .with_binding(NodeControlBinding::slot("field")),
745 NodeControlDescriptor::select("control.field.type")
746 .with_label("Type")
747 .with_binding(NodeControlBinding::data_path("schema.field.type"))
748 .with_options([
749 NodeControlOption::new("text", "Text"),
750 NodeControlOption::new("integer", "Integer"),
751 NodeControlOption::new("uuid", "UUID"),
752 ]),
753 ]),
754 )
755 .surface_slot(
756 NodeSurfaceSlotDescriptor::field_row("field.foreign_key")
757 .with_label("Foreign key")
758 .with_slot("foreign_key")
759 .with_anchor("field.foreign_key")
760 .with_lane("fields")
761 .with_order(2)
762 .with_control(
763 NodeControlDescriptor::port_binding("control.foreign_key.binding")
764 .with_label("Relation")
765 .with_binding(NodeControlBinding::port_anchor("field.foreign_key")),
766 ),
767 )
768 .surface_slot(
769 NodeSurfaceSlotDescriptor::badge("badge.cardinality")
770 .with_label("1:N")
771 .with_slot("meta.cardinality")
772 .with_anchor("meta.cardinality")
773 .with_order(0),
774 )
775 .surface_slot(
776 NodeSurfaceSlotDescriptor::metric_badge("metric.rows")
777 .with_label("Rows")
778 .with_slot("metrics.rows")
779 .with_anchor("metric.rows")
780 .with_order(1),
781 )
782 .surface_slot(
783 NodeSurfaceSlotDescriptor::action_row("actions.table")
784 .with_label("Actions")
785 .with_slot("actions.table")
786 .with_anchor("actions.table")
787 .with_order(2),
788 )
789 .repeatable_collection(
790 NodeRepeatableCollectionDescriptor::new("table.columns", "columns", "id")
791 .with_label("Columns")
792 .with_empty_label("No columns")
793 .with_item_template_slot(
794 NodeSurfaceSlotDescriptor::field_row("column")
795 .with_label("Column")
796 .with_slot("columns")
797 .with_control(
798 NodeControlDescriptor::text_input("control.column.name")
799 .with_label("Name")
800 .with_binding(NodeControlBinding::data_path("name")),
801 )
802 .with_control(
803 NodeControlDescriptor::select("control.column.type")
804 .with_label("Type")
805 .with_binding(NodeControlBinding::data_path("ty"))
806 .with_options([
807 NodeControlOption::new("uuid", "UUID"),
808 NodeControlOption::new("text", "Text"),
809 NodeControlOption::new("integer", "Integer"),
810 ]),
811 ),
812 )
813 .with_anchor_rule(
814 NodeRepeatableAnchorRule::new("field.column", "field.column")
815 .with_port_key_path("port"),
816 )
817 .with_min_items(1)
818 .with_max_items(16)
819 .reorderable()
820 .with_add_action("action.column.add")
821 .with_remove_action("action.column.remove")
822 .with_reorder_action("action.column.reorder"),
823 )
824 .action(
825 NodeActionDescriptor::new(
826 "action.column.add",
827 "Add column",
828 ActionTarget::Node {
829 node_kind: "demo.table".to_owned(),
830 },
831 ActionIntent::AddRepeatableItem {
832 collection_key: "table.columns".to_owned(),
833 },
834 )
835 .with_group("columns")
836 .with_order(0),
837 )
838 .action(
839 NodeActionDescriptor::new(
840 "action.column.remove",
841 "Remove column",
842 ActionTarget::Node {
843 node_kind: "demo.table".to_owned(),
844 },
845 ActionIntent::RemoveRepeatableItem {
846 collection_key: "table.columns".to_owned(),
847 item_id: String::new(),
848 },
849 )
850 .with_group("columns")
851 .danger(),
852 )
853 .action(
854 NodeActionDescriptor::new(
855 "action.column.reorder",
856 "Reorder column",
857 ActionTarget::Node {
858 node_kind: "demo.table".to_owned(),
859 },
860 ActionIntent::ReorderRepeatableItem {
861 collection_key: "table.columns".to_owned(),
862 item_id: String::new(),
863 },
864 )
865 .with_group("columns"),
866 )
867 .menu(
868 MenuDescriptor::new("menu.table", MenuSurface::Node).with_action_keys([
869 "action.column.add",
870 "action.column.remove",
871 "action.column.reorder",
872 ]),
873 )
874 .inspector(
875 InspectorDescriptor::new(
876 "inspector.column.email",
877 InspectorTarget::RepeatableItem {
878 collection_key: "table.columns".to_owned(),
879 item_id: "email".to_owned(),
880 },
881 )
882 .with_label("Column")
883 .with_control(
884 NodeControlDescriptor::text_input("inspector.column.name")
885 .with_label("Name")
886 .with_binding(NodeControlBinding::data_path("name")),
887 ),
888 )
889 .default_data(json!({
890 "title": "Table",
891 "summary": "id · field · field",
892 "meta": { "cardinality": "1:N" },
893 "metrics": { "rows": "12k" },
894 "actions": { "table": ["Add column", "Inspect relation"] },
895 "field_order": ["primary_key", "field", "foreign_key"],
896 "fields": {
897 "primary_key": "id",
898 "field": "field",
899 "foreign_key": "field_id"
900 },
901 "schema": {
902 "field": {
903 "type": "text"
904 }
905 },
906 "columns": [
907 { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
908 { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
909 { "id": "user_id", "name": "user_id", "ty": "uuid", "port": "fk" }
910 ]
911 }))
912 .build()
913}
914
915fn shader_texture_sample_schema() -> NodeSchema {
916 NodeSchema::builder("demo.shader.texture_sample", "Texture Sample")
917 .category(["Shader", "Blueprint"])
918 .keywords(["shader", "texture", "unreal", "graph"])
919 .renderer_key("shader-card")
920 .default_size(CanvasSize {
921 width: 224.0,
922 height: 156.0,
923 })
924 .layout_budget(product_layout_budget(
925 CanvasSize {
926 width: 360.0,
927 height: 272.0,
928 },
929 CanvasSize {
930 width: 384.0,
931 height: 296.0,
932 },
933 3,
934 2,
935 Some(3),
936 ))
937 .port(
938 data_input("uv")
939 .with_label("UV")
940 .with_type(shader_vec(2))
941 .on_left()
942 .with_view_anchor("rail.inputs")
943 .with_view_group("typed_ports")
944 .with_view_order(0),
945 )
946 .port(
947 data_output("color")
948 .with_label("Color")
949 .with_type(shader_vec(4))
950 .on_right()
951 .with_view_anchor("rail.outputs")
952 .with_view_group("typed_ports")
953 .with_view_order(0),
954 )
955 .surface_slot(
956 NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
957 .with_label("Inputs")
958 .with_slot("ports.inputs")
959 .with_anchor("rail.inputs")
960 .with_lane("ports")
961 .with_order(0),
962 )
963 .surface_slot(
964 NodeSurfaceSlotDescriptor::preview("preview.texture")
965 .with_label("Preview")
966 .with_slot("preview.texture")
967 .with_anchor("preview.texture")
968 .with_order(1)
969 .with_control(
970 NodeControlDescriptor::asset("control.texture")
971 .with_label("Texture")
972 .with_binding(NodeControlBinding::data_path("preview.texture"))
973 .with_option_source(NodeControlOptionSource::Assets),
974 ),
975 )
976 .surface_slot(
977 NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
978 .with_label("Outputs")
979 .with_slot("ports.outputs")
980 .with_anchor("rail.outputs")
981 .with_lane("ports")
982 .with_order(2),
983 )
984 .default_data(json!({
985 "title": "Texture Sample",
986 "summary": "Samples albedo from UV",
987 "ports": {
988 "inputs": ["vec2 uv"],
989 "outputs": ["vec4 color"]
990 },
991 "preview": {
992 "texture": "checker"
993 }
994 }))
995 .build()
996}
997
998fn shader_mix_schema() -> NodeSchema {
999 let shader_properties =
1000 NodeRepeatableCollectionDescriptor::new("shader.properties", "properties", "id")
1001 .with_item_template_slot(
1002 NodeSurfaceSlotDescriptor::field_row("property")
1003 .with_label("Property")
1004 .with_slot("properties")
1005 .with_control(
1006 NodeControlDescriptor::text_input("control.property.name")
1007 .with_label("Name")
1008 .with_binding(NodeControlBinding::data_path("name")),
1009 ),
1010 )
1011 .with_anchor_rule(NodeRepeatableAnchorRule::new("property", "property"))
1012 .with_add_action("action.shader_property.add");
1013
1014 NodeSchema::builder("demo.shader.mix", "Mix")
1015 .category(["Shader", "Blueprint"])
1016 .keywords(["shader", "mix", "lerp", "blueprint"])
1017 .renderer_key("shader-card")
1018 .default_size(CanvasSize {
1019 width: 224.0,
1020 height: 168.0,
1021 })
1022 .layout_budget(product_layout_budget(
1023 CanvasSize {
1024 width: 360.0,
1025 height: 272.0,
1026 },
1027 CanvasSize {
1028 width: 392.0,
1029 height: 340.0,
1030 },
1031 3,
1032 2,
1033 Some(3),
1034 ))
1035 .port(
1036 data_input("a")
1037 .with_label("A")
1038 .with_type(shader_vec(4))
1039 .on_left()
1040 .with_view_anchor("rail.inputs")
1041 .with_view_group("typed_ports")
1042 .with_view_order(0),
1043 )
1044 .port(
1045 data_input("b")
1046 .with_label("B")
1047 .with_type(shader_vec(4))
1048 .on_left()
1049 .with_view_anchor("rail.inputs")
1050 .with_view_group("typed_ports")
1051 .with_view_order(1),
1052 )
1053 .port(
1054 data_input("factor")
1055 .with_label("Factor")
1056 .with_type(TypeDesc::Float)
1057 .on_bottom()
1058 .with_view_anchor("config.factor")
1059 .with_view_group("config")
1060 .with_view_order(2),
1061 )
1062 .port(
1063 data_output("result")
1064 .with_label("Result")
1065 .with_type(shader_vec(4))
1066 .on_right()
1067 .with_view_anchor("rail.outputs")
1068 .with_view_group("typed_ports")
1069 .with_view_order(0),
1070 )
1071 .surface_slot(
1072 NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
1073 .with_label("Inputs")
1074 .with_slot("ports.inputs")
1075 .with_anchor("rail.inputs")
1076 .with_lane("ports")
1077 .with_order(0),
1078 )
1079 .surface_slot(
1080 NodeSurfaceSlotDescriptor::config_group("config.factor")
1081 .with_label("Factor")
1082 .with_slot("config.factor")
1083 .with_anchor("config.factor")
1084 .with_order(1)
1085 .with_control(
1086 NodeControlDescriptor::slider("control.factor")
1087 .with_label("Factor")
1088 .with_binding(NodeControlBinding::data_path("config.factor.default"))
1089 .with_validation_rule(NodeControlValidationRule::Range {
1090 min: Some(0.0),
1091 max: Some(1.0),
1092 }),
1093 ),
1094 )
1095 .surface_slot(
1096 NodeSurfaceSlotDescriptor::preview("preview.result")
1097 .with_label("Preview")
1098 .with_slot("preview.result")
1099 .with_anchor("preview.result")
1100 .with_order(2),
1101 )
1102 .surface_slot(
1103 NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
1104 .with_label("Outputs")
1105 .with_slot("ports.outputs")
1106 .with_anchor("rail.outputs")
1107 .with_lane("ports")
1108 .with_order(3),
1109 )
1110 .repeatable_collection(
1111 NodeRepeatableCollectionDescriptor::new("shader.inputs", "dynamic_inputs", "id")
1112 .with_label("Dynamic inputs")
1113 .with_empty_label("No dynamic inputs")
1114 .with_item_template_slot(
1115 NodeSurfaceSlotDescriptor::port_rail("input")
1116 .with_label("Input")
1117 .with_slot("dynamic_inputs")
1118 .with_control(
1119 NodeControlDescriptor::select("control.shader_input.type")
1120 .with_label("Type")
1121 .with_binding(NodeControlBinding::data_path("ty"))
1122 .with_options([
1123 NodeControlOption::new("float", "Float"),
1124 NodeControlOption::new("vec2", "Vec2"),
1125 NodeControlOption::new("vec4", "Vec4"),
1126 ]),
1127 ),
1128 )
1129 .with_anchor_rule(
1130 NodeRepeatableAnchorRule::new("rail.inputs", "rail.inputs")
1131 .with_port_key_path("port"),
1132 )
1133 .with_min_items(2)
1134 .with_max_items(8)
1135 .reorderable()
1136 .with_add_action("action.shader_input.add")
1137 .with_remove_action("action.shader_input.remove")
1138 .with_reorder_action("action.shader_input.reorder"),
1139 )
1140 .repeatable_collection(shader_properties.clone())
1141 .action(
1142 NodeActionDescriptor::new(
1143 "action.shader_input.add",
1144 "Add input",
1145 ActionTarget::Node {
1146 node_kind: "demo.shader.mix".to_owned(),
1147 },
1148 ActionIntent::AddRepeatableItem {
1149 collection_key: "shader.inputs".to_owned(),
1150 },
1151 )
1152 .with_group("inputs"),
1153 )
1154 .action(
1155 NodeActionDescriptor::new(
1156 "action.shader_input.remove",
1157 "Remove input",
1158 ActionTarget::Node {
1159 node_kind: "demo.shader.mix".to_owned(),
1160 },
1161 ActionIntent::RemoveRepeatableItem {
1162 collection_key: "shader.inputs".to_owned(),
1163 item_id: String::new(),
1164 },
1165 )
1166 .with_group("inputs")
1167 .danger(),
1168 )
1169 .action(
1170 NodeActionDescriptor::new(
1171 "action.shader_input.reorder",
1172 "Reorder input",
1173 ActionTarget::Node {
1174 node_kind: "demo.shader.mix".to_owned(),
1175 },
1176 ActionIntent::ReorderRepeatableItem {
1177 collection_key: "shader.inputs".to_owned(),
1178 item_id: String::new(),
1179 },
1180 )
1181 .with_group("inputs"),
1182 )
1183 .action(
1184 NodeActionDescriptor::new(
1185 "action.shader_property.add",
1186 "Add property",
1187 ActionTarget::Blackboard {
1188 blackboard_key: "blackboard.shader.properties".to_owned(),
1189 },
1190 ActionIntent::AddRepeatableItem {
1191 collection_key: "shader.properties".to_owned(),
1192 },
1193 )
1194 .with_group("blackboard"),
1195 )
1196 .blackboard(
1197 BlackboardDescriptor::new(
1198 "blackboard.shader.properties",
1199 "Shader properties",
1200 shader_properties,
1201 )
1202 .with_action_key("action.shader_property.add"),
1203 )
1204 .default_data(json!({
1205 "title": "Mix",
1206 "summary": "Blend two color streams",
1207 "ports": {
1208 "inputs": ["vec4 a", "vec4 b"],
1209 "outputs": ["vec4 result"]
1210 },
1211 "config": {
1212 "factor": {
1213 "type": "float",
1214 "default": 0.5
1215 }
1216 },
1217 "preview": {
1218 "result": "gradient"
1219 },
1220 "dynamic_inputs": [
1221 { "id": "a", "name": "A", "ty": "vec4", "port": "a" },
1222 { "id": "b", "name": "B", "ty": "vec4", "port": "b" },
1223 { "id": "factor", "name": "Factor", "ty": "float", "port": "factor" }
1224 ],
1225 "properties": [
1226 { "id": "base_color", "name": "Base Color", "ty": "vec4" },
1227 { "id": "roughness", "name": "Roughness", "ty": "float" }
1228 ]
1229 }))
1230 .build()
1231}
1232
1233fn mind_topic_schema() -> NodeSchema {
1234 NodeSchema::builder("demo.topic", "Topic")
1235 .category(["Mind map"])
1236 .keywords(["mindnode", "margin-note", "knowledge"])
1237 .renderer_key("topic-card")
1238 .default_size(CanvasSize {
1239 width: 210.0,
1240 height: 96.0,
1241 })
1242 .layout_budget(product_layout_budget(
1243 CanvasSize {
1244 width: 320.0,
1245 height: 220.0,
1246 },
1247 CanvasSize {
1248 width: 344.0,
1249 height: 240.0,
1250 },
1251 3,
1252 2,
1253 None,
1254 ))
1255 .port(input_port("in"))
1256 .port(output_port("out"))
1257 .surface_slot(
1258 NodeSurfaceSlotDescriptor::header("header.main")
1259 .with_label("Topic")
1260 .with_slot("title")
1261 .with_order(0)
1262 .with_control(
1263 NodeControlDescriptor::text_input("control.topic.title")
1264 .with_label("Title")
1265 .with_binding(NodeControlBinding::slot("title")),
1266 ),
1267 )
1268 .surface_slot(
1269 NodeSurfaceSlotDescriptor::body("body.summary")
1270 .with_label("Summary")
1271 .with_slot("summary")
1272 .with_anchor("body.summary")
1273 .with_order(1)
1274 .with_control(
1275 NodeControlDescriptor::text_area("control.topic.summary")
1276 .with_label("Summary")
1277 .with_binding(NodeControlBinding::slot("summary")),
1278 ),
1279 )
1280 .default_data(json!({ "title": "Topic", "summary": "Central idea" }))
1281 .build()
1282}
1283
1284fn mind_idea_schema() -> NodeSchema {
1285 NodeSchema::builder("demo.idea", "Idea")
1286 .category(["Mind map"])
1287 .keywords(["branch", "note", "thought"])
1288 .renderer_key("idea-card")
1289 .default_size(CanvasSize {
1290 width: 176.0,
1291 height: 76.0,
1292 })
1293 .layout_budget(product_layout_budget(
1294 CanvasSize {
1295 width: 248.0,
1296 height: 132.0,
1297 },
1298 CanvasSize {
1299 width: 280.0,
1300 height: 156.0,
1301 },
1302 2,
1303 1,
1304 None,
1305 ))
1306 .port(input_port("in"))
1307 .port(output_port("out"))
1308 .surface_slot(
1309 NodeSurfaceSlotDescriptor::header("header.main")
1310 .with_label("Idea")
1311 .with_slot("title")
1312 .with_order(0)
1313 .with_control(
1314 NodeControlDescriptor::text_input("control.idea.title")
1315 .with_label("Title")
1316 .with_binding(NodeControlBinding::slot("title")),
1317 ),
1318 )
1319 .default_data(json!({ "title": "Idea", "summary": "Branch note" }))
1320 .build()
1321}
1322
1323fn source_card_schema() -> NodeSchema {
1324 NodeSchema::builder("demo.source", "Source")
1325 .category(["Knowledge"])
1326 .keywords(["paper", "quote", "annotation", "marginnote"])
1327 .renderer_key("source-card")
1328 .default_size(CanvasSize {
1329 width: 210.0,
1330 height: 92.0,
1331 })
1332 .layout_budget(product_layout_budget(
1333 CanvasSize {
1334 width: 328.0,
1335 height: 220.0,
1336 },
1337 CanvasSize {
1338 width: 352.0,
1339 height: 240.0,
1340 },
1341 3,
1342 2,
1343 None,
1344 ))
1345 .port(output_port("out"))
1346 .surface_slot(
1347 NodeSurfaceSlotDescriptor::header("header.main")
1348 .with_label("Source")
1349 .with_slot("title")
1350 .with_order(0)
1351 .with_control(
1352 NodeControlDescriptor::text_input("control.source.title")
1353 .with_label("Title")
1354 .with_binding(NodeControlBinding::slot("title")),
1355 ),
1356 )
1357 .surface_slot(
1358 NodeSurfaceSlotDescriptor::preview("preview.main")
1359 .with_label("Excerpt")
1360 .with_slot("preview")
1361 .with_anchor("preview.main")
1362 .with_order(1)
1363 .with_control(
1364 NodeControlDescriptor::asset("control.source.asset")
1365 .with_label("Source")
1366 .with_binding(NodeControlBinding::slot("preview"))
1367 .with_option_source(NodeControlOptionSource::Assets),
1368 ),
1369 )
1370 .default_data(json!({
1371 "title": "Source",
1372 "summary": "Evidence card",
1373 "preview": "Annotated excerpt"
1374 }))
1375 .build()
1376}
1377
1378fn workflow_fixture() -> NodeKitFixture {
1379 NodeKitFixture::new("workflow.review", "Workflow review")
1380 .with_description("A compact review loop with branch and output paths.")
1381 .node(
1382 NodeKitFixtureNode::new(
1383 "trigger",
1384 "demo.trigger",
1385 CanvasPoint {
1386 x: -320.0,
1387 y: -40.0,
1388 },
1389 )
1390 .with_data(json!({
1391 "title": "Trigger",
1392 "summary": "Starts an automation"
1393 })),
1394 )
1395 .node(
1396 NodeKitFixtureNode::new("tool", "demo.tool", CanvasPoint { x: -20.0, y: -80.0 })
1397 .with_data(json!({
1398 "title": "Tool",
1399 "summary": "Runs an external action"
1400 })),
1401 )
1402 .node(
1403 NodeKitFixtureNode::new(
1404 "decision",
1405 "demo.decision",
1406 CanvasPoint { x: 300.0, y: -80.0 },
1407 )
1408 .with_data(json!({
1409 "title": "Decision",
1410 "summary": "Branch the flow"
1411 })),
1412 )
1413 .node(
1414 NodeKitFixtureNode::new(
1415 "output",
1416 "demo.workflow_output",
1417 CanvasPoint { x: 620.0, y: -40.0 },
1418 )
1419 .with_data(json!({
1420 "title": "Workflow output",
1421 "summary": "Returns data to caller"
1422 })),
1423 )
1424 .edge(
1425 NodeKitFixtureEdge::new("trigger", "tool", EdgeKind::Exec)
1426 .with_from_port("event")
1427 .with_to_port("in")
1428 .with_data(json!({ "label": "event" }))
1429 .with_view(
1430 EdgeViewDescriptor::new()
1431 .with_label("event")
1432 .with_label_anchor(EdgeLabelAnchor::Center)
1433 .with_target_marker_key("arrow")
1434 .with_style_token("workflow"),
1435 ),
1436 )
1437 .edge(
1438 NodeKitFixtureEdge::new("tool", "decision", EdgeKind::Exec)
1439 .with_from_port("out")
1440 .with_to_port("in")
1441 .with_data(json!({ "label": "flow" })),
1442 )
1443 .edge(
1444 NodeKitFixtureEdge::new("decision", "output", EdgeKind::Exec)
1445 .with_from_port("yes")
1446 .with_to_port("in")
1447 .with_data(json!({ "label": "yes" })),
1448 )
1449 .expect_counts(4, 3)
1450}
1451
1452fn erd_fixture() -> NodeKitFixture {
1453 NodeKitFixture::new("erd.customer_orders", "Customer orders")
1454 .with_description("Three linked tables showing a classic 1:N relationship.")
1455 .node(
1456 NodeKitFixtureNode::new(
1457 "customers",
1458 "demo.table",
1459 CanvasPoint {
1460 x: -520.0,
1461 y: -80.0,
1462 },
1463 )
1464 .with_data(json!({
1465 "title": "customers",
1466 "summary": "id · email · plan_id",
1467 "field_order": ["primary_key", "field", "foreign_key"],
1468 "fields": {
1469 "primary_key": "id",
1470 "field": "email",
1471 "foreign_key": "plan_id"
1472 },
1473 "meta": { "cardinality": "1:N" },
1474 "metrics": { "rows": "42k" },
1475 "actions": { "table": ["Add column", "Inspect relation"] },
1476 "columns": [
1477 { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1478 { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
1479 { "id": "plan_id", "name": "plan_id", "ty": "uuid", "port": "fk" }
1480 ]
1481 })),
1482 )
1483 .node(
1484 NodeKitFixtureNode::new("orders", "demo.table", CanvasPoint { x: 0.0, y: -100.0 })
1485 .with_data(json!({
1486 "title": "orders",
1487 "summary": "id · customer_id · total",
1488 "field_order": ["primary_key", "field", "foreign_key"],
1489 "fields": {
1490 "primary_key": "id",
1491 "field": "total",
1492 "foreign_key": "customer_id"
1493 },
1494 "meta": { "cardinality": "1:N" },
1495 "metrics": { "rows": "94k" },
1496 "actions": { "table": ["Add column", "Inspect relation"] },
1497 "columns": [
1498 { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1499 { "id": "customer_id", "name": "customer_id", "ty": "uuid", "port": "fk" },
1500 { "id": "total", "name": "total", "ty": "integer", "port": "field_total" }
1501 ]
1502 })),
1503 )
1504 .node(
1505 NodeKitFixtureNode::new(
1506 "order_items",
1507 "demo.table",
1508 CanvasPoint { x: 520.0, y: -80.0 },
1509 )
1510 .with_data(json!({
1511 "title": "order_items",
1512 "summary": "id · order_id · sku_id · qty",
1513 "field_order": ["primary_key", "field", "foreign_key", "field"],
1514 "fields": {
1515 "primary_key": "id",
1516 "field": "qty",
1517 "foreign_key": "order_id"
1518 },
1519 "meta": { "cardinality": "1:N" },
1520 "metrics": { "rows": "320k" },
1521 "actions": { "table": ["Add column", "Inspect relation"] },
1522 "columns": [
1523 { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1524 { "id": "order_id", "name": "order_id", "ty": "uuid", "port": "fk" },
1525 { "id": "qty", "name": "qty", "ty": "integer", "port": "field_qty" }
1526 ]
1527 })),
1528 )
1529 .edge(
1530 NodeKitFixtureEdge::new("customers", "orders", EdgeKind::Data)
1531 .with_from_port("pk")
1532 .with_to_port("fk")
1533 .with_data(json!({ "label": "1:N" })),
1534 )
1535 .edge(
1536 NodeKitFixtureEdge::new("orders", "order_items", EdgeKind::Data)
1537 .with_from_port("pk")
1538 .with_to_port("fk")
1539 .with_data(json!({ "label": "1:N" })),
1540 )
1541 .expect_counts(3, 2)
1542}
1543
1544fn shader_fixture() -> NodeKitFixture {
1545 NodeKitFixture::new("shader.material_mix", "Material mix")
1546 .with_description("A compact shader graph with typed rails and preview slots.")
1547 .node(
1548 NodeKitFixtureNode::new(
1549 "texture",
1550 "demo.shader.texture_sample",
1551 CanvasPoint {
1552 x: -320.0,
1553 y: -60.0,
1554 },
1555 )
1556 .with_data(json!({
1557 "title": "Albedo",
1558 "summary": "Texture sample",
1559 "ports": {
1560 "inputs": ["vec2 uv"],
1561 "outputs": ["vec4 color"]
1562 },
1563 "preview": {
1564 "texture": "checker"
1565 }
1566 })),
1567 )
1568 .node(
1569 NodeKitFixtureNode::new("mix", "demo.shader.mix", CanvasPoint { x: 80.0, y: -60.0 })
1570 .with_data(json!({
1571 "title": "Mix",
1572 "summary": "Blend albedo with tint",
1573 "ports": {
1574 "inputs": ["vec4 albedo", "vec4 tint"],
1575 "outputs": ["vec4 result"]
1576 },
1577 "config": {
1578 "factor": {
1579 "type": "float",
1580 "default": 0.5
1581 }
1582 },
1583 "preview": {
1584 "result": "gradient"
1585 }
1586 })),
1587 )
1588 .edge(
1589 NodeKitFixtureEdge::new("texture", "mix", EdgeKind::Data)
1590 .with_from_port("color")
1591 .with_to_port("a")
1592 .with_data(json!({ "label": "vec4" })),
1593 )
1594 .expect_counts(2, 1)
1595}
1596
1597fn mind_map_fixture() -> NodeKitFixture {
1598 NodeKitFixture::new("mind-map.strategy", "Strategy map")
1599 .with_description("A radial map showing topic, ideas, and source cards.")
1600 .node(
1601 NodeKitFixtureNode::new("center", "demo.topic", CanvasPoint { x: 0.0, y: 0.0 })
1602 .with_data(json!({
1603 "title": "Product strategy",
1604 "summary": "MindNode-style radial map"
1605 })),
1606 )
1607 .node(
1608 NodeKitFixtureNode::new(
1609 "users",
1610 "demo.idea",
1611 CanvasPoint {
1612 x: 432.0,
1613 y: -128.0,
1614 },
1615 )
1616 .with_data(json!({
1617 "title": "Users",
1618 "summary": "Researchers, builders, editors"
1619 })),
1620 )
1621 .node(
1622 NodeKitFixtureNode::new("risks", "demo.idea", CanvasPoint { x: 432.0, y: 136.0 })
1623 .with_data(json!({
1624 "title": "Risks",
1625 "summary": "Trust, scale, migration"
1626 })),
1627 )
1628 .node(
1629 NodeKitFixtureNode::new("sources", "demo.source", CanvasPoint { x: -420.0, y: 24.0 })
1630 .with_data(json!({
1631 "title": "Source",
1632 "summary": "Evidence card"
1633 })),
1634 )
1635 .edge(
1636 NodeKitFixtureEdge::new("center", "users", EdgeKind::Data)
1637 .with_data(json!({ "label": "branch" })),
1638 )
1639 .edge(
1640 NodeKitFixtureEdge::new("center", "risks", EdgeKind::Data)
1641 .with_data(json!({ "label": "branch" })),
1642 )
1643 .edge(
1644 NodeKitFixtureEdge::new("sources", "center", EdgeKind::Data)
1645 .with_from_port("out")
1646 .with_to_port("in")
1647 .with_data(json!({ "label": "evidence" })),
1648 )
1649 .expect_counts(4, 3)
1650}
1651
1652fn input_port(key: &str) -> PortDecl {
1653 PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi)
1654 .with_label(key)
1655 .on_left()
1656}
1657
1658fn output_port(key: &str) -> PortDecl {
1659 PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi)
1660 .with_label(key)
1661 .with_view(PortViewDescriptor::right())
1662}
1663
1664fn data_input(key: &str) -> PortDecl {
1665 PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi).with_label(key)
1666}
1667
1668fn data_output(key: &str) -> PortDecl {
1669 PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi).with_label(key)
1670}
1671
1672fn shader_vec(width: u8) -> TypeDesc {
1673 TypeDesc::Opaque {
1674 key: format!("shader.vec{width}"),
1675 params: Vec::new(),
1676 }
1677}
1678
1679fn exec_input(key: &str) -> PortDecl {
1680 PortDecl::new(key, PortDirection::In, PortKind::Exec, PortCapacity::Single).with_label(key)
1681}
1682
1683fn exec_output(key: &str) -> PortDecl {
1684 PortDecl::new(key, PortDirection::Out, PortKind::Exec, PortCapacity::Multi).with_label(key)
1685}