jaml-ui 0.24.16 → 0.24.18

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 (54) hide show
  1. package/dist/components/DeckSprite.js +2 -0
  2. package/dist/components/GameCard.js +1 -0
  3. package/dist/components/JamlAestheticSelector.js +3 -2
  4. package/dist/components/JamlCurator.js +3 -2
  5. package/dist/components/JamlIde.d.ts +4 -3
  6. package/dist/components/JamlIde.js +3 -31
  7. package/dist/components/JamlIdeToolbar.d.ts +1 -1
  8. package/dist/components/JamlIdeToolbar.js +5 -5
  9. package/dist/components/JamlMapPreview.js +3 -37
  10. package/dist/components/Jimbolate.d.ts +7 -0
  11. package/dist/components/Jimbolate.js +17 -0
  12. package/dist/components/PaginatedFilterBrowser.d.ts +23 -0
  13. package/dist/components/PaginatedFilterBrowser.js +54 -0
  14. package/dist/components/RunConfigModal.js +11 -150
  15. package/dist/components/jamlMap/CategoryPicker.d.ts +1 -2
  16. package/dist/components/jamlMap/CategoryPicker.js +2 -1
  17. package/dist/components/jamlMap/JamlMapEditor.js +8 -10
  18. package/dist/components/jamlMap/JokerPicker.d.ts +1 -2
  19. package/dist/components/jamlMap/JokerPicker.js +3 -7
  20. package/dist/components/jamlMap/MysterySlot.js +0 -15
  21. package/dist/hooks/searchWorker.d.ts +1 -29
  22. package/dist/hooks/searchWorker.js +8 -6
  23. package/dist/hooks/useIntersectionObserver.js +5 -3
  24. package/dist/hooks/useSearch.js +5 -1
  25. package/dist/hooks/useShopStream.js +5 -2
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/lib/cardParser.d.ts +1 -1
  29. package/dist/lib/cardParser.js +19 -17
  30. package/dist/lib/classes/BuyMetaData.d.ts +2 -2
  31. package/dist/lib/const.d.ts +22 -13
  32. package/dist/lib/data/constants.js +10 -9
  33. package/dist/lib/hooks/useJamlFilter.js +5 -5
  34. package/dist/lib/hooks/useSeedAnalyzer.js +7 -1
  35. package/dist/lib/jaml/jamlSchema.d.ts +18 -3
  36. package/dist/lib/jaml/jamlSchema.js +2 -2
  37. package/dist/lib/parseDailyRitual.d.ts +1 -1
  38. package/dist/lib/parseDailyRitual.js +2 -1
  39. package/dist/r3f/Card3D.js +2 -0
  40. package/dist/r3f/JimboBillboard.d.ts +1 -1
  41. package/dist/r3f/JimboBillboard.js +5 -2
  42. package/dist/ui/JimboInputModal.js +8 -2
  43. package/dist/ui/PanelSplitter.js +4 -2
  44. package/dist/ui/hooks.js +14 -5
  45. package/dist/ui/ide/JamlEditor.js +53 -63
  46. package/dist/ui/jimbo.css +70 -16
  47. package/dist/ui/jimboTooltip.js +12 -6
  48. package/dist/ui/panel.d.ts +1 -2
  49. package/dist/ui/panel.js +2 -2
  50. package/dist/ui/radial/RadialButton.js +1 -1
  51. package/dist/ui/showcase.js +1 -1
  52. package/dist/utils/jamlMapPreview.js +2 -1
  53. package/dist/utils/jamlVisualFilter.js +3 -2
  54. package/package.json +12 -6
@@ -4,7 +4,9 @@ import { useRef, useMemo, useState, memo } from 'react';
4
4
  import { useFrame, useLoader } from '@react-three/fiber';
5
5
  import { useSpring, animated } from '@react-spring/three';
6
6
  import * as THREE from 'three';
7
+ // eslint-disable-next-line react-refresh/only-export-components
7
8
  export const CARD_DIMENSIONS = { WIDTH: 0.7, HEIGHT: 0.95, DEPTH: 0.02 };
