jaml-ui 0.17.3 → 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 (43) 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.js +38 -120
  11. package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
  12. package/dist/components/jamlMap/JamlMapEditor.js +170 -0
  13. package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
  14. package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
  15. package/dist/components/jamlMap/JokerPicker.js +28 -157
  16. package/dist/components/jamlMap/MysterySlot.js +32 -5
  17. package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
  18. package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
  19. package/dist/components/jamlMap/index.d.ts +1 -1
  20. package/dist/components/jamlMap/index.js +1 -1
  21. package/dist/hooks/useAnalyzer.d.ts +4 -8
  22. package/dist/hooks/useAnalyzer.js +3 -6
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +1 -1
  25. package/dist/stories/Button.d.ts +15 -0
  26. package/dist/stories/Button.js +7 -0
  27. package/dist/stories/Button.stories.d.ts +24 -0
  28. package/dist/stories/Button.stories.js +50 -0
  29. package/dist/stories/Header.d.ts +12 -0
  30. package/dist/stories/Header.js +4 -0
  31. package/dist/stories/Header.stories.d.ts +18 -0
  32. package/dist/stories/Header.stories.js +26 -0
  33. package/dist/stories/Page.d.ts +3 -0
  34. package/dist/stories/Page.js +8 -0
  35. package/dist/stories/Page.stories.d.ts +12 -0
  36. package/dist/stories/Page.stories.js +24 -0
  37. package/dist/ui/Jimbo.stories.d.ts +7 -0
  38. package/dist/ui/Jimbo.stories.js +28 -0
  39. package/dist/ui/jimbo.css +20 -11
  40. package/dist/ui/jimboText.d.ts +1 -1
  41. package/dist/ui/panel.d.ts +1 -1
  42. package/dist/ui/panel.js +7 -5
  43. package/package.json +16 -3
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import type { StoryObj } from '@storybook/react';
2
+ import { JamlMapEditor } from './JamlMapEditor';
3
+ import "../../ui/jimbo.css";
4
+ declare const meta: Meta<typeof JamlMapEditor>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { JamlMapEditor } from './JamlMapEditor';
3
+ import { useState } from 'react';
4
+ import { JimboModalRoot } from '../../ui/panel';
5
+ import "../../ui/jimbo.css"; // Ensure global CSS is loaded
6
+ const meta = {
7
+ title: 'JamlMap/JamlMapEditor',
8
+ component: JamlMapEditor,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ viewport: {
12
+ defaultViewport: 'mobile1', // iPhone SE
13
+ },
14
+ },
15
+ decorators: [
16
+ (Story) => (_jsxs("div", { style: { width: "375px", height: "667px", margin: "0 auto", overflow: "hidden", border: "1px solid #333" }, children: [_jsx(Story, {}), _jsx(JimboModalRoot, {})] })),
17
+ ],
18
+ };
19
+ export default meta;
20
+ export const Default = {
21
+ render: () => {
22
+ // eslint-disable-next-line react-hooks/rules-of-hooks
23
+ const [clauses, setClauses] = useState([]);
24
+ return (_jsx(JamlMapEditor, { clauses: clauses, onChange: setClauses }));
25
+ },
26
+ };
@@ -2,7 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { JimboSprite } from "../../ui/sprites.js";
5
- import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
5
+ import { JimboColorOption } from "../../ui/tokens.js";
6
+ import { JimboButton } from "../../ui/panel.js";
7
+ import { JimboText } from "../../ui/jimboText.js";
6
8
  import { JOKERS } from "../../sprites/spriteData.js";
