jaml-ui 0.17.2 → 0.18.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.
Files changed (46) hide show
  1. package/DESIGN.md +7 -3
  2. package/dist/components/JamlAnalyzerFullscreen.d.ts +4 -1
  3. package/dist/components/JamlAnalyzerFullscreen.js +2 -2
  4. package/dist/components/JamlCurator.d.ts +4 -0
  5. package/dist/components/JamlCurator.js +63 -0
  6. package/dist/components/JamlCurator.stories.d.ts +6 -0
  7. package/dist/components/JamlCurator.stories.js +14 -0
  8. package/dist/components/JamlIde.js +1 -1
  9. package/dist/components/JamlIdeVisual.js +12 -20
  10. package/dist/components/jamlMap/CategoryPicker.d.ts +32 -0
  11. package/dist/components/jamlMap/CategoryPicker.js +142 -0
  12. package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
  13. package/dist/components/jamlMap/JamlMapEditor.js +170 -0
  14. package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
  15. package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
  16. package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +1 -1
  17. package/dist/components/jamlMap/JamlMapEditorDemo.js +174 -21
  18. package/dist/components/jamlMap/JokerPicker.js +28 -157
  19. package/dist/components/jamlMap/MysterySlot.js +32 -5
  20. package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
  21. package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
  22. package/dist/components/jamlMap/index.d.ts +2 -1
  23. package/dist/components/jamlMap/index.js +2 -1
  24. package/dist/hooks/useAnalyzer.d.ts +4 -8
  25. package/dist/hooks/useAnalyzer.js +3 -3
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/stories/Button.d.ts +15 -0
  29. package/dist/stories/Button.js +7 -0
  30. package/dist/stories/Button.stories.d.ts +24 -0
  31. package/dist/stories/Button.stories.js +50 -0
  32. package/dist/stories/Header.d.ts +12 -0
  33. package/dist/stories/Header.js +4 -0
  34. package/dist/stories/Header.stories.d.ts +18 -0
  35. package/dist/stories/Header.stories.js +26 -0
  36. package/dist/stories/Page.d.ts +3 -0
  37. package/dist/stories/Page.js +8 -0
  38. package/dist/stories/Page.stories.d.ts +12 -0
  39. package/dist/stories/Page.stories.js +24 -0
  40. package/dist/ui/Jimbo.stories.d.ts +7 -0
  41. package/dist/ui/Jimbo.stories.js +28 -0
  42. package/dist/ui/jimbo.css +20 -11
  43. package/dist/ui/jimboText.d.ts +1 -1
  44. package/dist/ui/panel.d.ts +1 -1
  45. package/dist/ui/panel.js +7 -5
  46. package/package.json +16 -3
package/DESIGN.md CHANGED
@@ -147,9 +147,11 @@ Must-clause items glow blue. Should-clause items glow gold/green. Non-matching i
147
147
 
148
148
  ## Typography
149
149
 
150
- m6x11plus (m6x11plusplus.otf) is the ONLY font. It is a single-weight pixel font. NEVER apply font-weight bold, semibold, or any weight other than 400. Bold makes it look muddy. Use size and letter-spacing for hierarchy instead.
150
+ m6x11plus (m6x11plusplus.otf) is the ONLY font. It is a single-weight pixel font. NEVER apply font-weight bold, semibold, or any weight other than 400. Bold makes it look muddy. NEVER USE HEAVY!
151
151
 
