jaml-ui 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/DESIGN.md +7 -3
  2. package/dist/components/JamlAnalyzerFullscreen.d.ts +4 -1
  3. package/dist/components/JamlAnalyzerFullscreen.js +2 -2
  4. package/dist/components/JamlCurator.d.ts +4 -0
  5. package/dist/components/JamlCurator.js +63 -0
  6. package/dist/components/JamlCurator.stories.d.ts +6 -0
  7. package/dist/components/JamlCurator.stories.js +14 -0
  8. package/dist/components/JamlIde.js +1 -1
  9. package/dist/components/JamlIdeVisual.js +12 -20
  10. package/dist/components/jamlMap/CategoryPicker.d.ts +32 -0
  11. package/dist/components/jamlMap/CategoryPicker.js +142 -0
  12. package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
  13. package/dist/components/jamlMap/JamlMapEditor.js +170 -0
  14. package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
  15. package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
  16. package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +1 -1
  17. package/dist/components/jamlMap/JamlMapEditorDemo.js +174 -21
  18. package/dist/components/jamlMap/JokerPicker.js +28 -157
  19. package/dist/components/jamlMap/MysterySlot.js +32 -5
  20. package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
  21. package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
  22. package/dist/components/jamlMap/index.d.ts +2 -1
  23. package/dist/components/jamlMap/index.js +2 -1
  24. package/dist/hooks/useAnalyzer.d.ts +4 -8
  25. package/dist/hooks/useAnalyzer.js +3 -3
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/stories/Button.d.ts +15 -0
  29. package/dist/stories/Button.js +7 -0
  30. package/dist/stories/Button.stories.d.ts +24 -0
  31. package/dist/stories/Button.stories.js +50 -0
  32. package/dist/stories/Header.d.ts +12 -0
  33. package/dist/stories/Header.js +4 -0
  34. package/dist/stories/Header.stories.d.ts +18 -0
  35. package/dist/stories/Header.stories.js +26 -0
  36. package/dist/stories/Page.d.ts +3 -0
  37. package/dist/stories/Page.js +8 -0
  38. package/dist/stories/Page.stories.d.ts +12 -0
  39. package/dist/stories/Page.stories.js +24 -0
  40. package/dist/ui/Jimbo.stories.d.ts +7 -0
  41. package/dist/ui/Jimbo.stories.js +28 -0
  42. package/dist/ui/jimbo.css +20 -11
  43. package/dist/ui/jimboText.d.ts +1 -1
  44. package/dist/ui/panel.d.ts +1 -1
  45. package/dist/ui/panel.js +7 -5
  46. package/package.json +16 -3
@@ -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,7 @@ import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
2
2
  export interface JamlMapEditorDemoProps {
3
3
  /** Initial zone for the demo. */
4
4
  zone?: JamlZone;
5
- /** Callback when a selection changes, to update JAML text. */
5
+ /** Callback when selections change. */
6
6
  onChange?: (slots: (SlotSelection | null)[]) => void;
7
7
  }
8
8
  export declare function JamlMapEditorDemo({ zone: initialZone, onChange, }: JamlMapEditorDemoProps): import("react/jsx-runtime").JSX.Element;
@@ -1,18 +1,32 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback } from "react";
3
+ import { useState, useCallback, useMemo } from "react";
4
4
  import { MysterySlot } from "./MysterySlot.js";
5
5
  import { JokerPicker } from "./JokerPicker.js";
6
+ import { CategoryPicker, VOUCHER_PICKER_CONFIG, TAG_PICKER_CONFIG, BOSS_PICKER_CONFIG, TAROT_PICKER_CONFIG, PLANET_PICKER_CONFIG, SPECTRAL_PICKER_CONFIG, PACK_PICKER_CONFIG, } from "./CategoryPicker.js";
6
7
  import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
7
- // ─── Component ───────────────────────────────────────────────────────────────
8
+ // ─── Category menu items ─────────────────────────────────────────────────────
8
9
  const C = JimboColorOption;
