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
@@ -2,4 +2,8 @@ export interface JimboCopyRowProps {
2
2
  value: string;
3
3
  label?: string;
4
4
  }
5
+ /**
6
+ * Inline copy-to-clipboard row with label + value + button.
7
+ * All styling via jimbo.css `.j-copy-row` classes.
8
+ */
5
9
  export declare function JimboCopyRow({ value, label }: JimboCopyRowProps): import("react/jsx-runtime").JSX.Element;
@@ -1,35 +1,18 @@
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 './tokens.js';
5
4
  import { JimboText } from './jimboText.js';
5
+ /**
6
+ * Inline copy-to-clipboard row with label + value + button.
7
+ * All styling via jimbo.css `.j-copy-row` classes.
8
+ */
6
9
  export function JimboCopyRow({ value, label }) {
7
10
  const [copied, setCopied] = useState(false);
8
- const C = JimboColorOption;
9
11
  function copy() {
10
12
  navigator.clipboard.writeText(value).then(() => {
11
13
  setCopied(true);
12
14
  setTimeout(() => setCopied(false), 1500);
13
15
  });
14
16
  }
15
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [label && (_jsx(JimboText, { size: "xs", tone: "grey", style: { letterSpacing: 2 }, children: label })), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: {
16
- flex: 1,
17
- padding: '6px 10px',
18
- background: C.DARKEST,
19
- border: `2px solid ${C.PANEL_EDGE}`,
20
- borderRadius: 4,
21
- wordBreak: 'break-all',
22
- }, children: _jsx(JimboText, { size: "sm", children: value }) }), _jsx("button", { type: "button", onClick: copy, style: {
23
- fontFamily: "'m6x11plus', 'Courier New', monospace",
24
- fontSize: 11,
25
- letterSpacing: 2,
26
- color: copied ? C.GREEN_TEXT : C.GOLD_TEXT,
27
- background: copied ? 'rgba(53,189,134,0.12)' : 'rgba(228,182,67,0.12)',
28
- border: `1px solid ${copied ? C.GREEN_TEXT : C.GOLD_TEXT}`,
29
- borderRadius: 4,
30
- padding: '4px 12px',
31
- cursor: 'pointer',
32
- flexShrink: 0,
33
- transition: 'color 0.15s, background 0.15s, border-color 0.15s',
34
- }, children: copied ? 'Copied' : 'Copy' })] })] }));
17
+ return (_jsxs("div", { className: "j-copy-row", children: [label && (_jsx(JimboText, { size: "xs", tone: "grey", className: "j-copy-row__label", children: label })), _jsxs("div", { className: "j-copy-row__field", children: [_jsx("div", { className: "j-copy-row__value", children: _jsx(JimboText, { size: "sm", children: value }) }), _jsx("button", { type: "button", className: "j-copy-row__btn", "data-copied": copied, onClick: copy, children: copied ? 'Copied' : 'Copy' })] })] }));
35
18
  }
@@ -17,9 +17,6 @@ export interface JimboFilterBarProps {
17
17
  }
18
18
  /**
19
19
  * Generic Balatro-styled filter row: search input with floating pill label
20
- * + optional sort dropdown with floating pill label. Adapted from
21
- * weejoker's FilterBar — no hardcoded sort options, no lucide dep.
22
- *
23
- * Pass `sortOptions` to show the sort side; omit to show search only.
20
+ * + optional sort dropdown with floating pill label.
24
21
  */
25
22
  export declare function JimboFilterBar({ search, onSearchChange, searchPlaceholder, searchLabel, sort, onSortChange, sortLabel, sortOptions, className, style, }: JimboFilterBarProps): import("react/jsx-runtime").JSX.Element;
@@ -1,71 +1,12 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { JimboColorOption } from './tokens.js';
4
3
  import { JimboText } from './jimboText.js';
5
4
  /**
6
5
  * Generic Balatro-styled filter row: search input with floating pill label
7
- * + optional sort dropdown with floating pill label. Adapted from
8
- * weejoker's FilterBar — no hardcoded sort options, no lucide dep.
9
- *
10
- * Pass `sortOptions` to show the sort side; omit to show search only.
6
+ * + optional sort dropdown with floating pill label.
11
7
  */