152
- All text is uppercase with generous letter-spacing (0.04em-0.1em) for labels and micro text. Seed codes use the display size (26px) in gold (#e4b643) with 0.04em tracking.
152
+ Text is NEVER ALL CAPS. Use proper Title Case or Sentence case. Seed codes use the display size (26px) in gold (#e4b643).
153
+
154
+ Contrast is critical. NEVER make grey text on top of a grey background. If using a dark grey background, use white or light contrasting text.
153
155
 
154
156
  ## Layout
155
157
 
@@ -188,7 +190,9 @@ JAML-hit items get a GlowRing: `box-shadow: 0 0 0 2px [color], 0 0 10px [color]`
188
190
  - DO use m6x11plus for everything except code/monospace.
189
191
  - DO design for 375px portrait.
190
192
  - DO use translateY + box-shadow for button depth. Not CSS 3D transforms.
191
- - DON'T use font-weight bold. m6x11plus is single-weight. Bold = muddy.
193
+ - DON'T use font-weight bold or heavy. m6x11plus is single-weight.
194
+ - DON'T use ALL CAPS. It is considered an embellishment and ruins the aesthetic.
195
+ - DON'T put grey text on top of a grey background.
192
196
  - DON'T use fat padding or margins. Balatro UI is dense and cozy.
193
197
  - DON'T add horizontal scroll. Vertical snap-scroll + horizontal swipe only.
194
198
  - DON'T use rounded corners larger than 10px. Balatro is chunky, not bubbly.
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import type { AnalyzerAnteView, AnalyzerItem } from "./AnalyzerExplorer.js";
2
3
  import type { AnalyzerLive } from "../hooks/useAnalyzer.js";
3
4
  import { type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
@@ -21,7 +22,9 @@ export interface JamlAnalyzerFullscreenProps {
21
22
  /** Pull size on each lazy load. */
22
23
  chunkSize?: number;
23
24
  className?: string;
25
+ /** Custom top page to render as Slide 0 */
26
+ topPage?: React.ReactNode;
24
27
  }
25
- export declare function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, topPage, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
26
29
  export type { AnalyzerItem };
27
30
  export { ANALYZER_STREAM_META, type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
@@ -17,7 +17,7 @@ const TONE_COLORS = {
17
17
  default: C.GOLD_TEXT,
18
18
  };
19
19
  import { JamlMapPreview } from "./JamlMapPreview.js";
20
- export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", }) {
20
+ export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", topPage, }) {
21
21
  const [internalEnabled, setInternalEnabled] = useState(enabledStreams ?? DEFAULT_ENABLED_STREAMS);
22
22
  const effectiveEnabled = enabledStreams ?? internalEnabled;
23
23
  const setEnabled = useCallback((next) => {
@@ -26,7 +26,7 @@ export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyL
26
26
  }, [onEnabledStreamsChange]);
27
27
  const { currentAnte, scrollRef, scrollToAnte, registerAnteRef } = useAnteTracker(antes);
28
28
  const [pickerOpen, setPickerOpen] = useState(false);
29
- return (_jsxs("div", { className: className, style: styles.root, children: [_jsxs("div", { ref: scrollRef, style: styles.scroller, children: [jaml && (_jsxs("section", { style: { ...styles.section, scrollSnapAlign: "start", justifyContent: 'center' }, children: [_jsxs("div", { style: { marginBottom: 20 }, children: [_jsx("div", { style: styles.anteLabel, children: "JAML" }), _jsx("div", { style: styles.anteNumber, children: "MAP" })] }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns, tallyLabels: tallyLabels }), _jsx("div", { style: { marginTop: 24, textAlign: 'center', opacity: 0.6 }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Scroll down to explore seed details" }) })] })), antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => registerAnteRef(ante.ante, el) }, ante.ante)))] }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
29
+ return (_jsxs("div", { className: className, style: styles.root, children: [_jsxs("div", { ref: scrollRef, style: styles.scroller, children: [topPage ? topPage : jaml && (_jsxs("section", { style: { ...styles.section, scrollSnapAlign: "start", justifyContent: 'center' }, children: [_jsxs("div", { style: { marginBottom: 20 }, children: [_jsx("div", { style: styles.anteLabel, children: "JAML" }), _jsx("div", { style: styles.anteNumber, children: "MAP" })] }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns, tallyLabels: tallyLabels }), _jsx("div", { style: { marginTop: 24, textAlign: 'center', opacity: 0.6 }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Scroll down to explore seed details" }) })] })), antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => registerAnteRef(ante.ante, el) }, ante.ante)))] }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
30
30
  }
31
31
  function AnteSection({ ante, live, enabledStreams, chunkSize, registerRef }) {
32
32
  return (_jsxs("section", { ref: registerRef, "data-ante": ante.ante, style: styles.section, children: [_jsxs("header", { style: styles.header, children: [_jsxs("div", { children: [_jsx("div", { style: styles.anteLabel, children: "Ante" }), _jsx("div", { style: styles.anteNumber, children: ante.ante })] }), ante.voucher && (_jsxs("div", { style: styles.voucherBlock, children: [_jsx(JamlVoucher, { voucherName: ante.voucher, scale: 0.85 }), _jsx("div", { style: styles.voucherCaption, children: ante.voucher })] }))] }), _jsxs("div", { style: styles.blindRow, children: [_jsx(BlindCell, { label: "Small", tag: ante.smallBlindTag }), _jsx(BlindCell, { label: "Big", tag: ante.bigBlindTag }), ante.boss && (_jsxs("div", { style: styles.bossCell, children: [_jsx("div", { style: styles.cellLabel, children: "Boss" }), _jsx(JamlBoss, { bossName: ante.boss, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: ante.boss })] }))] }), ante.packs && ante.packs.length > 0 && (_jsxs("div", { style: styles.streamLane, children: [_jsx("div", { style: styles.streamLabel, children: "Packs" }), _jsx("div", { style: styles.packRow, children: ante.packs.map((pack, i) => (_jsx("div", { style: styles.packPill, children: pack }, `${ante.ante}-pack-${i}`))) })] })), enabledStreams.map((key) => {
@@ -0,0 +1,4 @@
1
+ export interface JamlCuratorProps {
2
+ motelyWasmUrl: string;
3
+ }
4
+ export declare function JamlCurator({ motelyWasmUrl }: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { JimboButton, JimboPanel } from "../ui/panel.js";
5
+ import { JimboText } from "../ui/jimboText.js";
6
+ import { JimboColorOption } from "../ui/tokens.js";
7
+ import { JimboFlankNav } from "../ui/jimboFlankNav.js";
8
+ import { JamlMapEditor } from "./jamlMap/JamlMapEditor.js";
9
+ import { JamlAnalyzerFullscreen } from "./JamlAnalyzerFullscreen.js";
10
+ import { useSearch } from "../hooks/useSearch.js";
11
+ import { useAnalyzer } from "../hooks/useAnalyzer.js";
12
+ import { JamlSpeedometer } from "./JamlSpeedometer.js";
13
+ const C = JimboColorOption;
14
+ export function JamlCurator({ motelyWasmUrl }) {
15
+ // Use map editor by default to generate JAML
16
+ const [jamlText, setJamlText] = useState("");
17
+ const search = useSearch(motelyWasmUrl);
18
+ const analyzer = useAnalyzer();
19
+ // Search results pagination
20
+ const [resultIndex, setResultIndex] = useState(0);
21
+ const isSearching = search.status === "running";
22
+ const handleSearch = () => {
23
+ if (isSearching) {
24
+ search.cancel();
25
+ }
26
+ else {
27
+ setResultIndex(0);
28
+ search.start(jamlText, 1_000_000);
29
+ }
30
+ };
31
+ const currentSeed = search.results[resultIndex]?.seed;
32
+ useEffect(() => {
33
+ if (currentSeed) {
34
+ analyzer.analyze(currentSeed, "Red", "White", jamlText);
35
+ }
36
+ }, [currentSeed, jamlText]); // eslint-disable-line react-hooks/exhaustive-deps
37
+ // Map Editor changes
38
+ const handleMapChange = (jamlString) => {
39
+ setJamlText(jamlString);
40
+ };
41
+ return (_jsx("div", { style: {
42
+ width: "100%",
43
+ maxWidth: 375,
44
+ height: "100svh",
45
+ margin: "0 auto",
46
+ position: "relative",
47
+ background: C.DARKEST,
48
+ overflow: "hidden",
49
+ borderLeft: `1px solid ${C.PANEL_EDGE}`,
50
+ borderRight: `1px solid ${C.PANEL_EDGE}`,
51
+ boxShadow: `0 0 20px rgba(0,0,0,0.5)`,
52
+ }, children: _jsx(JamlAnalyzerFullscreen, { antes: analyzer.antes, live: analyzer.live, hidePicker: true, topPage: _jsxs("section", { style: {
53
+ width: "100%",
54
+ height: "100svh",
55
+ scrollSnapAlign: "start",
56
+ display: "flex",
57
+ flexDirection: "column",
58
+ gap: 12,
59
+ padding: "16px 12px 24px",
60
+ boxSizing: "border-box",
61
+ borderBottom: `2px solid ${C.GOLD}`,
62
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(JimboText, { size: "lg", tone: "gold", children: "JAML Curator" }), _jsx(JimboButton, { tone: isSearching ? "red" : "green", size: "sm", onClick: handleSearch, children: isSearching ? "STOP" : "SEARCH" })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto' }, className: "hide-scrollbar", children: _jsx(JamlMapEditor, { onChange: handleMapChange }) }), _jsx("div", { style: { flexShrink: 0 }, children: _jsx(JamlSpeedometer, { status: search.status, seedsPerSecond: search.seedsPerSecond, totalSearched: search.totalSearched, matchingSeeds: search.matchingSeeds }) }), _jsx("div", { style: { flexShrink: 0 }, children: _jsx(JimboPanel, { children: search.results.length === 0 ? (_jsx(JimboText, { size: "sm", tone: "grey", className: "j-text-center", children: isSearching ? "Searching..." : "No results yet." })) : (_jsxs("div", { className: "j-flex-col j-gap-sm", children: [_jsxs("div", { className: "j-flex j-items-center j-justify-between", children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "SEED MATCHES" }), _jsxs(JimboText, { size: "xs", tone: "gold", children: [search.matchingSeeds, " FOUND"] })] }), _jsx(JimboFlankNav, { canPrev: resultIndex > 0, canNext: resultIndex < search.results.length - 1, onPrev: () => setResultIndex(i => Math.max(0, i - 1)), onNext: () => setResultIndex(i => Math.min(search.results.length - 1, i + 1)), children: _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "lg", tone: "gold", style: { letterSpacing: 2 }, children: currentSeed }), _jsx(JimboButton, { tone: "grey", size: "xs", children: "Copy Seed" })] }) }), _jsx(JimboText, { size: "micro", tone: "grey", className: "j-text-center", style: { opacity: 0.7, marginTop: 8 }, children: "\u25BC SWIPE DOWN FOR ANTES \u25BC" })] })) }) })] }) }) }));
63
+ }
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { JamlCurator } from "./JamlCurator.js";
3
+ declare const meta: Meta<typeof JamlCurator>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof JamlCurator>;
6
+ export declare const Default: Story;
@@ -0,0 +1,14 @@
1
+ import { JamlCurator } from "./JamlCurator.js";
2
+ const meta = {
3
+ title: "JAML/JamlCurator",
4
+ component: JamlCurator,
5
+ parameters: {
6
+ layout: "fullscreen",
7
+ },
8
+ };
9
+ export default meta;
10
+ export const Default = {
11
+ args: {
12
+ motelyWasmUrl: "https://unpkg.com/@nims11/motely@0.2.2/motely_bg.wasm",
13
+ },
14
+ };
@@ -69,7 +69,7 @@ function ResultsView({ results, jaml }) {
69
69
  display: "flex",
70
70
  flexDirection: "column",
71
71
  gap: 8,
72
- }, children: [_jsx(JamlMapPreview, { jaml: jaml, tallyColumns: result.tallyColumns, tallyLabels: result.tallyLabels }), _jsxs("div", { style: { padding: "4px 8px 8px", display: "flex", flexDirection: "column", gap: 5 }, children: [_jsx("span", { style: { fontSize: 8, color: JimboColorOption.GREY, letterSpacing: "0.08em", textTransform: "uppercase" }, children: "RAW TALLY DATA" }), (result.tallyLabels ?? []).map((label, i) => {
72
+ }, children: [_jsx(JamlMapPreview, { jaml: jaml, tallyColumns: result.tallyColumns, tallyLabels: result.tallyLabels }), _jsxs("div", { style: { padding: "4px 8px 8px", display: "flex", flexDirection: "column", gap: 5 }, children: [_jsx("span", { style: { fontSize: 9, color: JimboColorOption.WHITE, opacity: 0.8 }, children: "Raw Tally Data" }), (result.tallyLabels ?? []).map((label, i) => {
73
73
  const val = result.tallyColumns[i] ?? 0;
74
74
  if (val === 0)
75
75
  return null;
@@ -6,9 +6,9 @@ import { JimboColorOption } from "../ui/tokens.js";
6
6
  import { JimboSprite } from "../ui/sprites.js";
7
7
  const C = JimboColorOption;
8
8
  const ZONE_META = {
9
- must: { label: "MUST", hint: "Seed must contain all of these.", color: C.BLUE, accent: "#4db5ff" },
10
- should: { label: "SHOULD", hint: "Bonus points per match.", color: C.RED, accent: "#ff8076" },
11
- mustnot: { label: "MUST NOT", hint: "Seed is rejected if any appear.", color: C.ORANGE, accent: "#ffb84d" },
9
+ must: { label: "Must", hint: "Seed must contain all of these.", color: C.BLUE, accent: "#4db5ff" },
10
+ should: { label: "Should", hint: "Bonus points per match.", color: C.RED, accent: "#ff8076" },
11
+ mustnot: { label: "Must Not", hint: "Rejected if any appear.", color: C.ORANGE, accent: "#ffb84d" },
12
12
  };
13
13
  function clauseSpriteSheet(type) {
14
14
  if (type === "joker" ||
@@ -127,8 +127,7 @@ function MysteryAddTile({ zone, onTap }) {
127
127
  fontFamily: "m6x11plus, ui-monospace, monospace",
128
128
  fontSize: 12,
129
129
  color: z.accent,
130
- letterSpacing: 2,
131
- }, children: ["ADD TO ", z.label] })] }));
130
+ }, children: ["Add to ", z.label] })] }));
132
131
  }
133
132
  function ZoneRail({ zone, clauses, onAdd, onRemove, onEdit, onDragStart, highlight, }) {
134
133
  const z = ZONE_META[zone];
@@ -140,23 +139,16 @@ function ZoneRail({ zone, clauses, onAdd, onRemove, onEdit, onDragStart, highlig
140
139
  transition: "background 100ms, border-color 100ms",
141
140
  }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }, children: [_jsx("div", { style: {
142
141
  fontFamily: "m6x11plus, ui-monospace, monospace",
143
- fontSize: 11,
144
- padding: "2px 8px",
142
+ fontSize: 12,
143
+ padding: "2px 6px",
145
144
  background: z.color,
146
145
  color: C.WHITE,
147
146
  borderRadius: 3,
148
- letterSpacing: 2,
149
147
  boxShadow: `0 2px 0 ${C.BLACK}`,
150
148
  }, children: z.label }), _jsx("div", { style: { flex: 1, height: 2, background: `${z.color}55`, borderRadius: 1 } }), _jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 8, color: C.GREY }, children: clauses.length })] }), _jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 9, color: C.GREY, letterSpacing: 0.5, marginBottom: 8 }, children: z.hint }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [clauses.map((c) => (_jsx(ClauseCard, { clause: c, zone: zone, onRemove: () => onRemove(c.id), onEdit: () => onEdit(c), onDragStart: onDragStart }, c.id))), _jsx(MysteryAddTile, { zone: zone, onTap: onAdd })] })] }));
151
149
  }
152
150
  function TopMatter({ filter, onChange, }) {
153
- return (_jsxs("div", { style: {
154
- background: C.DARK_GREY,
155
- borderRadius: 6,
156
- padding: 10,
157
- border: `2px solid ${C.PANEL_EDGE}`,
158
- boxShadow: `0 2px 0 ${C.BLACK}`,
159
- }, children: [_jsx("input", { value: filter.name ?? "", placeholder: "Untitled", onChange: (e) => onChange({ ...filter, name: e.target.value }), style: {
151
+ return (_jsxs("div", { className: "j-inner-panel", style: { padding: 10 }, children: [_jsx("input", { value: filter.name ?? "", placeholder: "Untitled", onChange: (e) => onChange({ ...filter, name: e.target.value }), style: {
160
152
  display: "block",
161
153
  width: "100%",
162
154
  background: "transparent",
@@ -168,7 +160,7 @@ function TopMatter({ filter, onChange, }) {
168
160
  letterSpacing: 1,
169
161
  padding: 0,
170
162
  marginBottom: 4,
171
- } }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 9, color: C.GREY, letterSpacing: 2 }, children: "BY" }), _jsx("input", { value: filter.author ?? "", placeholder: "anonymous", onChange: (e) => onChange({ ...filter, author: e.target.value }), style: {
163
+ } }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 10, color: C.WHITE }, children: "By" }), _jsx("input", { value: filter.author ?? "", placeholder: "anonymous", onChange: (e) => onChange({ ...filter, author: e.target.value }), style: {
172
164
  flex: 1,
173
165
  background: "transparent",
174
166
  border: "none",
@@ -176,7 +168,6 @@ function TopMatter({ filter, onChange, }) {
176
168
  fontFamily: "m6x11plus, ui-monospace, monospace",
177
169
  fontSize: 12,
178
170
  color: C.GOLD_TEXT,
179
- letterSpacing: 1,
180
171
  padding: 0,
181
172
  } })] }), _jsx("input", { value: filter.description ?? "", placeholder: "description", onChange: (e) => onChange({ ...filter, description: e.target.value }), style: {
182
173
  display: "block",
@@ -186,8 +177,9 @@ function TopMatter({ filter, onChange, }) {
186
177
  border: "none",
187
178
  outline: "none",
188
179
  fontFamily: "m6x11plus, ui-monospace, monospace",
189
- fontSize: 10,
190
- color: C.GREY,
180
+ fontSize: 11,
181
+ color: C.WHITE,
182
+ opacity: 0.8,
191
183
  lineHeight: 1.35,
192
184
  padding: 0,
193
185
  } })] }));
@@ -205,7 +197,7 @@ export function JamlIdeVisual({ filter, onChange, onEditClause, onAddClause }) {
205
197
  padding: 10,
206
198
  background: C.DARKEST,
207
199
  color: C.WHITE,
208
- }, children: [_jsx(TopMatter, { filter: filter, onChange: onChange }), ["must", "should", "mustnot"].map((zone) => (_jsx(ZoneRail, { zone: zone, clauses: filter[zone], onAdd: onAddClause ? () => onAddClause(zone) : undefined, onRemove: (id) => removeClause(zone, id), onEdit: (c) => onEditClause?.(zone, c), onDragStart: onDragStart, highlight: hoverZone === zone }, zone))), drag && (_jsx("div", { style: {
200
+ }, children: [_jsx(TopMatter, { filter: filter, onChange: onChange }), _jsxs("div", { style: { display: "flex", gap: 10 }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(ZoneRail, { zone: "must", clauses: filter.must, onAdd: onAddClause ? () => onAddClause("must") : undefined, onRemove: (id) => removeClause("must", id), onEdit: (c) => onEditClause?.("must", c), onDragStart: onDragStart, highlight: hoverZone === "must" }) }), _jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(ZoneRail, { zone: "mustnot", clauses: filter.mustnot, onAdd: onAddClause ? () => onAddClause("mustnot") : undefined, onRemove: (id) => removeClause("mustnot", id), onEdit: (c) => onEditClause?.("mustnot", c), onDragStart: onDragStart, highlight: hoverZone === "mustnot" }) })] }), _jsx(ZoneRail, { zone: "should", clauses: filter.should, onAdd: onAddClause ? () => onAddClause("should") : undefined, onRemove: (id) => removeClause("should", id), onEdit: (c) => onEditClause?.("should", c), onDragStart: onDragStart, highlight: hoverZone === "should" }), drag && (_jsx("div", { style: {
209
201
  position: "fixed",
210
202
  left: drag.x - drag.offX,
211
203
  top: drag.y - drag.offY,
@@ -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,142 @@
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 } from "../../ui/tokens.js";
6
+ import { JimboButton } from "../../ui/panel.js";
7
+ import { JimboText } from "../../ui/jimboText.js";
8
+ // ─── Component ───────────────────────────────────────────────────────────────
9
+ const C = JimboColorOption;
10
+ export function CategoryPicker({ config, onSelect, onCancel }) {
11
+ const [search, setSearch] = useState("");
12
+ const filtered = useMemo(() => {
13
+ if (!search)
14
+ return config.items;
15
+ const q = search.toLowerCase();
16
+ return config.items.filter((item) => item.name.toLowerCase().includes(q));
17
+ }, [config.items, search]);
18
+ const pairedVouchers = useMemo(() => {
19
+ if (config.category !== "voucher")
20
+ return null;
21
+ const bases = config.items.filter((item) => item.pos.y % 2 === 0);
22
+ const pairs = bases.map((base) => {
23
+ const upgrade = config.items.find((u) => u.pos.x === base.pos.x && u.pos.y === base.pos.y + 1);
24
+ return { base, upgrade };
25
+ });
26
+ if (!search)
27
+ return pairs;
28
+ const q = search.toLowerCase();
29
+ return pairs.filter(p => p.base.name.toLowerCase().includes(q) || p.upgrade?.name.toLowerCase().includes(q));
30
+ }, [config.items, search, config.category]);
31
+ const handleSelect = useCallback((item) => {
32
+ onSelect({
33
+ category: config.category,
34
+ value: item.name,
35
+ clauseKey: config.clauseKey,
36
+ });
37
+ }, [onSelect, config]);
38
+ const handleAny = useCallback(() => {
39
+ onSelect({
40
+ category: config.category,
41
+ value: "Any",
42
+ clauseKey: config.clauseKey,
43
+ });
44
+ }, [onSelect, config]);
45
+ const renderItem = (item, isMuted = false) => (_jsxs("div", { onClick: () => handleSelect(item), title: item.name, style: {
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ alignItems: "center",
49
+ gap: 3,
50
+ padding: 4,
51
+ borderRadius: 4,
52
+ cursor: "pointer",
53
+ opacity: isMuted ? 0.3 : 1,
54
+ }, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx(JimboText, { size: "micro", tone: "grey", style: { maxWidth: 60, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: item.name })] }, item.name));
55
+ return (_jsxs("div", { style: { padding: 0, maxWidth: 420, maxHeight: "80vh", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { className: "j-flex j-gap-sm", style: { padding: "8px 10px 4px" }, children: [_jsx("input", { className: "j-seed-input__field", type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: { fontSize: 13, padding: "6px 10px", textTransform: "none", letterSpacing: "0.04em" } }), _jsx(JimboButton, { tone: "gold", size: "sm", onClick: handleAny, children: "Any" })] }), config.hint && (_jsx("div", { className: "j-inner-panel", style: { margin: "4px 10px 6px", padding: "6px 10px" }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: config.hint }) })), _jsxs("div", { style: {
56
+ display: "flex",
57
+ flexWrap: "wrap",
58
+ gap: 6,
59
+ padding: "8px 10px 10px",
60
+ overflowY: "auto",
61
+ flex: 1,
62
+ alignContent: "flex-start",
63
+ }, children: [config.category === "voucher" && pairedVouchers ? (pairedVouchers.map((pair) => (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, width: 64 }, children: [renderItem(pair.base, search ? !pair.base.name.toLowerCase().includes(search.toLowerCase()) : false), pair.upgrade && renderItem(pair.upgrade, search ? !pair.upgrade.name.toLowerCase().includes(search.toLowerCase()) : false)] }, pair.base.name)))) : (filtered.map((item) => (_jsx("div", { style: { width: 64 }, children: renderItem(item) }, item.name)))), ((config.category === "voucher" && pairedVouchers?.length === 0) ||
64
+ (config.category !== "voucher" && filtered.length === 0)) && (_jsx("div", { style: { width: "100%", padding: 20, textAlign: "center" }, children: _jsxs(JimboText, { size: "sm", tone: "grey", children: ["No matches for \"", search, "\""] }) }))] })] }));
65
+ }
66
+ // ─── Pre-built configs ───────────────────────────────────────────────────────
67
+ import { VOUCHERS, TAGS, BOSSES, BOOSTER_PACKS, TAROTS_AND_PLANETS } from "../../sprites/spriteData.js";
68
+ // Split consumables by type
69
+ const TAROT_CARDS = TAROTS_AND_PLANETS.filter((c) => {
70
+ const y = c.pos.y;
71
+ return y <= 2 && c.name !== "The Soul" && c.name !== "Black Hole";
72
+ }).filter((c) => {
73
+ return c.pos.y <= 1 || (c.pos.y === 2 && c.pos.x <= 1);
74
+ });
75
+ const PLANET_CARDS = TAROTS_AND_PLANETS.filter((c) => {
76
+ return c.pos.y === 3 ||
77
+ c.name === "Planet X" || c.name === "Ceres" || c.name === "Eris" ||
78
+ c.name === "Black Hole";
79
+ });
80
+ const SPECTRAL_CARDS = TAROTS_AND_PLANETS.filter((c) => {
81
+ return c.pos.y >= 4 || c.name === "The Soul";
82
+ });
83
+ export const VOUCHER_PICKER_CONFIG = {
84
+ title: "Vouchers",
85
+ category: "voucher",
86
+ clauseKey: "voucher",
87
+ sheet: "Vouchers",
88
+ items: VOUCHERS,
89
+ accent: C.GOLD,
90
+ };
91
+ export const TAG_PICKER_CONFIG = {
92
+ title: "Tags",
93
+ category: "tag",
94
+ clauseKey: "tag",
95
+ sheet: "tags",
96
+ items: TAGS,
97
+ accent: C.GREEN,
98
+ };
99
+ export const BOSS_PICKER_CONFIG = {
100
+ title: "Boss Blinds",
101
+ category: "boss",
102
+ clauseKey: "boss",
103
+ sheet: "BlindChips",
104
+ items: BOSSES,
105
+ accent: C.RED,
106
+ hint: "Boss Blinds appear at the end of each Ante.",
107
+ };
108
+ export const TAROT_PICKER_CONFIG = {
109
+ title: "Tarot Cards",
110
+ category: "tarot",
111
+ clauseKey: "tarotCard",
112
+ sheet: "Tarots",
113
+ items: TAROT_CARDS,
114
+ accent: C.PURPLE,
115
+ hint: "Found in Arcana Packs and shops.",
116
+ };
117
+ export const PLANET_PICKER_CONFIG = {
118
+ title: "Planet Cards",
119
+ category: "planet",
120
+ clauseKey: "planetCard",
121
+ sheet: "Tarots",
122
+ items: PLANET_CARDS,
123
+ accent: C.BLUE,
124
+ hint: "Found in Celestial Packs and shops.",
125
+ };
126
+ export const SPECTRAL_PICKER_CONFIG = {
127
+ title: "Spectral Cards",
128
+ category: "spectral",
129
+ clauseKey: "spectralCard",
130
+ sheet: "Tarots",
131
+ items: SPECTRAL_CARDS,
132
+ accent: C.TEAL_GREY,
133
+ hint: "Found in Spectral Packs. Ghost Deck only for shop spawns!",
134
+ };
135
+ export const PACK_PICKER_CONFIG = {
136
+ title: "Booster Packs",
137
+ category: "pack",
138
+ clauseKey: "pack",
139
+ sheet: "Boosters",
140
+ items: BOOSTER_PACKS,
141
+ accent: C.ORANGE,
142
+ };
@@ -0,0 +1,11 @@
1
+ import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
2
+ export interface JamlMapEditorProps {
3
+ /** Initial zone for the demo. */
4
+ zone?: JamlZone;
5
+ /** Callback when selections change. Returns JAML string. */
6
+ onChange?: (jamlString: string) => void;
7
+ }
8
+ export interface MapSlotSelection extends SlotSelection {
9
+ zone: JamlZone;
10
+ }
11
+ export declare function JamlMapEditor({ zone: initialZone, onChange, }: JamlMapEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,170 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback, useMemo, useEffect } from "react";
4
+ import { MysterySlot } from "./MysterySlot.js";
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";
7
+ import { JimboButton, JimboModal } from "../../ui/panel.js";
8
+ import { JimboText } from "../../ui/jimboText.js";
9
+ import { JimboColorOption } from "../../ui/tokens.js";
10
+ import { JimboSprite } from "../../ui/sprites.js";
11
+ // ─── Category menu items ─────────────────────────────────────────────────────
12
+ const C = JimboColorOption;
13
+ const CATEGORIES = [
14
+ { key: "joker", label: "Joker", sprite: "Joker", sheet: "Jokers", tone: "blue", hint: "Shop, Buffoon Pack" },
15
+ { key: "voucher", label: "Voucher", sprite: "Blank", sheet: "Vouchers", tone: "gold", hint: "1 per Ante in shop" },
16
+ { key: "tarot", label: "Tarot Card", sprite: "The Fool", sheet: "Tarots", tone: "tarot", hint: "Arcana Pack, shop" },
17
+ { key: "planet", label: "Planet Card", sprite: "Mercury", sheet: "Tarots", tone: "planet", hint: "Celestial Pack, shop" },
18
+ { key: "spectral", label: "Spectral Card", sprite: "Grim", sheet: "Tarots", tone: "spectral", hint: "Ghost Deck, Spectral Pack" },
19
+ { key: "tag", label: "Tag", sprite: "Uncommon Tag", sheet: "tags", tone: "green", hint: "Skip blind reward" },
20
+ { key: "boss", label: "Boss Blind", sprite: "The Hook", sheet: "BlindChips", tone: "red", hint: "End of each Ante" },
21
+ { key: "pack", label: "Booster Pack", sprite: "Arcana Pack", sheet: "Boosters", tone: "orange", hint: "Arcana, Celestial, etc." },
22
+ ];
23
+ const ZONE_TONE = {
24
+ must: "blue",
25
+ should: "red",
26
+ mustnot: "orange",
27
+ };
28
+ const ZONE_LABEL = {
29
+ must: "Must",
30
+ should: "Should",
31
+ mustnot: "Must Not",
32
+ };
33
+ const CATEGORY_CONFIG_MAP = {
34
+ joker: VOUCHER_PICKER_CONFIG,
35
+ voucher: VOUCHER_PICKER_CONFIG,
36
+ tag: TAG_PICKER_CONFIG,
37
+ boss: BOSS_PICKER_CONFIG,
38
+ tarot: TAROT_PICKER_CONFIG,
39
+ planet: PLANET_PICKER_CONFIG,
40
+ spectral: SPECTRAL_PICKER_CONFIG,
41
+ pack: PACK_PICKER_CONFIG,
42
+ };
43
+ // ─── Component ───────────────────────────────────────────────────────────────
44
+ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
45
+ const [currentZone, setCurrentZone] = useState(initialZone);
46
+ const [ante, setAnte] = useState(1);
47
+ const [antesState, setAntesState] = useState({});
48
+ const [activeSlot, setActiveSlot] = useState(null);
49
+ const [pickerFlow, setPickerFlow] = useState("category");
50
+ const currentAnteSelections = antesState[ante] || {};
51
+ const handleSlotTap = useCallback((anteIndex, id, forceCategory) => {
52
+ setActiveSlot({ ante: anteIndex, id, forceCategory });
53
+ setPickerFlow(forceCategory || "category");
54
+ }, []);
55
+ const handleSlotClear = useCallback((anteIndex, id) => {
56
+ setAntesState((prev) => {
57
+ const next = { ...prev };
58
+ if (!next[anteIndex])
59
+ return next;
60
+ const nextAnte = { ...next[anteIndex] };
61
+ delete nextAnte[id];
62
+ next[anteIndex] = nextAnte;
63
+ return next;
64
+ });
65
+ }, []);
66
+ const handleCategorySelect = useCallback((cat) => {
67
+ setPickerFlow(cat);
68
+ }, []);
69
+ const handleItemSelect = useCallback((selection) => {
70
+ if (!activeSlot)
71
+ return;
72
+ setAntesState((prev) => {
73
+ const next = { ...prev };
74
+ const nextAnte = { ...(next[activeSlot.ante] || {}) };
75
+ nextAnte[activeSlot.id] = { ...selection, zone: currentZone };
76
+ next[activeSlot.ante] = nextAnte;
77
+ return next;
78
+ });
79
+ setActiveSlot(null);
80
+ }, [activeSlot, currentZone]);
81
+ const handlePickerCancel = useCallback(() => {
82
+ if (activeSlot?.forceCategory) {
83
+ setActiveSlot(null);
84
+ }
85
+ else if (pickerFlow !== "category") {
86
+ setPickerFlow("category");
87
+ }
88
+ else {
89
+ setActiveSlot(null);
90
+ }
91
+ }, [activeSlot, pickerFlow]);
92
+ const handleOverlayClose = useCallback(() => {
93
+ setActiveSlot(null);
94
+ }, []);
95
+ const jsonTree = useMemo(() => buildJsonTree(antesState), [antesState]);
96
+ useEffect(() => {
97
+ onChange?.(JSON.stringify(jsonTree, null, 2));
98
+ }, [jsonTree, onChange]);
99
+ const renderSlot = (anteIndex, id, width, sheetType, forceCategory) => {
100
+ const sel = (antesState[anteIndex] || {})[id];
101
+ return (_jsx(MysterySlot, { zone: sel ? sel.zone : currentZone, sheetType: sheetType, selection: sel, width: width, onTap: () => handleSlotTap(anteIndex, id, forceCategory), onClear: sel ? () => handleSlotClear(anteIndex, id) : undefined, style: { flexShrink: 0 } }, id));
102
+ };
103
+ return (_jsxs("div", { style: { width: "100%", height: "100%", display: "flex", flexDirection: "column" }, children: [_jsx("style", { children: `.hide-scrollbar { scrollbar-width: none; ms-overflow-style: none; } .hide-scrollbar::-webkit-scrollbar { display: none; }` }), _jsx("div", { style: { position: "sticky", top: 0, zIndex: 10, background: C.DARKEST, padding: "max(8px, env(safe-area-inset-top, 8px)) 0 8px 0", borderBottom: `2px solid ${C.PANEL_EDGE}` }, children: _jsx("div", { className: "j-flex j-gap-sm", style: { justifyContent: "center" }, children: ["must", "should", "mustnot"].map((z) => (_jsx(JimboButton, { tone: currentZone === z ? ZONE_TONE[z] : "grey", size: "sm", onClick: () => setCurrentZone(z), children: ZONE_LABEL[z] }, z))) }) }), _jsx("div", { className: "hide-scrollbar", style: {
104
+ flex: 1,
105
+ overflowY: "auto",
106
+ scrollSnapType: "y mandatory",
107
+ scrollBehavior: "smooth"
108
+ }, children: [1, 2, 3, 4, 5, 6, 7, 8].map((a) => (_jsxs("div", { style: {
109
+ scrollSnapAlign: "start",
110
+ padding: "24px 8px 64px 8px",
111
+ minHeight: "100%", // ensuring each ante takes at least full viewport height to snap cleanly
112
+ display: "flex",
113
+ flexDirection: "column",
114
+ gap: 24,
115
+ borderBottom: `2px solid ${C.DARK_GREY}`
116
+ }, children: [_jsxs(JimboText, { size: "md", tone: "grey", 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 && (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 }))) })] }));
117
+ }
118
+ // ─── Category Selection Menu ─────────────────────────────────────────────────
119
+ function CategoryMenu({ onSelect, }) {
120
+ return (_jsx("div", { style: {
121
+ display: "grid",
122
+ gridTemplateColumns: "1fr 1fr",
123
+ gap: 6,
124
+ padding: 10,
125
+ maxHeight: "70vh",
126
+ overflowY: "auto",
127
+ }, children: CATEGORIES.map((cat) => (_jsx(JimboButton, { tone: cat.tone, size: "sm", fullWidth: true, onClick: () => onSelect(cat.key), style: { padding: "8px 6px" }, children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, 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 }, children: cat.hint })] })] }) }, cat.key))) }));
128
+ }
129
+ // ─── Build JSON tree from slots ──────────────────────────────────────────────
130
+ function buildJsonTree(antes) {
131
+ const must = [];
132
+ const should = [];
133
+ const mustNot = [];
134
+ for (const [anteStr, selections] of Object.entries(antes)) {
135
+ const anteNum = parseInt(anteStr, 10);
136
+ // Group by zone
137
+ const byZone = {
138
+ must: {}, should: {}, mustnot: {}
139
+ };
140
+ for (const sel of Object.values(selections)) {
141
+ if (!byZone[sel.zone][sel.clauseKey]) {
142
+ byZone[sel.zone][sel.clauseKey] = [];
143
+ }
144
+ byZone[sel.zone][sel.clauseKey].push(sel.value);
145
+ }
146
+ for (const z of ["must", "should", "mustnot"]) {
147
+ const clauseList = Object.entries(byZone[z]);
148
+ if (clauseList.length === 0)
149
+ continue;
150
+ const obj = { ante: anteNum };
151
+ for (const [key, values] of clauseList) {
152
+ obj[key] = values.length === 1 ? values[0] : values;
153
+ }
154
+ if (z === "must")
155
+ must.push(obj);
156
+ else if (z === "should")
157
+ should.push(obj);
158
+ else if (z === "mustnot")
159
+ mustNot.push(obj);
160
+ }
161
+ }
162
+ const result = {};
163
+ if (must.length > 0)
164
+ result.must = must;
165
+ if (should.length > 0)
166
+ result.should = should;
167
+ if (mustNot.length > 0)
168
+ result.mustNot = mustNot;
169
+ return result;
170
+ }