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
@@ -6,9 +6,9 @@ import { JimboColorOption } from "../ui/tokens.js";
6
6
  import { extractVisualJamlItems, } from "../utils/jamlMapPreview.js";
7
7
  const C = JimboColorOption;
8
8
  const ZONES = {
9
- must: { label: "MUST", color: C.BLUE },
10
- should: { label: "SHOULD", color: C.RED },
11
- mustNot: { label: "MUST NOT", color: C.ORANGE },
9
+ must: { label: "MUST", color: C.BLUE, glow: C.BLUE },
10
+ should: { label: "SHOULD", color: C.RED, glow: C.GOLD },
11
+ mustNot: { label: "MUST NOT", color: C.ORANGE, glow: C.ORANGE },
12
12
  };
13
13
  const SECTION_ORDER = ["must", "should", "mustNot"];
14
14
  const SHEET_FOR_VISUAL = {
@@ -18,48 +18,126 @@ const SHEET_FOR_VISUAL = {
18
18
  tag: "tags",
19
19
  boss: "BlindChips",
20
20
  };
21
- function ClausePill({ item, color }) {
21
+ /**
22
+ * Pulsing glow animation for hits.
23
+ * Design ref: assets/...DesignsV2/src/v2/GlowRing.css
24
+ */
25
+ const GLOW_ANIMATION = `
26
+ @keyframes j-glow-pulse {
27
+ 0% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
28
+ 50% { box-shadow: 0 0 0 2px var(--glow-color), 0 0 12px var(--glow-color); opacity: 1; }
29
+ 100% { box-shadow: 0 0 0 1px var(--glow-color), 0 0 4px var(--glow-color); opacity: 0.8; }
30
+ }
31
+ `;
32
+ function ClausePill({ item, color, glow, matchCount }) {
33
+ const isHit = matchCount > 0;
34
+ const hasData = matchCount !== undefined && matchCount >= 0;
22
35
  return (_jsxs("div", { style: {
23
36
  display: "flex",
24
37
  alignItems: "center",
25
38
  gap: 6,
26
- background: C.DARK_GREY,
27
- border: `2px solid ${color}`,
28
- borderRadius: 6,
29
- padding: "5px 8px 5px 4px",
30
- boxShadow: `0 2px 0 ${C.BLACK}`,
31
- }, title: `${item.clauseKey}: ${item.value}`, children: [_jsx("div", { style: { color: C.GREY, fontSize: 12, lineHeight: 1, padding: "0 2px" }, children: "\u22EE\u22EE" }), _jsx(JimboSprite, { name: item.value, sheet: SHEET_FOR_VISUAL[item.visualType], width: 26 }), _jsx("div", { style: {
39
+ background: isHit ? `${glow}33` : C.DARKEST,
40
+ border: `2px solid ${isHit ? glow : C.PANEL_EDGE}`,
41
+ borderRadius: 4,
42
+ padding: "3px 8px",
43
+ position: "relative",
44
+ opacity: isHit ? 1 : 0.6,
45
+ // @ts-ignore -- CSS custom property
46
+ "--glow-color": glow,
47
+ animation: isHit ? "j-glow-pulse 1.6s ease-in-out infinite" : "none",
48
+ }, title: `${item.clauseKey}: ${item.value}${hasData ? ` (Found: ${matchCount})` : ""}`, children: [_jsx("style", { children: GLOW_ANIMATION }), _jsx(JimboSprite, { name: item.value, sheet: SHEET_FOR_VISUAL[item.visualType], width: 26 }), _jsx("div", { style: {
32
49
  fontSize: 10,
33
50
  color: C.WHITE,
34
- letterSpacing: 1,
51
+ letterSpacing: 0.5,
35
52
  textShadow: "1px 1px 0 rgba(0,0,0,.8)",
36
- }, children: item.value })] }));
53
+ }, children: item.value }), isHit && (_jsx("div", { style: {
54
+ position: "absolute",
55
+ top: -6,
56
+ right: -6,
57
+ background: C.GREEN,
58
+ color: C.WHITE,
59
+ fontSize: 7,
60
+ padding: "1px 3px",
61
+ borderRadius: 3,
62
+ border: `1px solid ${C.BLACK}`,
63
+ boxShadow: `0 1px 0 ${C.BLACK}`,
64
+ }, children: matchCount > 1 ? `x${matchCount}` : "✓" }))] }));
65
+ }
66
+ function VisualChip({ item, matchCount, compact }) {
67
+ const isHit = matchCount > 0;
68
+ const hasData = matchCount !== undefined;
69
+ const glow = ZONES[item.section].glow;
70
+ return (_jsxs("div", { style: {
71
+ display: "flex",
72
+ alignItems: "center",
73
+ gap: compact ? 3 : 6,
74
+ background: isHit ? `${glow}33` : C.DARKEST,
75
+ border: `2px solid ${isHit ? glow : C.PANEL_EDGE}`,
76
+ borderRadius: 4,
77
+ padding: compact ? "2px 4px" : "3px 8px",
78
+ position: "relative",
79
+ opacity: isHit ? 1 : 0.6,
80
+ // @ts-ignore
81
+ "--glow-color": glow,
82
+ animation: isHit ? "j-glow-pulse 1.6s ease-in-out infinite" : "none",
83
+ }, title: `${item.clauseKey}: ${item.value}${hasData ? ` (Found: ${matchCount})` : ""}`, children: [_jsx("style", { children: GLOW_ANIMATION }), _jsx("div", { style: { color: C.GREY, fontSize: 10, lineHeight: 1, padding: "0 2px" }, children: "\u22EE\u22EE" }), _jsx(JimboSprite, { name: item.value, sheet: SHEET_FOR_VISUAL[item.visualType], width: compact ? 20 : 26 }), _jsx("div", { style: {
84
+ fontSize: compact ? 9 : 10,
85
+ color: C.WHITE,
86
+ letterSpacing: 0.5,
87
+ textShadow: "1px 1px 0 rgba(0,0,0,.8)",
88
+ }, children: item.value }), isHit && (_jsx("div", { style: {
89
+ position: "absolute",
90
+ top: compact ? -4 : -6,
91
+ right: compact ? -4 : -6,
92
+ background: C.GREEN,
93
+ color: C.WHITE,
94
+ fontSize: 7,
95
+ padding: "1px 3px",
96
+ borderRadius: 3,
97
+ border: `1px solid ${C.BLACK}`,
98
+ boxShadow: `0 1px 0 ${C.BLACK}`,
99
+ }, children: matchCount > 1 ? `x${matchCount}` : "✓" }))] }));
37
100
  }