12
8
  export function JimboFilterBar({ search, onSearchChange, searchPlaceholder = 'Search...', searchLabel = 'Search', sort, onSortChange, sortLabel = 'Sort By', sortOptions, className = '', style, }) {
13
- return (_jsxs("div", { className: className, style: {
14
- display: 'flex',
15
- gap: 24,
16
- padding: 16,
17
- backgroundColor: JimboColorOption.DARK_GREY,
18
- border: `4px solid ${JimboColorOption.BORDER_SILVER}`,
19
- boxShadow: `0 3px 0 0 ${JimboColorOption.BORDER_SOUTH}`,
20
- borderRadius: 12,
21
- position: 'relative',
22
- flexWrap: 'wrap',
23
- ...style,
24
- }, children: [onSearchChange ? (_jsx(FloatingLabelField, { label: searchLabel, children: _jsxs("div", { style: { position: 'relative' }, children: [_jsx("div", { style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: 48, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', color: JimboColorOption.BLUE, zIndex: 1 }, children: _jsx(SearchIcon, {}) }), _jsx("input", { type: "text", value: search ?? '', onChange: (e) => onSearchChange(e.target.value), placeholder: searchPlaceholder, style: {
25
- width: '100%',
26
- paddingLeft: 48,
27
- paddingRight: 16,
28
- paddingTop: 14,
29
- paddingBottom: 14,
30
- backgroundColor: JimboColorOption.DARKEST,
31
- border: 'none',
32
- borderBottom: `4px solid ${JimboColorOption.PANEL_EDGE}`,
33
- borderRadius: 8,
34
- color: JimboColorOption.WHITE,
35
- fontFamily: "'m6x11plus', 'Courier New', monospace",
36
- fontSize: 20,
37
- letterSpacing: 2,
38
- outline: 'none',
39
- } })] }) })) : null, sortOptions && onSortChange ? (_jsx(FloatingLabelField, { label: sortLabel, children: _jsxs("div", { style: { position: 'relative' }, children: [_jsx("select", { value: sort ?? sortOptions[0]?.value, onChange: (e) => onSortChange(e.target.value), style: {
40
- appearance: 'none',
41
- WebkitAppearance: 'none',
42
- MozAppearance: 'none',
43
- backgroundColor: JimboColorOption.ORANGE,
44
- color: JimboColorOption.WHITE,
45
- border: 'none',
46
- borderBottom: `4px solid ${JimboColorOption.DARK_ORANGE}`,
47
- borderRadius: 8,
48
- cursor: 'pointer',
49
- fontFamily: "'m6x11plus', 'Courier New', monospace",
50
- fontSize: 18,
51
- letterSpacing: 2,
52
- padding: '14px 48px 14px 24px',
53
- minWidth: 200,
54
- textAlign: 'center',
55
- outline: 'none',
56
- }, children: sortOptions.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) }), _jsx("div", { style: { position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: JimboColorOption.WHITE, opacity: 0.85 }, children: _jsx(SortIcon, {}) })] }) })) : null] }));
57
- }
58
- function FloatingLabelField({ label, children }) {
59
- return (_jsxs("div", { style: { flex: 1, minWidth: 200, position: 'relative', marginTop: 10 }, children: [_jsx("div", { style: {
60
- position: 'absolute',
61
- top: -14,
62
- left: 16,
63
- backgroundColor: JimboColorOption.RED,
64
- border: `2px solid ${JimboColorOption.DARK_RED}`,
65
- borderRadius: 6,
66
- padding: '4px 12px',
67
- zIndex: 2,
68
- }, children: _jsx(JimboText, { size: "xs", children: label }) }), children] }));
9
+ return (_jsxs("div", { className: `j-filter-bar ${className}`, style: style, children: [onSearchChange ? (_jsxs("div", { className: "j-filter-bar__field", children: [_jsx("div", { className: "j-filter-bar__pill", children: _jsx(JimboText, { size: "xs", children: searchLabel }) }), _jsxs("div", { className: "j-relative", children: [_jsx("div", { className: "j-filter-bar__search-icon", children: _jsx(SearchIcon, {}) }), _jsx("input", { type: "text", value: search ?? '', onChange: (e) => onSearchChange(e.target.value), placeholder: searchPlaceholder, className: "j-filter-bar__input" })] })] })) : null, sortOptions && onSortChange ? (_jsxs("div", { className: "j-filter-bar__field", children: [_jsx("div", { className: "j-filter-bar__pill", children: _jsx(JimboText, { size: "xs", children: sortLabel }) }), _jsxs("div", { className: "j-relative", children: [_jsx("select", { value: sort ?? sortOptions[0]?.value, onChange: (e) => onSortChange(e.target.value), className: "j-filter-bar__select", children: sortOptions.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) }), _jsx("div", { className: "j-filter-bar__sort-icon", children: _jsx(SortIcon, {}) })] })] })) : null] }));
69
10
  }
70
11
  function SearchIcon() {
71
12
  return (_jsxs("svg", { width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("circle", { cx: 11, cy: 11, r: 8 }), _jsx("line", { x1: 21, y1: 21, x2: 16.65, y2: 16.65 })] }));
@@ -12,7 +12,6 @@ export interface JimboFlankNavProps {
12
12
  }
13
13
  /**
14
14
  * Prev/next navigation with flanking buttons around a central stage.
15
- * Generic adaptation of weejoker's DayNavigation no hardcoded "Day"
16
- * labels, no lucide dep (inline chevron SVGs).
15
+ * No hardcoded labels, no lucide dep (inline chevron SVGs).
17
16
  */
18
17
  export declare function JimboFlankNav({ onPrev, onNext, canPrev, canNext, prevLabel, nextLabel, children, className, style, }: JimboFlankNavProps): import("react/jsx-runtime").JSX.Element;
@@ -1,43 +1,18 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from 'react';
4
- import { JimboColorOption, JIMBO_ANIMATIONS } from './tokens.js';
5
4
  /**
6
5
  * Prev/next navigation with flanking buttons around a central stage.
7
- * Generic adaptation of weejoker's DayNavigation no hardcoded "Day"
8
- * labels, no lucide dep (inline chevron SVGs).
6
+ * No hardcoded labels, no lucide dep (inline chevron SVGs).
9
7
  */
