jaml-ui 0.17.1 → 0.17.3

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.
@@ -0,0 +1,32 @@
1
+ import type { SpriteEntry } from "../../sprites/spriteData.js";
2
+ import type { SpriteSheetType } from "../../sprites/spriteMapper.js";
3
+ import type { SlotSelection, SlotCategory } from "./MysterySlot.js";
4
+ export interface CategoryPickerConfig {
5
+ /** Display title, e.g. "Vouchers" */
6
+ title: string;
7
+ /** The SlotCategory value */
8
+ category: SlotCategory;
9
+ /** JAML clause key emitted on select, e.g. "voucher" */
10
+ clauseKey: string;
11
+ /** Which sprite sheet to render from */
12
+ sheet: SpriteSheetType;
13
+ /** Full list of items for the grid */
14
+ items: SpriteEntry[];
15
+ /** Accent color for the header/buttons */
16
+ accent: string;
17
+ /** Optional tooltip hint shown in the "Any" button area */
18
+ hint?: string;
19
+ }
20
+ export interface CategoryPickerProps {
21
+ config: CategoryPickerConfig;
22
+ onSelect: (selection: SlotSelection) => void;
23
+ onCancel: () => void;
24
+ }
25
+ export declare function CategoryPicker({ config, onSelect, onCancel }: CategoryPickerProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare const VOUCHER_PICKER_CONFIG: CategoryPickerConfig;
27
+ export declare const TAG_PICKER_CONFIG: CategoryPickerConfig;
28
+ export declare const BOSS_PICKER_CONFIG: CategoryPickerConfig;
29
+ export declare const TAROT_PICKER_CONFIG: CategoryPickerConfig;
30
+ export declare const PLANET_PICKER_CONFIG: CategoryPickerConfig;
31
+ export declare const SPECTRAL_PICKER_CONFIG: CategoryPickerConfig;
32
+ export declare const PACK_PICKER_CONFIG: CategoryPickerConfig;
@@ -0,0 +1,224 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback, useMemo } from "react";
4
+ import { JimboSprite } from "../../ui/sprites.js";
5
+ import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
6
+ // ─── Component ───────────────────────────────────────────────────────────────
7
+ export function CategoryPicker({ config, onSelect, onCancel }) {
8
+ const [search, setSearch] = useState("");
9
+ const filtered = useMemo(() => {
10
+ if (!search)
11
+ return config.items;
12
+ const q = search.toLowerCase();
13
+ return config.items.filter((item) => item.name.toLowerCase().includes(q));
14
+ }, [config.items, search]);
15
+ const handleSelect = useCallback((item) => {
16
+ onSelect({
17
+ category: config.category,
18
+ value: item.name,
19
+ clauseKey: config.clauseKey,
20
+ });
21
+ }, [onSelect, config]);
22
+ const handleAny = useCallback(() => {
23
+ onSelect({
24
+ category: config.category,
25
+ value: "Any",
26
+ clauseKey: config.clauseKey,
27
+ });
28
+ }, [onSelect, config]);
29
+ const C = JimboColorOption;
30
+ return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: { ...styles.header, borderBottomColor: withAlpha(config.accent, 0.3) }, children: [_jsx("button", { onClick: onCancel, style: styles.backBtn, children: "\u2715" }), _jsx("span", { style: { ...styles.title, color: config.accent }, children: config.title }), _jsx("div", { style: { width: 44 } })] }), _jsxs("div", { style: styles.searchRow, children: [_jsx("input", { type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: styles.searchInput }), _jsx("button", { onClick: handleAny, style: styles.anyBtn, children: "Any" })] }), config.hint && (_jsxs("div", { style: styles.hint, children: [_jsx("span", { children: "\uD83D\uDCA1" }), _jsx("span", { children: config.hint })] })), _jsxs("div", { style: styles.grid, children: [filtered.map((item) => (_jsxs("div", { onClick: () => handleSelect(item), title: item.name, style: styles.cell, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx("span", { style: styles.label, children: item.name })] }, item.name))), filtered.length === 0 && (_jsxs("div", { style: styles.emptyState, children: ["No matches for \"", search, "\""] }))] })] }));
31
+ }
32
+ // ─── Pre-built configs ───────────────────────────────────────────────────────
33
+ import { VOUCHERS, TAGS, BOSSES, BOOSTER_PACKS, TAROTS_AND_PLANETS } from "../../sprites/spriteData.js";
34
+ // Split consumables by type
35
+ const TAROT_CARDS = TAROTS_AND_PLANETS.filter((c) => {
36
+ const y = c.pos.y;
37
+ return y <= 2 && c.name !== "The Soul" && c.name !== "Black Hole";
38
+ }).filter((c) => {
39
+ // Tarots are y=0..1 + Judgement (y=2,x=0) + The World (y=2,x=1)
40
+ return c.pos.y <= 1 || (c.pos.y === 2 && c.pos.x <= 1);
41
+ });
42
+ const PLANET_CARDS = TAROTS_AND_PLANETS.filter((c) => {
43
+ return c.pos.y === 3 || // Main planets row
44
+ c.name === "Planet X" || c.name === "Ceres" || c.name === "Eris" ||
45
+ c.name === "Black Hole";
46
+ });
47
+ const SPECTRAL_CARDS = TAROTS_AND_PLANETS.filter((c) => {
48
+ return c.pos.y >= 4 || c.name === "The Soul";
49
+ });
50
+ const C = JimboColorOption;
51
+ export const VOUCHER_PICKER_CONFIG = {
52
+ title: "Vouchers",
53
+ category: "voucher",
54
+ clauseKey: "voucher",
55
+ sheet: "Vouchers",
56
+ items: VOUCHERS,
57
+ accent: C.GOLD,
58
+ };
59
+ export const TAG_PICKER_CONFIG = {
60
+ title: "Tags",
61
+ category: "tag",
62
+ clauseKey: "tag",
63
+ sheet: "tags",
64
+ items: TAGS,
65
+ accent: C.GREEN,
66
+ };
67
+ export const BOSS_PICKER_CONFIG = {
68
+ title: "Boss Blinds",
69
+ category: "boss",
70
+ clauseKey: "boss",
71
+ sheet: "BlindChips",
72
+ items: BOSSES,
73
+ accent: C.RED,
74
+ hint: "Boss Blinds appear at the end of each Ante.",
75
+ };
76
+ export const TAROT_PICKER_CONFIG = {
77
+ title: "Tarot Cards",
78
+ category: "tarot",
79
+ clauseKey: "tarotCard",
80
+ sheet: "Tarots",
81
+ items: TAROT_CARDS,
82
+ accent: C.PURPLE,
83
+ hint: "Found in Arcana Packs and shops.",
84
+ };
85
+ export const PLANET_PICKER_CONFIG = {
86
+ title: "Planet Cards",
87
+ category: "planet",
88
+ clauseKey: "planetCard",
89
+ sheet: "Tarots",
90
+ items: PLANET_CARDS,
91
+ accent: C.BLUE,
92
+ hint: "Found in Celestial Packs and shops.",
93
+ };
94
+ export const SPECTRAL_PICKER_CONFIG = {
95
+ title: "Spectral Cards",
96
+ category: "spectral",
97
+ clauseKey: "spectralCard",
98
+ sheet: "Tarots",
99
+ items: SPECTRAL_CARDS,
100
+ accent: C.TEAL_GREY,
101
+ hint: "Found in Spectral Packs. Ghost Deck only for shop spawns!",
102
+ };
103
+ export const PACK_PICKER_CONFIG = {
104
+ title: "Booster Packs",
105
+ category: "pack",
106
+ clauseKey: "pack",
107
+ sheet: "Boosters",
108
+ items: BOOSTER_PACKS,
109
+ accent: C.ORANGE,
110
+ };
111
+ // ─── Styles ──────────────────────────────────────────────────────────────────
112
+ const styles = {
113
+ container: {
114
+ background: JimboColorOption.DARKEST,
115
+ border: `2px solid ${JimboColorOption.TEAL_GREY}`,
116
+ borderRadius: 8,
117
+ padding: 0,
118
+ maxWidth: 420,
119
+ maxHeight: "80vh",
120
+ overflow: "hidden",
121
+ display: "flex",
122
+ flexDirection: "column",
123
+ fontFamily: "m6x11plus, ui-monospace, monospace",
124
+ boxShadow: `0 8px 32px ${withAlpha(JimboColorOption.BLACK, 0.6)}`,
125
+ },
126
+ header: {
127
+ display: "flex",
128
+ alignItems: "center",
129
+ justifyContent: "space-between",
130
+ padding: "10px 12px",
131
+ borderBottom: `1px solid ${JimboColorOption.TEAL_GREY}`,
132
+ background: withAlpha(JimboColorOption.DARK_GREY, 0.5),
133
+ },
134
+ backBtn: {
135
+ background: "none",
136
+ border: "none",
137
+ color: JimboColorOption.GREY,
138
+ fontFamily: "m6x11plus, ui-monospace, monospace",
139
+ fontSize: 14,
140
+ cursor: "pointer",
141
+ padding: "4px 8px",
142
+ },
143
+ title: {
144
+ fontSize: 16,
145
+ fontWeight: "bold",
146
+ letterSpacing: 0.5,
147
+ },
148
+ searchRow: {
149
+ display: "flex",
150
+ gap: 8,
151
+ padding: "10px 12px 6px",
152
+ },
153
+ searchInput: {
154
+ flex: 1,
155
+ padding: "6px 10px",
156
+ borderRadius: 4,
157
+ border: `1px solid ${JimboColorOption.TEAL_GREY}`,
158
+ background: withAlpha(JimboColorOption.DARK_GREY, 0.8),
159
+ color: JimboColorOption.WHITE,
160
+ fontSize: 13,
161
+ fontFamily: "m6x11plus, ui-monospace, monospace",
162
+ outline: "none",
163
+ },
164
+ anyBtn: {
165
+ padding: "6px 14px",
166
+ borderRadius: 4,
167
+ border: `1px solid ${JimboColorOption.GOLD}`,
168
+ background: withAlpha(JimboColorOption.GOLD, 0.15),
169
+ color: JimboColorOption.GOLD,
170
+ fontSize: 13,
171
+ fontFamily: "m6x11plus, ui-monospace, monospace",
172
+ cursor: "pointer",
173
+ fontWeight: "bold",
174
+ },
175
+ hint: {
176
+ display: "flex",
177
+ alignItems: "flex-start",
178
+ gap: 6,
179
+ margin: "4px 12px 8px",
180
+ padding: "8px 10px",
181
+ borderRadius: 4,
182
+ background: withAlpha(JimboColorOption.TEAL_GREY, 0.12),
183
+ border: `1px solid ${withAlpha(JimboColorOption.TEAL_GREY, 0.3)}`,
184
+ color: JimboColorOption.GREY,
185
+ fontSize: 11,
186
+ lineHeight: "1.4",
187
+ },
188
+ grid: {
189
+ display: "grid",
190
+ gridTemplateColumns: "repeat(auto-fill, minmax(64px, 1fr))",
191
+ gap: 6,
192
+ padding: "8px 12px 12px",
193
+ overflowY: "auto",
194
+ flex: 1,
195
+ },
196
+ cell: {
197
+ display: "flex",
198
+ flexDirection: "column",
199
+ alignItems: "center",
200
+ gap: 3,
201
+ padding: 4,
202
+ borderRadius: 4,
203
+ cursor: "pointer",
204
+ transition: "background 120ms",
205
+ background: "transparent",
206
+ },
207
+ label: {
208
+ fontSize: 9,
209
+ color: JimboColorOption.GREY,
210
+ textAlign: "center",
211
+ lineHeight: "1.2",
212
+ maxWidth: 60,
213
+ overflow: "hidden",
214
+ textOverflow: "ellipsis",
215
+ whiteSpace: "nowrap",
216
+ },
217
+ emptyState: {
218
+ gridColumn: "1 / -1",
219
+ textAlign: "center",
220
+ color: JimboColorOption.GREY,
221
+ fontSize: 13,
222
+ padding: 20,
223
+ },
224
+ };
@@ -2,7 +2,7 @@ import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
2
2
  export interface JamlMapEditorDemoProps {
3
3
  /** Initial zone for the demo. */
4
4
  zone?: JamlZone;
5
- /** Callback when a selection changes, to update JAML text. */
5
+ /** Callback when selections change. */
6
6
  onChange?: (slots: (SlotSelection | null)[]) => void;
7
7
  }
