jaml-ui 0.7.2 → 0.9.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.
package/dist/assets.js CHANGED
@@ -12,7 +12,7 @@ export const JAML_ASSET_FILES = {
12
12
  };
13
13
  const assetKeyByFileName = Object.fromEntries(Object.entries(JAML_ASSET_FILES).map(([key, fileName]) => [fileName, key]));
14
14
  // Keep in lockstep with package.json version. Upload assets to this path when publishing.
15
- const JAML_UI_VERSION = "0.7.0";
15
+ const JAML_UI_VERSION = "0.8.0";
16
16
  const CDN_BASE = `https://cdn.seedfinder.app/jaml-ui/${JAML_UI_VERSION}/assets/`;
17
17
  const defaultAssetUrls = {
18
18
  deck: `${CDN_BASE}${JAML_ASSET_FILES.deck}`,
@@ -0,0 +1,7 @@
1
+ export interface JamlCodeEditorProps {
2
+ value: string;
3
+ onChange: (value: string) => void;
4
+ placeholder?: string;
5
+ minHeight?: number;
6
+ }
7
+ export declare function JamlCodeEditor({ value, onChange, minHeight, }: JamlCodeEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import Editor from "@monaco-editor/react";
4
+ import { JimboColorOption } from "../ui/tokens.js";
5
+ // Monaco needs hex strings for its colors API. We strip the leading `#` from
6
+ // JimboColor tokens where Monaco expects raw hex for syntax rules (token
7
+ // foreground), and pass the full `#...` form for UI colors. Alpha suffix
8
+ // (e.g. WHITE + "20") is valid for Monaco colors but not for rules.
9
+ const hex = (token) => token.replace(/^#/, "");
10
+ const defineBalatroTheme = (monaco) => {
11
+ monaco.editor.defineTheme("jaml-balatro-dark", {
12
+ base: "vs-dark",
13
+ inherit: true,
14
+ rules: [
15
+ { token: "comment", foreground: hex(JimboColorOption.GREY), fontStyle: "italic" },
16
+ { token: "keyword", foreground: hex(JimboColorOption.RED) },
17
+ { token: "string", foreground: hex(JimboColorOption.GOLD_TEXT) },
18
+ { token: "number", foreground: hex(JimboColorOption.BLUE) },
19
+ { token: "type", foreground: hex(JimboColorOption.GREEN_TEXT) },
20
+ ],
21
+ colors: {
22
+ "editor.background": JimboColorOption.DARKEST,
23
+ "editor.foreground": JimboColorOption.WHITE,
24
+ "editorLineNumber.foreground": JimboColorOption.GREY,
25
+ "editorLineNumber.activeForeground": JimboColorOption.GOLD_TEXT,
26
+ "editor.selectionBackground": `${JimboColorOption.WHITE}20`,
27
+ "editor.inactiveSelectionBackground": `${JimboColorOption.WHITE}10`,
28
+ "editor.lineHighlightBackground": `${JimboColorOption.BLACK}20`,
29
+ "editorCursor.foreground": JimboColorOption.GOLD_TEXT,
30
+ "editorWidget.background": JimboColorOption.DARK_GREY,
31
+ "editorWidget.border": `${JimboColorOption.WHITE}20`,
32
+ "editorWidget.foreground": JimboColorOption.WHITE,
33
+ "list.activeSelectionBackground": JimboColorOption.GOLD,
34
+ "list.activeSelectionForeground": JimboColorOption.DARKEST,
35
+ "list.hoverBackground": JimboColorOption.PANEL_EDGE,
36
+ "list.hoverForeground": JimboColorOption.WHITE,
37
+ "list.focusBackground": JimboColorOption.GOLD,
38
+ "list.focusForeground": JimboColorOption.DARKEST,
39
+ },
40
+ });
41
+ };
42
+ export function JamlCodeEditor({ value, onChange, minHeight = 320, }) {
43
+ return (_jsx("div", { style: { width: "100%", minHeight, background: JimboColorOption.DARKEST }, children: _jsx(Editor, { height: `${minHeight}px`, defaultLanguage: "yaml", value: value, theme: "jaml-balatro-dark", onChange: (next) => onChange(next ?? ""), beforeMount: defineBalatroTheme, options: {
44
+ minimap: { enabled: false },
45
+ scrollBeyondLastLine: false,
46
+ fontSize: 13,
47
+ lineHeight: 22,
48
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
49
+ lineNumbers: "on",
50
+ automaticLayout: true,
51
+ padding: { top: 12, bottom: 12 },
52
+ wordWrap: "on",
53
+ formatOnPaste: true,
54
+ formatOnType: true,
55
+ renderLineHighlight: "line",
56
+ scrollbar: { verticalScrollbarSize: 8, horizontalScrollbarSize: 8 },
57
+ } }) }));
58
+ }
@@ -22,6 +22,10 @@ export interface JamlIdeProps {
22
22
  codePlaceholder?: string;
23
23
  onSearch?: () => void;
24
24
  isSearching?: boolean;
25
+ /**
26
+ * Controlled visual filter. When provided alongside `onVisualFilterChange`, the Visual tab
27
+ * is fully controlled by the parent. When absent, the Visual tab auto-derives from the text.
28
+ */
25
29
  visualFilter?: JamlVisualFilter;
26
30
  onVisualFilterChange?: (filter: JamlVisualFilter) => void;
27
31
  }
@@ -4,7 +4,9 @@ import { useMemo, useState } from "react";
4
4
  import { JamlMapPreview } from "./JamlMapPreview.js";
5
5
  import { JamlIdeToolbar } from "./JamlIdeToolbar.js";
6
6
  import { JamlIdeVisual } from "./JamlIdeVisual.js";
7
+ import { JamlCodeEditor } from "./JamlCodeEditor.js";
7
8
  import { JimboColorOption } from "../ui/tokens.js";
9
+ import { jamlTextToVisualFilter, visualFilterToJamlText } from "../utils/jamlVisualFilter.js";
8
10
  function TallyBar({ value, max }) {
9
11
  const pct = max > 0 ? Math.min(1, value / max) : 0;
10
12
  return (_jsx("div", { style: { flex: 1, height: 4, borderRadius: 999, background: `${JimboColorOption.DARK_GREY}88`, overflow: "hidden" }, children: _jsx("div", { style: {
@@ -93,6 +95,7 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
93
95
  const [mode, setMode] = useState(defaultMode);
94
96
  const [internalText, setInternalText] = useState(jaml ?? defaultJaml ?? "");
95
97
  const [lastJamlProp, setLastJamlProp] = useState(jaml);
98
+ // Adjust-state-during-render: sync controlled `jaml` prop into internal text.
96
99
  if (jaml !== lastJamlProp) {
97
100
  setLastJamlProp(jaml);
98
101
  if (jaml !== undefined)
@@ -103,6 +106,38 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
103
106
  setInternalText(next);
104
107
  onChange?.(next);
105
108
  };
109
+ // Derived visual filter state (used only when not externally controlled).
110
+ // Cache the last successfully parsed filter so a mid-edit invalid state
111
+ // doesn't flash the visual panel empty.
112
+ const [lastParsedText, setLastParsedText] = useState("");
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) {
116
+ try {
117
+ const parsed = jamlTextToVisualFilter(text);
118
+ setLastParsedText(text);
119
+ setLastParsedFilter(parsed);
120
+ }
121
+ catch {
122
+ // Keep previous filter on parse error — don't flash empty.
123
+ setLastParsedText(text);
124
+ }
125
+ }
126
+ const activeFilter = visualFilter ?? lastParsedFilter;
127
+ const handleVisualFilterChange = (next) => {
128
+ if (onVisualFilterChange) {
129
+ // Controlled: let parent own both.
130
+ onVisualFilterChange(next);
131
+ }
132
+ else {
133
+ // Uncontrolled: round-trip through text so textarea stays source of truth.
134
+ const nextText = visualFilterToJamlText(next);
135
+ setInternalText(nextText);
136
+ setLastParsedFilter(next);
137
+ setLastParsedText(nextText);
138
+ onChange?.(nextText);
139
+ }
140
+ };
106
141
  const results = useMemo(() => searchResults, [searchResults]);
107
142
  return (_jsxs("div", { className: className, style: {
108
143
  display: "flex",
@@ -122,17 +157,5 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
122
157
  padding: "10px 14px",
123
158
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
124
159
  background: JimboColorOption.TEAL_GREY,
125
- }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, fontWeight: 800, fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" && visualFilter && onVisualFilterChange ? (_jsx(JamlIdeVisual, { filter: visualFilter, onChange: onVisualFilterChange })) : mode === "visual" ? (_jsxs("div", { style: { padding: 16, color: JimboColorOption.GREY, fontSize: 12, textAlign: "center" }, children: ["Pass ", _jsx("code", { children: "visualFilter" }), " and ", _jsx("code", { children: "onVisualFilterChange" }), " props to enable the visual editor."] })) : null, mode === "code" ? (_jsx("textarea", { title: "JAML IDE Editor", value: text, onChange: (event) => handleTextChange(event.target.value), placeholder: codePlaceholder, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", style: {
126
- width: "100%",
127
- minHeight: 320,
128
- resize: "vertical",
129
- border: 0,
130
- outline: 0,
131
- padding: 16,
132
- background: "transparent",
133
- color: JimboColorOption.WHITE,
134
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
135
- fontSize: 13,
136
- lineHeight: 1.7,
137
- } })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results }) })) : null] })] }));
160
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, fontWeight: 800, fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results }) })) : null] })] }));
138
161
  }
