@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,1025 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NodeEditor = NodeEditor;
37
+ const react_1 = __importStar(require("react"));
38
+ const FlagSelector_1 = require("./FlagSelector");
39
+ const constants_1 = require("../types/constants");
40
+ const lucide_react_1 = require("lucide-react");
41
+ const reactflow_converter_1 = require("../utils/reactflow-converter");
42
+ const EdgeIcon_1 = require("./EdgeIcon");
43
+ const ConditionAutocomplete_1 = require("./ConditionAutocomplete");
44
+ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }) {
45
+ // Local state for condition input values (keyed by block id for conditional blocks, choice id for choices)
46
+ const [conditionInputs, setConditionInputs] = (0, react_1.useState)({});
47
+ const [debouncedConditionInputs, setDebouncedConditionInputs] = (0, react_1.useState)({});
48
+ const [editingCondition, setEditingCondition] = (0, react_1.useState)(null);
49
+ const [debouncedEditingValue, setDebouncedEditingValue] = (0, react_1.useState)('');
50
+ const [dismissedConditions, setDismissedConditions] = (0, react_1.useState)(new Set()); // Track which conditions have been dismissed
51
+ const [expandedConditions, setExpandedConditions] = (0, react_1.useState)(new Set()); // Track which conditions are manually opened
52
+ const prevNodeIdRef = (0, react_1.useRef)(node.id);
53
+ const initializedBlocksRef = (0, react_1.useRef)(new Set());
54
+ const initializedChoicesRef = (0, react_1.useRef)(new Set());
55
+ const debounceTimersRef = (0, react_1.useRef)({});
56
+ const editingDebounceTimerRef = (0, react_1.useRef)(null);
57
+ // Validation function for condition expressions
58
+ const validateCondition = (0, react_1.useMemo)(() => {
59
+ return (conditionStr) => {
60
+ const errors = [];
61
+ const warnings = [];
62
+ if (!conditionStr.trim()) {
63
+ return { isValid: true, errors: [], warnings: [] }; // Empty is valid (optional)
64
+ }
65
+ // Check for basic syntax issues
66
+ const parts = conditionStr.split(/\s+and\s+/i);
67
+ let hasValidPart = false;
68
+ parts.forEach((part, idx) => {
69
+ part = part.trim();
70
+ if (!part)
71
+ return;
72
+ // Check if it's a valid condition pattern (including literals for always-true/false)
73
+ const patterns = [
74
+ /^not\s+\$(\w+)$/, // not $flag
75
+ /^\$(\w+)\s*>=\s*(.+)$/, // $flag >= value
76
+ /^\$(\w+)\s*<=\s*(.+)$/, // $flag <= value
77
+ /^\$(\w+)\s*!=\s*(.+)$/, // $flag != value
78
+ /^\$(\w+)\s*==\s*(.+)$/, // $flag == value
79
+ /^\$(\w+)\s*>\s*(.+)$/, // $flag > value
80
+ /^\$(\w+)\s*<\s*(.+)$/, // $flag < value
81
+ /^\$(\w+)$/, // $flag
82
+ // Allow literal comparisons (for always-true/false expressions)
83
+ /^(.+)\s*==\s*(.+)$/, // literal == literal (e.g., 1 == 1, true == true)
84
+ /^(.+)\s*!=\s*(.+)$/, // literal != literal
85
+ /^(.+)\s*>=\s*(.+)$/, // literal >= literal
86
+ /^(.+)\s*<=\s*(.+)$/, // literal <= literal
87
+ /^(.+)\s*>\s*(.+)$/, // literal > literal
88
+ /^(.+)\s*<\s*(.+)$/, // literal < literal
89
+ /^(true|false)$/i, // boolean literals
90
+ ];
91
+ const matches = patterns.some(pattern => pattern.test(part));
92
+ if (!matches) {
93
+ errors.push(`Invalid syntax in part ${idx + 1}: "${part}"`);
94
+ return;
95
+ }
96
+ hasValidPart = true;
97
+ // Extract flag name (only if it starts with $)
98
+ const flagMatch = part.match(/\$(\w+)/);
99
+ if (flagMatch) {
100
+ const flagName = flagMatch[1];
101
+ // Check if flag exists in schema
102
+ if (flagSchema) {
103
+ const flagDef = flagSchema.flags.find(f => f.id === flagName);
104
+ if (!flagDef) {
105
+ warnings.push(`Flag "${flagName}" is not defined in your flag schema`);
106
+ }
107
+ else {
108
+ // Check if operator matches flag type
109
+ if (part.includes('>') || part.includes('<') || part.includes('>=') || part.includes('<=')) {
110
+ if (flagDef.valueType !== 'number') {
111
+ warnings.push(`Flag "${flagName}" is not a number type, but you're using a numeric comparison`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ else {
117
+ warnings.push(`No flag schema provided - cannot validate flag "${flagName}"`);
118
+ }
119
+ }
120
+ else {
121
+ // This is a literal comparison (like "1 == 1" or "true")
122
+ // These are valid but warn that they're unusual
123
+ if (part.match(/^(true|false)$/i)) {
124
+ // Boolean literal - this is fine
125
+ }
126
+ else if (part.includes('==') || part.includes('!=') || part.includes('>') || part.includes('<')) {
127
+ // Literal comparison - warn that this is unusual but allow it
128
+ warnings.push(`Literal comparison "${part}" will always evaluate to the same result. Consider using a flag variable instead.`);
129
+ }
130
+ }
131
+ });
132
+ if (!hasValidPart && conditionStr.trim()) {
133
+ errors.push('Invalid condition syntax. Use: $flag, $flag == value, $flag > 10, 1 == 1, etc.');
134
+ }
135
+ return {
136
+ isValid: errors.length === 0,
137
+ errors,
138
+ warnings
139
+ };
140
+ };
141
+ }, [flagSchema]);
142
+ // Only initialize condition inputs when node changes or when new blocks are added
143
+ (0, react_1.useEffect)(() => {
144
+ // Clear everything when switching to a different node
145
+ if (prevNodeIdRef.current !== node.id) {
146
+ prevNodeIdRef.current = node.id;
147
+ setConditionInputs({});
148
+ setDismissedConditions(new Set()); // Reset dismissed conditions when switching nodes
149
+ setExpandedConditions(new Set()); // Reset expanded conditions when switching nodes
150
+ initializedBlocksRef.current.clear();
151
+ initializedChoicesRef.current.clear();
152
+ }
153
+ // Always sync condition inputs with actual block conditions (not just initialize once)
154
+ if (node.conditionalBlocks) {
155
+ setConditionInputs(prev => {
156
+ const newInputs = { ...prev };
157
+ node.conditionalBlocks.forEach(block => {
158
+ if (block.type !== 'else') {
159
+ // Always convert condition array to Yarn-style string from the actual block data
160
+ if (block.condition && block.condition.length > 0) {
161
+ const conditionStr = block.condition.map(cond => {
162
+ const varName = `$${cond.flag}`;
163
+ if (cond.operator === 'is_set') {
164
+ return varName;
165
+ }
166
+ else if (cond.operator === 'is_not_set') {
167
+ return `not ${varName}`;
168
+ }
169
+ else if (cond.value !== undefined) {
170
+ const op = cond.operator === 'equals' ? '==' :
171
+ cond.operator === 'not_equals' ? '!=' :
172
+ cond.operator === 'greater_than' ? '>' :
173
+ cond.operator === 'less_than' ? '<' :
174
+ cond.operator === 'greater_equal' ? '>=' :
175
+ cond.operator === 'less_equal' ? '<=' : '==';
176
+ const value = typeof cond.value === 'string' ? `"${cond.value}"` : cond.value;
177
+ return `${varName} ${op} ${value}`;
178
+ }
179
+ return '';
180
+ }).filter(c => c).join(' and ') || '';
181
+ // Only update if it's different from what we have (to avoid overwriting user typing)
182
+ if (newInputs[block.id] !== conditionStr) {
183
+ newInputs[block.id] = conditionStr;
184
+ }
185
+ }
186
+ else {
187
+ // Empty condition - only set if not already set or if it's different
188
+ if (newInputs[block.id] === undefined || newInputs[block.id] !== '') {
189
+ newInputs[block.id] = '';
190
+ }
191
+ }
192
+ }
193
+ });
194
+ // Remove inputs for blocks that no longer exist
195
+ const blockIds = new Set(node.conditionalBlocks.map(b => b.id));
196
+ Object.keys(newInputs).forEach(id => {
197
+ if (!blockIds.has(id) && !id.startsWith('choice-')) {
198
+ delete newInputs[id];
199
+ initializedBlocksRef.current.delete(id);
200
+ }
201
+ });
202
+ return newInputs;
203
+ });
204
+ }
205
+ else {
206
+ // Clear block inputs but keep choice inputs
207
+ setConditionInputs(prev => {
208
+ const newInputs = {};
209
+ Object.keys(prev).forEach(key => {
210
+ // Keep choice inputs (they start with 'choice-')
211
+ if (key.startsWith('choice-')) {
212
+ newInputs[key] = prev[key];
213
+ }
214
+ });
215
+ return newInputs;
216
+ });
217
+ initializedBlocksRef.current.clear();
218
+ }
219
+ // Always sync choice condition inputs with actual choice data (not just initialize once)
220
+ if (node.choices) {
221
+ setConditionInputs(prev => {
222
+ const newInputs = { ...prev };
223
+ node.choices.forEach(choice => {
224
+ const choiceKey = `choice-${choice.id}`;
225
+ // Always sync with actual choice data to ensure conditions persist
226
+ if (choice.conditions && choice.conditions.length > 0) {
227
+ // Convert condition array to Yarn-style string
228
+ const conditionStr = choice.conditions.map(cond => {
229
+ const varName = `$${cond.flag}`;
230
+ if (cond.operator === 'is_set') {
231
+ return varName;
232
+ }
233
+ else if (cond.operator === 'is_not_set') {
234
+ return `not ${varName}`;
235
+ }
236
+ else if (cond.value !== undefined) {
237
+ const op = cond.operator === 'equals' ? '==' :
238
+ cond.operator === 'not_equals' ? '!=' :
239
+ cond.operator === 'greater_than' ? '>' :
240
+ cond.operator === 'less_than' ? '<' :
241
+ cond.operator === 'greater_equal' ? '>=' :
242
+ cond.operator === 'less_equal' ? '<=' : '==';
243
+ const value = typeof cond.value === 'string' ? `"${cond.value}"` : cond.value;
244
+ return `${varName} ${op} ${value}`;
245
+ }
246
+ return '';
247
+ }).filter(c => c).join(' and ') || '';
248
+ // Only update if different to avoid overwriting user typing
249
+ if (newInputs[choiceKey] !== conditionStr) {
250
+ newInputs[choiceKey] = conditionStr;
251
+ }
252
+ }
253
+ else if (choice.conditions !== undefined) {
254
+ // Empty array - user clicked "Add Condition"
255
+ if (newInputs[choiceKey] === undefined || newInputs[choiceKey] !== '') {
256
+ newInputs[choiceKey] = '';
257
+ }
258
+ }
259
+ else {
260
+ // No conditions - clear the input if it exists
261
+ if (newInputs[choiceKey] !== undefined) {
262
+ delete newInputs[choiceKey];
263
+ }
264
+ }
265
+ // Mark as initialized
266
+ if (!initializedChoicesRef.current.has(choiceKey)) {
267
+ initializedChoicesRef.current.add(choiceKey);
268
+ }
269
+ });
270
+ // Remove inputs for choices that no longer exist
271
+ const choiceIds = new Set(node.choices.map(c => `choice-${c.id}`));
272
+ Object.keys(newInputs).forEach(key => {
273
+ if (key.startsWith('choice-') && !choiceIds.has(key)) {
274
+ delete newInputs[key];
275
+ initializedChoicesRef.current.delete(key);
276
+ }
277
+ });
278
+ return newInputs;
279
+ });
280
+ }
281
+ else {
282
+ // Clear choice inputs
283
+ setConditionInputs(prev => {
284
+ const newInputs = {};
285
+ Object.keys(prev).forEach(key => {
286
+ if (!key.startsWith('choice-')) {
287
+ newInputs[key] = prev[key];
288
+ }
289
+ });
290
+ return newInputs;
291
+ });
292
+ initializedChoicesRef.current.clear();
293
+ }
294
+ }, [node.id, node.conditionalBlocks?.length, node.choices?.length]); // Only depend on length, not the arrays themselves
295
+ // Determine border color based on node type - use duller border colors
296
+ const getBorderColor = () => {
297
+ if (node.type === 'npc')
298
+ return 'border-df-npc-border';
299
+ if (node.type === 'player')
300
+ return 'border-df-player-border';
301
+ if (node.type === 'conditional')
302
+ return 'border-df-conditional-border';
303
+ return 'border-df-control-border';
304
+ };
305
+ // Get node type badge colors
306
+ const getNodeTypeBadge = () => {
307
+ if (node.type === 'npc')
308
+ return 'bg-df-npc-selected/20 text-df-npc-selected';
309
+ if (node.type === 'player')
310
+ return 'bg-df-player-selected/20 text-df-player-selected';
311
+ if (node.type === 'conditional')
312
+ return 'bg-df-conditional-border/20 text-df-conditional-border';
313
+ return 'bg-df-control-bg text-df-text-secondary';
314
+ };
315
+ return (react_1.default.createElement(react_1.default.Fragment, null,
316
+ react_1.default.createElement("style", null, `
317
+ @keyframes fadeIn {
318
+ from { opacity: 0; }
319
+ to { opacity: 1; }
320
+ }
321
+ @keyframes slideUp {
322
+ from {
323
+ opacity: 0;
324
+ transform: translateY(10px) scale(0.95);
325
+ }
326
+ to {
327
+ opacity: 1;
328
+ transform: translateY(0) scale(1);
329
+ }
330
+ }
331
+ `),
332
+ react_1.default.createElement("aside", { className: `w-80 border-l ${getBorderColor()} bg-df-sidebar-bg overflow-y-auto` },
333
+ react_1.default.createElement("div", { className: "p-4 space-y-4" },
334
+ react_1.default.createElement("div", { className: "flex items-center justify-between" },
335
+ react_1.default.createElement("span", { className: `text-xs px-2 py-0.5 rounded ${getNodeTypeBadge()}` }, node.type === 'npc' ? 'NPC' : node.type === 'player' ? 'PLAYER' : 'CONDITIONAL'),
336
+ react_1.default.createElement("div", { className: "flex gap-1" },
337
+ react_1.default.createElement("button", { onClick: onDelete, className: "p-1 text-gray-500 hover:text-red-400", title: "Delete node" },
338
+ react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
339
+ react_1.default.createElement("polyline", { points: "3 6 5 6 21 6" }),
340
+ react_1.default.createElement("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }))),
341
+ react_1.default.createElement("button", { onClick: onClose, className: "p-1 text-gray-500 hover:text-white", title: "Close" },
342
+ react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
343
+ react_1.default.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
344
+ react_1.default.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))),
345
+ react_1.default.createElement("div", null,
346
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "ID"),
347
+ react_1.default.createElement("input", { value: node.id, disabled: true, className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-xs text-gray-500 font-mono" })),
348
+ node.type === 'npc' && (react_1.default.createElement(react_1.default.Fragment, null,
349
+ react_1.default.createElement("div", null,
350
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
351
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
352
+ react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
353
+ react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-gray-500" })),
354
+ react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Character name" }))),
355
+ react_1.default.createElement("div", null,
356
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Content"),
357
+ react_1.default.createElement("textarea", { value: node.content, onChange: (e) => onUpdate({ content: e.target.value }), className: "w-full bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none min-h-[100px] resize-y", placeholder: "What the character says..." })),
358
+ react_1.default.createElement("div", null,
359
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Next Node"),
360
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
361
+ node.nextNodeId && onFocusNode && (react_1.default.createElement("button", { type: "button", onClick: (e) => {
362
+ e.preventDefault();
363
+ e.stopPropagation();
364
+ onFocusNode(node.nextNodeId);
365
+ }, className: "transition-colors cursor-pointer flex-shrink-0 group", title: `Focus on node: ${node.nextNodeId}` },
366
+ react_1.default.createElement(EdgeIcon_1.EdgeIcon, { size: 16, color: "#2a2a3e", className: "group-hover:[&_circle]:fill-[#2a2a3e] group-hover:[&_line]:stroke-[#2a2a3e] transition-colors" }))),
367
+ react_1.default.createElement("select", { value: node.nextNodeId || '', onChange: (e) => onUpdate({ nextNodeId: e.target.value || undefined }), className: "flex-1 bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-sm text-gray-200 outline-none" },
368
+ react_1.default.createElement("option", { value: "" }, "\u2014 End \u2014"),
369
+ Object.keys(dialogue.nodes).filter(id => id !== node.id).map(id => (react_1.default.createElement("option", { key: id, value: id }, id)))))))),
370
+ node.type === 'conditional' && (react_1.default.createElement(react_1.default.Fragment, null,
371
+ react_1.default.createElement("div", null,
372
+ react_1.default.createElement("div", { className: "flex items-center justify-between mb-2" },
373
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Conditional Blocks")),
374
+ node.conditionalBlocks ? (react_1.default.createElement("div", { className: "space-y-2" },
375
+ node.conditionalBlocks.map((block, idx) => {
376
+ const conditionValue = block.type !== 'else' ? (conditionInputs[block.id] || '') : '';
377
+ const debouncedValue = block.type !== 'else' ? (debouncedConditionInputs[block.id] || '') : '';
378
+ // Use current value for immediate validation feedback, debounced value for final validation
379
+ const valueToValidate = debouncedValue || conditionValue;
380
+ const validation = block.type !== 'else' ? validateCondition(valueToValidate) : { isValid: true, errors: [], warnings: [] };
381
+ const hasError = !validation.isValid;
382
+ const hasWarning = validation.warnings.length > 0;
383
+ const showValidation = conditionValue.trim().length > 0;
384
+ const isManuallyOpen = expandedConditions.has(block.id);
385
+ const shouldExpand = isManuallyOpen;
386
+ // Determine block styling based on type
387
+ const blockTypeStyles = {
388
+ if: {
389
+ bg: 'bg-[#0a0a0a]',
390
+ border: 'border-[#1a1a1a]',
391
+ tagBg: 'bg-black',
392
+ tagText: 'text-white',
393
+ text: 'text-gray-100'
394
+ },
395
+ elseif: {
396
+ bg: 'bg-[#0f0f0f]',
397
+ border: 'border-[#1f1f1f]',
398
+ tagBg: 'bg-black',
399
+ tagText: 'text-white',
400
+ text: 'text-gray-200'
401
+ },
402
+ else: {
403
+ bg: 'bg-[#141414]',
404
+ border: 'border-[#242424]',
405
+ tagBg: 'bg-black',
406
+ tagText: 'text-white',
407
+ text: 'text-gray-200'
408
+ }
409
+ };
410
+ const styles = blockTypeStyles[block.type];
411
+ return (react_1.default.createElement("div", { key: block.id, className: `rounded p-2 space-y-2 ${styles.bg} ${styles.border} border-2` },
412
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
413
+ react_1.default.createElement("span", { className: `text-[9px] px-1.5 py-0.5 rounded ${styles.tagBg} ${styles.tagText} font-semibold` }, block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE'),
414
+ react_1.default.createElement("div", { className: "flex items-center gap-2 flex-1" },
415
+ react_1.default.createElement("div", { className: "w-6 h-6 rounded-full bg-[#2a2a2a] flex items-center justify-center flex-shrink-0" },
416
+ react_1.default.createElement(lucide_react_1.User, { size: 12, className: "text-gray-400" })),
417
+ react_1.default.createElement("input", { type: "text", value: block.speaker || '', onChange: (e) => {
418
+ const newBlocks = [...node.conditionalBlocks];
419
+ newBlocks[idx] = { ...block, speaker: e.target.value || undefined };
420
+ onUpdate({ conditionalBlocks: newBlocks });
421
+ }, className: `flex-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-xs ${styles.text} focus:border-blue-500 outline-none`, placeholder: "Speaker (optional)" }))),
422
+ block.type !== 'else' && (() => {
423
+ const parseCondition = (conditionStr) => {
424
+ const conditions = [];
425
+ if (!conditionStr.trim())
426
+ return conditions;
427
+ const parts = conditionStr.split(/\s+and\s+/i);
428
+ parts.forEach(part => {
429
+ part = part.trim();
430
+ if (part.startsWith('not ')) {
431
+ const varMatch = part.match(/not\s+\$(\w+)/);
432
+ if (varMatch) {
433
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_NOT_SET });
434
+ }
435
+ }
436
+ else if (part.includes('>=')) {
437
+ const match = part.match(/\$(\w+)\s*>=\s*(.+)/);
438
+ if (match) {
439
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
440
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
441
+ }
442
+ }
443
+ else if (part.includes('<=')) {
444
+ const match = part.match(/\$(\w+)\s*<=\s*(.+)/);
445
+ if (match) {
446
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
447
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
448
+ }
449
+ }
450
+ else if (part.includes('!=')) {
451
+ const match = part.match(/\$(\w+)\s*!=\s*(.+)/);
452
+ if (match) {
453
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
454
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.NOT_EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
455
+ }
456
+ }
457
+ else if (part.includes('==')) {
458
+ const match = part.match(/\$(\w+)\s*==\s*(.+)/);
459
+ if (match) {
460
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
461
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
462
+ }
463
+ }
464
+ else if (part.includes('>') && !part.includes('>=')) {
465
+ const match = part.match(/\$(\w+)\s*>\s*(.+)/);
466
+ if (match) {
467
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
468
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_THAN, value: isNaN(Number(value)) ? value : Number(value) });
469
+ }
470
+ }
471
+ else if (part.includes('<') && !part.includes('<=')) {
472
+ const match = part.match(/\$(\w+)\s*<\s*(.+)/);
473
+ if (match) {
474
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
475
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_THAN, value: isNaN(Number(value)) ? value : Number(value) });
476
+ }
477
+ }
478
+ else {
479
+ const varMatch = part.match(/\$(\w+)/);
480
+ if (varMatch) {
481
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_SET });
482
+ }
483
+ }
484
+ });
485
+ return conditions;
486
+ };
487
+ return (react_1.default.createElement("div", { className: "bg-[#1a1a1a] border border-[#2a2a2a] rounded p-2 space-y-1" },
488
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
489
+ react_1.default.createElement("label", { className: `text-[10px] ${styles.text} uppercase font-medium` }, "Condition")),
490
+ react_1.default.createElement("div", { className: "relative" },
491
+ react_1.default.createElement("input", { type: "text", value: conditionValue, onChange: (e) => {
492
+ setConditionInputs(prev => ({ ...prev, [block.id]: e.target.value }));
493
+ const newBlocks = [...node.conditionalBlocks];
494
+ newBlocks[idx] = {
495
+ ...block,
496
+ condition: parseCondition(e.target.value)
497
+ };
498
+ onUpdate({ conditionalBlocks: newBlocks });
499
+ }, className: `w-full bg-[#1a1a1a] border rounded px-2 py-1 pr-24 text-xs ${styles.text} font-mono outline-none transition-all`, style: {
500
+ borderColor: showValidation
501
+ ? (hasError ? '#ef4444' :
502
+ hasWarning ? '#eab308' :
503
+ validation.isValid ? '#22c55e' : '#2a2a2a')
504
+ : '#2a2a2a'
505
+ }, placeholder: 'e.g., $flag == "value" or $stat >= 100' }),
506
+ react_1.default.createElement("div", { className: "absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1" },
507
+ showValidation && (react_1.default.createElement(react_1.default.Fragment, null, hasError ? (react_1.default.createElement("div", { className: "group relative" },
508
+ react_1.default.createElement(lucide_react_1.AlertCircle, { className: "w-4 h-4 text-red-500" }),
509
+ react_1.default.createElement("div", { className: "absolute right-0 top-6 w-64 p-2 bg-[#1a1a1a] border border-red-500 rounded text-xs text-gray-300 z-50 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" },
510
+ react_1.default.createElement("div", { className: "font-semibold text-red-400 mb-1" }, "Validation Errors:"),
511
+ validation.errors.map((error, i) => (react_1.default.createElement("div", { key: i },
512
+ "\u2022 ",
513
+ error)))))) : hasWarning ? (react_1.default.createElement("div", { className: "group relative" },
514
+ react_1.default.createElement(lucide_react_1.Info, { className: "w-4 h-4 text-yellow-500" }),
515
+ react_1.default.createElement("div", { className: "absolute right-0 top-6 w-64 p-2 bg-[#1a1a1a] border border-yellow-500 rounded text-xs text-gray-300 z-50 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" },
516
+ react_1.default.createElement("div", { className: "font-semibold text-yellow-400 mb-1" }, "Warnings:"),
517
+ validation.warnings.map((warning, i) => (react_1.default.createElement("div", { key: i },
518
+ "\u2022 ",
519
+ warning)))))) : (react_1.default.createElement(lucide_react_1.CheckCircle, { className: "w-4 h-4 text-green-500" })))),
520
+ react_1.default.createElement("button", { onClick: () => {
521
+ setExpandedConditions(prev => {
522
+ const next = new Set(prev);
523
+ next.add(block.id);
524
+ return next;
525
+ });
526
+ }, className: "p-1 text-gray-400 hover:text-blue-400 transition-colors", title: "Expand editor" },
527
+ react_1.default.createElement(lucide_react_1.Maximize2, { size: 14 })))),
528
+ showValidation && validation.errors.length > 0 && (react_1.default.createElement("p", { className: "text-[10px] text-red-500 mt-1" }, validation.errors[0])),
529
+ showValidation && validation.warnings.length > 0 && validation.errors.length === 0 && (react_1.default.createElement("p", { className: "text-[10px] text-yellow-500 mt-1" }, validation.warnings[0])),
530
+ showValidation && validation.isValid && validation.errors.length === 0 && validation.warnings.length === 0 && (react_1.default.createElement("p", { className: "text-[10px] text-green-500 mt-1" }, "Valid condition")),
531
+ !conditionValue && (react_1.default.createElement("p", { className: "text-[10px] text-blue-400/80 mt-1" }, "Type Yarn condition: $flag, $flag == value, $stat >= 100, etc."))));
532
+ })(),
533
+ shouldExpand && (react_1.default.createElement("div", { className: "fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4 transition-opacity", style: { animation: 'fadeIn 0.2s ease-in-out' }, onMouseDown: (e) => {
534
+ if (e.target === e.currentTarget) {
535
+ e.currentTarget.setAttribute('data-mousedown-backdrop', 'true');
536
+ }
537
+ }, onMouseUp: (e) => {
538
+ if (e.target === e.currentTarget &&
539
+ e.currentTarget.getAttribute('data-mousedown-backdrop') === 'true') {
540
+ setExpandedConditions(prev => {
541
+ const next = new Set(prev);
542
+ next.delete(block.id);
543
+ return next;
544
+ });
545
+ }
546
+ e.currentTarget.removeAttribute('data-mousedown-backdrop');
547
+ } },
548
+ react_1.default.createElement("div", { className: "bg-[#0d0d14] border border-[#2a2a3e] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col transition-all", style: { animation: 'slideUp 0.2s ease-out' }, onClick: (e) => e.stopPropagation() },
549
+ react_1.default.createElement("div", { className: "p-3 border-b border-[#2a2a3e] flex items-center justify-between bg-gradient-to-r from-[#0d0d14] to-[#1a1a2e]" },
550
+ react_1.default.createElement("h3", { className: "text-sm font-semibold text-white flex items-center gap-2" },
551
+ react_1.default.createElement("span", { className: "text-blue-400" }, "\u26A1"),
552
+ "Condition Editor"),
553
+ react_1.default.createElement("button", { onClick: () => {
554
+ setExpandedConditions(prev => {
555
+ const next = new Set(prev);
556
+ next.delete(block.id);
557
+ return next;
558
+ });
559
+ }, className: "p-1 text-gray-400 hover:text-white transition-colors", title: "Close (Esc)" },
560
+ react_1.default.createElement(lucide_react_1.X, { size: 16 }))),
561
+ react_1.default.createElement("div", { className: "flex flex-1 overflow-hidden" },
562
+ react_1.default.createElement("div", { className: "w-44 bg-[#0a0a0f] border-r border-[#2a2a3e] p-3 overflow-y-auto flex flex-col gap-4" },
563
+ react_1.default.createElement("div", { className: "bg-gradient-to-br from-blue-500/10 to-purple-500/10 border border-blue-500/30 rounded-lg p-2.5" },
564
+ react_1.default.createElement("div", { className: "flex items-center gap-1.5 mb-1.5" },
565
+ react_1.default.createElement("span", { className: "text-sm" }, "\uD83D\uDCA1"),
566
+ react_1.default.createElement("span", { className: "text-[10px] font-semibold text-blue-400 uppercase tracking-wide" }, "Pro Tip")),
567
+ react_1.default.createElement("p", { className: "text-[10px] text-gray-400 leading-relaxed" },
568
+ "Type ",
569
+ react_1.default.createElement("code", { className: "text-purple-400 bg-purple-500/20 px-1 rounded font-bold" }, "$"),
570
+ " to access variables & flags.")),
571
+ react_1.default.createElement("div", null,
572
+ react_1.default.createElement("label", { className: "text-[9px] text-gray-500 uppercase mb-1.5 block font-semibold tracking-wider" }, "Operators"),
573
+ react_1.default.createElement("div", { className: "grid grid-cols-3 gap-1" }, ['==', '!=', '>=', '<=', '>', '<'].map((op) => (react_1.default.createElement("button", { key: op, type: "button", onClick: () => {
574
+ const val = conditionValue;
575
+ const space = val.length > 0 && !val.endsWith(' ') ? ' ' : '';
576
+ setConditionInputs(prev => ({ ...prev, [block.id]: val + space + op + ' ' }));
577
+ }, className: "px-1.5 py-1 bg-purple-500/20 text-purple-400 border border-purple-500/30 rounded text-xs font-mono hover:bg-purple-500/40 transition-all" }, op))))),
578
+ react_1.default.createElement("div", null,
579
+ react_1.default.createElement("label", { className: "text-[9px] text-gray-500 uppercase mb-1.5 block font-semibold tracking-wider" }, "Keywords"),
580
+ react_1.default.createElement("div", { className: "grid grid-cols-2 gap-1" }, ['and', 'not'].map((kw) => (react_1.default.createElement("button", { key: kw, type: "button", onClick: () => {
581
+ const val = conditionValue;
582
+ const space = val.length > 0 && !val.endsWith(' ') ? ' ' : '';
583
+ setConditionInputs(prev => ({ ...prev, [block.id]: val + space + kw + ' ' }));
584
+ }, className: "px-1.5 py-1 bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded text-xs font-mono hover:bg-blue-500/40 transition-all" }, kw))))),
585
+ react_1.default.createElement("div", null,
586
+ react_1.default.createElement("label", { className: "text-[9px] text-gray-500 uppercase mb-1.5 block font-semibold tracking-wider" }, "Templates"),
587
+ react_1.default.createElement("div", { className: "flex flex-col gap-1" }, [
588
+ { p: '$flag == true', l: 'Boolean' },
589
+ { p: '$stat >= 100', l: 'Compare' },
590
+ { p: '$a and $b', l: 'Multiple' },
591
+ ].map(({ p, l }) => (react_1.default.createElement("button", { key: p, type: "button", onClick: () => setConditionInputs(prev => ({ ...prev, [block.id]: p })), className: "text-left px-2 py-1 bg-gray-500/10 text-gray-400 border border-gray-500/20 rounded text-[10px] font-mono hover:bg-gray-500/20 transition-all" },
592
+ react_1.default.createElement("div", { className: "text-gray-300" }, p),
593
+ react_1.default.createElement("div", { className: "text-[8px] text-gray-600" }, l))))))),
594
+ react_1.default.createElement("div", { className: "flex-1 p-4 overflow-y-auto flex flex-col gap-3" },
595
+ react_1.default.createElement(ConditionAutocomplete_1.ConditionAutocomplete, { value: conditionValue, onChange: (newValue) => {
596
+ setConditionInputs(prev => ({ ...prev, [block.id]: newValue }));
597
+ const parseCondition = (conditionStr) => {
598
+ const conditions = [];
599
+ if (!conditionStr.trim())
600
+ return conditions;
601
+ const parts = conditionStr.split(/\s+and\s+/i);
602
+ parts.forEach(part => {
603
+ part = part.trim();
604
+ if (part.startsWith('not ')) {
605
+ const varMatch = part.match(/not\s+\$(\w+)/);
606
+ if (varMatch)
607
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_NOT_SET });
608
+ }
609
+ else if (part.includes('>=')) {
610
+ const match = part.match(/\$(\w+)\s*>=\s*(.+)/);
611
+ if (match) {
612
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
613
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
614
+ }
615
+ }
616
+ else if (part.includes('<=')) {
617
+ const match = part.match(/\$(\w+)\s*<=\s*(.+)/);
618
+ if (match) {
619
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
620
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
621
+ }
622
+ }
623
+ else if (part.includes('!=')) {
624
+ const match = part.match(/\$(\w+)\s*!=\s*(.+)/);
625
+ if (match) {
626
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
627
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.NOT_EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
628
+ }
629
+ }
630
+ else if (part.includes('==')) {
631
+ const match = part.match(/\$(\w+)\s*==\s*(.+)/);
632
+ if (match) {
633
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
634
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
635
+ }
636
+ }
637
+ else if (part.includes('>') && !part.includes('>=')) {
638
+ const match = part.match(/\$(\w+)\s*>\s*(.+)/);
639
+ if (match) {
640
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
641
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_THAN, value: isNaN(Number(value)) ? value : Number(value) });
642
+ }
643
+ }
644
+ else if (part.includes('<') && !part.includes('<=')) {
645
+ const match = part.match(/\$(\w+)\s*<\s*(.+)/);
646
+ if (match) {
647
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
648
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_THAN, value: isNaN(Number(value)) ? value : Number(value) });
649
+ }
650
+ }
651
+ else {
652
+ const varMatch = part.match(/\$(\w+)/);
653
+ if (varMatch)
654
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_SET });
655
+ }
656
+ });
657
+ return conditions;
658
+ };
659
+ const newBlocks = [...node.conditionalBlocks];
660
+ newBlocks[idx] = { ...block, condition: parseCondition(newValue) };
661
+ onUpdate({ conditionalBlocks: newBlocks });
662
+ }, flagSchema: flagSchema, textarea: true, placeholder: 'e.g., $flag == "value" or $stat >= 100', className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-3 py-2 text-sm text-gray-200 font-mono outline-none focus:border-blue-500 min-h-[180px] resize-y" }),
663
+ showValidation && (react_1.default.createElement("div", { className: `p-2 rounded text-xs ${hasError ? 'bg-red-500/10 border border-red-500/30 text-red-400' :
664
+ hasWarning ? 'bg-yellow-500/10 border border-yellow-500/30 text-yellow-400' :
665
+ 'bg-green-500/10 border border-green-500/30 text-green-400'}` },
666
+ hasError && (react_1.default.createElement("div", null,
667
+ react_1.default.createElement("strong", null, "Errors:"),
668
+ react_1.default.createElement("ul", { className: "list-disc list-inside mt-1 ml-2" }, validation.errors.map((error, i) => (react_1.default.createElement("li", { key: i }, error)))))),
669
+ hasWarning && (react_1.default.createElement("div", { className: hasError ? 'mt-2' : '' },
670
+ react_1.default.createElement("strong", null, "Warnings:"),
671
+ react_1.default.createElement("ul", { className: "list-disc list-inside mt-1 ml-2" }, validation.warnings.map((warning, i) => (react_1.default.createElement("li", { key: i }, warning)))))),
672
+ !hasError && !hasWarning && (react_1.default.createElement("div", null, "\u2713 Valid condition expression"))))))))),
673
+ react_1.default.createElement("div", null,
674
+ react_1.default.createElement("label", { className: `text-[10px] ${styles.text} uppercase mb-1 block` }, "Content"),
675
+ react_1.default.createElement("textarea", { value: block.content, onChange: (e) => {
676
+ const newBlocks = [...node.conditionalBlocks];
677
+ newBlocks[idx] = { ...block, content: e.target.value };
678
+ onUpdate({ conditionalBlocks: newBlocks });
679
+ }, className: `w-full bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-sm ${styles.text} outline-none min-h-[80px] resize-y`, placeholder: "Dialogue content..." })),
680
+ react_1.default.createElement("div", null,
681
+ react_1.default.createElement("label", { className: `text-[10px] ${styles.text} uppercase` }, "Next Node (optional)"),
682
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
683
+ block.nextNodeId && onFocusNode && (react_1.default.createElement("button", { type: "button", onClick: (e) => {
684
+ e.preventDefault();
685
+ e.stopPropagation();
686
+ onFocusNode(block.nextNodeId);
687
+ }, className: "transition-colors cursor-pointer flex-shrink-0 group", title: `Focus on node: ${block.nextNodeId}` },
688
+ react_1.default.createElement(EdgeIcon_1.EdgeIcon, { size: 16, color: block.nextNodeId ? '#3b82f6' : '#2a2a3e', className: "group-hover:[&_circle]:fill-[#3b82f6] group-hover:[&_line]:stroke-[#3b82f6] transition-colors" }))),
689
+ react_1.default.createElement("div", { className: "relative flex-1" },
690
+ react_1.default.createElement("select", { value: block.nextNodeId || '', onChange: (e) => {
691
+ const newBlocks = [...node.conditionalBlocks];
692
+ newBlocks[idx] = { ...block, nextNodeId: e.target.value || undefined };
693
+ onUpdate({ conditionalBlocks: newBlocks });
694
+ }, className: `w-full bg-[#1a1a1a] border rounded px-2 py-1 pr-8 text-xs ${styles.text} outline-none`, style: {
695
+ borderColor: block.nextNodeId ? '#3b82f6' : '#2a2a3e',
696
+ } },
697
+ react_1.default.createElement("option", { value: "" }, "\u2014 Continue \u2014"),
698
+ Object.keys(dialogue.nodes).filter(id => id !== node.id).map(id => (react_1.default.createElement("option", { key: id, value: id }, id)))),
699
+ block.nextNodeId && (react_1.default.createElement("div", { className: "absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none", title: `Connects to node: ${block.nextNodeId}`, style: { color: '#3b82f6' } },
700
+ react_1.default.createElement(lucide_react_1.GitBranch, { size: 14 }))))))));
701
+ }),
702
+ react_1.default.createElement("div", { className: "flex gap-2" },
703
+ node.conditionalBlocks[node.conditionalBlocks.length - 1].type !== 'else' && (react_1.default.createElement("button", { onClick: () => {
704
+ const newBlocks = [...node.conditionalBlocks];
705
+ newBlocks.push({
706
+ id: `block_${Date.now()}`,
707
+ type: newBlocks.some(b => b.type === 'if') ? 'elseif' : 'if',
708
+ condition: [],
709
+ content: '',
710
+ speaker: undefined
711
+ });
712
+ onUpdate({ conditionalBlocks: newBlocks });
713
+ }, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" },
714
+ "+ Add ",
715
+ node.conditionalBlocks.some(b => b.type === 'if') ? 'Else If' : 'If')),
716
+ !node.conditionalBlocks.some(b => b.type === 'else') && (react_1.default.createElement("button", { onClick: () => {
717
+ const newBlocks = [...node.conditionalBlocks];
718
+ newBlocks.push({
719
+ id: `block_${Date.now()}`,
720
+ type: 'else',
721
+ condition: undefined,
722
+ content: '',
723
+ speaker: undefined
724
+ });
725
+ onUpdate({ conditionalBlocks: newBlocks });
726
+ }, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" }, "+ Add Else"))))) : (react_1.default.createElement("div", { className: "text-xs text-gray-500 p-4 text-center border border-[#2a2a3e] rounded" }, "No conditional blocks. Add an \"If\" block to start."))))),
727
+ node.type === 'player' && (react_1.default.createElement("div", null,
728
+ react_1.default.createElement("div", null,
729
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
730
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
731
+ react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
732
+ react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-gray-500" })),
733
+ react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-sm text-gray-200 focus:border-[#8b5cf6] outline-none", placeholder: "Character name (optional)" }))),
734
+ react_1.default.createElement("div", { className: "flex items-center justify-between mb-2 mt-4" },
735
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Choices"),
736
+ react_1.default.createElement("button", { onClick: onAddChoice, className: "text-[10px] text-[#e94560] hover:text-[#ff6b6b]" }, "+ Add")),
737
+ react_1.default.createElement("div", { className: "space-y-2" }, node.choices?.map((choice, idx) => {
738
+ const hasCondition = choice.conditions !== undefined;
739
+ const choiceKey = `choice-${choice.id}`;
740
+ const conditionValue = conditionInputs[choiceKey] || '';
741
+ const debouncedValue = debouncedConditionInputs[choiceKey] || '';
742
+ // Use debounced value for validation
743
+ const validationResult = validateCondition(debouncedValue);
744
+ const choiceColor = reactflow_converter_1.CHOICE_COLORS[idx % reactflow_converter_1.CHOICE_COLORS.length];
745
+ // Darken the choice color for inputs (reduce brightness by ~40%)
746
+ const darkenColor = (color) => {
747
+ // Convert hex to RGB
748
+ const hex = color.replace('#', '');
749
+ const r = parseInt(hex.substr(0, 2), 16);
750
+ const g = parseInt(hex.substr(2, 2), 16);
751
+ const b = parseInt(hex.substr(4, 2), 16);
752
+ // Darken by 40%
753
+ const darkR = Math.floor(r * 0.6);
754
+ const darkG = Math.floor(g * 0.6);
755
+ const darkB = Math.floor(b * 0.6);
756
+ return `rgb(${darkR}, ${darkG}, ${darkB})`;
757
+ };
758
+ const darkChoiceColor = darkenColor(choiceColor);
759
+ return (react_1.default.createElement("div", { key: choice.id, className: `rounded p-2 space-y-2 ${hasCondition
760
+ ? 'bg-blue-500/10 border-2 border-blue-500/50'
761
+ : 'bg-[#12121a] border border-[#2a2a3e]'}`, style: {
762
+ borderTopColor: hasCondition ? undefined : choiceColor
763
+ } },
764
+ react_1.default.createElement("div", { className: "flex items-center gap-2 pb-2 border-b", style: {
765
+ borderBottomColor: hasCondition ? '#2a2a3e' : choiceColor
766
+ } },
767
+ react_1.default.createElement("label", { className: "flex items-center cursor-pointer" },
768
+ react_1.default.createElement("div", { className: "relative" },
769
+ react_1.default.createElement("input", { type: "checkbox", checked: hasCondition, onChange: (e) => {
770
+ if (e.target.checked) {
771
+ // Initialize with empty array to show condition input
772
+ onUpdateChoice(idx, { conditions: [] });
773
+ }
774
+ else {
775
+ // Remove condition
776
+ onUpdateChoice(idx, { conditions: undefined });
777
+ }
778
+ }, className: "sr-only" }),
779
+ react_1.default.createElement("div", { className: `w-7 h-3.5 rounded-full transition-all duration-200 ease-in-out ${hasCondition ? 'bg-blue-500' : 'bg-[#2a2a3e]'}` },
780
+ react_1.default.createElement("div", { className: `w-2.5 h-2.5 rounded-full bg-white transition-all duration-200 ease-in-out mt-0.5 ${hasCondition ? 'translate-x-4' : 'translate-x-0.5'}` })))),
781
+ hasCondition ? (react_1.default.createElement("span", { className: "text-[9px] px-1.5 py-0.5 rounded bg-blue-500/30 text-blue-400 border border-blue-500/50 font-medium" }, "CONDITIONAL")) : (react_1.default.createElement("span", { className: "text-[9px] px-1.5 py-0.5 rounded bg-[#2a2a3e] text-gray-400 border border-[#2a2a3e] font-medium" }, "CHOICE")),
782
+ choice.nextNodeId && onFocusNode && (react_1.default.createElement("button", { type: "button", onClick: (e) => {
783
+ e.preventDefault();
784
+ e.stopPropagation();
785
+ if (choice.nextNodeId && onFocusNode) {
786
+ onFocusNode(choice.nextNodeId);
787
+ }
788
+ }, className: "transition-colors cursor-pointer flex-shrink-0", title: `Focus on node: ${choice.nextNodeId}` },
789
+ react_1.default.createElement(EdgeIcon_1.EdgeIcon, { size: 16, color: reactflow_converter_1.CHOICE_COLORS[idx % reactflow_converter_1.CHOICE_COLORS.length], className: "transition-colors" }))),
790
+ react_1.default.createElement("div", { className: "relative flex-1" },
791
+ react_1.default.createElement("select", { value: choice.nextNodeId || '', onChange: (e) => onUpdateChoice(idx, { nextNodeId: e.target.value || undefined }), className: "w-full bg-[#0d0d14] border rounded px-2 py-1 pr-8 text-xs text-gray-300 outline-none", style: {
792
+ borderColor: choice.nextNodeId ? darkChoiceColor : '#2a2a3e',
793
+ }, onFocus: (e) => {
794
+ if (choice.nextNodeId) {
795
+ e.target.style.borderColor = darkChoiceColor;
796
+ }
797
+ else {
798
+ e.target.style.borderColor = '#e94560';
799
+ }
800
+ }, onBlur: (e) => {
801
+ if (choice.nextNodeId) {
802
+ e.target.style.borderColor = darkChoiceColor;
803
+ }
804
+ else {
805
+ e.target.style.borderColor = '#2a2a3e';
806
+ }
807
+ } },
808
+ react_1.default.createElement("option", { value: "" }, "\u2014 Select target \u2014"),
809
+ Object.keys(dialogue.nodes).map(id => (react_1.default.createElement("option", { key: id, value: id }, id)))),
810
+ choice.nextNodeId && (react_1.default.createElement("div", { className: "absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none", title: `Connects to node: ${choice.nextNodeId}`, style: { color: reactflow_converter_1.CHOICE_COLORS[idx % reactflow_converter_1.CHOICE_COLORS.length] } },
811
+ react_1.default.createElement(lucide_react_1.GitBranch, { size: 14 })))),
812
+ react_1.default.createElement("button", { onClick: () => onRemoveChoice(idx), className: "text-gray-600 hover:text-red-400 flex-shrink-0", title: "Remove choice" },
813
+ react_1.default.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
814
+ react_1.default.createElement("polyline", { points: "3 6 5 6 21 6" }),
815
+ react_1.default.createElement("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })))),
816
+ react_1.default.createElement("div", { className: "pt-2" },
817
+ react_1.default.createElement("input", { type: "text", value: choice.text, onChange: (e) => onUpdateChoice(idx, { text: e.target.value }), className: `w-full bg-[#0d0d14] border rounded px-3 py-2 text-sm outline-none transition-colors ${hasCondition ? 'text-gray-100' : 'text-gray-200'}`, style: {
818
+ borderColor: choice.text ? darkChoiceColor : '#2a2a3e'
819
+ }, placeholder: "Dialogue text..." })),
820
+ hasCondition && (react_1.default.createElement("div", { className: "bg-blue-500/5 border border-blue-500/30 rounded p-2 space-y-1" },
821
+ react_1.default.createElement("div", { className: "flex items-center gap-2" },
822
+ react_1.default.createElement("label", { className: "text-[10px] text-blue-400 uppercase font-medium" }, "Condition"),
823
+ react_1.default.createElement("button", { onClick: () => onUpdateChoice(idx, { conditions: undefined }), className: "text-[10px] text-gray-500 hover:text-red-400 ml-auto", title: "Remove condition" }, "Remove")),
824
+ react_1.default.createElement("div", { className: "relative" },
825
+ react_1.default.createElement(ConditionAutocomplete_1.ConditionAutocomplete, { value: conditionValue, onChange: (newValue) => {
826
+ setConditionInputs(prev => ({ ...prev, [choiceKey]: newValue }));
827
+ // Parse and update condition immediately
828
+ const parseCondition = (conditionStr) => {
829
+ const conditions = [];
830
+ if (!conditionStr.trim())
831
+ return conditions;
832
+ const parts = conditionStr.split(/\s+and\s+/i);
833
+ parts.forEach(part => {
834
+ part = part.trim();
835
+ if (part.startsWith('not ')) {
836
+ const flagName = part.substring(4).replace('$', '');
837
+ conditions.push({ flag: flagName, operator: 'is_not_set' });
838
+ }
839
+ else if (part.match(/^\$(\w+)$/)) {
840
+ const match = part.match(/^\$(\w+)$/);
841
+ if (match) {
842
+ conditions.push({ flag: match[1], operator: 'is_set' });
843
+ }
844
+ }
845
+ else {
846
+ const match = part.match(/^\$(\w+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
847
+ if (match) {
848
+ const [, flagName, op, valueStr] = match;
849
+ let value = valueStr.trim();
850
+ // Remove quotes if present
851
+ if (value.startsWith('"') && value.endsWith('"')) {
852
+ value = value.slice(1, -1);
853
+ }
854
+ else if (!isNaN(Number(value))) {
855
+ value = Number(value);
856
+ }
857
+ const operator = op === '==' ? 'equals' :
858
+ op === '!=' ? 'not_equals' :
859
+ op === '>=' ? 'greater_equal' :
860
+ op === '<=' ? 'less_equal' :
861
+ op === '>' ? 'greater_than' :
862
+ op === '<' ? 'less_than' : 'equals';
863
+ conditions.push({ flag: flagName, operator, value });
864
+ }
865
+ }
866
+ });
867
+ return conditions;
868
+ };
869
+ const newConditions = parseCondition(newValue);
870
+ onUpdateChoice(idx, {
871
+ conditions: newConditions.length > 0 ? newConditions : []
872
+ });
873
+ }, placeholder: "e.g., $reputation > 10 or $flag == \"value\"", className: "w-full bg-[#0d0d14] border rounded px-2 py-1 pr-8 text-xs text-gray-300 font-mono outline-none hover:border-blue-500/50 transition-all", style: {
874
+ borderColor: conditionValue.trim().length > 0 && debouncedValue.trim().length > 0
875
+ ? (validationResult.isValid ? 'rgba(59, 130, 246, 0.5)' :
876
+ validationResult.errors.length > 0 ? '#ef4444' : '#eab308')
877
+ : '#2a2a3e'
878
+ }, flagSchema: flagSchema })),
879
+ conditionValue.trim().length > 0 && debouncedValue.trim().length > 0 && validationResult.errors.length > 0 && (react_1.default.createElement("p", { className: "text-[10px] text-red-500 mt-1" }, validationResult.errors[0])),
880
+ validationResult.warnings.length > 0 && validationResult.errors.length === 0 && (react_1.default.createElement("p", { className: "text-[10px] text-yellow-500 mt-1" }, validationResult.warnings[0])),
881
+ validationResult.isValid && validationResult.errors.length === 0 && validationResult.warnings.length === 0 && conditionValue && (react_1.default.createElement("p", { className: "text-[10px] text-green-500 mt-1" }, "Valid condition")),
882
+ !conditionValue && (react_1.default.createElement("p", { className: "text-[10px] text-blue-400/80 mt-1" }, "Only shows if condition is true")))),
883
+ react_1.default.createElement(FlagSelector_1.FlagSelector, { value: choice.setFlags || [], onChange: (flags) => onUpdateChoice(idx, { setFlags: flags.length > 0 ? flags : undefined }), flagSchema: flagSchema, placeholder: "Set flags..." })));
884
+ })))),
885
+ react_1.default.createElement("div", null,
886
+ react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Set Flags (on enter)"),
887
+ react_1.default.createElement(FlagSelector_1.FlagSelector, { value: node.setFlags || [], onChange: (flags) => onUpdate({ setFlags: flags.length > 0 ? flags : undefined }), flagSchema: flagSchema, placeholder: "flag1, flag2" })),
888
+ onPlayFromHere && (react_1.default.createElement("button", { onClick: () => onPlayFromHere(node.id), className: "w-full py-2 bg-[#e94560] hover:bg-[#d63850] text-white rounded text-sm flex items-center justify-center gap-2" },
889
+ react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" },
890
+ react_1.default.createElement("polygon", { points: "5 3 19 12 5 21 5 3" })),
891
+ "Play from Here"))),
892
+ editingCondition && (react_1.default.createElement("div", { className: "fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4", onClick: (e) => {
893
+ if (e.target === e.currentTarget) {
894
+ setEditingCondition(null);
895
+ }
896
+ } },
897
+ react_1.default.createElement("div", { className: "bg-[#0d0d14] border border-[#2a2a3e] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col", onClick: (e) => e.stopPropagation() },
898
+ react_1.default.createElement("div", { className: "p-4 border-b border-[#2a2a3e] flex items-center justify-between" },
899
+ react_1.default.createElement("h3", { className: "text-sm font-semibold text-white" }, "Edit Condition"),
900
+ react_1.default.createElement("button", { onClick: () => setEditingCondition(null), className: "p-1 text-gray-400 hover:text-white transition-colors" },
901
+ react_1.default.createElement(lucide_react_1.X, { size: 18 }))),
902
+ react_1.default.createElement("div", { className: "flex-1 overflow-y-auto p-4" },
903
+ react_1.default.createElement("div", { className: "space-y-4" },
904
+ react_1.default.createElement("div", null,
905
+ react_1.default.createElement("label", { className: "text-xs text-gray-400 uppercase mb-2 block" }, "Yarn Condition Expression"),
906
+ react_1.default.createElement("textarea", { value: editingCondition.value, onChange: (e) => {
907
+ setEditingCondition({
908
+ ...editingCondition,
909
+ value: e.target.value
910
+ });
911
+ }, className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-4 py-3 text-base text-gray-200 font-mono outline-none focus:border-blue-500 min-h-[200px] resize-y", placeholder: 'e.g., $flag == "value" or $stat >= 100', autoFocus: true }),
912
+ react_1.default.createElement("p", { className: "text-[10px] text-gray-500 mt-2" }, "Type Yarn condition: $flag, $flag == value, $stat >= 100, etc.")),
913
+ (() => {
914
+ const validation = validateCondition(debouncedEditingValue);
915
+ const hasError = !validation.isValid;
916
+ const hasWarning = validation.warnings.length > 0;
917
+ const showValidation = debouncedEditingValue.trim().length > 0 && editingCondition.value.trim().length > 0;
918
+ if (!showValidation)
919
+ return null;
920
+ return (react_1.default.createElement("div", { className: `p-3 rounded border ${hasError ? 'bg-red-500/10 border-red-500/30' :
921
+ hasWarning ? 'bg-yellow-500/10 border-yellow-500/30' :
922
+ 'bg-green-500/10 border-green-500/30'}` },
923
+ react_1.default.createElement("div", { className: "flex items-start gap-2" },
924
+ hasError ? (react_1.default.createElement(lucide_react_1.AlertCircle, { size: 16, className: "text-red-500 mt-0.5 flex-shrink-0" })) : hasWarning ? (react_1.default.createElement(lucide_react_1.Info, { size: 16, className: "text-yellow-500 mt-0.5 flex-shrink-0" })) : (react_1.default.createElement(lucide_react_1.CheckCircle, { size: 16, className: "text-green-500 mt-0.5 flex-shrink-0" })),
925
+ react_1.default.createElement("div", { className: "flex-1 text-xs" },
926
+ hasError && (react_1.default.createElement("div", null,
927
+ react_1.default.createElement("strong", { className: "text-red-400" }, "Errors:"),
928
+ react_1.default.createElement("ul", { className: "list-disc list-inside mt-1 ml-2 text-red-300" }, validation.errors.map((error, i) => (react_1.default.createElement("li", { key: i }, error)))))),
929
+ hasWarning && (react_1.default.createElement("div", { className: hasError ? 'mt-2' : '' },
930
+ react_1.default.createElement("strong", { className: "text-yellow-400" }, "Warnings:"),
931
+ react_1.default.createElement("ul", { className: "list-disc list-inside mt-1 ml-2 text-yellow-300" }, validation.warnings.map((warning, i) => (react_1.default.createElement("li", { key: i }, warning)))))),
932
+ !hasError && !hasWarning && (react_1.default.createElement("div", { className: "text-green-400" }, "\u2713 Valid condition expression"))))));
933
+ })())),
934
+ react_1.default.createElement("div", { className: "p-4 border-t border-[#2a2a3e] flex items-center justify-end gap-2" },
935
+ react_1.default.createElement("button", { onClick: () => setEditingCondition(null), className: "px-4 py-2 bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300 rounded transition-colors" }, "Cancel"),
936
+ react_1.default.createElement("button", { onClick: () => {
937
+ // Parse and save the condition
938
+ const parseCondition = (conditionStr) => {
939
+ const conditions = [];
940
+ if (!conditionStr.trim())
941
+ return conditions;
942
+ const parts = conditionStr.split(/\s+and\s+/i);
943
+ parts.forEach(part => {
944
+ part = part.trim();
945
+ if (part.startsWith('not ')) {
946
+ const varMatch = part.match(/not\s+\$(\w+)/);
947
+ if (varMatch) {
948
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_NOT_SET });
949
+ }
950
+ }
951
+ else if (part.includes('>=')) {
952
+ const match = part.match(/\$(\w+)\s*>=\s*(.+)/);
953
+ if (match) {
954
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
955
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
956
+ }
957
+ }
958
+ else if (part.includes('<=')) {
959
+ const match = part.match(/\$(\w+)\s*<=\s*(.+)/);
960
+ if (match) {
961
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
962
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_EQUAL, value: isNaN(Number(value)) ? value : Number(value) });
963
+ }
964
+ }
965
+ else if (part.includes('!=')) {
966
+ const match = part.match(/\$(\w+)\s*!=\s*(.+)/);
967
+ if (match) {
968
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
969
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.NOT_EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
970
+ }
971
+ }
972
+ else if (part.includes('==')) {
973
+ const match = part.match(/\$(\w+)\s*==\s*(.+)/);
974
+ if (match) {
975
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
976
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.EQUALS, value: isNaN(Number(value)) ? value : Number(value) });
977
+ }
978
+ }
979
+ else if (part.includes('>') && !part.includes('>=')) {
980
+ const match = part.match(/\$(\w+)\s*>\s*(.+)/);
981
+ if (match) {
982
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
983
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.GREATER_THAN, value: isNaN(Number(value)) ? value : Number(value) });
984
+ }
985
+ }
986
+ else if (part.includes('<') && !part.includes('<=')) {
987
+ const match = part.match(/\$(\w+)\s*<\s*(.+)/);
988
+ if (match) {
989
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
990
+ conditions.push({ flag: match[1], operator: constants_1.CONDITION_OPERATOR.LESS_THAN, value: isNaN(Number(value)) ? value : Number(value) });
991
+ }
992
+ }
993
+ else {
994
+ const varMatch = part.match(/\$(\w+)/);
995
+ if (varMatch) {
996
+ conditions.push({ flag: varMatch[1], operator: constants_1.CONDITION_OPERATOR.IS_SET });
997
+ }
998
+ }
999
+ });
1000
+ return conditions;
1001
+ };
1002
+ if (editingCondition.type === 'block' && editingCondition.blockIdx !== undefined) {
1003
+ const newBlocks = [...node.conditionalBlocks];
1004
+ newBlocks[editingCondition.blockIdx] = {
1005
+ ...newBlocks[editingCondition.blockIdx],
1006
+ condition: parseCondition(editingCondition.value)
1007
+ };
1008
+ onUpdate({ conditionalBlocks: newBlocks });
1009
+ setConditionInputs(prev => ({ ...prev, [editingCondition.id]: editingCondition.value }));
1010
+ }
1011
+ else if (editingCondition.type === 'choice' && editingCondition.choiceIdx !== undefined) {
1012
+ if (editingCondition.value.trim()) {
1013
+ const newConditions = parseCondition(editingCondition.value);
1014
+ onUpdateChoice(editingCondition.choiceIdx, {
1015
+ conditions: newConditions.length > 0 ? newConditions : []
1016
+ });
1017
+ }
1018
+ else {
1019
+ onUpdateChoice(editingCondition.choiceIdx, { conditions: [] });
1020
+ }
1021
+ setConditionInputs(prev => ({ ...prev, [editingCondition.id]: editingCondition.value }));
1022
+ }
1023
+ setEditingCondition(null);
1024
+ }, className: "px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors" }, "Save"))))))));
1025
+ }