8
8
  export declare function JamlMapEditorDemo({ zone: initialZone, onChange, }: JamlMapEditorDemoProps): import("react/jsx-runtime").JSX.Element;
@@ -1,18 +1,32 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback } from "react";
3
+ import { useState, useCallback, useMemo } from "react";
4
4
  import { MysterySlot } from "./MysterySlot.js";
5
5
  import { JokerPicker } from "./JokerPicker.js";
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";
6
7
  import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
7
- // ─── Component ───────────────────────────────────────────────────────────────
8
+ // ─── Category menu items ─────────────────────────────────────────────────────
8
9
  const C = JimboColorOption;
9
- const INITIAL_SLOTS = 8; // 8 shop item slots like in Balatro ante
10
+ const CATEGORIES = [
11
+ { key: "joker", label: "Joker", icon: "🃏", color: C.BLUE, hint: "Shop, Buffoon Pack" },
12
+ { key: "voucher", label: "Voucher", icon: "🎫", color: C.GOLD, hint: "1 per Ante in shop" },
13
+ { key: "tarot", label: "Tarot Card", icon: "🔮", color: C.PURPLE, hint: "Arcana Pack, shop" },
14
+ { key: "planet", label: "Planet Card", icon: "🪐", color: C.BLUE, hint: "Celestial Pack, shop" },
15
+ { key: "spectral", label: "Spectral Card", icon: "👻", color: C.TEAL_GREY, hint: "Ghost Deck, Spectral Pack" },
16
+ { key: "tag", label: "Tag", icon: "🏷️", color: C.GREEN, hint: "Skip blind reward" },
17
+ { key: "boss", label: "Boss Blind", icon: "👁️", color: C.RED, hint: "End of each Ante" },
18
+ { key: "pack", label: "Booster Pack", icon: "📦", color: C.ORANGE, hint: "Arcana, Celestial, etc." },
19
+ ];
20
+ // ─── Component ───────────────────────────────────────────────────────────────
21
+ const INITIAL_SLOTS = 8;
10
22
  export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