7
9
  const LEGENDARY_JOKERS = new Set([
8
10
  "Canio", "Triboulet", "Yorick", "Chicot", "Perkeo",
@@ -52,13 +54,13 @@ function getJokerRarity(name) {
52
54
  return "uncommon";
53
55
  return "common";
54
56
  }
55
- // ─── Rarity colors ───────────────────────────────────────────────────────────
57
+ // ─── Rarity JimboButton tones ──────────────────────────────────────────────
56
58
  const C = JimboColorOption;
57
59
  const RARITY_META = {
58
- common: { label: "Common", color: C.BLUE, bg: withAlpha(C.BLUE, 0.12), hint: "Found in shops and Buffoon Packs" },
59
- uncommon: { label: "Uncommon", color: C.GREEN, bg: withAlpha(C.GREEN, 0.12), hint: "Found in shops and Buffoon Packs" },
60
- rare: { label: "Rare", color: C.RED, bg: withAlpha(C.RED, 0.12), hint: "Found in shops and Buffoon Packs" },
61
- legendary: { label: "Legendary", color: C.PURPLE, bg: withAlpha(C.PURPLE, 0.12), hint: "Spawns from The Soul only! Find in Arcana Pack, Spectral Pack, Charm Tag, or Ethereal Tag." },
60
+ common: { label: "Common", tone: "blue", hint: "Found in shops and Buffoon Packs" },
61
+ uncommon: { label: "Uncommon", tone: "green", hint: "Found in shops and Buffoon Packs" },
62
+ rare: { label: "Rare", tone: "red", hint: "Found in shops and Buffoon Packs" },
63
+ legendary: { label: "Legendary", tone: "gold", hint: "Spawns from The Soul only!" },
62
64
  };
63
65
  export function JokerPicker({ onSelect, onCancel }) {
64
66
  const [step, setStep] = useState("rarity");
@@ -103,156 +105,25 @@ export function JokerPicker({ onSelect, onCancel }) {
103
105
  rarity,
104
106
  });
105
107
  }, [onSelect, selectedRarity]);
