jaml-ui 0.24.2 → 0.24.4

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/README.md CHANGED
@@ -1,147 +1,145 @@
1
- # jaml-ui
2
-
3
- React components, UI tokens, sprites, and utilities for Balatro/JAML apps.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install jaml-ui react react-dom
9
- ```
10
-
11
- ## Package exports
12
-
13
- | Entry | Contents |
14
- |-------|----------|
15
- | `jaml-ui` | Game card components, JAML IDE, Analyzer Explorer, hooks |
16
- | `jaml-ui/ui` | Jimbo design system — JimboPanel, JimboButton, JimboModal, tokens |
17
- | `jaml-ui/core` | Pure asset helpers, sprite metadata, decode utilities (no React) |
18
- | `jaml-ui/motely` | motely-wasm decode helpers (requires `motely-wasm` peer) |
19
- | `jaml-ui/r3f` | 3D card component via React Three Fiber (requires r3f peers) |
20
-
21
- ## Quick start
22
-
23
- ```tsx
24
- import { JamlGameCard, AnalyzerExplorer, JamlIde } from "jaml-ui";
25
- import { JimboPanel, JimboButton } from "jaml-ui/ui";
26
- ```
27
-
28
- ### Game card
29
-
30
- ```tsx
31
- import { JamlGameCard } from "jaml-ui";
32
-
33
- <JamlGameCard
34
- type="joker"
35
- card={{ name: "Blueprint", edition: "Foil", isEternal: true, scale: 1.5 }}
36
- />
37
- ```
38
-
39
- ### Jimbo UI (Balatro design system)
40
-
41
- ```tsx
42
- import { JimboPanel, JimboButton, JimboModal } from "jaml-ui/ui";
43
- import { JimboColorOption } from "jaml-ui/ui";
44
-
45
- <JimboPanel sway onBack={() => setOpen(false)}>
46
- <JimboButton variant="primary" onClick={handleSearch}>Search</JimboButton>
47
- </JimboPanel>
48
- ```
49
-
50
- Available variants: `primary`, `secondary`, `danger`, `back`, `ghost`
51
-
52
- ### JAML IDE
53
-
54
- ```tsx
55
- import { JamlIde } from "jaml-ui";
56
-
57
- <JamlIde
58
- jaml={jaml}
59
- onChange={setJaml}
60
- searchResults={results}
61
- onSearch={handleSearch}
62
- isSearching={isSearching}
63
- />
64
- ```
65
-
66
- ### Analyzer Explorer
67
-
68
- ```tsx
69
- import { AnalyzerExplorer } from "jaml-ui";
70
-
71
- // antes: AnalyzerAnteView[] — stream from motely-wasm createSearchContext
72
- <AnalyzerExplorer antes={antes} totalAntes={8} highlights={highlights} />
73
- ```
74
-
75
- ### JAML Map Preview
76
-
77
- ```tsx
78
- import { JamlMapPreview } from "jaml-ui";
79
-
80
- <JamlMapPreview jaml={jaml} />
81
- ```
82
-
83
- ## Asset handling
84
-
85
- By default sprites resolve from the package `assets/` directory via `import.meta.url`.
86
-
87
- Override at app startup:
88
-
89
- ```ts
90
- import { setJamlAssetBaseUrl, clearJamlAssetBaseUrl } from "jaml-ui";
91
-
92
- setJamlAssetBaseUrl("/vendor/jaml-ui/"); // custom CDN
93
- clearJamlAssetBaseUrl(); // back to default
94
- ```
95
-
96
- ## Core utilities
97
-
98
- ```ts
99
- import { SPRITE_SHEETS, getSpriteData, resolveJamlAssetUrl } from "jaml-ui/core";
100
- ```
101
-
102
- ## Motely decode helpers
103
-
104
- ```ts
105
- import { decodeMotelyItemName, motelyItemTypeName } from "jaml-ui/motely";
106
- ```
107
-
108
- ## 3D card (optional)
109
-
110
- ```bash
111
- npm install three @react-three/fiber @react-three/drei @react-spring/three
112
- ```
113
-
114
- ```tsx
115
- import { Card3D } from "jaml-ui/r3f";
116
-
117
- <Card3D itemName="Blueprint" />
118
- ```
119
-
120
- ## Next.js
121
-
122
- Import pure helpers from `jaml-ui/core` for server components. For local workspace installs add:
123
-
124
- ```ts
125
- // next.config.ts
126
- const nextConfig = { transpilePackages: ["jaml-ui"] };
127
- ```
128
-
129
- ## Search Worker Architecture
130
-
131
- The library provides `useAnalyzer` to interact with `motely-wasm`'s search context natively.
132
- **Important:** As of `motely-wasm@14.3.3`, the search worker utilizes Vite's `?worker&inline` pattern. You no longer need to pass `motelyWasmUrl`.
133
-
134
- Ensure `motely-wasm` is imported and booted at the module level in your application:
135
-
136
- ```tsx
137
- import { boot } from "motely-wasm";
138
- boot(); // Call early in your application lifecycle
139
- ```
140
-
141
- ## Peer dependencies
142
-
143
- | Peer | Required for |
144
- |------|-------------|
145
- | `react`, `react-dom` | All components |
146
- | `motely-wasm ^14.3.3` | `jaml-ui/motely`, `AnalyzerExplorer`, `useAnalyzer` data |
147
- | `three`, `@react-three/fiber`, `@react-three/drei`, `@react-spring/three` | `jaml-ui/r3f` only |
1
+ # jaml-ui
2
+
3
+ React components, UI tokens, sprites, and utilities for Balatro/JAML apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install jaml-ui react react-dom
9
+ ```
10
+
11
+ ## Package exports
12
+
13
+ | Entry | Contents |
14
+ |-------|----------|
15
+ | `jaml-ui` | Game card components, JAML IDE, Analyzer Explorer, hooks |
16
+ | `jaml-ui/ui` | Jimbo design system — JimboPanel, JimboButton, JimboModal, tokens |
17
+ | `jaml-ui/core` | Pure asset helpers, sprite metadata, decode utilities (no React) |
18
+ | `jaml-ui/motely` | motely-wasm decode helpers (requires `motely-wasm` peer) |
19
+ | `jaml-ui/r3f` | 3D card component via React Three Fiber (requires r3f peers) |
20
+
21
+ ## Quick start
22
+
23
+ ```tsx
24
+ import { JamlGameCard, AnalyzerExplorer, JamlIde } from "jaml-ui";
25
+ import { JimboPanel, JimboButton } from "jaml-ui/ui";
26
+ ```
27
+
28
+ ### Game card
29
+
30
+ ```tsx
31
+ import { JamlGameCard } from "jaml-ui";
32
+
33
+ <JamlGameCard
34
+ type="joker"
35
+ card={{ name: "Blueprint", edition: "Foil", isEternal: true, scale: 1.5 }}
36
+ />
37
+ ```
38
+
39
+ ### Jimbo UI (Balatro design system)
40
+
41
+ ```tsx
42
+ import { JimboPanel, JimboButton, JimboModal } from "jaml-ui/ui";
43
+ import { JimboColorOption } from "jaml-ui/ui";
44
+
45
+ <JimboPanel sway onBack={() => setOpen(false)}>
46
+ <JimboButton variant="primary" onClick={handleSearch}>Search</JimboButton>
47
+ </JimboPanel>
48
+ ```
49
+
50
+ Available variants: `primary`, `secondary`, `danger`, `back`, `ghost`
51
+
52
+ ### JAML IDE
53
+
54
+ ```tsx
55
+ import { JamlIde } from "jaml-ui";
56
+
57
+ <JamlIde
58
+ jaml={jaml}
59
+ onChange={setJaml}
60
+ searchResults={results}
61
+ onSearch={handleSearch}
62
+ isSearching={isSearching}
63
+ />
64
+ ```
65
+
66
+ ### Analyzer Explorer
67
+
68
+ ```tsx
69
+ import { AnalyzerExplorer } from "jaml-ui";
70
+
71
+ // antes: AnalyzerAnteView[] — stream from motely-wasm createSearchContext
72
+ <AnalyzerExplorer antes={antes} totalAntes={8} highlights={highlights} />
73
+ ```
74
+
75
+ ### JAML Map Preview
76
+
77
+ ```tsx
78
+ import { JamlMapPreview } from "jaml-ui";
79
+
80
+ <JamlMapPreview jaml={jaml} />
81
+ ```
82
+
83
+ ## Asset handling
84
+
85
+ By default sprites resolve from the package `assets/` directory via `import.meta.url`.
86
+
87
+ Override at app startup:
88
+
89
+ ```ts
90
+ import { setJamlAssetBaseUrl, clearJamlAssetBaseUrl } from "jaml-ui";
91
+
92
+ setJamlAssetBaseUrl("/vendor/jaml-ui/");
93
+ clearJamlAssetBaseUrl(); // back to default
94
+ ```
95
+
96
+ ## Core utilities
97
+
98
+ ```ts
99
+ import { SPRITE_SHEETS, getSpriteData, resolveJamlAssetUrl } from "jaml-ui/core";
100
+ ```
101
+
102
+ ## Motely decode helpers
103
+
104
+ ```ts
105
+ import { decodeMotelyItemName, motelyItemTypeName } from "jaml-ui/motely";
106
+ ```
107
+
108
+ ## 3D card (optional)
109
+
110
+ ```bash
111
+ npm install three @react-three/fiber @react-three/drei @react-spring/three
112
+ ```
113
+
114
+ ```tsx
115
+ import { Card3D } from "jaml-ui/r3f";
116
+
117
+ <Card3D itemName="Blueprint" />
118
+ ```
119
+
120
+ ## Next.js
121
+
122
+ Import pure helpers from `jaml-ui/core` for server components. For local workspace installs add:
123
+
124
+ ```ts
125
+ // next.config.ts
126
+ const nextConfig = { transpilePackages: ["jaml-ui"] };
127
+ ```
128
+
129
+ ## Search Worker Architecture
130
+
131
+ The library provides `useAnalyzer` to interact with `motely-wasm`'s search context natively.
132
+ Ensure `motely-wasm` is imported and booted at the module level in your application:
133
+
134
+ ```tsx
135
+ import { boot } from "motely-wasm";
136
+ boot(); // Call early in your application lifecycle
137
+ ```
138
+
139
+ ## Peer dependencies
140
+
141
+ | Peer | Required for |
142
+ |------|-------------|
143
+ | `react`, `react-dom` | All components |
144
+ | `motely-wasm ^14.3.3` | `jaml-ui/motely`, `AnalyzerExplorer`, `useAnalyzer` data |
145
+ | `three`, `@react-three/fiber`, `@react-three/drei`, `@react-spring/three` | `jaml-ui/r3f` only |
@@ -21,6 +21,6 @@ export interface DeckSpriteProps {
21
21
  * Balatro deck box sprite — optionally overlaid with a stake sticker.
22
22
  * Draws from the Enhancers.png atlas (deck thumbnails live in the bottom
23
23
  * two rows) and stickers.png for the stake band. Uses jaml-ui's
24
- * `resolveJamlAssetUrl` so consumers can override the CDN base.
24
+ * `resolveJamlAssetUrl` so consumers can override the asset base URL.
25
25
  */
26
26
  export declare function DeckSprite({ deck, stake, size, className, style }: DeckSpriteProps): import("react/jsx-runtime").JSX.Element;
@@ -40,7 +40,7 @@ const STICKER_ROWS = 3;
40
40
  * Balatro deck box sprite — optionally overlaid with a stake sticker.
41
41
  * Draws from the Enhancers.png atlas (deck thumbnails live in the bottom
42
42
  * two rows) and stickers.png for the stake band. Uses jaml-ui's
43
- * `resolveJamlAssetUrl` so consumers can override the CDN base.
43
+ * `resolveJamlAssetUrl` so consumers can override the asset base URL.
44
44
  */
45
45
  export function DeckSprite({ deck, stake, size = 50, className = '', style }) {
46
46
  const deckKey = (deck || 'erratic').toLowerCase().replace(/\s*deck$/, '').trim();
@@ -1,5 +1,6 @@
1
1
  import { type Motely } from "motely-wasm";
2
2
  export interface JamlCuratorProps {
3
3
  motely: typeof Motely | null;
4
+ motelyWasmUrl: string;
4
5
  }
5
- export declare function JamlCurator({ motely }: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function JamlCurator({ motely, motelyWasmUrl }: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
@@ -11,10 +11,10 @@ import { useSearch } from "../hooks/useSearch.js";
11
11
  import { useAnalyzer } from "../hooks/useAnalyzer.js";
12
12
  import { JamlSpeedometer } from "./JamlSpeedometer.js";
13
13
  const C = JimboColorOption;
14
- export function JamlCurator({ motely }) {
14
+ export function JamlCurator({ motely, motelyWasmUrl }) {
15
15
  // Use map editor by default to generate JAML
16
16
  const [jamlText, setJamlText] = useState("");
17
- const search = useSearch();
17
+ const search = useSearch(motelyWasmUrl);
18
18
  const analyzer = useAnalyzer(motely);
19
19
  // Search results pagination
20
20
  const [resultIndex, setResultIndex] = useState(0);
@@ -2,11 +2,22 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useState } from "react";
4
4
  import { JamlMapPreview } from "./JamlMapPreview.js";
5
+ import { JamlMapEditor, CategoryMenu, JokerPicker, CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./jamlMap/index.js";
5
6
  import { JamlIdeToolbar } from "./JamlIdeToolbar.js";
6
7
  import { JamlIdeVisual } from "./JamlIdeVisual.js";
7
8
  import { JamlCodeEditor } from "./JamlCodeEditor.js";
8
9
  import { JimboColorOption } from "../ui/tokens.js";
10
+ import { JimboModal } from "../ui/panel.js";
9
11
  import { jamlTextToVisualFilter, visualFilterToJamlText } from "../utils/jamlVisualFilter.js";
12
+ const CATEGORY_CONFIG_MAP = {
13
+ voucher: VOUCHER_PICKER_CONFIG,
14
+ tag: TAG_PICKER_CONFIG,
15
+ boss: BOSS_PICKER_CONFIG,
16
+ tarot: TAROT_PICKER_CONFIG,
17
+ planet: PLANET_PICKER_CONFIG,
18
+ spectral: SPECTRAL_PICKER_CONFIG,
19
+ pack: PACK_PICKER_CONFIG,
20
+ };
10
21
  function TallyBar({ value, max }) {
11
22
  const pct = max > 0 ? Math.min(1, value / max) : 0;
12
23
  return (_jsx("div", { style: { flex: 1, height: 4, borderRadius: 999, background: `${JimboColorOption.DARK_GREY}88`, overflow: "hidden" }, children: _jsx("div", { style: {
@@ -127,6 +138,33 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
127
138
  }
128
139
  };
129
140
  const results = useMemo(() => searchResults, [searchResults]);
141
+ // ── Add-clause picker state ──────────────────────────────────────────────
142
+ const [addZone, setAddZone] = useState(null);
143
+ const [pickerFlow, setPickerFlow] = useState("category");
144
+ const handleAddClause = (zone) => {
145
+ setAddZone(zone);
146
+ setPickerFlow("category");
147
+ };
148
+ const handlePickerSelect = (sel) => {
149
+ if (!addZone)
150
+ return;
151
+ const clause = {
152
+ id: `${Date.now()}-${Math.random()}`,
153
+ type: sel.clauseKey,
154
+ value: sel.value,
155
+ label: sel.value,
156
+ };
157
+ handleVisualFilterChange({ ...activeFilter, [addZone]: [...activeFilter[addZone], clause] });
158
+ setAddZone(null);
159
+ };
160
+ const handlePickerClose = () => {
161
+ if (pickerFlow !== "category") {
162
+ setPickerFlow("category");
163
+ }
164
+ else {
165
+ setAddZone(null);
166
+ }
167
+ };
130
168
  return (_jsxs("div", { className: className, style: {
131
169
  display: "flex",
132
170
  flexDirection: "column",
@@ -147,5 +185,5 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
147
185
  padding: "10px 14px",
148
186
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
149
187
  background: JimboColorOption.TEAL_GREY,
150
- }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results, jaml: text }) })) : null] })] }));
188
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange, onAddClause: handleAddClause })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapEditor, { onChange: handleTextChange }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results, jaml: text }) })) : null] }), _jsx(JimboModal, { open: addZone !== null, onClose: handlePickerClose, children: addZone !== null && (pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: (cat) => setPickerFlow(cat) })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handlePickerSelect, onCancel: handlePickerClose })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handlePickerSelect, onCancel: handlePickerClose }))) })] }));
151
189
  }
