jaml-ui 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +135 -135
  3. package/assets/8BitDeck.png +0 -0
  4. package/assets/Balatro Seed Curator (DesignsV2)/.design-canvas.state.json +1 -0
  5. package/assets/Balatro Seed Curator (DesignsV2)/Assets/BlindChips.png +0 -0
  6. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/Boosters.json +303 -0
  7. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/boosters.png +0 -0
  8. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters.png +0 -0
  9. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/BlindChips.png +0 -0
  10. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/blinds_metadata.json +51 -0
  11. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/8BitDeck.png +0 -0
  12. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/Enhancers.png +0 -0
  13. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/balatro-stake-chips.png +0 -0
  14. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/enhancers_metadata.json +52 -0
  15. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/playing_cards_metadata.json +74 -0
  16. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/stakes.json +19 -0
  17. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Editions.png +0 -0
  18. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Enhancers.png +0 -0
  19. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Editions.png +0 -0
  20. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Jokers.png +0 -0
  21. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/jokers.json +1087 -0
  22. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers.png +0 -0
  23. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers_metadata.json +25 -0
  24. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers.png +0 -0
  25. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.json +191 -0
  26. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.png +0 -0
  27. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/Tarots.png +0 -0
  28. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/planets.json +15 -0
  29. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/spectrals.json +21 -0
  30. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/tarots.json +163 -0
  31. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots.png +0 -0
  32. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/Vouchers.png +0 -0
  33. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/vouchers.json +130 -0
  34. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers.png +0 -0
  35. package/assets/Balatro Seed Curator (DesignsV2)/Assets/blinds.json +51 -0
  36. package/assets/Balatro Seed Curator (DesignsV2)/Assets/boosters.json +303 -0
  37. package/assets/Balatro Seed Curator (DesignsV2)/Assets/fonts/m6x11plusplus.otf +0 -0
  38. package/assets/Balatro Seed Curator (DesignsV2)/Assets/jokers.json +1087 -0
  39. package/assets/Balatro Seed Curator (DesignsV2)/Assets/planets.json +15 -0
  40. package/assets/Balatro Seed Curator (DesignsV2)/Assets/spectrals.json +21 -0
  41. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stakes.png +0 -0
  42. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stickers.png +0 -0
  43. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.json +191 -0
  44. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.png +0 -0
  45. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tarots.json +163 -0
  46. package/assets/Balatro Seed Curator (DesignsV2)/Assets/vouchers.json +130 -0
  47. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail v2.html +40 -0
  48. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail.html +34 -0
  49. package/assets/Balatro Seed Curator (DesignsV2)/public/fonts/m6x11plusplus.otf +0 -0
  50. package/assets/Balatro Seed Curator (DesignsV2)/src/AntePage.jsx +228 -0
  51. package/assets/Balatro Seed Curator (DesignsV2)/src/SeedDetail.jsx +222 -0
  52. package/assets/Balatro Seed Curator (DesignsV2)/src/app.jsx +35 -0
  53. package/assets/Balatro Seed Curator (DesignsV2)/src/mockData.js +185 -0
  54. package/assets/Balatro Seed Curator (DesignsV2)/src/sprites.jsx +259 -0
  55. package/assets/Balatro Seed Curator (DesignsV2)/src/tokens.js +49 -0
  56. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/AntePageV2.jsx +290 -0
  57. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/BalButton.jsx +107 -0
  58. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlBuilderV2.jsx +594 -0
  59. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlIde.jsx +302 -0
  60. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SearchResultsV2.jsx +286 -0
  61. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedDetailV2.jsx +336 -0
  62. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedOGCard.jsx +251 -0
  63. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/Showcase.jsx +131 -0
  64. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/app.jsx +55 -0
  65. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/data.js +296 -0
  66. package/assets/Balatro Seed Curator (DesignsV2)/starters/design-canvas.jsx +622 -0
  67. package/assets/Balatro Seed Curator (DesignsV2)/uploads/8BitDeck.png +0 -0
  68. package/assets/Balatro Seed Curator (DesignsV2)/uploads/BlindChips.png +0 -0
  69. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Boosters.png +0 -0
  70. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Editions.png +0 -0
  71. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Enhancers.png +0 -0
  72. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Jokers.png +0 -0
  73. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Tarots.png +0 -0
  74. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749540653-0.png +0 -0
  75. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749644934-0.png +0 -0
  76. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749661871-0.png +0 -0
  77. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749674748-0.png +0 -0
  78. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749703076-0.png +0 -0
  79. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749882759-0.png +0 -0
  80. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750354200-0.png +0 -0
  81. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750733265-0.png +0 -0
  82. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776751928925-0.png +0 -0
  83. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776800975060-0.png +0 -0
  84. package/assets/Balatro Seed Curator (DesignsV2)/uploads/stickers.png +0 -0
  85. package/assets/Balatro Seed Curator (DesignsV2)/uploads/tags.png +0 -0
  86. package/assets/BlindChips.png +0 -0
  87. package/assets/Boosters.png +0 -0
  88. package/assets/Editions.png +0 -0
  89. package/assets/Enhancers.png +0 -0
  90. package/assets/Jokers.png +0 -0
  91. package/assets/Tarots.png +0 -0
  92. package/assets/Vouchers.png +0 -0
  93. package/assets/fonts/m6x11plusplus.otf +0 -0
  94. package/assets/stickers.png +0 -0
  95. package/assets/tags.png +0 -0
  96. package/dist/assets.js +10 -13
  97. package/dist/components/GameCard.js +7 -5
  98. package/dist/components/JamlCodeEditor.js +28 -16
  99. package/dist/components/JamlIdeVisual.d.ts +5 -3
  100. package/dist/components/JamlIdeVisual.js +194 -36
  101. package/dist/components/JamlMapPreview.d.ts +1 -5
  102. package/dist/components/JamlMapPreview.js +67 -107
  103. package/dist/components/PlayingCard.js +15 -50
  104. package/dist/data/balatro-jokers.json +1241 -0
  105. package/dist/decode/motelyItemDecoder.js +1 -1
  106. package/dist/hooks/searchWorkerCode.js +59 -59
  107. package/dist/hooks/useAnalyzer.d.ts +1 -0
  108. package/dist/hooks/useAnalyzer.js +5 -1
  109. package/dist/hooks/useSearch.d.ts +1 -0
  110. package/dist/hooks/useSearch.js +4 -1
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/motelyDisplay.js +2 -0
  114. package/dist/r3f/BalatroJokerMesh3D.d.ts +8 -0
  115. package/dist/r3f/BalatroJokerMesh3D.js +98 -0
  116. package/dist/r3f/BalatroJokerPreview3D.d.ts +14 -0
  117. package/dist/r3f/BalatroJokerPreview3D.js +30 -0
  118. package/dist/r3f/BalatroPlayingCard3D.d.ts +22 -0
  119. package/dist/r3f/BalatroPlayingCard3D.js +62 -0
  120. package/dist/r3f/cardConstants.d.ts +16 -0
  121. package/dist/r3f/cardConstants.js +14 -0
  122. package/dist/r3f/compositedAtlas.d.ts +5 -0
  123. package/dist/r3f/compositedAtlas.js +56 -0
  124. package/dist/r3f/gridUV.d.ts +22 -0
  125. package/dist/r3f/gridUV.js +30 -0
  126. package/dist/r3f/index.d.ts +12 -0
  127. package/dist/r3f/index.js +13 -0
  128. package/dist/r3f/jokerRegistry.d.ts +28 -0
  129. package/dist/r3f/jokerRegistry.js +40 -0
  130. package/dist/r3f/jokerTilt.d.ts +8 -0
  131. package/dist/r3f/jokerTilt.js +41 -0
  132. package/dist/r3f/magneticTilt.d.ts +18 -0
  133. package/dist/r3f/magneticTilt.js +34 -0
  134. package/dist/r3f/playingCardTypes.d.ts +24 -0
  135. package/dist/r3f/playingCardTypes.js +32 -0
  136. package/dist/r3f/playingCardVisuals.d.ts +7 -0
  137. package/dist/r3f/playingCardVisuals.js +45 -0
  138. package/dist/r3f/usePlayingCardTexture.d.ts +7 -0
  139. package/dist/r3f/usePlayingCardTexture.js +92 -0
  140. package/dist/sprites/spriteMapper.d.ts +1 -1
  141. package/dist/sprites/spriteMapper.js +14 -33
  142. package/dist/ui/footer.js +5 -5
  143. package/dist/ui/jimboBackground.js +55 -55
  144. package/dist/ui/jimboCopyRow.d.ts +5 -0
  145. package/dist/ui/jimboCopyRow.js +36 -0
  146. package/dist/ui/jimboTabs.js +5 -5
  147. package/dist/ui.d.ts +1 -0
  148. package/dist/ui.js +1 -0
  149. package/dist/utils/gameCardUtils.d.ts +4 -12
  150. package/dist/utils/gameCardUtils.js +9 -43
  151. package/fonts.css +5 -5
  152. package/package.json +4 -3
