@magicborn/dialogue-forge 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.
Files changed (241) hide show
  1. package/README.md +233 -0
  2. package/bin/dialogue-forge.js +78 -0
  3. package/demo/app/layout.tsx +36 -0
  4. package/demo/app/page.tsx +440 -0
  5. package/demo/components/ThemeSwitcher.tsx +611 -0
  6. package/demo/next.config.mjs +7 -0
  7. package/demo/package.json +29 -0
  8. package/demo/postcss.config.mjs +7 -0
  9. package/demo/public/logo.svg +1 -0
  10. package/demo/styles/globals.css +19 -0
  11. package/demo/tailwind.config.ts +90 -0
  12. package/demo/tsconfig.json +42 -0
  13. package/dist/components/ChoiceEdgeV2.d.ts +3 -0
  14. package/dist/components/ChoiceEdgeV2.js +103 -0
  15. package/dist/components/CodeBlock.d.ts +8 -0
  16. package/dist/components/CodeBlock.js +24 -0
  17. package/dist/components/ConditionAutocomplete.d.ts +14 -0
  18. package/dist/components/ConditionAutocomplete.js +284 -0
  19. package/dist/components/ConditionalNodeV2.d.ts +16 -0
  20. package/dist/components/ConditionalNodeV2.js +147 -0
  21. package/dist/components/DialogueEditorV2.d.ts +22 -0
  22. package/dist/components/DialogueEditorV2.js +1170 -0
  23. package/dist/components/EdgeIcon.d.ts +8 -0
  24. package/dist/components/EdgeIcon.js +13 -0
  25. package/dist/components/ExampleLoader.d.ts +11 -0
  26. package/dist/components/ExampleLoader.js +52 -0
  27. package/dist/components/ExampleLoaderButton.d.ts +15 -0
  28. package/dist/components/ExampleLoaderButton.js +102 -0
  29. package/dist/components/FlagManager.d.ts +11 -0
  30. package/dist/components/FlagManager.js +282 -0
  31. package/dist/components/FlagSelector.d.ts +11 -0
  32. package/dist/components/FlagSelector.js +235 -0
  33. package/dist/components/GuidePanel.d.ts +7 -0
  34. package/dist/components/GuidePanel.js +1176 -0
  35. package/dist/components/Minimap.d.ts +16 -0
  36. package/dist/components/Minimap.js +93 -0
  37. package/dist/components/NPCEdgeV2.d.ts +3 -0
  38. package/dist/components/NPCEdgeV2.js +104 -0
  39. package/dist/components/NPCNodeV2.d.ts +26 -0
  40. package/dist/components/NPCNodeV2.js +86 -0
  41. package/dist/components/NodeEditor.d.ts +18 -0
  42. package/dist/components/NodeEditor.js +1025 -0
  43. package/dist/components/PlayView.d.ts +12 -0
  44. package/dist/components/PlayView.js +307 -0
  45. package/dist/components/PlayerNodeV2.d.ts +16 -0
  46. package/dist/components/PlayerNodeV2.js +139 -0
  47. package/dist/components/ReactFlowPOC.d.ts +61 -0
  48. package/dist/components/ReactFlowPOC.js +312 -0
  49. package/dist/components/ScenePlayer.d.ts +18 -0
  50. package/dist/components/ScenePlayer.js +196 -0
  51. package/dist/components/YarnView.d.ts +9 -0
  52. package/dist/components/YarnView.js +45 -0
  53. package/dist/components/ZoomControls.d.ts +11 -0
  54. package/dist/components/ZoomControls.js +34 -0
  55. package/dist/esm/components/ChoiceEdgeV2.d.ts +3 -0
  56. package/dist/esm/components/ChoiceEdgeV2.js +67 -0
  57. package/dist/esm/components/CodeBlock.d.ts +8 -0
  58. package/dist/esm/components/CodeBlock.js +18 -0
  59. package/dist/esm/components/ConditionAutocomplete.d.ts +14 -0
  60. package/dist/esm/components/ConditionAutocomplete.js +248 -0
  61. package/dist/esm/components/ConditionalNodeV2.d.ts +16 -0
  62. package/dist/esm/components/ConditionalNodeV2.js +111 -0
  63. package/dist/esm/components/DialogueEditorV2.d.ts +22 -0
  64. package/dist/esm/components/DialogueEditorV2.js +1134 -0
  65. package/dist/esm/components/EdgeIcon.d.ts +8 -0
  66. package/dist/esm/components/EdgeIcon.js +7 -0
  67. package/dist/esm/components/ExampleLoader.d.ts +11 -0
  68. package/dist/esm/components/ExampleLoader.js +46 -0
  69. package/dist/esm/components/ExampleLoaderButton.d.ts +15 -0
  70. package/dist/esm/components/ExampleLoaderButton.js +66 -0
  71. package/dist/esm/components/FlagManager.d.ts +11 -0
  72. package/dist/esm/components/FlagManager.js +246 -0
  73. package/dist/esm/components/FlagSelector.d.ts +11 -0
  74. package/dist/esm/components/FlagSelector.js +199 -0
  75. package/dist/esm/components/GuidePanel.d.ts +7 -0
  76. package/dist/esm/components/GuidePanel.js +1140 -0
  77. package/dist/esm/components/Minimap.d.ts +16 -0
  78. package/dist/esm/components/Minimap.js +57 -0
  79. package/dist/esm/components/NPCEdgeV2.d.ts +3 -0
  80. package/dist/esm/components/NPCEdgeV2.js +68 -0
  81. package/dist/esm/components/NPCNodeV2.d.ts +26 -0
  82. package/dist/esm/components/NPCNodeV2.js +80 -0
  83. package/dist/esm/components/NodeEditor.d.ts +18 -0
  84. package/dist/esm/components/NodeEditor.js +989 -0
  85. package/dist/esm/components/PlayView.d.ts +12 -0
  86. package/dist/esm/components/PlayView.js +271 -0
  87. package/dist/esm/components/PlayerNodeV2.d.ts +16 -0
  88. package/dist/esm/components/PlayerNodeV2.js +103 -0
  89. package/dist/esm/components/ReactFlowPOC.d.ts +61 -0
  90. package/dist/esm/components/ReactFlowPOC.js +275 -0
  91. package/dist/esm/components/ScenePlayer.d.ts +18 -0
  92. package/dist/esm/components/ScenePlayer.js +160 -0
  93. package/dist/esm/components/YarnView.d.ts +9 -0
  94. package/dist/esm/components/YarnView.js +39 -0
  95. package/dist/esm/components/ZoomControls.d.ts +11 -0
  96. package/dist/esm/components/ZoomControls.js +28 -0
  97. package/dist/esm/examples/example-loader.d.ts +29 -0
  98. package/dist/esm/examples/example-loader.js +103 -0
  99. package/dist/esm/examples/examples-registry.d.ts +38 -0
  100. package/dist/esm/examples/examples-registry.js +153 -0
  101. package/dist/esm/examples/index.d.ts +26 -0
  102. package/dist/esm/examples/index.js +50 -0
  103. package/dist/esm/examples/legacy-examples.d.ts +9 -0
  104. package/dist/esm/examples/legacy-examples.js +814 -0
  105. package/dist/esm/examples/yarn-examples.d.ts +35 -0
  106. package/dist/esm/examples/yarn-examples.js +181 -0
  107. package/dist/esm/index.d.ts +21 -0
  108. package/dist/esm/index.js +26 -0
  109. package/dist/esm/lib/flag-manager.d.ts +21 -0
  110. package/dist/esm/lib/flag-manager.js +93 -0
  111. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  112. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.js +169 -0
  113. package/dist/esm/lib/yarn-converter.d.ts +17 -0
  114. package/dist/esm/lib/yarn-converter.js +521 -0
  115. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  116. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.js +171 -0
  117. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  118. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.js +237 -0
  119. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  120. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.js +106 -0
  121. package/dist/esm/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  122. package/dist/esm/lib/yarn-runner/condition-evaluator.js +56 -0
  123. package/dist/esm/lib/yarn-runner/index.d.ts +12 -0
  124. package/dist/esm/lib/yarn-runner/index.js +11 -0
  125. package/dist/esm/lib/yarn-runner/node-processor.d.ts +18 -0
  126. package/dist/esm/lib/yarn-runner/node-processor.js +129 -0
  127. package/dist/esm/lib/yarn-runner/variable-manager.d.ts +51 -0
  128. package/dist/esm/lib/yarn-runner/variable-manager.js +120 -0
  129. package/dist/esm/lib/yarn-runner/variable-operations.d.ts +16 -0
  130. package/dist/esm/lib/yarn-runner/variable-operations.js +88 -0
  131. package/dist/esm/types/conditionals.d.ts +29 -0
  132. package/dist/esm/types/conditionals.js +1 -0
  133. package/dist/esm/types/constants.d.ts +59 -0
  134. package/dist/esm/types/constants.js +55 -0
  135. package/dist/esm/types/flags.d.ts +49 -0
  136. package/dist/esm/types/flags.js +49 -0
  137. package/dist/esm/types/game-state.d.ts +62 -0
  138. package/dist/esm/types/game-state.js +6 -0
  139. package/dist/esm/types/index.d.ts +77 -0
  140. package/dist/esm/types/index.js +1 -0
  141. package/dist/esm/utils/constants.d.ts +5 -0
  142. package/dist/esm/utils/constants.js +5 -0
  143. package/dist/esm/utils/feature-flags.d.ts +11 -0
  144. package/dist/esm/utils/feature-flags.js +11 -0
  145. package/dist/esm/utils/game-state-flattener.d.ts +41 -0
  146. package/dist/esm/utils/game-state-flattener.js +135 -0
  147. package/dist/esm/utils/layout/collision.d.ts +27 -0
  148. package/dist/esm/utils/layout/collision.js +74 -0
  149. package/dist/esm/utils/layout/index.d.ts +82 -0
  150. package/dist/esm/utils/layout/index.js +98 -0
  151. package/dist/esm/utils/layout/registry.d.ts +91 -0
  152. package/dist/esm/utils/layout/registry.js +148 -0
  153. package/dist/esm/utils/layout/strategies/dagre.d.ts +19 -0
  154. package/dist/esm/utils/layout/strategies/dagre.js +182 -0
  155. package/dist/esm/utils/layout/strategies/force.d.ts +21 -0
  156. package/dist/esm/utils/layout/strategies/force.js +178 -0
  157. package/dist/esm/utils/layout/strategies/grid.d.ts +17 -0
  158. package/dist/esm/utils/layout/strategies/grid.js +91 -0
  159. package/dist/esm/utils/layout/strategies/index.d.ts +8 -0
  160. package/dist/esm/utils/layout/strategies/index.js +8 -0
  161. package/dist/esm/utils/layout/types.d.ts +100 -0
  162. package/dist/esm/utils/layout/types.js +7 -0
  163. package/dist/esm/utils/layout.d.ts +9 -0
  164. package/dist/esm/utils/layout.js +17 -0
  165. package/dist/esm/utils/node-helpers.d.ts +7 -0
  166. package/dist/esm/utils/node-helpers.js +94 -0
  167. package/dist/esm/utils/reactflow-converter.d.ts +42 -0
  168. package/dist/esm/utils/reactflow-converter.js +217 -0
  169. package/dist/examples/example-loader.d.ts +29 -0
  170. package/dist/examples/example-loader.js +109 -0
  171. package/dist/examples/examples-registry.d.ts +38 -0
  172. package/dist/examples/examples-registry.js +160 -0
  173. package/dist/examples/index.d.ts +26 -0
  174. package/dist/examples/index.js +63 -0
  175. package/dist/examples/legacy-examples.d.ts +9 -0
  176. package/dist/examples/legacy-examples.js +817 -0
  177. package/dist/examples/yarn-examples.d.ts +35 -0
  178. package/dist/examples/yarn-examples.js +189 -0
  179. package/dist/index.d.ts +21 -0
  180. package/dist/index.js +66 -0
  181. package/dist/lib/flag-manager.d.ts +21 -0
  182. package/dist/lib/flag-manager.js +99 -0
  183. package/dist/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  184. package/dist/lib/yarn-converter/__tests__/round-trip.test.js +171 -0
  185. package/dist/lib/yarn-converter.d.ts +17 -0
  186. package/dist/lib/yarn-converter.js +525 -0
  187. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  188. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.js +173 -0
  189. package/dist/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  190. package/dist/lib/yarn-runner/__tests__/node-processor.test.js +239 -0
  191. package/dist/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  192. package/dist/lib/yarn-runner/__tests__/variable-manager.test.js +108 -0
  193. package/dist/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  194. package/dist/lib/yarn-runner/condition-evaluator.js +60 -0
  195. package/dist/lib/yarn-runner/index.d.ts +12 -0
  196. package/dist/lib/yarn-runner/index.js +21 -0
  197. package/dist/lib/yarn-runner/node-processor.d.ts +18 -0
  198. package/dist/lib/yarn-runner/node-processor.js +133 -0
  199. package/dist/lib/yarn-runner/variable-manager.d.ts +51 -0
  200. package/dist/lib/yarn-runner/variable-manager.js +124 -0
  201. package/dist/lib/yarn-runner/variable-operations.d.ts +16 -0
  202. package/dist/lib/yarn-runner/variable-operations.js +92 -0
  203. package/dist/types/conditionals.d.ts +29 -0
  204. package/dist/types/conditionals.js +2 -0
  205. package/dist/types/constants.d.ts +59 -0
  206. package/dist/types/constants.js +58 -0
  207. package/dist/types/flags.d.ts +49 -0
  208. package/dist/types/flags.js +52 -0
  209. package/dist/types/game-state.d.ts +62 -0
  210. package/dist/types/game-state.js +7 -0
  211. package/dist/types/index.d.ts +77 -0
  212. package/dist/types/index.js +2 -0
  213. package/dist/utils/constants.d.ts +5 -0
  214. package/dist/utils/constants.js +8 -0
  215. package/dist/utils/feature-flags.d.ts +11 -0
  216. package/dist/utils/feature-flags.js +14 -0
  217. package/dist/utils/game-state-flattener.d.ts +41 -0
  218. package/dist/utils/game-state-flattener.js +140 -0
  219. package/dist/utils/layout/collision.d.ts +27 -0
  220. package/dist/utils/layout/collision.js +77 -0
  221. package/dist/utils/layout/index.d.ts +82 -0
  222. package/dist/utils/layout/index.js +109 -0
  223. package/dist/utils/layout/registry.d.ts +91 -0
  224. package/dist/utils/layout/registry.js +151 -0
  225. package/dist/utils/layout/strategies/dagre.d.ts +19 -0
  226. package/dist/utils/layout/strategies/dagre.js +189 -0
  227. package/dist/utils/layout/strategies/force.d.ts +21 -0
  228. package/dist/utils/layout/strategies/force.js +182 -0
  229. package/dist/utils/layout/strategies/grid.d.ts +17 -0
  230. package/dist/utils/layout/strategies/grid.js +95 -0
  231. package/dist/utils/layout/strategies/index.d.ts +8 -0
  232. package/dist/utils/layout/strategies/index.js +14 -0
  233. package/dist/utils/layout/types.d.ts +100 -0
  234. package/dist/utils/layout/types.js +8 -0
  235. package/dist/utils/layout.d.ts +9 -0
  236. package/dist/utils/layout.js +25 -0
  237. package/dist/utils/node-helpers.d.ts +7 -0
  238. package/dist/utils/node-helpers.js +101 -0
  239. package/dist/utils/reactflow-converter.d.ts +42 -0
  240. package/dist/utils/reactflow-converter.js +223 -0
  241. package/package.json +70 -0
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react';
2
+ import { BaseEdge, getSmoothStepPath, getBezierPath, Position } from 'reactflow';
3
+ export function ChoiceEdgeV2({ id, sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, data, selected, }) {
4
+ const [hovered, setHovered] = useState(false);
5
+ const isBackEdge = data?.isBackEdge ?? false;
6
+ // Use smooth step path for angular look (like the horizontal example)
7
+ // For back edges, use bezier for a more curved appearance
8
+ const [edgePath, labelX, labelY] = isBackEdge
9
+ ? getBezierPath({
10
+ sourceX,
11
+ sourceY,
12
+ sourcePosition,
13
+ targetX,
14
+ targetY,
15
+ targetPosition,
16
+ curvature: 0.5,
17
+ })
18
+ : getSmoothStepPath({
19
+ sourceX,
20
+ sourceY,
21
+ sourcePosition: sourcePosition || Position.Bottom,
22
+ targetX,
23
+ targetY,
24
+ targetPosition: targetPosition || Position.Top,
25
+ borderRadius: 8,
26
+ });
27
+ const choiceIndex = data?.choiceIndex ?? 0;
28
+ // Map choice index to CSS variable
29
+ const choiceColorVar = `var(--color-df-edge-choice-${Math.min(choiceIndex % 5, 4) + 1})`;
30
+ // Use loop color for back edges, otherwise use choice color
31
+ const colorVar = isBackEdge ? 'var(--color-df-edge-loop)' : choiceColorVar;
32
+ const isSelected = selected || hovered;
33
+ const isDimmed = data?.isDimmed ?? false;
34
+ // Make edge thicker and more opaque when hovered or selected
35
+ // Dim edges not in path when highlighting is on
36
+ const strokeWidth = isSelected ? 4 : 2;
37
+ const opacity = isDimmed ? 0.15 : (isSelected ? 1 : 0.7);
38
+ // Use dimmed color when dimmed
39
+ const strokeColor = isDimmed ? 'var(--color-df-edge-dimmed)' : colorVar;
40
+ // Add glow effect when hovered (only if not dimmed)
41
+ const filter = hovered && !isDimmed ? `drop-shadow(0 0 4px ${colorVar})` : undefined;
42
+ // For pulse animation, we'll use a slightly brighter version
43
+ // Since we can't easily brighten CSS variables, we'll use the same color with higher opacity
44
+ const pulseColor = colorVar;
45
+ const shouldAnimate = data?.isInPathToSelected ?? false;
46
+ return (React.createElement(React.Fragment, null,
47
+ React.createElement("path", { d: edgePath, fill: "none", stroke: "transparent", strokeWidth: 20, style: { cursor: 'pointer', pointerEvents: 'all' }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false) }),
48
+ React.createElement(BaseEdge, { id: id, path: edgePath, style: {
49
+ stroke: strokeColor,
50
+ strokeWidth,
51
+ opacity,
52
+ cursor: 'pointer',
53
+ transition: 'all 0.2s ease',
54
+ pointerEvents: 'none',
55
+ filter,
56
+ // Dashed line for back edges
57
+ strokeDasharray: isBackEdge ? '8 4' : undefined,
58
+ }, markerEnd: isDimmed ? undefined : `url(#react-flow__arrowclosed-choice-${choiceIndex})` }),
59
+ isBackEdge && (React.createElement("g", { transform: `translate(${labelX - 10}, ${labelY - 10})` },
60
+ React.createElement("circle", { cx: "10", cy: "10", r: "12", fill: "var(--color-df-base)", stroke: strokeColor, strokeWidth: "2" }),
61
+ React.createElement("text", { x: "10", y: "14", textAnchor: "middle", fontSize: "12", fill: strokeColor }, "\u21BA"))),
62
+ shouldAnimate && (React.createElement("circle", { r: "6", fill: pulseColor, opacity: 0.9 },
63
+ React.createElement("animateMotion", { dur: "2s", repeatCount: "indefinite", path: edgePath }))),
64
+ React.createElement("defs", null,
65
+ React.createElement("marker", { id: `react-flow__arrowclosed-choice-${choiceIndex}`, markerWidth: "12.5", markerHeight: "12.5", viewBox: "-10 -10 20 20", markerUnits: "strokeWidth", orient: "auto", refX: "0", refY: "0" },
66
+ React.createElement("polyline", { stroke: colorVar, strokeLinecap: "round", strokeLinejoin: "round", fill: colorVar, points: "-5,-4 0,0 -5,4 -5,-4" })))));
67
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface CodeBlockProps {
3
+ code: string;
4
+ language?: string;
5
+ className?: string;
6
+ }
7
+ export declare function CodeBlock({ code, language, className }: CodeBlockProps): React.JSX.Element;
8
+ export {};
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ export function CodeBlock({ code, language = 'typescript', className = '' }) {
5
+ return (React.createElement("div", { className: `bg-[#12121a] rounded border border-[#2a2a3e] overflow-hidden ${className}` },
6
+ React.createElement(SyntaxHighlighter, { language: language, style: vscDarkPlus, customStyle: {
7
+ margin: 0,
8
+ padding: '1rem',
9
+ background: '#12121a',
10
+ fontSize: '0.75rem',
11
+ lineHeight: '1.5',
12
+ borderRadius: 0,
13
+ }, codeTagProps: {
14
+ style: {
15
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
16
+ }
17
+ }, PreTag: "div" }, code)));
18
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { FlagSchema } from '../types/flags';
3
+ interface ConditionAutocompleteProps {
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ onBlur?: () => void;
7
+ placeholder?: string;
8
+ className?: string;
9
+ style?: React.CSSProperties;
10
+ flagSchema?: FlagSchema;
11
+ textarea?: boolean;
12
+ }
13
+ export declare function ConditionAutocomplete({ value, onChange, onBlur, placeholder, className, style, flagSchema, textarea }: ConditionAutocompleteProps): React.JSX.Element;
14
+ export {};
@@ -0,0 +1,248 @@
1
+ import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
+ import { BookOpen, Trophy, Package, TrendingUp, Crown, Globe, MessageSquare } from 'lucide-react';
3
+ function getFlagIcon(flagType) {
4
+ switch (flagType) {
5
+ case 'quest':
6
+ return BookOpen;
7
+ case 'achievement':
8
+ return Trophy;
9
+ case 'item':
10
+ return Package;
11
+ case 'stat':
12
+ return TrendingUp;
13
+ case 'title':
14
+ return Crown;
15
+ case 'global':
16
+ return Globe;
17
+ case 'dialogue':
18
+ default:
19
+ return MessageSquare;
20
+ }
21
+ }
22
+ function getFlagColorClasses(flagType) {
23
+ switch (flagType) {
24
+ case 'quest':
25
+ return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
26
+ case 'achievement':
27
+ return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
28
+ case 'item':
29
+ return 'bg-green-500/20 text-green-400 border-green-500/30';
30
+ case 'stat':
31
+ return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
32
+ case 'title':
33
+ return 'bg-pink-500/20 text-pink-400 border-pink-500/30';
34
+ case 'global':
35
+ return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
36
+ case 'dialogue':
37
+ default:
38
+ return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
39
+ }
40
+ }
41
+ export function ConditionAutocomplete({ value, onChange, onBlur, placeholder, className = '', style, flagSchema, textarea = false }) {
42
+ const [showSuggestions, setShowSuggestions] = useState(false);
43
+ const [selectedIndex, setSelectedIndex] = useState(0);
44
+ const [cursorPosition, setCursorPosition] = useState(0);
45
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
46
+ const containerRef = useRef(null);
47
+ const inputRef = useRef(null);
48
+ const debounceTimerRef = useRef(null);
49
+ // Extract the current word being typed (for autocomplete matching)
50
+ const currentWord = useMemo(() => {
51
+ const textBeforeCursor = value.substring(0, cursorPosition);
52
+ const match = textBeforeCursor.match(/(\$?\w*)$/);
53
+ return match ? match[1] : '';
54
+ }, [value, cursorPosition]);
55
+ // Debounce search term for better performance
56
+ useEffect(() => {
57
+ if (debounceTimerRef.current) {
58
+ clearTimeout(debounceTimerRef.current);
59
+ }
60
+ debounceTimerRef.current = setTimeout(() => {
61
+ setDebouncedSearchTerm(currentWord);
62
+ }, 300); // 300ms debounce for smoother experience
63
+ return () => {
64
+ if (debounceTimerRef.current) {
65
+ clearTimeout(debounceTimerRef.current);
66
+ }
67
+ };
68
+ }, [currentWord]);
69
+ // Generate suggestions based on current input (using debounced search term)
70
+ const suggestions = useMemo(() => {
71
+ const result = [];
72
+ const word = debouncedSearchTerm.toLowerCase();
73
+ const isTypingVariable = word.startsWith('$') || word.length > 0;
74
+ // If typing a variable (starts with $ or has text), show flags
75
+ if (flagSchema && (isTypingVariable || word === '')) {
76
+ const searchTerm = word.startsWith('$') ? word.substring(1) : word;
77
+ flagSchema.flags.forEach(flag => {
78
+ const flagId = flag.id.toLowerCase();
79
+ const flagName = flag.name.toLowerCase();
80
+ // Match if search term is empty (showing $ or empty field) or matches flag id/name
81
+ if (!searchTerm || flagId.includes(searchTerm) || flagName.includes(searchTerm)) {
82
+ result.push({
83
+ type: 'flag',
84
+ label: `$${flag.id}`, // Show with dollar sign
85
+ insert: `$${flag.id}`,
86
+ description: flag.name,
87
+ flagType: flag.type
88
+ });
89
+ }
90
+ });
91
+ }
92
+ // Show operators and keywords when user has typed $ or is actively typing
93
+ if (word === '$' || (word.startsWith('$') && word.length > 1)) {
94
+ // Operators - only show when user has typed $ and is looking for what comes next
95
+ result.push({ type: 'operator', label: '==', insert: ' == ', description: 'Equals' }, { type: 'operator', label: '!=', insert: ' != ', description: 'Not equals' }, { type: 'operator', label: '>=', insert: ' >= ', description: 'Greater than or equal' }, { type: 'operator', label: '<=', insert: ' <= ', description: 'Less than or equal' }, { type: 'operator', label: '>', insert: ' > ', description: 'Greater than' }, { type: 'operator', label: '<', insert: ' < ', description: 'Less than' });
96
+ // Keywords
97
+ result.push({ type: 'keyword', label: 'and', insert: ' and ', description: 'Logical AND' }, { type: 'keyword', label: 'not', insert: 'not ', description: 'Logical NOT' });
98
+ }
99
+ // Limit results
100
+ return result.slice(0, 20);
101
+ }, [debouncedSearchTerm, flagSchema]);
102
+ // Handle input change
103
+ const handleChange = (e) => {
104
+ const newValue = e.target.value;
105
+ const newCursorPos = e.target.selectionStart || 0;
106
+ // Mark that user has actively typed
107
+ setHasActivelyTyped(true);
108
+ onChange(newValue);
109
+ setCursorPosition(newCursorPos);
110
+ };
111
+ // Track if user has actively typed in this session (not just focused on existing content)
112
+ const [hasActivelyTyped, setHasActivelyTyped] = useState(false);
113
+ // Update suggestions visibility when value or cursor changes
114
+ // Show suggestions when typing $ or when actively typing
115
+ useEffect(() => {
116
+ const textBeforeCursor = value.substring(0, cursorPosition);
117
+ const match = textBeforeCursor.match(/(\$?\w*)$/);
118
+ const word = match ? match[1] : '';
119
+ // Show suggestions when:
120
+ // 1. User is typing a variable (starts with $) OR
121
+ // 2. User has actively typed something and we have suggestions OR
122
+ // 3. Field is empty and user just focused (allow showing on empty field when typing $)
123
+ const isTypingVariable = word.startsWith('$') || word === '$';
124
+ const isEmpty = value.trim() === '';
125
+ const shouldShow = (hasActivelyTyped || isTypingVariable || isEmpty) && suggestions.length > 0;
126
+ setShowSuggestions(shouldShow);
127
+ }, [value, cursorPosition, suggestions.length, hasActivelyTyped]);
128
+ // Handle selection change (for cursor tracking)
129
+ const handleSelect = (e) => {
130
+ const target = e.currentTarget;
131
+ setCursorPosition(target.selectionStart || 0);
132
+ };
133
+ // Handle keydown
134
+ const handleKeyDown = (e) => {
135
+ if (!showSuggestions || suggestions.length === 0) {
136
+ if (e.key === 'Escape' && onBlur) {
137
+ onBlur();
138
+ }
139
+ return;
140
+ }
141
+ if (e.key === 'ArrowDown') {
142
+ e.preventDefault();
143
+ setSelectedIndex(prev => (prev + 1) % suggestions.length);
144
+ }
145
+ else if (e.key === 'ArrowUp') {
146
+ e.preventDefault();
147
+ setSelectedIndex(prev => (prev - 1 + suggestions.length) % suggestions.length);
148
+ }
149
+ else if (e.key === 'Enter' || e.key === 'Tab') {
150
+ e.preventDefault();
151
+ insertSuggestion(suggestions[selectedIndex]);
152
+ }
153
+ else if (e.key === 'Escape') {
154
+ setShowSuggestions(false);
155
+ }
156
+ };
157
+ // Insert suggestion at cursor position
158
+ const insertSuggestion = useCallback((suggestion) => {
159
+ const textBeforeCursor = value.substring(0, cursorPosition);
160
+ const textAfterCursor = value.substring(cursorPosition);
161
+ // Find the start of the current word
162
+ const wordMatch = textBeforeCursor.match(/(\$?\w*)$/);
163
+ const wordStart = wordMatch ? cursorPosition - wordMatch[1].length : cursorPosition;
164
+ // Replace the current word with the suggestion
165
+ const newText = value.substring(0, wordStart) +
166
+ suggestion.insert +
167
+ textAfterCursor;
168
+ onChange(newText);
169
+ // Set cursor position after inserted text
170
+ const newCursorPos = wordStart + suggestion.insert.length;
171
+ setTimeout(() => {
172
+ if (inputRef.current) {
173
+ inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
174
+ inputRef.current.focus();
175
+ }
176
+ }, 0);
177
+ setShowSuggestions(false);
178
+ setSelectedIndex(0);
179
+ }, [value, cursorPosition, onChange]);
180
+ // Handle drag start for draggable suggestions
181
+ const handleDragStart = useCallback((e, suggestion) => {
182
+ e.dataTransfer.setData('text/plain', suggestion.insert);
183
+ e.dataTransfer.effectAllowed = 'copy';
184
+ }, []);
185
+ // Handle click outside
186
+ useEffect(() => {
187
+ const handleClickOutside = (event) => {
188
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
189
+ setShowSuggestions(false);
190
+ }
191
+ };
192
+ document.addEventListener('mousedown', handleClickOutside);
193
+ return () => document.removeEventListener('mousedown', handleClickOutside);
194
+ }, []);
195
+ const InputComponent = textarea ? 'textarea' : 'input';
196
+ const inputProps = {
197
+ ref: inputRef,
198
+ type: textarea ? undefined : 'text',
199
+ value,
200
+ onChange: handleChange,
201
+ onSelect: handleSelect,
202
+ onKeyDown: handleKeyDown,
203
+ onFocus: () => {
204
+ if (inputRef.current) {
205
+ setCursorPosition(inputRef.current.selectionStart || 0);
206
+ }
207
+ // Don't reset hasActivelyTyped on focus - allow showing suggestions if field is empty or has $
208
+ // This allows suggestions to show when user focuses and types $
209
+ },
210
+ onBlur: () => {
211
+ // Delay to allow click on suggestion
212
+ setTimeout(() => {
213
+ if (onBlur)
214
+ onBlur();
215
+ }, 200);
216
+ },
217
+ placeholder,
218
+ className,
219
+ };
220
+ if (style) {
221
+ inputProps.style = style;
222
+ }
223
+ return (React.createElement("div", { className: "relative", ref: containerRef },
224
+ React.createElement(InputComponent, { ...inputProps }),
225
+ showSuggestions && suggestions.length > 0 && (React.createElement("div", { className: "absolute z-50 mt-1 w-full bg-[#1a1a2e] border border-[#2a2a3e] rounded-lg shadow-xl max-h-64 overflow-y-auto" }, suggestions.map((suggestion, idx) => {
226
+ const isSelected = idx === selectedIndex;
227
+ const IconComponent = suggestion.type === 'flag' && suggestion.flagType
228
+ ? getFlagIcon(suggestion.flagType)
229
+ : null;
230
+ // Get color classes for operators and keywords
231
+ const operatorColorClasses = 'bg-purple-500/20 text-purple-400 border-purple-500/30';
232
+ const keywordColorClasses = 'bg-blue-500/20 text-blue-400 border-blue-500/30';
233
+ return (React.createElement("button", { key: `${suggestion.type}-${suggestion.label}-${idx}`, type: "button", onClick: () => insertSuggestion(suggestion), onMouseEnter: () => setSelectedIndex(idx), draggable: true, onDragStart: (e) => handleDragStart(e, suggestion), className: `w-full text-left px-3 py-2 text-sm flex items-center gap-2 transition-colors ${isSelected
234
+ ? 'bg-[#2a2a3e] text-white'
235
+ : 'text-gray-300 hover:bg-[#252538]'}`, style: { minHeight: 'auto', height: 'auto' } },
236
+ suggestion.type === 'flag' && suggestion.flagType && (React.createElement(React.Fragment, null,
237
+ IconComponent && (React.createElement(IconComponent, { size: 14, className: `flex-shrink-0 ${getFlagColorClasses(suggestion.flagType).split(' ')[1]}` })),
238
+ React.createElement("span", { className: `text-xs px-2 py-1 rounded border flex-shrink-0 font-medium whitespace-nowrap ${getFlagColorClasses(suggestion.flagType)}` }, suggestion.label),
239
+ suggestion.description && (React.createElement("span", { className: "text-[10px] text-gray-500 truncate flex-1 min-w-0 ml-1" }, suggestion.description)),
240
+ React.createElement("span", { className: `text-[10px] px-1.5 py-0.5 rounded border flex-shrink-0 whitespace-nowrap ${getFlagColorClasses(suggestion.flagType)}` }, suggestion.flagType === 'dialogue' ? 'temp' : suggestion.flagType))),
241
+ suggestion.type === 'operator' && (React.createElement(React.Fragment, null,
242
+ React.createElement("span", { className: `text-xs px-2 py-1 rounded border flex-shrink-0 font-mono font-medium whitespace-nowrap ${operatorColorClasses}` }, suggestion.label),
243
+ suggestion.description && (React.createElement("span", { className: "text-[10px] text-gray-500 truncate flex-1 min-w-0 ml-1" }, suggestion.description)))),
244
+ suggestion.type === 'keyword' && (React.createElement(React.Fragment, null,
245
+ React.createElement("span", { className: `text-xs px-2 py-1 rounded border flex-shrink-0 font-mono font-medium whitespace-nowrap ${keywordColorClasses}` }, suggestion.label),
246
+ suggestion.description && (React.createElement("span", { className: "text-[10px] text-gray-500 truncate flex-1 min-w-0 ml-1" }, suggestion.description))))));
247
+ })))));
248
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { NodeProps } from 'reactflow';
3
+ import { DialogueNode } from '../types';
4
+ import { FlagSchema } from '../types/flags';
5
+ import { LayoutDirection } from '../utils/layout';
6
+ interface ConditionalNodeData {
7
+ node: DialogueNode;
8
+ flagSchema?: FlagSchema;
9
+ isDimmed?: boolean;
10
+ isInPath?: boolean;
11
+ layoutDirection?: LayoutDirection;
12
+ isStartNode?: boolean;
13
+ isEndNode?: boolean;
14
+ }
15
+ export declare function ConditionalNodeV2({ data, selected }: NodeProps<ConditionalNodeData>): React.JSX.Element;
16
+ export {};
@@ -0,0 +1,111 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
3
+ import { GitBranch, Play, Flag } from 'lucide-react';
4
+ // Color scheme for conditional block edges
5
+ const CONDITIONAL_COLORS = ['#3b82f6', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
6
+ export function ConditionalNodeV2({ data, selected }) {
7
+ const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
8
+ const blocks = node.conditionalBlocks || [];
9
+ const updateNodeInternals = useUpdateNodeInternals();
10
+ const headerRef = useRef(null);
11
+ const blockRefs = useRef([]);
12
+ const [handlePositions, setHandlePositions] = useState([]);
13
+ // Handle positions based on layout direction
14
+ const isHorizontal = layoutDirection === 'LR';
15
+ const targetPosition = isHorizontal ? Position.Left : Position.Top;
16
+ // Calculate handle positions based on actual rendered heights
17
+ useEffect(() => {
18
+ if (headerRef.current && blocks.length > 0) {
19
+ const positions = [];
20
+ const headerHeight = headerRef.current.offsetHeight;
21
+ let cumulativeHeight = headerHeight;
22
+ blocks.forEach((_block, idx) => {
23
+ const blockEl = blockRefs.current[idx];
24
+ if (blockEl) {
25
+ const blockHeight = blockEl.offsetHeight;
26
+ const handleY = cumulativeHeight + (blockHeight / 2);
27
+ positions.push(handleY);
28
+ cumulativeHeight += blockHeight;
29
+ }
30
+ else {
31
+ // Fallback: estimate height
32
+ const estimatedHeight = 40;
33
+ const handleY = cumulativeHeight + (estimatedHeight / 2);
34
+ positions.push(handleY);
35
+ cumulativeHeight += estimatedHeight;
36
+ }
37
+ });
38
+ setHandlePositions(positions);
39
+ setTimeout(() => {
40
+ updateNodeInternals(node.id);
41
+ }, 0);
42
+ }
43
+ }, [blocks.length, node.id, updateNodeInternals]);
44
+ // Border color based on state
45
+ const borderClass = selected
46
+ ? 'border-df-conditional-border shadow-lg shadow-df-glow'
47
+ : isStartNode
48
+ ? 'border-df-start shadow-md'
49
+ : isEndNode
50
+ ? 'border-df-end shadow-md'
51
+ : 'border-df-conditional-border';
52
+ return (React.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-conditional-border/70' : ''} bg-df-conditional-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
53
+ isStartNode && (React.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
54
+ React.createElement(Play, { size: 8, fill: "currentColor" }),
55
+ " START")),
56
+ isEndNode && (React.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
57
+ React.createElement(Flag, { size: 8 }),
58
+ " END")),
59
+ React.createElement(Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
60
+ React.createElement("div", { ref: headerRef, className: "px-3 py-1.5 border-b border-df-control-border bg-df-conditional-header flex items-center gap-2 rounded-t-lg" },
61
+ React.createElement(GitBranch, { size: 12, className: "text-df-conditional-border" }),
62
+ React.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
63
+ React.createElement("span", { className: "text-[10px] text-df-conditional-border" }, "IF/ELSE")),
64
+ React.createElement("div", { className: "py-1" }, blocks.map((block, idx) => {
65
+ const color = CONDITIONAL_COLORS[idx % CONDITIONAL_COLORS.length];
66
+ const blockType = block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE';
67
+ return (React.createElement("div", { key: block.id, ref: el => { blockRefs.current[idx] = el; }, className: "px-3 py-1.5 border-b border-df-control-border last:border-b-0" },
68
+ React.createElement("div", { className: "flex items-center gap-2 mb-1" },
69
+ React.createElement("span", { className: "text-[9px] px-1.5 py-0.5 rounded bg-df-base text-df-text-primary font-semibold" }, blockType),
70
+ block.condition && block.condition.length > 0 && (React.createElement("span", { className: "text-[9px] text-df-text-secondary font-mono truncate flex-1" }, block.condition.map((c) => {
71
+ const varName = `$${c.flag}`;
72
+ if (c.operator === 'is_set')
73
+ return varName;
74
+ if (c.operator === 'is_not_set')
75
+ return `not ${varName}`;
76
+ if (c.value !== undefined) {
77
+ const op = c.operator === 'equals' ? '==' :
78
+ c.operator === 'not_equals' ? '!=' :
79
+ c.operator === 'greater_than' ? '>' :
80
+ c.operator === 'less_than' ? '<' :
81
+ c.operator === 'greater_equal' ? '>=' :
82
+ c.operator === 'less_equal' ? '<=' : '==';
83
+ const value = typeof c.value === 'string' ? `"${c.value}"` : c.value;
84
+ return `${varName} ${op} ${value}`;
85
+ }
86
+ return '';
87
+ }).filter(c => c).join(' and ').slice(0, 30)))),
88
+ block.speaker && (React.createElement("div", { className: "text-[9px] text-df-conditional-border font-medium" }, block.speaker)),
89
+ React.createElement("div", { className: "text-[10px] text-df-text-primary line-clamp-1 bg-df-base border border-df-control-border rounded px-2 py-1 mt-1" },
90
+ "\"",
91
+ block.content.slice(0, 40) + (block.content.length > 40 ? '...' : ''),
92
+ "\""),
93
+ React.createElement(Handle, { type: "source", position: Position.Right, id: `block-${idx}`, style: {
94
+ top: `${handlePositions[idx] || 0}px`,
95
+ right: -8,
96
+ }, className: "!bg-[#2a2a3e] !border-[#4a4a6a] !w-3 !h-3 !rounded-full hover:!border-blue-400 hover:!bg-blue-400/20" })));
97
+ })),
98
+ node.setFlags && node.setFlags.length > 0 && (React.createElement("div", { className: "px-3 py-1 border-t border-[#2a2a3e] flex flex-wrap gap-1" }, node.setFlags.map(flagId => {
99
+ const flag = flagSchema?.flags.find(f => f.id === flagId);
100
+ const flagType = flag?.type || 'dialogue';
101
+ const colorClass = flagType === 'dialogue' ? 'bg-gray-500/20 text-gray-400 border-gray-500/30' :
102
+ flagType === 'quest' ? 'bg-blue-500/20 text-blue-400 border-blue-500/30' :
103
+ flagType === 'achievement' ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' :
104
+ flagType === 'item' ? 'bg-green-500/20 text-green-400 border-green-500/30' :
105
+ flagType === 'stat' ? 'bg-purple-500/20 text-purple-400 border-purple-500/30' :
106
+ flagType === 'title' ? 'bg-pink-500/20 text-pink-400 border-pink-500/30' :
107
+ flagType === 'global' ? 'bg-orange-500/20 text-orange-400 border-orange-500/30' :
108
+ 'bg-gray-500/20 text-gray-400 border-gray-500/30';
109
+ return (React.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
110
+ })))));
111
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Dialogue Editor V2 - React Flow Implementation
3
+ *
4
+ * This is the new version using React Flow for graph rendering.
5
+ * See V2_MIGRATION_PLAN.md for implementation details.
6
+ */
7
+ import React from 'react';
8
+ import 'reactflow/dist/style.css';
9
+ import { DialogueEditorProps } from '../types';
10
+ import { FlagSchema } from '../types/flags';
11
+ type ViewMode = 'graph' | 'yarn' | 'play';
12
+ export declare function DialogueEditorV2(props: DialogueEditorProps & {
13
+ flagSchema?: FlagSchema;
14
+ initialViewMode?: ViewMode;
15
+ viewMode?: ViewMode;
16
+ onViewModeChange?: (mode: ViewMode) => void;
17
+ layoutStrategy?: string;
18
+ onLayoutStrategyChange?: (strategy: string) => void;
19
+ onOpenFlagManager?: () => void;
20
+ onOpenGuide?: () => void;
21
+ }): React.JSX.Element;
22
+ export {};