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.
- package/DESIGN.md +7 -3
- package/dist/components/JamlAnalyzerFullscreen.d.ts +4 -1
- package/dist/components/JamlAnalyzerFullscreen.js +2 -2
- package/dist/components/JamlCurator.d.ts +4 -0
- package/dist/components/JamlCurator.js +63 -0
- package/dist/components/JamlCurator.stories.d.ts +6 -0
- package/dist/components/JamlCurator.stories.js +14 -0
- package/dist/components/JamlIde.js +1 -1
- package/dist/components/JamlIdeVisual.js +12 -20
- package/dist/components/jamlMap/CategoryPicker.d.ts +32 -0
- package/dist/components/jamlMap/CategoryPicker.js +142 -0
- package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
- package/dist/components/jamlMap/JamlMapEditor.js +170 -0
- package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
- package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
- package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +1 -1
- package/dist/components/jamlMap/JamlMapEditorDemo.js +174 -21
- package/dist/components/jamlMap/JokerPicker.js +28 -157
- package/dist/components/jamlMap/MysterySlot.js +32 -5
- package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
- package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
- package/dist/components/jamlMap/index.d.ts +2 -1
- package/dist/components/jamlMap/index.js +2 -1
- package/dist/hooks/useAnalyzer.d.ts +4 -8
- package/dist/hooks/useAnalyzer.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/stories/Button.d.ts +15 -0
- package/dist/stories/Button.js +7 -0
- package/dist/stories/Button.stories.d.ts +24 -0
- package/dist/stories/Button.stories.js +50 -0
- package/dist/stories/Header.d.ts +12 -0
- package/dist/stories/Header.js +4 -0
- package/dist/stories/Header.stories.d.ts +18 -0
- package/dist/stories/Header.stories.js +26 -0
- package/dist/stories/Page.d.ts +3 -0
- package/dist/stories/Page.js +8 -0
- package/dist/stories/Page.stories.d.ts +12 -0
- package/dist/stories/Page.stories.js +24 -0
- package/dist/ui/Jimbo.stories.d.ts +7 -0
- package/dist/ui/Jimbo.stories.js +28 -0
- package/dist/ui/jimbo.css +20 -11
- package/dist/ui/jimboText.d.ts +1 -1
- package/dist/ui/panel.d.ts +1 -1
- package/dist/ui/panel.js +7 -5
- 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.
|
|
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
|
-
|
|
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.
|
|
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,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,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:
|
|
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: "
|
|
10
|
-
should: { label: "
|
|
11
|
-
mustnot: { label: "
|
|
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
|
-
|
|
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:
|
|
144
|
-
padding: "2px
|
|
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:
|
|
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:
|
|
190
|
-
color: C.
|
|
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 }),
|
|
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
|
+
}
|