38
- function ZoneRail({ zone, items }) {
101
+ function ZoneRail({ zone, items, matchMap, compact = false }) {
39
102
  const meta = ZONES[zone];
40
103
  return (_jsxs("div", { style: {
41
104
  border: `2px dashed ${meta.color}55`,
42
105
  borderRadius: 6,
43
- padding: 8,
106
+ padding: compact ? 4 : 8,
44
107
  }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }, children: [_jsx("div", { style: {
45
- fontSize: 10,
108
+ fontSize: compact ? 8 : 10,
46
109
  letterSpacing: 2,
47
110
  padding: "2px 8px",
48
111
  background: meta.color,
49
112
  color: C.WHITE,
50
113
  borderRadius: 3,
51
114
  textShadow: "1px 1px 0 rgba(0,0,0,.8)",
52
- }, children: meta.label }), _jsx("div", { style: { flex: 1, height: 1, background: `${meta.color}44` } }), _jsx("div", { style: { fontSize: 8, color: C.GREY }, children: items.length })] }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: items.length === 0 ? (_jsx("div", { style: { fontSize: 10, color: C.GREY, padding: 10, fontStyle: "italic" }, children: "drop clauses here" })) : (items.map((item) => _jsx(ClausePill, { item: item, color: meta.color }, item.id))) })] }));
115
+ }, children: meta.label }), _jsx("div", { style: { flex: 1, height: 1, background: `${meta.color}44` } }), _jsx("div", { style: { fontSize: 8, color: C.GREY }, children: items.length })] }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: items.length === 0 ? (_jsx("div", { style: { fontSize: 10, color: C.GREY, padding: 10, fontStyle: "italic" }, children: "drop clauses here" })) : (items.map((item) => {
116
+ // Match logic: the engine labels usually look like "must: joker: Blueprint"
117
+ // or "must: rareJoker: Blueprint". We try to match the item value and section.
118
+ const labelKey = `${item.section}: ${item.clauseKey}: ${item.value}`;
119
+ const count = matchMap[labelKey] ?? -1;
120
+ return (_jsx(ClausePill, { item: item, color: meta.color, glow: meta.glow, matchCount: count }, item.id));
121
+ })) })] }));
53
122
  }
