jaml-ui 0.11.0 → 0.11.1

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.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
3
4
  import Editor from "@monaco-editor/react";
4
5
  import { JimboColorOption } from "../ui/tokens.js";
5
6
  // Monaco needs hex strings for its colors API. We strip the leading `#` from
@@ -40,10 +41,77 @@ const defineBalatroTheme = (monaco) => {
40
41
  });
41
42
  };
42
43
  export function JamlCodeEditor({ value, onChange, minHeight = 320, }) {
44
+ const editorRef = useRef(null);
45
+ const monacoRef = useRef(null);
46
+ // Suppress our onChange while we're applying a programmatic edit, so the
47
+ // streamed parent value doesn't loop back through onChange and bounce.
48
+ const suppressEmitRef = useRef(false);
49
+ // Capture initial value for the uncontrolled editor mount; subsequent
50
+ // updates flow through the useEffect below.
51
+ const initialValueRef = useRef(value);
52
+ // Track value across renders so we can apply only the streamed delta when
53
+ // the new value is a strict suffix-extension of what's already in the model.
54
+ const lastSyncedValueRef = useRef(value);
55
+ const handleMount = (editor, m) => {
56
+ editorRef.current = editor;
57
+ monacoRef.current = m;
58
+ };
59
+ useEffect(() => {
60
+ const editor = editorRef.current;
61
+ const m = monacoRef.current;
62
+ if (!editor || !m)
63
+ return;
64
+ const model = editor.getModel();
65
+ if (!model)
66
+ return;
67
+ const current = editor.getValue();
68
+ if (current === value) {
69
+ lastSyncedValueRef.current = value;
70
+ return;
71
+ }
72
+ suppressEmitRef.current = true;
73
+ try {
74
+ // Streaming-friendly path: when the new value just appends to what
75
+ // Monaco already has, push an insert at end-of-document. Monaco
76
+ // re-tokenizes only from the insertion point — no full-doc churn,
77
+ // no syntax-color strobe, no cursor reset.
78
+ if (value.length > current.length && value.startsWith(current)) {
79
+ const suffix = value.slice(current.length);
80
+ const lastLine = model.getLineCount();
81
+ const lastCol = model.getLineMaxColumn(lastLine);
82
+ model.applyEdits([
83
+ {
84
+ range: new m.Range(lastLine, lastCol, lastLine, lastCol),
85
+ text: suffix,
86
+ forceMoveMarkers: false,
87
+ },
88
+ ]);
89
+ }
90
+ else {
91
+ // Real replacement (paste from outside, parent rewrite, undo, etc.).
92
+ editor.executeEdits("", [
93
+ {
94
+ range: model.getFullModelRange(),
95
+ text: value,
96
+ forceMoveMarkers: true,
97
+ },
98
+ ]);
99
+ editor.pushUndoStop();
100
+ }
101
+ }
102
+ finally {
103
+ suppressEmitRef.current = false;
104
+ lastSyncedValueRef.current = value;
105
+ }
106
+ }, [value]);
43
107
  return (_jsxs("div", { style: { width: "100%", minHeight, background: JimboColorOption.DARKEST }, children: [_jsx("style", { children: `
44
108
  .monaco-editor .iPadShowKeyboard,
45
109
  .monaco-editor [class*="iPadShowKeyboard"] { display: none !important; }
46
- ` }), _jsx(Editor, { height: `${minHeight}px`, defaultLanguage: "yaml", value: value, theme: "jaml-balatro-dark", onChange: (next) => onChange(next ?? ""), beforeMount: defineBalatroTheme, options: {
110
+ ` }), _jsx(Editor, { height: `${minHeight}px`, defaultLanguage: "yaml", defaultValue: initialValueRef.current, theme: "jaml-balatro-dark", onChange: (next) => {
111
+ if (suppressEmitRef.current)
112
+ return;
113
+ onChange(next ?? "");
114
+ }, onMount: handleMount, beforeMount: defineBalatroTheme, options: {
47
115
  minimap: { enabled: false },
48
116
  scrollBeyondLastLine: false,
49
117
  fontSize: 13,
@@ -111,8 +111,10 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
111
111
  // doesn't flash the visual panel empty.
112
112
  const [lastParsedText, setLastParsedText] = useState("");
113
113
  const [lastParsedFilter, setLastParsedFilter] = useState(() => jamlTextToVisualFilter(jaml ?? defaultJaml ?? ""));
114
- // Adjust-state-during-render: reparse when text changes (only if not controlled).
115
- if (visualFilter === undefined && text !== lastParsedText) {
114
+ // Adjust-state-during-render: reparse when text changes (only if not
115
+ // controlled). Gated on `mode === "visual"` so we don't burn CPU parsing
116
+ // on every streamed token while the user is in the .jaml/map/results tab.
117
+ if (visualFilter === undefined && mode === "visual" && text !== lastParsedText) {
116
118
  try {
117
119
  const parsed = jamlTextToVisualFilter(text);
118
120
  setLastParsedText(text);
@@ -1,62 +1,22 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { JimboButton } from "../ui/panel.js";
4
+ import { JimboTabs } from "../ui/jimboTabs.js";
4
5
  import { JimboColorOption } from "../ui/tokens.js";
5
- const TABS = [
6
- { id: "visual", label: "Visual" },
7
- { id: "code", label: ".jaml" },
8
- { id: "map", label: "Map" },
9
- { id: "results", label: "Results" },
10
- ];
11
6
  export function JamlIdeToolbar({ mode, onModeChange, resultCount = 0, className = "", onSearch, isSearching = false }) {
7
+ const tabs = [
8
+ { id: "visual", label: "Visual" },
9
+ { id: "code", label: ".jaml" },
10
+ { id: "map", label: "Map" },
11
+ { id: "results", label: resultCount > 0 ? `Results (${resultCount})` : "Results" },
12
+ ];
12
13
  return (_jsxs("div", { className: className, style: {
13
14
  display: "flex",
14
15
  alignItems: "center",
15
16
  justifyContent: "space-between",
16
17
  gap: 8,
17
- padding: "6px 10px",
18
+ padding: "10px 10px 6px",
18
19
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
19
20
  background: JimboColorOption.DARKEST,
20
- }, children: [_jsx("div", { style: {
21
- display: "flex",
22
- alignItems: "stretch",
23
- gap: 0,
24
- background: `${JimboColorOption.DARK_GREY}cc`,
25
- borderRadius: 7,
26
- border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
27
- padding: 2,
28
- overflow: "hidden",
29
- }, children: TABS.map((tab) => {
30
- const isActive = mode === tab.id;
31
- return (_jsxs("button", { type: "button", onClick: () => onModeChange(tab.id), style: {
32
- display: "flex",
33
- alignItems: "center",
34
- gap: 5,
35
- padding: "3px 10px",
36
- border: "none",
37
- borderRadius: 5,
38
- cursor: "pointer",
39
- fontFamily: "m6x11plus, monospace",
40
- fontSize: 10,
41
- letterSpacing: 1,
42
- textTransform: "uppercase",
43
- lineHeight: 1.2,
44
- transition: "background 80ms, color 80ms, box-shadow 80ms",
45
- color: isActive ? JimboColorOption.DARKEST : JimboColorOption.GREY,
46
- background: isActive ? JimboColorOption.GOLD : "transparent",
47
- boxShadow: isActive ? `0 2px 0 #8a6a1e` : "none",
48
- textShadow: isActive ? "none" : "none",
49
- userSelect: "none",
50
- position: "relative",
51
- transform: isActive ? "translateY(0)" : "translateY(0)",
52
- }, children: [tab.label, tab.id === "results" && resultCount > 0 ? (_jsx("span", { style: {
53
- borderRadius: 999,
54
- background: isActive ? `${JimboColorOption.DARKEST}44` : `${JimboColorOption.GOLD}33`,
55
- color: isActive ? JimboColorOption.DARKEST : JimboColorOption.GOLD_TEXT,
56
- padding: "0px 5px",
57
- fontSize: 9,
58
- fontFamily: "monospace",
59
- letterSpacing: 0,
60
- }, children: resultCount })) : null] }, tab.id));
61
- }) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
21
+ }, children: [_jsx(JimboTabs, { tabs: tabs, activeTab: mode, onTabChange: (id) => onModeChange(id) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
62
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaml-ui",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
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",