9
- const INITIAL_SLOTS = 8; // 8 shop item slots like in Balatro ante
10
+ const CATEGORIES = [
11
+ { key: "joker", label: "Joker", icon: "🃏", color: C.BLUE, hint: "Shop, Buffoon Pack" },
12
+ { key: "voucher", label: "Voucher", icon: "🎫", color: C.GOLD, hint: "1 per Ante in shop" },
13
+ { key: "tarot", label: "Tarot Card", icon: "🔮", color: C.PURPLE, hint: "Arcana Pack, shop" },
14
+ { key: "planet", label: "Planet Card", icon: "🪐", color: C.BLUE, hint: "Celestial Pack, shop" },
15
+ { key: "spectral", label: "Spectral Card", icon: "👻", color: C.TEAL_GREY, hint: "Ghost Deck, Spectral Pack" },
16
+ { key: "tag", label: "Tag", icon: "🏷️", color: C.GREEN, hint: "Skip blind reward" },
17
+ { key: "boss", label: "Boss Blind", icon: "👁️", color: C.RED, hint: "End of each Ante" },
18
+ { key: "pack", label: "Booster Pack", icon: "📦", color: C.ORANGE, hint: "Arcana, Celestial, etc." },
19
+ ];
20
+ // ─── Component ───────────────────────────────────────────────────────────────
21
+ const INITIAL_SLOTS = 8;
10
22
  export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
11
23
  const [zone, setZone] = useState(initialZone);
12
24
  const [slots, setSlots] = useState(Array(INITIAL_SLOTS).fill(null));
13
25
  const [activeSlot, setActiveSlot] = useState(null);
26
+ const [pickerFlow, setPickerFlow] = useState("category");
14
27
  const handleSlotTap = useCallback((index) => {
15
28
  setActiveSlot(index);
29
+ setPickerFlow("category");
16
30
  }, []);
17
31
  const handleSlotClear = useCallback((index) => {
18
32
  setSlots((prev) => {
@@ -22,7 +36,15 @@ export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
22
36
  return next;
23
37
  });
24
38
  }, [onChange]);