54
- export function JamlMapPreview({ jaml, className = "", emptyMessage = "No visual JAML clauses found yet.", }) {
123
+ export function JamlMapPreview({ jaml, className = "", emptyMessage = "No visual JAML clauses found yet.", tallyColumns, tallyLabels, compact = false, }) {
55
124
  const groups = useMemo(() => extractVisualJamlItems(jaml), [jaml]);
56
125
  const totalItems = SECTION_ORDER.reduce((sum, s) => sum + groups[s].length, 0);
126
+ const matchMap = useMemo(() => {
127
+ const map = {};
128
+ if (tallyColumns && tallyLabels) {
129
+ tallyLabels.forEach((label, i) => {
130
+ map[label] = tallyColumns[i] ?? 0;
131
+ });
132
+ }
133
+ return map;
134
+ }, [tallyColumns, tallyLabels]);
57
135
  if (totalItems === 0) {
58
136
  return (_jsx("div", { className: className, style: {
59
137
  background: C.DARKEST,
60
138
  border: `2px solid ${C.PANEL_EDGE}`,
61
139
  borderRadius: 6,
62
- padding: 16,
140
+ padding: compact ? 8 : 16,
63
141
  color: C.GREY,
64
142
  fontSize: 11,
65
143
  fontStyle: "italic",
@@ -69,9 +147,9 @@ export function JamlMapPreview({ jaml, className = "", emptyMessage = "No visual
69
147
  return (_jsx("div", { className: className, style: {
70
148
  display: "flex",
71
149
  flexDirection: "column",
72
- gap: 10,
73
- padding: 10,
150
+ gap: compact ? 6 : 10,
151
+ padding: compact ? 6 : 10,
74
152
  background: C.DARKEST,
75
153
  color: C.WHITE,
76
- }, children: SECTION_ORDER.map((section) => (_jsx(ZoneRail, { zone: section, items: groups[section] }, section))) }));
154
+ }, children: SECTION_ORDER.map((section) => (_jsx(ZoneRail, { zone: section, items: groups[section], matchMap: matchMap, compact: compact }, section))) }));
77
155
  }
@@ -6,4 +6,9 @@ export interface JamlSeedInputProps {
6
6
  className?: string;
7
7
  style?: React.CSSProperties;
8
8
  }
9
+ /**
10
+ * Balatro-styled seed input field.
11
+ * Validates 1-8 uppercase alphanumeric characters.
12
+ * All styling via jimbo.css `.j-seed-input` classes — zero inline styles.
13
+ */
9
14
  export declare function JamlSeedInput({ value, onChange, placeholder, className, style }: JamlSeedInputProps): import("react/jsx-runtime").JSX.Element;
@@ -1,29 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { JimboColorOption } from "../ui/tokens.js";
5
4
  import { JimboText } from "../ui/jimboText.js";
6
5
  const SEED_PATTERN = /^[A-Z0-9]{0,8}$/;
6
+ /**
7
+ * Balatro-styled seed input field.
8
+ * Validates 1-8 uppercase alphanumeric characters.
9
+ * All styling via jimbo.css `.j-seed-input` classes — zero inline styles.
10
+ */
7
11
  export function JamlSeedInput({ value, onChange, placeholder = "Enter seed (e.g. J4SPZMWW)", className, style }) {
8
12
  const [internal, setInternal] = useState(value ?? "");
9
13
  const display = value ?? internal;
10
14
  const isValid = display.length === 0 || SEED_PATTERN.test(display);
15
+ // Validation state drives data-valid attribute for CSS border color
16
+ const validState = display.length === 0 ? "partial"
17
+ : !isValid ? "false"
18
+ : display.length === 8 ? "true"
19
+ : "partial";
11
20
  const handleChange = (e) => {
12
21
  const raw = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 8);
13
22
  setInternal(raw);
14
23
  onChange?.(raw);
15
24
  };
16
- return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 4, ...style }, children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "Seed" }), _jsx("input", { type: "text", value: display, onChange: handleChange, placeholder: placeholder, maxLength: 8, spellCheck: false, autoComplete: "off", style: {
17
- padding: "6px 10px",
18
- borderRadius: 6,
19
- border: `2px solid ${!isValid ? JimboColorOption.RED : display.length === 8 ? JimboColorOption.GREEN : JimboColorOption.PANEL_EDGE}`,
20
- background: JimboColorOption.DARKEST,
21
- color: JimboColorOption.GOLD_TEXT,
22
- fontSize: 16,
23
- fontFamily: "m6x11plus, monospace",
24
- letterSpacing: 2,
25
- textTransform: "uppercase",
26
- outline: "none",
27
- transition: "border-color 100ms",
28
- } }), display.length > 0 && display.length < 8 && (_jsxs(JimboText, { size: "xs", tone: "grey", children: [8 - display.length, " more characters"] }))] }));
25
+ return (_jsxs("div", { className: `j-seed-input ${className ?? ""}`, style: style, children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "Seed" }), _jsx("input", { type: "text", className: "j-seed-input__field", "data-valid": validState, value: display, onChange: handleChange, placeholder: placeholder, maxLength: 8, spellCheck: false, autoComplete: "off" }), display.length > 0 && display.length < 8 && (_jsxs("span", { className: "j-seed-input__hint", children: [8 - display.length, " more characters"] }))] }));
29
26
  }