11
23
  const [zone, setZone] = useState(initialZone);
12
24
  const [slots, setSlots] = useState(Array(INITIAL_SLOTS).fill(null));
13
25
  const [activeSlot, setActiveSlot] = useState(null);
26
+ const [pickerFlow, setPickerFlow] = useState("category");
14
27
  const handleSlotTap = useCallback((index) => {
15
28
  setActiveSlot(index);
29
+ setPickerFlow("category");
16
30
  }, []);
17
31
  const handleSlotClear = useCallback((index) => {
18
32
  setSlots((prev) => {
@@ -22,7 +36,15 @@ export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
22
36
  return next;
23
37
  });
24
38
  }, [onChange]);
25
- const handleJokerSelect = useCallback((selection) => {
39
+ const handleCategorySelect = useCallback((cat) => {
40
+ if (cat === "joker") {
41
+ setPickerFlow("joker");
42
+ }
43
+ else {
44
+ setPickerFlow(cat);
45
+ }
46
+ }, []);
47
+ const handleItemSelect = useCallback((selection) => {
26
48
  if (activeSlot === null)
27
49
  return;
28
50
  setSlots((prev) => {
@@ -34,23 +56,64 @@ export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
34
56
  setActiveSlot(null);
35
57
  }, [activeSlot, onChange]);
36
58
  const handlePickerCancel = useCallback(() => {
59
+ if (pickerFlow !== "category") {
60
+ // Go back to category selection
61
+ setPickerFlow("category");
62
+ }
63
+ else {
64
+ setActiveSlot(null);
65
+ }
66
+ }, [pickerFlow]);
67
+ const handleOverlayClose = useCallback(() => {
37
68
  setActiveSlot(null);
38
69
  }, []);
39
70
  const filledCount = slots.filter(Boolean).length;
71
+ // Build the JSON tree from slots
72
+ const jsonTree = useMemo(() => buildJsonTree(zone, slots), [zone, slots]);
40
73
  return (_jsxs("div", { style: styles.wrapper, children: [_jsx("div", { style: styles.zoneBar, children: ["must", "should", "mustnot"].map((z) => (_jsx("button", { onClick: () => setZone(z), style: {
41
74
  ...styles.zoneBtn,
42
75
  borderColor: zone === z ? ZONE_COLORS[z] : C.TEAL_GREY,
43
76
  color: zone === z ? ZONE_COLORS[z] : C.GREY,
44
77
  background: zone === z ? withAlpha(ZONE_COLORS[z], 0.1) : "transparent",
45
- }, children: z.toUpperCase() }, z))) }), _jsxs("div", { style: styles.sectionLabel, children: [_jsx("span", { style: { color: ZONE_COLORS[zone] }, children: "Shop Items" }), _jsxs("span", { style: { color: C.GREY, fontSize: 11 }, children: [filledCount, "/", INITIAL_SLOTS, " defined"] })] }), _jsx("div", { style: styles.slotRow, children: slots.map((selection, i) => (_jsx(MysterySlot, { zone: zone, sheetType: "Jokers", selection: selection ?? undefined, width: 48, onTap: () => handleSlotTap(i), onClear: selection ? () => handleSlotClear(i) : undefined }, i))) }), _jsx("div", { style: styles.scrollHint, children: "\u25C0 swipe \u25B6" }), activeSlot !== null && (_jsxs("div", { style: styles.overlay, children: [_jsx("div", { style: styles.overlayBackdrop, onClick: handlePickerCancel }), _jsx("div", { style: styles.pickerWrapper, children: _jsx(JokerPicker, { onSelect: handleJokerSelect, onCancel: handlePickerCancel }) })] })), filledCount > 0 && (_jsxs("div", { style: styles.jamlPreview, children: [_jsx("div", { style: styles.jamlHeader, children: "Generated JAML" }), _jsx("pre", { style: styles.jamlCode, children: generateJamlSnippet(zone, slots) })] }))] }));
78
+ }, children: z.toUpperCase() }, z))) }), _jsxs("div", { style: styles.sectionLabel, children: [_jsx("span", { style: { color: ZONE_COLORS[zone] }, children: "Shop Items" }), _jsxs("span", { style: { color: C.GREY, fontSize: 11 }, children: [filledCount, "/", INITIAL_SLOTS, " defined"] })] }), _jsx("div", { style: styles.slotRow, children: slots.map((selection, i) => (_jsx(MysterySlot, { zone: zone, sheetType: "Jokers", selection: selection ?? undefined, width: 48, onTap: () => handleSlotTap(i), onClear: selection ? () => handleSlotClear(i) : undefined }, i))) }), _jsx("div", { style: styles.scrollHint, children: "\u25C0 swipe \u25B6" }), activeSlot !== null && (_jsxs("div", { style: styles.overlay, children: [_jsx("div", { style: styles.overlayBackdrop, onClick: handleOverlayClose }), _jsx("div", { style: styles.pickerWrapper, children: pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: handleCategorySelect, onCancel: handleOverlayClose })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handleItemSelect, onCancel: handlePickerCancel })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handleItemSelect, onCancel: handlePickerCancel })) })] })), filledCount > 0 && (_jsxs("div", { style: styles.jsonPreview, children: [_jsxs("div", { style: styles.jsonHeader, children: [_jsx("span", { children: "Filter Preview" }), _jsx("span", { style: { fontSize: 10, opacity: 0.6 }, children: "JSON" })] }), _jsx("div", { style: styles.jsonBody, children: _jsx(JsonTreeNode, { data: jsonTree, indent: 0 }) })] }))] }));
79
+ }
80
+ // ─── Category Selection Menu ─────────────────────────────────────────────────
81
+ function CategoryMenu({ onSelect, onCancel, }) {
82
+ return (_jsxs("div", { style: styles.catMenuContainer, children: [_jsxs("div", { style: styles.catMenuHeader, children: [_jsx("button", { onClick: onCancel, style: styles.backBtn, children: "\u2715" }), _jsx("span", { style: styles.catMenuTitle, children: "Select Category" }), _jsx("div", { style: { width: 44 } })] }), _jsx("div", { style: styles.catGrid, children: CATEGORIES.map((cat) => (_jsxs("button", { onClick: () => onSelect(cat.key), style: {
83
+ ...styles.catBtn,
84
+ borderColor: withAlpha(cat.color, 0.3),
85
+ }, children: [_jsx("span", { style: styles.catIcon, children: cat.icon }), _jsxs("div", { style: styles.catText, children: [_jsx("span", { style: { ...styles.catLabel, color: cat.color }, children: cat.label }), _jsx("span", { style: styles.catHint, children: cat.hint })] })] }, cat.key))) })] }));
46
86
  }