10
8
  export function JimboFlankNav({ onPrev, onNext, canPrev = true, canNext = true, prevLabel = 'Previous', nextLabel = 'Next', children, className = '', style, }) {
11
- return (_jsxs("div", { className: className, style: {
12
- display: 'flex',
13
- alignItems: 'stretch',
14
- justifyContent: 'center',
15
- gap: 8,
16
- width: '100%',
17
- position: 'relative',
18
- ...style,
19
- }, children: [_jsx(NavButton, { direction: "left", onClick: onPrev, disabled: !canPrev, "aria-label": prevLabel }), _jsx("div", { style: { position: 'relative', flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }, children: children }), _jsx(NavButton, { direction: "right", onClick: onNext, disabled: !canNext, "aria-label": nextLabel })] }));
9
+ return (_jsxs("div", { className: `j-flank ${className}`, style: style, children: [_jsx(NavButton, { direction: "left", onClick: onPrev, disabled: !canPrev, "aria-label": prevLabel }), _jsx("div", { className: "j-flank__content", children: children }), _jsx(NavButton, { direction: "right", onClick: onNext, disabled: !canNext, "aria-label": nextLabel })] }));
20
10
  }
21
11
  function NavButton({ direction, onClick, disabled, 'aria-label': ariaLabel, }) {
22
12
  const [pressed, setPressed] = React.useState(false);
23
- return (_jsx("button", { type: "button", onClick: onClick, disabled: disabled, "aria-label": ariaLabel, title: ariaLabel, onMouseDown: () => !disabled && setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onTouchStart: () => !disabled && setPressed(true), onTouchEnd: () => setPressed(false), style: {
24
- flexShrink: 0,
25
- width: 48,
26
- border: 'none',
27
- borderRadius: 8,
28
- cursor: disabled ? 'default' : 'pointer',
29
- opacity: 1,
30
- backgroundColor: disabled ? JimboColorOption.DARK_RED : JimboColorOption.RED,
31
- color: JimboColorOption.WHITE,
32
- display: 'flex',
33
- alignItems: 'center',
34
- justifyContent: 'center',
35
- transform: pressed ? `translateY(${JIMBO_ANIMATIONS.PRESS_TRANSLATE_Y}px)` : 'translateY(0)',
36
- boxShadow: pressed || disabled ? 'none' : `0 ${JIMBO_ANIMATIONS.PRESS_TRANSLATE_Y}px 0 0 ${JimboColorOption.DARK_RED}`,
37
- transition: `transform ${JIMBO_ANIMATIONS.PRESS_DURATION}ms ease, box-shadow ${JIMBO_ANIMATIONS.PRESS_DURATION}ms ease, background-color ${JIMBO_ANIMATIONS.PRESS_DURATION}ms ease`,
38
- }, children: _jsx(ChevronSvg, { direction: direction }) }));
13
+ return (_jsx("button", { type: "button", className: "j-flank__btn", "data-pressed": pressed && !disabled, onClick: onClick, disabled: disabled, "aria-label": ariaLabel, title: ariaLabel, onMouseDown: () => !disabled && setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onTouchStart: () => !disabled && setPressed(true), onTouchEnd: () => setPressed(false), children: _jsx(ChevronSvg, { direction: direction }) }));
39
14
  }
40
15
  function ChevronSvg({ direction }) {
41
- const points = direction === 'left' ? '20,4 8,16 20,28' : '12,4 24,16 12,28';
42
- return (_jsx("svg", { width: 32, height: 32, viewBox: "0 0 32 32", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("polyline", { points: points }) }));
16
+ const points = direction === 'left' ? '18,4 8,14 18,24' : '10,4 20,14 10,24';
17
+ return (_jsx("svg", { width: 28, height: 28, viewBox: "0 0 28 28", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("polyline", { points: points }) }));
43
18
  }
@@ -12,14 +12,10 @@ export interface JimboTabsProps {
12
12
  }
13
13
  /**
14
14
  * Horizontal tab navigation with bouncing triangle indicator on the active
15
- * tab. Ported from JAMMY's jimbo-ui/Tabs.tsx triangle attaches to each
16
- * button and animates only on the active one.
15
+ * tab. Triangle attaches to each button and animates only on the active one.
17
16
  */
18
17
  export declare function JimboTabs({ tabs, activeTab, onTabChange, className, style }: JimboTabsProps): import("react/jsx-runtime").JSX.Element;
19
18
  /**
20
19
  * Vertical tab strip — rotated labels (writing-mode) for space efficiency.
21
- * Ported from JAMMY's JimboVerticalTabs. Typical use: inline on the left
22
- * side of a content panel to pick between content categories
23
- * (e.g. JOKERS / CONSUMABLES / VOUCHERS).
24
20
  */
25
21
  export declare function JimboVerticalTabs({ tabs, activeTab, onTabChange, className, style }: JimboTabsProps): import("react/jsx-runtime").JSX.Element;
@@ -1,59 +1,24 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
- import { JimboColorOption, JIMBO_ANIMATIONS } from './tokens.js';
5
4
  import { JimboText } from './jimboText.js';