@@ -4,7 +4,7 @@ import { JimboButton } from "../ui/panel.js";
4
4
  import { JimboColorOption } from "../ui/tokens.js";
5
5
  const TABS = [
6
6
  { id: "visual", label: "Visual" },
7
- { id: "code", label: "Code" },
7
+ { id: "code", label: ".jaml" },
8
8
  { id: "map", label: "Map" },
9
9
  { id: "results", label: "Results" },
10
10
  ];
@@ -17,5 +17,46 @@ export function JamlIdeToolbar({ mode, onModeChange, resultCount = 0, className
17
17
  padding: "6px 10px",
18
18
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
19
19
  background: JimboColorOption.DARKEST,
20
- }, children: [_jsx("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: TABS.map((tab) => (_jsxs(JimboButton, { tone: mode === tab.id ? "gold" : "grey", size: "xs", onClick: () => onModeChange(tab.id), children: [tab.label, tab.id === "results" && resultCount > 0 ? (_jsx("span", { style: { marginLeft: 6, borderRadius: 999, background: "rgba(228,182,67,0.2)", color: JimboColorOption.GOLD_TEXT, padding: "1px 6px", fontSize: 10 }, children: resultCount })) : null] }, tab.id))) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
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
62
  }
@@ -35,7 +35,7 @@ function DragClausePill({ clause, zone, onDragStart, }) {
35
35
  borderRadius: 6, padding: "5px 8px 5px 4px",
36
36
  boxShadow: `0 2px 0 ${JimboColorOption.BLACK}`,
37
37
  cursor: "grab", userSelect: "none", touchAction: "none",
38
- }, children: [_jsx("div", { style: { color: JimboColorOption.GREY, fontSize: 12, lineHeight: 1, padding: "0 2px" }, children: "\u22EE\u22EE" }), _jsx(ClauseSprite, { clause: clause, size: 26 }), _jsx("div", { style: { fontSize: 10, color: JimboColorOption.WHITE, letterSpacing: 1, textShadow: "1px 1px 0 rgba(0,0,0,.8)" }, children: clause.label || clause.value }), clause.antes && clause.antes.length > 0 && (_jsxs("div", { style: { display: "flex", gap: 2 }, children: [clause.antes.slice(0, 3).map((a) => (_jsx("div", { style: { fontSize: 8, padding: "0 3px", background: JimboColorOption.DARKEST, color: z.color, borderRadius: 2 }, children: a }, a))), clause.antes.length > 3 && _jsxs("div", { style: { fontSize: 8, color: JimboColorOption.GREY }, children: ["+", clause.antes.length - 3] })] })), clause.score != null && (_jsxs("div", { style: { fontSize: 9, padding: "0 4px", background: JimboColorOption.RED, color: JimboColorOption.WHITE, borderRadius: 2 }, children: ["+", clause.score] }))] }));
38
+ }, children: [_jsx("div", { style: { color: JimboColorOption.GREY, fontSize: 12, lineHeight: 1, padding: "0 2px" }, children: "\u22EE\u22EE" }), _jsx(ClauseSprite, { clause: clause, size: 26 }), _jsx("div", { style: { fontSize: 10, color: JimboColorOption.WHITE, letterSpacing: 1, textShadow: `1px 1px 0 ${JimboColorOption.BLACK}cc` }, children: clause.label || clause.value }), clause.antes && clause.antes.length > 0 && (_jsxs("div", { style: { display: "flex", gap: 2 }, children: [clause.antes.slice(0, 3).map((a) => (_jsx("div", { style: { fontSize: 8, padding: "0 3px", background: JimboColorOption.DARKEST, color: z.color, borderRadius: 2 }, children: a }, a))), clause.antes.length > 3 && _jsxs("div", { style: { fontSize: 8, color: JimboColorOption.GREY }, children: ["+", clause.antes.length - 3] })] })), clause.score != null && (_jsxs("div", { style: { fontSize: 9, padding: "0 4px", background: JimboColorOption.RED, color: JimboColorOption.WHITE, borderRadius: 2 }, children: ["+", clause.score] }))] }));
39
39
  }