106
- return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: styles.header, children: [_jsx("button", { onClick: step === "rarity" ? onCancel : () => setStep("rarity"), style: styles.backBtn, children: step === "rarity" ? "✕" : "← Back" }), _jsx("span", { style: styles.title, children: step === "rarity" ? "Select Joker Rarity" : `${RARITY_META[selectedRarity].label} Jokers` }), _jsx("div", { style: { width: 44 } })] }), step === "rarity" && (_jsx("div", { style: styles.rarityGrid, children: ["common", "uncommon", "rare", "legendary"].map((rarity) => {
108
+ return (_jsxs("div", { style: { padding: 0, display: "flex", flexDirection: "column" }, children: [step === "rarity" && (_jsx("div", { className: "j-flex-col j-gap-sm", style: { padding: 10 }, children: ["common", "uncommon", "rare", "legendary"].map((rarity) => {
107
109
  const meta = RARITY_META[rarity];
108
- return (_jsxs("button", { onClick: () => handleRaritySelect(rarity), style: {
109
- ...styles.rarityBtn,
110
- borderColor: meta.color,
111
- background: meta.bg,
112
- }, children: [_jsx("span", { style: { ...styles.rarityLabel, color: meta.color }, children: meta.label }), _jsx("span", { style: styles.rarityHint, children: meta.hint })] }, rarity));
113
- }) })), step === "specific" && selectedRarity && (_jsxs(_Fragment, { children: [_jsxs("div", { style: styles.searchRow, children: [_jsx("input", { type: "text", placeholder: "Search jokers...", value: search, onChange: (e) => setSearch(e.target.value), style: styles.searchInput }), _jsx("button", { onClick: handleAnySelect, style: styles.anyBtn, children: "Any" })] }), selectedRarity === "legendary" && (_jsxs("div", { style: styles.legendaryBanner, children: [_jsx("span", { style: { fontSize: 14 }, children: "\uD83D\uDC51" }), _jsxs("span", { children: ["Legendary jokers spawn from ", _jsx("b", { children: "The Soul" }), ". Find it in ", _jsx("b", { children: "Arcana Pack" }), ",", " ", _jsx("b", { children: "Spectral Pack" }), ", ", _jsx("b", { children: "Charm Tag" }), ", or ", _jsx("b", { children: "Ethereal Tag" }), " only!"] })] })), _jsxs("div", { style: styles.jokerGrid, children: [filteredJokers.map((joker) => (_jsxs("div", { onClick: () => handleJokerSelect(joker), title: joker.name, style: styles.jokerCell, children: [_jsx(JimboSprite, { name: joker.name, sheet: "Jokers", width: 48 }), _jsx("span", { style: styles.jokerName, children: joker.name })] }, joker.name))), filteredJokers.length === 0 && (_jsxs("div", { style: styles.emptyState, children: ["No jokers match \"", search, "\""] }))] })] }))] }));
110
+ return (_jsx(JimboButton, { tone: meta.tone, size: "md", fullWidth: true, onClick: () => handleRaritySelect(rarity), children: _jsxs("span", { style: { display: "flex", flexDirection: "column", gap: 2, textAlign: "left", width: "100%" }, children: [_jsx("span", { children: meta.label }), _jsx("span", { style: { fontSize: 9, opacity: 0.7 }, children: meta.hint })] }) }, rarity));
111
+ }) })), step === "specific" && selectedRarity && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "j-flex j-items-center", style: {
112
+ justifyContent: "space-between",
113
+ padding: "8px 10px",
114
+ borderBottom: `2px solid ${C.PANEL_EDGE}`,
115
+ }, children: [_jsx(JimboButton, { tone: "grey", size: "xs", onClick: () => setStep("rarity"), children: "\u2190 Back" }), _jsxs(JimboText, { size: "md", children: [RARITY_META[selectedRarity].label, " Jokers"] }), _jsx("div", { style: { width: 44 } })] }), _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 jokers...", 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: handleAnySelect, children: "Any" })] }), selectedRarity === "legendary" && (_jsx("div", { className: "j-inner-panel", style: { margin: "4px 10px 6px", padding: "6px 10px" }, children: _jsx(JimboText, { size: "xs", tone: "purple", children: "Legendary jokers spawn from The Soul. Find it in Arcana Pack, Spectral Pack, Charm Tag, or Ethereal Tag only!" }) })), _jsxs("div", { style: {
116
+ display: "grid",
117
+ gridTemplateColumns: "repeat(auto-fill, minmax(64px, 1fr))",
118
+ gap: 6,
119
+ padding: "8px 10px 10px"
120
+ }, children: [filteredJokers.map((joker) => (_jsxs("div", { onClick: () => handleJokerSelect(joker), title: joker.name, style: {
121
+ display: "flex",
122
+ flexDirection: "column",
123
+ alignItems: "center",
124
+ gap: 3,
125
+ padding: 4,
126
+ borderRadius: 4,
127
+ cursor: "pointer",
128
+ }, children: [_jsx(JimboSprite, { name: joker.name, sheet: "Jokers", width: 48 }), _jsx(JimboText, { size: "micro", tone: "grey", style: { maxWidth: 60, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: joker.name })] }, joker.name))), filteredJokers.length === 0 && (_jsx("div", { style: { gridColumn: "1 / -1", padding: 20, textAlign: "center" }, children: _jsxs(JimboText, { size: "sm", tone: "grey", children: ["No jokers match \"", search, "\""] }) }))] })] }))] }));
114
129
  }
