jaml-ui 0.24.10 → 0.24.13

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.
@@ -26,7 +26,7 @@ const balatroTheme = EditorView.theme({
26
26
  height: "100%",
27
27
  },
28
28
  ".cm-content": {
29
- fontFamily: "'m6x11mono', 'm6x11plus', ui-monospace, monospace",
29
+ fontFamily: "var(--j-font-code, 'JetBrains Mono', ui-monospace, monospace)",
30
30
  lineHeight: "22px",
31
31
  padding: "12px 0",
32
32
  caretColor: JimboColorOption.GOLD_TEXT,
@@ -19,10 +19,14 @@ export interface JamlIdeProps {
19
19
  className?: string;
20
20
  style?: React.CSSProperties;
21
21
  title?: string;
22
+ subtitle?: React.ReactNode;
23
+ compactHeader?: boolean;
22
24
  actions?: React.ReactNode;
23
25
  codePlaceholder?: string;
24
26
  onSearch?: () => void;
25
27
  isSearching?: boolean;
28
+ /** Hide the Balatro attribution footer. Default: false (always shown). */
29
+ hideFooter?: boolean;
26
30
  /**
27
31
  * Controlled visual filter. When provided alongside `onVisualFilterChange`, the Visual tab
28
32
  * is fully controlled by the parent. When absent, the Visual tab auto-derives from the text.
@@ -32,4 +36,4 @@ export interface JamlIdeProps {
32
36
  }
33
37
  export type { JamlVisualFilter } from "./JamlIdeVisual.js";
34
38
  export type { JamlVisualClause, JamlZone } from "./JamlIdeVisual.js";
35
- export declare function JamlIde({ jaml, defaultJaml, onChange, defaultMode, searchResults, className, style, title, actions, codePlaceholder, onSearch, isSearching, visualFilter, onVisualFilterChange, }: JamlIdeProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare function JamlIde({ jaml, defaultJaml, onChange, defaultMode, searchResults, className, style, title, subtitle, compactHeader, actions, codePlaceholder, onSearch, isSearching, hideFooter, visualFilter, onVisualFilterChange, }: JamlIdeProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useState } from "react";
4
+ import { JimboBalatroFooter } from "../ui/footer.js";
4
5
  import { JamlMapPreview } from "./JamlMapPreview.js";
5
6
  import { JamlMapEditor, CategoryMenu, JokerPicker, CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./jamlMap/index.js";
6
7
  import { JamlIdeToolbar } from "./JamlIdeToolbar.js";
@@ -88,7 +89,7 @@ function ResultsView({ results, jaml }) {
88
89
  })] })] })) : null] }, result.seed));
89
90
  }) }));
90
91
  }
91
- export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", searchResults = [], className = "", style, title = "JAML IDE", actions, codePlaceholder = "Enter JAML...", onSearch, isSearching = false, visualFilter, onVisualFilterChange, }) {
92
+ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", searchResults = [], className = "", style, title = "JAML IDE", subtitle = "Jimbo's Ante Markup Language", compactHeader = false, actions, codePlaceholder = "Enter JAML...", onSearch, isSearching = false, hideFooter = false, visualFilter, onVisualFilterChange, }) {
92
93
  const [mode, setMode] = useState(defaultMode);
93
94
  const [internalText, setInternalText] = useState(jaml ?? defaultJaml ?? "");
94
95
  const [lastJamlProp, setLastJamlProp] = useState(jaml);
@@ -184,9 +185,9 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
184
185
  alignItems: "center",
185
186
  justifyContent: "space-between",
186
187
  flexWrap: "wrap",
187
- gap: 12,
188
- padding: "10px 14px",
188
+ gap: compactHeader ? 8 : 12,
189
+ padding: compactHeader ? "8px 10px" : "10px 14px",
189
190
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
190
191
  background: JimboColorOption.TEAL_GREY,
191
- }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", 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, onAddClause: handleAddClause })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapEditor, { onChange: handleTextChange }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results, jaml: text }) })) : null] }), _jsx(JimboModal, { open: addZone !== null, onClose: handlePickerClose, children: addZone !== null && (pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: (cat) => setPickerFlow(cat) })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handlePickerSelect, onCancel: handlePickerClose })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handlePickerSelect, onCancel: handlePickerClose }))) })] }));
192
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), subtitle ? _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: subtitle }) : null] }), 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: mode === "map" ? "hidden" : "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange, onAddClause: handleAddClause })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapEditor, { onChange: handleTextChange }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results, jaml: text }) })) : null] }), !hideFooter && _jsx(JimboBalatroFooter, {}), _jsx(JimboModal, { open: addZone !== null, onClose: handlePickerClose, children: addZone !== null && (pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: (cat) => setPickerFlow(cat) })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handlePickerSelect, onCancel: handlePickerClose })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handlePickerSelect, onCancel: handlePickerClose }))) })] }));
192
193
  }
@@ -5,6 +5,7 @@ export interface JamlVisualClause {
5
5
  value: string;
6
6
  label?: string;
7
7
  antes?: number[];
8
+ boosterPacks?: number[];
8
9
  score?: number;
9
10
  edition?: string;
10
11
  }
@@ -21,11 +21,21 @@ function clauseSpriteSheet(type) {
21
21
  return "Jokers";
22
22
  if (type === "voucher")
23
23
  return "Vouchers";
24
- if (type === "smallblindtag" || type === "bigblindtag" || type === "tag")
24
+ if (type === "tag" ||
25
+ type === "tags" ||
26
+ type === "smallBlindTag" ||
27
+ type === "bigBlindTag" ||
28
+ type === "smallblindtag" ||
29
+ type === "bigblindtag")
25
30
  return "tags";
26
31
  if (type === "boss")
27
32
  return "BlindChips";
28
- if (type === "tarot" || type === "spectral")
33
+ if (type === "tarot" ||
34
+ type === "tarotCard" ||
35
+ type === "spectral" ||
36
+ type === "spectralCard" ||
37
+ type === "planet" ||
38
+ type === "planetCard")
29
39
  return "Tarots";
30
40
  return undefined;
31
41
  }
@@ -6,7 +6,7 @@ import { JokerPicker } from "./JokerPicker.js";
6
6
  import { CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./CategoryPicker.js";
7
7
  import { JimboButton, JimboModal } from "../../ui/panel.js";
8
8
  import { JimboText } from "../../ui/jimboText.js";
9
- import { JimboColorOption } from "../../ui/tokens.js";
9
+ import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
10
10
  import { JimboSprite } from "../../ui/sprites.js";
11
11
  // ─── Category menu items ─────────────────────────────────────────────────────
12
12
  const C = JimboColorOption;
@@ -48,12 +48,19 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
48
48
  const [ante, setAnte] = useState(1);
49
49
  const [antesState, setAntesState] = useState({});
50
50
  const [activeSlot, setActiveSlot] = useState(null);
51
+ const [activePackDetail, setActivePackDetail] = useState(null);
51
52
  const [pickerFlow, setPickerFlow] = useState("category");
53
+ const [activePackSelection, setActivePackSelection] = useState(null);
52
54
  const currentAnteSelections = antesState[ante] || {};
53
55
  const handleSlotTap = useCallback((anteIndex, id, forceCategory) => {
56
+ const existing = (antesState[anteIndex] || {})[id];
57
+ if (forceCategory === "pack" && existing?.packName) {
58
+ setActivePackDetail({ ante: anteIndex, id });
59
+ return;
60
+ }
54
61
  setActiveSlot({ ante: anteIndex, id, forceCategory });
55
62
  setPickerFlow(forceCategory || "category");
56
- }, []);
63
+ }, [antesState]);
57
64
  const handleSlotClear = useCallback((anteIndex, id) => {
58
65
  setAntesState((prev) => {
59
66
  const next = { ...prev };
@@ -65,25 +72,51 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
65
72
  onChangeRef.current?.(buildJamlText(next));
66
73
  return next;
67
74
  });
75
+ setActivePackDetail((prev) => prev && prev.ante === anteIndex && prev.id === id ? null : prev);
68
76
  }, []);
77
+ const handlePackChange = useCallback(() => {
78
+ if (!activePackDetail)
79
+ return;
80
+ setActivePackDetail(null);
81
+ setActivePackSelection(null);
82
+ setActiveSlot({ ante: activePackDetail.ante, id: activePackDetail.id, forceCategory: "pack" });
83
+ setPickerFlow("pack");
84
+ }, [activePackDetail]);
69
85
  const handleCategorySelect = useCallback((cat) => {
70
86
  setPickerFlow(cat);
71
87
  }, []);
72
88
  const handleItemSelect = useCallback((selection) => {
73
89
  if (!activeSlot)
74
90
  return;
91
+ if (activeSlot.forceCategory === "pack" && selection.category === "pack") {
92
+ const slotIndex = getPackSlotIndex(activeSlot.id);
93
+ if (slotIndex === null)
94
+ return;
95
+ const nextFlow = getPackFollowupFlow(selection.value);
96
+ setActivePackSelection({ packName: selection.value, slotIndex });
97
+ setPickerFlow(nextFlow);
98
+ return;
99
+ }
100
+ const finalSelection = activePackSelection
101
+ ? { ...selection, packName: activePackSelection.packName, boosterPacks: [activePackSelection.slotIndex] }
102
+ : selection;
75
103
  setAntesState((prev) => {
76
104
  const next = { ...prev };
77
105
  const nextAnte = { ...(next[activeSlot.ante] || {}) };
78
- nextAnte[activeSlot.id] = { ...selection, zone: currentZone };
106
+ nextAnte[activeSlot.id] = { ...finalSelection, zone: currentZone };
79
107
  next[activeSlot.ante] = nextAnte;
80
108
  onChangeRef.current?.(buildJamlText(next));
81
109
  return next;
82
110
  });
111
+ setActivePackSelection(null);
83
112
  setActiveSlot(null);
84
- }, [activeSlot, currentZone]);
113
+ }, [activePackSelection, activeSlot, currentZone]);
85
114
  const handlePickerCancel = useCallback(() => {
86
- if (activeSlot?.forceCategory) {
115
+ if (activeSlot?.forceCategory === "pack" && activePackSelection) {
116
+ setActivePackSelection(null);
117
+ setPickerFlow("pack");
118
+ }
119
+ else if (activeSlot?.forceCategory) {
87
120
  setActiveSlot(null);
88
121
  }
89
122
  else if (pickerFlow !== "category") {
@@ -92,11 +125,16 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
92
125
  else {
93
126
  setActiveSlot(null);
94
127
  }
95
- }, [activeSlot, pickerFlow]);
128
+ }, [activePackSelection, activeSlot, pickerFlow]);
96
129
  const handleOverlayClose = useCallback(() => {
130
+ setActivePackSelection(null);
97
131
  setActiveSlot(null);
132
+ setActivePackDetail(null);
98
133
  }, []);
99
134
  const jamlText = useMemo(() => buildJamlText(antesState), [antesState]);
135
+ const activePackDetailSelection = activePackDetail
136
+ ? (antesState[activePackDetail.ante] || {})[activePackDetail.id]
137
+ : undefined;
100
138
  const handleScrollAttach = useCallback((node) => {
101
139
  if (!node)
102
140
  return;
@@ -110,18 +148,26 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
110
148
  };
111
149
  return (_jsxs("div", { style: { width: "100%", height: "100%", display: "flex", flexDirection: "column" }, children: [_jsx("div", { ref: handleScrollAttach, className: "hide-scrollbar", style: {
112
150
  flex: 1,
113
- overflowY: "auto",
151
+ overflowY: "scroll",
152
+ overflowX: "hidden",
114
153
  scrollSnapType: "y mandatory",
115
- scrollBehavior: "smooth"
154
+ scrollBehavior: "smooth",
155
+ WebkitOverflowScrolling: "touch",
156
+ overscrollBehaviorY: "contain",
116
157
  }, children: Array.from({ length: 40 }, (_, i) => i).map((a) => (_jsxs("div", { style: {
117
158
  scrollSnapAlign: "start",
159
+ scrollSnapStop: "always",
118
160
  padding: "24px 8px 64px 8px",
119
- minHeight: "100%", // ensuring each ante takes at least full viewport height to snap cleanly
161
+ minHeight: "100%",
162
+ boxSizing: "border-box",
120
163
  display: "flex",
121
164
  flexDirection: "column",
122
165
  gap: 24,
123
166
  borderBottom: `2px solid ${C.DARK_GREY}`
124
- }, children: [_jsxs(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 8 }, children: ["Ante ", a] }), _jsxs("div", { className: "j-flex j-justify-between j-items-end", children: [_jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Voucher" }), renderSlot(a, `ante_${a}_voucher`, 42, "Vouchers", "voucher")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Small" }), renderSlot(a, `ante_${a}_tag_small`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Big" }), renderSlot(a, `ante_${a}_tag_big`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Boss" }), renderSlot(a, `ante_${a}_boss`, 42, "BlindChips", "boss")] })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Shop Items" }), _jsx("div", { className: "j-flex hide-scrollbar j-gap-sm", style: { overflowX: "auto", paddingBottom: 8 }, children: [1, 2, 3, 4, 5, 6, 7, 8].map(i => renderSlot(a, `ante_${a}_shop_${i}`, 52, "Jokers")) })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Packs" }), _jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: [1, 2, 3, 4, 5, 6].map(i => renderSlot(a, `ante_${a}_pack_${i}`, 64, "Boosters", "pack")) })] })] }, a))) }), _jsx(JimboModal, { open: activeSlot !== null, onClose: handlePickerCancel, title: pickerFlow === "category" ? "Select Category" : undefined, className: "j-picker-modal", children: activeSlot !== null && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsxs("div", { className: "j-inner-panel", style: { padding: "8px 10px", marginBottom: 2 }, children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { display: "block", marginBottom: 6 }, children: "This pick will be added as" }), _jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: ["must", "should", "mustnot"].map((z) => (_jsx(JimboButton, { tone: currentZone === z ? ZONE_TONE[z] : "grey", size: "xs", onClick: () => setCurrentZone(z), children: ZONE_LABEL[z] }, z))) })] }), pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: handleCategorySelect })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handleItemSelect, onCancel: handlePickerCancel })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handleItemSelect, onCancel: handlePickerCancel }))] })) })] }));
167
+ }, children: [_jsxs(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 8 }, children: ["Ante ", a] }), _jsxs("div", { className: "j-flex j-justify-between j-items-end", children: [_jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Voucher" }), renderSlot(a, `ante_${a}_voucher`, 42, "Vouchers", "voucher")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Small" }), renderSlot(a, `ante_${a}_tag_small`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Big" }), renderSlot(a, `ante_${a}_tag_big`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Boss" }), renderSlot(a, `ante_${a}_boss`, 42, "BlindChips", "boss")] })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Shop Items" }), _jsx("div", { className: "j-flex hide-scrollbar j-gap-sm", style: { overflowX: "auto", paddingBottom: 8 }, children: [1, 2, 3, 4, 5, 6, 7, 8].map(i => renderSlot(a, `ante_${a}_shop_${i}`, 52, "Jokers")) })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Packs" }), _jsx("div", { className: "j-flex-col j-gap-sm", children: getPackRows(a).map((row, rowIndex) => (_jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "nowrap" }, children: row.map(i => renderSlot(a, `ante_${a}_pack_${i}`, 64, "Boosters", "pack")) }, rowIndex))) })] })] }, a))) }), _jsx(JimboModal, { open: activeSlot !== null, onClose: handlePickerCancel, title: pickerFlow === "category" ? "Select Category" : undefined, className: "j-picker-modal", children: activeSlot !== null && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsxs("div", { className: "j-inner-panel", style: { padding: "8px 10px", marginBottom: 2 }, children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { display: "block", marginBottom: 6 }, children: "This pick will be added as" }), _jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: ["must", "should", "mustnot"].map((z) => (_jsx(JimboButton, { tone: currentZone === z ? ZONE_TONE[z] : "grey", size: "xs", onClick: () => setCurrentZone(z), children: ZONE_LABEL[z] }, z))) })] }), pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: handleCategorySelect })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handleItemSelect, onCancel: handlePickerCancel })) : pickerFlow === "packUnsupported" ? (_jsxs("div", { className: "j-flex-col j-gap-sm", style: { padding: 10, maxWidth: 360 }, children: [_jsxs("div", { className: "j-inner-panel", style: { padding: "10px 12px" }, children: [_jsx(JimboText, { size: "sm", tone: "orange", children: "Standard Packs need a dedicated playing-card picker." }), _jsx(JimboText, { size: "xs", tone: "grey", style: { display: "block", marginTop: 6 }, children: "Arcana, Celestial, Spectral, and Buffoon pack flows are wired. Standard Pack support can come next without faking the JAML shape." })] }), _jsx(JimboButton, { tone: "orange", size: "sm", fullWidth: true, onClick: () => {
168
+ setActivePackSelection(null);
169
+ setPickerFlow("pack");
170
+ }, children: "Back to Packs" })] })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handleItemSelect, onCancel: handlePickerCancel }))] })) }), _jsx(JimboModal, { open: activePackDetail !== null && !!activePackDetailSelection?.packName, onClose: handleOverlayClose, title: activePackDetailSelection?.packName ?? "Pack", className: "j-picker-modal", children: activePackDetail !== null && activePackDetailSelection?.packName && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10, maxWidth: 360 }, children: [_jsx("div", { className: "j-inner-panel", style: { padding: "10px 12px", background: withAlpha(C.DARKEST, 0.84) }, children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [_jsx(JimboSprite, { name: activePackDetailSelection.packName, sheet: "Boosters", width: 56 }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsxs(JimboText, { size: "sm", tone: "white", children: ["Ante ", activePackDetail.ante, " pack ", ((activePackDetailSelection.boosterPacks?.[0] ?? 0) + 1)] }), _jsx(JimboText, { size: "xs", tone: "grey", children: getPackHelperText(activePackDetailSelection.packName) })] })] }) }), _jsxs("div", { className: "j-inner-panel", style: { padding: "10px 12px" }, children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { display: "block", marginBottom: 8 }, children: "Peek" }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }, children: [_jsx(JimboSprite, { name: activePackDetailSelection.packName, sheet: "Boosters", width: 44 }), _jsx(JimboText, { size: "sm", tone: "grey", children: "\u2192" }), _jsx(JimboSprite, { name: activePackDetailSelection.value, sheet: categoryToPreviewSheet(activePackDetailSelection.category), width: 48 }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 3 }, children: [_jsx(JimboText, { size: "sm", tone: "white", children: activePackDetailSelection.value }), _jsxs(JimboText, { size: "xs", tone: "grey", children: [getSelectionCategoryLabel(activePackDetailSelection.category), " from ", activePackDetailSelection.packName] })] })] }), _jsxs("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: [_jsxs("div", { className: "j-inner-panel", style: { padding: "6px 8px", minWidth: 96 }, children: [_jsx(JimboText, { size: "micro", tone: "grey", style: { display: "block", marginBottom: 2 }, children: "Zone" }), _jsx(JimboText, { size: "xs", tone: getZoneTextTone(activePackDetailSelection.zone), children: ZONE_LABEL[activePackDetailSelection.zone] })] }), _jsxs("div", { className: "j-inner-panel", style: { padding: "6px 8px", minWidth: 120 }, children: [_jsx(JimboText, { size: "micro", tone: "grey", style: { display: "block", marginBottom: 2 }, children: "Clause" }), _jsx(JimboText, { size: "xs", tone: "white", children: activePackDetailSelection.clauseKey })] }), _jsxs("div", { className: "j-inner-panel", style: { padding: "6px 8px", minWidth: 96 }, children: [_jsx(JimboText, { size: "micro", tone: "grey", style: { display: "block", marginBottom: 2 }, children: "Source" }), _jsxs(JimboText, { size: "xs", tone: "white", children: ["boosterPacks: [", activePackDetailSelection.boosterPacks?.join(", ") ?? "", "]"] })] })] })] }), _jsxs("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: [_jsx(JimboButton, { tone: "blue", size: "sm", fullWidth: true, onClick: handlePackChange, children: "Re-pick Contents" }), _jsx(JimboButton, { tone: "red", size: "sm", fullWidth: true, onClick: () => handleSlotClear(activePackDetail.ante, activePackDetail.id), children: "Clear This Pack" })] })] })) })] }));
125
171
  }
126
172
  // ─── Category Selection Menu ─────────────────────────────────────────────────
127
173
  export function CategoryMenu({ onSelect, }) {
@@ -135,6 +181,79 @@ export function CategoryMenu({ onSelect, }) {
135
181
  }, children: CATEGORIES.map((cat) => (_jsx(JimboButton, { tone: cat.tone, size: "sm", fullWidth: true, onClick: () => onSelect(cat.key), children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, width: "100%", textAlign: "left" }, children: [_jsx(JimboSprite, { name: cat.sprite, sheet: cat.sheet, width: 24 }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 1 }, children: [_jsx("span", { style: { fontSize: 11 }, children: cat.label }), _jsx("span", { style: { fontSize: 8, opacity: 0.7, letterSpacing: "0.04em", lineHeight: 1, whiteSpace: "normal" }, children: cat.hint })] })] }) }, cat.key))) }));
136
182
  }
137
183
  // ─── Build JAML text from slots ──────────────────────────────────────────────
184
+ function getPackSlotIndex(slotId) {
185
+ const match = /_pack_(\d+)$/.exec(slotId);
186
+ if (!match)
187
+ return null;
188
+ return Number(match[1]) - 1;
189
+ }
190
+ function getPackRows(ante) {
191
+ const packCount = ante < 2 ? 4 : 6;
192
+ const rows = [];
193
+ for (let i = 1; i <= packCount; i += 2) {
194
+ rows.push([i, i + 1].filter((slot) => slot <= packCount));
195
+ }
196
+ return rows;
197
+ }
198
+ function getPackFollowupFlow(packName) {
199
+ if (packName.includes("Buffoon"))
200
+ return "joker";
201
+ if (packName.includes("Arcana"))
202
+ return "tarot";
203
+ if (packName.includes("Celestial"))
204
+ return "planet";
205
+ if (packName.includes("Spectral"))
206
+ return "spectral";
207
+ return "packUnsupported";
208
+ }
209
+ function categoryToPreviewSheet(category) {
210
+ if (category === "joker")
211
+ return "Jokers";
212
+ if (category === "voucher")
213
+ return "Vouchers";
214
+ if (category === "tag")
215
+ return "tags";
216
+ if (category === "boss")
217
+ return "BlindChips";
218
+ if (category === "pack")
219
+ return "Boosters";
220
+ return "Tarots";
221
+ }
222
+ function getPackHelperText(packName) {
223
+ if (packName.includes("Buffoon"))
224
+ return "Peek the joker this pack is meant to carry.";
225
+ if (packName.includes("Arcana"))
226
+ return "Peek the tarot card currently attached to this pack.";
227
+ if (packName.includes("Celestial"))
228
+ return "Peek the planet card currently attached to this pack.";
229
+ if (packName.includes("Spectral"))
230
+ return "Peek the spectral card currently attached to this pack.";
231
+ return "Peek the item currently attached to this pack.";
232
+ }
233
+ function getSelectionCategoryLabel(category) {
234
+ if (category === "joker")
235
+ return "Joker";
236
+ if (category === "voucher")
237
+ return "Voucher";
238
+ if (category === "tag")
239
+ return "Tag";
240
+ if (category === "boss")
241
+ return "Boss Blind";
242
+ if (category === "tarot")
243
+ return "Tarot Card";
244
+ if (category === "planet")
245
+ return "Planet Card";
246
+ if (category === "spectral")
247
+ return "Spectral Card";
248
+ return "Pack";
249
+ }
250
+ function getZoneTextTone(zone) {
251
+ if (zone === "must")
252
+ return "blue";
253
+ if (zone === "should")
254
+ return "green";
255
+ return "red";
256
+ }
138
257
  function buildJamlText(antes) {
139
258
  const byZone = {
140
259
  must: {}, should: {}, mustnot: {}
@@ -147,13 +266,14 @@ function buildJamlText(antes) {
147
266
  if (!byZone[zone][key]) {
148
267
  byZone[zone][key] = [];
149
268
  }
150
- const existing = byZone[zone][key].find(item => item.value === sel.value);
269
+ const existing = byZone[zone][key].find(item => item.value === sel.value &&
270
+ (item.boosterPacks ?? []).join(",") === (sel.boosterPacks ?? []).join(","));
151
271
  if (existing) {
152
272
  if (!existing.antes.includes(anteNum))
153
273
  existing.antes.push(anteNum);
154
274
  }
155
275
  else {
156
- byZone[zone][key].push({ value: sel.value, antes: [anteNum] });
276
+ byZone[zone][key].push({ value: sel.value, antes: [anteNum], boosterPacks: sel.boosterPacks });
157
277
  }
158
278
  }
159
279
  }
@@ -175,6 +295,10 @@ function buildJamlText(antes) {
175
295
  if (item.antes.length < 8) {
176
296
  lines.push(` antes: [${item.antes.sort((a, b) => a - b).join(", ")}]`);
177
297
  }
298
+ if (item.boosterPacks && item.boosterPacks.length > 0) {
299
+ lines.push(` sources:`);
300
+ lines.push(` boosterPacks: [${item.boosterPacks.join(", ")}]`);
301
+ }
178
302
  }
179
303
  }
180
304
  }
@@ -10,6 +10,10 @@ export interface SlotSelection {
10
10
  value: string;
11
11
  /** JAML clause key (e.g. "commonJoker", "legendaryJoker", "voucher"). */
12
12
  clauseKey: string;
13
+ /** Optional selected pack display name for pack slots. */
14
+ packName?: string;
15
+ /** Optional source pack indices for pack-derived item clauses. */
16
+ boosterPacks?: number[];
13
17
  /** Optional rarity for jokers. */
14
18
  rarity?: "common" | "uncommon" | "rare" | "legendary";
15
19
  }
@@ -35,9 +35,12 @@ export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onC
35
35
  const borderColor = ZONE_BORDER[zone];
36
36
  const isEmpty = !selection;
37
37
  const cardH = Math.round((width * 95) / 71);
38
- // Determine what to render
39
- const spriteName = selection?.value ?? "";
40
- const spriteSheet = selection ? categoryToSheet(selection.category) ?? sheetType : sheetType;
38
+ const spriteName = selection?.packName ?? selection?.value ?? "";
39
+ const spriteSheet = selection?.packName
40
+ ? "Boosters"
41
+ : selection
42
+ ? categoryToSheet(selection.category) ?? sheetType
43
+ : sheetType;
41
44
  const scale = pressed
42
45
  ? 0.95
43
46
  : hover
@@ -1 +1,29 @@
1
- export {};
1
+ declare let MotelyWasm: any;
2
+ declare let MotelyWasmEvents: any;
3
+ declare let activeSearch: {
4
+ cancel(): void;
5
+ } | null;
6
+ declare let activeSearchRunId: number;
7
+ type WorkerMessage = {
8
+ type: "init";
9
+ url: string;
10
+ } | {
11
+ type: "start";
12
+ jaml: string;
13
+ mode?: string;
14
+ count?: number;
15
+ aesthetic?: number;
16
+ seeds?: string[];
17
+ keywords?: string;
18
+ padding?: string;
19
+ batchCharCount?: number;
20
+ startBatch?: string;
21
+ endBatch?: string;
22
+ } | {
23
+ type: "stop";
24
+ } | {
25
+ type: "get_tally_labels";
26
+ jaml: string;
27
+ };
28
+ declare function post(message: Record<string, unknown>): void;
29
+ declare function resetEventHandlers(): void;
@@ -1,68 +1,117 @@
1
- import motely, { Motely } from "motely-wasm";
2
- // Boot motely immediately when this module is loaded
3
- motely.boot().catch(console.error);
1
+ "use strict";
2
+ let MotelyWasm = null;
3
+ let MotelyWasmEvents = null;
4
4
  let activeSearch = null;
5
- self.addEventListener('message', function (e) {
5
+ let activeSearchRunId = 0;
6
+ function post(message) {
7
+ self.postMessage(message);
8
+ }
9
+ function resetEventHandlers() {
10
+ if (!MotelyWasmEvents)
11
+ return;
12
+ MotelyWasmEvents.notifyResult = () => { };
13
+ MotelyWasmEvents.notifyProgress = () => { };
14
+ MotelyWasmEvents.notifyComplete = () => { };
15
+ }
16
+ self.addEventListener("message", async (e) => {
6
17
  const msg = e.data;
7
- if (msg.type === 'start') {
8
- const validation = Motely.MotelyWasm.validateJaml(msg.jaml);
9
- if (validation !== 'valid') {
10
- self.postMessage({ type: 'error', message: validation });
18
+ if (msg.type === "init") {
19
+ try {
20
+ const mod = await import(/* @vite-ignore */ msg.url);
21
+ await mod.default.boot();
22
+ const motely = mod.Motely;
23
+ MotelyWasm = motely.MotelyWasm;
24
+ MotelyWasmEvents = motely.MotelyWasmEvents;
25
+ post({ type: "ready" });
26
+ }
27
+ catch (err) {
28
+ post({ type: "error", message: String(err) });
29
+ }
30
+ return;
31
+ }
32
+ if (msg.type === "start") {
33
+ if (!MotelyWasm) {
34
+ post({ type: "error", message: "Not initialized" });
35
+ return;
36
+ }
37
+ const validation = MotelyWasm.validateJaml(msg.jaml);
38
+ if (validation !== "valid") {
39
+ post({ type: "error", message: validation });
11
40
  return;
12
41
  }
42
+ const runId = ++activeSearchRunId;
13
43
  function cleanup() {
14
- Motely.MotelyWasmEvents.notifyResult = () => { };
15
- Motely.MotelyWasmEvents.notifyProgress = () => { };
16
- Motely.MotelyWasmEvents.notifyComplete = () => { };
17
- activeSearch = null;
44
+ resetEventHandlers();
45
+ if (runId === activeSearchRunId) {
46
+ activeSearch = null;
47
+ }
18
48
  }
19
- Motely.MotelyWasmEvents.notifyResult = function (seed, score, tallyColumns) {
20
- self.postMessage({ type: 'result', seed, score, tallyColumns: Array.from(tallyColumns) });
49
+ MotelyWasmEvents.notifyResult = (seed, score, tallyColumns) => {
50
+ if (runId !== activeSearchRunId)
51
+ return;
52
+ post({ type: "result", seed, score, tallyColumns: Array.from(tallyColumns) });
21
53
  };
22
- Motely.MotelyWasmEvents.notifyProgress = function (searched, matching) {
23
- self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });
54
+ MotelyWasmEvents.notifyProgress = (searched, matching) => {
55
+ if (runId !== activeSearchRunId)
56
+ return;
57
+ post({ type: "progress", searched: searched.toString(), matching: matching.toString() });
24
58
  };
25
- Motely.MotelyWasmEvents.notifyComplete = function (status, searched, matched) {
59
+ MotelyWasmEvents.notifyComplete = (status, searched, matched) => {
60
+ if (runId !== activeSearchRunId)
61
+ return;
26
62
  cleanup();
27
- self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });
63
+ post({ type: "complete", status, searched: searched.toString(), matched: matched.toString() });
28
64
  };
29
65
  try {
30
- const mode = msg.mode || 'random';
31
- if (mode === 'random') {
32
- activeSearch = Motely.MotelyWasm.startRandomSearch(msg.jaml, msg.count);
66
+ const mode = msg.mode || "random";
67
+ if (mode === "random") {
68
+ activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
33
69
  }
34
- else if (mode === 'aesthetic') {
35
- activeSearch = Motely.MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);
70
+ else if (mode === "aesthetic") {
71
+ activeSearch = MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);
36
72
  }
37
- else if (mode === 'seedList') {
38
- activeSearch = Motely.MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);
73
+ else if (mode === "seedList") {
74
+ activeSearch = MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);
39
75
  }
40
- else if (mode === 'keyword') {
41
- activeSearch = Motely.MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || '');
76
+ else if (mode === "keyword") {
77
+ activeSearch = MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || "");
42
78
  }
43
- else if (mode === 'sequential') {
44
- activeSearch = Motely.MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch), BigInt(msg.endBatch));
79
+ else if (mode === "sequential") {
80
+ activeSearch = MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch || "0"), BigInt(msg.endBatch || "0"));
81
+ }
82
+ else {
83
+ post({ type: "error", message: `Unknown search mode: ${mode}` });
84
+ cleanup();
85
+ return;
45
86
  }
46
87
  }
47
88
  catch (err) {
48
89
  cleanup();
49
- self.postMessage({ type: 'error', message: String(err) });
90
+ post({ type: "error", message: String(err) });
50
91
  }
92
+ return;
51
93
  }
52
- else if (msg.type === 'stop') {
94
+ if (msg.type === "stop") {
95
+ activeSearchRunId++;
96
+ resetEventHandlers();
53
97
  if (activeSearch) {
54
98
  activeSearch.cancel();
55
99
  activeSearch = null;
56
- self.postMessage({ type: 'cancelled' });
57
100
  }
101
+ post({ type: "cancelled" });
102
+ return;
58
103
  }
59
- else if (msg.type === 'get_tally_labels') {
104
+ if (msg.type === "get_tally_labels") {
105
+ if (!MotelyWasm) {
106
+ post({ type: "error", message: "Not initialized" });
107
+ return;
108
+ }
60
109
  try {
61
- const labels = Motely.MotelyWasm.getTallyLabels(msg.jaml);
62
- self.postMessage({ type: 'tally_labels', labels: Array.from(labels) });
110
+ const labels = MotelyWasm.getTallyLabels(msg.jaml);
111
+ post({ type: "tally_labels", labels: Array.from(labels) });
63
112
  }
64
113
  catch (err) {
65
- self.postMessage({ type: 'error', message: String(err) });
114
+ post({ type: "error", message: String(err) });
66
115
  }
67
116
  }
68
117
  });
@@ -9,106 +9,8 @@ const INITIAL_STATE = {
9
9
  seedsPerSecond: 0,
10
10
  tallyLabels: [],
11
11
  };
12
- const SEARCH_WORKER_CODE = `
13
- let MotelyWasm = null;
14
- let MotelyWasmEvents = null;
15
- let activeSearch = null;
16
- let activeSearchRunId = 0;
17
-
18
- self.addEventListener('message', async function(e) {
19
- const msg = e.data;
20
-
21
- if (msg.type === 'init') {
22
- try {
23
- const mod = await import(msg.url);
24
- await mod.default.boot();
25
- const motely = mod.Motely;
26
- MotelyWasm = motely.MotelyWasm;
27
- MotelyWasmEvents = motely.MotelyWasmEvents;
28
- self.postMessage({ type: 'ready' });
29
- } catch (err) {
30
- self.postMessage({ type: 'error', message: String(err) });
31
- }
32
- return;
33
- }
34
-
35
- if (msg.type === 'start') {
36
- if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }
37
- const validation = MotelyWasm.validateJaml(msg.jaml);
38
- if (validation !== 'valid') { self.postMessage({ type: 'error', message: validation }); return; }
39
- const runId = ++activeSearchRunId;
40
-
41
- function cleanup() {
42
- MotelyWasmEvents.notifyResult = () => {};
43
- MotelyWasmEvents.notifyProgress = () => {};
44
- MotelyWasmEvents.notifyComplete = () => {};
45
- if (runId === activeSearchRunId) {
46
- activeSearch = null;
47
- }
48
- }
49
-
50
- MotelyWasmEvents.notifyResult = function(seed, score, tallyColumns) {
51
- if (runId !== activeSearchRunId) return;
52
- self.postMessage({ type: 'result', seed, score, tallyColumns: Array.from(tallyColumns) });
53
- };
54
- MotelyWasmEvents.notifyProgress = function(searched, matching) {
55
- if (runId !== activeSearchRunId) return;
56
- self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });
57
- };
58
- MotelyWasmEvents.notifyComplete = function(status, searched, matched) {
59
- if (runId !== activeSearchRunId) return;
60
- cleanup();
61
- self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });
62
- };
63
-
64
- try {
65
- const mode = msg.mode || 'random';
66
-
67
- if (mode === 'random') {
68
- activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
69
- } else if (mode === 'aesthetic') {
70
- activeSearch = MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);
71
- } else if (mode === 'seedList') {
72
- activeSearch = MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);
73
- } else if (mode === 'keyword') {
74
- activeSearch = MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || '');
75
- } else if (mode === 'sequential') {
76
- activeSearch = MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch), BigInt(msg.endBatch));
77
- } else {
78
- self.postMessage({ type: 'error', message: 'Unknown search mode: ' + mode });
79
- cleanup();
80
- return;
81
- }
82
- } catch (err) {
83
- cleanup();
84
- self.postMessage({ type: 'error', message: String(err) });
85
- }
86
- return;
87
- }
88
-
89
- if (msg.type === 'stop') {
90
- activeSearchRunId++;
91
- MotelyWasmEvents.notifyResult = () => {};
92
- MotelyWasmEvents.notifyProgress = () => {};
93
- MotelyWasmEvents.notifyComplete = () => {};
94
- if (activeSearch) { activeSearch.cancel(); activeSearch = null; }
95
- self.postMessage({ type: 'cancelled' });
96
- }
97
-
98
- if (msg.type === 'get_tally_labels') {
99
- if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }
100
- try {
101
- const labels = MotelyWasm.getTallyLabels(msg.jaml);
102
- self.postMessage({ type: 'tally_labels', labels: Array.from(labels) });
103
- } catch (err) {
104
- self.postMessage({ type: 'error', message: String(err) });
105
- }
106
- }
107
- });
108
- `;
109
12
  function createWorker() {
110
- const blob = new Blob([SEARCH_WORKER_CODE], { type: "application/javascript" });
111
- return new Worker(URL.createObjectURL(blob), { type: "module" });
13
+ return new Worker(new URL("./searchWorker.js", import.meta.url), { type: "module" });
112
14
  }
113
15
  export function useSearch(motelyWasmUrl) {
114
16
  const [state, setState] = useState(INITIAL_STATE);
package/dist/ui/jimbo.css CHANGED
@@ -56,6 +56,10 @@
56
56
 
57
57
  /* Typography */
58
58
  --j-font: 'm6x11plus', 'Courier New', monospace;
59
+ /* Coding font — used by JamlCodeEditor and .j-code-block. JetBrains Mono
60
+ with a long OS-native fallback chain so the UI still reads as code even
61
+ when the Google Fonts stylesheet fails to load. */
62
+ --j-font-code: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', SFMono-Regular, Menlo, Consolas, ui-monospace, 'Courier New', monospace;
59
63
  --j-text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.8);
60
64
 
61
65
  /* Spacing */
@@ -756,7 +760,7 @@
756
760
  .j-code-block__pre {
757
761
  padding: 12px;
758
762
  overflow-x: auto;
759
- font-family: monospace;
763
+ font-family: var(--j-font-code);
760
764
  font-size: 0.875rem;
761
765
  line-height: 1.6;
762
766
  color: #f6f0d5;
@@ -915,11 +919,18 @@
915
919
  max-height: 90vh;
916
920
  display: flex;
917
921
  flex-direction: column;
922
+ overflow: hidden;
923
+ }
924
+
925
+ .j-modal .j-panel__body {
926
+ overflow-y: auto;
927
+ min-height: 0;
918
928
  }
919
929
 
920
930
  .j-modal__title {
921
931
  text-align: center;
922
932
  margin: 0 0 var(--j-space-xl);
933
+ flex-shrink: 0;
923
934
  }
924
935
 
925
936
 
@@ -4,6 +4,7 @@ import { JimboButton } from './panel.js';
4
4
  import { JimboSprite } from './sprites.js';
5
5
  import { JimboText } from './jimboText.js';
6
6
  import { JimboApp, JimboAppFooter } from './jimboApp.js';
7
+ import { JimboBalatroFooter } from './footer.js';
7
8
  import { JimboSectionHeader } from './jimboSectionHeader.js';
8
9
  import { JimboInfoCard, JimboInfoCardBody, JimboInfoCardTitle, JimboInfoCardSub, JimboInfoCardAside } from './jimboInfoCard.js';
9
10
  /**
@@ -15,5 +16,5 @@ export function Showcase({ title = 'Balatro', subtitle = 'Seed Curator', hotFilt
15
16
  padding: '3px 8px',
16
17
  background: 'var(--j-dark-grey)', borderRadius: 4,
17
18
  border: '1px solid var(--j-panel-edge)',
18
- }, 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" })] })] }));
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
20
  }
@@ -78,6 +78,8 @@ export function jamlTextToVisualFilter(text) {
78
78
  const clause = { id: uid(), type: current.type, value: current.value };
79
79
  if (current.antes && current.antes.length > 0)
80
80
  clause.antes = current.antes;
81
+ if (current.boosterPacks && current.boosterPacks.length > 0)
82
+ clause.boosterPacks = current.boosterPacks;
81
83
  if (current.score !== undefined)
82
84
  clause.score = current.score;
83
85
  if (current.edition)
@@ -132,6 +134,12 @@ export function jamlTextToVisualFilter(text) {
132
134
  .filter((n) => !isNaN(n));
133
135
  current.antes = nums;
134
136
  }
137
+ else if (key === "boosterPacks") {
138
+ const nums = parseInlineList(val)
139
+ .map(Number)
140
+ .filter((n) => !isNaN(n));
141
+ current.boosterPacks = nums;
142
+ }
135
143
  else if (key === "score") {
136
144
  const n = Number(val);
137
145
  if (!isNaN(n))
@@ -140,6 +148,9 @@ export function jamlTextToVisualFilter(text) {
140
148
  else if (key === "edition") {
141
149
  current.edition = parseScalarValue(val) ?? undefined;
142
150
  }
151
+ else if (key === "sources") {
152
+ // handled by subsequent nested lines like `boosterPacks: [0, 1]`
153
+ }
143
154
  }
144
155
  }
145
156
  }
@@ -157,6 +168,10 @@ function serializeClause(clause) {
157
168
  if (clause.antes && clause.antes.length > 0) {
158
169
  out += ` antes: [${clause.antes.join(", ")}]\n`;
159
170
  }
171
+ if (clause.boosterPacks && clause.boosterPacks.length > 0) {
172
+ out += ` sources:\n`;
173
+ out += ` boosterPacks: [${clause.boosterPacks.join(", ")}]\n`;
174
+ }
160
175
  if (clause.score !== undefined) {
161
176
  out += ` score: ${clause.score}\n`;
162
177
  }
package/fonts.css CHANGED
@@ -1,3 +1,12 @@
1
+ /*
2
+ * Coding font: JetBrains Mono via Google Fonts (woff2, variable axis).
3
+ * Falls through to Cascadia Code / Fira Code / ui-monospace when offline.
4
+ * Using @import instead of @font-face keeps this stylesheet bundler-safe —
5
+ * no absolute URLs to self-host, no extra asset copy step for consumers.
6
+ * Must be the first rule in the file per CSS @import spec.
7
+ */
8
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,700;1,400&display=swap');
9
+
1
10
  @font-face {
2
11
  font-family: 'm6x11plus';
3
12
  src: local('m6x11plus'), local('m6x11plusplus'), url('./assets/fonts/m6x11plusplus.otf') format('opentype');
package/jaml.schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://www.seedfinder.app/jaml.schema.json",
4
- "version": "5.1.0.0",
4
+ "version": "15.1.2",
5
5
  "title": "JAML — Jimbo's Ante Markup Language",
6
6
  "description": "JSON Schema for JAML (.jaml), Motely's Balatro seed search language. Use it for validation, completions, and editor tooling.",
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaml-ui",
3
- "version": "0.24.10",
3
+ "version": "0.24.13",
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",
@@ -96,7 +96,7 @@
96
96
  "@react-spring/three": ">=9.0.0",
97
97
  "@react-three/drei": ">=9.0.0",
98
98
  "@react-three/fiber": ">=8.0.0",
99
- "motely-wasm": "^15.1.1",
99
+ "motely-wasm": "^15.1.2",
100
100
  "react": "^18.2.0 || ^19.0.0",
101
101
  "react-dom": "^18.2.0 || ^19.0.0",
102
102
  "react-icons": ">=5.0.0",
@@ -140,7 +140,7 @@
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.1",
143
+ "motely-wasm": "^15.1.2",
144
144
  "playwright": "^1.59.1",
145
145
  "react": "^19.2.4",
146
146
  "react-dom": "^19.2.4",
@@ -159,11 +159,11 @@
159
159
  "@codemirror/view": "^6.41.1",
160
160
  "@json-render/core": "^0.18.0",
161
161
  "@lezer/highlight": "^1.2.3",
162
+ "@types/js-yaml": "^4.0.9",
162
163
  "clsx": "^2.1.1",
163
164
  "js-yaml": "^4.1.1",
164
165
  "lucide-react": "^1.14.0",
165
166
  "tailwind-merge": "^2.6.1",
166
- "zustand": "^5.0.0",
167
- "@types/js-yaml": "^4.0.9"
167
+ "zustand": "^5.0.0"
168
168
  }
169
169
  }