jaml-ui 0.16.0 → 0.17.1

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 (86) hide show
  1. package/DESIGN.md +9 -11
  2. package/dist/assets.d.ts +6 -0
  3. package/dist/assets.js +9 -0
  4. package/dist/components/AnalyzerExplorer.d.ts +4 -1
  5. package/dist/components/AnalyzerExplorer.js +14 -48
  6. package/dist/components/GameCard.js +8 -7
  7. package/dist/components/JamlAestheticSelector.d.ts +4 -0
  8. package/dist/components/JamlAestheticSelector.js +6 -19
  9. package/dist/components/JamlAnalyzerFullscreen.d.ts +7 -1
  10. package/dist/components/JamlAnalyzerFullscreen.js +18 -47
  11. package/dist/components/JamlIde.js +12 -24
  12. package/dist/components/JamlIdeVisual.js +3 -56
  13. package/dist/components/JamlMapPreview.d.ts +6 -1
  14. package/dist/components/JamlMapPreview.js +99 -21
  15. package/dist/components/JamlSeedInput.d.ts +5 -0
  16. package/dist/components/JamlSeedInput.js +11 -14
  17. package/dist/components/JamlSpeedometer.d.ts +8 -8
  18. package/dist/components/JamlSpeedometer.js +24 -46
  19. package/dist/components/MotelyVersionBadge.d.ts +1 -3
  20. package/dist/components/MotelyVersionBadge.js +4 -16
  21. package/dist/components/jamlMap/JamlMapEditorDemo.d.ts +8 -0
  22. package/dist/components/jamlMap/JamlMapEditorDemo.js +170 -0
  23. package/dist/components/jamlMap/JokerPicker.d.ts +7 -0
  24. package/dist/components/jamlMap/JokerPicker.js +258 -0
  25. package/dist/components/jamlMap/MysterySlot.d.ts +32 -0
  26. package/dist/components/jamlMap/MysterySlot.js +109 -0
  27. package/dist/components/jamlMap/index.d.ts +3 -0
  28. package/dist/components/jamlMap/index.js +3 -0
  29. package/dist/core.d.ts +0 -2
  30. package/dist/core.js +0 -2
  31. package/dist/decode/motelyItemDecoder.d.ts +10 -23
  32. package/dist/decode/motelyItemDecoder.js +103 -272
  33. package/dist/decode/motelySprite.d.ts +4 -0
  34. package/dist/decode/motelySprite.js +57 -0
  35. package/dist/hooks/analyzerStreamRegistry.js +30 -82
  36. package/dist/hooks/useAnalyzer.d.ts +10 -3
  37. package/dist/hooks/useAnalyzer.js +11 -6
  38. package/dist/hooks/useIntersectionObserver.d.ts +14 -0
  39. package/dist/hooks/useIntersectionObserver.js +50 -0
  40. package/dist/index.d.ts +5 -8
  41. package/dist/index.js +4 -7
  42. package/dist/motely.d.ts +2 -2
  43. package/dist/motely.js +2 -2
  44. package/dist/motelyDisplay.d.ts +4 -623
  45. package/dist/motelyDisplay.js +26 -165
  46. package/dist/r3f/Card3D.d.ts +2 -2
  47. package/dist/r3f/Card3D.js +13 -48
  48. package/dist/r3f/JimboText3D.js +3 -2
  49. package/dist/render/CanvasRenderer.js +7 -171
  50. package/dist/sprites/spriteMapper.d.ts +71 -0
  51. package/dist/sprites/spriteMapper.js +40 -0
  52. package/dist/ui/JimboBadge.d.ts +8 -2
  53. package/dist/ui/JimboBadge.js +6 -22
  54. package/dist/ui/JimboToggleList.js +2 -7
  55. package/dist/ui/codeBlock.js +2 -3
  56. package/dist/ui/footer.d.ts +4 -0
  57. package/dist/ui/footer.js +6 -4
  58. package/dist/ui/hooks.d.ts +89 -0
  59. package/dist/ui/hooks.js +551 -0
  60. package/dist/ui/jimboBackground.js +2 -131
  61. package/dist/ui/jimboCopyRow.d.ts +4 -0
  62. package/dist/ui/jimboCopyRow.js +5 -22
  63. package/dist/ui/jimboFilterBar.d.ts +1 -4
  64. package/dist/ui/jimboFilterBar.js +2 -61
  65. package/dist/ui/jimboFlankNav.d.ts +1 -2
  66. package/dist/ui/jimboFlankNav.js +5 -30
  67. package/dist/ui/jimboTabs.d.ts +1 -5
  68. package/dist/ui/jimboTabs.js +6 -41
  69. package/dist/ui/jimboText.d.ts +1 -1
  70. package/dist/ui/jimboText.js +15 -32
  71. package/dist/ui/jimboTooltip.d.ts +1 -12
  72. package/dist/ui/jimboTooltip.js +6 -82
  73. package/dist/ui/panel.d.ts +2 -1
  74. package/dist/ui/panel.js +11 -47
  75. package/dist/ui/showcase.d.ts +4 -0
  76. package/dist/ui/showcase.js +9 -36
  77. package/dist/ui/sprites.js +3 -2
  78. package/dist/ui.d.ts +1 -0
  79. package/dist/ui.js +2 -0
  80. package/package.json +7 -6
  81. package/dist/decode/packedBalatroItem.d.ts +0 -13
  82. package/dist/decode/packedBalatroItem.js +0 -26
  83. package/dist/hooks/loadMotelyWasm.d.ts +0 -7
  84. package/dist/hooks/loadMotelyWasm.js +0 -16
  85. package/dist/utils/itemUtils.d.ts +0 -11
  86. package/dist/utils/itemUtils.js +0 -71
package/DESIGN.md CHANGED
@@ -124,7 +124,7 @@ components:
124
124
 
125
125
  Jimbo is the design system for Balatro seed finder tools (JAML-UI, WeeJoker, Seed Finder). It recreates the cozy, tactile, chunky feel of LocalThunk's Balatro — dark panels with silver borders, 3D-press buttons, pixel typography, juice animations. Everything feels like a physical object you can poke.
