jaml-ui 0.14.2 → 0.14.4

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 (94) hide show
  1. package/DESIGN.md +197 -0
  2. package/dist/components/AnalyzerExplorer.js +0 -11
  3. package/dist/components/JamlAestheticSelector.js +1 -3
  4. package/dist/components/JamlAnalyzerFullscreen.js +3 -3
  5. package/dist/components/JamlIde.js +0 -2
  6. package/dist/components/JamlSeedInput.js +1 -2
  7. package/dist/components/JamlSpeedometer.js +1 -1
  8. package/dist/ui/codeBlock.js +1 -1
  9. package/dist/ui/jimboCopyRow.js +1 -2
  10. package/dist/ui/jimboFilterBar.js +2 -4
  11. package/dist/ui/showcase.js +4 -4
  12. package/package.json +8 -5
  13. package/assets/Balatro Seed Curator (DesignsV2)/.design-canvas.state.json +0 -1
  14. package/assets/Balatro Seed Curator (DesignsV2)/Assets/BlindChips.png +0 -0
  15. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/Boosters.json +0 -303
  16. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters/boosters.png +0 -0
  17. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Boosters.png +0 -0
  18. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/BlindChips.png +0 -0
  19. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Bosses/blinds_metadata.json +0 -51
  20. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/8BitDeck.png +0 -0
  21. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/Enhancers.png +0 -0
  22. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/balatro-stake-chips.png +0 -0
  23. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/enhancers_metadata.json +0 -52
  24. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/playing_cards_metadata.json +0 -249
  25. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Decks/stakes.json +0 -19
  26. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Editions.png +0 -0
  27. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Enhancers.png +0 -0
  28. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Editions.png +0 -0
  29. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/Jokers.png +0 -0
  30. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/jokers.json +0 -1087
  31. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers.png +0 -0
  32. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers/stickers_metadata.json +0 -25
  33. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Jokers.png +0 -0
  34. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.json +0 -191
  35. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tags/tags.png +0 -0
  36. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/Tarots.png +0 -0
  37. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/planets.json +0 -15
  38. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/spectrals.json +0 -21
  39. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots/tarots.json +0 -163
  40. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Tarots.png +0 -0
  41. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/Vouchers.png +0 -0
  42. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers/vouchers.json +0 -130
  43. package/assets/Balatro Seed Curator (DesignsV2)/Assets/Vouchers.png +0 -0
  44. package/assets/Balatro Seed Curator (DesignsV2)/Assets/blinds.json +0 -51
  45. package/assets/Balatro Seed Curator (DesignsV2)/Assets/boosters.json +0 -303
  46. package/assets/Balatro Seed Curator (DesignsV2)/Assets/fonts/m6x11plusplus.otf +0 -0
  47. package/assets/Balatro Seed Curator (DesignsV2)/Assets/jokers.json +0 -1087
  48. package/assets/Balatro Seed Curator (DesignsV2)/Assets/planets.json +0 -15
  49. package/assets/Balatro Seed Curator (DesignsV2)/Assets/spectrals.json +0 -21
  50. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stakes.png +0 -0
  51. package/assets/Balatro Seed Curator (DesignsV2)/Assets/stickers.png +0 -0
  52. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.json +0 -191
  53. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tags.png +0 -0
  54. package/assets/Balatro Seed Curator (DesignsV2)/Assets/tarots.json +0 -163
  55. package/assets/Balatro Seed Curator (DesignsV2)/Assets/vouchers.json +0 -130
  56. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail v2.html +0 -40
  57. package/assets/Balatro Seed Curator (DesignsV2)/Seed Detail.html +0 -34
  58. package/assets/Balatro Seed Curator (DesignsV2)/public/fonts/m6x11plusplus.otf +0 -0
  59. package/assets/Balatro Seed Curator (DesignsV2)/src/AntePage.jsx +0 -228
  60. package/assets/Balatro Seed Curator (DesignsV2)/src/SeedDetail.jsx +0 -222
  61. package/assets/Balatro Seed Curator (DesignsV2)/src/app.jsx +0 -35
  62. package/assets/Balatro Seed Curator (DesignsV2)/src/mockData.js +0 -185
  63. package/assets/Balatro Seed Curator (DesignsV2)/src/sprites.jsx +0 -259
  64. package/assets/Balatro Seed Curator (DesignsV2)/src/tokens.js +0 -49
  65. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/AntePageV2.jsx +0 -290
  66. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/BalButton.jsx +0 -107
  67. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlBuilderV2.jsx +0 -594
  68. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/JamlIde.jsx +0 -302
  69. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SearchResultsV2.jsx +0 -286
  70. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedDetailV2.jsx +0 -336
  71. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/SeedOGCard.jsx +0 -251
  72. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/Showcase.jsx +0 -131
  73. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/app.jsx +0 -55
  74. package/assets/Balatro Seed Curator (DesignsV2)/src/v2/data.js +0 -296
  75. package/assets/Balatro Seed Curator (DesignsV2)/starters/design-canvas.jsx +0 -622
  76. package/assets/Balatro Seed Curator (DesignsV2)/uploads/8BitDeck.png +0 -0
  77. package/assets/Balatro Seed Curator (DesignsV2)/uploads/BlindChips.png +0 -0
  78. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Boosters.png +0 -0
  79. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Editions.png +0 -0
  80. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Enhancers.png +0 -0
  81. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Jokers.png +0 -0
  82. package/assets/Balatro Seed Curator (DesignsV2)/uploads/Tarots.png +0 -0
  83. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749540653-0.png +0 -0
  84. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749644934-0.png +0 -0
  85. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749661871-0.png +0 -0
  86. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749674748-0.png +0 -0
  87. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749703076-0.png +0 -0
  88. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776749882759-0.png +0 -0
  89. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750354200-0.png +0 -0
  90. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776750733265-0.png +0 -0
  91. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776751928925-0.png +0 -0
  92. package/assets/Balatro Seed Curator (DesignsV2)/uploads/pasted-1776800975060-0.png +0 -0
  93. package/assets/Balatro Seed Curator (DesignsV2)/uploads/stickers.png +0 -0
  94. package/assets/Balatro Seed Curator (DesignsV2)/uploads/tags.png +0 -0