6
5
  /**
7
6
  * Horizontal tab navigation with bouncing triangle indicator on the active
8
- * tab. Ported from JAMMY's jimbo-ui/Tabs.tsx triangle attaches to each
9
- * button and animates only on the active one.
7
+ * tab. Triangle attaches to each button and animates only on the active one.
10
8
  */
11
9
  export function JimboTabs({ tabs, activeTab, onTabChange, className = '', style }) {
12
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: className, style: { display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap', ...style }, children: tabs.map((tab) => (_jsx(TabButton, { label: tab.label, active: activeTab === tab.id, onClick: () => onTabChange(tab.id) }, tab.id))) }), _jsx("style", { children: JIMBO_BOUNCE_KEYFRAMES })] }));
10
+ return (_jsx("div", { className: `j-tabs ${className}`, style: style, children: tabs.map((tab) => (_jsx(TabButton, { label: tab.label, active: activeTab === tab.id, onClick: () => onTabChange(tab.id) }, tab.id))) }));
13
11
  }
14
- const JIMBO_BOUNCE_KEYFRAMES = `
15
- @keyframes jimbo-bounce {
16
- 0%, 100% { transform: translateY(0); }
17
- 50% { transform: translateY(-3px); }
18
- }
19
- `;
20
12
  function TabButton({ label, active, onClick }) {
21
13
  const [pressed, setPressed] = useState(false);
22
- return (_jsxs("div", { style: { position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center' }, children: [_jsx("div", { style: {
23
- marginBottom: 4,
24
- opacity: active ? 1 : 0,
25
- transition: 'opacity 150ms',
26
- animation: active ? 'jimbo-bounce 0.8s cubic-bezier(0.68, 0, 0.68, 1) infinite' : 'none',
27
- }, "aria-hidden": true, children: _jsx("svg", { width: 14, height: 10, viewBox: "0 0 14 10", fill: JimboColorOption.RED, children: _jsx("polygon", { points: "7,10 0,0 14,0" }) }) }), _jsx("button", { type: "button", onClick: onClick, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onTouchStart: () => setPressed(true), onTouchEnd: () => setPressed(false), style: {
28
- border: 'none',
29
- cursor: 'pointer',
30
- borderRadius: 8,
31
- padding: '8px 16px',
32
- backgroundColor: JimboColorOption.RED,
33
- transform: pressed ? `translateY(${JIMBO_ANIMATIONS.PRESS_TRANSLATE_Y}px)` : 'translateY(0)',
34
- boxShadow: pressed ? 'none' : `0 ${JIMBO_ANIMATIONS.PRESS_TRANSLATE_Y}px 0 0 ${JimboColorOption.BLACK}80`,
35
- transition: `transform ${JIMBO_ANIMATIONS.PRESS_DURATION}ms ease, box-shadow ${JIMBO_ANIMATIONS.PRESS_DURATION}ms ease`,
36
- }, children: _jsx(JimboText, { size: "sm", uppercase: true, children: label }) })] }));
14
+ return (_jsxs("div", { className: "j-tab", children: [_jsx("div", { className: "j-tab__indicator", "data-active": active, "aria-hidden": true, children: _jsx("svg", { width: 14, height: 10, viewBox: "0 0 14 10", fill: "var(--j-red)", children: _jsx("polygon", { points: "7,10 0,0 14,0" }) }) }), _jsx("button", { type: "button", className: "j-tab__btn", "data-pressed": pressed, onClick: onClick, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), onMouseEnter: () => { }, onMouseLeave: () => { setPressed(false); }, onTouchStart: () => setPressed(true), onTouchEnd: () => setPressed(false), children: _jsx(JimboText, { size: "sm", children: label }) })] }));
37
15
  }
38
16
  /**
39
17
  * Vertical tab strip — rotated labels (writing-mode) for space efficiency.
40
- * Ported from JAMMY's JimboVerticalTabs. Typical use: inline on the left
41
- * side of a content panel to pick between content categories
42
- * (e.g. JOKERS / CONSUMABLES / VOUCHERS).
43
18
  */