126
126
 
127
- The system is built **Mobile First**. The absolute minimum viewport width is **320px**. All components must be accessible and usable at 320px without breaking layouts or horizontal scrolling. No fat padding, no bloated margins — every pixel earns its place.
127
+ The system is built **Mobile First**. The absolute minimum viewport width is **375px**. All components must be accessible and usable at 375px without breaking layouts or horizontal scrolling. No fat padding, no bloated margins — every pixel earns its place.
128
128
 
129
129
  ## Colors
130
130
 
@@ -133,12 +133,12 @@ All colors are eyedropped from Balatro's actual rendered shader output. Do NOT s
133
133
  - **Red (#ff4c40):** Primary action, mult scoring, should-clause hits. The "play" color.
134
134
  - **Blue (#0093ff):** Secondary action, chips scoring, must-clause gates. The "requirement" color.
135
135
  - **Green (#429f79):** Success, positive state, money.
136
- - **Orange (#ff9800):** Back/return actions, warning.
136
+ - **Orange (#ff9800):** Back/return actions, warning, configuration, misc.
137
137
  - **Gold (#e4b643):** Seed text, premium highlights, active tab. The "treasure" color.
138
138
  - **Purple (#9e74ce):** Joker rarity, tarot cards.
139
139
  - **Dark Grey (#3a5055):** Panel backgrounds — the primary surface.
140
140
  - **Darkest (#1e2b2d):** Deepest background, inset areas.
141
- - **Grey (#708386):** Disabled text, labels, inactive elements.
141
+ - **Grey (#708386):** Disabled text, labels.
142
142
  - **Border Silver (#b9c2d2):** Panel top/side borders — the "silver frame."
143
143
  - **Border South (#777e89):** Panel bottom border — creates the 3D depth illusion.
144
144
  - **Panel Edge (#1e2e32):** Thin outer edge on panels.
@@ -153,13 +153,13 @@ All text is uppercase with generous letter-spacing (0.04em-0.1em) for labels and
153
153
 
154
154
  ## Layout
155
155
 
156
- Target: Minimum 320px portrait width. Components must scale gracefully using relative units and flexible layouts. Avoid fixed widths that break at 320px. Vertical snap-scroll for ante pages. Horizontal swipe for seed navigation.
156
+ Target: Minimum 375px portrait width. Components must scale gracefully using relative units and flexible layouts. Avoid fixed widths that break at 375px. Vertical snap-scroll for ante pages. Horizontal swipe for seed navigation.
157
157
 
158
158
  Panels use 2px solid borders with border-silver on top/sides and border-south on bottom, creating a subtle 3D card effect. Inner shadow: `inset 0 0 0 1px rgba(255,255,255,0.04)`. Outer shadow: `0 2px 0 #000`.
159
159
 
160
160
  ## Elevation & Depth
161
161
 
162
- Buttons have a colored "underside" via box-shadow (not blur). On press, translateY increases by 2-3px and the shadow collapses — the button physically sinks. On hover, translateY decreases by 2px (lifts) with a tiny brightness bump.
162
+ Buttons have a colored "underside" via box-shadow (not blur). On press, translateY increases by 2-3px and the shadow collapses — the button physically sinks. On hover, apply a tiny brightness bump (no lift).
163
163
 
164
164
  Panels sit on a dark south-shadow (`0 3px 0 rgba(0,0,0,0.55)`). Translucent panels (for swirl-background contexts) use `rgba(15, 24, 26, 0.78)` with `backdrop-filter: blur(2px)`.
165
165
 
@@ -167,7 +167,7 @@ JAML-hit items get a GlowRing: `box-shadow: 0 0 0 2px [color], 0 0 10px [color]`
167
167
 
168
168
  ## Components
169
169
 
170
- **Button:** Chunky 3D press. Colored underside via box-shadow. Hover lifts -2px + brightness. Press sinks +2-3px + shadow collapse. Variants: primary (red), secondary (blue), back (orange), ghost (transparent). Sizes via padding, not font-size. Easing: `cubic-bezier(0.34, 1.56, 0.64, 1)`.
170
+ **Button:** Chunky 3D press. Colored underside via box-shadow. Hover brightness bump. Press sinks +2-3px + shadow collapse. Variants: primary (red), secondary (blue), back (orange). Sizes via padding, not font-size. Easing: `cubic-bezier(0.34, 1.56, 0.64, 1)`.
171
171
 
172
172
  **Panel:** Dark grey (#3a5055) background, 2px solid border (silver top/sides, south bottom), border-radius 6px. Inner highlight: `inset 0 0 0 1px rgba(255,255,255,0.04)`. Drop: `0 2px 0 #000`.
173
173
 
@@ -185,13 +185,11 @@ JAML-hit items get a GlowRing: `box-shadow: 0 0 0 2px [color], 0 0 10px [color]`
185
185
 
186
186
  ## Do's and Don'ts
187
187
 
188
- - DO use m6x11plus for everything. No fallback display fonts.
189
- - DO eyedrop colors from the game. Never guess or approximate.
190
- - DO design for 320px portrait first. Desktop is an expanded view of the mobile baseline.
188
+ - DO use m6x11plus for everything except code/monospace.
189
+ - DO design for 375px portrait.
191
190
  - DO use translateY + box-shadow for button depth. Not CSS 3D transforms.
192
- - DO dim non-matching items (opacity 0.4 + grayscale 0.6). They stay visible for context.
193
191
  - DON'T use font-weight bold. m6x11plus is single-weight. Bold = muddy.
194
192
  - DON'T use fat padding or margins. Balatro UI is dense and cozy.
195
193
  - DON'T add horizontal scroll. Vertical snap-scroll + horizontal swipe only.
196
194
  - DON'T use rounded corners larger than 10px. Balatro is chunky, not bubbly.
197
- - DON'T use blur-based shadows for depth. Use solid colored box-shadows.
195
+ - DON'T use blur-based shadows for depth. Use solid colored box-shadows 80% opaque.
package/dist/assets.d.ts CHANGED
@@ -10,6 +10,7 @@ export declare const JAML_ASSET_FILES: {
10
10
  readonly stickers: "stickers.png";
11
11
  readonly tags: "tags.png";
12
12
  readonly stakes: "balatro-stake-chips.png";
13
+ readonly font: "fonts/m6x11plusplus.otf";
13
14
  };
14
15
  export type JamlAssetKey = keyof typeof JAML_ASSET_FILES;
15
16
  export type JamlAssetFile = (typeof JAML_ASSET_FILES)[JamlAssetKey];
@@ -17,3 +18,8 @@ export declare function setJamlAssetBaseUrl(baseUrl: string | null | undefined):
17
18
  export declare function clearJamlAssetBaseUrl(): void;
18
19
  export declare function resolveJamlAssetUrl(asset: JamlAssetKey | JamlAssetFile): string;
19
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
@@ -10,6 +10,7 @@ export const JAML_ASSET_FILES = {
10
10
  stickers: "stickers.png",
11
11
  tags: "tags.png",
12
12
  stakes: "balatro-stake-chips.png",
13
+ font: "fonts/m6x11plusplus.otf",
13
14
  };
14
15
  const assetKeyByFileName = Object.fromEntries(Object.entries(JAML_ASSET_FILES).map(([key, fileName]) => [fileName, key]));
15
16
  const defaultAssetUrls = {
@@ -24,6 +25,7 @@ const defaultAssetUrls = {
24
25
  stickers: new URL(`../assets/${JAML_ASSET_FILES.stickers}`, import.meta.url).href,
25
26
  tags: new URL(`../assets/${JAML_ASSET_FILES.tags}`, import.meta.url).href,
26
27
  stakes: new URL(`../assets/${JAML_ASSET_FILES.stakes}`, import.meta.url).href,
28
+ font: new URL(`../assets/${JAML_ASSET_FILES.font}`, import.meta.url).href,
27
29
  };
28
30
  let customAssetBaseUrl = null;
29
31
  function normalizeBaseUrl(baseUrl) {
@@ -66,3 +68,10 @@ export function resolveJamlAssetUrl(asset) {
66
68
  export function getDefaultJamlAssetUrlMap() {
67
69
  return defaultAssetUrls;
68
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
+ }
@@ -40,5 +40,8 @@ export interface AnalyzerExplorerProps {
40
40
  visibleAntes?: number;
41
41
  totalAntes?: number;
42
42
  className?: string;
43
+ jaml?: string;
44
+ tallyColumns?: number[][];
45
+ tallyLabels?: string[];
43
46
  }
44
- export declare function AnalyzerExplorer({ antes, highlights, visibleAntes, totalAntes, className, }: AnalyzerExplorerProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function AnalyzerExplorer({ antes, highlights, visibleAntes, totalAntes, className, jaml, tallyColumns, tallyLabels, }: AnalyzerExplorerProps): import("react/jsx-runtime").JSX.Element;
@@ -1,40 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useEffect, useRef } from "react";
4
+ import { useAnteTracker } from "../ui/hooks.js";
4
5
  import { JamlBoss, JamlGameCard, JamlTag, JamlVoucher, resolveAnalyzerShopItem, } from "./GameCard.js";
5
- export function AnalyzerExplorer({ antes, highlights = [], visibleAntes, totalAntes, className = "", }) {
6
- const [currentAnte, setCurrentAnte] = useState(antes[0]?.ante ?? 0);
7
- const scrollRef = useRef(null);
8
- const anteRefs = useRef(new Map());
6
+ import { JamlMapPreview } from "./JamlMapPreview.js";
7
+ export function AnalyzerExplorer({ antes, highlights = [], visibleAntes, totalAntes, className = "", jaml, tallyColumns, tallyLabels, }) {
8
+ const { currentAnte, scrollRef, scrollToAnte, registerAnteRef } = useAnteTracker(antes, {
9
+ threshold: [0.45, 0.72, 0.9],
10
+ });
9
11
  const highlightRefs = useRef(new Map());
10
- useEffect(() => {
11
- setCurrentAnte(antes[0]?.ante ?? 0);
12
- }, [antes]);
13
- useEffect(() => {
14
- const root = scrollRef.current;
15
- if (!root) {
16
- return;
17
- }
18
- const observer = new IntersectionObserver((entries) => {
19
- const mostVisibleEntry = entries
20
- .filter((entry) => entry.isIntersecting)
21
- .sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
22
- if (!mostVisibleEntry) {
23
- return;
24
- }
25
- const ante = Number(mostVisibleEntry.target.dataset.ante);
26
- if (!Number.isNaN(ante)) {
27
- setCurrentAnte(ante);
28
- }
29
- }, {
30
- root,
31
- threshold: [0.45, 0.72, 0.9],
32
- });
33
- for (const [, element] of anteRefs.current) {
34
- observer.observe(element);
35
- }
36
- return () => observer.disconnect();
37
- }, [antes]);
38
12
  useEffect(() => {
39
13
  const activeHighlight = highlights.find((highlight) => highlight.ante === currentAnte);
40
14
  if (!activeHighlight) {
@@ -47,18 +21,12 @@ export function AnalyzerExplorer({ antes, highlights = [], visibleAntes, totalAn
47
21
  block: "nearest",
48
22
  });
49
23
  }, [currentAnte, highlights]);
50
- const scrollToAnte = useCallback((ante) => {
51
- anteRefs.current.get(ante)?.scrollIntoView({
52
- behavior: "smooth",
53
- block: "start",
54
- });
55
- }, []);
56
24
  const currentAnteIndex = antes.findIndex((ante) => ante.ante === currentAnte);
57
25
  const previousAnte = currentAnteIndex > 0 ? antes[currentAnteIndex - 1]?.ante ?? null : null;
58
26
  const nextAnte = currentAnteIndex >= 0 && currentAnteIndex < antes.length - 1 ? antes[currentAnteIndex + 1]?.ante ?? null : null;
59
27
  const shownAntes = visibleAntes ?? antes.length;
60
28
  const availableAntes = totalAntes ?? shownAntes;
61
- return (_jsxs("div", { className: className, style: styles.root, children: [highlights.length > 0 ? (_jsxs("section", { style: styles.highlightSection, children: [_jsxs("div", { style: styles.highlightHeader, children: [_jsx("span", { style: styles.highlightTitle, children: "Highlights" }), _jsx("span", { style: styles.highlightSubtitle, children: "Swipe, tap, jump" })] }), _jsx("div", { style: styles.highlightRail, children: highlights.map((highlight) => {
29
+ return (_jsxs("div", { className: className, style: styles.root, children: [jaml ? (_jsxs("section", { style: styles.jamlSection, children: [_jsx("div", { style: styles.highlightHeader, children: _jsx("span", { style: styles.highlightTitle, children: "JAML Map" }) }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns?.[0], tallyLabels: tallyLabels, compact: true })] })) : null, highlights.length > 0 ? (_jsxs("section", { style: styles.highlightSection, children: [_jsxs("div", { style: styles.highlightHeader, children: [_jsx("span", { style: styles.highlightTitle, children: "Highlights" }), _jsx("span", { style: styles.highlightSubtitle, children: "Swipe, tap, jump" })] }), _jsx("div", { style: styles.highlightRail, children: highlights.map((highlight) => {
62
30
  const isActive = highlight.ante === currentAnte;
63
31
  return (_jsxs("button", { ref: (element) => {
64
32
  if (element) {
@@ -74,14 +42,7 @@ export function AnalyzerExplorer({ antes, highlights = [], visibleAntes, totalAn
74
42
  }, children: "\u25B2" }), _jsxs("div", { style: styles.navLabel, children: ["Ante ", currentAnte, _jsxs("span", { style: styles.navSubLabel, children: ["of ", shownAntes, availableAntes > shownAntes ? ` / ${availableAntes}` : ""] })] }), _jsx("button", { type: "button", onClick: () => nextAnte !== null && scrollToAnte(nextAnte), disabled: nextAnte === null, style: {
75
43
  ...styles.navButton,
76
44
  opacity: nextAnte !== null ? 1 : 0.25,
77
- }, children: "\u25BC" })] }), _jsx("div", { ref: scrollRef, style: styles.scrollRegion, children: antes.map((ante) => (_jsx("div", { "data-ante": ante.ante, ref: (element) => {
78
- if (element) {
79
- anteRefs.current.set(ante.ante, element);
80
- }
81
- else {
82
- anteRefs.current.delete(ante.ante);
83
- }
84
- }, style: styles.antePage, children: _jsx(AnteSection, { ante: ante }) }, ante.ante))) })] }));
45
+ }, children: "\u25BC" })] }), _jsx("div", { ref: scrollRef, style: styles.scrollRegion, children: antes.map((ante) => (_jsx("div", { "data-ante": ante.ante, ref: (element) => registerAnteRef(ante.ante, element), style: styles.antePage, children: _jsx(AnteSection, { ante: ante }) }, ante.ante))) })] }));
85
46
  }
86
47
  function AnteSection({ ante }) {
87
48
  return (_jsxs("section", { style: styles.anteSection, children: [_jsxs("div", { style: styles.anteHeader, children: [_jsxs("span", { style: styles.anteHeading, children: ["Ante ", ante.ante] }), ante.boss ? (_jsxs("div", { style: styles.bossRow, children: [_jsx(JamlBoss, { bossName: ante.boss, scale: 0.62 }), _jsx("span", { style: styles.bossName, children: ante.boss })] })) : null] }), ante.smallBlindTag || ante.bigBlindTag ? (_jsxs(AnalyzerRow, { label: "Tags", children: [ante.smallBlindTag ? (_jsx(CompactCard, { label: "Small", visual: _jsx(JamlTag, { tagName: ante.smallBlindTag, scale: 0.58, hoverTilt: true }), text: ante.smallBlindTag })) : null, ante.bigBlindTag ? (_jsx(CompactCard, { label: "Big", visual: _jsx(JamlTag, { tagName: ante.bigBlindTag, scale: 0.58, hoverTilt: true }), text: ante.bigBlindTag })) : null] })) : null, ante.voucher ? (_jsx(AnalyzerRow, { label: "Voucher", children: _jsx(CompactCard, { visual: _jsx(JamlVoucher, { voucherName: ante.voucher, scale: 0.58, hoverTilt: true }), text: ante.voucher }) })) : null, ante.shop && ante.shop.length > 0 ? (_jsx(AnalyzerRow, { label: "Shop", dense: true, children: ante.shop.map((item) => (_jsx(ResolvedItemCard, { item: item }, `${ante.ante}-${item.id}-${item.name}`))) })) : null, ante.packs && ante.packs.length > 0 ? (_jsx(AnalyzerRow, { label: "Packs", children: ante.packs.map((pack) => (_jsx("div", { style: styles.packChip, children: pack }, `${ante.ante}-${pack}`))) })) : null, ante.facts && ante.facts.length > 0 ? (_jsx(AnalyzerRow, { label: "Facts", children: ante.facts.map((fact) => (_jsxs("div", { style: styles.factCard, children: [_jsx("span", { style: styles.factLabel, children: fact.label }), _jsx("span", { style: styles.factValue, children: fact.value })] }, `${ante.ante}-${fact.label}-${fact.value}`))) })) : null] }));
@@ -147,6 +108,11 @@ const styles = {
147
108
  borderBottom: "1px solid #1a1a34",
148
109
  background: "#0f0f22",
149
110
  },
111
+ jamlSection: {
112
+ padding: "6px 8px",
113
+ borderBottom: "1px solid #1a1a34",
114
+ background: "#0f0f22",
115
+ },
150
116
  highlightHeader: {
151
117
  display: "flex",
152
118
  alignItems: "center",
@@ -3,7 +3,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Layer } from "../render/Layer.js";
4
4
  import { JamlCardRenderer } from "../render/CanvasRenderer.js";
5
5
  import { JOKERS, JOKER_FACES, TAROTS_AND_PLANETS, CONSUMABLE_FACES, TAGS, VOUCHERS, BOSSES, EDITION_MAP, SPRITE_SHEETS, STICKER_MAP, RANK_MAP, SUIT_MAP, ENHANCER_MAP, SEAL_MAP, } from "../sprites/spriteData.js";
6
- import { BalatroItemCategory, isPackedItemValid, packedItemCategory } from "../decode/packedBalatroItem.js";
7
6
  function normalizeCardRank(raw) {
8
7
  const value = raw.trim().toUpperCase();
9
8
  if (value === "A" || value === "ACE")
@@ -106,21 +105,23 @@ function stripModifiers(name) {
106
105
  return { baseName: remaining, edition, isEternal, isPerishable, isRental };
107
106
  }
108
107
  function resolvePackedAnalyzerItem(item, scale) {
109
- if (typeof item.value !== "number" || !Number.isFinite(item.value) || !isPackedItemValid(item.value)) {
108
+ if (typeof item.value !== "number" || !Number.isFinite(item.value)) {
110
109
  return null;
111
110
  }
112
111
  const displayName = String(item.name || "").trim();
113
112
  const { baseName, edition, isEternal, isPerishable, isRental } = stripModifiers(displayName);
114
- const category = packedItemCategory(item.value);
115
- if (category === BalatroItemCategory.Joker) {
113
+ // Use motely-wasm enum to determine category no hand-rolled bitmasks
114
+ const itemType = item.value & 0xffff;
115
+ const catNibble = (itemType >> 12) & 0xf;
116
+ if (catNibble === 5 /* Joker */) {
116
117
  const jokerName = JOKERS.some((joker) => joker.name === baseName) ? baseName : displayName;
117
118
  if (JOKERS.some((joker) => joker.name === jokerName)) {
118
119
  return { kind: "joker", type: "joker", card: { name: jokerName, edition, isEternal, isPerishable, isRental, scale } };
119
120
  }
120
121
  }
121
- if (category === BalatroItemCategory.Tarot ||
122
- category === BalatroItemCategory.Planet ||
123
- category === BalatroItemCategory.Spectral) {
122
+ if (catNibble === 3 /* Tarot */ ||
123
+ catNibble === 4 /* Planet */ ||
124
+ catNibble === 2 /* Spectral */) {
124
125
  const consumableName = TAROTS_AND_PLANETS.some((consumable) => consumable.name === baseName) ? baseName : displayName;
125
126
  if (TAROTS_AND_PLANETS.some((consumable) => consumable.name === consumableName)) {
126
127
  return { kind: "consumable", type: "consumable", card: { name: consumableName, edition, scale } };
@@ -6,4 +6,8 @@ export interface JamlAestheticSelectorProps {
6
6
  className?: string;
7
7
  style?: React.CSSProperties;
8
8
  }
9
+ /**
10
+ * Pill-toggle selector for seed aesthetic filters.
11
+ * All styling via jimbo.css `.j-aesthetic-selector` / `.j-aesthetic-pill` — zero inline styles.
12
+ */
9
13
  export declare function JamlAestheticSelector({ value, onChange, className, style }: JamlAestheticSelectorProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { JimboColorOption } from "../ui/tokens.js";
4
3
  import { JimboText } from "../ui/jimboText.js";
5
4
  const AESTHETICS = [
6
5
  { id: "Palindrome", value: 0, label: "Palindrome", desc: "Seeds that read the same forwards and backwards" },
@@ -10,25 +9,13 @@ const AESTHETICS = [
10
9
  { id: "Funny", value: 4, label: "Funny", desc: "Seeds that spell funny words" },
11
10
  { id: "Balatro", value: 5, label: "Balatro", desc: "Seeds referencing the game itself" },
12
11
  ];
12
+ /**
13
+ * Pill-toggle selector for seed aesthetic filters.
14
+ * All styling via jimbo.css `.j-aesthetic-selector` / `.j-aesthetic-pill` — zero inline styles.
15
+ */
13
16
  export function JamlAestheticSelector({ value, onChange, className, style }) {
14
- return (_jsxs("div", { className: className, style: {
15
- display: "flex",
16
- flexDirection: "column",
17
- gap: 4,
18
- ...style,
19
- }, children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "Seed Aesthetics" }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: AESTHETICS.map((a) => {
17
+ return (_jsxs("div", { className: `j-aesthetic-selector ${className ?? ""}`, style: style, children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "Seed Aesthetics" }), _jsx("div", { className: "j-aesthetic-selector__list", children: AESTHETICS.map((a) => {
20
18
  const isActive = value === a.id;
21
- return (_jsx("button", { type: "button", onClick: () => onChange(isActive ? null : a.id, a.value), title: a.desc, style: {
22
- padding: "4px 10px",
23
- borderRadius: 6,
24
- border: `2px solid ${isActive ? JimboColorOption.GOLD : JimboColorOption.PANEL_EDGE}`,
25
- background: isActive ? `${JimboColorOption.GOLD}22` : JimboColorOption.DARKEST,
26
- color: isActive ? JimboColorOption.GOLD_TEXT : JimboColorOption.GREY,
27
- cursor: "pointer",
28
- fontSize: 11,
29
- fontFamily: "m6x11plus, monospace",
30
- letterSpacing: 0.5,
31
- transition: "border-color 100ms, background 100ms",
32
- }, children: a.label }, a.id));
19
+ return (_jsx("button", { type: "button", className: "j-aesthetic-pill", "data-active": isActive, onClick: () => onChange(isActive ? null : a.id, a.value), title: a.desc, children: a.label }, a.id));
33
20
  }) })] }));
34
21
  }
@@ -6,6 +6,12 @@ export interface JamlAnalyzerFullscreenProps {
6
6
  antes: AnalyzerAnteView[];
7
7
  /** Live ctx from useAnalyzer.live; null disables additional stream lanes. */
8
8
  live: AnalyzerLive | null;
9
+ /** JAML string for visual breakdown. */
10
+ jaml?: string;
11
+ /** Tally column data for JAML map highlighting. */
12
+ tallyColumns?: number[];
13
+ /** Tally labels mapping to columns. */
14
+ tallyLabels?: string[];
9
15
  /** Stream lanes to surface. Defaults to shop + soul jokers. */
10
16
  enabledStreams?: AnalyzerStreamKey[];
11
17
  /** Called when the user toggles a stream in the picker. Owners persist if desired. */
@@ -16,6 +22,6 @@ export interface JamlAnalyzerFullscreenProps {
16
22
  chunkSize?: number;
17
23
  className?: string;
18
24
  }
19
- export declare function JamlAnalyzerFullscreen({ antes, live, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
25
+ export declare function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
20
26
  export type { AnalyzerItem };
21
27
  export { ANALYZER_STREAM_META, type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { useCallback, useMemo, useRef, useState } from "react";
4
4
  import { JamlBoss, JamlGameCard, JamlTag, JamlVoucher, resolveAnalyzerShopItem } from "./GameCard.js";
5
5
  import { useMotelyStream } from "../hooks/useShopStream.js";
6
+ import { useInfiniteScroll } from "../hooks/useIntersectionObserver.js";
7
+ import { useAnteTracker } from "../ui/hooks.js";
8
+ import { JimboText } from "../ui/jimboText.js";
6
9
  import { ANALYZER_STREAM_META, DEFAULT_ENABLED_STREAMS, buildStreamHandle, } from "../hooks/analyzerStreamRegistry.js";
7
10
  import { JimboColorOption, withAlpha } from "../ui/tokens.js";
8
11
  const C = JimboColorOption;
@@ -13,43 +16,17 @@ const TONE_COLORS = {
13
16
  spectral: C.SPECTRAL_BUTTON,
14
17
  default: C.GOLD_TEXT,
15
18
  };
16
- export function JamlAnalyzerFullscreen({ antes, live, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", }) {
19
+ import { JamlMapPreview } from "./JamlMapPreview.js";
20
+ export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", }) {
17
21
  const [internalEnabled, setInternalEnabled] = useState(enabledStreams ?? DEFAULT_ENABLED_STREAMS);
18
22
  const effectiveEnabled = enabledStreams ?? internalEnabled;
19
23
  const setEnabled = useCallback((next) => {
20
24
  setInternalEnabled(next);
21
25
  onEnabledStreamsChange?.(next);
22
26
  }, [onEnabledStreamsChange]);
23
- const scrollRef = useRef(null);
24
- const sectionRefs = useRef(new Map());
25
- const [currentAnte, setCurrentAnte] = useState(antes[0]?.ante ?? 1);
27
+ const { currentAnte, scrollRef, scrollToAnte, registerAnteRef } = useAnteTracker(antes);
26
28
  const [pickerOpen, setPickerOpen] = useState(false);
27
- useEffect(() => {
28
- const root = scrollRef.current;
29
- if (!root)
30
- return;
31
- const observer = new IntersectionObserver((entries) => {
32
- const top = entries
33
- .filter((e) => e.isIntersecting)
34
- .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
35
- if (!top)
36
- return;
37
- const ante = Number(top.target.dataset.ante);
38
- if (!Number.isNaN(ante))
39
- setCurrentAnte(ante);
40
- }, { root, threshold: [0.4, 0.6, 0.8] });
41
- sectionRefs.current.forEach((el) => observer.observe(el));
42
- return () => observer.disconnect();
43
- }, [antes]);
44
- const scrollToAnte = useCallback((ante) => {
45
- sectionRefs.current.get(ante)?.scrollIntoView({ behavior: "smooth", block: "start" });
46
- }, []);
47
- return (_jsxs("div", { className: className, style: styles.root, children: [_jsx("div", { ref: scrollRef, style: styles.scroller, children: antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => {
48
- if (el)
49
- sectionRefs.current.set(ante.ante, el);
50
- else
51
- sectionRefs.current.delete(ante.ante);
52
- } }, ante.ante))) }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
29
+ return (_jsxs("div", { className: className, style: styles.root, children: [_jsxs("div", { ref: scrollRef, style: styles.scroller, children: [jaml && (_jsxs("section", { style: { ...styles.section, scrollSnapAlign: "start", justifyContent: 'center' }, children: [_jsxs("div", { style: { marginBottom: 20 }, children: [_jsx("div", { style: styles.anteLabel, children: "JAML" }), _jsx("div", { style: styles.anteNumber, children: "MAP" })] }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns, tallyLabels: tallyLabels }), _jsx("div", { style: { marginTop: 24, textAlign: 'center', opacity: 0.6 }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Scroll down to explore seed details" }) })] })), antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => registerAnteRef(ante.ante, el) }, ante.ante)))] }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
53
30
  }
54
31
  function AnteSection({ ante, live, enabledStreams, chunkSize, registerRef }) {
55
32
  return (_jsxs("section", { ref: registerRef, "data-ante": ante.ante, style: styles.section, children: [_jsxs("header", { style: styles.header, children: [_jsxs("div", { children: [_jsx("div", { style: styles.anteLabel, children: "Ante" }), _jsx("div", { style: styles.anteNumber, children: ante.ante })] }), ante.voucher && (_jsxs("div", { style: styles.voucherBlock, children: [_jsx(JamlVoucher, { voucherName: ante.voucher, scale: 0.85 }), _jsx("div", { style: styles.voucherCaption, children: ante.voucher })] }))] }), _jsxs("div", { style: styles.blindRow, children: [_jsx(BlindCell, { label: "Small", tag: ante.smallBlindTag }), _jsx(BlindCell, { label: "Big", tag: ante.bigBlindTag }), ante.boss && (_jsxs("div", { style: styles.bossCell, children: [_jsx("div", { style: styles.cellLabel, children: "Boss" }), _jsx(JamlBoss, { bossName: ante.boss, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: ante.boss })] }))] }), ante.packs && ante.packs.length > 0 && (_jsxs("div", { style: styles.streamLane, children: [_jsx("div", { style: styles.streamLabel, children: "Packs" }), _jsx("div", { style: styles.packRow, children: ante.packs.map((pack, i) => (_jsx("div", { style: styles.packPill, children: pack }, `${ante.ante}-pack-${i}`))) })] })), enabledStreams.map((key) => {
@@ -78,24 +55,18 @@ function BlindCell({ label, tag }) {
78
55
  return (_jsxs("div", { style: styles.blindCell, children: [_jsx("div", { style: styles.cellLabel, children: label }), _jsx(JamlTag, { tagName: tag, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: tag })] }));
79
56
  }
80
57
  function ShopRow({ items, desired, loadingMore, ready, onPullMore }) {
81
- const sentinelRef = useRef(null);
82
58
  const lastTriggerRef = useRef(0);
83
- useEffect(() => {
84
- const el = sentinelRef.current;
85
- if (!el || !ready)
59
+ const throttlePull = useCallback(() => {
60
+ const now = Date.now();
61
+ if (now - lastTriggerRef.current < 200)
86
62
  return;
87
- const observer = new IntersectionObserver((entries) => {
88
- if (entries[0]?.isIntersecting && !loadingMore) {
89
- const now = Date.now();
90
- if (now - lastTriggerRef.current < 200)
91
- return;
92
- lastTriggerRef.current = now;
93
- onPullMore();
94
- }
95
- }, { root: el.parentElement, threshold: 0.1, rootMargin: "0px 200px 0px 0px" });
96
- observer.observe(el);
97
- return () => observer.disconnect();
98
- }, [ready, loadingMore, onPullMore]);
63
+ lastTriggerRef.current = now;
64
+ onPullMore();
65
+ }, [onPullMore]);
66
+ const sentinelRef = useInfiniteScroll(throttlePull, {
67
+ threshold: 0.1,
68
+ rootMargin: "0px 200px 0px 0px",
69
+ }, ready && !loadingMore);
99
70
  return (_jsxs("div", { style: styles.shopRow, children: [items.map((item) => (_jsx(ShopItem, { item: item, desired: desired.has(item.name.toLowerCase()) }, item.id))), _jsx("div", { ref: sentinelRef, style: styles.sentinel, children: loadingMore ? "…" : "" })] }));
100
71
  }
101
72
  function ShopItem({ item, desired }) {
@@ -17,7 +17,7 @@ function TallyBar({ value, max }) {
17
17
  transition: "width 200ms ease",
18
18
  } }) }));
19
19
  }
20
- function ResultsView({ results }) {
20
+ function ResultsView({ results, jaml }) {
21
21
  const [expanded, setExpanded] = useState(null);
22
22
  if (results.length === 0) {
23
23
  return (_jsx("div", { style: {
@@ -30,11 +30,10 @@ function ResultsView({ results }) {
30
30
  textAlign: "center",
31
31
  }, children: "No results yet. Run a search to find seeds." }));
32
32
  }
33
- const labels = results[0]?.tallyLabels ?? [];
34
33
  const maxScore = Math.max(...results.map((r) => r.score ?? 0));
35
34
  return (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: results.map((result) => {
36
35
  const isOpen = expanded === result.seed;
37
- const hasTally = result.tallyColumns && result.tallyColumns.length > 0 && labels.length > 0;
36
+ const hasTally = result.tallyColumns && result.tallyColumns.length > 0;
38
37
  return (_jsxs("div", { style: {
39
38
  borderRadius: 10,
40
39
  border: `1px solid ${isOpen ? JimboColorOption.GOLD + "55" : JimboColorOption.PANEL_EDGE}`,
@@ -64,29 +63,18 @@ function ResultsView({ results }) {
64
63
  color: result.score > 0 ? JimboColorOption.GREEN_TEXT : JimboColorOption.GREY,
65
64
  minWidth: 36,
66
65
  textAlign: "right",
67
- }, children: result.score })] })) : null, hasTally ? (_jsx("span", { style: { fontSize: 11, color: JimboColorOption.GREY, marginLeft: 2 }, children: isOpen ? "▲" : "▼" })) : null] }), isOpen && hasTally ? (_jsx("div", { style: {
66
+ }, children: result.score })] })) : null, hasTally ? (_jsx("span", { style: { fontSize: 11, color: JimboColorOption.GREY, marginLeft: 2 }, children: isOpen ? "▲" : "▼" })) : null] }), isOpen && hasTally ? (_jsxs("div", { style: {
68
67
  borderTop: `1px solid ${JimboColorOption.PANEL_EDGE}`,
69
- padding: "8px 12px 10px",
68
+ padding: "4px",
70
69
  display: "flex",
71
70
  flexDirection: "column",
72
- gap: 5,
73
- }, children: labels.map((label, i) => {
74
- const val = result.tallyColumns[i] ?? 0;
75
- const maxVal = Math.max(...results.map((r) => r.tallyColumns?.[i] ?? 0));
76
- return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: {
77
- fontSize: 11,
78
- color: val > 0 ? JimboColorOption.WHITE : JimboColorOption.GREY,
79
- minWidth: 140,
80
- overflow: "hidden",
81
- textOverflow: "ellipsis",
82
- whiteSpace: "nowrap",
83
- }, children: label }), _jsx(TallyBar, { value: val, max: maxVal }), _jsx("span", { style: {
84
- fontSize: 11,
85
- color: val > 0 ? JimboColorOption.GREEN_TEXT : JimboColorOption.DARK_GREY,
86
- minWidth: 24,
87
- textAlign: "right",
88
- }, children: val })] }, label));
89
- }) })) : null] }, result.seed));
71
+ gap: 8,
72
+ }, children: [_jsx(JamlMapPreview, { jaml: jaml, tallyColumns: result.tallyColumns, tallyLabels: result.tallyLabels }), _jsxs("div", { style: { padding: "4px 8px 8px", display: "flex", flexDirection: "column", gap: 5 }, children: [_jsx("span", { style: { fontSize: 8, color: JimboColorOption.GREY, letterSpacing: "0.08em", textTransform: "uppercase" }, children: "RAW TALLY DATA" }), (result.tallyLabels ?? []).map((label, i) => {
73
+ const val = result.tallyColumns[i] ?? 0;
74
+ if (val === 0)
75
+ return null;
76
+ return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: { fontSize: 10, color: JimboColorOption.WHITE, flex: 1 }, children: label }), _jsx("span", { style: { fontSize: 10, color: JimboColorOption.GREEN_TEXT }, children: val })] }, label));
77
+ })] })] })) : null] }, result.seed));
90
78
  }) }));
91
79
  }
92
80
  export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", searchResults = [], className = "", style, title = "JAML IDE", actions, codePlaceholder = "Enter JAML...", onSearch, isSearching = false, visualFilter, onVisualFilterChange, }) {
@@ -159,5 +147,5 @@ export function JamlIde({ jaml, defaultJaml, onChange, defaultMode = "code", sea
159
147
  padding: "10px 14px",
160
148
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
161
149
  background: JimboColorOption.TEAL_GREY,
162
- }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results }) })) : null] })] }));
150
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: "normal", fontFamily: "m6x11plus, monospace", color: JimboColorOption.GOLD_TEXT }, children: title }), _jsx("div", { style: { fontSize: 11, color: JimboColorOption.GREY }, children: "Jimbo's Ante Markup Language" })] }), actions ? _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: actions }) : null] }), _jsx(JamlIdeToolbar, { mode: mode, onModeChange: setMode, resultCount: results.length, onSearch: onSearch, isSearching: isSearching }), _jsxs("div", { style: { flex: 1, minHeight: 0, overflow: "auto", background: JimboColorOption.DARKEST }, children: [mode === "visual" ? (_jsx(JamlIdeVisual, { filter: activeFilter, onChange: handleVisualFilterChange })) : null, mode === "code" ? (_jsx(JamlCodeEditor, { value: text, onChange: handleTextChange, placeholder: codePlaceholder })) : null, mode === "map" ? _jsx(JamlMapPreview, { jaml: text }) : null, mode === "results" ? (_jsx("div", { style: { padding: 12 }, children: _jsx(ResultsView, { results: results, jaml: text }) })) : null] })] }));
163
151
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useRef } from "react";
4
+ import { useJamlIdeDrag } from "../ui/hooks.js";
4
5
  import { JimboColorOption } from "../ui/tokens.js";