40
40
  function ZoneDropRail({ zone, clauses, onDragStart, highlight, }) {
41
41
  const z = ZONE_META[zone];
@@ -47,7 +47,7 @@ function ZoneDropRail({ zone, clauses, onDragStart, highlight, }) {
47
47
  }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }, children: [_jsx("div", { style: {
48
48
  fontSize: 10, letterSpacing: 2, padding: "2px 8px",
49
49
  background: z.color, color: JimboColorOption.WHITE, borderRadius: 3,
50
- textShadow: "1px 1px 0 rgba(0,0,0,.8)",
50
+ textShadow: `1px 1px 0 ${JimboColorOption.BLACK}cc`,
51
51
  }, children: z.label }), _jsx("div", { style: { flex: 1, height: 1, background: `${z.color}44` } }), _jsx("div", { style: { fontSize: 8, color: JimboColorOption.GREY }, children: clauses.length })] }), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: [clauses.map((c) => (_jsx(DragClausePill, { clause: c, zone: zone, onDragStart: onDragStart }, c.id))), clauses.length === 0 && (_jsx("div", { style: { fontSize: 10, color: JimboColorOption.GREY, padding: 10, fontStyle: "italic" }, children: "drop clauses here" }))] })] }));
52
52
  }
53
53
  export function JamlIdeVisual({ filter, onChange, onSave, onBack }) {
@@ -103,11 +103,11 @@ export function JamlIdeVisual({ filter, onChange, onSave, onBack }) {
103
103
  return (_jsxs("div", { ref: rootRef, style: { display: "flex", flexDirection: "column", gap: 10, padding: 10 }, children: [_jsxs("div", { style: {
104
104
  background: JimboColorOption.DARK_GREY, border: `2px solid ${JimboColorOption.PANEL_EDGE}`,
105
105
  borderRadius: 6, padding: 8, boxShadow: `0 2px 0 ${JimboColorOption.BLACK}`,
106
- }, children: [_jsx("div", { style: { fontSize: 9, color: JimboColorOption.GREY, letterSpacing: 2 }, children: "FILE" }), _jsxs("div", { style: { fontSize: 14, color: JimboColorOption.WHITE, textShadow: "1px 1px 0 rgba(0,0,0,.8)" }, children: [filter.name || "Untitled", ".jaml"] }), filter.author && (_jsxs("div", { style: { fontSize: 9, color: JimboColorOption.GOLD_TEXT, marginTop: 2 }, children: ["by ", filter.author] }))] }), _jsx("div", { style: { fontSize: 9, color: JimboColorOption.GREY, letterSpacing: 1, textAlign: "center" }, children: "\u22EE\u22EE drag clauses between zones \u00B7 tap to edit" }), _jsx(ZoneDropRail, { zone: "must", clauses: filter.must, onDragStart: onDragStart, highlight: hoverZone === "must" }), _jsx(ZoneDropRail, { zone: "should", clauses: filter.should, onDragStart: onDragStart, highlight: hoverZone === "should" }), _jsx(ZoneDropRail, { zone: "mustnot", clauses: filter.mustnot, onDragStart: onDragStart, highlight: hoverZone === "mustnot" }), drag && (_jsx("div", { style: {
106
+ }, children: [_jsx("div", { style: { fontSize: 9, color: JimboColorOption.GREY, letterSpacing: 2 }, children: "FILE" }), _jsxs("div", { style: { fontSize: 14, color: JimboColorOption.WHITE, textShadow: `1px 1px 0 ${JimboColorOption.BLACK}cc` }, children: [filter.name || "Untitled", ".jaml"] }), filter.author && (_jsxs("div", { style: { fontSize: 9, color: JimboColorOption.GOLD_TEXT, marginTop: 2 }, children: ["by ", filter.author] }))] }), _jsx(ZoneDropRail, { zone: "must", clauses: filter.must, onDragStart: onDragStart, highlight: hoverZone === "must" }), _jsx(ZoneDropRail, { zone: "should", clauses: filter.should, onDragStart: onDragStart, highlight: hoverZone === "should" }), _jsx(ZoneDropRail, { zone: "mustnot", clauses: filter.mustnot, onDragStart: onDragStart, highlight: hoverZone === "mustnot" }), drag && (_jsx("div", { style: {
107
107
  position: "fixed",
108
108
  left: drag.x - drag.offX, top: drag.y - drag.offY,
109
109
  pointerEvents: "none", zIndex: 999,
110
110
  transform: "rotate(-2deg) scale(1.05)",
111
- filter: "drop-shadow(0 4px 6px rgba(0,0,0,.6))", opacity: 0.92,
111
+ filter: `drop-shadow(0 4px 6px ${JimboColorOption.BLACK}99)`, opacity: 0.92,
112
112
  }, children: _jsx(DragClausePill, { clause: drag.clause, zone: drag.fromZone, onDragStart: () => { } }) }))] }));
113
113
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { AnalyzerExplorer, type AnalyzerAnteView, type AnalyzerBadge, type Analy
6
6
  export { JamlMapPreview, type JamlMapPreviewProps } from "./components/JamlMapPreview.js";
7
7
  export { JamlIde, type JamlIdeProps, type JamlIdeSearchResult, type JamlVisualFilter, type JamlVisualClause, type JamlZone, } from "./components/JamlIde.js";
8
8
  export { JamlIdeVisual, type JamlIdeVisualProps, } from "./components/JamlIdeVisual.js";
9
+ export { JamlCodeEditor, type JamlCodeEditorProps, } from "./components/JamlCodeEditor.js";
9
10
  export { JamlIdeToolbar, type JamlIdeMode, type JamlIdeToolbarProps, } from "./components/JamlIdeToolbar.js";
10
11
  export { CardList, type CardListProps } from "./components/CardList.js";
11
12
  export { extractVisualJamlItems, type JamlPreviewGroups, type JamlPreviewItem, type JamlPreviewSection, type JamlPreviewVisualType, } from "./utils/jamlMapPreview.js";
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export { AnalyzerExplorer, } from "./components/AnalyzerExplorer.js";
7
7
  export { JamlMapPreview } from "./components/JamlMapPreview.js";
8
8
  export { JamlIde, } from "./components/JamlIde.js";
9
9
  export { JamlIdeVisual, } from "./components/JamlIdeVisual.js";
10
+ export { JamlCodeEditor, } from "./components/JamlCodeEditor.js";
10
11
  export { JamlIdeToolbar, } from "./components/JamlIdeToolbar.js";
11
12
  export { CardList } from "./components/CardList.js";
12
13
  export { extractVisualJamlItems, } from "./utils/jamlMapPreview.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utilities for converting between JAML text and JamlVisualFilter.
3
+ *
4
+ * Intentionally does NOT depend on a YAML library — uses the same
5
+ * line-by-line approach as jamlMapPreview.ts to stay zero-dep.
6
+ */
7
+ import type { JamlVisualFilter } from "../components/JamlIdeVisual.js";
8
+ export declare function jamlTextToVisualFilter(text: string): JamlVisualFilter;
9
+ export declare function visualFilterToJamlText(filter: JamlVisualFilter): string;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Utilities for converting between JAML text and JamlVisualFilter.
3
+ *
4
+ * Intentionally does NOT depend on a YAML library — uses the same
5
+ * line-by-line approach as jamlMapPreview.ts to stay zero-dep.
6
+ */
7
+ // ─── Text → Filter ────────────────────────────────────────────────────────────
8
+ function stripQuotes(s) {
9
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
10
+ return s.slice(1, -1).trim();
11
+ }
12
+ return s;
13
+ }
14
+ function parseScalarValue(raw) {
15
+ const v = stripQuotes(raw.trim().replace(/,$/, "").trim());
16
+ return v || null;
17
+ }
18
+ function parseInlineList(raw) {
19
+ const t = raw.trim();
20
+ if (t.startsWith("[") && t.includes("]")) {
21
+ const body = t.slice(1, t.indexOf("]"));
22
+ return body
23
+ .split(",")
24
+ .map((s) => parseScalarValue(s))
25
+ .filter((s) => s !== null);
26
+ }
27
+ const v = parseScalarValue(t);
28
+ return v ? [v] : [];
29
+ }
30
+ function topLevelScalar(lines, key) {
31
+ for (const line of lines) {
32
+ const m = new RegExp(`^${key}:\\s*(.+)$`).exec(line.trim());
33
+ if (m)
34
+ return stripQuotes(m[1].trim());
35
+ }
36
+ return undefined;
37
+ }
38
+ const CLAUSE_ZONE_KEYS = new Set([
39
+ "joker", "jokers", "commonJoker", "commonJokers", "uncommonJoker", "uncommonJokers",
40
+ "rareJoker", "rareJokers", "mixedJoker", "mixedJokers", "soulJoker", "legendaryJoker",
41
+ "voucher", "vouchers",
42
+ "tarot", "tarotCard", "spectral", "spectralCard", "planet", "planetCard",
43
+ "boss", "bosses",
44
+ "tag", "tags", "smallBlindTag", "bigBlindTag", "smallblindtag", "bigblindtag",
45
+ ]);
46
+ // JAML uses "mustnot" as zone key in some contexts; the visual filter uses "mustnot".
47
+ // The text format may use "mustnot" or "must_not" — handle both, normalise to "mustnot".
48
+ function sectionToZone(raw) {
49
+ if (raw === "must")
50
+ return "must";
51
+ if (raw === "should")
52
+ return "should";
53
+ if (raw === "mustnot" || raw === "must_not" || raw === "mustNot")
54
+ return "mustnot";
55
+ return null;
56
+ }
57
+ let _uid = 0;
58
+ function uid() {
59
+ return `clause-${++_uid}`;
60
+ }
61
+ export function jamlTextToVisualFilter(text) {
62
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
63
+ const filter = { must: [], should: [], mustnot: [] };
64
+ filter.name = topLevelScalar(lines, "name");
65
+ filter.author = topLevelScalar(lines, "author");
66
+ filter.description = topLevelScalar(lines, "description");
67
+ filter.deck = topLevelScalar(lines, "deck");
68
+ filter.stake = topLevelScalar(lines, "stake");
69
+ let zone = null;
70
+ let current = null;
71
+ const seen = new Set();
72
+ function flushClause() {
73
+ if (!current || !zone)
74
+ return;
75
+ const dedupeKey = `${zone}:${current.type}:${current.value.toLowerCase()}`;
76
+ if (!seen.has(dedupeKey)) {
77
+ seen.add(dedupeKey);
78
+ const clause = { id: uid(), type: current.type, value: current.value };
79
+ if (current.antes && current.antes.length > 0)
80
+ clause.antes = current.antes;
81
+ if (current.score !== undefined)
82
+ clause.score = current.score;
83
+ if (current.edition)
84
+ clause.edition = current.edition;
85
+ clause.label = current.value;
86
+ filter[zone].push(clause);
87
+ }
88
+ current = null;
89
+ }
90
+ for (const rawLine of lines) {
91
+ const trimmed = rawLine.trim();
92
+ if (!trimmed || trimmed.startsWith("#"))
93
+ continue;
94
+ // Top-level section header: must:/should:/mustnot:
95
+ const sectionMatch = /^(must|should|mustnot|must_not|mustNot):\s*$/.exec(trimmed);
96
+ if (sectionMatch) {
97
+ flushClause();
98
+ zone = sectionToZone(sectionMatch[1]);
99
+ continue;
100
+ }
101
+ // Top-level key (non-section) resets zone
102
+ const indent = rawLine.search(/\S|$/);
103
+ if (indent === 0 && /^[A-Za-z]/.test(trimmed) && !trimmed.startsWith("-")) {
104
+ flushClause();
105
+ if (!sectionToZone(trimmed.replace(/:.*/, "")))
106
+ zone = null;
107
+ continue;
108
+ }
109
+ if (!zone)
110
+ continue;
111
+ // New clause start: " - rareJoker: Blueprint"
112
+ const clauseStart = /^-\s*([A-Za-z][A-Za-z0-9]*):\s*(.*?)\s*$/.exec(trimmed);
113
+ if (clauseStart) {
114
+ flushClause();
115
+ const type = clauseStart[1];
116
+ const rawVal = clauseStart[2];
117
+ if (!CLAUSE_ZONE_KEYS.has(type))
118
+ continue;
119
+ const value = parseScalarValue(rawVal) ?? "Any";
120
+ current = { type, value };
121
+ continue;
122
+ }
123
+ // Continuation line inside a clause: " antes: [1,2,3]"
124
+ if (current && indent > 0 && !trimmed.startsWith("-")) {
125
+ const contMatch = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)\s*$/.exec(trimmed);
126
+ if (contMatch) {
127
+ const key = contMatch[1];
128
+ const val = contMatch[2];
129
+ if (key === "antes") {
130
+ const nums = parseInlineList(val)
131
+ .map(Number)
132
+ .filter((n) => !isNaN(n));
133
+ current.antes = nums;
134
+ }
135
+ else if (key === "score") {
136
+ const n = Number(val);
137
+ if (!isNaN(n))
138
+ current.score = n;
139
+ }
140
+ else if (key === "edition") {
141
+ current.edition = parseScalarValue(val) ?? undefined;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ flushClause();
147
+ return filter;
148
+ }
149
+ // ─── Filter → Text ───────────────────────────────────────────────────────────
150
+ function q(s) {
151
+ if (!s)
152
+ return "";
153
+ return /[:#\[\]{}|>&*!,'"?]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
154
+ }
155
+ function serializeClause(clause) {
156
+ let out = ` - ${clause.type}: ${q(clause.value)}\n`;
157
+ if (clause.antes && clause.antes.length > 0) {
158
+ out += ` antes: [${clause.antes.join(", ")}]\n`;
159
+ }
160
+ if (clause.score !== undefined) {
161
+ out += ` score: ${clause.score}\n`;
162
+ }
163
+ if (clause.edition) {
164
+ out += ` edition: ${q(clause.edition)}\n`;
165
+ }
166
+ return out;
167
+ }
168
+ export function visualFilterToJamlText(filter) {
169
+ const parts = [];
170
+ if (filter.name)
171
+ parts.push(`name: ${q(filter.name)}`);
172
+ if (filter.author)
173
+ parts.push(`author: ${q(filter.author)}`);
174
+ if (filter.description)
175
+ parts.push(`description: ${q(filter.description)}`);
176
+ if (filter.deck)
177
+ parts.push(`deck: ${q(filter.deck)}`);
178
+ if (filter.stake)
179
+ parts.push(`stake: ${q(filter.stake)}`);
180
+ const zones = [
181
+ { key: "must", label: "must", clauses: filter.must },
182
+ { key: "should", label: "should", clauses: filter.should },
183
+ { key: "mustnot", label: "mustnot", clauses: filter.mustnot },
184
+ ];
185
+ for (const { label, clauses } of zones) {
186
+ if (clauses.length > 0) {
187
+ parts.push(`${label}:`);
188
+ for (const c of clauses) {
189
+ parts.push(serializeClause(c).trimEnd());
190
+ }
191
+ }
192
+ }
193
+ return parts.join("\n") + "\n";
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaml-ui",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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,6 +37,7 @@
37
37
  "scripts": {
38
38
  "build": "tsc --pretty false",
39
39
  "dev": "tsc --watch",
40
+ "demo": "vite --config demo/vite.config.ts",
40
41
  "typecheck": "tsc --noEmit --pretty false",
41
42
  "prepack": "npm run build"
42
43
  },
@@ -66,8 +67,10 @@
66
67
  "author": "pifreak",
67
68
  "license": "MIT",
68
69
  "peerDependencies": {
70
+ "@monaco-editor/react": ">=4.0.0",
69
71
  "@react-spring/three": ">=9.0.0",
70
72
  "@react-three/fiber": ">=8.0.0",
73
+ "monaco-editor": ">=0.50.0",
71
74
  "motely-wasm": "^10.2.0 || ^11.0.0 || ^12.0.0",
72
75
  "react": "^18.2.0 || ^19.0.0",
73
76
  "react-dom": "^18.2.0 || ^19.0.0",
@@ -92,16 +95,20 @@
92
95
  }
93
96
  },
94
97
  "devDependencies": {
98
+ "@monaco-editor/react": "^4.7.0",
95
99
  "@react-spring/three": "^10.0.3",
96
100
  "@react-three/fiber": "^9.6.0",
97
101
  "@types/react": "^19.2.14",
98
102
  "@types/react-dom": "^19.2.3",
99
103
  "@types/three": "^0.184.0",
104
+ "@vitejs/plugin-react": "^5.0.4",
105
+ "monaco-editor": "^0.55.1",
100
106
  "motely-wasm": "^12.0.0",
101
107
  "react": "^19.2.4",
102
108
  "react-dom": "^19.2.4",
103
109
  "react-icons": "^5.6.0",
104
110
  "three": "^0.184.0",
105
- "typescript": "^5.9.3"
111
+ "typescript": "^5.9.3",
112
+ "vite": "^8.0.9"
106
113
  }
107
114
  }