@@ -1,11 +1,11 @@
1
- import React from "react";
2
- import type { SearchStatus } from "../hooks/useSearch.js";
1
+ export type JamlSpeedometerStatus = "idle" | "booting" | "running" | "completed" | "cancelled" | "error";
3
2
  export interface JamlSpeedometerProps {
4
3
  seedsPerSecond: number;
5
- totalSearched: bigint;
6
- matchingSeeds: bigint;
7
- status: SearchStatus;
8
- className?: string;
9
- style?: React.CSSProperties;
4
+ totalSearched: bigint | number;
5
+ matchingSeeds: bigint | number;
6
+ status: JamlSpeedometerStatus;
10
7
  }
11
- export declare function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, className, style, }: JamlSpeedometerProps): import("react/jsx-runtime").JSX.Element;
8
+ /**
9
+ * Compact live-search stats strip for MCP/app chrome.
10
+ */
11
+ export declare function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, }: JamlSpeedometerProps): import("react/jsx-runtime").JSX.Element;
@@ -1,54 +1,32 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { JimboColorOption } from "../ui/tokens.js";
4
- import { JimboText } from "../ui/jimboText.js";
5
- function formatCount(n) {
6
- if (n >= 1000000000n)
7
- return `${(Number(n / 1000000n) / 1000).toFixed(1)}B`;
8
- if (n >= 1000000n)
9
- return `${(Number(n / 1000n) / 1000).toFixed(1)}M`;
10
- if (n >= 1000n)
11
- return `${(Number(n) / 1000).toFixed(1)}K`;
12
- return n.toString();
4
+ const C = JimboColorOption;
5
+ function formatCount(value) {
6
+ return Number(value).toLocaleString();
13
7
  }
14
- function formatSpeed(sps) {
15
- if (sps >= 1_000_000)
16
- return `${(sps / 1_000_000).toFixed(1)}M`;
17
- if (sps >= 1_000)
18
- return `${(sps / 1_000).toFixed(0)}K`;
19
- return sps.toString();
8
+ function formatSpeed(value) {
9
+ if (!Number.isFinite(value) || value <= 0)
10
+ return "0/s";
11
+ if (value >= 1_000_000)
12
+ return `${(value / 1_000_000).toFixed(1)}M/s`;
13
+ if (value >= 1_000)
14
+ return `${(value / 1_000).toFixed(1)}K/s`;
15
+ return `${Math.round(value)}/s`;
20
16
  }
21
- function needleAngle(sps) {
22
- if (sps <= 0)
23
- return -90;
24
- const maxLog = Math.log10(5_000_000);
25
- const clamped = Math.min(sps, 5_000_000);
26
- const pct = Math.log10(Math.max(clamped, 1)) / maxLog;
27
- return -90 + pct * 180;
28
- }
29
- export function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, className, style, }) {
30
- const isActive = status === "running" || status === "booting";
31
- const angle = needleAngle(seedsPerSecond);
32
- const speedColor = seedsPerSecond >= 500_000
33
- ? JimboColorOption.GREEN
34
- : seedsPerSecond >= 100_000
35
- ? JimboColorOption.GOLD
36
- : seedsPerSecond > 0
37
- ? JimboColorOption.ORANGE
38
- : JimboColorOption.GREY;
39
- return (_jsxs("div", { className: className, style: {
17
+ /**
18
+ * Compact live-search stats strip for MCP/app chrome.
19
+ */
20
+ export function JamlSpeedometer({ seedsPerSecond, totalSearched, matchingSeeds, status, }) {
21
+ const active = status === "running" || status === "booting";
22
+ const tone = status === "error" ? C.RED : active ? C.GOLD : C.GREY;
23
+ return (_jsxs("div", { style: {
40
24
  display: "flex",
41
- flexDirection: "column",
42
25
  alignItems: "center",
43
- gap: 6,
44
- padding: "12px 16px",
45
- borderRadius: 10,
46
- background: `${JimboColorOption.DARKEST}cc`,
47
- border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
48
- ...style,
49
- }, children: [_jsx("div", { style: { position: "relative", width: 120, height: 68, overflow: "hidden" }, children: _jsxs("svg", { viewBox: "0 0 120 68", width: 120, height: 68, children: [_jsx("path", { d: "M 10 65 A 50 50 0 0 1 110 65", fill: "none", stroke: JimboColorOption.DARK_GREY, strokeWidth: 6, strokeLinecap: "round" }), isActive && (_jsx("path", { d: "M 10 65 A 50 50 0 0 1 110 65", fill: "none", stroke: speedColor, strokeWidth: 6, strokeLinecap: "round", strokeDasharray: "157", strokeDashoffset: 157 - (157 * ((angle + 90) / 180)), style: { transition: "stroke-dashoffset 300ms ease, stroke 300ms ease" } })), _jsx("line", { x1: 60, y1: 65, x2: 60, y2: 20, stroke: isActive ? JimboColorOption.RED : JimboColorOption.GREY, strokeWidth: 2, strokeLinecap: "round", style: {
50
- transformOrigin: "60px 65px",
51
- transform: `rotate(${angle}deg)`,
52
- transition: "transform 300ms ease",
53
- } }), _jsx("circle", { cx: 60, cy: 65, r: 4, fill: JimboColorOption.RED })] }) }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 20, fontFamily: "m6x11plus, monospace", color: isActive ? speedColor : JimboColorOption.GREY }, children: isActive ? formatSpeed(seedsPerSecond) : "---" }), _jsx(JimboText, { size: "xs", tone: "grey", children: "seeds / sec" })] }), _jsxs("div", { style: { display: "flex", gap: 16, marginTop: 2 }, children: [_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 13, fontFamily: "m6x11plus, monospace", color: JimboColorOption.WHITE }, children: formatCount(totalSearched) }), _jsx(JimboText, { size: "xs", tone: "grey", children: "searched" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 13, fontFamily: "m6x11plus, monospace", color: JimboColorOption.GREEN_TEXT }, children: formatCount(matchingSeeds) }), _jsx(JimboText, { size: "xs", tone: "grey", children: "matches" })] })] })] }));
26
+ gap: 8,
27
+ color: tone,
28
+ fontSize: 11,
29
+ fontFamily: "var(--font-sans, m6x11plus), monospace",
30
+ whiteSpace: "nowrap",
31
+ }, children: [_jsx("span", { children: status }), _jsx("span", { children: formatSpeed(seedsPerSecond) }), _jsxs("span", { children: [formatCount(totalSearched), " searched"] }), _jsxs("span", { children: [formatCount(matchingSeeds), " matches"] })] }));
54
32
  }