@@ -0,0 +1,594 @@
1
+ // JamlBuilderV2 — mobile JAML filter builder.
2
+ //
3
+ // ┌──────────────────────┐
4
+ // │ ◂ BACK (full-width orange)
5
+ // ├──────────────────────┤
6
+ // │ Filter name · author │ editable top matter
7
+ // │ │
8
+ // │ MUST ━━━━━━━━━━━━━━━━│ blue zone rail
9
+ // │ [card] [card] [ ? ]│ clause cards + mystery-slot ADD tile
10
+ // │ │
11
+ // │ SHOULD ━━━━━━━━━━━━━━│ red zone rail
12
+ // │ [card] [card] [ ? ]│
13
+ // │ │
14
+ // │ MUST NOT ━━━━━━━━━━━━│ orange zone rail
15
+ // │ [ ? ] │
16
+ // │ │
17
+ // ├──────────────────────┤
18
+ // │ SEARCH (full-width green)
19
+ // └──────────────────────┘
20
+ //
21
+ // Bottom sheet cascade picker:
22
+ // Level 1 (category): Joker / Voucher / Tag / Boss / Tarot / Pack
23
+ // Level 2 (item): grid of sprites from that sheet — tap to commit
24
+ // Level 3 (settings): antes toggles 1-8, score (should-only), commit
25
+ //
26
+ // Snapshot: this is the mockup — no state persisted upward, but structure is real.
27
+
28
+ const { useState: bUS, useRef: bUR, useEffect: bUE, useMemo: bUM } = React;
29
+ const Col = window.JimboColor;
30
+
31
+ // ── ZONE META ──
32
+ const ZONES = {
33
+ must: { label: 'MUST', hint: 'Seed must contain all of these.', color: Col.BLUE, accent: '#4db5ff' },
34
+ should: { label: 'SHOULD', hint: 'Bonus points per match.', color: Col.RED, accent: '#ff8076' },
35
+ mustnot: { label: 'MUST NOT', hint: 'Seed is rejected if any appear.', color: Col.ORANGE, accent: '#ffb84d' },
36
+ };
37
+
38
+ // ── CATEGORY META (for the picker) ──
39
+ const CATEGORIES = [
40
+ { id: 'joker', label: 'JOKER', sheet: 'jokers', render: (n,s) => <JokerMini name={n} size={s} />, detect: 'jokers' },
41
+ { id: 'souljoker', label: 'SOUL', sheet: 'jokers', render: (n,s) => <JokerMini name={n} size={s} />, filter: ['perkeo','triboulet','yorick','chicot','canio'] },
42
+ { id: 'voucher', label: 'VOUCHER', sheet: 'vouchers', render: (n,s) => <VoucherMini name={n} size={s} /> },
43
+ { id: 'smallblindtag', label: 'SMALL TAG',sheet: 'tags', render: (n,s) => <TagChip name={n} size={s} /> },
44
+ { id: 'bigblindtag', label: 'BIG TAG', sheet: 'tags', render: (n,s) => <TagChip name={n} size={s} /> },
45
+ { id: 'boss', label: 'BOSS', sheet: 'blinds', render: (n,s) => <BossChip name={n} size={s} /> },
46
+ { id: 'tarot', label: 'TAROT', sheet: 'tarots', render: (n,s) => <TarotMini name={n} size={s} /> },
47
+ ];
48
+
49
+ // ── Chunky 3D Button (DEPRECATED — use window.BalButton; kept only for zone commit color overrides) ──
50
+ function ChunkyButton({ color = Col.ORANGE, shadow = Col.DARK_ORANGE, children, onClick, style, fullWidth, small }) {
51
+ const [pressed, setPressed] = bUS(false);
52
+ return (
53
+ <div
54
+ onMouseDown={() => setPressed(true)}
55
+ onMouseUp={() => setPressed(false)}
56
+ onMouseLeave={() => setPressed(false)}
57
+ onTouchStart={() => setPressed(true)}
58
+ onTouchEnd={() => setPressed(false)}
59
+ onClick={onClick}
60
+ style={{
61
+ display: 'inline-block',
62
+ width: fullWidth ? '100%' : 'auto',
63
+ cursor: 'pointer', userSelect: 'none',
64
+ paddingBottom: pressed ? 0 : 4,
65
+ transition: 'padding-bottom 60ms ease-out',
66
+ ...style,
67
+ }}
68
+ >
69
+ <div style={{
70
+ position: 'relative',
71
+ background: shadow,
72
+ borderRadius: 6,
73
+ paddingBottom: pressed ? 0 : 4,
74
+ boxShadow: `inset 0 -4px 0 ${shadow}`,
75
+ }}>
76
+ <div style={{
77
+ background: color,
78
+ borderRadius: 6,
79
+ padding: small ? '6px 12px' : '12px 16px',
80
+ transform: pressed ? 'translateY(4px)' : 'translateY(0)',
81
+ transition: 'transform 60ms ease-out',
82
+ fontFamily: 'm6x11plus, monospace',
83
+ color: Col.WHITE,
84
+ letterSpacing: 2,
85
+ fontSize: small ? 12 : 16,
86
+ textAlign: 'center',
87
+ textShadow: `0 1px 0 ${shadow}`,
88
+ boxShadow: `inset 0 2px 0 rgba(255,255,255,.18), inset 0 -2px 0 rgba(0,0,0,.22)`,
89
+ }}>{children}</div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // ── Ante chip pill (1..8) ──
96
+ function AnteChip({ n, active, onToggle }) {
97
+ return (
98
+ <div
99
+ onClick={onToggle}
100
+ style={{
101
+ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
102
+ background: active ? Col.GOLD : Col.DARKEST,
103
+ border: `2px solid ${active ? Col.GOLD_TEXT : Col.PANEL_EDGE}`,
104
+ borderRadius: 14,
105
+ fontFamily: 'm6x11plus, monospace', fontSize: 13,
106
+ color: active ? Col.BLACK : Col.GREY,
107
+ cursor: 'pointer', userSelect: 'none',
108
+ boxShadow: active ? `0 0 6px ${Col.GOLD}aa` : 'none',
109
+ }}
110
+ >{n}</div>
111
+ );
112
+ }
113
+
114
+ // ── Preview renderer for a clause item ──
115
+ function clauseSprite(c, size = 40) {
116
+ const cat = CATEGORIES.find(x => x.id === c.type);
117
+ if (!cat) return null;
118
+ return cat.render(c.value, size);
119
+ }
120
+
121
+ // ── ClauseCard — horizontal compact card ──
122
+ function ClauseCard({ clause, zoneKey, onRemove, onEdit }) {
123
+ const z = ZONES[zoneKey];
124
+ return (
125
+ <div
126
+ onClick={onEdit}
127
+ style={{
128
+ position: 'relative',
129
+ background: Col.DARK_GREY,
130
+ border: `2px solid ${z.color}`,
131
+ borderRadius: 6,
132
+ padding: '8px 8px 8px 6px',
133
+ display: 'flex', alignItems: 'center', gap: 8,
134
+ minWidth: 0, cursor: 'pointer',
135
+ boxShadow: `0 2px 0 ${Col.BLACK}`,
136
+ }}
137
+ >
138
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 44, height: 50, flexShrink: 0 }}>
139
+ {clauseSprite(clause, 40)}
140
+ </div>
141
+ <div style={{ flex: 1, minWidth: 0 }}>
142
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 13, color: Col.WHITE, letterSpacing: 0.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
143
+ {clause.label || clause.value}
144
+ </div>
145
+ <div style={{ display: 'flex', gap: 3, marginTop: 3, alignItems: 'center', flexWrap: 'wrap' }}>
146
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 8, color: Col.GREY, letterSpacing: 1 }}>A</div>
147
+ {clause.antes.map(a => (
148
+ <div key={a} style={{
149
+ fontFamily: 'm6x11plus, monospace', fontSize: 9,
150
+ padding: '1px 4px', background: Col.DARKEST, color: z.accent,
151
+ borderRadius: 3, letterSpacing: 0.5, lineHeight: 1,
152
+ }}>{a}</div>
153
+ ))}
154
+ {zoneKey === 'should' && clause.score != null && (
155
+ <div style={{
156
+ marginLeft: 4,
157
+ fontFamily: 'm6x11plus, monospace', fontSize: 9,
158
+ padding: '1px 5px', background: Col.RED, color: Col.WHITE,
159
+ borderRadius: 3, letterSpacing: 0.5, lineHeight: 1,
160
+ }}>+{clause.score}</div>
161
+ )}
162
+ </div>
163
+ </div>
164
+ <button
165
+ onClick={(e) => { e.stopPropagation(); onRemove(); }}
166
+ style={{
167
+ width: 22, height: 22, flexShrink: 0,
168
+ border: `2px solid ${Col.BLACK}`, borderRadius: 4,
169
+ background: Col.RED, color: Col.WHITE,
170
+ fontFamily: 'm6x11plus, monospace', fontSize: 12, lineHeight: 1,
171
+ cursor: 'pointer', padding: 0,
172
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,.2), 0 2px 0 ${Col.BLACK}`,
173
+ }}
174
+ >×</button>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ // ── MysteryAddTile — the "?" placeholder ──
180
+ function MysteryAddTile({ zoneKey, onTap }) {
181
+ const z = ZONES[zoneKey];
182
+ return (
183
+ <div
184
+ onClick={onTap}
185
+ style={{
186
+ cursor: 'pointer',
187
+ border: `2px dashed ${z.color}`,
188
+ borderRadius: 6,
189
+ padding: '12px 8px',
190
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
191
+ background: `${z.color}0d`,
192
+ minHeight: 60,
193
+ }}
194
+ >
195
+ <div style={{
196
+ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center',
197
+ background: Col.DARKEST, border: `2px solid ${z.color}`, borderRadius: 6,
198
+ fontFamily: 'm6x11plus, monospace', fontSize: 24, color: z.color,
199
+ }}>?</div>
200
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 12, color: z.accent, letterSpacing: 2 }}>
201
+ ADD TO {z.label}
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ // ── ZoneRail ──
208
+ function ZoneRail({ zoneKey, clauses, onAdd, onRemove, onEdit }) {
209
+ const z = ZONES[zoneKey];
210
+ return (
211
+ <div>
212
+ {/* Zone header: colored pill + hint */}
213
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
214
+ <div style={{
215
+ fontFamily: 'm6x11plus, monospace', fontSize: 11,
216
+ padding: '2px 8px',
217
+ background: z.color, color: Col.WHITE,
218
+ borderRadius: 3, letterSpacing: 2,
219
+ boxShadow: `0 2px 0 ${Col.BLACK}`,
220
+ }}>{z.label}</div>
221
+ <div style={{ flex: 1, height: 2, background: `${z.color}55`, borderRadius: 1 }} />
222
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 8, color: Col.GREY }}>
223
+ {clauses.length}
224
+ </div>
225
+ </div>
226
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 9, color: Col.GREY, letterSpacing: 0.5, marginBottom: 8 }}>
227
+ {z.hint}
228
+ </div>
229
+
230
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
231
+ {clauses.map(c => (
232
+ <ClauseCard
233
+ key={c.id}
234
+ clause={c}
235
+ zoneKey={zoneKey}
236
+ onRemove={() => onRemove(c.id)}
237
+ onEdit={() => onEdit(c)}
238
+ />
239
+ ))}
240
+ <MysteryAddTile zoneKey={zoneKey} onTap={onAdd} />
241
+ </div>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ // ── PICKER: cascade bottom sheet ──
247
+ function PickerSheet({ open, zoneKey, editing, onClose, onCommit }) {
248
+ // Steps: 'cat' (category) → 'item' (sprite grid) → 'settings' (antes/score/commit)
249
+ const [step, setStep] = bUS('cat');
250
+ const [cat, setCat] = bUS(null); // selected CATEGORIES entry
251
+ const [item, setItem] = bUS(null); // selected item name
252
+ const [antes, setAntes] = bUS([1]);
253
+ const [score, setScore] = bUS(1);
254
+
255
+ bUE(() => {
256
+ if (open) {
257
+ if (editing) {
258
+ setCat(CATEGORIES.find(c => c.id === editing.type));
259
+ setItem(editing.value);
260
+ setAntes(editing.antes || [1]);
261
+ setScore(editing.score ?? 1);
262
+ setStep('settings');
263
+ } else {
264
+ setStep('cat');
265
+ setCat(null);
266
+ setItem(null);
267
+ setAntes([1]);
268
+ setScore(1);
269
+ }
270
+ }
271
+ }, [open, editing]);
272
+
273
+ if (!open) return null;
274
+
275
+ const z = ZONES[zoneKey];
276
+ const toggleAnte = (a) => setAntes(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a].sort((x,y)=>x-y));
277
+
278
+ const commit = () => {
279
+ onCommit({
280
+ id: editing?.id || `c${Math.random().toString(36).slice(2,7)}`,
281
+ type: cat.id,
282
+ value: item,
283
+ antes: antes.length ? antes : [1],
284
+ score: zoneKey === 'should' ? score : undefined,
285
+ label: item,
286
+ });
287
+ };
288
+
289
+ // Build item list for the current category
290
+ const items = bUM(() => {
291
+ if (!cat) return [];
292
+ if (cat.filter) return cat.filter;
293
+ const map = window.SPRITE_MAPS?.[cat.sheet];
294
+ if (map) return Object.keys(map);
295
+ return [];
296
+ }, [cat]);
297
+
298
+ return (
299
+ <>
300
+ {/* Scrim */}
301
+ <div
302
+ onClick={onClose}
303
+ style={{
304
+ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 80,
305
+ animation: 'fadeIn 180ms ease-out',
306
+ }}
307
+ />
308
+ {/* Sheet */}
309
+ <div style={{
310
+ position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 81,
311
+ background: Col.DARKEST,
312
+ borderTop: `3px solid ${z.color}`,
313
+ borderRadius: '14px 14px 0 0',
314
+ boxShadow: `0 -8px 24px rgba(0,0,0,.5)`,
315
+ animation: 'slideUp 220ms cubic-bezier(.2,.8,.2,1)',
316
+ maxHeight: '80%', display: 'flex', flexDirection: 'column',
317
+ }}>
318
+ {/* Handle */}
319
+ <div style={{ display: 'flex', justifyContent: 'center', padding: '6px 0 2px' }}>
320
+ <div style={{ width: 36, height: 4, background: Col.GREY, borderRadius: 2 }} />
321
+ </div>
322
+
323
+ {/* Header */}
324
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 12px 8px', borderBottom: `2px solid ${Col.BLACK}` }}>
325
+ {step !== 'cat' && (
326
+ <button onClick={() => setStep(step === 'settings' ? 'item' : 'cat')} style={{
327
+ background: Col.DARK_GREY, border: `2px solid ${Col.BLACK}`, borderRadius: 4,
328
+ width: 26, height: 26, color: Col.WHITE, cursor: 'pointer', padding: 0,
329
+ fontFamily: 'm6x11plus, monospace', fontSize: 14,
330
+ }}>◂</button>
331
+ )}
332
+ <div style={{
333
+ fontFamily: 'm6x11plus, monospace', fontSize: 11,
334
+ padding: '2px 8px', background: z.color, color: Col.WHITE,
335
+ borderRadius: 3, letterSpacing: 2,
336
+ }}>{z.label}</div>
337
+ <div style={{ flex: 1, fontFamily: 'm6x11plus, monospace', fontSize: 13, color: Col.WHITE, letterSpacing: 1 }}>
338
+ {step === 'cat' && 'Pick type'}
339
+ {step === 'item' && cat?.label}
340
+ {step === 'settings' && (item || '—')}
341
+ </div>
342
+ <button onClick={onClose} style={{
343
+ background: Col.DARK_GREY, border: `2px solid ${Col.BLACK}`, borderRadius: 4,
344
+ width: 26, height: 26, color: Col.WHITE, cursor: 'pointer', padding: 0,
345
+ fontFamily: 'm6x11plus, monospace', fontSize: 12,
346
+ }}>×</button>
347
+ </div>
348
+
349
+ {/* Body */}
350
+ <div style={{ flex: 1, overflowY: 'auto', padding: 12 }}>
351
+ {step === 'cat' && (
352
+ <div style={{
353
+ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8,
354
+ }}>
355
+ {CATEGORIES.map(c => (
356
+ <div
357
+ key={c.id}
358
+ onClick={() => { setCat(c); setStep('item'); }}
359
+ style={{
360
+ padding: 12, background: Col.DARK_GREY, borderRadius: 6,
361
+ border: `2px solid ${Col.PANEL_EDGE}`,
362
+ display: 'flex', alignItems: 'center', gap: 10,
363
+ cursor: 'pointer',
364
+ boxShadow: `0 2px 0 ${Col.BLACK}`,
365
+ }}
366
+ >
367
+ <div style={{ width: 32, height: 38, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
368
+ {c.render(
369
+ c.id === 'joker' ? 'joker' :
370
+ c.id === 'souljoker' ? 'perkeo' :
371
+ c.id === 'voucher' ? 'overstock' :
372
+ c.id === 'smallblindtag' || c.id === 'bigblindtag' ? 'uncommontag' :
373
+ c.id === 'boss' ? 'thehook' :
374
+ c.id === 'tarot' ? 'thefool' : '',
375
+ 28,
376
+ )}
377
+ </div>
378
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 12, color: Col.WHITE, letterSpacing: 1.5 }}>
379
+ {c.label}
380
+ </div>
381
+ </div>
382
+ ))}
383
+ </div>
384
+ )}
385
+
386
+ {step === 'item' && cat && (
387
+ <div style={{
388
+ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 6,
389
+ }}>
390
+ {items.map(name => (
391
+ <div
392
+ key={name}
393
+ onClick={() => { setItem(name); setStep('settings'); }}
394
+ style={{
395
+ padding: 4,
396
+ background: item === name ? `${z.color}33` : Col.DARK_GREY,
397
+ border: `2px solid ${item === name ? z.color : Col.PANEL_EDGE}`,
398
+ borderRadius: 5,
399
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
400
+ cursor: 'pointer', minHeight: 60,
401
+ boxShadow: `0 2px 0 ${Col.BLACK}`,
402
+ }}
403
+ >
404
+ {cat.render(name, 36)}
405
+ </div>
406
+ ))}
407
+ {!items.length && (
408
+ <div style={{ gridColumn: '1/-1', color: Col.GREY, fontSize: 11, fontFamily: 'm6x11plus, monospace', textAlign: 'center', padding: 20 }}>
409
+ Loading sprites…
410
+ </div>
411
+ )}
412
+ </div>
413
+ )}
414
+
415
+ {step === 'settings' && cat && item && (
416
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
417
+ {/* Preview */}
418
+ <div style={{
419
+ display: 'flex', alignItems: 'center', gap: 12,
420
+ padding: 10, background: Col.DARK_GREY, borderRadius: 6,
421
+ border: `2px solid ${z.color}`,
422
+ }}>
423
+ {cat.render(item, 52)}
424
+ <div style={{ flex: 1 }}>
425
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 15, color: Col.WHITE, letterSpacing: 0.5 }}>{item}</div>
426
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 10, color: Col.GREY, letterSpacing: 1 }}>{cat.label}</div>
427
+ </div>
428
+ </div>
429
+
430
+ {/* Antes */}
431
+ <div>
432
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 11, color: Col.GREY, letterSpacing: 2, marginBottom: 6 }}>
433
+ SEARCH ANTES
434
+ </div>
435
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
436
+ {[1,2,3,4,5,6,7,8].map(n => (
437
+ <AnteChip key={n} n={n} active={antes.includes(n)} onToggle={() => toggleAnte(n)} />
438
+ ))}
439
+ </div>
440
+ </div>
441
+
442
+ {/* Score (should only) */}
443
+ {zoneKey === 'should' && (
444
+ <div>
445
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 11, color: Col.GREY, letterSpacing: 2, marginBottom: 6 }}>
446
+ SCORE ON MATCH
447
+ </div>
448
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
449
+ <button onClick={() => setScore(Math.max(0, score - 1))} style={stepperBtn}>–</button>
450
+ <div style={{
451
+ fontFamily: 'm6x11plus, monospace', fontSize: 22, color: Col.RED,
452
+ minWidth: 52, textAlign: 'center',
453
+ border: `2px solid ${Col.RED}`, borderRadius: 6,
454
+ padding: '4px 10px', background: Col.DARKEST,
455
+ }}>+{score}</div>
456
+ <button onClick={() => setScore(score + 1)} style={stepperBtn}>+</button>
457
+ </div>
458
+ </div>
459
+ )}
460
+
461
+ {/* Commit button — color matches zone */}
462
+ <window.BalButton
463
+ fullWidth
464
+ color={z.color}
465
+ shadow={zoneKey === 'must' ? Col.DARK_BLUE : zoneKey === 'should' ? Col.DARK_RED : Col.DARK_ORANGE}
466
+ onClick={commit}
467
+ >{editing ? 'Update' : 'Add'}</window.BalButton>
468
+ </div>
469
+ )}
470
+ </div>
471
+ </div>
472
+ </>
473
+ );
474
+ }
475
+
476
+ const stepperBtn = {
477
+ width: 38, height: 38,
478
+ background: Col.DARK_GREY, border: `2px solid ${Col.BLACK}`, borderRadius: 6,
479
+ color: Col.WHITE, fontFamily: 'm6x11plus, monospace', fontSize: 20, cursor: 'pointer',
480
+ boxShadow: `inset 0 1px 0 rgba(255,255,255,.15), 0 2px 0 ${Col.BLACK}`,
481
+ padding: 0,
482
+ };
483
+
484
+ // ── Top matter: filter name / author / description ──
485
+ function TopMatter({ name, author, desc }) {
486
+ return (
487
+ <div style={{
488
+ background: Col.DARK_GREY, borderRadius: 6, padding: 10,
489
+ border: `2px solid ${Col.PANEL_EDGE}`,
490
+ boxShadow: `0 2px 0 ${Col.BLACK}`,
491
+ }}>
492
+ <input
493
+ defaultValue={name}
494
+ style={{
495
+ display: 'block', width: '100%', background: 'transparent', border: 'none', outline: 'none',
496
+ fontFamily: 'm6x11plus, monospace', fontSize: 18, color: Col.WHITE, letterSpacing: 1,
497
+ padding: 0, marginBottom: 4,
498
+ }}
499
+ />
500
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
501
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 9, color: Col.GREY, letterSpacing: 2 }}>BY</div>
502
+ <input
503
+ defaultValue={author}
504
+ style={{
505
+ flex: 1, background: 'transparent', border: 'none', outline: 'none',
506
+ fontFamily: 'm6x11plus, monospace', fontSize: 12, color: Col.GOLD_TEXT, letterSpacing: 1,
507
+ padding: 0,
508
+ }}
509
+ />
510
+ </div>
511
+ <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 10, color: Col.GREY, marginTop: 6, lineHeight: 1.35 }}>
512
+ {desc}
513
+ </div>
514
+ </div>
515
+ );
516
+ }
517
+
518
+ // ── Main ──
519
+ function JamlBuilderV2({ initial }) {
520
+ const base = initial || window.FILTER_V2;
521
+ const [must, setMust] = bUS(() => (base.must || []).map(c => ({ ...c })));
522
+ const [should, setShould] = bUS(() => (base.should || []).map(c => ({ ...c })));
523
+ const [mustnot, setMustNot] = bUS([]);
524
+
525
+ const [picker, setPicker] = bUS({ open: false, zone: null, editing: null });
526
+
527
+ const openAdd = (zone) => setPicker({ open: true, zone, editing: null });
528
+ const openEdit = (zone, c) => setPicker({ open: true, zone, editing: c });
529
+ const close = () => setPicker({ open: false, zone: null, editing: null });
530
+
531
+ const commit = (clause) => {
532
+ const setters = { must: setMust, should: setShould, mustnot: setMustNot };
533
+ const current = { must, should, mustnot }[picker.zone];
534
+ const next = picker.editing
535
+ ? current.map(c => c.id === clause.id ? clause : c)
536
+ : [...current, clause];
537
+ setters[picker.zone](next);
538
+ close();
539
+ };
540
+
541
+ const remove = (zone, id) => {
542
+ const setters = { must: setMust, should: setShould, mustnot: setMustNot };
543
+ const current = { must, should, mustnot }[zone];
544
+ setters[zone](current.filter(c => c.id !== id));
545
+ };
546
+
547
+ return (
548
+ <div style={{
549
+ width: '100%', height: '100%', background: Col.DARKEST,
550
+ position: 'relative', overflow: 'hidden',
551
+ display: 'flex', flexDirection: 'column',
552
+ fontFamily: 'm6x11plus, monospace', color: Col.WHITE,
553
+ }}>
554
+ {/* Scroll body — NO top bar; Back docks to bottom */}
555
+ <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 10, display: 'flex', flexDirection: 'column', gap: 14 }}>
556
+ <TopMatter name={base.name} author={base.author} desc={base.description} />
557
+
558
+ <ZoneRail zoneKey="must" clauses={must} onAdd={() => openAdd('must')} onRemove={(id) => remove('must', id)} onEdit={(c) => openEdit('must', c)} />
559
+ <ZoneRail zoneKey="should" clauses={should} onAdd={() => openAdd('should')} onRemove={(id) => remove('should', id)} onEdit={(c) => openEdit('should', c)} />
560
+ <ZoneRail zoneKey="mustnot" clauses={mustnot} onAdd={() => openAdd('mustnot')} onRemove={(id) => remove('mustnot', id)} onEdit={(c) => openEdit('mustnot', c)} />
561
+
562
+ <div style={{ height: 20 }} />
563
+ </div>
564
+
565
+ {/* Bottom: SEARCH (green, full-width) then BACK (orange, full-width, BOTTOM-MOST, thumb zone) */}
566
+ <div style={{ padding: '8px 10px 10px', borderTop: `2px solid ${Col.BLACK}`, background: Col.DARK_GREY, display: 'flex', flexDirection: 'column', gap: 6 }}>
567
+ <window.BalButton tone="green" fullWidth size="md">Search</window.BalButton>
568
+ <window.BalButton tone="orange" fullWidth size="md">Back</window.BalButton>
569
+ </div>
570
+
571
+ <PickerSheet
572
+ open={picker.open}
573
+ zoneKey={picker.zone || 'must'}
574
+ editing={picker.editing}
575
+ onClose={close}
576
+ onCommit={commit}
577
+ />
578
+ </div>
579
+ );
580
+ }
581
+
582
+ // Keyframes
583
+ (function injectBuilderKf(){
584
+ if (document.getElementById('v2-builder-kf')) return;
585
+ const s = document.createElement('style');
586
+ s.id = 'v2-builder-kf';
587
+ s.textContent = `
588
+ @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
589
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
590
+ `;
591
+ document.head.appendChild(s);
592
+ })();
593
+
594
+ window.JamlBuilderV2 = JamlBuilderV2;