25
- const handleJokerSelect = useCallback((selection) => {
39
+ const handleCategorySelect = useCallback((cat) => {
40
+ if (cat === "joker") {
41
+ setPickerFlow("joker");
42
+ }
43
+ else {
44
+ setPickerFlow(cat);
45
+ }
46
+ }, []);
47
+ const handleItemSelect = useCallback((selection) => {
26
48
  if (activeSlot === null)
27
49
  return;
28
50
  setSlots((prev) => {
@@ -34,23 +56,64 @@ export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
34
56
  setActiveSlot(null);
35
57
  }, [activeSlot, onChange]);
36
58
  const handlePickerCancel = useCallback(() => {
59
+ if (pickerFlow !== "category") {
60
+ // Go back to category selection
61
+ setPickerFlow("category");
62
+ }
63
+ else {
64
+ setActiveSlot(null);
65
+ }
66
+ }, [pickerFlow]);
67
+ const handleOverlayClose = useCallback(() => {
37
68
  setActiveSlot(null);
38
69
  }, []);
39
70
  const filledCount = slots.filter(Boolean).length;
71
+ // Build the JSON tree from slots
72
+ const jsonTree = useMemo(() => buildJsonTree(zone, slots), [zone, slots]);
40
73
  return (_jsxs("div", { style: styles.wrapper, children: [_jsx("div", { style: styles.zoneBar, children: ["must", "should", "mustnot"].map((z) => (_jsx("button", { onClick: () => setZone(z), style: {
41
74
  ...styles.zoneBtn,
42
75
  borderColor: zone === z ? ZONE_COLORS[z] : C.TEAL_GREY,
43
76
  color: zone === z ? ZONE_COLORS[z] : C.GREY,
44
77
  background: zone === z ? withAlpha(ZONE_COLORS[z], 0.1) : "transparent",
45
- }, children: z.toUpperCase() }, z))) }), _jsxs("div", { style: styles.sectionLabel, children: [_jsx("span", { style: { color: ZONE_COLORS[zone] }, children: "Shop Items" }), _jsxs("span", { style: { color: C.GREY, fontSize: 11 }, children: [filledCount, "/", INITIAL_SLOTS, " defined"] })] }), _jsx("div", { style: styles.slotRow, children: slots.map((selection, i) => (_jsx(MysterySlot, { zone: zone, sheetType: "Jokers", selection: selection ?? undefined, width: 48, onTap: () => handleSlotTap(i), onClear: selection ? () => handleSlotClear(i) : undefined }, i))) }), _jsx("div", { style: styles.scrollHint, children: "\u25C0 swipe \u25B6" }), activeSlot !== null && (_jsxs("div", { style: styles.overlay, children: [_jsx("div", { style: styles.overlayBackdrop, onClick: handlePickerCancel }), _jsx("div", { style: styles.pickerWrapper, children: _jsx(JokerPicker, { onSelect: handleJokerSelect, onCancel: handlePickerCancel }) })] })), filledCount > 0 && (_jsxs("div", { style: styles.jamlPreview, children: [_jsx("div", { style: styles.jamlHeader, children: "Generated JAML" }), _jsx("pre", { style: styles.jamlCode, children: generateJamlSnippet(zone, slots) })] }))] }));
78
+ }, children: z.toUpperCase() }, z))) }), _jsxs("div", { style: styles.sectionLabel, children: [_jsx("span", { style: { color: ZONE_COLORS[zone] }, children: "Shop Items" }), _jsxs("span", { style: { color: C.GREY, fontSize: 11 }, children: [filledCount, "/", INITIAL_SLOTS, " defined"] })] }), _jsx("div", { style: styles.slotRow, children: slots.map((selection, i) => (_jsx(MysterySlot, { zone: zone, sheetType: "Jokers", selection: selection ?? undefined, width: 48, onTap: () => handleSlotTap(i), onClear: selection ? () => handleSlotClear(i) : undefined }, i))) }), _jsx("div", { style: styles.scrollHint, children: "\u25C0 swipe \u25B6" }), activeSlot !== null && (_jsxs("div", { style: styles.overlay, children: [_jsx("div", { style: styles.overlayBackdrop, onClick: handleOverlayClose }), _jsx("div", { style: styles.pickerWrapper, children: pickerFlow === "category" ? (_jsx(CategoryMenu, { onSelect: handleCategorySelect, onCancel: handleOverlayClose })) : pickerFlow === "joker" ? (_jsx(JokerPicker, { onSelect: handleItemSelect, onCancel: handlePickerCancel })) : (_jsx(CategoryPicker, { config: CATEGORY_CONFIG_MAP[pickerFlow], onSelect: handleItemSelect, onCancel: handlePickerCancel })) })] })), filledCount > 0 && (_jsxs("div", { style: styles.jsonPreview, children: [_jsxs("div", { style: styles.jsonHeader, children: [_jsx("span", { children: "Filter Preview" }), _jsx("span", { style: { fontSize: 10, opacity: 0.6 }, children: "JSON" })] }), _jsx("div", { style: styles.jsonBody, children: _jsx(JsonTreeNode, { data: jsonTree, indent: 0 }) })] }))] }));
79
+ }
80
+ // ─── Category Selection Menu ─────────────────────────────────────────────────
81
+ function CategoryMenu({ onSelect, onCancel, }) {
82
+ return (_jsxs("div", { style: styles.catMenuContainer, children: [_jsxs("div", { style: styles.catMenuHeader, children: [_jsx("button", { onClick: onCancel, style: styles.backBtn, children: "\u2715" }), _jsx("span", { style: styles.catMenuTitle, children: "Select Category" }), _jsx("div", { style: { width: 44 } })] }), _jsx("div", { style: styles.catGrid, children: CATEGORIES.map((cat) => (_jsxs("button", { onClick: () => onSelect(cat.key), style: {
83
+ ...styles.catBtn,
84
+ borderColor: withAlpha(cat.color, 0.3),
85
+ }, children: [_jsx("span", { style: styles.catIcon, children: cat.icon }), _jsxs("div", { style: styles.catText, children: [_jsx("span", { style: { ...styles.catLabel, color: cat.color }, children: cat.label }), _jsx("span", { style: styles.catHint, children: cat.hint })] })] }, cat.key))) })] }));
46
86
  }