@@ -23,8 +23,6 @@ export interface MotelyVersionBadgeProps {
23
23
  }
24
24
  /**
25
25
  * Badge showing the loaded motely-wasm version + optional SIMD / threads
26
- * capability indicators. Ported from weejoker.app with no dependency on
27
- * weejoker's lib/api — the consumer owns capability fetching and passes
28
- * the result in.
26
+ * capability indicators. All styling via jimbo.css `.j-motely-badge`.
29
27
  */
30
28
  export declare function MotelyVersionBadge({ caps, version, minimal, loading, className, style, }: MotelyVersionBadgeProps): import("react/jsx-runtime").JSX.Element;
@@ -1,31 +1,19 @@
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
  /**
6
5
  * Badge showing the loaded motely-wasm version + optional SIMD / threads
7
- * capability indicators. Ported from weejoker.app with no dependency on
8
- * weejoker's lib/api — the consumer owns capability fetching and passes
9
- * the result in.
6
+ * capability indicators. All styling via jimbo.css `.j-motely-badge`.
10
7
  */
11
8
  export function MotelyVersionBadge({ caps, version, minimal = false, loading = false, className = '', style, }) {
12
9
  if (loading) {
13
- return (_jsx("span", { className: className, style: style, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Initializing\u2026" }) }));
10
+ return (_jsx("span", { className: `j-motely-badge ${className}`, style: style, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Initializing\u2026" }) }));
14
11
  }
15
12
  const resolved = caps?.version ?? version ?? '?';
16
13
  const simd = caps?.simd;
17
14
  const threads = caps?.threads;
18
15
  if (minimal) {
19
- return (_jsxs("span", { className: className, style: { display: 'inline-flex', alignItems: 'center', gap: 6, ...style }, children: [_jsxs(JimboText, { size: "xs", tone: "grey", children: ["v", resolved] }), simd ? (_jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" })) : null, threads ? (_jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" })) : null] }));
16
+ return (_jsxs("span", { className: `j-motely-badge ${className}`, style: style, children: [_jsxs(JimboText, { size: "xs", tone: "grey", children: ["v", resolved] }), simd ? _jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" }) : null, threads ? _jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" }) : null] }));
20
17
  }
21
- return (_jsxs("div", { className: className, style: {
22
- display: 'inline-flex',
23
- alignItems: 'center',
24
- gap: 6,
25
- padding: '3px 8px',
26
- borderRadius: 4,
27
- background: JimboColorOption.DARKEST,
28
- border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
29
- ...style,
30
- }, children: [_jsxs(JimboText, { size: "xs", tone: "gold", uppercase: true, children: ["motely v", resolved] }), simd ? _jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" }) : null, threads ? _jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" }) : null] }));
18
+ return (_jsxs("div", { className: `j-motely-badge j-motely-badge--chip ${className}`, style: style, children: [_jsxs(JimboText, { size: "xs", tone: "gold", uppercase: true, children: ["motely v", resolved] }), simd ? _jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" }) : null, threads ? _jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" }) : null] }));
31
19
  }
