jaml-ui 0.21.3 → 0.21.4

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 (119) hide show
  1. package/DESIGN.md +36 -6
  2. package/dist/components/JamlAnalyzerFullscreen.d.ts +1 -1
  3. package/dist/components/JamlAnalyzerFullscreen.js +5 -81
  4. package/dist/components/JamlCurator.js +1 -1
  5. package/dist/components/JamlSpeedometer.d.ts +7 -2
  6. package/dist/components/JamlSpeedometer.js +8 -15
  7. package/dist/components/jamlMap/JamlMapEditor.js +42 -38
  8. package/dist/components/jamlMap/JokerPicker.js +2 -2
  9. package/dist/components/jamlMap/MysterySlot.js +4 -4
  10. package/dist/hooks/useSearch.d.ts +2 -1
  11. package/dist/hooks/useSearch.js +111 -8
  12. package/dist/lib/SpriteMapper.d.ts +10 -0
  13. package/dist/lib/SpriteMapper.js +48 -0
  14. package/dist/lib/cardParser.d.ts +8 -0
  15. package/dist/lib/cardParser.js +65 -0
  16. package/dist/lib/classes/BuyMetaData.d.ts +11 -0
  17. package/dist/lib/classes/BuyMetaData.js +1 -0
  18. package/dist/lib/config.d.ts +13 -0
  19. package/dist/lib/config.js +15 -0
  20. package/dist/lib/const.d.ts +61 -0
  21. package/dist/lib/const.js +521 -0
  22. package/dist/lib/data/constants.d.ts +11 -0
  23. package/dist/lib/data/constants.js +17 -0
  24. package/dist/lib/hooks/useDragScroll.d.ts +4 -0
  25. package/dist/lib/hooks/useDragScroll.js +48 -0
  26. package/dist/lib/hooks/useJamlFilter.d.ts +48 -0
  27. package/dist/lib/hooks/useJamlFilter.js +219 -0
  28. package/dist/lib/hooks/useSeedAnalyzer.d.ts +6 -0
  29. package/dist/lib/hooks/useSeedAnalyzer.js +48 -0
  30. package/dist/lib/jaml/jamlCompletion.d.ts +12 -0
  31. package/dist/lib/jaml/jamlCompletion.js +13 -0
  32. package/dist/lib/jaml/jamlData.d.ts +3 -0
  33. package/dist/lib/jaml/jamlData.js +8 -0
  34. package/dist/lib/jaml/jamlObjectives.d.ts +13 -0
  35. package/dist/lib/jaml/jamlObjectives.js +97 -0
  36. package/dist/lib/jaml/jamlParser.d.ts +14 -0
  37. package/dist/lib/jaml/jamlParser.js +47 -0
  38. package/dist/lib/jaml/jamlPresets.d.ts +8 -0
  39. package/dist/lib/jaml/jamlPresets.js +61 -0
  40. package/dist/lib/jaml/jamlSchema.d.ts +54 -0
  41. package/dist/lib/jaml/jamlSchema.js +91 -0
  42. package/dist/lib/parseDailyRitual.d.ts +45 -0
  43. package/dist/lib/parseDailyRitual.js +69 -0
  44. package/dist/lib/tts/getRevealPos.d.ts +5 -0
  45. package/dist/lib/tts/getRevealPos.js +16 -0
  46. package/dist/lib/tts/splitTtsDisplay.d.ts +19 -0
  47. package/dist/lib/tts/splitTtsDisplay.js +35 -0
  48. package/dist/lib/types.d.ts +121 -0
  49. package/dist/lib/types.js +1 -0
  50. package/dist/lib/utils.d.ts +2 -0
  51. package/dist/lib/utils.js +5 -0
  52. package/dist/ui/JimboIconButton.d.ts +10 -0
  53. package/dist/ui/JimboIconButton.js +28 -0
  54. package/dist/ui/JimboInputModal.d.ts +13 -0
  55. package/dist/ui/JimboInputModal.js +60 -0
  56. package/dist/ui/JimboSelect.d.ts +18 -0
  57. package/dist/ui/JimboSelect.js +43 -0
  58. package/dist/ui/PanelSplitter.d.ts +7 -0
  59. package/dist/ui/PanelSplitter.js +76 -0
  60. package/dist/ui/ide/AgnosticSeedCard.d.ts +19 -0
  61. package/dist/ui/ide/AgnosticSeedCard.js +48 -0
  62. package/dist/ui/ide/DeckSprite.d.ts +1 -0
  63. package/dist/ui/ide/DeckSprite.js +2 -0
  64. package/dist/ui/ide/JamlBuilder.d.ts +1 -0
  65. package/dist/ui/ide/JamlBuilder.js +112 -0
  66. package/dist/ui/ide/JamlEditor.d.ts +7 -0
  67. package/dist/ui/ide/JamlEditor.js +496 -0
  68. package/dist/ui/ide/JamlEditorMonaco.d.ts +8 -0
  69. package/dist/ui/ide/JamlEditorMonaco.js +78 -0
  70. package/dist/ui/ide/WasmStatus.d.ts +1 -0
  71. package/dist/ui/ide/WasmStatus.js +42 -0
  72. package/dist/ui/jimbo.css +336 -31
  73. package/dist/ui/jimboApp.d.ts +12 -0
  74. package/dist/ui/jimboApp.js +15 -0
  75. package/dist/ui/jimboInfoCard.d.ts +31 -0
  76. package/dist/ui/jimboInfoCard.js +26 -0
  77. package/dist/ui/jimboInset.d.ts +9 -0
  78. package/dist/ui/jimboInset.js +9 -0
  79. package/dist/ui/jimboSectionHeader.d.ts +11 -0
  80. package/dist/ui/jimboSectionHeader.js +9 -0
  81. package/dist/ui/jimboStatGrid.d.ts +13 -0
  82. package/dist/ui/jimboStatGrid.js +9 -0
  83. package/dist/ui/jimboWordmark.d.ts +10 -0
  84. package/dist/ui/jimboWordmark.js +9 -0
  85. package/dist/ui/mascot/JammySpeechBox.d.ts +9 -0
  86. package/dist/ui/mascot/JammySpeechBox.js +30 -0
  87. package/dist/ui/mascot/SeedMascot.d.ts +37 -0
  88. package/dist/ui/mascot/SeedMascot.js +17 -0
  89. package/dist/ui/mascot/index.d.ts +3 -0
  90. package/dist/ui/mascot/index.js +3 -0
  91. package/dist/ui/mascot/menuConfig.d.ts +102 -0
  92. package/dist/ui/mascot/menuConfig.js +12 -0
  93. package/dist/ui/panel.d.ts +1 -1
  94. package/dist/ui/panel.js +3 -21
  95. package/dist/ui/radial/RadialBadge.d.ts +17 -0
  96. package/dist/ui/radial/RadialBadge.js +43 -0
  97. package/dist/ui/radial/RadialBreadcrumb.d.ts +12 -0
  98. package/dist/ui/radial/RadialBreadcrumb.js +18 -0
  99. package/dist/ui/radial/RadialButton.d.ts +61 -0
  100. package/dist/ui/radial/RadialButton.js +102 -0
  101. package/dist/ui/radial/RadialMenu.d.ts +38 -0
  102. package/dist/ui/radial/RadialMenu.js +168 -0
  103. package/dist/ui/radial/RadialPill.d.ts +18 -0
  104. package/dist/ui/radial/RadialPill.js +15 -0
  105. package/dist/ui/radial/index.d.ts +16 -0
  106. package/dist/ui/radial/index.js +18 -0
  107. package/dist/ui/radial/radialMenuStore.d.ts +31 -0
  108. package/dist/ui/radial/radialMenuStore.js +122 -0
  109. package/dist/ui/radial/radialMenuViewport.d.ts +6 -0
  110. package/dist/ui/radial/radialMenuViewport.js +59 -0
  111. package/dist/ui/radial/useRadialMenu.d.ts +35 -0
  112. package/dist/ui/radial/useRadialMenu.js +107 -0
  113. package/dist/ui/showcase.d.ts +14 -6
  114. package/dist/ui/showcase.js +13 -21
  115. package/dist/ui/tokens.d.ts +5 -19
  116. package/dist/ui/tokens.js +5 -21
  117. package/dist/ui.d.ts +14 -0
  118. package/dist/ui.js +15 -0
  119. package/package.json +145 -146