47
- // ─── JAML generation ─────────────────────────────────────────────────────────
48
- function generateJamlSnippet(zone, slots) {
87
+ // ─── JSON tree renderer ──────────────────────────────────────────────────────
88
+ function JsonTreeNode({ data, indent }) {
89
+ if (data === null || data === undefined) {
90
+ return _jsx("span", { style: { color: C.GREY }, children: "null" });
91
+ }
92
+ if (typeof data === "string") {
93
+ return _jsxs("span", { style: { color: C.GREEN_TEXT }, children: ["\"", data, "\""] });
94
+ }
95
+ if (typeof data === "number" || typeof data === "boolean") {
96
+ return _jsx("span", { style: { color: C.GOLD }, children: String(data) });
97
+ }
98
+ if (Array.isArray(data)) {
99
+ if (data.length === 0)
100
+ return _jsx("span", { style: { color: C.GREY }, children: "[]" });
101
+ return (_jsxs("span", { children: [_jsx("span", { style: { color: C.GREY }, children: "[" }), data.map((item, i) => (_jsxs("div", { style: { paddingLeft: (indent + 1) * 14 }, children: [_jsx(JsonTreeNode, { data: item, indent: indent + 1 }), i < data.length - 1 && _jsx("span", { style: { color: C.GREY }, children: "," })] }, i))), _jsx("div", { style: { paddingLeft: indent * 14 }, children: _jsx("span", { style: { color: C.GREY }, children: "]" }) })] }));
102
+ }
103
+ if (typeof data === "object") {
104
+ const entries = Object.entries(data);
105
+ if (entries.length === 0)
106
+ return _jsx("span", { style: { color: C.GREY }, children: "{}" });
107
+ return (_jsxs("span", { children: [_jsx("span", { style: { color: C.GREY }, children: "{" }), entries.map(([key, val], i) => (_jsxs("div", { style: { paddingLeft: (indent + 1) * 14 }, children: [_jsx("span", { style: { color: C.BLUE }, children: key }), _jsx("span", { style: { color: C.GREY }, children: ": " }), _jsx(JsonTreeNode, { data: val, indent: indent + 1 }), i < entries.length - 1 && _jsx("span", { style: { color: C.GREY }, children: "," })] }, key))), _jsx("div", { style: { paddingLeft: indent * 14 }, children: _jsx("span", { style: { color: C.GREY }, children: "}" }) })] }));
108
+ }
109
+ return _jsx("span", { children: String(data) });
110
+ }
111
+ // ─── Build JSON tree from slots ──────────────────────────────────────────────
112
+ function buildJsonTree(zone, slots) {
49
113
  const filled = slots.filter(Boolean);
50
114
  if (filled.length === 0)
51
- return "# empty";
115
+ return {};
52
116
  const jamlZone = zone === "mustnot" ? "mustNot" : zone;
53
- const lines = [`${jamlZone}:`];
54
117
  // Group by clauseKey
55
118
  const groups = new Map();
56
119
  for (const s of filled) {
@@ -58,22 +121,35 @@ function generateJamlSnippet(zone, slots) {
58
121
  existing.push(s.value);
59
122
  groups.set(s.clauseKey, existing);
60
123
  }
124
+ const clauses = [];
61
125
  for (const [key, values] of groups) {
62
126
  if (values.length === 1) {
63
- lines.push(` - ${key}: ${values[0]}`);
127
+ clauses.push({ [key]: values[0] });
64
128
  }
65
129
  else {
66
- lines.push(` - ${key}: [${values.join(", ")}]`);
130
+ clauses.push({ [key]: values });
67
131
  }
68
132
  }
69
- return lines.join("\n");
133
+ return { [jamlZone]: clauses };
70
134
  }
71
- // ─── Constants & styles ──────────────────────────────────────────────────────
135
+ // ─── Category picker config mapping ────────────────────────────────────────
136
+ const CATEGORY_CONFIG_MAP = {
137
+ joker: VOUCHER_PICKER_CONFIG, // Not used — routed to JokerPicker
138
+ voucher: VOUCHER_PICKER_CONFIG,
139
+ tag: TAG_PICKER_CONFIG,
140
+ boss: BOSS_PICKER_CONFIG,
141
+ tarot: TAROT_PICKER_CONFIG,
142
+ planet: PLANET_PICKER_CONFIG,
143
+ spectral: SPECTRAL_PICKER_CONFIG,
144
+ pack: PACK_PICKER_CONFIG,
145
+ };
146
+ // ─── Zone colors ─────────────────────────────────────────────────────────────
72
147
  const ZONE_COLORS = {
73
148
  must: C.BLUE,
74
149
  should: C.RED,
75
150
  mustnot: C.ORANGE,
76
151
  };
152
+ // ─── Styles ──────────────────────────────────────────────────────────────────
77
153
  const styles = {
78
154
  wrapper: {
79
155
  position: "relative",
@@ -141,15 +217,90 @@ const styles = {
141
217
  position: "relative",
142
218
  zIndex: 1,
143
219
  width: "90%",
144
- maxWidth: 400,
220
+ maxWidth: 420,
221
+ },
222
+ // Category menu
223
+ catMenuContainer: {
224
+ background: C.DARKEST,
225
+ border: `2px solid ${C.TEAL_GREY}`,
226
+ borderRadius: 8,
227
+ overflow: "hidden",
228
+ fontFamily: "m6x11plus, ui-monospace, monospace",
229
+ boxShadow: `0 8px 32px ${withAlpha(C.BLACK, 0.6)}`,
145
230
  },
146
- jamlPreview: {
231
+ catMenuHeader: {
232
+ display: "flex",
233
+ alignItems: "center",
234
+ justifyContent: "space-between",
235
+ padding: "10px 12px",
236
+ borderBottom: `1px solid ${C.TEAL_GREY}`,
237
+ background: withAlpha(C.DARK_GREY, 0.5),
238
+ },
239
+ catMenuTitle: {
240
+ color: C.WHITE,
241
+ fontSize: 16,
242
+ fontWeight: "bold",
243
+ letterSpacing: 0.5,
244
+ },
245
+ backBtn: {
246
+ background: "none",
247
+ border: "none",
248
+ color: C.GREY,
249
+ fontFamily: "m6x11plus, ui-monospace, monospace",
250
+ fontSize: 14,
251
+ cursor: "pointer",
252
+ padding: "4px 8px",
253
+ },
254
+ catGrid: {
255
+ display: "grid",
256
+ gridTemplateColumns: "1fr 1fr",
257
+ gap: 6,
258
+ padding: 10,
259
+ maxHeight: "70vh",
260
+ overflowY: "auto",
261
+ },
262
+ catBtn: {
263
+ display: "flex",
264
+ alignItems: "center",
265
+ gap: 8,
266
+ padding: "10px 10px",
267
+ border: "1px solid",
268
+ borderRadius: 6,
269
+ cursor: "pointer",
270
+ background: withAlpha(C.DARK_GREY, 0.3),
271
+ textAlign: "left",
272
+ transition: "background 150ms, transform 100ms",
273
+ fontFamily: "m6x11plus, ui-monospace, monospace",
274
+ },
275
+ catIcon: {
276
+ fontSize: 20,
277
+ flexShrink: 0,
278
+ },
279
+ catText: {
280
+ display: "flex",
281
+ flexDirection: "column",
282
+ gap: 2,
283
+ minWidth: 0,
284
+ },
285
+ catLabel: {
286
+ fontSize: 13,
287
+ fontWeight: "bold",
288
+ fontFamily: "m6x11plus, ui-monospace, monospace",
289
+ },
290
+ catHint: {
291
+ fontSize: 9,
292
+ color: C.GREY,
293
+ fontFamily: "m6x11plus, ui-monospace, monospace",
294
+ lineHeight: "1.2",
295
+ },
296
+ // JSON preview
297
+ jsonPreview: {
147
298
  marginTop: 12,
148
299
  borderRadius: 4,
149
300
  border: `1px solid ${C.TEAL_GREY}`,
150
301
  overflow: "hidden",
151
302
  },
152
- jamlHeader: {
303
+ jsonHeader: {
153
304
  padding: "6px 10px",
154
305
  fontSize: 11,
155
306
  color: C.GREY,
@@ -157,14 +308,16 @@ const styles = {
157
308
  borderBottom: `1px solid ${C.TEAL_GREY}`,
158
309
  letterSpacing: 1,
159
310
  textTransform: "uppercase",
311
+ display: "flex",
312
+ justifyContent: "space-between",
313
+ alignItems: "center",
160
314
  },
161
- jamlCode: {
162
- margin: 0,
315
+ jsonBody: {
163
316
  padding: "8px 10px",
164
317
  fontSize: 12,
165
- color: C.GREEN_TEXT,
166
318
  background: withAlpha(C.DARKEST, 0.8),
167
- lineHeight: "1.5",
168
- whiteSpace: "pre-wrap",
319
+ lineHeight: "1.6",
320
+ fontFamily: "m6x11plus, ui-monospace, monospace",
321
+ overflowX: "auto",
169
322
  },
170
323
  };
@@ -1,3 +1,4 @@
1
1
  export { MysterySlot, type MysterySlotProps, type SlotSelection, type SlotCategory, type JamlZone } from "./MysterySlot.js";
2
2
  export { JokerPicker, type JokerPickerProps, type JokerRarity } from "./JokerPicker.js";
3
3
  export { JamlMapEditorDemo, type JamlMapEditorDemoProps } from "./JamlMapEditorDemo.js";
4
+ export { CategoryPicker, type CategoryPickerConfig, type CategoryPickerProps, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./CategoryPicker.js";
@@ -1,3 +1,4 @@
1
1
  export { MysterySlot } from "./MysterySlot.js";
2
2
  export { JokerPicker } from "./JokerPicker.js";
3
3
  export { JamlMapEditorDemo } from "./JamlMapEditorDemo.js";
4
+ export { 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";
@@ -26,7 +26,7 @@ export interface AnalyzerLive {
26
26
  deck: string;
27
27
  stake: string;
28
28
  }
29
- export declare function useAnalyzer(motelyRuntime: MotelyRuntime): {
29
+ export declare function useAnalyzer(motelyRuntime: MotelyRuntime | null): {
30
30
  antes: AnalyzerAnteView[];
31
31
  status: AnalyzerStatus;
32
32
  error: string | null;
@@ -17,6 +17,9 @@ export function useAnalyzer(motelyRuntime) {
17
17
  setStatus("running");
18
18
  setError(null);
19
19
  try {
20
+ if (!motelyRuntime) {
21
+ throw new Error("motely-wasm runtime is still booting.");
22
+ }
20
23
  const { MotelyWasm, Motely } = motelyRuntime;
21
24
  const deckEnum = Motely.MotelyDeck[deck] ?? Motely.MotelyDeck.Red;
22
25
  const stakeEnum = Motely.MotelyStake[stake] ?? Motely.MotelyStake.White;
package/dist/index.d.ts CHANGED
@@ -24,4 +24,4 @@ export { useSearch, type SearchResult, type SearchStatus, type UseSearchState, }
24
24
  export { useAnalyzer, type AnalyzerStatus, type AnalyzerLive, type MotelyRuntime, type MotelyJsRunState, } from "./hooks/useAnalyzer.js";
25
25
  export { JamlAestheticSelector, type JamlAestheticSelectorProps, type JamlAestheticOption, } from "./components/JamlAestheticSelector.js";
26
26
  export { JamlSeedInput, type JamlSeedInputProps, } from "./components/JamlSeedInput.js";
27
- export { JamlMapEditorDemo, JokerPicker, MysterySlot, type JamlMapEditorDemoProps, type JokerPickerProps, type JokerRarity, type MysterySlotProps, type SlotCategory, type SlotSelection, } from "./components/jamlMap/index.js";
27
+ export { JamlMapEditorDemo, JokerPicker, MysterySlot, CategoryPicker, type JamlMapEditorDemoProps, type JokerPickerProps, type JokerRarity, type MysterySlotProps, type SlotCategory, type SlotSelection, type CategoryPickerConfig, type CategoryPickerProps, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";
package/dist/index.js CHANGED
@@ -25,4 +25,4 @@ export { useSearch, } from "./hooks/useSearch.js";
25
25
  export { useAnalyzer, } from "./hooks/useAnalyzer.js";
26
26
  export { JamlAestheticSelector, } from "./components/JamlAestheticSelector.js";
27
27
  export { JamlSeedInput, } from "./components/JamlSeedInput.js";
28
- export { JamlMapEditorDemo, JokerPicker, MysterySlot, } from "./components/jamlMap/index.js";
28
+ export { JamlMapEditorDemo, JokerPicker, MysterySlot, CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";