115
- // ─── Styles ──────────────────────────────────────────────────────────────────
116
- const styles = {
117
- container: {
118
- background: C.DARKEST,
119
- border: `2px solid ${C.TEAL_GREY}`,
120
- borderRadius: 8,
121
- padding: 0,
122
- maxWidth: 400,
123
- maxHeight: "80vh",
124
- overflow: "hidden",
125
- display: "flex",
126
- flexDirection: "column",
127
- fontFamily: "m6x11plus, ui-monospace, monospace",
128
- boxShadow: `0 8px 32px ${withAlpha(C.BLACK, 0.6)}`,
129
- },
130
- header: {
131
- display: "flex",
132
- alignItems: "center",
133
- justifyContent: "space-between",
134
- padding: "10px 12px",
135
- borderBottom: `1px solid ${C.TEAL_GREY}`,
136
- background: withAlpha(C.DARK_GREY, 0.5),
137
- },
138
- backBtn: {
139
- background: "none",
140
- border: "none",
141
- color: C.GREY,
142
- fontFamily: "m6x11plus, ui-monospace, monospace",
143
- fontSize: 14,
144
- cursor: "pointer",
145
- padding: "4px 8px",
146
- },
147
- title: {
148
- color: C.WHITE,
149
- fontSize: 16,
150
- fontWeight: "bold",
151
- letterSpacing: 0.5,
152
- },
153
- rarityGrid: {
154
- display: "flex",
155
- flexDirection: "column",
156
- gap: 8,
157
- padding: 12,
158
- },
159
- rarityBtn: {
160
- display: "flex",
161
- flexDirection: "column",
162
- alignItems: "flex-start",
163
- gap: 4,
164
- padding: "12px 14px",
165
- border: "2px solid",
166
- borderRadius: 6,
167
- cursor: "pointer",
168
- textAlign: "left",
169
- transition: "background 150ms",
170
- },
171
- rarityLabel: {
172
- fontSize: 16,
173
- fontWeight: "bold",
174
- fontFamily: "m6x11plus, ui-monospace, monospace",
175
- },
176
- rarityHint: {
177
- fontSize: 11,
178
- color: C.GREY,
179
- fontFamily: "m6x11plus, ui-monospace, monospace",
180
- lineHeight: "1.3",
181
- },
182
- searchRow: {
183
- display: "flex",
184
- gap: 8,
185
- padding: "10px 12px 6px",
186
- },
187
- searchInput: {
188
- flex: 1,
189
- padding: "6px 10px",
190
- borderRadius: 4,
191
- border: `1px solid ${C.TEAL_GREY}`,
192
- background: withAlpha(C.DARK_GREY, 0.8),
193
- color: C.WHITE,
194
- fontSize: 13,
195
- fontFamily: "m6x11plus, ui-monospace, monospace",
196
- outline: "none",
197
- },
198
- anyBtn: {
199
- padding: "6px 14px",
200
- borderRadius: 4,
201
- border: `1px solid ${C.GOLD}`,
202
- background: withAlpha(C.GOLD, 0.15),
203
- color: C.GOLD,
204
- fontSize: 13,
205
- fontFamily: "m6x11plus, ui-monospace, monospace",
206
- cursor: "pointer",
207
- fontWeight: "bold",
208
- },
209
- legendaryBanner: {
210
- display: "flex",
211
- alignItems: "flex-start",
212
- gap: 6,
213
- margin: "4px 12px 8px",
214
- padding: "8px 10px",
215
- borderRadius: 4,
216
- background: withAlpha(C.PURPLE, 0.12),
217
- border: `1px solid ${withAlpha(C.PURPLE, 0.3)}`,
218
- color: C.PURPLE,
219
- fontSize: 11,
220
- lineHeight: "1.4",
221
- },
222
- jokerGrid: {
223
- display: "grid",
224
- gridTemplateColumns: "repeat(auto-fill, minmax(64px, 1fr))",
225
- gap: 6,
226
- padding: "8px 12px 12px",
227
- overflowY: "auto",
228
- flex: 1,
229
- },
230
- jokerCell: {
231
- display: "flex",
232
- flexDirection: "column",
233
- alignItems: "center",
234
- gap: 3,
235
- padding: 4,
236
- borderRadius: 4,
237
- cursor: "pointer",
238
- transition: "background 120ms",
239
- background: "transparent",
240
- },
241
- jokerName: {
242
- fontSize: 9,
243
- color: C.GREY,
244
- textAlign: "center",
245
- lineHeight: "1.2",
246
- maxWidth: 60,
247
- overflow: "hidden",
248
- textOverflow: "ellipsis",
249
- whiteSpace: "nowrap",
250
- },
251
- emptyState: {
252
- gridColumn: "1 / -1",
253
- textAlign: "center",
254
- color: C.GREY,
255
- fontSize: 13,
256
- padding: 20,
257
- },
258
- };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import React, { useState } from "react";
4
4
  import { JimboSprite } from "../../ui/sprites.js";
