jaml-ui 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +7 -3
- package/dist/components/JamlAnalyzerFullscreen.d.ts +4 -1
- package/dist/components/JamlAnalyzerFullscreen.js +2 -2
- package/dist/components/JamlCurator.d.ts +4 -0
- package/dist/components/JamlCurator.js +63 -0
- package/dist/components/JamlCurator.stories.d.ts +6 -0
- package/dist/components/JamlCurator.stories.js +14 -0
- package/dist/components/JamlIde.js +1 -1
- package/dist/components/JamlIdeVisual.js +12 -20
- package/dist/components/jamlMap/CategoryPicker.d.ts +32 -0
- package/dist/components/jamlMap/CategoryPicker.js +142 -0
- package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
- package/dist/components/jamlMap/JamlMapEditor.js +170 -0
- package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
- package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
- package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +1 -1
- package/dist/components/jamlMap/JamlMapEditorDemo.js +174 -21
- package/dist/components/jamlMap/JokerPicker.js +28 -157
- package/dist/components/jamlMap/MysterySlot.js +32 -5
- package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
- package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
- package/dist/components/jamlMap/index.d.ts +2 -1
- package/dist/components/jamlMap/index.js +2 -1
- package/dist/hooks/useAnalyzer.d.ts +4 -8
- package/dist/hooks/useAnalyzer.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/stories/Button.d.ts +15 -0
- package/dist/stories/Button.js +7 -0
- package/dist/stories/Button.stories.d.ts +24 -0
- package/dist/stories/Button.stories.js +50 -0
- package/dist/stories/Header.d.ts +12 -0
- package/dist/stories/Header.js +4 -0
- package/dist/stories/Header.stories.d.ts +18 -0
- package/dist/stories/Header.stories.js +26 -0
- package/dist/stories/Page.d.ts +3 -0
- package/dist/stories/Page.js +8 -0
- package/dist/stories/Page.stories.d.ts +12 -0
- package/dist/stories/Page.stories.js +24 -0
- package/dist/ui/Jimbo.stories.d.ts +7 -0
- package/dist/ui/Jimbo.stories.js +28 -0
- package/dist/ui/jimbo.css +20 -11
- package/dist/ui/jimboText.d.ts +1 -1
- package/dist/ui/panel.d.ts +1 -1
- package/dist/ui/panel.js +7 -5
- package/package.json +16 -3
|
@@ -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
|
|
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
|
-
// ───
|
|
8
|
+
// ─── Category menu items ─────────────────────────────────────────────────────
|
|
8
9
|
const C = JimboColorOption;
|
|
9
|
-
const
|
|
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
|
|
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:
|
|
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
|
-
// ───
|
|
48
|
-
function
|
|
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
|
|
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
|
-
|
|
127
|
+
clauses.push({ [key]: values[0] });
|
|
64
128
|
}
|
|
65
129
|
else {
|
|
66
|
-
|
|
130
|
+
clauses.push({ [key]: values });
|
|
67
131
|
}
|
|
68
132
|
}
|
|
69
|
-
return
|
|
133
|
+
return { [jamlZone]: clauses };
|
|
70
134
|
}
|
|
71
|
-
// ───
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
168
|
-
|
|
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
|
|
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
|
|
57
|
+
// ─── Rarity → JimboButton tones ──────────────────────────────────────────────
|
|
56
58
|
const C = JimboColorOption;
|
|
57
59
|
const RARITY_META = {
|
|
58
|
-
common: { label: "Common",
|
|
59
|
-
uncommon: { label: "Uncommon",
|
|
60
|
-
rare: { label: "Rare",
|
|
61
|
-
legendary: { label: "Legendary",
|
|
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:
|
|
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 (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|