@@ -1,336 +0,0 @@
1
- // SeedDetailV2 — phone-size shell around AntePageV2 list.
2
- // ┌──────────────────────┐
3
- // │ TOP BAR │ seed · deck/stake · per-clause score cols
4
- // ├──────────────────────┤
5
- // │ EDGE HIT PILLS │ COD-style edge indicators for hits off-screen
6
- // │ │
7
- // │ ante 1 │ scroll-snapped vertical list of AntePageV2
8
- // │ ante 2 │
9
- // │ … │
10
- // │ ante 8 │
11
- // └──────────────────────┘
12
-
13
- const { useState: sUS, useRef: sUR, useEffect: sUE, useLayoutEffect: sULE, useMemo: sUM, useCallback: sUCB } = React;
14
- const Cv = window.JimboColor;
15
-
16
- // ── ScoreChip — shoulds: bare sprite + count badge (count IS the data)
17
- // · musts: framed box with ✓/✗ (presence IS the data)
18
- function ScoreCol({ clause, hits }) {
19
- const lit = hits > 0;
20
- const must = clause._kind === 'must';
21
- // Balatro metaphor: must = BLUE (chips, the base), should = RED (mult, the bonus)
22
- const color = must ? Cv.BLUE : lit ? Cv.RED : Cv.GREY;
23
-
24
- const spriteKind =
25
- clause.type === 'joker' || clause.type === 'souljoker' ? 'jokers' :
26
- clause.type === 'voucher' ? 'vouchers' :
27
- clause.type === 'smallblindtag' || clause.type === 'bigblindtag' ? 'tags' :
28
- clause.type === 'boss' ? 'blinds' : null;
29
-
30
- const w = 30, h = spriteKind === 'tags' || spriteKind === 'blinds' ? 30 : 40;
31
- const sprite = (
32
- spriteKind === 'jokers' ? <Sprite sheet="jokers" name={clause.value} width={w} height={h} /> :
33
- spriteKind === 'vouchers' ? <Sprite sheet="vouchers" name={clause.value} width={w} height={h} /> :
34
- spriteKind === 'tags' ? <Sprite sheet="tags" name={clause.value} width={h} height={h} /> :
35
- spriteKind === 'blinds' ? <BossChip name={clause.value} size={h} /> : null
36
- );
37
-
38
- if (must) {
39
- // Framed: the box + ✓/✗ IS the signal
40
- return (
41
- <div style={{
42
- position: 'relative',
43
- padding: '4px 4px 2px',
44
- border: `2px solid ${lit ? color : Cv.DARK_GREY}`,
45
- borderRadius: 5,
46
- background: lit ? `${color}18` : Cv.DARKEST,
47
- display: 'flex', alignItems: 'center', justifyContent: 'center',
48
- opacity: lit ? 1 : 0.6,
49
- }}>
50
- {sprite}
51
- <div style={{
52
- position: 'absolute', bottom: -8, left: '50%', transform: 'translateX(-50%)',
53
- width: 16, height: 16,
54
- background: Cv.DARKEST, border: `2px solid ${color}`, borderRadius: 8,
55
- fontFamily: 'm6x11plus, monospace', fontSize: 11, lineHeight: '12px',
56
- color, textAlign: 'center',
57
- }}>
58
- {lit ? '✓' : '✗'}
59
- </div>
60
- </div>
61
- );
62
- }
63
- // Should: bare sprite + corner count
64
- return (
65
- <div style={{
66
- position: 'relative', display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
67
- opacity: lit ? 1 : 0.4, filter: lit ? 'none' : 'grayscale(0.6)',
68
- }}>
69
- {sprite}
70
- <div style={{
71
- position: 'absolute', bottom: -4, right: -4,
72
- minWidth: 16, height: 16, padding: '0 3px',
73
- background: Cv.DARKEST,
74
- fontFamily: 'm6x11plus, monospace', fontSize: 11, lineHeight: '16px',
75
- color, textAlign: 'center',
76
- borderRadius: 8,
77
- }}>
78
- ×{hits}
79
- </div>
80
- </div>
81
- );
82
- }
83
-
84
- function TopBar({ seed, filter }) {
85
- const allClauses = [...filter.must.map(c => ({ ...c, _kind: 'must' })), ...filter.should.map(c => ({ ...c, _kind: 'should' }))];
86
- return (
87
- <div style={{
88
- background: Cv.DARKEST,
89
- borderBottom: `2px solid ${Cv.BLACK}`,
90
- padding: '8px 10px 8px',
91
- position: 'sticky', top: 0, zIndex: 30,
92
- boxShadow: '0 2px 0 #000',
93
- }}>
94
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
95
- <button style={btn}>
96
- <svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M9 2L4 7l5 5"/></svg>
97
- </button>
98
- <div style={{ textAlign: 'center', flex: 1 }}>
99
- <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 18, color: Cv.WHITE, letterSpacing: 3, lineHeight: 1 }}>{seed.seed}</div>
100
- <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 9, color: Cv.GREY, letterSpacing: 1.5, marginTop: 2 }}>{seed.deck} · {seed.stake} · score {seed.score.totalScore}</div>
101
- </div>
102
- <button style={btn}>
103
- <svg width="12" height="12" viewBox="0 0 14 14" fill="currentColor"><circle cx="3" cy="7" r="1.3"/><circle cx="7" cy="7" r="1.3"/><circle cx="11" cy="7" r="1.3"/></svg>
104
- </button>
105
- </div>
106
-
107
- <div style={{ display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'flex-end', padding: '0 4px' }}>
108
- {allClauses.map(c => (
109
- <ScoreCol key={c.id} clause={c} hits={seed.score.totals[c.id] || 0} />
110
- ))}
111
- </div>
112
- </div>
113
- );
114
- }
115
-
116
- const btn = {
117
- width: 26, height: 26, border: `2px solid ${Cv.BLACK}`, borderRadius: 4, background: Cv.DARK_GREY,
118
- color: Cv.WHITE, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
119
- boxShadow: `inset 0 1px 0 rgba(255,255,255,.08), 0 2px 0 ${Cv.BLACK}`, padding: 0,
120
- };
121
-
122
- // ── EdgeHitIndicator — COD-style: when a hit is scrolled off the top/bottom,
123
- // render a pinned arrow at the edge with a small joker sprite.
124
- function EdgeHitIndicator({ direction, item, onTap }) {
125
- const name = item.type === 'pack' ? 'arcana' : item.value;
126
- const sheet = item.type === 'pack' ? 'boosters' : 'jokers';
127
- const hit = item._bestHit;
128
- const color = hitColor(hit);
129
- const pointUp = direction === 'up';
130
- return (
131
- <div
132
- onClick={onTap}
133
- style={{
134
- pointerEvents: 'auto',
135
- display: 'flex', alignItems: 'center', gap: 6,
136
- padding: '3px 8px 3px 4px',
137
- background: `${Cv.DARKEST}ee`,
138
- border: `2px solid ${color}`,
139
- borderRadius: pointUp ? '0 0 14px 14px' : '14px 14px 0 0',
140
- cursor: 'pointer',
141
- animation: 'edgeBob 1.4s ease-in-out infinite',
142
- boxShadow: `0 0 10px ${color}aa`,
143
- }}
144
- title={`${item.value} · Ante ${item.ante} · tap to scroll`}
145
- >
146
- <div style={{
147
- width: 20, height: 20, overflow: 'hidden', borderRadius: 3,
148
- border: `1px solid ${color}`,
149
- }}>
150
- <div style={{ transform: 'scale(1.6)', transformOrigin: '50% 30%' }}>
151
- <Sprite sheet={sheet} name={name} width={20} height={27} />
152
- </div>
153
- </div>
154
- <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}>
155
- <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 9, color: Cv.WHITE, letterSpacing: 0.5, whiteSpace: 'nowrap', maxWidth: 74, overflow: 'hidden', textOverflow: 'ellipsis' }}>
156
- {item.value}
157
- </div>
158
- <div style={{ fontFamily: 'm6x11plus, monospace', fontSize: 8, color, letterSpacing: 1 }}>
159
- A{item.ante}·{item.where}
160
- </div>
161
- </div>
162
- <svg width="10" height="10" viewBox="0 0 10 10" style={{ color }}>
163
- {pointUp
164
- ? <path d="M5 2 L9 7 L1 7 Z" fill="currentColor" />
165
- : <path d="M5 8 L1 3 L9 3 Z" fill="currentColor" />}
166
- </svg>
167
- </div>
168
- );
169
- }
170
-
171
- // ── SeedBody — scroll container + vertical ante list + edge indicators
172
- function SeedBody({ seed, filter }) {
173
- const scrollRef = sUR(null);
174
- const [edgeHits, setEdgeHits] = sUS({ up: [], down: [] });
175
-
176
- // Collect all hits in this seed, with where-info for labels.
177
- const allHits = sUM(() => {
178
- const out = [];
179
- seed.antes.forEach(a => {
180
- a.shopQueue.forEach((it, i) => {
181
- if (it.hits?.length) out.push({ value: it.value, type: 'joker', ante: a.ante, where: `Shop#${i+1}`, _bestHit: bestHit(it.hits), _findEl: () => it._el });
182
- });
183
- a.boosterPacks.forEach((p, pi) => {
184
- p.itemHits?.forEach((hArr, ii) => {
185
- if (hArr.length) out.push({ value: p.items[ii], type: 'joker', ante: a.ante, where: `P${pi+1}·${ii+1}`, _bestHit: bestHit(hArr), _findEl: () => p._el });
186
- });
187
- });
188
- if (a._soulHits?.length) out.push({ value: a.soulJoker.value, type: 'joker', ante: a.ante, where: 'Soul', _bestHit: bestHit(a._soulHits), _findEl: () => a._soulEl });
189
- });
190
- return out;
191
- }, [seed]);
192
-
193
- sUE(() => {
194
- const el = scrollRef.current;
195
- if (!el) return;
196
- const onScroll = () => {
197
- const top = el.scrollTop;
198
- const bottom = top + el.clientHeight;
199
- const up = [], down = [];
200
- for (const h of allHits) {
201
- const node = h._findEl?.();
202
- if (!node) continue;
203
- const rect = node.getBoundingClientRect();
204
- const parentRect = el.getBoundingClientRect();
205
- const yTop = rect.top - parentRect.top + el.scrollTop;
206
- const yBot = yTop + rect.height;
207
- if (yBot < top + 40) up.push(h);
208
- else if (yTop > bottom - 40) down.push(h);
209
- }
210
- setEdgeHits({ up: up.slice(-3).reverse(), down: down.slice(0, 3) });
211
- };
212
- onScroll();
213
- el.addEventListener('scroll', onScroll, { passive: true });
214
- const id = setInterval(onScroll, 400); // catch layout settling
215
- return () => { el.removeEventListener('scroll', onScroll); clearInterval(id); };
216
- }, [allHits]);
217
-
218
- const jumpTo = (hit) => {
219
- const el = scrollRef.current;
220
- const node = hit._findEl?.();
221
- if (!el || !node) return;
222
- const parentRect = el.getBoundingClientRect();
223
- const rect = node.getBoundingClientRect();
224
- const target = el.scrollTop + (rect.top - parentRect.top) - 120;
225
- el.scrollTo({ top: target, behavior: 'smooth' });
226
- };
227
-
228
- return (
229
- <div style={{ position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden' }}>
230
- {/* Scroll container */}
231
- <div
232
- ref={scrollRef}
233
- className="v2-shop-scroll"
234
- style={{
235
- height: '100%',
236
- overflowY: 'auto',
237
- overflowX: 'hidden',
238
- scrollSnapType: 'y proximity',
239
- scrollbarWidth: 'none',
240
- }}
241
- >
242
- {seed.antes.map((a, i) => (
243
- <div key={i} style={{ scrollSnapAlign: 'start' }}>
244
- <AntePageV2 ante={a} />
245
- {i < seed.antes.length - 1 && (
246
- <div style={{
247
- height: 2, margin: '0 14px',
248
- background: `repeating-linear-gradient(90deg, ${Cv.PANEL_EDGE} 0 6px, transparent 6px 12px)`,
249
- }} />
250
- )}
251
- </div>
252
- ))}
253
- </div>
254
-
255
- {/* Edge indicators */}
256
- <div style={{
257
- position: 'absolute', top: 0, left: 0, right: 0,
258
- display: 'flex', justifyContent: 'center', gap: 6, padding: '0 8px',
259
- pointerEvents: 'none', zIndex: 20,
260
- flexWrap: 'wrap',
261
- }}>
262
- {edgeHits.up.map((h, i) => <EdgeHitIndicator key={`u${i}`} direction="up" item={h} onTap={() => jumpTo(h)} />)}
263
- </div>
264
- <div style={{
265
- position: 'absolute', bottom: 0, left: 0, right: 0,
266
- display: 'flex', justifyContent: 'center', gap: 6, padding: '0 8px',
267
- pointerEvents: 'none', zIndex: 20,
268
- flexWrap: 'wrap',
269
- }}>
270
- {edgeHits.down.map((h, i) => <EdgeHitIndicator key={`d${i}`} direction="down" item={h} onTap={() => jumpTo(h)} />)}
271
- </div>
272
- </div>
273
- );
274
- }
275
-
276
- // ── SeedDetailV2 — horizontal seed swiper + body
277
- function SeedDetailV2({ seeds, filter }) {
278
- const [idx, setIdx] = sUS(0);
279
- const [drag, setDrag] = sUS({ dx: 0, active: false });
280
- const touch = sUR({ x0: 0, y0: 0, locked: null });
281
-
282
- const onDown = (e) => {
283
- const t = e.touches ? e.touches[0] : e;
284
- touch.current = { x0: t.clientX, y0: t.clientY, locked: null };
285
- };
286
- const onMove = (e) => {
287
- if (!touch.current.x0) return;
288
- const t = e.touches ? e.touches[0] : e;
289
- const dx = t.clientX - touch.current.x0;
290
- const dy = t.clientY - touch.current.y0;
291
- if (touch.current.locked == null) {
292
- if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy) * 1.5) touch.current.locked = 'x';
293
- else if (Math.abs(dy) > 8) touch.current.locked = 'y';
294
- }
295
- if (touch.current.locked === 'x') setDrag({ dx, active: true });
296
- };
297
- const onUp = () => {
298
- if (touch.current.locked === 'x') {
299
- const dx = drag.dx;
300
- if (dx < -70 && idx < seeds.length - 1) setIdx(idx + 1);
301
- else if (dx > 70 && idx > 0) setIdx(idx - 1);
302
- }
303
- touch.current = { x0: 0, y0: 0, locked: null };
304
- setDrag({ dx: 0, active: false });
305
- };
306
-
307
- const seed = seeds[idx];
308
-
309
- return (
310
- <div style={{
311
- width: '100%', height: '100%', background: Cv.DARKEST,
312
- display: 'flex', flexDirection: 'column', overflow: 'hidden',
313
- fontFamily: 'm6x11plus, monospace', color: Cv.WHITE,
314
- }}>
315
- <div onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}
316
- style={{ touchAction: 'pan-y' }}>
317
- <TopBar seed={seed} filter={filter} />
318
- </div>
319
- <div style={{
320
- flex: 1, minHeight: 0, position: 'relative',
321
- transform: drag.active ? `translateX(${drag.dx * 0.25}px) rotate(${drag.dx * 0.015}deg)` : 'none',
322
- transition: drag.active ? 'none' : 'transform 260ms cubic-bezier(.33,1,.4,1)',
323
- }}>
324
- <SeedBody key={seed.seed} seed={seed} filter={filter} />
325
- </div>
326
- {/* Seed dots */}
327
- <div style={{ position: 'absolute', bottom: 4, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 5, zIndex: 40 }}>
328
- {seeds.map((_, i) => (
329
- <div key={i} style={{ width: 6, height: 6, borderRadius: 3, background: i === idx ? Cv.GOLD : Cv.DARK_GREY }} />
330
- ))}
331
- </div>
332
- </div>
333
- );
334
- }
335
-
336
- window.SeedDetailV2 = SeedDetailV2;
@@ -1,251 +0,0 @@
1
- // SeedOGCard — 1200×630 social-preview rollup.
2
- // /seed/[seed]/og.png renders this server-side.
3
- //
4
- // Design rule (from pifreak): DON'T SHOW OFF CARDS THAT AREN'T THERE.
5
- // The point of posting a seed is showing off the JOKERS YOU FOUND, splayed
6
- // out like a Balatro poker hand. Unmatched clauses get a tiny stat line,
7
- // not a grayed-out sprite.
8
- //
9
- // Splay math: mirrors Balatro `cardarea.lua` hand positioning —
10
- // rotate = (i - center) * spread° (≈6° per card, clamped)
11
- // y = abs(i - center) * arcDip (arc dip, not a straight line)
12
- // ambient_tilt: ~0.2 per Balatro/card.lua — each card gets a unique
13
- // phase (i/1.14212) and a gentle sin/cos wobble. Pure CSS anim.
14
- //
15
- // Score columns: red SHOULD line has room for per-clause `+N` pills so
16
- // the share card doubles as a scoring preview.
17
-
18
- const Co = window.JimboColor;
19
-
20
- // Balatro rarity palette
21
- const RARITY_COLOR = {
22
- Common: '#009dff',
23
- Uncommon: '#3bc47e',
24
- Rare: '#fe5f55',
25
- Legendary: '#b26cbd',
26
- };
27
-
28
- // Inject ambient_tilt keyframes once (Balatro card.lua: G.TIMERS.REAL*1.56 + id/1.35 drift)
29
- (function injectOgKf(){
30
- if (document.getElementById('og-card-kf')) return;
31
- const s = document.createElement('style');
32
- s.id = 'og-card-kf';
33
- s.textContent = `
34
- @keyframes ogTilt0 { 0%,100% { transform: var(--base) rotate(var(--r, 0deg)) translateY(var(--ty, 0px)); }
35
- 50% { transform: var(--base) rotate(calc(var(--r, 0deg) + 1.2deg)) translateY(calc(var(--ty, 0px) - 2px)); } }
36
- @keyframes ogTilt1 { 0%,100% { transform: var(--base) rotate(var(--r, 0deg)) translateY(var(--ty, 0px)); }
37
- 50% { transform: var(--base) rotate(calc(var(--r, 0deg) - 1.4deg)) translateY(calc(var(--ty, 0px) + 2px)); } }
38
- @keyframes ogTilt2 { 0%,100% { transform: var(--base) rotate(var(--r, 0deg)) translateY(var(--ty, 0px)); }
39
- 50% { transform: var(--base) rotate(calc(var(--r, 0deg) + 0.9deg)) translateY(calc(var(--ty, 0px) - 1px)); } }
40
- @keyframes ogTilt3 { 0%,100% { transform: var(--base) rotate(var(--r, 0deg)) translateY(var(--ty, 0px)); }
41
- 50% { transform: var(--base) rotate(calc(var(--r, 0deg) - 0.7deg)) translateY(calc(var(--ty, 0px) + 1.5px)); } }
42
- `;
43
- document.head.appendChild(s);
44
- })();
45
-
46
- // One splayed card in the hand. Includes ambient tilt + per-card phase.
47
- function FannedCard({ sprite, label, score, frame, rotate, yOff, zIndex, phase }) {
48
- const tiltKf = ['ogTilt0','ogTilt1','ogTilt2','ogTilt3'][phase % 4];
49
- return (
50
- <div style={{
51
- position: 'relative',
52
- marginLeft: -14, // cards overlap like a hand
53
- zIndex,
54
- animation: `${tiltKf} ${4 + (phase % 3)}s ease-in-out infinite`,
55
- animationDelay: `${phase * 0.37}s`,
56
- ['--base']: 'translate(0,0)',
57
- ['--r']: `${rotate}deg`,
58
- ['--ty']: `${yOff}px`,
59
- transformOrigin: '50% 110%', // pivot below card, like holding from bottom
60
- transform: `translate(0,0) rotate(${rotate}deg) translateY(${yOff}px)`,
61
- }}>
62
- <div style={{
63
- padding: 6, borderRadius: 8,
64
- border: `4px solid ${frame || Co.GOLD}`,
65
- background: `${Co.DARKEST}`,
66
- boxShadow: `0 6px 0 rgba(0,0,0,.55), 0 0 18px ${frame || Co.GOLD}55`,
67
- position: 'relative',
68
- }}>
69
- {sprite}
70
- {score != null && (
71
- <div style={{
72
- position: 'absolute', top: -14, right: -14,
73
- minWidth: 34, padding: '2px 8px', height: 30,
74
- background: Co.RED, border: `3px solid ${Co.DARKEST}`, borderRadius: 15,
75
- color: Co.WHITE, fontSize: 18, lineHeight: '22px', textAlign: 'center',
76
- textShadow: '1px 1px 0 rgba(0,0,0,.8)', letterSpacing: 1,
77
- }}>+{score}</div>
78
- )}
79
- {label && (
80
- <div style={{
81
- position: 'absolute', left: 0, right: 0, bottom: -26, textAlign: 'center',
82
- fontSize: 13, color: frame || Co.GOLD_TEXT, letterSpacing: 2,
83
- textShadow: '1px 1px 0 rgba(0,0,0,.8)',
84
- whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
85
- }}>{label}</div>
86
- )}
87
- </div>
88
- </div>
89
- );
90
- }
91
-
92
- function SeedOGCard({ seed, filter }) {
93
- const must = filter.must || [];
94
- const should = filter.should || [];
95
- const allMustHit = must.every(c => (seed.score?.totals[c.id] || 0) > 0);
96
-
97
- // Collect actual HITS across MUST + SHOULD to render as the hand.
98
- // Each entry: { value, edition, rarity, score, kind, clauseId }
99
- const hand = [];
100
- for (const c of must) {
101
- const matches = seed.score?.matches?.[c.id] || [];
102
- if (!matches.length) continue;
103
- const m = matches[0];
104
- hand.push({
105
- kind: 'must', value: m.value, edition: m.edition, clauseId: c.id,
106
- frame: Co.BLUE, label: c.label || m.value, score: null,
107
- sheet: c.type === 'joker' || c.type === 'souljoker' ? 'jokers' :
108
- c.type === 'voucher' ? 'vouchers' :
109
- c.type.includes('tag') ? 'tags' :
110
- c.type === 'boss' ? 'blinds' : 'jokers',
111
- });
112
- }
113
- for (const c of should) {
114
- const matches = seed.score?.matches?.[c.id] || [];
115
- if (!matches.length) continue;
116
- const m = matches[0];
117
- const rarity = c.rarity || window.jokerRarity?.(m.value) || 'Common';
118
- hand.push({
119
- kind: 'should', value: m.value, edition: m.edition, clauseId: c.id,
120
- frame: RARITY_COLOR[rarity], label: c.label && c.label !== 'Any Rare' ? c.label : m.value,
121
- score: (c.score || 1) * (seed.score.totals[c.id] || 1),
122
- sheet: 'jokers',
123
- });
124
- }
125
-
126
- // Un-matched SHOULD clauses go in a tiny "didn't hit" score line (no big art).
127
- const unmatched = should.filter(c => !(seed.score?.totals[c.id] > 0));
128
-
129
- // Splay math — Balatro hand fan.
130
- const n = hand.length;
131
- const center = (n - 1) / 2;
132
- const spread = Math.min(8, 44 / Math.max(1, n)); // degrees per card, clamp so 8 cards look right
133
- const arcDip = 3.5; // px per step of arc
134
- const mkFan = (i) => {
135
- const off = i - center;
136
- return { rotate: off * spread, yOff: Math.abs(off) * arcDip, zIndex: 100 - Math.round(Math.abs(off) * 10) };
137
- };
138
-
139
- // Sprite renderer for a hand entry
140
- const renderSprite = (h, size = 96) => {
141
- const w = size, hh = h.sheet === 'tags' || h.sheet === 'blinds' ? size : Math.round(size * 95 / 71);
142
- if (h.sheet === 'blinds') return <BossChip name={h.value} size={hh} />;
143
- return <Sprite sheet={h.sheet} name={h.value} width={w} height={hh} />;
144
- };
145
-
146
- return (
147
- <div style={{
148
- width: 1200, height: 630, position: 'relative', overflow: 'hidden',
149
- background: `radial-gradient(ellipse at top left, #2d4a38 0%, #0f1a13 70%)`,
150
- fontFamily: 'm6x11plus, monospace', color: Co.WHITE,
151
- }}>
152
- {/* Felt texture overlay */}
153
- <div style={{
154
- position: 'absolute', inset: 0, opacity: 0.08,
155
- backgroundImage: 'repeating-linear-gradient(45deg, #fff 0 1px, transparent 1px 3px)',
156
- }} />
157
-
158
- {/* Top strip — filter name + branding */}
159
- <div style={{ position: 'absolute', top: 28, left: 40, right: 40, display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
160
- <div style={{ minWidth: 0, flex: 1 }}>
161
- <div style={{ fontSize: 16, color: Co.GOLD, letterSpacing: 3, textShadow: '2px 2px 0 rgba(0,0,0,.8)' }}>FILTER</div>
162
- <div style={{ fontSize: 28, color: Co.WHITE, letterSpacing: 1, textShadow: '2px 2px 0 rgba(0,0,0,.8)',
163
- overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 760 }}>
164
- {filter.name}
165
- </div>
166
- </div>
167
- <div style={{ fontSize: 16, color: Co.GREY, letterSpacing: 3 }}>Balatro Seed Curator</div>
168
- </div>
169
-
170
- {/* Seed code — big, LEFT */}
171
- <div style={{ position: 'absolute', top: 96, left: 40 }}>
172
- <div style={{ fontSize: 18, color: Co.GREY, letterSpacing: 4 }}>SEED</div>
173
- <div style={{ fontSize: 108, color: Co.WHITE, letterSpacing: 10, lineHeight: 1, marginTop: 4, textShadow: `3px 4px 0 ${Co.BLACK}` }}>{seed.seed}</div>
174
- <div style={{ display: 'flex', gap: 16, marginTop: 8, fontSize: 20, color: Co.GREY, letterSpacing: 2 }}>
175
- <span>{seed.deck} Deck</span>
176
- <span style={{ color: Co.PANEL_EDGE }}>·</span>
177
- <span>{seed.stake} Stake</span>
178
- <span style={{ color: Co.PANEL_EDGE }}>·</span>
179
- <span style={{ color: allMustHit ? Co.BLUE : Co.RED }}>
180
- {allMustHit ? '✓ must complete' : '✗ must incomplete'}
181
- </span>
182
- </div>
183
- </div>
184
-
185
- {/* Score — big number, RIGHT */}
186
- <div style={{ position: 'absolute', top: 96, right: 40, textAlign: 'right' }}>
187
- <div style={{ fontSize: 18, color: Co.GREY, letterSpacing: 4 }}>SCORE</div>
188
- <div style={{ fontSize: 140, color: Co.RED, letterSpacing: 4, lineHeight: 1, marginTop: 4, textShadow: `4px 6px 0 ${Co.BLACK}` }}>
189
- {seed.score.totalScore}
190
- </div>
191
- </div>
192
-
193
- {/* THE HAND — splayed rainbow poker-hand of everything that hit. */}
194
- <div style={{
195
- position: 'absolute', left: 40, right: 40, bottom: 78,
196
- display: 'flex', justifyContent: 'center', alignItems: 'flex-end',
197
- paddingLeft: 14, // compensate first card's -14 marginLeft
198
- }}>
199
- {hand.map((h, i) => {
200
- const fan = mkFan(i);
201
- return (
202
- <FannedCard
203
- key={h.clauseId + ':' + i}
204
- sprite={renderSprite(h, 96)}
205
- label={h.label}
206
- score={h.kind === 'should' ? h.score : null}
207
- frame={h.frame}
208
- rotate={fan.rotate}
209
- yOff={fan.yOff}
210
- zIndex={fan.zIndex}
211
- phase={i}
212
- />
213
- );
214
- })}
215
- {hand.length === 0 && (
216
- <div style={{ fontSize: 22, color: Co.GREY, letterSpacing: 3, padding: 40 }}>
217
- no hits — try a different seed
218
- </div>
219
- )}
220
- </div>
221
-
222
- {/* SHOULD score line — unmatched clauses get tiny sad-face stat pills here.
223
- Room preserved so this reads as a proper scoring readout, not just art. */}
224
- {unmatched.length > 0 && (
225
- <div style={{
226
- position: 'absolute', left: 40, right: 40, bottom: 30,
227
- display: 'flex', alignItems: 'center', gap: 10,
228
- }}>
229
- <div style={{
230
- fontSize: 12, letterSpacing: 3, padding: '3px 10px',
231
- background: Co.DARK_RED, color: Co.WHITE, borderRadius: 3,
232
- textShadow: '1px 1px 0 rgba(0,0,0,.8)',
233
- }}>MISSED</div>
234
- <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
235
- {unmatched.map(c => (
236
- <div key={c.id} style={{
237
- fontSize: 12, color: Co.GREY, letterSpacing: 1.5,
238
- padding: '2px 8px', border: `1px dashed ${Co.GREY}`, borderRadius: 3,
239
- }}>
240
- {c.label || c.value} <span style={{ color: Co.DARK_RED }}>+{c.score || 1}</span>
241
- </div>
242
- ))}
243
- </div>
244
- </div>
245
- )}
246
- </div>
247
- );
248
- }
249
-
250
- window.SeedOGCard = SeedOGCard;
251
- window.RARITY_COLOR = RARITY_COLOR;