5
5
  import { JimboColorOption, withAlpha, JIMBO_ANIMATIONS } from "../../ui/tokens.js";
6
6
  // ─── Zone colors ─────────────────────────────────────────────────────────────
@@ -30,6 +30,8 @@ function getWildcardName(category) {
30
30
  export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onClear, style, }) {
31
31
  const [hover, setHover] = useState(false);
32
32
  const [pressed, setPressed] = useState(false);
33
+ const [tilt, setTilt] = useState({ rx: 0, ry: 0, tx: 0, ty: 0 });
34
+ const cardRef = React.useRef(null);
33
35
  const borderColor = ZONE_BORDER[zone];
34
36
  const isEmpty = !selection;
35
37
  const cardH = Math.round((width * 95) / 71);
@@ -41,7 +43,26 @@ export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onC
41
43
  : hover
42
44
  ? JIMBO_ANIMATIONS.JUICE_UP_SCALE
43
45
  : 1;
44
- return (_jsxs("div", { onClick: onTap, onMouseEnter: () => setHover(true), onMouseLeave: () => { setHover(false); setPressed(false); }, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), style: {
46
+ const handleMouseMove = (e) => {
47
+ if (!cardRef.current)
48
+ return;
49
+ const rect = cardRef.current.getBoundingClientRect();
50
+ // Normalize coordinates: -1 to 1
51
+ const nx = Math.max(-1, Math.min(1, ((e.clientX - rect.left) / rect.width - 0.5) * 2));
52
+ const ny = Math.max(-1, Math.min(1, ((e.clientY - rect.top) / rect.height - 0.5) * 2));
53
+ setTilt({
54
+ rx: ny * -20, // max 20deg tilt
55
+ ry: nx * 20,
56
+ tx: nx * -4, // subtle shift
57
+ ty: ny * -4,
58
+ });
59
+ };
60
+ const handleMouseLeave = () => {
61
+ setHover(false);
62
+ setPressed(false);
63
+ setTilt({ rx: 0, ry: 0, tx: 0, ty: 0 });
64
+ };
65
+ return (_jsxs("div", { ref: cardRef, onClick: onTap, onMouseEnter: () => setHover(true), onMouseLeave: handleMouseLeave, onMouseMove: handleMouseMove, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), style: {
45
66
  position: "relative",
46
67
  width: width + 8,
47
68
  height: cardH + 8,
@@ -56,8 +77,13 @@ export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onC
56
77
  background: isEmpty
57
78
  ? withAlpha(borderColor, 0.06)
58
79
  : withAlpha(C.DARKEST, 0.8),
59
- transform: `scale(${scale})`,
60
- transition: `transform ${JIMBO_ANIMATIONS.JUICE_DURATION}ms ${JIMBO_ANIMATIONS.JUICE_EASING}, border-color 200ms`,
80
+ transform: `perspective(600px) scale(${scale}) rotateX(${tilt.rx}deg) rotateY(${tilt.ry}deg) translate(${tilt.tx}px, ${tilt.ty}px)`,
81
+ transformStyle: "preserve-3d",
82
+ transition: hover
83
+ ? `border-color 200ms`
84
+ : `transform 400ms ${JIMBO_ANIMATIONS.JUICE_EASING}, border-color 200ms`,
85
+ boxShadow: hover ? `0 8px 16px ${withAlpha(C.BLACK, 0.4)}` : `0 2px 4px ${withAlpha(C.BLACK, 0.2)}`,
86
+ zIndex: hover ? 10 : 1,
61
87
  ...style,
62
88
  }, children: [_jsx(JimboSprite, { name: spriteName, sheet: spriteSheet, width: width, style: {
63
89
  opacity: isEmpty ? 0.5 : 1,
@@ -80,11 +106,12 @@ export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onC
80
106
  cursor: "pointer",
81
107
  lineHeight: 1,
82
108
  boxShadow: `0 1px 4px ${withAlpha(C.BLACK, 0.5)}`,
109
+ transform: "translateZ(10px)", // Pop out in 3D
83
110
  }, children: "\u00D7" })), isEmpty && hover && (_jsx("div", { style: {
84
111
  position: "absolute",
85
112
  bottom: -16,
86
113
  left: "50%",
87
- transform: "translateX(-50%)",
114
+ transform: "translateX(-50%) translateZ(10px)",
88
115
  fontFamily: "m6x11plus, ui-monospace, monospace",
89
116
  fontSize: 10,
90
117
  color: borderColor,
@@ -0,0 +1,7 @@
1
+ import type { StoryObj } from '@storybook/react';
2
+ import { MysterySlot } from './MysterySlot';
3
+ declare const meta: Meta<typeof MysterySlot>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Empty: Story;
7
+ export declare const FilledJoker: Story;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MysterySlot } from './MysterySlot';
3
+ const meta = {
4
+ title: 'JAML/MysterySlot',
5
+ component: MysterySlot,
6
+ parameters: {
7
+ layout: 'centered',
8
+ },
9
+ };
10
+ export default meta;
11
+ export const Empty = {
12
+ args: {
13
+ zone: 'must',
14
+ width: 64,
15
+ },
16
+ render: (args) => (_jsx("div", { style: { background: '#1e2b2d', padding: 20 }, children: _jsx(MysterySlot, { ...args }) })),
17
+ };
18
+ export const FilledJoker = {
19
+ args: {
20
+ zone: 'must',
21
+ width: 64,
22
+ selection: {
23
+ clauseKey: 'joker',
24
+ value: 'Joker',
25
+ displayLabel: 'Joker',
26
+ spriteName: 'Joker',
27
+ sheetType: 'Jokers',
28
+ },
29
+ },
30
+ render: (args) => (_jsx("div", { style: { background: '#1e2b2d', padding: 20 }, children: _jsx(MysterySlot, { ...args }) })),
31
+ };
@@ -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 { JamlMapEditorDemo, type JamlMapEditorDemoProps } from "./JamlMapEditorDemo.js";
3
+ export { JamlMapEditor, 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 { JamlMapEditorDemo } from "./JamlMapEditorDemo.js";
3
+ export { JamlMapEditor } 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";
@@ -1,9 +1,5 @@
1
- import type { Motely as MotelyT, MotelyWasm as MotelyWasmT } from "motely-wasm";
1
+ import { MotelyWasm, Motely } from "motely-wasm";
2
2
  import type { AnalyzerAnteView } from "../components/AnalyzerExplorer.js";
3
- export interface MotelyRuntime {
4
- MotelyWasm: typeof MotelyWasmT;
5
- Motely: typeof MotelyT;
6
- }
7
3
  export type AnalyzerStatus = "idle" | "running" | "done" | "error";
8
4
  export type MotelyJsRunState = {
9
5
  voucherBitfield: number;
@@ -18,15 +14,15 @@ export type MotelyJsRunState = {
18
14
  * `createShopItemStream(ante, runState, ...)`.
19
15
  */
20
16
  export interface AnalyzerLive {
21
- ctx: ReturnType<typeof MotelyWasmT.createSearchContext>;
22
- Motely: typeof MotelyT;
17
+ ctx: ReturnType<typeof MotelyWasm.createSearchContext>;
18
+ Motely: typeof Motely;
23
19
  runStates: Record<number, MotelyJsRunState>;
24
20
  desiredNames: ReadonlySet<string>;
25
21
  seed: string;
26
22
  deck: string;
27
23
  stake: string;
28
24
  }
29
- export declare function useAnalyzer(motelyRuntime: MotelyRuntime | null): {
25
+ export declare function useAnalyzer(): {
30
26
  antes: AnalyzerAnteView[];
31
27
  status: AnalyzerStatus;
32
28
  error: string | null;
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { useState, useCallback } from "react";
3
+ import { MotelyWasm, Motely } from "motely-wasm";
3
4
  import { extractVisualJamlItems } from "../utils/jamlMapPreview.js";
4
5
  import { motelyItemDisplayNameFromValue } from "../motelyDisplay.js";
5
- export function useAnalyzer(motelyRuntime) {
6
+ export function useAnalyzer() {
6
7
  const [antes, setAntes] = useState([]);
7
8
  const [status, setStatus] = useState("idle");
8
9
  const [error, setError] = useState(null);
@@ -17,10 +18,6 @@ export function useAnalyzer(motelyRuntime) {
17
18
  setStatus("running");
18
19
  setError(null);
19
20
  try {
20
- if (!motelyRuntime) {
21
- throw new Error("motely-wasm runtime is still booting.");
22
- }
23
- const { MotelyWasm, Motely } = motelyRuntime;
24
21
  const deckEnum = Motely.MotelyDeck[deck] ?? Motely.MotelyDeck.Red;
25
22
  const stakeEnum = Motely.MotelyStake[stake] ?? Motely.MotelyStake.White;
26
23
  const desiredNames = new Set();
@@ -80,7 +77,7 @@ export function useAnalyzer(motelyRuntime) {
80
77
  setError(e instanceof Error ? e.message : String(e));
81
78
  setStatus("error");
82
79
  }
83
- }, [motelyRuntime]);
80
+ }, []);
84
81
  const clearError = useCallback(() => {
85
82
  setError(null);
86
83
  setStatus((s) => (s === "error" ? "idle" : s));
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export { Showcase, type ShowcaseFilter, type ShowcaseLiveStats, type ShowcasePro
21
21
  export { extractVisualJamlItems, type JamlPreviewGroups, type JamlPreviewItem, type JamlPreviewSection, type JamlPreviewVisualType, } from "./utils/jamlMapPreview.js";
22
22
  export { useMotelyStream, type StreamItem, type StreamState } from "./hooks/useShopStream.js";
23
23
  export { useSearch, type SearchResult, type SearchStatus, type UseSearchState, } from "./hooks/useSearch.js";
24
- export { useAnalyzer, type AnalyzerStatus, type AnalyzerLive, type MotelyRuntime, type MotelyJsRunState, } from "./hooks/useAnalyzer.js";
24
+ export { useAnalyzer, type AnalyzerStatus, type AnalyzerLive, type MotelyJsRunState, } from "./hooks/useAnalyzer.js";
25
25
  export { JamlAestheticSelector, type JamlAestheticSelectorProps, type JamlAestheticOption, } from "./components/JamlAestheticSelector.js";
26
26
  export { JamlSeedInput, type JamlSeedInputProps, } from "./components/JamlSeedInput.js";
27
- export { JamlMapEditorDemo, JokerPicker, MysterySlot, CategoryPicker, type JamlMapEditorDemoProps, type JokerPickerProps, type JokerRarity, type MysterySlotProps, type SlotCategory, type SlotSelection, type CategoryPickerConfig, type CategoryPickerProps, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";
27
+ export { JamlMapEditor, JokerPicker, MysterySlot, CategoryPicker, type JamlMapEditorProps, type JokerPickerProps, type JokerRarity, type MysterySlotProps, type SlotCategory, type SlotSelection, type CategoryPickerConfig, type CategoryPickerProps, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";
package/dist/index.js CHANGED
@@ -25,4 +25,4 @@ export { useSearch, } from "./hooks/useSearch.js";
25
25
  export { useAnalyzer, } from "./hooks/useAnalyzer.js";
26
26
  export { JamlAestheticSelector, } from "./components/JamlAestheticSelector.js";
27
27
  export { JamlSeedInput, } from "./components/JamlSeedInput.js";
28
- export { JamlMapEditorDemo, JokerPicker, MysterySlot, CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";
28
+ export { JamlMapEditor, JokerPicker, MysterySlot, CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./components/jamlMap/index.js";