5
6
  import { JimboSprite } from "../ui/sprites.js";
6
7
  const C = JimboColorOption;
@@ -192,65 +193,11 @@ function TopMatter({ filter, onChange, }) {
192
193
  } })] }));
193
194
  }
194
195
  export function JamlIdeVisual({ filter, onChange, onEditClause, onAddClause }) {
195
- const [drag, setDrag] = useState(null);
196
- const [hoverZone, setHoverZone] = useState(null);
197
196
  const rootRef = useRef(null);
197
+ const { drag, hoverZone, onDragStart } = useJamlIdeDrag(filter, onChange, rootRef);
198
198
  const removeClause = (zone, id) => {
199
199
  onChange({ ...filter, [zone]: filter[zone].filter((c) => c.id !== id) });
200
200
  };
201
- const onDragStart = (e, clause, fromZone) => {
202
- // Don't preventDefault; we want clicks to still fire if there's no actual drag.
203
- const t = "touches" in e ? e.touches[0] : e;
204
- const rect = e.currentTarget.getBoundingClientRect();
205
- setDrag({
206
- clause,
207
- fromZone,
208
- x: t.clientX,
209
- y: t.clientY,
210
- offX: t.clientX - rect.left,
211
- offY: t.clientY - rect.top,
212
- });
213
- };
214
- useEffect(() => {
215
- if (!drag)
216
- return;
217
- const move = (e) => {
218
- const t = "touches" in e ? e.touches[0] : e;
219
- setDrag((d) => d && { ...d, x: t.clientX, y: t.clientY });
220
- const rails = rootRef.current?.querySelectorAll("[data-zone]") ?? [];
221
- let found = null;
222
- for (const r of rails) {
223
- const rc = r.getBoundingClientRect();
224
- if (t.clientX >= rc.left && t.clientX <= rc.right && t.clientY >= rc.top && t.clientY <= rc.bottom) {
225
- found = r.getAttribute("data-zone");
226
- break;
227
- }
228
- }
229
- setHoverZone(found);
230
- };
231
- const up = () => {
232
- if (hoverZone && hoverZone !== drag.fromZone) {
233
- const to = hoverZone;
234
- onChange({
235
- ...filter,
236
- [drag.fromZone]: filter[drag.fromZone].filter((c) => c.id !== drag.clause.id),
237
- [to]: [...filter[to], { ...drag.clause }],
238
- });
239
- }
240
- setDrag(null);
241
- setHoverZone(null);
242
- };
243
- window.addEventListener("mousemove", move);
244
- window.addEventListener("mouseup", up);
245
- window.addEventListener("touchmove", move, { passive: false });
246
- window.addEventListener("touchend", up);
247
- return () => {
248
- window.removeEventListener("mousemove", move);
249
- window.removeEventListener("mouseup", up);
250
- window.removeEventListener("touchmove", move);
251
- window.removeEventListener("touchend", up);
252
- };
253
- }, [drag, hoverZone, filter, onChange]);
254
201
  return (_jsxs("div", { ref: rootRef, style: {
255
202
  display: "flex",
256
203
  flexDirection: "column",
@@ -1,6 +1,11 @@
1
1
  export interface JamlMapPreviewProps {
2
+ /** The raw JAML string to parse and visualize. */
2
3
  jaml: string;
3
4
  className?: string;
4
5
  emptyMessage?: string;
6
+ tallyColumns?: number[];
7
+ tallyLabels?: string[];
8
+ /** Reduces padding and sizes for sidebar/explorer usage. */
9
+ compact?: boolean;
5
10
  }
6
- export declare function JamlMapPreview({ jaml, className, emptyMessage, }: JamlMapPreviewProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function JamlMapPreview({ jaml, className, emptyMessage, tallyColumns, tallyLabels, compact, }: JamlMapPreviewProps): import("react/jsx-runtime").JSX.Element;