@@ -6,9 +6,9 @@ import { JimboColorOption } from "../ui/tokens.js";
6
6
  import { extractVisualJamlItems, } from "../utils/jamlMapPreview.js";
7
7
  const C = JimboColorOption;
8
8
  const ZONES = {
9
- must: { label: "MUST", color: C.BLUE, glow: C.BLUE },
10
- should: { label: "SHOULD", color: C.RED, glow: C.GOLD },
11
- mustNot: { label: "MUST NOT", color: C.ORANGE, glow: C.ORANGE },
9
+ must: { label: "Must", color: C.BLUE, glow: C.BLUE },
10
+ should: { label: "Should", color: C.RED, glow: C.GOLD },
11
+ mustNot: { label: "Must Not", color: C.ORANGE, glow: C.ORANGE },
12
12
  };
13
13
  const SECTION_ORDER = ["must", "should", "mustNot"];
14
14
  const SHEET_FOR_VISUAL = {
@@ -22,12 +22,12 @@ const SHEET_FOR_VISUAL = {
22
22
  * Pulsing glow animation for hits.
23
23
  * Design ref: assets/...DesignsV2/src/v2/GlowRing.css
24
24
  */
25
- const GLOW_ANIMATION = `
26
- @keyframes j-glow-pulse {
27
- 0% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
28
- 50% { box-shadow: 0 0 0 2px var(--glow-color), 0 0 12px var(--glow-color); opacity: 1; }
29
- 100% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
30
- }
25
+ const GLOW_ANIMATION = `
26
+ @keyframes j-glow-pulse {
27
+ 0% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
28
+ 50% { box-shadow: 0 0 0 2px var(--glow-color), 0 0 12px var(--glow-color); opacity: 1; }
29
+ 100% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
30
+ }
31
31
  `;
32
32
  function ClausePill({ item, color, glow, matchCount }) {
33
33
  const isHit = matchCount > 0;
@@ -20,6 +20,51 @@ function formatSpeed(value) {
20
20
  */
21
21
  export function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, className = "", style, }) {
22
22
  const active = status === "running" || status === "booting";
23
- const statusTone = status === "error" ? "red" : active ? "green" : "grey";
24
- return (_jsxs("div", { className: `j-stat-grid ${className}`, style: style, children: [_jsxs("div", { children: [_jsx("div", { className: "j-stat-grid__value", children: _jsx(JimboText, { size: "md", tone: active ? "gold" : "grey", children: formatSpeed(seedsPerSecond) }) }), _jsx("div", { className: "j-stat-grid__label", children: "speed" })] }), _jsxs("div", { children: [_jsx("div", { className: "j-stat-grid__value", children: _jsx(JimboText, { size: "md", tone: "white", children: formatCount(totalSearched) }) }), _jsx("div", { className: "j-stat-grid__label", children: "searched" })] }), _jsxs("div", { children: [_jsx("div", { className: "j-stat-grid__value", children: _jsx(JimboText, { size: "md", tone: Number(matchingSeeds) > 0 ? "green" : "grey", children: formatCount(matchingSeeds) }) }), _jsx("div", { className: "j-stat-grid__label", children: "matches" })] })] }));
23
+ const statusTone = status === "error"
24
+ ? "red"
25
+ : status === "completed"
26
+ ? "green"
27
+ : status === "cancelled"
28
+ ? "orange"
29
+ : active
30
+ ? "gold"
31
+ : "grey";
32
+ const statusLabel = status === "booting"
33
+ ? "booting"
34
+ : status === "running"
35
+ ? "searching"
36
+ : status === "completed"
37
+ ? "done"
38
+ : status === "cancelled"
39
+ ? "cancelled"
40
+ : status === "error"
41
+ ? "error"
42
+ : "idle";
43
+ const rootStyle = {
44
+ display: "flex",
45
+ alignItems: "center",
46
+ flexWrap: "wrap",
47
+ gap: 8,
48
+ padding: "8px 10px",
49
+ borderRadius: 12,
50
+ border: "1px solid var(--j-panel-edge, rgba(255,255,255,0.12))",
51
+ background: "rgba(0,0,0,0.28)",
52
+ ...style,
53
+ };
54
+ const dotStyle = {
55
+ width: 8,
56
+ height: 8,
57
+ borderRadius: 999,
58
+ background: status === "error"
59
+ ? "var(--j-red, #ff4c40)"
60
+ : status === "completed"
61
+ ? "var(--j-green, #429f79)"
62
+ : status === "cancelled"
63
+ ? "var(--j-orange, #ff9800)"
64
+ : active
65
+ ? "var(--j-gold, #e4b643)"
66
+ : "var(--j-grey, #8b8b8b)",
67
+ flexShrink: 0,
68
+ };
69
+ return (_jsxs("div", { className: className, style: rootStyle, children: [_jsx("span", { style: dotStyle, "aria-hidden": "true" }), _jsx(JimboText, { size: "sm", tone: statusTone, children: statusLabel }), _jsx(JimboText, { size: "sm", tone: active ? "gold" : "grey", children: formatSpeed(seedsPerSecond) }), _jsxs(JimboText, { size: "sm", tone: "white", children: [formatCount(totalSearched), " searched"] }), _jsxs(JimboText, { size: "sm", tone: Number(matchingSeeds) > 0 ? "green" : "grey", children: [formatCount(matchingSeeds), " matches"] })] }));
25
70
  }
@@ -1,4 +1,4 @@
1
- import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
1
+ import { type SlotSelection, type JamlZone, type SlotCategory } from "./MysterySlot.js";
2
2
  export interface JamlMapEditorProps {
3
3
  /** Initial zone for the demo. */
4
4
  zone?: JamlZone;
@@ -9,3 +9,6 @@ export interface MapSlotSelection extends SlotSelection {
9
9
  zone: JamlZone;
10
10
  }
11
11
  export declare function JamlMapEditor({ zone: initialZone, onChange, }: JamlMapEditorProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function CategoryMenu({ onSelect, }: {
13
+ onSelect: (cat: SlotCategory) => void;
14
+ }): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback, useMemo, useEffect } from "react";
3
+ import { useState, useCallback, useMemo, useRef } from "react";
4
4
  import { MysterySlot } from "./MysterySlot.js";
5
5
  import { JokerPicker } from "./JokerPicker.js";
6
6
  import { CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./CategoryPicker.js";
@@ -42,6 +42,8 @@ const CATEGORY_CONFIG_MAP = {
42
42
  };
43
43
  // ─── Component ───────────────────────────────────────────────────────────────
44
44
  export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
45
+ const onChangeRef = useRef(onChange);
46
+ onChangeRef.current = onChange;
45
47
  const [currentZone, setCurrentZone] = useState(initialZone);
46
48
  const [ante, setAnte] = useState(1);
47
49
  const [antesState, setAntesState] = useState({});
@@ -60,6 +62,7 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
60
62
  const nextAnte = { ...next[anteIndex] };
61
63
  delete nextAnte[id];
62
64
  next[anteIndex] = nextAnte;
65
+ onChangeRef.current?.(buildJamlText(next));
63
66
  return next;
64
67
  });
65
68
  }, []);
@@ -74,6 +77,7 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
74
77
  const nextAnte = { ...(next[activeSlot.ante] || {}) };
75
78
  nextAnte[activeSlot.id] = { ...selection, zone: currentZone };
76
79
  next[activeSlot.ante] = nextAnte;
80
+ onChangeRef.current?.(buildJamlText(next));
77
81
  return next;
78
82
  });
79
83
  setActiveSlot(null);
@@ -93,19 +97,23 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
93
97
  setActiveSlot(null);
94
98
  }, []);
95
99
  const jamlText = useMemo(() => buildJamlText(antesState), [antesState]);
96
- useEffect(() => {
97
- onChange?.(jamlText);
98
- }, [jamlText, onChange]);
100
+ const handleScrollAttach = useCallback((node) => {
101
+ if (!node)
102
+ return;
103
+ const firstChild = node.children[1];
104
+ if (firstChild)
105
+ node.scrollTop = firstChild.offsetTop;
106
+ }, []);
99
107
  const renderSlot = (anteIndex, id, width, sheetType, forceCategory) => {
100
108
  const sel = (antesState[anteIndex] || {})[id];
101
109
  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
110
  };
103
- return (_jsxs("div", { style: { width: "100%", height: "100%", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { style: { position: "sticky", top: 0, zIndex: 10, background: C.DARKEST, padding: "max(32px, env(safe-area-inset-top, 32px)) 0 8px 0", borderBottom: `2px solid ${C.PANEL_EDGE}` }, children: [_jsx(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 12 }, children: "JAML VISUAL BUILDER" }), _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] : "blue", size: "sm", onClick: () => setCurrentZone(z), style: { opacity: currentZone === z ? 1 : 0.4 }, children: ZONE_LABEL[z] }, z))) })] }), _jsx("div", { className: "hide-scrollbar", style: {
111
+ return (_jsxs("div", { style: { width: "100%", height: "100%", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { style: { position: "sticky", top: 0, zIndex: 10, background: C.DARKEST, padding: "max(32px, env(safe-area-inset-top, 32px)) 0 8px 0", borderBottom: `2px solid ${C.PANEL_EDGE}` }, children: [_jsx(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 12 }, children: "Jaml Visual Builder" }), _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] : "blue", size: "sm", onClick: () => setCurrentZone(z), style: { opacity: currentZone === z ? 1 : 0.4 }, children: ZONE_LABEL[z] }, z))) })] }), _jsx("div", { ref: handleScrollAttach, className: "hide-scrollbar", style: {
104
112
  flex: 1,
105
113
  overflowY: "auto",
106
114
  scrollSnapType: "y mandatory",
107
115
  scrollBehavior: "smooth"
108
- }, children: [1, 2, 3, 4, 5, 6, 7, 8].map((a) => (_jsxs("div", { style: {
116
+ }, children: Array.from({ length: 40 }, (_, i) => i).map((a) => (_jsxs("div", { style: {
109
117
  scrollSnapAlign: "start",
110
118
  padding: "24px 8px 64px 8px",
111
119
  minHeight: "100%", // ensuring each ante takes at least full viewport height to snap cleanly
@@ -113,10 +121,10 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
113
121
  flexDirection: "column",
114
122
  gap: 24,
115
123
  borderBottom: `2px solid ${C.DARK_GREY}`
116
- }, children: [_jsxs(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 8 }, children: ["ANTE ", a] }), _jsxs("div", { className: "j-flex j-justify-between j-items-end", children: [_jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "VOUCHER" }), renderSlot(a, `ante_${a}_voucher`, 42, "Vouchers", "voucher")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "SMALL" }), renderSlot(a, `ante_${a}_tag_small`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "BIG" }), renderSlot(a, `ante_${a}_tag_big`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "BOSS" }), renderSlot(a, `ante_${a}_boss`, 42, "BlindChips", "boss")] })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "SHOP ITEMS" }), _jsx("div", { className: "j-flex hide-scrollbar j-gap-sm", style: { overflowX: "auto", paddingBottom: 8 }, children: [1, 2, 3, 4, 5, 6, 7, 8].map(i => renderSlot(a, `ante_${a}_shop_${i}`, 52, "Jokers")) })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "PACKS" }), _jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: [1, 2, 3, 4, 5, 6].map(i => renderSlot(a, `ante_${a}_pack_${i}`, 64, "Boosters", "pack")) })] })] }, a))) }), _jsx(JimboModal, { open: activeSlot !== null, onClose: handlePickerCancel, title: pickerFlow === "category" ? "Select Category" : undefined, className: "j-picker-modal", children: activeSlot !== null && (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 }))) })] }));
124
+ }, children: [_jsxs(JimboText, { size: "md", tone: "white", style: { textAlign: "center", marginBottom: 8 }, children: ["Ante ", a] }), _jsxs("div", { className: "j-flex j-justify-between j-items-end", children: [_jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Voucher" }), renderSlot(a, `ante_${a}_voucher`, 42, "Vouchers", "voucher")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Small" }), renderSlot(a, `ante_${a}_tag_small`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Big" }), renderSlot(a, `ante_${a}_tag_big`, 42, "tags", "tag")] }), _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "micro", tone: "grey", children: "Boss" }), renderSlot(a, `ante_${a}_boss`, 42, "BlindChips", "boss")] })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Shop Items" }), _jsx("div", { className: "j-flex hide-scrollbar j-gap-sm", style: { overflowX: "auto", paddingBottom: 8 }, children: [1, 2, 3, 4, 5, 6, 7, 8].map(i => renderSlot(a, `ante_${a}_shop_${i}`, 52, "Jokers")) })] }), _jsxs("div", { className: "j-flex-col j-gap-xs", children: [_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 1 }, children: "Packs" }), _jsx("div", { className: "j-flex j-gap-sm", style: { flexWrap: "wrap" }, children: [1, 2, 3, 4, 5, 6].map(i => renderSlot(a, `ante_${a}_pack_${i}`, 64, "Boosters", "pack")) })] })] }, a))) }), _jsx(JimboModal, { open: activeSlot !== null, onClose: handlePickerCancel, title: pickerFlow === "category" ? "Select Category" : undefined, className: "j-picker-modal", children: activeSlot !== null && (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
125
  }
118
126
  // ─── Category Selection Menu ─────────────────────────────────────────────────
119
- function CategoryMenu({ onSelect, }) {
127
+ export function CategoryMenu({ onSelect, }) {
120
128
  return (_jsx("div", { className: "hide-scrollbar", style: {
121
129
  display: "flex",
122
130
  flexDirection: "column",
@@ -1,4 +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
- export { JamlMapEditor, type JamlMapEditorProps } from "./JamlMapEditor.js";
3
+ export { JamlMapEditor, CategoryMenu, type JamlMapEditorProps } from "./JamlMapEditor.js";
4
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,4 +1,4 @@
1
1
  export { MysterySlot } from "./MysterySlot.js";
2
2
  export { JokerPicker } from "./JokerPicker.js";
3
- export { JamlMapEditor } from "./JamlMapEditor.js";
3
+ export { JamlMapEditor, CategoryMenu } from "./JamlMapEditor.js";
4
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";