9
+ // eslint-disable-next-line react-refresh/only-export-components
8
10
  export const CARD_MAGNET = {
9
11
  MAX_TILT_X: 0.36,
10
12
  MAX_TILT_Y: 0.42,
@@ -7,4 +7,4 @@ export interface JimboBillboardProps {
7
7
  yLockOnly?: boolean;
8
8
  position?: [number, number, number];
9
9
  }
10
- export declare function JimboBillboard({ sprite, label, width, height, yLockOnly, position }: JimboBillboardProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export declare function JimboBillboard(props: JimboBillboardProps): import("react/jsx-runtime").JSX.Element | null;
@@ -3,9 +3,12 @@ import { useMemo } from 'react';
3
3
  import { Billboard } from '@react-three/drei';
4
4
  import { useLoader } from '@react-three/fiber';
5
5
  import * as THREE from 'three';
6
- export function JimboBillboard({ sprite, label, width = 3.4, height = 4.5, yLockOnly = false, position = [0, 0, 0] }) {
7
- if (!sprite)
6
+ export function JimboBillboard(props) {
7
+ if (!props.sprite)
8
8
  return null;
9
+ return _jsx(JimboBillboardInner, { ...props, sprite: props.sprite });
10
+ }
11
+ function JimboBillboardInner({ sprite, width = 3.4, height = 4.5, yLockOnly = false, position = [0, 0, 0] }) {
9
12
  // Memoize texture to avoid per-render allocation
10
13
  const texture = useLoader(THREE.TextureLoader, sprite.atlasPath);
11
14
  const clonedTexture = useMemo(() => {
@@ -11,14 +11,20 @@ export function JimboInputModal({ open, title, message, placeholder, initialValu
11
11
  const [value, setValue] = useState(initialValue);
12
12
  const [error, setError] = useState(null);
13
13
  const inputRef = useRef(null);
14
- useEffect(() => {
14
+ const [prevOpen, setPrevOpen] = useState(open);
15
+ if (open !== prevOpen) {
16
+ setPrevOpen(open);
15
17
  if (open) {
16
18
  setValue(initialValue);
17
19
  setError(null);
20
+ }
21
+ }
22
+ useEffect(() => {
23
+ if (open) {
18
24
  const t = setTimeout(() => inputRef.current?.focus(), 30);
19
25
  return () => clearTimeout(t);
20
26
  }
21
- }, [open, initialValue]);
27
+ }, [open]);
22
28
  function submit() {
23
29
  const err = validate?.(value) ?? null;
24
30
  if (err) {
@@ -3,13 +3,15 @@
3
3
  "use client";
4
4
  import { jsx as _jsx } from "react/jsx-runtime";
5
5
  import { JimboColorOption } from "./tokens.js";
6
- import { useCallback, useEffect, useRef } from "react";
6
+ import { useCallback, useEffect, useRef, useLayoutEffect } from "react";
7
7
  const C = JimboColorOption;
8
8
  export function PanelSplitter({ orientation = "vertical", onDrag, onKeyAdjust, "aria-label": ariaLabel, }) {
9
9
  const draggingRef = useRef(false);
10
10
  const lastRef = useRef(0);
11
11
  const onDragRef = useRef(onDrag);
12
- onDragRef.current = onDrag;
12
+ useLayoutEffect(() => {
13
+ onDragRef.current = onDrag;
14
+ });
13
15
  const handlePointerDown = useCallback((e) => {
14
16
  e.preventDefault();
15
17
  e.target.setPointerCapture(e.pointerId);
package/dist/ui/hooks.js CHANGED
@@ -66,14 +66,22 @@ export function useSway(active) {
66
66
  export function useDelayedVisibility(open, delay) {
67
67
  const [visible, setVisible] = useState(open);
68
68
  const [opacity, setOpacity] = useState(open ? 1 : 0);
69
- useEffect(() => {
69
+ const [prevOpen, setPrevOpen] = useState(open);
70
+ if (open !== prevOpen) {
71
+ setPrevOpen(open);
70
72
  if (open) {
71
73
  setVisible(true);
74
+ }
75
+ else {
76
+ setOpacity(0);
77
+ }
78
+ }
79
+ useEffect(() => {
80
+ if (open) {
72
81
  const frame = requestAnimationFrame(() => setOpacity(1));
73
82
  return () => cancelAnimationFrame(frame);
74
83
  }
75
84
  else {
76
- setOpacity(0);
77
85
  const t = setTimeout(() => setVisible(false), delay);
78
86
  return () => clearTimeout(t);
79
87
  }
@@ -436,14 +444,15 @@ export function useJamlCardRenderer({ layers, invert = false, hoverTilt = false,
436
444
  */
437
445
  export function useAnteTracker(antes, options = {}) {
438
446
  const [currentAnte, setCurrentAnte] = useState(antes[0]?.ante ?? 0);
447
+ const [prevFirstAnte, setPrevFirstAnte] = useState(antes[0]?.ante);
439
448
  const scrollRef = useRef(null);
440
449
  const anteRefs = useRef(new Map());
441
- useEffect(() => {
442
- // Reset to first ante when list changes
450
+ if (antes[0]?.ante !== prevFirstAnte) {
451
+ setPrevFirstAnte(antes[0]?.ante);
443
452
  if (antes.length > 0) {
444
453
  setCurrentAnte(antes[0].ante);
445
454
  }
446
- }, [antes]);
455
+ }
447
456
  useEffect(() => {
448
457
  const root = scrollRef.current;
449
458
  if (!root || antes.length === 0)
@@ -48,7 +48,7 @@ function SuggestionList({ suggestions, selectedIndex, onSelect, onHover }) {
48
48
  `, children: [_jsx("span", { children: s.displayText }), isSelected && _jsx("span", { className: "opacity-50 text-[11px] ml-2", children: "\u21B5" })] }, `${s.text}-${idx}`));
49
49
  }) }));
50
50
  }
51
- function AntesToggle({ values, onToggle, onStartEdit, color, darkColor }) {
51
+ function AntesToggle({ values, onToggle, color }) {
52
52
  const [expanded, setExpanded] = useState(false);
53
53
  const maxAnte = 8;
54
54
  const selectedAntes = new Set(values.map(v => parseInt(v, 10)).filter(n => !isNaN(n)));
@@ -128,7 +128,6 @@ export default function JamlEditor({ initialJaml, onJamlChange, className }) {
128
128
  const [editingLineId, setEditingLineId] = useState(null);
129
129
  const [editingPart, setEditingPart] = useState(null);
130
130
  const [editingArrayIndex, setEditingArrayIndex] = useState(null);
131
- const [focusedLineIndex, setFocusedLineIndex] = useState(0);
132
131
  const editorRef = useRef(null);
133
132
  // -- Parsing Logic --
134
133
  const parseJamlToLines = useCallback((text) => {
@@ -183,14 +182,14 @@ export default function JamlEditor({ initialJaml, onJamlChange, className }) {
183
182
  });
184
183
  }, []);
185
184
  const linesToJaml = useCallback((linesList) => linesList.map(l => l.raw).join('\n'), []);
186
- useEffect(() => {
185
+ const [prevInitialJaml, setPrevInitialJaml] = useState(initialJaml);
186
+ if (initialJaml !== prevInitialJaml) {
187
+ setPrevInitialJaml(initialJaml);
187
188
  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]);
189
+ if (lines.length === 0 || linesToJaml(lines) !== text) {
190
+ setLines(parseJamlToLines(text));
191
+ }
192
+ }
194
193
  const updateLineValue = useCallback((lineId, part, newValue) => {
195
194
  const newLines = lines.map(line => {
196
195
  if (line.id !== lineId)
@@ -291,28 +290,6 @@ export default function JamlEditor({ initialJaml, onJamlChange, className }) {
291
290
  }
292
291
  }
293
292
  }, [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
293
  const deleteLine = useCallback((lineId) => {
317
294
  const filtered = lines.filter(l => l.id !== lineId);
318
295
  const renumbered = filtered.map((l, i) => ({ ...l, lineNumber: i, id: `line-${i}` }));
@@ -338,22 +315,57 @@ export default function JamlEditor({ initialJaml, onJamlChange, className }) {
338
315
  }
339
316
  return byIndent;
340
317
  }, [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) => {
318
+ 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) => (_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
319
  setEditingLineId(line.id);
343
320
  setEditingPart(part);
344
321
  setEditingArrayIndex(idx ?? null);
345
- setFocusedLineIndex(index);
346
322
  }, onEndEdit: () => {
347
323
  setEditingLineId(null);
348
324
  setEditingPart(null);
349
325
  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" })] })] }));
326
+ }, 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) }, 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
327
  }
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([]);
328
+ function JamlLine({ line, keyWidth, isEditing, editingPart, editingArrayIndex, onStartEdit, onEndEdit, onChange, onArrayItemChange, onArrayItemAdd, onArrayItemRemove, onDelete }) {
355
329
  const [selectedIndex, setSelectedIndex] = useState(0);
356
330
  const [localValue, setLocalValue] = useState('');
331
+ const [prevContext, setPrevContext] = useState('');
332
+ const [prevEditState, setPrevEditState] = useState({ isEditing, editingPart, editingArrayIndex, lineRaw: line.raw });
333
+ if (isEditing !== prevEditState.isEditing ||
334
+ editingPart !== prevEditState.editingPart ||
335
+ editingArrayIndex !== prevEditState.editingArrayIndex ||
336
+ line.raw !== prevEditState.lineRaw) {
337
+ setPrevEditState({ isEditing, editingPart, editingArrayIndex, lineRaw: line.raw });
338
+ if (isEditing) {
339
+ if (editingPart === 'key')
340
+ setLocalValue(line.key || '');
341
+ else if (editingPart === 'value')
342
+ setLocalValue((line.value || '').replace(/^~|~$/g, ''));
343
+ else if (editingPart === 'arrayItem' && editingArrayIndex !== null)
344
+ setLocalValue(line.arrayValues?.[editingArrayIndex] || '');
345
+ else
346
+ setLocalValue('');
347
+ }
348
+ }
349
+ const currentContext = useMemo(() => {
350
+ if (!isEditing || !editingPart)
351
+ return '';
352
+ if (editingPart === 'value' && line.key) {
353
+ return `${line.key}: ${localValue}`;
354
+ }
355
+ else if (editingPart === 'key') {
356
+ return localValue;
357
+ }
358
+ return line.raw;
359
+ }, [isEditing, editingPart, localValue, line.raw, line.key]);
360
+ const suggestions = useMemo(() => {
361
+ if (!currentContext)
362
+ return [];
363
+ return JamlCompletionService.getCompletions(currentContext).slice(0, 10);
364
+ }, [currentContext]);
365
+ if (currentContext !== prevContext) {
366
+ setPrevContext(currentContext);
367
+ setSelectedIndex(0);
368
+ }
357
369
  const inputRef = useRef(null);
358
370
  const targetRef = useRef(null);
359
371
  const floatingCoords = useFloatingPosition(targetRef, isEditing && suggestions.length > 0);
@@ -378,35 +390,13 @@ function JamlLine({ line, keyWidth, isEditing, editingPart, editingArrayIndex, o
378
390
  default: return '#FFF';
379
391
  }
380
392
  };
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]);
393
+ // Suggestions Logic derived above
396
394
  // Input Focus
397
395
  useEffect(() => {
398
396
  if (isEditing && inputRef.current) {
399
397
  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
398
  }
409
- }, [isEditing, editingPart, editingArrayIndex, line]);
399
+ }, [isEditing]);
410
400
  const handleKeyDown = (e) => {
411
401
  if (e.key === 'Enter') {
412
402
  e.preventDefault();
@@ -457,7 +447,7 @@ function JamlLine({ line, keyWidth, isEditing, editingPart, editingArrayIndex, o
457
447
  '--popover-left': `${floatingCoords.left}px`,
458
448
  '--popover-transform': floatingCoords.position === 'top' ? 'translateY(-100%)' : 'none',
459
449
  } : {};
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: {
450
+ return (_jsxs("div", { className: "relative flex items-center py-0.5 group", 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
451
  '--jaml-min-w': `${keyWidth}ch`,
462
452
  '--jaml-color': (isEditing && editingPart === 'key') ? getBrightColor() : getBaseColor(),
463
453
  '--jaml-bg': (isEditing && editingPart === 'key') ? `${getBrightColor()}15` : 'transparent',
@@ -470,7 +460,7 @@ function JamlLine({ line, keyWidth, isEditing, editingPart, editingArrayIndex, o
470
460
  onArrayItemRemove(idx);
471
461
  else
472
462
  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: {
463
+ }, color: getBrightColor() })) : (_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
464
  '--jaml-min-w': '24px',
475
465
  '--jaml-bg': `${getBaseColor()}15`,
476
466
  '--jaml-border': `1px solid ${getBaseColor()}40`,
package/dist/ui/jimbo.css CHANGED
@@ -4,6 +4,14 @@
4
4
  Eyedropped from actual game shader output — not Lua hex values.
5
5
  ═══════════════════════════════════════════════════════════════════════════ */
6
6
 
7
+ @font-face {
8
+ font-family: 'm6x11plus';
9
+ src: url('/fonts/m6x11plus.otf') format('opentype');
10
+ font-weight: normal;
11
+ font-style: normal;
12
+ font-display: swap;
13
+ }
14
+
7
15
  /* Global Scrollbar Hide */
8
16
  * {
9
17
  scrollbar-width: none !important;
@@ -433,9 +441,12 @@
433
441
  gap: var(--j-space-sm);
434
442
  align-items: flex-end;
435
443
  justify-content: center;
436
- flex-wrap: wrap;
444
+ flex-wrap: nowrap;
437
445
  width: 100%;
446
+ overflow-x: auto;
447
+ scrollbar-width: none;
438
448
  }
449
+ .j-tabs::-webkit-scrollbar { display: none; }
439
450
 
440
451
  .j-tab {
441
452
  display: flex;
@@ -459,7 +470,7 @@
459
470
  }
460
471
 
461
472
  .j-tab__indicator[data-active="true"] {
462
- animation: jimbo-bounce 0.8s cubic-bezier(0.68, 0, 0.68, 1) infinite;
473
+ animation: jimbo-bounce 0.6s ease-in-out infinite;
463
474
  }
464
475
 
465
476
  .j-tab__indicator[data-active="false"] {
@@ -496,9 +507,6 @@
496
507
  filter: brightness(1.08);
497
508
  }
498
509
 
499
- .j-tab__btn[data-active="false"] {
500
- opacity: 0.82;
501
- }
502
510
 
503
511
  @keyframes jimbo-bounce {
504
512
 
@@ -600,11 +608,50 @@
600
608
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.8);
601
609
  color: var(--j-white);
602
610
  pointer-events: none;
603
- z-index: 10000;
611
+ z-index: 9999;
604
612
  transition: opacity 120ms ease;
605
613
  }
606
614
 
607
615
 
616
+
617
+ /* ── Layout Utilities ─────────────────────────────────────────────────── */
618
+
619
+ .j-flex {
620
+ display: flex;
621
+ }
622
+
623
+ .j-flex-col {
624
+ display: flex;
625
+ flex-direction: column;
626
+ }
627
+
628
+ .j-items-center {
629
+ align-items: center;
630
+ }
631
+
632
+ .j-items-start {
633
+ align-items: flex-start;
634
+ }
635
+
636
+ .j-justify-center {
637
+ justify-content: center;
638
+ }
639
+
640
+ .j-justify-between {
641
+ justify-content: space-between;
642
+ }
643
+
644
+ .j-gap-xs { gap: var(--j-space-xs); }
645
+ .j-gap-sm { gap: var(--j-space-sm); }
646
+ .j-gap-md { gap: var(--j-space-md); }
647
+ .j-gap-lg { gap: var(--j-space-lg); }
648
+
649
+ .j-w-full { width: 100%; }
650
+ .j-h-full { height: 100%; }
651
+
652
+ .j-text-center { text-align: center; }
653
+
654
+
608
655
  /* ── Flank Nav ────────────────────────────────────────────────────────── */
609
656
 
610
657
  .j-flank {
@@ -902,24 +949,31 @@
902
949
 
903
950
  /* ── Modal ────────────────────────────────────────────────────────────── */
904
951
 
952
+ @keyframes jimbo-modal-in {
953
+ from { transform: scale(0.88); opacity: 0; }
954
+ to { transform: scale(1); opacity: 1; }
955
+ }
956
+
905
957
  .j-modal-overlay {
906
- position: fixed;
958
+ position: absolute;
907
959
  inset: 0;
908
- z-index: 50;
960
+ z-index: 1000;
909
961
  display: flex;
910
962
  align-items: center;
911
963
  justify-content: center;
912
- padding: var(--j-space-xl);
913
- background: rgba(0, 0, 0, 0.7);
964
+ padding: var(--j-space-md);
965
+ background: rgba(0, 0, 0, 0.82);
914
966
  }
915
967
 
916
968
  .j-modal {
917
969
  width: 100%;
918
- max-width: 320px;
919
- max-height: 90vh;
970
+ max-width: 345px;
971
+ max-height: calc(100% - 32px);
920
972
  display: flex;
921
973
  flex-direction: column;
922
974
  overflow: hidden;
975
+ border-radius: 12px;
976
+ animation: jimbo-modal-in 140ms cubic-bezier(0.2, 0, 0.2, 1.4) both;
923
977
  }
924
978
 
925
979
  .j-modal .j-panel__body {
@@ -1154,10 +1208,10 @@
1154
1208
  .j-app {
1155
1209
  container-type: inline-size;
1156
1210
  container-name: jimbo;
1157
- width: 320px;
1158
- max-width: 320px;
1159
- height: 568px;
1160
- max-height: 568px;
1211
+ width: 375px;
1212
+ max-width: 375px;
1213
+ height: 667px;
1214
+ max-height: 667px;
1161
1215
  margin: 0 auto;
1162
1216
  display: flex;
1163
1217
  flex-direction: column;
@@ -1,6 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback } from 'react';
3
3
  import { useJimboTooltip } from './hooks.js';
4
+ function assignRef(ref, value) {
5
+ if (typeof ref === 'function')
6
+ ref(value);
7
+ else if (ref && typeof ref === 'object') {
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ ref.current = value;
10
+ }
11
+ }
4
12
  /**
5
13
  * Canonical Balatro-style tooltip: dark panel, silver border, pixel font.
6
14
  * Wrap any target to get a hover/focus popover.
@@ -8,14 +16,12 @@ import { useJimboTooltip } from './hooks.js';
8
16
  export function JimboTooltip({ content, children, mode = 'snap', placement = 'auto', delay = 80, maxWidth = 280, disabled = false, }) {
9
17
  const { visible, pos, targetRef, tooltipRef, show, hide, handleMouseMove, } = useJimboTooltip({ mode, placement, delay, disabled });
10
18
  const child = React.Children.only(children);
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const childRef = child.ref;
11
21
  const refHandler = useCallback((node) => {
12
22
  targetRef.current = node;
13
- const childRef = child.ref;
14
- if (typeof childRef === 'function')
15
- childRef(node);
16
- else if (childRef && 'current' in childRef)
17
- childRef.current = node;
18
- }, [child, targetRef]);
23
+ assignRef(childRef, node);
24
+ }, [childRef, targetRef]);
19
25
  const wrapped = React.cloneElement(child, {
20
26
  ref: refHandler,
21
27
  onMouseEnter: (e) => { show(); child.props.onMouseEnter?.(e); },
@@ -5,8 +5,7 @@ export interface JimboPanelProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  hideBack?: boolean;
6
6
  }
7
7
  export declare const JimboPanel: React.MemoExoticComponent<({ children, className, sway, onBack, hideBack, style, ...props }: JimboPanelProps) => import("react/jsx-runtime").JSX.Element>;
8
- export interface JimboInnerPanelProps extends React.HTMLAttributes<HTMLDivElement> {
9
- }
8
+ export type JimboInnerPanelProps = React.HTMLAttributes<HTMLDivElement>;
10
9
  export declare const JimboInnerPanel: React.MemoExoticComponent<({ children, className, style, ...props }: JimboInnerPanelProps) => import("react/jsx-runtime").JSX.Element>;
11
10
  export type JimboTone = 'orange' | 'red' | 'blue' | 'green' | 'tarot' | 'planet' | 'spectral' | 'grey';
12
11
  export interface JimboButtonProps {
package/dist/ui/panel.js CHANGED
@@ -17,8 +17,8 @@ export function JimboButton({ tone = 'orange', size = 'md', fullWidth = false, d
17
17
  export function JimboBackButton({ onClick }) {
18
18
  return (_jsx("div", { className: "j-flex j-justify-center j-w-full", style: { padding: '4px 0' }, children: _jsx(JimboButton, { tone: "orange", size: "md", fullWidth: true, onClick: onClick, className: "j-back-btn", children: "Back" }) }));
19
19
  }
20
- export function JimboModal({ children, open, onClose, title, className, showBack = false }) {
20
+ export function JimboModal({ children, open, onClose, title, className, showBack = true }) {
21
21
  if (!open)
22
22
  return null;
23
- return (_jsx("div", { className: "j-modal-overlay", onClick: onClose, children: _jsxs(JimboPanel, { onBack: showBack ? onClose : undefined, className: `j-modal ${className ?? ''}`, onClick: (e) => e.stopPropagation(), children: [title && _jsx(JimboText, { as: "h2", size: "lg", className: "j-modal__title", children: title }), children] }) }));
23
+ return (_jsx("div", { className: "j-modal-overlay", children: _jsxs(JimboPanel, { onBack: showBack ? onClose : undefined, className: `j-modal ${className ?? ''}`, children: [title && _jsx(JimboText, { as: "h2", size: "lg", className: "j-modal__title", children: title }), children] }) }));
24
24
  }
@@ -66,7 +66,7 @@ export function RadialButton(props) {
66
66
  }, children: [variant === "toggle" && _jsx(ToggleDot, { active: props.active, disabled: isDisabled }), _jsx("span", { className: "jimbo-radial-label jimbo-radial-label--action", children: label }), variant === "count" && _jsx(CountBadge, { count: props.count, icon: props.icon })] }));
67
67
  }
68
68
  // ── Internal sub-components ───────────────────────────────────────────────────
69
- function ToggleDot({ active, disabled: _disabled }) {
69
+ function ToggleDot({ active }) {
70
70
  const dotStyle = active
71
71
  ? { backgroundColor: JimboColorOption.GREEN_TEXT, borderColor: JimboColorOption.BORDER_SILVER }
72
72
  : { backgroundColor: JimboColorOption.DARK_RED, borderColor: JimboColorOption.DARK_GREY };
@@ -16,5 +16,5 @@ export function Showcase({ title = 'Balatro', subtitle = 'Seed Curator', hotFilt
16
16
  padding: '3px 8px',
17
17
  background: 'var(--j-dark-grey)', borderRadius: 4,
18
18
  border: '1px solid var(--j-panel-edge)',
19
- }, children: [_jsx(JimboText, { size: "micro", tone: "purple", children: mcpInfo.engine }), _jsx(JimboText, { size: "micro", tone: "grey", children: mcpInfo.features })] })), hotFilters.length > 0 && (_jsxs(_Fragment, { children: [_jsx(JimboSectionHeader, { label: "Filters", tone: "blue" }), _jsx("div", { className: "j-flex-col", style: { gap: 4 }, children: hotFilters.slice(0, 4).map((f, i) => (_jsxs(JimboInfoCard, { tone: f.tone, onClick: () => onFilterClick?.(f, i), style: { cursor: onFilterClick ? 'pointer' : undefined }, children: [_jsx("div", { className: "j-flex j-gap-xs", children: f.sample.slice(0, 2).map((name, j) => (_jsx("div", { style: { width: 22, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(JimboSprite, { name: name, width: 20 }) }, j))) }), _jsxs(JimboInfoCardBody, { children: [_jsx(JimboInfoCardTitle, { children: f.name }), _jsxs(JimboInfoCardSub, { children: ["by ", f.author] })] }), _jsx(JimboInfoCardAside, { children: _jsx(JimboText, { size: "xs", tone: f.tone === 'gold' ? 'gold' : f.tone, children: f.hits }) })] }, i))) })] })), recentFinds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(JimboSectionHeader, { label: "Recent", tone: "green" }), _jsx("div", { style: { lineHeight: 1.5 }, children: recentFinds.slice(0, 3).map((r, i) => (_jsxs("div", { className: "j-flex j-gap-sm", children: [_jsx(JimboText, { size: "micro", tone: "gold", children: r.seed }), _jsx(JimboText, { size: "micro", tone: "grey", children: r.filterName }), r.score > 0 && _jsxs(JimboText, { size: "micro", tone: "green", children: ["+", r.score] })] }, i))) })] }))] }), _jsxs(JimboAppFooter, { children: [_jsx(JimboButton, { tone: "green", fullWidth: true, size: "lg", onClick: onNewSearch, children: "New Search" }), _jsx(JimboButton, { tone: "blue", fullWidth: true, size: "lg", onClick: onBrowseFilters, children: "Browse Filters" }), _jsx(JimboBalatroFooter, {})] })] }));
19
+ }, children: [_jsx(JimboText, { size: "micro", tone: "purple", children: mcpInfo.engine }), _jsx(JimboText, { size: "micro", tone: "grey", children: mcpInfo.features })] })), hotFilters.length > 0 && (_jsxs(_Fragment, { children: [_jsx(JimboSectionHeader, { label: "Filters", tone: "blue" }), _jsx("div", { className: "j-flex-col", style: { gap: 4 }, children: hotFilters.slice(0, 4).map((f, i) => (_jsxs(JimboInfoCard, { tone: f.tone, onClick: () => onFilterClick?.(f, i), style: { cursor: onFilterClick ? 'pointer' : undefined }, children: [_jsx("div", { className: "j-flex j-gap-xs", children: f.sample.slice(0, 2).map((name, j) => (_jsx("div", { style: { width: 22, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(JimboSprite, { name: name, width: 20 }) }, j))) }), _jsxs(JimboInfoCardBody, { children: [_jsx(JimboInfoCardTitle, { children: f.name }), _jsxs(JimboInfoCardSub, { children: ["by ", f.author] })] }), _jsx(JimboInfoCardAside, { children: _jsx(JimboText, { size: "xs", tone: f.tone === 'gold' ? 'gold' : f.tone, children: f.hits }) })] }, i))) })] })), recentFinds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(JimboSectionHeader, { label: "Recent", tone: "green" }), _jsx("div", { style: { lineHeight: 1.5 }, children: recentFinds.slice(0, 3).map((r, i) => (_jsxs("div", { className: "j-flex j-gap-sm", children: [_jsx(JimboText, { size: "micro", tone: "gold", children: r.seed }), _jsx(JimboText, { size: "micro", tone: "grey", children: r.filterName }), r.score > 0 && _jsxs(JimboText, { size: "micro", tone: "green", children: ["+", r.score] })] }, i))) })] }))] }), _jsxs(JimboAppFooter, { children: [_jsx(JimboButton, { tone: "green", fullWidth: true, size: "lg", onClick: onNewSearch, children: "New Search" }), _jsx(JimboButton, { tone: "blue", fullWidth: true, size: "lg", onClick: onBrowseFilters, children: "Browse Filters" })] }), _jsx(JimboBalatroFooter, {})] }));
20
20
  }
@@ -61,7 +61,8 @@ function parseInlineValues(raw) {
61
61
  export function extractVisualJamlItems(jaml) {
62
62
  const groups = createEmptyGroups();
63
63
  const seen = new Set();
64
- const lines = jaml.replace(/\r\n/g, "\n").split("\n");
64
+ const safeJaml = jaml || "";
65
+ const lines = safeJaml.replace(/\r\n/g, "\n").split("\n");
65
66
  let currentSection = null;
66
67
  for (const rawLine of lines) {
67
68
  const trimmed = rawLine.trim();
@@ -59,7 +59,8 @@ function uid() {
59
59
  return `clause-${++_uid}`;
60
60
  }
61
61
  export function jamlTextToVisualFilter(text) {
62
- const lines = text.replace(/\r\n/g, "\n").split("\n");
62
+ const safeText = text || "";
63
+ const lines = safeText.replace(/\r\n/g, "\n").split("\n");
63
64
  const filter = { must: [], should: [], mustnot: [] };
64
65
  filter.name = topLevelScalar(lines, "name");
65
66
  filter.author = topLevelScalar(lines, "author");
@@ -161,7 +162,7 @@ export function jamlTextToVisualFilter(text) {
161
162
  function q(s) {
162
163
  if (!s)
163
164
  return "";
164
- return /[:#\[\]{}|>&*!,'"?]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
165
+ return /[:#[\]{}|>&*!,'"?]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
165
166
  }
166
167
  function serializeClause(clause) {
167
168
  let out = ` - ${clause.type}: ${q(clause.value)}\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaml-ui",
3
- "version": "0.24.16",
3
+ "version": "0.24.18",
4
4
  "description": "Balatro rendering components, sprite metadata, and optional Motely helpers for React apps.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -37,8 +37,7 @@
37
37
  "default": "./dist/r3f.js"
38
38
  },
39
39
  "./jimbo.css": "./dist/ui/jimbo.css",
40
- "./fonts.css": "./fonts.css",
41
- "./jaml.schema.json": "./jaml.schema.json"
40
+ "./fonts.css": "./fonts.css"
42
41
  },
43
42
  "sideEffects": [
44
43
  "./fonts.css",
@@ -58,7 +57,7 @@
58
57
  "LICENSE"
59
58
  ],
60
59
  "scripts": {
61
- "build": "tsc --pretty false && node -e \"const fs=require('fs');fs.mkdirSync('dist/ui',{recursive:true});fs.copyFileSync('src/ui/jimbo.css','dist/ui/jimbo.css');fs.copyFileSync('node_modules/motely-wasm/jaml.schema.json','jaml.schema.json');\"",
60
+ "build": "tsc --pretty false && node -e \"const fs=require('fs');fs.mkdirSync('dist/ui',{recursive:true});fs.copyFileSync('src/ui/jimbo.css','dist/ui/jimbo.css');\"",
62
61
  "dev": "tsc --watch",
63
62
  "demo": "vite --config demo/vite.config.ts",
64
63
  "typecheck": "tsc --noEmit --pretty false",
@@ -96,7 +95,7 @@
96
95
  "@react-spring/three": ">=9.0.0",
97
96
  "@react-three/drei": ">=9.0.0",
98
97
  "@react-three/fiber": ">=8.0.0",
99
- "motely-wasm": "^15.1.2",
98
+ "motely-wasm": "^15.1.3",
100
99
  "react": "^18.2.0 || ^19.0.0",
101
100
  "react-dom": "^18.2.0 || ^19.0.0",
102
101
  "react-icons": ">=5.0.0",
@@ -124,6 +123,7 @@
124
123
  },
125
124
  "devDependencies": {
126
125
  "@chromatic-com/storybook": "^5.1.2",
126
+ "@eslint/js": "^10.0.1",
127
127
  "@google/design.md": "^0.1.1",
128
128
  "@react-spring/three": "^10.0.3",
129
129
  "@react-three/drei": ">=9.0.0",
@@ -140,7 +140,11 @@
140
140
  "@vitejs/plugin-react": "^5.0.4",
141
141
  "@vitest/browser-playwright": "^4.1.5",
142
142
  "@vitest/coverage-v8": "^4.1.5",
143
- "motely-wasm": "^15.1.2",
143
+ "eslint": "^10.3.0",
144
+ "eslint-plugin-react-hooks": "^7.1.1",
145
+ "eslint-plugin-react-refresh": "^0.5.2",
146
+ "globals": "^17.6.0",
147
+ "motely-wasm": "^15.1.3",
144
148
  "playwright": "^1.59.1",
145
149
  "react": "^19.2.4",
146
150
  "react-dom": "^19.2.4",
@@ -148,10 +152,12 @@
148
152
  "storybook": "^10.3.6",
149
153
  "three": "^0.184.0",
150
154
  "typescript": "^5.9.3",
155
+ "typescript-eslint": "^8.59.2",
151
156
  "vite": "^8.0.9",
152
157
  "vitest": "^4.1.5"
153
158
  },
154
159
  "dependencies": {
160
+ "@codemirror/autocomplete": "^6.20.1",
155
161
  "@codemirror/commands": "^6.10.3",
156
162
  "@codemirror/lang-yaml": "^6.1.3",
157
163
  "@codemirror/language": "^6.12.3",