@@ -0,0 +1,8 @@
1
+ import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
2
+ export interface JamlMapEditorDemoProps {
3
+ /** Initial zone for the demo. */
4
+ zone?: JamlZone;
5
+ /** Callback when a selection changes, to update JAML text. */
6
+ onChange?: (slots: (SlotSelection | null)[]) => void;
7
+ }
8
+ export declare function JamlMapEditorDemo({ zone: initialZone, onChange, }: JamlMapEditorDemoProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,170 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback } from "react";
4
+ import { MysterySlot } from "./MysterySlot.js";
5
+ import { JokerPicker } from "./JokerPicker.js";
6
+ import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
7
+ // ─── Component ───────────────────────────────────────────────────────────────
8
+ const C = JimboColorOption;
9
+ const INITIAL_SLOTS = 8; // 8 shop item slots like in Balatro ante
10
+ export function JamlMapEditorDemo({ zone: initialZone = "must", onChange, }) {
11
+ const [zone, setZone] = useState(initialZone);
12
+ const [slots, setSlots] = useState(Array(INITIAL_SLOTS).fill(null));
13
+ const [activeSlot, setActiveSlot] = useState(null);
14
+ const handleSlotTap = useCallback((index) => {
15
+ setActiveSlot(index);
16
+ }, []);
17
+ const handleSlotClear = useCallback((index) => {
18
+ setSlots((prev) => {
19
+ const next = [...prev];
20
+ next[index] = null;
21
+ onChange?.(next);
22
+ return next;
23
+ });
24
+ }, [onChange]);
25
+ const handleJokerSelect = useCallback((selection) => {
26
+ if (activeSlot === null)
27
+ return;
28
+ setSlots((prev) => {
29
+ const next = [...prev];
30
+ next[activeSlot] = selection;
31
+ onChange?.(next);
32
+ return next;
33
+ });
34
+ setActiveSlot(null);
35
+ }, [activeSlot, onChange]);
36
+ const handlePickerCancel = useCallback(() => {
37
+ setActiveSlot(null);
38
+ }, []);
39
+ const filledCount = slots.filter(Boolean).length;
40
+ 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
+ ...styles.zoneBtn,
42
+ borderColor: zone === z ? ZONE_COLORS[z] : C.TEAL_GREY,
43
+ color: zone === z ? ZONE_COLORS[z] : C.GREY,
44
+ background: zone === z ? withAlpha(ZONE_COLORS[z], 0.1) : "transparent",
45
+ }, children: z.toUpperCase() }, z))) }), _jsxs("div", { style: styles.sectionLabel, children: [_jsx("span", { style: { color: ZONE_COLORS[zone] }, children: "Shop Items" }), _jsxs("span", { style: { color: C.GREY, fontSize: 11 }, children: [filledCount, "/", INITIAL_SLOTS, " defined"] })] }), _jsx("div", { style: styles.slotRow, children: slots.map((selection, i) => (_jsx(MysterySlot, { zone: zone, sheetType: "Jokers", selection: selection ?? undefined, width: 48, onTap: () => handleSlotTap(i), onClear: selection ? () => handleSlotClear(i) : undefined }, i))) }), _jsx("div", { style: styles.scrollHint, children: "\u25C0 swipe \u25B6" }), activeSlot !== null && (_jsxs("div", { style: styles.overlay, children: [_jsx("div", { style: styles.overlayBackdrop, onClick: handlePickerCancel }), _jsx("div", { style: styles.pickerWrapper, children: _jsx(JokerPicker, { onSelect: handleJokerSelect, onCancel: handlePickerCancel }) })] })), filledCount > 0 && (_jsxs("div", { style: styles.jamlPreview, children: [_jsx("div", { style: styles.jamlHeader, children: "Generated JAML" }), _jsx("pre", { style: styles.jamlCode, children: generateJamlSnippet(zone, slots) })] }))] }));
46
+ }
47
+ // ─── JAML generation ─────────────────────────────────────────────────────────
48
+ function generateJamlSnippet(zone, slots) {
49
+ const filled = slots.filter(Boolean);
50
+ if (filled.length === 0)
51
+ return "# empty";
52
+ const jamlZone = zone === "mustnot" ? "mustNot" : zone;
53
+ const lines = [`${jamlZone}:`];
54
+ // Group by clauseKey
55
+ const groups = new Map();
56
+ for (const s of filled) {
57
+ const existing = groups.get(s.clauseKey) ?? [];
58
+ existing.push(s.value);
59
+ groups.set(s.clauseKey, existing);
60
+ }
61
+ for (const [key, values] of groups) {
62
+ if (values.length === 1) {
63
+ lines.push(` - ${key}: ${values[0]}`);
64
+ }
65
+ else {
66
+ lines.push(` - ${key}: [${values.join(", ")}]`);
67
+ }
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+ // ─── Constants & styles ──────────────────────────────────────────────────────
72
+ const ZONE_COLORS = {
73
+ must: C.BLUE,
74
+ should: C.RED,
75
+ mustnot: C.ORANGE,
76
+ };
77
+ const styles = {
78
+ wrapper: {
79
+ position: "relative",
80
+ padding: 16,
81
+ background: C.DARKEST,
82
+ borderRadius: 8,
83
+ border: `1px solid ${C.TEAL_GREY}`,
84
+ fontFamily: "m6x11plus, ui-monospace, monospace",
85
+ },
86
+ zoneBar: {
87
+ display: "flex",
88
+ gap: 6,
89
+ marginBottom: 12,
90
+ },
91
+ zoneBtn: {
92
+ padding: "5px 12px",
93
+ border: "2px solid",
94
+ borderRadius: 4,
95
+ background: "none",
96
+ fontFamily: "m6x11plus, ui-monospace, monospace",
97
+ fontSize: 12,
98
+ fontWeight: "bold",
99
+ cursor: "pointer",
100
+ transition: "all 150ms",
101
+ letterSpacing: 1,
102
+ },
103
+ sectionLabel: {
104
+ display: "flex",
105
+ justifyContent: "space-between",
106
+ alignItems: "center",
107
+ marginBottom: 8,
108
+ fontSize: 14,
109
+ fontWeight: "bold",
110
+ },
111
+ slotRow: {
112
+ display: "flex",
113
+ gap: 6,
114
+ overflowX: "auto",
115
+ paddingBottom: 8,
116
+ WebkitOverflowScrolling: "touch",
117
+ },
118
+ scrollHint: {
119
+ textAlign: "center",
120
+ fontSize: 10,
121
+ color: C.GREY,
122
+ opacity: 0.5,
123
+ marginTop: 2,
124
+ marginBottom: 8,
125
+ },
126
+ overlay: {
127
+ position: "fixed",
128
+ inset: 0,
129
+ zIndex: 9999,
130
+ display: "flex",
131
+ alignItems: "center",
132
+ justifyContent: "center",
133
+ },
134
+ overlayBackdrop: {
135
+ position: "absolute",
136
+ inset: 0,
137
+ background: withAlpha(C.BLACK, 0.6),
138
+ backdropFilter: "blur(4px)",
139
+ },
140
+ pickerWrapper: {
141
+ position: "relative",
142
+ zIndex: 1,
143
+ width: "90%",
144
+ maxWidth: 400,
145
+ },
146
+ jamlPreview: {
147
+ marginTop: 12,
148
+ borderRadius: 4,
149
+ border: `1px solid ${C.TEAL_GREY}`,
150
+ overflow: "hidden",
151
+ },
152
+ jamlHeader: {
153
+ padding: "6px 10px",
154
+ fontSize: 11,
155
+ color: C.GREY,
156
+ background: withAlpha(C.DARK_GREY, 0.5),
157
+ borderBottom: `1px solid ${C.TEAL_GREY}`,
158
+ letterSpacing: 1,
159
+ textTransform: "uppercase",
160
+ },
161
+ jamlCode: {
162
+ margin: 0,
163
+ padding: "8px 10px",
164
+ fontSize: 12,
165
+ color: C.GREEN_TEXT,
166
+ background: withAlpha(C.DARKEST, 0.8),
167
+ lineHeight: "1.5",
168
+ whiteSpace: "pre-wrap",
169
+ },
170
+ };
@@ -0,0 +1,7 @@
1
+ import type { SlotSelection } from "./MysterySlot.js";
2
+ export type JokerRarity = "common" | "uncommon" | "rare" | "legendary";
3
+ export interface JokerPickerProps {
4
+ onSelect: (selection: SlotSelection) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function JokerPicker({ onSelect, onCancel }: JokerPickerProps): import("react/jsx-runtime").JSX.Element;