jaml-ui 0.19.0 → 0.21.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/dist/assets.d.ts CHANGED
@@ -18,8 +18,3 @@ export declare function setJamlAssetBaseUrl(baseUrl: string | null | undefined):
18
18
  export declare function clearJamlAssetBaseUrl(): void;
19
19
  export declare function resolveJamlAssetUrl(asset: JamlAssetKey | JamlAssetFile): string;
20
20
  export declare function getDefaultJamlAssetUrlMap(): Readonly<Record<JamlAssetKey, string>>;
21
- /**
22
- * Returns the versioned Vercel Blob URL for motely-wasm's Bootsharp module.
23
- * Pass the same pinned motely-wasm version the app installed/uploaded.
24
- */
25
- export declare function getMotelyWasmUrl(version: string): string;
package/dist/assets.js CHANGED
@@ -68,10 +68,3 @@ export function resolveJamlAssetUrl(asset) {
68
68
  export function getDefaultJamlAssetUrlMap() {
69
69
  return defaultAssetUrls;
70
70
  }
71
- /**
72
- * Returns the versioned Vercel Blob URL for motely-wasm's Bootsharp module.
73
- * Pass the same pinned motely-wasm version the app installed/uploaded.
74
- */
75
- export function getMotelyWasmUrl(version) {
76
- return `https://cdn.seedfinder.app/motely-wasm/${version}/index.mjs`;
77
- }
@@ -1,4 +1,3 @@
1
1
  export interface JamlCuratorProps {
2
- motelyWasmUrl: string;
3
2
  }
4
- export declare function JamlCurator({ motelyWasmUrl }: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare function JamlCurator({}: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
@@ -11,10 +11,10 @@ import { useSearch } from "../hooks/useSearch.js";
11
11
  import { useAnalyzer } from "../hooks/useAnalyzer.js";
12
12
  import { JamlSpeedometer } from "./JamlSpeedometer.js";
13
13
  const C = JimboColorOption;
14
- export function JamlCurator({ motelyWasmUrl }) {
14
+ export function JamlCurator({}) {
15
15
  // Use map editor by default to generate JAML
16
16
  const [jamlText, setJamlText] = useState("");
17
- const search = useSearch(motelyWasmUrl);
17
+ const search = useSearch();
18
18
  const analyzer = useAnalyzer();
19
19
  // Search results pagination
20
20
  const [resultIndex, setResultIndex] = useState(0);
@@ -42,7 +42,7 @@ export function CategoryPicker({ config, onSelect, onCancel }) {
42
42
  clauseKey: config.clauseKey,
43
43
  });
44
44
  }, [onSelect, config]);
45
- const renderItem = (item, isMuted = false) => (_jsxs("div", { onClick: () => handleSelect(item), title: item.name, style: {
45
+ const renderItem = (item, isMuted = false) => (_jsxs("div", { className: "j-juice-hover", onClick: () => handleSelect(item), title: item.name, style: {
46
46
  display: "flex",
47
47
  flexDirection: "column",
48
48
  alignItems: "center",
@@ -51,8 +51,8 @@ export function CategoryPicker({ config, onSelect, onCancel }) {
51
51
  borderRadius: 4,
52
52
  cursor: "pointer",
53
53
  opacity: isMuted ? 0.3 : 1,
54
- }, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx(JimboText, { size: "micro", tone: "grey", style: { maxWidth: 60, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: item.name })] }, item.name));
55
- return (_jsxs("div", { style: { padding: 0, maxWidth: 420, maxHeight: "80vh", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { className: "j-flex j-gap-sm", style: { padding: "8px 10px 4px" }, children: [_jsx("input", { className: "j-seed-input__field", type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: { fontSize: 13, padding: "6px 10px", textTransform: "none", letterSpacing: "0.04em" } }), _jsx(JimboButton, { tone: "gold", size: "sm", onClick: handleAny, children: "Any" })] }), config.hint && (_jsx("div", { className: "j-inner-panel", style: { margin: "4px 10px 6px", padding: "6px 10px" }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: config.hint }) })), _jsxs("div", { style: {
54
+ }, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx(JimboText, { size: "micro", tone: "white", style: { lineHeight: 1.1, whiteSpace: "normal", textAlign: "center" }, children: item.name })] }, item.name));
55
+ return (_jsxs("div", { style: { padding: 0, maxWidth: 420, maxHeight: "80vh", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { className: "j-flex j-gap-sm", style: { padding: "8px 10px 4px" }, children: [_jsx("input", { className: "j-seed-input__field", type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: { fontSize: 13, padding: "6px 10px", textTransform: "none", letterSpacing: "0.04em" } }), _jsx(JimboButton, { tone: "red", size: "sm", onClick: handleAny, children: "Any" })] }), config.hint && (_jsx("div", { className: "j-inner-panel", style: { margin: "4px 10px 6px", padding: "6px 10px" }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: config.hint }) })), _jsxs("div", { className: "hide-scrollbar", style: {
56
56
  display: "flex",
57
57
  flexWrap: "wrap",
58
58
  gap: 6,
@@ -86,7 +86,7 @@ export const VOUCHER_PICKER_CONFIG = {
86
86
  clauseKey: "voucher",
87
87
  sheet: "Vouchers",
88
88
  items: VOUCHERS,
89
- accent: C.GOLD,
89
+ accent: C.ORANGE,
90
90
  };
91
91
  export const TAG_PICKER_CONFIG = {
92
92
  title: "Tags",
@@ -12,7 +12,7 @@ import { JimboSprite } from "../../ui/sprites.js";
12
12
  const C = JimboColorOption;
13
13
  const CATEGORIES = [
14
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" },
15
+ { key: "voucher", label: "Voucher", sprite: "Blank", sheet: "Vouchers", tone: "orange", hint: "1 per Ante in shop" },
16
16
  { key: "tarot", label: "Tarot Card", sprite: "The Fool", sheet: "Tarots", tone: "tarot", hint: "Arcana Pack, shop" },
17
17
  { key: "planet", label: "Planet Card", sprite: "Mercury", sheet: "Tarots", tone: "planet", hint: "Celestial Pack, shop" },
18
18
  { key: "spectral", label: "Spectral Card", sprite: "Grim", sheet: "Tarots", tone: "spectral", hint: "Ghost Deck, Spectral Pack" },
@@ -100,7 +100,7 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
100
100
  const sel = (antesState[anteIndex] || {})[id];
101
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
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: {
103
+ return (_jsxs("div", { style: { width: "100%", height: "100%", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { style: { position: "sticky", top: 0, zIndex: 10, background: C.DARKEST, padding: "max(32px, env(safe-area-inset-top, 32px)) 0 8px 0", borderBottom: `2px solid ${C.PANEL_EDGE}` }, children: [_jsx(JimboText, { size: "md", tone: "white", dance: true, style: { textAlign: "center", marginBottom: 12 }, children: "JAML VISUAL BUILDER" }), _jsx("div", { className: "j-flex j-gap-sm", style: { justifyContent: "center" }, children: ["must", "should", "mustnot"].map((z) => (_jsx(JimboButton, { tone: currentZone === z ? ZONE_TONE[z] : "blue", size: "sm", onClick: () => setCurrentZone(z), style: { opacity: currentZone === z ? 1 : 0.4 }, children: ZONE_LABEL[z] }, z))) })] }), _jsx("div", { className: "hide-scrollbar", style: {
104
104
  flex: 1,
105
105
  overflowY: "auto",
106
106
  scrollSnapType: "y mandatory",
@@ -117,14 +117,14 @@ export function JamlMapEditor({ zone: initialZone = "must", onChange, }) {
117
117
  }
118
118
  // ─── Category Selection Menu ─────────────────────────────────────────────────
119
119
  function CategoryMenu({ onSelect, }) {
120
- return (_jsx("div", { style: {
121
- display: "grid",
122
- gridTemplateColumns: "1fr 1fr",
123
- gap: 6,
124
- padding: 10,
120
+ return (_jsx("div", { className: "hide-scrollbar", style: {
121
+ display: "flex",
122
+ flexDirection: "column",
123
+ gap: 12,
124
+ padding: "10px 4px",
125
125
  maxHeight: "70vh",
126
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))) }));
127
+ }, children: CATEGORIES.map((cat) => (_jsx(JimboButton, { tone: cat.tone, size: "sm", fullWidth: true, onClick: () => onSelect(cat.key), children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, 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, whiteSpace: "normal" }, children: cat.hint })] })] }) }, cat.key))) }));
128
128
  }
129
129
  // ─── Build JSON tree from slots ──────────────────────────────────────────────
130
130
  function buildJsonTree(antes) {
@@ -62,63 +62,67 @@ export function MysterySlot({ zone, sheetType, selection, width = 56, onTap, onC
62
62
  setPressed(false);
63
63
  setTilt({ rx: 0, ry: 0, tx: 0, ty: 0 });
64
64
  };
65
- return (_jsxs("div", { ref: cardRef, onClick: onTap, onMouseEnter: () => setHover(true), onMouseLeave: handleMouseLeave, onMouseMove: handleMouseMove, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), style: {
65
+ return (_jsx("div", { ref: cardRef, onClick: onTap, onMouseEnter: () => setHover(true), onMouseLeave: handleMouseLeave, onMouseMove: handleMouseMove, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), style: {
66
66
  position: "relative",
67
67
  width: width + 8,
68
68
  height: cardH + 8,
69
- display: "flex",
70
- alignItems: "center",
71
- justifyContent: "center",
72
69
  cursor: onTap ? "pointer" : "default",
73
- borderRadius: 6,
74
- border: isEmpty
75
- ? `2px dashed ${withAlpha(borderColor, 0.4)}`
76
- : `3px solid ${borderColor}`,
77
- background: isEmpty
78
- ? withAlpha(borderColor, 0.06)
79
- : withAlpha(C.DARKEST, 0.8),
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,
87
70
  ...style,
88
- }, children: [_jsx(JimboSprite, { name: spriteName, sheet: spriteSheet, width: width, style: {
89
- opacity: isEmpty ? 0.5 : 1,
90
- filter: isEmpty ? "saturate(0.3)" : "none",
91
- transition: "opacity 200ms, filter 200ms",
92
- } }), selection && onClear && (_jsx("div", { onClick: (e) => { e.stopPropagation(); onClear(); }, style: {
93
- position: "absolute",
94
- top: -6,
95
- right: -6,
96
- width: 18,
97
- height: 18,
98
- borderRadius: "50%",
99
- background: C.RED,
100
- color: C.WHITE,
101
- display: "flex",
102
- alignItems: "center",
103
- justifyContent: "center",
104
- fontSize: 11,
105
- fontFamily: "m6x11plus, ui-monospace, monospace",
106
- cursor: "pointer",
107
- lineHeight: 1,
108
- boxShadow: `0 1px 4px ${withAlpha(C.BLACK, 0.5)}`,
109
- transform: "translateZ(10px)", // Pop out in 3D
110
- }, children: "\u00D7" })), isEmpty && hover && (_jsx("div", { style: {
111
- position: "absolute",
112
- bottom: -16,
113
- left: "50%",
114
- transform: "translateX(-50%) translateZ(10px)",
115
- fontFamily: "m6x11plus, ui-monospace, monospace",
116
- fontSize: 10,
117
- color: borderColor,
118
- whiteSpace: "nowrap",
119
- textTransform: "uppercase",
120
- letterSpacing: 1,
121
- }, children: "+ tap" }))] }));
71
+ }, children: _jsxs("div", { style: {
72
+ position: "absolute",
73
+ inset: 0,
74
+ display: "flex",
75
+ alignItems: "center",
76
+ justifyContent: "center",
77
+ borderRadius: 6,
78
+ border: isEmpty
79
+ ? `2px dashed ${withAlpha(borderColor, 0.4)}`
80
+ : `3px solid ${borderColor}`,
81
+ background: isEmpty
82
+ ? withAlpha(borderColor, 0.06)
83
+ : withAlpha(C.DARKEST, 0.8),
84
+ transform: `perspective(600px) scale(${scale}) rotateX(${tilt.rx}deg) rotateY(${tilt.ry}deg) translate(${tilt.tx}px, ${tilt.ty}px)`,
85
+ transformStyle: "preserve-3d",
86
+ transition: hover
87
+ ? `border-color 200ms`
88
+ : `transform 400ms ${JIMBO_ANIMATIONS.JUICE_EASING}, border-color 200ms`,
89
+ boxShadow: hover ? `0 8px 16px ${withAlpha(C.BLACK, 0.4)}` : `0 2px 4px ${withAlpha(C.BLACK, 0.2)}`,
90
+ zIndex: hover ? 10 : 1,
91
+ pointerEvents: "none",
92
+ }, children: [_jsx(JimboSprite, { name: spriteName, sheet: spriteSheet, width: width, style: {
93
+ opacity: isEmpty ? 0.5 : 1,
94
+ filter: isEmpty ? "saturate(0.3)" : "none",
95
+ transition: "opacity 200ms, filter 200ms",
96
+ } }), selection && onClear && (_jsx("div", { onClick: (e) => { e.stopPropagation(); onClear(); }, style: {
97
+ position: "absolute",
98
+ top: -6,
99
+ right: -6,
100
+ width: 18,
101
+ height: 18,
102
+ borderRadius: "50%",
103
+ background: C.RED,
104
+ color: C.WHITE,
105
+ display: "flex",
106
+ alignItems: "center",
107
+ justifyContent: "center",
108
+ fontSize: 11,
109
+ fontFamily: "m6x11plus, ui-monospace, monospace",
110
+ cursor: "pointer",
111
+ lineHeight: 1,
112
+ boxShadow: `0 1px 4px ${withAlpha(C.BLACK, 0.5)}`,
113
+ transform: "translateZ(10px)", // Pop out in 3D
114
+ }, children: "\u00D7" })), isEmpty && hover && (_jsx("div", { style: {
115
+ position: "absolute",
116
+ bottom: -16,
117
+ left: "50%",
118
+ transform: "translateX(-50%) translateZ(10px)",
119
+ fontFamily: "m6x11plus, ui-monospace, monospace",
120
+ fontSize: 10,
121
+ color: borderColor,
122
+ whiteSpace: "nowrap",
123
+ textTransform: "uppercase",
124
+ letterSpacing: 1,
125
+ }, children: "+ tap" }))] }) }));
122
126
  }
123
127
  // ─── Helpers ─────────────────────────────────────────────────────────────────
124
128
  function categoryToSheet(cat) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import motely, { MotelyWasm, MotelyWasmEvents } from "motely-wasm";
2
+ // Boot motely immediately when this module is loaded
3
+ motely.boot().catch(console.error);
4
+ let activeSearch = null;
5
+ self.addEventListener('message', function (e) {
6
+ const msg = e.data;
7
+ if (msg.type === 'start') {
8
+ const validation = MotelyWasm.validateJaml(msg.jaml);
9
+ if (validation !== 'valid') {
10
+ self.postMessage({ type: 'error', message: validation });
11
+ return;
12
+ }
13
+ function cleanup() {
14
+ MotelyWasmEvents.notifyResult = () => { };
15
+ MotelyWasmEvents.notifyProgress = () => { };
16
+ MotelyWasmEvents.notifyComplete = () => { };
17
+ activeSearch = null;
18
+ }
19
+ MotelyWasmEvents.notifyResult = function (seed, score, tallyColumns) {
20
+ self.postMessage({ type: 'result', seed, score, tallyColumns: Array.from(tallyColumns) });
21
+ };
22
+ MotelyWasmEvents.notifyProgress = function (searched, matching) {
23
+ self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });
24
+ };
25
+ MotelyWasmEvents.notifyComplete = function (status, searched, matched) {
26
+ cleanup();
27
+ self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });
28
+ };
29
+ try {
30
+ const mode = msg.mode || 'random';
31
+ if (mode === 'random') {
32
+ activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
33
+ }
34
+ else if (mode === 'aesthetic') {
35
+ activeSearch = MotelyWasm.startAestheticSearch(msg.jaml, msg.aesthetic);
36
+ }
37
+ else if (mode === 'seedList') {
38
+ activeSearch = MotelyWasm.startSeedListSearch(msg.jaml, msg.seeds);
39
+ }
40
+ else if (mode === 'keyword') {
41
+ activeSearch = MotelyWasm.startKeywordSearch(msg.jaml, msg.keywords, msg.padding || '');
42
+ }
43
+ else if (mode === 'sequential') {
44
+ activeSearch = MotelyWasm.startSequentialSearch(msg.jaml, msg.batchCharCount, BigInt(msg.startBatch), BigInt(msg.endBatch));
45
+ }
46
+ }
47
+ catch (err) {
48
+ cleanup();
49
+ self.postMessage({ type: 'error', message: String(err) });
50
+ }
51
+ }
52
+ else if (msg.type === 'stop') {
53
+ if (activeSearch) {
54
+ activeSearch.cancel();
55
+ activeSearch = null;
56
+ self.postMessage({ type: 'cancelled' });
57
+ }
58
+ }
59
+ else if (msg.type === 'get_tally_labels') {
60
+ try {
61
+ const labels = MotelyWasm.getTallyLabels(msg.jaml);
62
+ self.postMessage({ type: 'tally_labels', labels: Array.from(labels) });
63
+ }
64
+ catch (err) {
65
+ self.postMessage({ type: 'error', message: String(err) });
66
+ }
67
+ }
68
+ });
@@ -13,7 +13,7 @@ export interface UseSearchState {
13
13
  seedsPerSecond: number;
14
14
  tallyLabels: string[];
15
15
  }
16
- export declare function useSearch(motelyWasmUrl: string): {
16
+ export declare function useSearch(): {
17
17
  start: (jaml: string, count: number) => void;
18
18
  startAesthetic: (jaml: string, aesthetic: number) => void;
19
19
  startSeedList: (jaml: string, seeds: string[]) => void;
@@ -1,12 +1,8 @@
1
1
  "use client";
2
2
  import { useState, useCallback, useRef, useEffect } from "react";
3
- import { SEARCH_WORKER_CODE } from "./searchWorkerCode.js";
4
- function createWorker(motelyWasmUrl) {
5
- const blob = new Blob([SEARCH_WORKER_CODE], { type: "text/javascript" });
6
- const blobUrl = URL.createObjectURL(blob);
7
- const worker = new Worker(blobUrl);
8
- worker.postMessage({ type: "init", url: motelyWasmUrl });
9
- return worker;
3
+ import SearchWorker from "./searchWorker.ts?worker&inline";
4
+ function createWorker() {
5
+ return new SearchWorker();
10
6
  }
11
7
  const INITIAL_STATE = {
12
8
  results: [],
@@ -17,16 +13,15 @@ const INITIAL_STATE = {
17
13
  seedsPerSecond: 0,
18
14
  tallyLabels: [],
19
15
  };
20
- export function useSearch(motelyWasmUrl) {
16
+ export function useSearch() {
21
17
  const [state, setState] = useState(INITIAL_STATE);
22
18
  const workerRef = useRef(null);
23
- const readyRef = useRef(false);
19
+ const readyRef = useRef(true); // Worker is ready implicitly since boot is handled by import
24
20
  const speedRef = useRef({ lastSearched: 0n, lastTime: 0, ema: 0 });
25
21
  useEffect(() => {
26
- setState((s) => ({ ...s, status: "booting" }));
27
- const worker = createWorker(motelyWasmUrl);
22
+ setState((s) => ({ ...s, status: "idle" }));
23
+ const worker = createWorker();
28
24
  workerRef.current = worker;
29
- readyRef.current = false;
30
25
  worker.onmessage = (e) => {
31
26
  const msg = e.data;
32
27
  if (msg.type === "ready") {
@@ -88,7 +83,7 @@ export function useSearch(motelyWasmUrl) {
88
83
  worker.terminate();
89
84
  workerRef.current = null;
90
85
  };
91
- }, [motelyWasmUrl]);
86
+ }, []);
92
87
  const sendStart = useCallback((payload) => {
93
88
  const worker = workerRef.current;
94
89
  if (!worker)
@@ -66,5 +66,5 @@ export const Card3D = memo(function Card3D({ sprite, position = [0, 0, 0], rotat
66
66
  const reset = () => { target.current = { rx: 0, ry: 0, rz: 0, ox: 0, oy: 0 }; };
67
67
  if (!texture)
68
68
  return null;
69
- return (_jsx(animated.group, { "position-x": position[0], "position-y": posY.to((y) => position[1] + y), "position-z": position[2], "rotation-x": rotation[0], "rotation-y": rotation[1], "rotation-z": rotation[2], scale: scale, children: _jsxs("group", { ref: tiltRef, children: [highlighted && (_jsx("pointLight", { color: glowColor, intensity: 1.5, distance: 1, position: [0, 0, 0.1] })), _jsxs("mesh", { onClick: (e) => { e.stopPropagation(); onClick?.(); }, onPointerMove: onMove, onPointerEnter: (e) => { e.stopPropagation(); setHovered(true); onPointerEnter?.(); document.body.style.cursor = 'pointer'; }, onPointerLeave: (e) => { e.stopPropagation(); setHovered(false); reset(); onPointerLeave?.(); document.body.style.cursor = 'auto'; }, castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [CARD_DIMENSIONS.WIDTH, CARD_DIMENSIONS.HEIGHT, CARD_DIMENSIONS.DEPTH] }), _jsx("meshBasicMaterial", { attach: "material-4", map: texture, toneMapped: false }), _jsx("meshStandardMaterial", { attach: "material-5", color: "#1a1a2e", metalness: 0.2, roughness: 0.8 }), _jsx("meshStandardMaterial", { attach: "material-0", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-1", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-2", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-3", color: "#f5f5dc" })] }), selected && (_jsxs("mesh", { position: [0, 0, -CARD_DIMENSIONS.DEPTH], children: [_jsx("ringGeometry", { args: [0.45, 0.5, 32] }), _jsx("meshBasicMaterial", { color: "#e4b643", transparent: true, opacity: 0.8 })] }))] }) }));
69
+ return (_jsxs(animated.group, { "position-x": position[0], "position-y": posY.to((y) => position[1] + y), "position-z": position[2], "rotation-x": rotation[0], "rotation-y": rotation[1], "rotation-z": rotation[2], scale: scale, children: [_jsxs("mesh", { visible: false, onClick: (e) => { e.stopPropagation(); onClick?.(); }, onPointerMove: onMove, onPointerEnter: (e) => { e.stopPropagation(); setHovered(true); onPointerEnter?.(); document.body.style.cursor = 'pointer'; }, onPointerLeave: (e) => { e.stopPropagation(); setHovered(false); reset(); onPointerLeave?.(); document.body.style.cursor = 'auto'; }, children: [_jsx("boxGeometry", { args: [CARD_DIMENSIONS.WIDTH, CARD_DIMENSIONS.HEIGHT, CARD_DIMENSIONS.DEPTH * 2] }), _jsx("meshBasicMaterial", {})] }), _jsxs("group", { ref: tiltRef, children: [highlighted && (_jsx("pointLight", { color: glowColor, intensity: 1.5, distance: 1, position: [0, 0, 0.1] })), _jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [CARD_DIMENSIONS.WIDTH, CARD_DIMENSIONS.HEIGHT, CARD_DIMENSIONS.DEPTH] }), _jsx("meshBasicMaterial", { attach: "material-4", map: texture, toneMapped: false }), _jsx("meshStandardMaterial", { attach: "material-5", color: "#1a1a2e", metalness: 0.2, roughness: 0.8 }), _jsx("meshStandardMaterial", { attach: "material-0", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-1", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-2", color: "#f5f5dc" }), _jsx("meshStandardMaterial", { attach: "material-3", color: "#f5f5dc" })] }), selected && (_jsxs("mesh", { position: [0, 0, -CARD_DIMENSIONS.DEPTH], children: [_jsx("ringGeometry", { args: [0.45, 0.5, 32] }), _jsx("meshBasicMaterial", { color: "#e4b643", transparent: true, opacity: 0.8 })] }))] })] }));
70
70
  });
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useJamlCardRenderer } from "../ui/hooks.js";
4
4
  export function JamlCardRenderer({ layers, invert = false, className = "", hoverTilt = false }) {
5
5
  const { canvasRef, containerStyle, canvasStyle, handlers } = useJamlCardRenderer({
@@ -7,5 +7,5 @@ export function JamlCardRenderer({ layers, invert = false, className = "", hover
7
7
  invert,
8
8
  hoverTilt
9
9
  });
10
- return (_jsx("div", { className: className, style: containerStyle, ...handlers, children: _jsx("canvas", { ref: canvasRef, style: canvasStyle }) }));
10
+ return (_jsxs("div", { className: className, style: { ...containerStyle, position: "relative" }, children: [_jsx("canvas", { ref: canvasRef, style: canvasStyle }), hoverTilt && (_jsx("div", { style: { position: "absolute", inset: "-15px", zIndex: 10, cursor: "pointer" }, ...handlers })), !hoverTilt && (_jsx("div", { style: { position: "absolute", inset: "0", zIndex: 10 }, ...handlers }))] }));
11
11
  }
@@ -87,3 +87,16 @@ export declare function useJamlIdeDrag(filter: JamlVisualFilter, onChange: (filt
87
87
  hoverZone: string | null;
88
88
  onDragStart: (e: React.MouseEvent | React.TouchEvent, clause: JamlVisualClause, fromZone: JamlZone) => void;
89
89
  };
90
+ /**
91
+ * Provides a magnetic 3D tilt effect for DOM elements, replicating the 'juice' of Balatro cards.
92
+ * Ensures the hit-detection area remains stable by separating container events from the transformed style.
93
+ */
94
+ export declare function useDOMMagneticTilt(enabled?: boolean): {
95
+ handlers: {
96
+ onPointerEnter: ((event: React.PointerEvent) => void) | undefined;
97
+ onPointerLeave: (() => void) | undefined;
98
+ onPointerMove: ((event: React.PointerEvent) => void) | undefined;
99
+ };
100
+ tiltStyle: React.CSSProperties;
101
+ isHovered: boolean;
102
+ };
package/dist/ui/hooks.js CHANGED
@@ -405,18 +405,18 @@ export function useJamlCardRenderer({ layers, invert = false, hoverTilt = false,
405
405
  aspectRatio: String(ratio),
406
406
  width: '100%',
407
407
  display: 'flex',
408
- transition: hoverTilt && !isHovered ? 'transform 0.4s ease' : undefined,
409
- transform: hoverTilt ? (isHovered ? transform : 'none') : undefined,
410
- transformStyle: hoverTilt ? 'preserve-3d' : undefined,
411
- transformOrigin: hoverTilt ? 'center center' : undefined,
408
+ perspective: hoverTilt ? '1000px' : undefined,
412
409
  userSelect: 'none',
413
410
  WebkitUserSelect: 'none',
414
411
  };
415
412
  const canvasStyle = {
413
+ transition: hoverTilt && !isHovered ? 'transform 0.4s ease, box-shadow 0.4s ease-out' : 'transform 0.1s ease-out',
414
+ transform: hoverTilt ? (isHovered ? transform : 'none') : undefined,
415
+ transformStyle: hoverTilt ? 'preserve-3d' : undefined,
416
+ transformOrigin: hoverTilt ? 'center center' : undefined,
416
417
  borderRadius: '6px',
417
418
  boxShadow: hoverTilt && isHovered ? '0 2px 12px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.2)',
418
419
  imageRendering: 'pixelated',
419
- transition: hoverTilt && !isHovered ? 'box-shadow 0.4s ease-out' : undefined,
420
420
  pointerEvents: 'none',
421
421
  };
422
422
  return {
@@ -551,3 +551,47 @@ export function useJamlIdeDrag(filter, onChange, rootRef) {
551
551
  onDragStart,
552
552
  };
553
553
  }
554
+ /**
555
+ * Provides a magnetic 3D tilt effect for DOM elements, replicating the 'juice' of Balatro cards.
556
+ * Ensures the hit-detection area remains stable by separating container events from the transformed style.
557
+ */
558
+ export function useDOMMagneticTilt(enabled = true) {
559
+ const [isHovered, setIsHovered] = useState(false);
560
+ const [transform, setTransform] = useState('none');
561
+ const onPointerEnter = (event) => {
562
+ if (!enabled || event.pointerType === 'touch')
563
+ return;
564
+ setIsHovered(true);
565
+ };
566
+ const onPointerLeave = () => {
567
+ if (!enabled)
568
+ return;
569
+ setIsHovered(false);
570
+ setTransform('none');
571
+ };
572
+ const onPointerMove = (event) => {
573
+ if (!enabled || event.pointerType === 'touch')
574
+ return;
575
+ const rect = event.currentTarget.getBoundingClientRect();
576
+ const x = event.clientX - rect.left;
577
+ const y = event.clientY - rect.top;
578
+ const rotateY = (x / rect.width) * 12 - 6;
579
+ const rotateX = (y / rect.height) * -16 + 8;
580
+ const juiceScale = 1.05;
581
+ const juiceY = -2; // slight move up
582
+ setTransform(`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${juiceScale}) translateY(${juiceY}px)`);
583
+ };
584
+ const handlers = {
585
+ onPointerEnter: enabled ? onPointerEnter : undefined,
586
+ onPointerLeave: enabled ? onPointerLeave : undefined,
587
+ onPointerMove: enabled ? onPointerMove : undefined,
588
+ };
589
+ const tiltStyle = {
590
+ transition: enabled && !isHovered ? 'transform 0.4s ease, box-shadow 0.4s ease-out' : 'transform 0.1s ease-out',
591
+ transform: enabled ? (isHovered ? transform : 'none') : undefined,
592
+ transformStyle: enabled ? 'preserve-3d' : undefined,
593
+ transformOrigin: enabled ? 'center center' : undefined,
594
+ willChange: enabled ? 'transform' : undefined,
595
+ };
596
+ return { handlers, tiltStyle, isHovered };
597
+ }