@@ -0,0 +1,496 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import yaml from 'js-yaml';
5
+ import { Plus, Minus, Check } from 'lucide-react';
6
+ import { JamlCompletionService } from '../../lib/jaml/jamlCompletion';
7
+ // Balatro Colors - High Contrast Theme
8
+ const COLORS = {
9
+ white: '#FFFFFF',
10
+ black: '#000000',
11
+ red: '#ff5e5e', // Balatro Red
12
+ blue: '#4bb1ff', // Balatro Blue
13
+ green: '#46bc77', // Balatro Green
14
+ purple: '#9074ff', // Balatro Purple
15
+ orange: '#ff9d4d', // Balatro Orange
16
+ yellow: '#ffcf4d', // Balatro Yellow
17
+ gold: '#efb82d',
18
+ // Editor background
19
+ editorBg: '#0f1416',
20
+ editorBgAlt: '#1c2629',
21
+ };
22
+ const DEFAULT_JAML = `# My JAML Filter
23
+ name: My Filter
24
+ deck: Red
25
+ stake: White
26
+
27
+ must:
28
+ - joker: Blueprint
29
+ edition: Negative
30
+ antes: [1, 2, 3]
31
+
32
+ should:
33
+ - soulJoker: Any
34
+ score: 5
35
+ `;
36
+ const METADATA_KEYS = ['name', 'author', 'description', 'deck', 'stake', 'label'];
37
+ const REQUIRED_KEYS = ['joker', 'soulJoker', 'voucher', 'tarotCard', 'planetCard', 'spectralCard', 'standardCard', 'tag', 'boss', 'event'];
38
+ // --- Sub-components ---
39
+ function SuggestionList({ suggestions, selectedIndex, onSelect, onHover }) {
40
+ if (suggestions.length === 0) {
41
+ return (_jsx("div", { className: "p-2 text-xs text-white/40 italic", children: "No suggestions..." }));
42
+ }
43
+ return (_jsx("div", { className: "flex flex-col max-h-[200px] overflow-y-auto", onMouseDown: e => e.preventDefault(), children: suggestions.map((s, idx) => {
44
+ const isSelected = idx === selectedIndex;
45
+ return (_jsxs("div", { onClick: () => onSelect(s.text), onMouseEnter: () => onHover(idx), className: `
46
+ flex items-center justify-between px-3 py-1.5 cursor-pointer font-mono text-[13px] transition-colors
47
+ ${isSelected ? 'bg-[var(--balatro-gold)] text-black' : 'text-white hover:bg-white/10'}
48
+ `, children: [_jsx("span", { children: s.displayText }), isSelected && _jsx("span", { className: "opacity-50 text-[11px] ml-2", children: "\u21B5" })] }, `${s.text}-${idx}`));
49
+ }) }));
50
+ }
51
+ function AntesToggle({ values, onToggle, onStartEdit, color, darkColor }) {
52
+ const [expanded, setExpanded] = useState(false);
53
+ const maxAnte = 8;
54
+ const selectedAntes = new Set(values.map(v => parseInt(v, 10)).filter(n => !isNaN(n)));
55
+ const getDisplayText = () => {
56
+ if (selectedAntes.size === 0)
57
+ return 'tap to select';
58
+ const sorted = Array.from(selectedAntes).sort((a, b) => a - b);
59
+ if (sorted.length === 1)
60
+ return `Ante ${sorted[0]}`;
61
+ const min = sorted[0];
62
+ const max = sorted[sorted.length - 1];
63
+ const isConsecutive = sorted.every((v, i) => i === 0 || v === sorted[i - 1] + 1);
64
+ if (isConsecutive)
65
+ return `Antes ${min}-${max}`;
66
+ return `Antes ${sorted.join(', ')}`;
67
+ };
68
+ if (!expanded) {
69
+ return (_jsx("div", { onClick: () => setExpanded(true), className: "jaml-block", style: {
70
+ '--jaml-bg': selectedAntes.size > 0 ? `${color}20` : `${COLORS.red}15`,
71
+ '--jaml-border': `1px solid ${selectedAntes.size > 0 ? color : COLORS.red}`,
72
+ '--jaml-color': selectedAntes.size > 0 ? color : COLORS.red,
73
+ '--jaml-min-w': '100px',
74
+ '--jaml-shadow': selectedAntes.size > 0 ? `0 0 10px ${color}20` : 'none',
75
+ }, children: getDisplayText() }));
76
+ }
77
+ return (_jsxs("div", { className: "flex flex-row items-center", children: [Array.from({ length: maxAnte + 1 }, (_, i) => i).map((ante) => {
78
+ const isSelected = selectedAntes.has(ante);
79
+ const borderRadiusClass = ante === 0 ? 'jaml-ante-btn--first' : ante === maxAnte ? 'jaml-ante-btn--last' : 'jaml-ante-btn';
80
+ return (_jsx("div", { onClick: (e) => {
81
+ e.stopPropagation();
82
+ onToggle(ante.toString());
83
+ }, className: `jaml-block ${borderRadiusClass}`, style: {
84
+ '--jaml-min-w': '28px',
85
+ '--jaml-bg': isSelected ? `${color}40` : 'transparent',
86
+ '--jaml-border': `1px solid ${isSelected ? color : 'rgba(255,255,255,0.2)'}`,
87
+ '--jaml-color': isSelected ? color : 'rgba(255,255,255,0.4)',
88
+ borderRight: ante < maxAnte ? 'none' : undefined,
89
+ }, children: ante }, ante));
90
+ }), _jsx("div", { onClick: () => setExpanded(false), className: "jaml-block jaml-antes-confirm", children: _jsx(Check, { size: 16, strokeWidth: 3 }) })] }));
91
+ }
92
+ // Custom Hook for Floating Position (Viewport-aware)
93
+ function useFloatingPosition(targetRef, isOpen) {
94
+ const [coords, setCoords] = useState(null);
95
+ useEffect(() => {
96
+ if (!isOpen || !targetRef.current) {
97
+ setCoords(null);
98
+ return;
99
+ }
100
+ const updatePosition = () => {
101
+ if (!targetRef.current)
102
+ return;
103
+ const rect = targetRef.current.getBoundingClientRect();
104
+ const spaceBelow = window.innerHeight - rect.bottom;
105
+ const spaceAbove = rect.top;
106
+ // Prefer bottom, unless too close to edge
107
+ const showBelow = spaceBelow > 220 || spaceBelow > spaceAbove;
108
+ setCoords({
109
+ left: rect.left,
110
+ top: showBelow ? rect.bottom + 4 : rect.top - 4,
111
+ position: showBelow ? 'bottom' : 'top'
112
+ });
113
+ };
114
+ updatePosition();
115
+ // Update on scroll/resize to keep pinned
116
+ window.addEventListener('scroll', updatePosition, true);
117
+ window.addEventListener('resize', updatePosition);
118
+ return () => {
119
+ window.removeEventListener('scroll', updatePosition, true);
120
+ window.removeEventListener('resize', updatePosition);
121
+ };
122
+ }, [isOpen, targetRef]);
123
+ return coords;
124
+ }
125
+ // --- Main Editor Component ---
126
+ export default function JamlEditor({ initialJaml, onJamlChange, className }) {
127
+ const [lines, setLines] = useState([]);
128
+ const [editingLineId, setEditingLineId] = useState(null);
129
+ const [editingPart, setEditingPart] = useState(null);
130
+ const [editingArrayIndex, setEditingArrayIndex] = useState(null);
131
+ const [focusedLineIndex, setFocusedLineIndex] = useState(0);
132
+ const editorRef = useRef(null);
133
+ // -- Parsing Logic --
134
+ const parseJamlToLines = useCallback((text) => {
135
+ const rawLines = text.split('\n');
136
+ let currentClauseType;
137
+ return rawLines.map((raw, index) => {
138
+ const indent = raw.search(/\S|$/);
139
+ const trimmed = raw.trim();
140
+ const isComment = trimmed.startsWith('#');
141
+ const isArrayItem = trimmed.startsWith('- ');
142
+ let key;
143
+ let value;
144
+ let isArrayValue = false;
145
+ let arrayValues;
146
+ if (!isComment && trimmed.includes(':')) {
147
+ const colonIndex = trimmed.indexOf(':');
148
+ const rawKey = trimmed.slice(isArrayItem ? 2 : 0, colonIndex).trim();
149
+ const rawValue = trimmed.slice(colonIndex + 1).trim();
150
+ key = rawKey;
151
+ value = rawValue || undefined;
152
+ if (value && value.startsWith('[') && value.endsWith(']')) {
153
+ isArrayValue = true;
154
+ const inner = value.slice(1, -1);
155
+ arrayValues = inner.split(',').map(v => v.trim()).filter(v => v);
156
+ }
157
+ }
158
+ if (indent === 0 && !isArrayItem)
159
+ currentClauseType = undefined;
160
+ // Basic clause type tracking (heuristic)
161
+ if (isArrayItem && key && REQUIRED_KEYS.includes(key)) {
162
+ currentClauseType = key;
163
+ }
164
+ let validationState = 'complete';
165
+ let isInvalidValue = false;
166
+ if (key) {
167
+ if (METADATA_KEYS.includes(key))
168
+ validationState = 'metadata';
169
+ else if (REQUIRED_KEYS.includes(key))
170
+ validationState = value ? 'complete' : 'required-incomplete';
171
+ else if (!value && key !== 'must' && key !== 'should' && key !== 'mustNot')
172
+ validationState = 'optional-incomplete';
173
+ if (value && value.startsWith('~') && value.endsWith('~')) {
174
+ isInvalidValue = true;
175
+ validationState = 'invalid';
176
+ }
177
+ }
178
+ return {
179
+ id: `line-${index}`,
180
+ raw, indent, key, value, isComment, isArrayItem, lineNumber: index,
181
+ clauseType: currentClauseType, validationState, isInvalidValue, isArrayValue, arrayValues
182
+ };
183
+ });
184
+ }, []);
185
+ const linesToJaml = useCallback((linesList) => linesList.map(l => l.raw).join('\n'), []);
186
+ useEffect(() => {
187
+ const text = initialJaml || DEFAULT_JAML;
188
+ // Prevent feedback loop: If the incoming text matches what we already have, don't re-parse/reset.
189
+ // This is crucial when onJamlChange -> parent -> initialJaml cycle exists.
190
+ if (lines.length > 0 && linesToJaml(lines) === text)
191
+ return;
192
+ setLines(parseJamlToLines(text));
193
+ }, [initialJaml, parseJamlToLines, lines, linesToJaml]);
194
+ const updateLineValue = useCallback((lineId, part, newValue) => {
195
+ const newLines = lines.map(line => {
196
+ if (line.id !== lineId)
197
+ return line;
198
+ const indent = ' '.repeat(line.indent);
199
+ const prefix = line.isArrayItem ? '- ' : '';
200
+ let newRaw = line.raw;
201
+ if (part === 'value' && line.key) {
202
+ newRaw = `${indent}${prefix}${line.key}: ${newValue}`;
203
+ }
204
+ else if (part === 'key') {
205
+ const valuePart = line.value ? `: ${line.value}` : ':';
206
+ newRaw = `${indent}${prefix}${newValue}${valuePart}`;
207
+ }
208
+ let isArrayValue = false;
209
+ let arrayValues;
210
+ if (newValue && newValue.startsWith('[') && newValue.endsWith(']')) {
211
+ isArrayValue = true;
212
+ arrayValues = newValue.slice(1, -1).split(',').map(v => v.trim()).filter(v => v);
213
+ }
214
+ return { ...line, raw: newRaw, [part]: newValue, isArrayValue, arrayValues };
215
+ });
216
+ setLines(newLines);
217
+ const txt = linesToJaml(newLines);
218
+ if (onJamlChange) {
219
+ try {
220
+ onJamlChange(txt, yaml.load(txt), true);
221
+ }
222
+ catch {
223
+ onJamlChange(txt, null, false);
224
+ }
225
+ }
226
+ }, [lines, linesToJaml, onJamlChange]);
227
+ const updateArrayItem = useCallback((lineId, arrayIndex, newValue) => {
228
+ const newLines = lines.map(line => {
229
+ if (line.id !== lineId || !line.arrayValues)
230
+ return line;
231
+ const newArrayValues = [...line.arrayValues];
232
+ newArrayValues[arrayIndex] = newValue;
233
+ const newArrayStr = `[${newArrayValues.join(', ')}]`;
234
+ const indent = ' '.repeat(line.indent);
235
+ const prefix = line.isArrayItem ? '- ' : '';
236
+ const newRaw = `${indent}${prefix}${line.key}: ${newArrayStr}`;
237
+ return { ...line, raw: newRaw, value: newArrayStr, arrayValues: newArrayValues };
238
+ });
239
+ setLines(newLines);
240
+ const txt = linesToJaml(newLines);
241
+ if (onJamlChange) {
242
+ try {
243
+ onJamlChange(txt, yaml.load(txt), true);
244
+ }
245
+ catch {
246
+ onJamlChange(txt, null, false);
247
+ }
248
+ }
249
+ }, [lines, linesToJaml, onJamlChange]);
250
+ const addArrayItem = useCallback((lineId, newValue) => {
251
+ const newLines = lines.map(line => {
252
+ if (line.id !== lineId)
253
+ return line;
254
+ const newArrayValues = [...(line.arrayValues || []), newValue];
255
+ const newArrayStr = `[${newArrayValues.join(', ')}]`;
256
+ const indent = ' '.repeat(line.indent);
257
+ const prefix = line.isArrayItem ? '- ' : '';
258
+ const newRaw = `${indent}${prefix}${line.key}: ${newArrayStr}`;
259
+ return { ...line, raw: newRaw, value: newArrayStr, arrayValues: newArrayValues, isArrayValue: true };
260
+ });
261
+ setLines(newLines);
262
+ const txt = linesToJaml(newLines);
263
+ if (onJamlChange) {
264
+ try {
265
+ onJamlChange(txt, yaml.load(txt), true);
266
+ }
267
+ catch {
268
+ onJamlChange(txt, null, false);
269
+ }
270
+ }
271
+ }, [lines, linesToJaml, onJamlChange]);
272
+ const removeArrayItem = useCallback((lineId, arrayIndex) => {
273
+ const newLines = lines.map(line => {
274
+ if (line.id !== lineId || !line.arrayValues)
275
+ return line;
276
+ const newArrayValues = line.arrayValues.filter((_, i) => i !== arrayIndex);
277
+ const newArrayStr = newArrayValues.length > 0 ? `[${newArrayValues.join(', ')}]` : '';
278
+ const indent = ' '.repeat(line.indent);
279
+ const prefix = line.isArrayItem ? '- ' : '';
280
+ const newRaw = `${indent}${prefix}${line.key}: ${newArrayStr}`;
281
+ return { ...line, raw: newRaw, value: newArrayStr || undefined, arrayValues: newArrayValues.length > 0 ? newArrayValues : undefined, isArrayValue: newArrayValues.length > 0 };
282
+ });
283
+ setLines(newLines);
284
+ const txt = linesToJaml(newLines);
285
+ if (onJamlChange) {
286
+ try {
287
+ onJamlChange(txt, yaml.load(txt), true);
288
+ }
289
+ catch {
290
+ onJamlChange(txt, null, false);
291
+ }
292
+ }
293
+ }, [lines, linesToJaml, onJamlChange]);
294
+ const addLineAfter = useCallback((afterLineId, content) => {
295
+ const index = lines.findIndex(l => l.id === afterLineId);
296
+ if (index === -1)
297
+ return;
298
+ const currentLine = lines[index];
299
+ const newLineRaw = ' '.repeat(currentLine.indent) + content;
300
+ const newParsed = parseJamlToLines(newLineRaw)[0];
301
+ const newLine = { ...newParsed, id: `line-new-${Date.now()}`, clauseType: currentLine.clauseType };
302
+ const newLines = [...lines];
303
+ newLines.splice(index + 1, 0, newLine);
304
+ const renumbered = newLines.map((l, i) => ({ ...l, lineNumber: i, id: `line-${i}` })); // simplified ID update
305
+ setLines(renumbered);
306
+ const txt = linesToJaml(renumbered);
307
+ if (onJamlChange) {
308
+ try {
309
+ onJamlChange(txt, yaml.load(txt), true);
310
+ }
311
+ catch {
312
+ onJamlChange(txt, null, false);
313
+ }
314
+ }
315
+ }, [lines, linesToJaml, onJamlChange, parseJamlToLines]);
316
+ const deleteLine = useCallback((lineId) => {
317
+ const filtered = lines.filter(l => l.id !== lineId);
318
+ const renumbered = filtered.map((l, i) => ({ ...l, lineNumber: i, id: `line-${i}` }));
319
+ setLines(renumbered);
320
+ const txt = linesToJaml(renumbered);
321
+ if (onJamlChange) {
322
+ try {
323
+ onJamlChange(txt, yaml.load(txt), true);
324
+ }
325
+ catch {
326
+ onJamlChange(txt, null, false);
327
+ }
328
+ }
329
+ }, [lines, linesToJaml, onJamlChange]);
330
+ // -- Render --
331
+ const maxKeyLengthByIndent = useMemo(() => {
332
+ const byIndent = {};
333
+ for (const line of lines) {
334
+ if (line.key) {
335
+ // Cap the alignment width at 12 characters to prevent huge gaps
336
+ byIndent[line.indent] = Math.min(Math.max(byIndent[line.indent] || 0, line.key.length), 12);
337
+ }
338
+ }
339
+ return byIndent;
340
+ }, [lines]);
341
+ return (_jsxs("div", { ref: editorRef, className: `flex flex-col bg-[#0f1416] text-white font-mono text-[13px] leading-[1.8] outline-none rounded-md p-4 overflow-auto min-h-[500px] border border-white/5 ${className}`, tabIndex: 0, children: [_jsx("div", { className: "flex flex-col gap-0.5", children: lines.map((line, index) => (_jsx(JamlLine, { line: line, keyWidth: maxKeyLengthByIndent[line.indent] || 8, isEditing: editingLineId === line.id, editingPart: editingLineId === line.id ? editingPart : null, editingArrayIndex: editingLineId === line.id ? editingArrayIndex : null, onStartEdit: (part, idx) => {
342
+ setEditingLineId(line.id);
343
+ setEditingPart(part);
344
+ setEditingArrayIndex(idx ?? null);
345
+ setFocusedLineIndex(index);
346
+ }, onEndEdit: () => {
347
+ setEditingLineId(null);
348
+ setEditingPart(null);
349
+ setEditingArrayIndex(null);
350
+ }, onChange: (part, val) => updateLineValue(line.id, part, val), onArrayItemChange: (idx, val) => updateArrayItem(line.id, idx, val), onArrayItemAdd: (val) => addArrayItem(line.id, val), onArrayItemRemove: (idx) => removeArrayItem(line.id, idx), onDelete: () => deleteLine(line.id), onAddLine: (content) => addLineAfter(line.id, content) }, line.id))) }), _jsxs("div", { className: "mt-4 p-2 bg-black/40 rounded border border-white/5 flex flex-wrap gap-4 text-[12px] text-white/50 font-mono", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "jaml-legend-dot jaml-legend-dot--red" }), " required"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "jaml-legend-dot jaml-legend-dot--blue" }), " optional"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "jaml-legend-dot jaml-legend-dot--green" }), " complete"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "jaml-legend-dot jaml-legend-dot--purple" }), " metadata"] }), _jsx("span", { className: "ml-auto opacity-40", children: "Click to edit \u2022 Tab to navigate" })] })] }));
351
+ }
352
+ function JamlLine({ line, keyWidth, isEditing, editingPart, editingArrayIndex, onStartEdit, onEndEdit, onChange, onArrayItemChange, onArrayItemAdd, onArrayItemRemove, onDelete, onAddLine }) {
353
+ const [, setHovered] = useState(false);
354
+ const [suggestions, setSuggestions] = useState([]);
355
+ const [selectedIndex, setSelectedIndex] = useState(0);
356
+ const [localValue, setLocalValue] = useState('');
357
+ const inputRef = useRef(null);
358
+ const targetRef = useRef(null);
359
+ const floatingCoords = useFloatingPosition(targetRef, isEditing && suggestions.length > 0);
360
+ // Colors
361
+ const getBaseColor = () => {
362
+ switch (line.validationState) {
363
+ case 'required-incomplete': return COLORS.red;
364
+ case 'optional-incomplete': return COLORS.blue;
365
+ case 'complete': return COLORS.green;
366
+ case 'invalid': return COLORS.red;
367
+ case 'metadata': return COLORS.purple;
368
+ default: return '#888';
369
+ }
370
+ };
371
+ const getBrightColor = () => {
372
+ switch (line.validationState) {
373
+ case 'required-incomplete': return COLORS.red;
374
+ case 'optional-incomplete': return COLORS.blue;
375
+ case 'complete': return COLORS.green;
376
+ case 'invalid': return COLORS.red;
377
+ case 'metadata': return COLORS.purple;
378
+ default: return '#FFF';
379
+ }
380
+ };
381
+ // Suggestions Logic
382
+ useEffect(() => {
383
+ if (isEditing && editingPart) {
384
+ let textContext = line.raw;
385
+ if (editingPart === 'value' && line.key) {
386
+ textContext = `${line.key}: ${localValue}`;
387
+ }
388
+ else if (editingPart === 'key') {
389
+ textContext = localValue;
390
+ }
391
+ const sugs = JamlCompletionService.getCompletions(textContext);
392
+ setSuggestions(sugs.slice(0, 10));
393
+ setSelectedIndex(0);
394
+ }
395
+ }, [isEditing, editingPart, localValue, line.raw, line.key]);
396
+ // Input Focus
397
+ useEffect(() => {
398
+ if (isEditing && inputRef.current) {
399
+ inputRef.current.focus();
400
+ if (editingPart === 'key')
401
+ setLocalValue(line.key || '');
402
+ else if (editingPart === 'value')
403
+ setLocalValue((line.value || '').replace(/^~|~$/g, ''));
404
+ else if (editingPart === 'arrayItem' && editingArrayIndex !== null)
405
+ setLocalValue(line.arrayValues?.[editingArrayIndex] || '');
406
+ else
407
+ setLocalValue('');
408
+ }
409
+ }, [isEditing, editingPart, editingArrayIndex, line]);
410
+ const handleKeyDown = (e) => {
411
+ if (e.key === 'Enter') {
412
+ e.preventDefault();
413
+ const finalVal = (suggestions.length > 0 && selectedIndex >= 0) ? suggestions[selectedIndex].text : localValue;
414
+ if (editingPart === 'key')
415
+ onChange('key', finalVal);
416
+ else if (editingPart === 'value')
417
+ onChange('value', finalVal);
418
+ else if (editingPart === 'arrayItem') {
419
+ if (editingArrayIndex >= (line.arrayValues?.length || 0))
420
+ onArrayItemAdd(finalVal);
421
+ else
422
+ onArrayItemChange(editingArrayIndex, finalVal);
423
+ }
424
+ onEndEdit();
425
+ }
426
+ else if (e.key === 'ArrowDown') {
427
+ e.preventDefault();
428
+ setSelectedIndex(Math.min(selectedIndex + 1, suggestions.length - 1));
429
+ }
430
+ else if (e.key === 'ArrowUp') {
431
+ e.preventDefault();
432
+ setSelectedIndex(Math.max(selectedIndex - 1, 0));
433
+ }
434
+ else if (e.key === 'Escape') {
435
+ onEndEdit();
436
+ }
437
+ else if (e.key === 'Backspace' && localValue === '' && editingPart === 'arrayItem') {
438
+ onArrayItemRemove(editingArrayIndex);
439
+ onEndEdit();
440
+ }
441
+ };
442
+ if (line.isComment) {
443
+ return _jsx("div", { className: "pl-8 text-stone-400 italic text-xs py-0.5", children: line.raw });
444
+ }
445
+ if (!line.key && !line.raw.trim()) {
446
+ return _jsx("div", { className: "h-5" });
447
+ }
448
+ if ((line.key === 'must' || line.key === 'should' || line.key === 'mustNot') && !line.value) {
449
+ const color = line.key === 'must' ? COLORS.red : COLORS.blue;
450
+ return _jsx("div", { className: "jaml-clause-header pl-8 py-1 text-[16px] border-b border-white/10 mt-4 mb-2 uppercase tracking-widest", style: { '--jaml-color': color }, children: line.raw });
451
+ }
452
+ const indentSpaces = ' '.repeat(line.indent);
453
+ const prefix = line.isArrayItem ? '- ' : '';
454
+ // Helper: popover CSS vars from floating coords
455
+ const popoverVars = floatingCoords ? {
456
+ '--popover-top': `${floatingCoords.top}px`,
457
+ '--popover-left': `${floatingCoords.left}px`,
458
+ '--popover-transform': floatingCoords.position === 'top' ? 'translateY(-100%)' : 'none',
459
+ } : {};
460
+ return (_jsxs("div", { className: "relative flex items-center py-0.5 group", onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [_jsx("div", { className: "w-6 h-full flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity", onClick: (e) => { e.stopPropagation(); onDelete(); }, children: _jsx(Minus, { size: 12, className: "text-orange-500 hover:bg-orange-100 rounded" }) }), _jsxs("span", { className: "whitespace-pre text-stone-400", children: [indentSpaces, prefix] }), line.key && (_jsxs("div", { className: "relative", children: [_jsx("div", { ref: editingPart === 'key' ? targetRef : null, onClick: (e) => { e.stopPropagation(); onStartEdit('key'); }, className: "jaml-block jaml-block--start", style: {
461
+ '--jaml-min-w': `${keyWidth}ch`,
462
+ '--jaml-color': (isEditing && editingPart === 'key') ? getBrightColor() : getBaseColor(),
463
+ '--jaml-bg': (isEditing && editingPart === 'key') ? `${getBrightColor()}15` : 'transparent',
464
+ }, children: isEditing && editingPart === 'key' ? (_jsx("input", { ref: inputRef, title: "Edit key", value: localValue, onChange: e => setLocalValue(e.target.value), onKeyDown: handleKeyDown, onBlur: () => setTimeout(onEndEdit, 200), className: "jaml-inline-input", style: { '--jaml-color': getBrightColor() } })) : line.key }), isEditing && editingPart === 'key' && suggestions.length > 0 && floatingCoords && createPortal(_jsx("div", { className: "jaml-suggestion-popover", style: popoverVars, children: _jsx(SuggestionList, { suggestions: suggestions, selectedIndex: selectedIndex, onHover: setSelectedIndex, onSelect: (val) => {
465
+ onChange('key', val);
466
+ onEndEdit();
467
+ } }) }), document.body)] })), line.key && _jsx("span", { className: "text-stone-400 mr-1", children: ":" }), line.key && (line.isArrayValue && line.arrayValues ? (line.key === 'antes' ? (_jsx(AntesToggle, { values: line.arrayValues, onToggle: (val) => {
468
+ const idx = line.arrayValues.indexOf(val);
469
+ if (idx >= 0)
470
+ onArrayItemRemove(idx);
471
+ else
472
+ onArrayItemAdd(val);
473
+ }, onStartEdit: () => onStartEdit('arrayItem', 0), color: getBrightColor(), darkColor: getBaseColor() })) : (_jsxs("div", { className: "flex gap-0.5 items-center", children: [line.arrayValues.map((val, idx) => (_jsx("div", { onClick: (e) => { e.stopPropagation(); onStartEdit('arrayItem', idx); }, className: "jaml-block", style: {
474
+ '--jaml-min-w': '24px',
475
+ '--jaml-bg': `${getBaseColor()}15`,
476
+ '--jaml-border': `1px solid ${getBaseColor()}40`,
477
+ '--jaml-color': getBaseColor(),
478
+ }, children: isEditing && editingPart === 'arrayItem' && editingArrayIndex === idx ? (_jsx("input", { ref: inputRef, title: "Edit array item", value: localValue, onChange: e => setLocalValue(e.target.value), onKeyDown: handleKeyDown, onBlur: () => setTimeout(onEndEdit, 200), className: "bg-transparent w-[3ch] text-center outline-none" })) : val }, idx))), _jsx("div", { onClick: (e) => { e.stopPropagation(); onStartEdit('arrayItem', line.arrayValues.length); }, className: "jaml-block", style: {
479
+ '--jaml-min-w': '24px',
480
+ '--jaml-bg': `${COLORS.green}15`,
481
+ '--jaml-border': `1px solid ${COLORS.green}40`,
482
+ '--jaml-color': COLORS.green,
483
+ }, children: _jsx(Plus, { size: 12 }) })] }))) : (_jsxs("div", { className: "relative", children: [_jsx("div", { ref: editingPart === 'value' ? targetRef : null, onClick: (e) => { e.stopPropagation(); onStartEdit('value'); }, className: "jaml-block", style: {
484
+ '--jaml-min-w': line.value ? undefined : '60px',
485
+ '--jaml-color': line.isInvalidValue ? COLORS.red : (line.value ? getBaseColor() : COLORS.red),
486
+ '--jaml-bg': line.isInvalidValue ? `${COLORS.red}15` : (line.value ? `${getBaseColor()}10` : `${COLORS.red}08`),
487
+ '--jaml-border': `1px solid ${line.isInvalidValue ? `${COLORS.red}60` : (line.value ? `${getBaseColor()}40` : `${COLORS.red}40`)}`,
488
+ '--jaml-text-decoration': line.isInvalidValue ? 'line-through' : 'none',
489
+ }, children: isEditing && editingPart === 'value' ? (_jsx("input", { ref: inputRef, title: "Edit value", value: localValue, onChange: e => setLocalValue(e.target.value), onKeyDown: handleKeyDown, onBlur: () => setTimeout(onEndEdit, 200), className: "jaml-inline-input jaml-inline-input--sized", style: {
490
+ '--jaml-color': getBaseColor(),
491
+ '--jaml-input-w': `${Math.max(localValue.length, 4)}ch`,
492
+ } })) : (line.isInvalidValue ? line.value?.replace(/~/g, '') : (line.value || '???')) }), isEditing && editingPart === 'value' && suggestions.length > 0 && floatingCoords && createPortal(_jsx("div", { className: "jaml-suggestion-popover", style: popoverVars, children: _jsx(SuggestionList, { suggestions: suggestions, selectedIndex: selectedIndex, onHover: setSelectedIndex, onSelect: (val) => {
493
+ onChange('value', val);
494
+ onEndEdit();
495
+ } }) }), document.body)] })))] }));
496
+ }
@@ -0,0 +1,8 @@
1
+ interface JamlEditorMonacoProps {
2
+ value: string;
3
+ onChange: (value: string | undefined) => void;
4
+ diagnostics?: any;
5
+ className?: string;
6
+ }
7
+ export default function JamlEditorMonaco({ value, onChange, diagnostics, className }: JamlEditorMonacoProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef } from 'react';
4
+ import Editor from "@monaco-editor/react";
5
+ import { Copy, Check, AlertCircle } from 'lucide-react';
6
+ import { cn } from '../../lib/utils';
7
+ export default function JamlEditorMonaco({ value, onChange, diagnostics, className }) {
8
+ const editorRef = useRef(null);
9
+ const handleEditorWillMount = (monaco) => {
10
+ // Define Balatro Theme
11
+ monaco.editor.defineTheme('balatro-dark', {
12
+ base: 'vs-dark',
13
+ inherit: true,
14
+ rules: [
15
+ { token: 'comment', foreground: '5f7377', fontStyle: 'italic' },
16
+ { token: 'keyword', foreground: 'fe5f55' }, // Balatro Red
17
+ { token: 'string', foreground: 'eac058' }, // Balatro Gold
18
+ { token: 'number', foreground: '009dff' }, // Balatro Blue
19
+ { token: 'type', foreground: '4bc292' }, // Balatro Green
20
+ ],
21
+ colors: {
22
+ 'editor.background': '#1e2b2d', // Authentic G.C.BLACK variant
23
+ 'editor.foreground': '#ffffff',
24
+ 'editorLineNumber.foreground': '#4f6367',
25
+ 'editorLineNumber.activeForeground': '#eac058',
26
+ 'editor.selectionBackground': '#ffffff20',
27
+ 'editor.inactiveSelectionBackground': '#ffffff10',
28
+ 'editor.lineHighlightBackground': '#00000020',
29
+ 'editorCursor.foreground': '#eac058',
30
+ // Widget/Popover Colors (Fixes User Issue: "White text on white background")
31
+ 'editorWidget.background': '#1e2b2d', // Balatro Dark
32
+ 'editorWidget.border': '#ffffff20',
33
+ 'editorWidget.foreground': '#ffffff',
34
+ // Suggestion List Colors
35
+ 'list.activeSelectionBackground': '#d8b97d', // Balatro Gold Selection
36
+ 'list.activeSelectionForeground': '#1e2b2d', // Dark text on gold
37
+ 'list.hoverBackground': '#2a3b3d',
38
+ 'list.hoverForeground': '#ffffff',
39
+ 'list.focusBackground': '#d8b97d',
40
+ 'list.focusForeground': '#1e2b2d',
41
+ }
42
+ });
43
+ };
44
+ const handleEditorDidMount = (editor, monaco) => {
45
+ editorRef.current = editor;
46
+ // Focus editor
47
+ editor.focus();
48
+ // Optional: Add custom JAML commands/actions here
49
+ };
50
+ const handleFormat = () => {
51
+ if (editorRef.current) {
52
+ editorRef.current.getAction('editor.action.formatDocument').run();
53
+ }
54
+ };
55
+ const handleCopy = () => {
56
+ navigator.clipboard.writeText(value);
57
+ };
58
+ const hasErrors = diagnostics?.errors?.length > 0;
59
+ return (_jsxs("div", { className: cn("flex flex-col h-full bg-[#1e2b2d] border border-white/10 rounded-xl overflow-hidden shadow-2xl group", className), children: [_jsxs("div", { className: "h-10 bg-black/40 border-b border-white/5 flex items-center justify-between px-4 shrink-0 overflow-hidden", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("span", { className: "font-header text-xs text-[var(--balatro-red)] tracking-widest uppercase flex items-center gap-2", children: [_jsx("div", { className: "w-2 h-2 rounded-full bg-[var(--balatro-red)] animate-pulse" }), "JAML EDITOR"] }), _jsx("div", { className: "h-4 w-px bg-white/10" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("button", { onClick: handleFormat, className: "text-[9px] font-pixel text-white/30 hover:text-white/80 uppercase tracking-tighter transition-colors", children: "Format" }), _jsxs("button", { onClick: handleCopy, className: "text-[9px] font-pixel text-white/30 hover:text-white/80 uppercase tracking-tighter transition-colors flex items-center gap-1", children: [_jsx(Copy, { size: 10 }), " Copy"] })] })] }), _jsx("div", { className: "flex items-center gap-2", children: hasErrors ? (_jsxs("div", { className: "flex items-center gap-1.5 px-2 py-0.5 bg-red-500/10 border border-red-500/20 rounded text-[9px] font-pixel text-red-400", children: [_jsx(AlertCircle, { size: 10 }), _jsxs("span", { children: [diagnostics.errors.length, " SYNTAX ERROR(S)"] })] })) : (_jsxs("div", { className: "flex items-center gap-1.5 px-2 py-0.5 bg-green-500/10 border border-green-500/20 rounded text-[9px] font-pixel text-green-400 opacity-60", children: [_jsx(Check, { size: 10 }), _jsx("span", { children: "SYNTAX VALID" })] })) })] }), _jsx("div", { className: "flex-1 min-h-0 relative", children: _jsx(Editor, { height: "100%", defaultLanguage: "yaml", value: value, theme: "balatro-dark", onChange: onChange, onMount: handleEditorDidMount, beforeMount: handleEditorWillMount, options: {
60
+ minimap: { enabled: false },
61
+ scrollBeyondLastLine: false,
62
+ fontSize: 14,
63
+ fontFamily: 'var(--font-mono)',
64
+ lineNumbers: 'on',
65
+ roundedSelection: true,
66
+ readOnly: false,
67
+ cursorStyle: 'line',
68
+ automaticLayout: true,
69
+ padding: { top: 16, bottom: 16 },
70
+ wordWrap: 'on',
71
+ formatOnPaste: true,
72
+ formatOnType: true,
73
+ suggest: {
74
+ showKeywords: true,
75
+ showSnippets: true,
76
+ }
77
+ } }) }), _jsxs("div", { className: "h-6 bg-black/20 border-t border-white/5 flex items-center px-4 justify-between shrink-0", children: [_jsxs("div", { className: "text-[8px] font-pixel text-white/20 uppercase", children: ["Lines: ", value.split('\n').length, " | Encoding: UTF-8"] }), _jsx("div", { className: "text-[8px] font-pixel text-white/20 uppercase tracking-widest", children: "Motely JAML Engine v1.0.4" })] })] }));
78
+ }
@@ -0,0 +1 @@
1
+ export declare function WasmStatus(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import motely, { MotelyWasm } from 'motely-wasm';
5
+ import { Cpu, Loader2, CheckCircle2, XCircle } from 'lucide-react';
6
+ import { cn } from '../../lib/utils';
7
+ export function WasmStatus() {
8
+ const [status, setStatus] = useState('idle');
9
+ const [version, setVersion] = useState(null);
10
+ const [error, setError] = useState(null);
11
+ useEffect(() => {
12
+ let cancelled = false;
13
+ const load = async () => {
14
+ setStatus('loading');
15
+ try {
16
+ await motely.boot();
17
+ if (cancelled)
18
+ return;
19
+ setVersion(MotelyWasm.getVersion());
20
+ setStatus('ready');
21
+ }
22
+ catch (err) {
23
+ console.error("WASM Status Error:", err);
24
+ if (cancelled)
25
+ return;
26
+ setStatus('error');
27
+ setError(err.message || String(err));
28
+ }
29
+ };
30
+ load();
31
+ return () => {
32
+ cancelled = true;
33
+ };
34
+ }, []);
35
+ const displayVersion = version;
36
+ return (_jsxs("div", { className: cn("fixed bottom-10 right-10 z-[100] flex items-center gap-3 px-4 py-2 rounded-full border shadow-2xl backdrop-blur-md transition-all", status === 'ready' ? "bg-green-500/10 border-green-500/50 text-green-400" :
37
+ status === 'error' ? "bg-red-500/10 border-red-500/50 text-red-400" :
38
+ "bg-blue-500/10 border-blue-500/50 text-blue-400"), children: [status === 'loading' ? _jsx(Loader2, { size: 16, className: "animate-spin" }) :
39
+ status === 'ready' ? _jsx(CheckCircle2, { size: 16 }) :
40
+ status === 'error' ? _jsx(XCircle, { size: 16 }) :
41
+ _jsx(Cpu, { size: 16 }), _jsxs("div", { className: "flex flex-col", children: [_jsxs("span", { className: "text-[12px] uppercase tracking-widest leading-tight", children: ["WASM: ", status.toUpperCase()] }), displayVersion && (_jsxs("span", { className: "text-[11px] opacity-70 leading-tight", children: ["v", displayVersion] })), error && _jsx("span", { className: "text-[11px] opacity-70 truncate max-w-[200px] leading-tight", children: error })] })] }));
42
+ }