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