44
19
  export function JimboVerticalTabs({ tabs, activeTab, onTabChange, className = '', style }) {
45
- return (_jsx("div", { className: className, style: { display: 'flex', flexDirection: 'column', gap: 4, ...style }, children: tabs.map((tab) => {
20
+ return (_jsx("div", { className: `j-vtabs ${className}`, style: style, children: tabs.map((tab) => {
46
21
  const isActive = activeTab === tab.id;
47
- return (_jsx("button", { type: "button", onClick: () => onTabChange(tab.id), style: {
48
- border: 'none',
49
- cursor: 'pointer',
50
- borderRadius: '8px 0 0 8px',
51
- padding: '16px 8px',
52
- backgroundColor: isActive ? JimboColorOption.DARK_GREY : JimboColorOption.INNER_BORDER,
53
- writingMode: 'vertical-rl',
54
- textOrientation: 'mixed',
55
- transform: 'rotate(180deg)',
56
- transition: 'background-color 120ms ease',
57
- }, children: _jsx(JimboText, { size: "sm", uppercase: true, tone: isActive ? 'default' : 'grey', children: tab.label }) }, tab.id));
22
+ return (_jsx("button", { type: "button", className: "j-vtab", onClick: () => onTabChange(tab.id), children: _jsx(JimboText, { size: "sm", tone: isActive ? 'default' : 'grey', children: tab.label }) }, tab.id));
58
23
  }) }));
59
24
  }
@@ -23,4 +23,4 @@ export interface JimboTextProps extends React.HTMLAttributes<HTMLElement> {
23
23
  * so the @font-face declaration lands (font is base64-embedded, no
24
24
  * runtime fetch).
25
25
  */
26
- export declare function JimboText({ tone, size, shadow, uppercase, letterSpacing, as: Tag, style, children, ...rest }: JimboTextProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function JimboText({ tone, size, shadow, uppercase, letterSpacing, as: Tag, className, style, children, ...rest }: JimboTextProps): import("react/jsx-runtime").JSX.Element;
@@ -1,25 +1,5 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { JimboColorOption } from './tokens.js';
4
- const TONE_COLOR = {
5
- default: JimboColorOption.WHITE,
6
- mult: JimboColorOption.RED,
7
- chips: JimboColorOption.BLUE,
8
- gold: JimboColorOption.GOLD_TEXT,
9
- green: JimboColorOption.GREEN_TEXT,
10
- red: JimboColorOption.RED,
11
- blue: JimboColorOption.BLUE,
12
- orange: JimboColorOption.ORANGE_TEXT,
13
- purple: JimboColorOption.PURPLE,
14
- grey: JimboColorOption.GREY,
15
- };
16
- const SIZE_PX = {
17
- xs: 10,
18
- sm: 12,
19
- md: 14,
20
- lg: 18,
21
- xl: 24,
22
- };
23
3
  /**
24
4
  * Canonical pixel-font text wrapper. Uses `m6x11plus` as its family and
25
5
  * applies the authentic Balatro drop shadow by default. Prefer this over
@@ -30,16 +10,19 @@ const SIZE_PX = {
30
10
  * so the @font-face declaration lands (font is base64-embedded, no
31
11
  * runtime fetch).
32
12
  */
33
- export function JimboText({ tone = 'default', size = 'md', shadow = true, uppercase = false, letterSpacing, as: Tag = 'span', style, children, ...rest }) {
34
- const resolvedLetterSpacing = letterSpacing ?? (uppercase ? 2 : undefined);
35
- return (_jsx(Tag, { style: {
36
- fontFamily: "'m6x11plus', 'Courier New', monospace",
37
- fontSize: SIZE_PX[size],
38
- color: TONE_COLOR[tone],
39
- textShadow: shadow ? `1px 1px 0 ${JimboColorOption.BLACK}cc` : 'none',
40
- textTransform: uppercase ? 'uppercase' : 'none',
41
- letterSpacing: resolvedLetterSpacing,
42
- lineHeight: 1.2,
43
- ...style,
44
- }, ...rest, children: children }));
13
+ export function JimboText({ tone = 'default', size = 'md', shadow = true, uppercase = false, letterSpacing, as: Tag = 'span', className = '', style, children, ...rest }) {
14
+ const sizeClass = `j-text--${size}`;
15
+ const toneClass = `j-text--${tone}`;
16
+ const shadowClass = shadow ? '' : 'j-text--no-shadow';
17
+ const upperClass = uppercase ? 'j-text--upper' : '';
18
+ const inlineStyle = {};
19
+ if (letterSpacing != null) {
20
+ inlineStyle.letterSpacing = letterSpacing;
21
+ }
22
+ else if (uppercase && letterSpacing == null) {
23
+ inlineStyle.letterSpacing = 2;
24
+ }
25
+ if (style)
26
+ Object.assign(inlineStyle, style);
27
+ return (_jsx(Tag, { className: `j-text ${sizeClass} ${toneClass} ${shadowClass} ${upperClass} ${className}`.trim(), style: Object.keys(inlineStyle).length > 0 ? inlineStyle : undefined, ...rest, children: children }));
45
28
  }
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- export type JimboTooltipMode = 'snap' | 'mouse';
3
- export type JimboTooltipPlacement = 'top' | 'bottom' | 'auto';
2
+ import { type JimboTooltipMode, type JimboTooltipPlacement } from './hooks.js';
4
3
  export interface JimboTooltipProps {
5
4
  /** Content rendered inside the tooltip panel. Typically a card's ability text, joker description, etc. */
6
5
  content: React.ReactNode;
@@ -21,15 +20,5 @@ export interface JimboTooltipProps {
21
20
  /**
22
21
  * Canonical Balatro-style tooltip: dark panel, silver border, pixel font.
23
22
  * Wrap any target to get a hover/focus popover.
24
- *
25
- * <JimboTooltip content={<JimboText size="sm">Gains +4 mult per face card</JimboText>}>
26
- * <GameCard joker="Blueprint" />
27
- * </JimboTooltip>
28
- *
29
- * Modes:
30
- * - `snap` (default): anchored to the target's bounding rect, chooses
31
- * top or bottom automatically so it stays in viewport.
32
- * - `mouse`: follows the mouse position — useful for large targets
33
- * (full card fans, zone rails) where "above the element" is imprecise.
34
23
  */
35
24
  export declare function JimboTooltip({ content, children, mode, placement, delay, maxWidth, disabled, }: JimboTooltipProps): import("react/jsx-runtime").JSX.Element;
@@ -1,87 +1,21 @@
1
- 'use client';
2
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import React, { useEffect, useRef, useState } from 'react';
4
- import { JimboColorOption } from './tokens.js';
2
+ import React, { useCallback } from 'react';
3
+ import { useJimboTooltip } from './hooks.js';
5
4
  /**
6
5
  * Canonical Balatro-style tooltip: dark panel, silver border, pixel font.
7
6
  * Wrap any target to get a hover/focus popover.
8
- *
9
- * <JimboTooltip content={<JimboText size="sm">Gains +4 mult per face card</JimboText>}>
10
- * <GameCard joker="Blueprint" />
11
- * </JimboTooltip>
12
- *
13
- * Modes:
14
- * - `snap` (default): anchored to the target's bounding rect, chooses
15
- * top or bottom automatically so it stays in viewport.
16
- * - `mouse`: follows the mouse position — useful for large targets
17
- * (full card fans, zone rails) where "above the element" is imprecise.
18
7
  */
19
8
  export function JimboTooltip({ content, children, mode = 'snap', placement = 'auto', delay = 80, maxWidth = 280, disabled = false, }) {
20
- const [visible, setVisible] = useState(false);
21
- const [pos, setPos] = useState(null);
22
- const targetRef = useRef(null);
23
- const tooltipRef = useRef(null);
24
- const delayTimerRef = useRef(null);
25
- useEffect(() => () => {
26
- if (delayTimerRef.current)
27
- clearTimeout(delayTimerRef.current);
28
- }, []);
29
- const show = () => {
30
- if (disabled)
31
- return;
32
- if (delayTimerRef.current)
33
- clearTimeout(delayTimerRef.current);
34
- delayTimerRef.current = setTimeout(() => setVisible(true), delay);
35
- };
36
- const hide = () => {
37
- if (delayTimerRef.current)
38
- clearTimeout(delayTimerRef.current);
39
- setVisible(false);
40
- setPos(null);
41
- };
42
- const computeSnapPos = () => {
43
- const el = targetRef.current;
44
- const tip = tooltipRef.current;
45
- if (!el || !tip)
46
- return;
47
- const rect = el.getBoundingClientRect();
48
- const tipRect = tip.getBoundingClientRect();
49
- const roomAbove = rect.top;
50
- const align = placement === 'top' ? 'top'
51
- : placement === 'bottom' ? 'bottom'
52
- : roomAbove >= tipRect.height + 12 ? 'top' : 'bottom';
53
- const left = rect.left + rect.width / 2 - tipRect.width / 2;
54
- const top = align === 'top' ? rect.top - tipRect.height - 8 : rect.bottom + 8;
55
- setPos({ left: Math.max(8, Math.min(window.innerWidth - tipRect.width - 8, left)), top, align });
56
- };
57
- useEffect(() => {
58
- if (!visible || mode !== 'snap')
59
- return;
60
- // Recompute after the tooltip renders so its size is known.
61
- const raf = requestAnimationFrame(computeSnapPos);
62
- window.addEventListener('resize', computeSnapPos);
63
- window.addEventListener('scroll', computeSnapPos, true);
64
- return () => {
65
- cancelAnimationFrame(raf);
66
- window.removeEventListener('resize', computeSnapPos);
67
- window.removeEventListener('scroll', computeSnapPos, true);
68
- };
69
- // eslint-disable-next-line react-hooks/exhaustive-deps
70
- }, [visible, mode, placement]);
71
- const handleMouseMove = (e) => {
72
- if (mode !== 'mouse')
73
- return;
74
- setPos({ left: e.clientX + 12, top: e.clientY + 16, align: 'bottom' });
75
- };
9
+ const { visible, pos, targetRef, tooltipRef, show, hide, handleMouseMove, } = useJimboTooltip({ mode, placement, delay, disabled });
76
10
  const child = React.Children.only(children);
77
- const refHandler = (node) => {
11
+ const refHandler = useCallback((node) => {
78
12
  targetRef.current = node;
79
13
  const childRef = child.ref;
80
14
  if (typeof childRef === 'function')
81
15
  childRef(node);
82
16
  else if (childRef && 'current' in childRef)
83
17
  childRef.current = node;
84
- };
18
+ }, [child, targetRef]);
85
19
  const wrapped = React.cloneElement(child, {
86
20
  ref: refHandler,
87
21
  onMouseEnter: (e) => { show(); child.props.onMouseEnter?.(e); },
@@ -90,20 +24,10 @@ export function JimboTooltip({ content, children, mode = 'snap', placement = 'au
90
24
  onBlur: (e) => { hide(); child.props.onBlur?.(e); },
91
25
  onMouseMove: (e) => { handleMouseMove(e); child.props.onMouseMove?.(e); },
92
26
  });
93
- return (_jsxs(_Fragment, { children: [wrapped, visible ? (_jsx("div", { ref: tooltipRef, role: "tooltip", style: {
94
- position: 'fixed',
27
+ return (_jsxs(_Fragment, { children: [wrapped, visible ? (_jsx("div", { ref: tooltipRef, role: "tooltip", className: "j-tooltip", style: {
95
28
  left: pos?.left ?? -9999,
96
29
  top: pos?.top ?? -9999,
97
30
  maxWidth,
98
- padding: '6px 10px',
99
- borderRadius: 6,
100
- background: JimboColorOption.DARKEST,
101
- border: `2px solid ${JimboColorOption.BORDER_SILVER}`,
102
- boxShadow: `0 2px 0 ${JimboColorOption.BLACK}cc`,
103
- color: JimboColorOption.WHITE,
104
- pointerEvents: 'none',
105
- zIndex: 10000,
106
31
  opacity: pos ? 1 : 0,
107
- transition: 'opacity 120ms ease',
108
32
  }, children: content })) : null] }));
109
33
  }
@@ -17,9 +17,10 @@ export interface JimboButtonProps {
17
17
  uppercase?: boolean;
18
18
  onClick?: () => void;
19
19
  style?: React.CSSProperties;
20
+ className?: string;
20
21
  children?: React.ReactNode;
21
22
  }
22
- export declare function JimboButton({ tone, size, fullWidth, disabled, uppercase, onClick, style, children, }: JimboButtonProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function JimboButton({ tone, size, fullWidth, disabled, uppercase, onClick, style, className, children, }: JimboButtonProps): import("react/jsx-runtime").JSX.Element;
23
24
  export declare function JimboBackButton({ onClick }: {
24
25
  onClick?: () => void;
25
26
  }): import("react/jsx-runtime").JSX.Element;
package/dist/ui/panel.js CHANGED
@@ -1,33 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useRef, memo } from 'react';
3
+ import { useState, memo } from 'react';
4
4
  import { JimboColorOption, JIMBO_ANIMATIONS } from './tokens.js';
5
+ import { useSway, useDelayedVisibility } from './hooks.js';
5
6
  import { JimboText } from './jimboText.js';
6
7
  export const JimboPanel = memo(({ children, className = '', sway = false, onBack, hideBack = false, style, ...props }) => {
7
- const panelRef = useRef(null);
8
- useEffect(() => {
9
- if (!sway || !panelRef.current)
10
- return;
11
- let frame;
12
- const start = Date.now();
13
- const el = panelRef.current;
14
- const tick = () => {
15
- const t = ((Date.now() - start) % JIMBO_ANIMATIONS.SWAY_DURATION) / JIMBO_ANIMATIONS.SWAY_DURATION * Math.PI * 2;
16
- el.style.transform = `translate(${Math.sin(t) * JIMBO_ANIMATIONS.SWAY_AMOUNT * 0.3}px, ${Math.sin(t * 0.8) * JIMBO_ANIMATIONS.SWAY_AMOUNT}px)`;
17
- frame = requestAnimationFrame(tick);
18
- };
19
- frame = requestAnimationFrame(tick);
20
- return () => { cancelAnimationFrame(frame); el.style.transform = ''; };
21
- }, [sway]);
22
- return (_jsxs("div", { ref: panelRef, className: 'rounded-xl p-4 flex flex-col items-stretch overflow-hidden ' + className, style: {
23
- backgroundColor: JimboColorOption.DARK_GREY,
24
- border: `3px solid ${JimboColorOption.BORDER_SILVER}`,
25
- boxShadow: `0 3px 0 0 ${JimboColorOption.BORDER_SOUTH}`,
26
- ...style,
27
- }, ...props, children: [_jsx("div", { className: "flex-1 overflow-auto", children: children }), onBack && !hideBack && (_jsx("div", { className: "mt-4 pt-2 shrink-0", children: _jsx(JimboBackButton, { onClick: onBack }) }))] }));
8
+ const panelRef = useSway(sway);
9
+ return (_jsxs("div", { ref: panelRef, className: `j-panel ${className}`, style: style, ...props, children: [_jsx("div", { className: "j-panel__body", children: children }), onBack && !hideBack && (_jsx("div", { className: "j-panel__back", children: _jsx(JimboBackButton, { onClick: onBack }) }))] }));
28
10
  });
29
11
  JimboPanel.displayName = 'JimboPanel';
30
- export const JimboInnerPanel = memo(({ children, className = '', style, ...props }) => (_jsx("div", { className: 'rounded-lg p-3 ' + className, style: { backgroundColor: JimboColorOption.INNER_BORDER, border: `2px solid ${JimboColorOption.PANEL_EDGE}`, ...style }, ...props, children: children })));
12
+ export const JimboInnerPanel = memo(({ children, className = '', style, ...props }) => (_jsx("div", { className: `j-inner-panel ${className}`, style: style, ...props, children: children })));
31
13
  JimboInnerPanel.displayName = 'JimboInnerPanel';
32
14
  // ─── JimboButton ──────────────────────────────────────────────────────────────
33
15
  // Canonical flat 2D Balatro-style button.
@@ -41,39 +23,21 @@ const JIMBO_TONE_PAIRS = {
41
23
  gold: [JimboColorOption.GOLD, '#8a6a1e'],
42
24
  grey: [JimboColorOption.DARK_GREY, JimboColorOption.DARKEST],
43
25
  };
44
- export function JimboButton({ tone = 'orange', size = 'md', fullWidth = false, disabled = false, uppercase = false, onClick, style, children, }) {
26
+ export function JimboButton({ tone = 'orange', size = 'md', fullWidth = false, disabled = false, uppercase = false, onClick, style, className = '', children, }) {
45
27
  const [pressed, setPressed] = useState(false);
46
28
  const [fg, sh] = JIMBO_TONE_PAIRS[tone] ?? JIMBO_TONE_PAIRS.orange;
47
- const pad = size === 'xs' ? '2px 8px' : size === 'sm' ? '4px 10px' : size === 'lg' ? '10px 18px' : '6px 14px';
48
29
  const textSize = size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : size === 'lg' ? 'lg' : 'md';
49
- return (_jsxs("div", { onMouseDown: () => { if (!disabled)
30
+ return (_jsxs("div", { className: `j-btn j-btn--${tone} j-btn--${size} ${fullWidth ? 'j-btn--full' : ''} ${disabled ? 'j-btn--disabled' : ''} ${className}`, "data-pressed": pressed, onMouseDown: () => { if (!disabled)
50
31
  setPressed(true); }, onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onTouchStart: () => { if (!disabled)
51
32
  setPressed(true); }, onTouchEnd: () => setPressed(false), onClick: () => { if (!disabled)
52
- onClick?.(); }, style: { display: 'inline-block', width: fullWidth ? '100%' : undefined, position: 'relative', cursor: disabled ? 'not-allowed' : 'pointer', userSelect: 'none', opacity: disabled ? 0.55 : 1, ...style }, children: [_jsx("div", { style: { position: 'absolute', left: 1, top: 3, right: -1, bottom: -3, background: sh, borderRadius: 6, opacity: pressed ? 0 : 1 } }), _jsx("div", { style: {
53
- position: 'relative', background: fg, borderRadius: 6, padding: pad,
54
- transform: pressed ? 'translate(1px, 3px)' : 'translate(0,0)',
55
- transition: 'transform 55ms linear',
56
- textAlign: 'center',
57
- }, children: _jsx(JimboText, { size: textSize, uppercase: uppercase, children: children }) })] }));
33
+ onClick?.(); }, style: style, children: [_jsx("div", { className: "j-btn__shadow", style: { background: sh } }), _jsx("div", { className: "j-btn__face", style: { background: fg }, children: _jsx(JimboText, { size: textSize, uppercase: uppercase, children: children }) })] }));
58
34
  }
59
35
  export function JimboBackButton({ onClick }) {
60
- return (_jsx("div", { style: { display: 'flex', justifyContent: 'center', width: '100%', padding: '4px 0' }, children: _jsx(JimboButton, { tone: "orange", size: "md", fullWidth: true, onClick: onClick, children: "Back" }) }));
36
+ return (_jsx("div", { className: "j-flex j-justify-center j-w-full", style: { padding: '4px 0' }, children: _jsx(JimboButton, { tone: "orange", size: "md", fullWidth: true, onClick: onClick, children: "Back" }) }));
61
37
  }
62
38
  export function JimboModal({ children, open, onClose, title, className }) {
63
- const [visible, setVisible] = useState(open);
64
- const [opacity, setOpacity] = useState(open ? 1 : 0);
65
- useEffect(() => {
66
- if (open) {
67
- setVisible(true);
68
- requestAnimationFrame(() => setOpacity(1));
69
- }
70
- else {
71
- setOpacity(0);
72
- const t = setTimeout(() => setVisible(false), JIMBO_ANIMATIONS.MENU_SINK_DURATION);
73
- return () => clearTimeout(t);
74
- }
75
- }, [open]);
39
+ const { visible, opacity } = useDelayedVisibility(open, JIMBO_ANIMATIONS.MENU_SINK_DURATION);
76
40
  if (!visible)
77
41
  return null;
78
- return (_jsx("div", { style: { position: 'fixed', inset: 0, zIndex: 50, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1rem', background: 'rgba(0,0,0,0.7)', opacity, transition: `opacity ${JIMBO_ANIMATIONS.MENU_SINK_DURATION}ms ease` }, onClick: onClose, children: _jsxs(JimboPanel, { sway: true, onBack: onClose, className: 'w-full flex flex-col max-h-[90vh] ' + (className ?? 'max-w-lg'), onClick: (e) => e.stopPropagation(), children: [title && _jsx(JimboText, { as: "h2", size: "lg", style: { textAlign: 'center', margin: '0 0 1rem' }, children: title }), children] }) }));
42
+ return (_jsx("div", { className: "j-modal-overlay", style: { opacity, transition: `opacity ${JIMBO_ANIMATIONS.MENU_SINK_DURATION}ms ease` }, onClick: onClose, children: _jsxs(JimboPanel, { sway: true, onBack: onClose, className: `j-modal ${className ?? ''}`, onClick: (e) => e.stopPropagation(), children: [title && _jsx(JimboText, { as: "h2", size: "lg", className: "j-modal__title", children: title }), children] }) }));
79
43
  }
@@ -23,4 +23,8 @@ export interface ShowcaseProps {
23
23
  onBrowseFilters?: () => void;
24
24
  onBack?: () => void;
25
25
  }
26
+ /**
27
+ * Landing/showcase screen for the seed curator.
28
+ * All styling via jimbo.css `.j-showcase` classes — zero inline styles.
29
+ */
26
30
  export declare function Showcase({ hotFilters, recentFinds, stats, onNewSearch, onBrowseFilters, onBack, }: ShowcaseProps): import("react/jsx-runtime").JSX.Element;