47
- // ─── JAML generation ─────────────────────────────────────────────────────────
48
- function generateJamlSnippet(zone, slots) {
87
+ // ─── JSON tree renderer ──────────────────────────────────────────────────────
88
+ function JsonTreeNode({ data, indent }) {
89
+ if (data === null || data === undefined) {
90
+ return _jsx("span", { style: { color: C.GREY }, children: "null" });
91
+ }
92
+ if (typeof data === "string") {
93
+ return _jsxs("span", { style: { color: C.GREEN_TEXT }, children: ["\"", data, "\""] });
94
+ }
95
+ if (typeof data === "number" || typeof data === "boolean") {
96
+ return _jsx("span", { style: { color: C.GOLD }, children: String(data) });
97
+ }
98
+ if (Array.isArray(data)) {
99
+ if (data.length === 0)
100
+ return _jsx("span", { style: { color: C.GREY }, children: "[]" });
101
+ return (_jsxs("span", { children: [_jsx("span", { style: { color: C.GREY }, children: "[" }), data.map((item, i) => (_jsxs("div", { style: { paddingLeft: (indent + 1) * 14 }, children: [_jsx(JsonTreeNode, { data: item, indent: indent + 1 }), i < data.length - 1 && _jsx("span", { style: { color: C.GREY }, children: "," })] }, i))), _jsx("div", { style: { paddingLeft: indent * 14 }, children: _jsx("span", { style: { color: C.GREY }, children: "]" }) })] }));
102
+ }
103
+ if (typeof data === "object") {
104
+ const entries = Object.entries(data);
105
+ if (entries.length === 0)
106
+ return _jsx("span", { style: { color: C.GREY }, children: "{}" });
107
+ return (_jsxs("span", { children: [_jsx("span", { style: { color: C.GREY }, children: "{" }), entries.map(([key, val], i) => (_jsxs("div", { style: { paddingLeft: (indent + 1) * 14 }, children: [_jsx("span", { style: { color: C.BLUE }, children: key }), _jsx("span", { style: { color: C.GREY }, children: ": " }), _jsx(JsonTreeNode, { data: val, indent: indent + 1 }), i < entries.length - 1 && _jsx("span", { style: { color: C.GREY }, children: "," })] }, key))), _jsx("div", { style: { paddingLeft: indent * 14 }, children: _jsx("span", { style: { color: C.GREY }, children: "}" }) })] }));
108
+ }
109
+ return _jsx("span", { children: String(data) });
110
+ }
111
+ // ─── Build JSON tree from slots ──────────────────────────────────────────────
112
+ function buildJsonTree(zone, slots) {
49
113
  const filled = slots.filter(Boolean);
50
114
  if (filled.length === 0)
51
- return "# empty";
115
+ return {};
52
116
  const jamlZone = zone === "mustnot" ? "mustNot" : zone;
53
- const lines = [`${jamlZone}:`];
54
117
  // Group by clauseKey
55
118
  const groups = new Map();
56
119
  for (const s of filled) {
@@ -58,22 +121,35 @@ function generateJamlSnippet(zone, slots) {
58
121
  existing.push(s.value);
59
122
  groups.set(s.clauseKey, existing);
60
123
  }
124
+ const clauses = [];
61
125
  for (const [key, values] of groups) {
62
126
  if (values.length === 1) {
63
- lines.push(` - ${key}: ${values[0]}`);
127
+ clauses.push({ [key]: values[0] });
64
128
  }
65
129
  else {
66
- lines.push(` - ${key}: [${values.join(", ")}]`);
130
+ clauses.push({ [key]: values });
67
131
  }
68
132
  }
69
- return lines.join("\n");
133
+ return { [jamlZone]: clauses };
70
134
  }
71
- // ─── Constants & styles ──────────────────────────────────────────────────────
135
+ // ─── Category picker config mapping ────────────────────────────────────────
136
+ const CATEGORY_CONFIG_MAP = {
137
+ joker: VOUCHER_PICKER_CONFIG, // Not used — routed to JokerPicker
138
+ voucher: VOUCHER_PICKER_CONFIG,
139
+ tag: TAG_PICKER_CONFIG,
140
+ boss: BOSS_PICKER_CONFIG,
141
+ tarot: TAROT_PICKER_CONFIG,
142
+ planet: PLANET_PICKER_CONFIG,
143
+ spectral: SPECTRAL_PICKER_CONFIG,
144
+ pack: PACK_PICKER_CONFIG,
145
+ };
146
+ // ─── Zone colors ─────────────────────────────────────────────────────────────
72
147
  const ZONE_COLORS = {
73
148
  must: C.BLUE,
74
149
  should: C.RED,
75
150
  mustnot: C.ORANGE,
76
151
  };
152
+ // ─── Styles ──────────────────────────────────────────────────────────────────
77
153
  const styles = {
78
154
  wrapper: {
79
155
  position: "relative",
@@ -141,15 +217,90 @@ const styles = {
141
217
  position: "relative",
142
218
  zIndex: 1,
143
219
  width: "90%",
144
- maxWidth: 400,
220
+ maxWidth: 420,
221
+ },
222
+ // Category menu
223
+ catMenuContainer: {
224
+ background: C.DARKEST,
225
+ border: `2px solid ${C.TEAL_GREY}`,
226
+ borderRadius: 8,
227
+ overflow: "hidden",
228
+ fontFamily: "m6x11plus, ui-monospace, monospace",
229
+ boxShadow: `0 8px 32px ${withAlpha(C.BLACK, 0.6)}`,
145
230
  },
146
- jamlPreview: {
231
+ catMenuHeader: {
232
+ display: "flex",
233
+ alignItems: "center",
234
+ justifyContent: "space-between",
235
+ padding: "10px 12px",
236
+ borderBottom: `1px solid ${C.TEAL_GREY}`,
237
+ background: withAlpha(C.DARK_GREY, 0.5),
238
+ },
239
+ catMenuTitle: {
240
+ color: C.WHITE,
241
+ fontSize: 16,
242
+ fontWeight: "bold",
243
+ letterSpacing: 0.5,
244
+ },
245
+ backBtn: {
246
+ background: "none",
247
+ border: "none",
248
+ color: C.GREY,
249
+ fontFamily: "m6x11plus, ui-monospace, monospace",
250
+ fontSize: 14,
251
+ cursor: "pointer",
252
+ padding: "4px 8px",
253
+ },
254
+ catGrid: {
255
+ display: "grid",
256
+ gridTemplateColumns: "1fr 1fr",
257
+ gap: 6,
258
+ padding: 10,
259
+ maxHeight: "70vh",
260
+ overflowY: "auto",
261
+ },
262
+ catBtn: {
263
+ display: "flex",
264
+ alignItems: "center",
265
+ gap: 8,
266
+ padding: "10px 10px",
267
+ border: "1px solid",
268
+ borderRadius: 6,
269
+ cursor: "pointer",
270
+ background: withAlpha(C.DARK_GREY, 0.3),
271
+ textAlign: "left",
272
+ transition: "background 150ms, transform 100ms",
273
+ fontFamily: "m6x11plus, ui-monospace, monospace",
274
+ },
275
+ catIcon: {
276
+ fontSize: 20,
277
+ flexShrink: 0,
278
+ },
279
+ catText: {
280
+ display: "flex",
281
+ flexDirection: "column",
282
+ gap: 2,
283
+ minWidth: 0,
284
+ },
285
+ catLabel: {
286
+ fontSize: 13,
287
+ fontWeight: "bold",
288
+ fontFamily: "m6x11plus, ui-monospace, monospace",
289
+ },
290
+ catHint: {
291
+ fontSize: 9,
292
+ color: C.GREY,
293
+ fontFamily: "m6x11plus, ui-monospace, monospace",
294
+ lineHeight: "1.2",
295
+ },
296
+ // JSON preview
297
+ jsonPreview: {
147
298
  marginTop: 12,
148
299
  borderRadius: 4,
149
300
  border: `1px solid ${C.TEAL_GREY}`,
150
301
  overflow: "hidden",
151
302
  },
152
- jamlHeader: {
303
+ jsonHeader: {
153
304
  padding: "6px 10px",
154
305
  fontSize: 11,
155
306
  color: C.GREY,
@@ -157,14 +308,16 @@ const styles = {
157
308
  borderBottom: `1px solid ${C.TEAL_GREY}`,
158
309
  letterSpacing: 1,
159
310
  textTransform: "uppercase",
311
+ display: "flex",
312
+ justifyContent: "space-between",
313
+ alignItems: "center",
160
314
  },
161
- jamlCode: {
162
- margin: 0,
315
+ jsonBody: {
163
316
  padding: "8px 10px",
164
317
  fontSize: 12,
165
- color: C.GREEN_TEXT,
166
318
  background: withAlpha(C.DARKEST, 0.8),
167
- lineHeight: "1.5",
168
- whiteSpace: "pre-wrap",
319
+ lineHeight: "1.6",
320
+ fontFamily: "m6x11plus, ui-monospace, monospace",
321
+ overflowX: "auto",
169
322
  },
170
323
  };
@@ -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;