jaml-ui 0.9.0 → 0.10.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.
package/dist/assets.js CHANGED
@@ -12,7 +12,7 @@ export const JAML_ASSET_FILES = {
12
12
  };
13
13
  const assetKeyByFileName = Object.fromEntries(Object.entries(JAML_ASSET_FILES).map(([key, fileName]) => [fileName, key]));
14
14
  // Keep in lockstep with package.json version. Upload assets to this path when publishing.
15
- const JAML_UI_VERSION = "0.8.0";
15
+ const JAML_UI_VERSION = "0.10.0";
16
16
  const CDN_BASE = `https://cdn.seedfinder.app/jaml-ui/${JAML_UI_VERSION}/assets/`;
17
17
  const defaultAssetUrls = {
18
18
  deck: `${CDN_BASE}${JAML_ASSET_FILES.deck}`,
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ export interface CardFanProps {
3
+ /** Total number of cards to render (used when `cards` is not provided). */
4
+ count?: number;
5
+ /** Array of cards as "rank_suit" strings (e.g. ["2_C", "10_H", "A_S"]). Takes priority over `count`. */
6
+ cards?: string[];
7
+ /** Optional label below the fan. */
8
+ label?: string;
9
+ showLabel?: boolean;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ }
13
+ /**
14
+ * Parabolic card fan with rotation + arc overlap. Ported from weejoker's
15
+ * CardFan.tsx. Uses mathematical transforms (inline style, not Tailwind)
16
+ * to produce the authentic Balatro spread: cards overlap toward the
17
+ * center, tilt outward, sit higher at the edges.
18
+ *
19
+ * Sizing, overlap, and max rotation auto-scale with card count so full
20
+ * 52-card decks render cleanly and tiny 3-card hands look deliberate.
21
+ */
22
+ export declare function CardFan({ count, cards, className, style, label, showLabel }: CardFanProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { RealPlayingCard } from './PlayingCard.js';
4
+ import { JimboColorOption } from '../ui/tokens.js';
5
+ import { JimboText } from '../ui/jimboText.js';
6
+ const RANK_MAP = {
7
+ '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '10': '10',
8
+ J: 'Jack', Q: 'Queen', K: 'King', A: 'Ace',
9
+ };
10
+ const SUIT_MAP = {
11
+ H: 'Hearts', C: 'Clubs', D: 'Diamonds', S: 'Spades',
12
+ };
13
+ function parseJamlCard(input) {
14
+ const [r, s] = input.split('_');
15
+ return {
16
+ rank: RANK_MAP[r] ?? '2',
17
+ suit: SUIT_MAP[s] ?? 'Clubs',
18
+ };
19
+ }
20
+ /**
21
+ * Parabolic card fan with rotation + arc overlap. Ported from weejoker's
22
+ * CardFan.tsx. Uses mathematical transforms (inline style, not Tailwind)
23
+ * to produce the authentic Balatro spread: cards overlap toward the
24
+ * center, tilt outward, sit higher at the edges.
25
+ *
26
+ * Sizing, overlap, and max rotation auto-scale with card count so full
27
+ * 52-card decks render cleanly and tiny 3-card hands look deliberate.
28
+ */
29
+ export function CardFan({ count = 0, cards, className = '', style, label, showLabel = true }) {
30
+ const displayCount = cards ? cards.length : count;
31
+ const cardSize = displayCount > 40 ? 46
32
+ : displayCount > 30 ? 32
33
+ : displayCount > 12 ? 36
34
+ : displayCount > 8 ? 42
35
+ : displayCount > 5 ? 48
36
+ : 54;
37
+ const overlap = displayCount > 40 ? 0.88
38
+ : displayCount > 30 ? 0.85
39
+ : displayCount > 15 ? 0.75
40
+ : displayCount > 6 ? 0.60
41
+ : 0.45;
42
+ const cardSpacing = cardSize * (1 - overlap);
43
+ const maxRotation = displayCount > 40 ? 40
44
+ : displayCount > 20 ? 30
45
+ : displayCount > 10 ? 15
46
+ : 25;
47
+ const cardsHeight = 120;
48
+ return (_jsxs("div", { className: className, style: {
49
+ display: 'flex',
50
+ flexDirection: 'column',
51
+ alignItems: 'center',
52
+ gap: 4,
53
+ ...style,
54
+ }, children: [_jsx("div", { style: { position: 'relative', width: '100%', height: cardsHeight }, children: displayCount > 0 ? (Array.from({ length: displayCount }).map((_, i) => {
55
+ const centerIndex = (displayCount - 1) / 2;
56
+ const offset = i - centerIndex;
57
+ const xPos = offset * cardSpacing;
58
+ // Parabolic lift — outer cards sit higher than center (bowed upward)
59
+ const yOffset = Math.pow(Math.abs(offset / (centerIndex || 1)), 2) * (displayCount > 20 ? 20 : 10);
60
+ const rotation = (offset / (centerIndex || 1)) * maxRotation;
61
+ const parsed = cards ? parseJamlCard(cards[i]) : { rank: '2', suit: 'Clubs' };
62
+ return (_jsx("div", { style: {
63
+ position: 'absolute',
64
+ left: '50%',
65
+ bottom: 0,
66
+ transform: `translateX(calc(-50% + ${xPos}px)) translateY(${yOffset}px) rotate(${rotation}deg)`,
67
+ transformOrigin: 'bottom center',
68
+ zIndex: i,
69
+ }, children: _jsx(RealPlayingCard, { rank: parsed.rank, suit: parsed.suit, size: cardSize, style: { filter: `drop-shadow(0 2px 3px ${JimboColorOption.BLACK}66)` } }) }, i));
70
+ })) : (_jsx("div", { style: {
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ width: '100%',
75
+ height: '100%',
76
+ background: `${JimboColorOption.WHITE}0d`,
77
+ border: `1px solid ${JimboColorOption.WHITE}0d`,
78
+ borderRadius: 8,
79
+ }, children: _jsx(JimboText, { size: "xs", uppercase: true, tone: "grey", children: "Deck Empty" }) })) }), label && showLabel ? _jsx(JimboText, { size: "xs", uppercase: true, tone: "grey", children: label }) : null] }));
80
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ export declare const DECK_SPRITE_POS: Record<string, {
3
+ x: number;
4
+ y: number;
5
+ }>;
6
+ export declare const STAKE_SPRITE_POS: Record<string, {
7
+ x: number;
8
+ y: number;
9
+ }>;
10
+ export interface DeckSpriteProps {
11
+ /** Deck name — case-insensitive, "Deck" suffix tolerated (e.g. "Erratic Deck" → "erratic"). */
12
+ deck: string;
13
+ /** Optional stake name to overlay its sticker on the deck. */
14
+ stake?: string;
15
+ /** Rendered width in pixels. Height is scaled proportionally from the 142x190 sprite. */
16
+ size?: number;
17
+ className?: string;
18
+ style?: React.CSSProperties;
19
+ }
20
+ /**
21
+ * Balatro deck box sprite — optionally overlaid with a stake sticker.
22
+ * Draws from the Enhancers.png atlas (deck thumbnails live in the bottom
23
+ * two rows) and stickers.png for the stake band. Uses jaml-ui's
24
+ * `resolveJamlAssetUrl` so consumers can override the CDN base.
25
+ */
26
+ export declare function DeckSprite({ deck, stake, size, className, style }: DeckSpriteProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { resolveJamlAssetUrl } from '../assets.js';
4
+ export const DECK_SPRITE_POS = {
5
+ red: { x: 0, y: 0 },
6
+ nebula: { x: 3, y: 0 },
7
+ locked: { x: 4, y: 0 },
8
+ blue: { x: 0, y: 2 },
9
+ yellow: { x: 1, y: 2 },
10
+ green: { x: 2, y: 2 },
11
+ black: { x: 3, y: 2 },
12
+ plasma: { x: 4, y: 2 },
13
+ ghost: { x: 6, y: 2 },
14
+ magic: { x: 0, y: 3 },
15
+ checkered: { x: 1, y: 3 },
16
+ erratic: { x: 2, y: 3 },
17
+ abandoned: { x: 3, y: 3 },
18
+ painted: { x: 4, y: 3 },
19
+ challenge: { x: 0, y: 4 },
20
+ anaglyph: { x: 2, y: 4 },
21
+ zodiac: { x: 3, y: 4 },
22
+ };
23
+ export const STAKE_SPRITE_POS = {
24
+ white: { x: 1, y: 0 },
25
+ red: { x: 2, y: 0 },
26
+ green: { x: 3, y: 0 },
27
+ blue: { x: 4, y: 0 },
28
+ black: { x: 0, y: 1 },
29
+ purple: { x: 1, y: 1 },
30
+ orange: { x: 2, y: 1 },
31
+ gold: { x: 3, y: 1 },
32
+ };
33
+ const SPRITE_WIDTH = 142;
34
+ const SPRITE_HEIGHT = 190;
35
+ const DECK_COLS = 7;
36
+ const DECK_ROWS = 5;
37
+ const STICKER_COLS = 5;
38
+ const STICKER_ROWS = 3;
39
+ /**
40
+ * Balatro deck box sprite — optionally overlaid with a stake sticker.
41
+ * Draws from the Enhancers.png atlas (deck thumbnails live in the bottom
42
+ * two rows) and stickers.png for the stake band. Uses jaml-ui's
43
+ * `resolveJamlAssetUrl` so consumers can override the CDN base.
44
+ */
45
+ export function DeckSprite({ deck, stake, size = 50, className = '', style }) {
46
+ const deckKey = (deck || 'erratic').toLowerCase().replace(/\s*deck$/, '').trim();
47
+ const deckPos = DECK_SPRITE_POS[deckKey] ?? DECK_SPRITE_POS.erratic;
48
+ const stakePos = stake ? STAKE_SPRITE_POS[stake.toLowerCase().replace(/\s*stake$/, '').trim()] : null;
49
+ const scale = size / SPRITE_WIDTH;
50
+ const displayHeight = SPRITE_HEIGHT * scale;
51
+ return (_jsxs("div", { className: className, style: {
52
+ position: 'relative',
53
+ width: size,
54
+ height: displayHeight,
55
+ ...style,
56
+ }, children: [_jsx("div", { style: {
57
+ position: 'absolute',
58
+ inset: 0,
59
+ backgroundImage: `url(${resolveJamlAssetUrl('enhancers')})`,
60
+ backgroundSize: `${DECK_COLS * 100}% ${DECK_ROWS * 100}%`,
61
+ backgroundPosition: `${(deckPos.x / (DECK_COLS - 1)) * 100}% ${(deckPos.y / (DECK_ROWS - 1)) * 100}%`,
62
+ backgroundRepeat: 'no-repeat',
63
+ imageRendering: 'pixelated',
64
+ } }), stakePos ? (_jsx("div", { style: {
65
+ position: 'absolute',
66
+ inset: 0,
67
+ backgroundImage: `url(${resolveJamlAssetUrl('stickers')})`,
68
+ backgroundSize: `${STICKER_COLS * 100}% ${STICKER_ROWS * 100}%`,
69
+ backgroundPosition: `${(stakePos.x / (STICKER_COLS - 1)) * 100}% ${(stakePos.y / (STICKER_ROWS - 1)) * 100}%`,
70
+ backgroundRepeat: 'no-repeat',
71
+ imageRendering: 'pixelated',
72
+ } })) : null] }));
73
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ export interface MotelyCapabilities {
3
+ version: string;
4
+ simd?: boolean;
5
+ threads?: boolean;
6
+ }
7
+ export interface MotelyVersionBadgeProps {
8
+ /**
9
+ * Runtime capabilities from `motely.getCapabilities()`. If omitted, the
10
+ * consumer can pass a static `version` (typically from motely-wasm's
11
+ * own package.json) and the component will render without the SIMD /
12
+ * threads indicators.
13
+ */
14
+ caps?: MotelyCapabilities | null;
15
+ /** Static fallback version when `caps` is null/undefined. */
16
+ version?: string;
17
+ /** Compact single-line badge instead of the labelled chip. Default false. */
18
+ minimal?: boolean;
19
+ /** Loading placeholder (shown while caps are being fetched). */
20
+ loading?: boolean;
21
+ className?: string;
22
+ style?: React.CSSProperties;
23
+ }
24
+ /**
25
+ * Badge showing the loaded motely-wasm version + optional SIMD / threads
26
+ * capability indicators. Ported from weejoker.app with no dependency on
27
+ * weejoker's lib/api — the consumer owns capability fetching and passes
28
+ * the result in.
29
+ */
30
+ export declare function MotelyVersionBadge({ caps, version, minimal, loading, className, style, }: MotelyVersionBadgeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { JimboColorOption } from '../ui/tokens.js';
4
+ import { JimboText } from '../ui/jimboText.js';
5
+ /**
6
+ * Badge showing the loaded motely-wasm version + optional SIMD / threads
7
+ * capability indicators. Ported from weejoker.app with no dependency on
8
+ * weejoker's lib/api — the consumer owns capability fetching and passes
9
+ * the result in.
10
+ */
11
+ export function MotelyVersionBadge({ caps, version, minimal = false, loading = false, className = '', style, }) {
12
+ if (loading) {
13
+ return (_jsx("span", { className: className, style: style, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Initializing\u2026" }) }));
14
+ }
15
+ const resolved = caps?.version ?? version ?? '?';
16
+ const simd = caps?.simd;
17
+ const threads = caps?.threads;
18
+ if (minimal) {
19
+ return (_jsxs("span", { className: className, style: { display: 'inline-flex', alignItems: 'center', gap: 6, ...style }, children: [_jsxs(JimboText, { size: "xs", tone: "grey", children: ["v", resolved] }), simd ? (_jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" })) : null, threads ? (_jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" })) : null] }));
20
+ }
21
+ return (_jsxs("div", { className: className, style: {
22
+ display: 'inline-flex',
23
+ alignItems: 'center',
24
+ gap: 6,
25
+ padding: '3px 8px',
26
+ borderRadius: 4,
27
+ background: JimboColorOption.DARKEST,
28
+ border: `1px solid ${JimboColorOption.PANEL_EDGE}`,
29
+ ...style,
30
+ }, children: [_jsxs(JimboText, { size: "xs", tone: "gold", uppercase: true, children: ["motely v", resolved] }), simd ? _jsx(JimboText, { size: "xs", tone: "blue", title: "SIMD enabled", children: "\u26A1" }) : null, threads ? _jsx(JimboText, { size: "xs", tone: "green", title: "Multi-threaded", children: "\uD83E\uDDF5" }) : null] }));
31
+ }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,17 @@ export { JamlMapPreview, type JamlMapPreviewProps } from "./components/JamlMapPr
7
7
  export { JamlIde, type JamlIdeProps, type JamlIdeSearchResult, type JamlVisualFilter, type JamlVisualClause, type JamlZone, } from "./components/JamlIde.js";
8
8
  export { JamlIdeVisual, type JamlIdeVisualProps, } from "./components/JamlIdeVisual.js";
9
9
  export { JamlCodeEditor, type JamlCodeEditorProps, } from "./components/JamlCodeEditor.js";
10
+ export { JimboText, type JimboTextProps, type JimboTextTone, type JimboTextSize, } from "./ui/jimboText.js";
11
+ export { JimboTabs, JimboVerticalTabs, type JimboTabItem, type JimboTabsProps, } from "./ui/jimboTabs.js";
12
+ export { JimboFlankNav, type JimboFlankNavProps, } from "./ui/jimboFlankNav.js";
13
+ export { JimboFilterBar, type JimboFilterBarProps, type JimboFilterSortOption, } from "./ui/jimboFilterBar.js";
14
+ export { JimboBackground } from "./ui/jimboBackground.js";
15
+ export { JimboTooltip, type JimboTooltipProps, type JimboTooltipMode, type JimboTooltipPlacement, } from "./ui/jimboTooltip.js";
10
16
  export { JamlIdeToolbar, type JamlIdeMode, type JamlIdeToolbarProps, } from "./components/JamlIdeToolbar.js";
11
17
  export { CardList, type CardListProps } from "./components/CardList.js";
18
+ export { CardFan, type CardFanProps } from "./components/CardFan.js";
19
+ export { DeckSprite, DECK_SPRITE_POS, STAKE_SPRITE_POS, type DeckSpriteProps, } from "./components/DeckSprite.js";
20
+ export { MotelyVersionBadge, type MotelyVersionBadgeProps, type MotelyCapabilities, } from "./components/MotelyVersionBadge.js";
12
21
  export { extractVisualJamlItems, type JamlPreviewGroups, type JamlPreviewItem, type JamlPreviewSection, type JamlPreviewVisualType, } from "./utils/jamlMapPreview.js";
13
22
  export { useMotelyStream, type StreamItem, type StreamState } from "./hooks/useShopStream.js";
23
+ export { useSearch, type SearchResult, type SearchStatus, type UseSearchState, } from "./hooks/useSearch.js";
package/dist/index.js CHANGED
@@ -8,7 +8,17 @@ export { JamlMapPreview } from "./components/JamlMapPreview.js";
8
8
  export { JamlIde, } from "./components/JamlIde.js";
9
9
  export { JamlIdeVisual, } from "./components/JamlIdeVisual.js";
10
10
  export { JamlCodeEditor, } from "./components/JamlCodeEditor.js";
11
+ export { JimboText, } from "./ui/jimboText.js";
12
+ export { JimboTabs, JimboVerticalTabs, } from "./ui/jimboTabs.js";
13
+ export { JimboFlankNav, } from "./ui/jimboFlankNav.js";
14
+ export { JimboFilterBar, } from "./ui/jimboFilterBar.js";
15
+ export { JimboBackground } from "./ui/jimboBackground.js";
16
+ export { JimboTooltip, } from "./ui/jimboTooltip.js";
11
17
  export { JamlIdeToolbar, } from "./components/JamlIdeToolbar.js";
12
18
  export { CardList } from "./components/CardList.js";
19
+ export { CardFan } from "./components/CardFan.js";
20
+ export { DeckSprite, DECK_SPRITE_POS, STAKE_SPRITE_POS, } from "./components/DeckSprite.js";
21
+ export { MotelyVersionBadge, } from "./components/MotelyVersionBadge.js";
13
22
  export { extractVisualJamlItems, } from "./utils/jamlMapPreview.js";
14
23
  export { useMotelyStream } from "./hooks/useShopStream.js";
24
+ export { useSearch, } from "./hooks/useSearch.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Fullscreen WebGL CRT/spin background — the authentic Balatro hypnotic
3
+ * swirl, pixelated and animated. Ported from weejoker.app's
4
+ * BackgroundShader.tsx; no config required.
5
+ *
6
+ * Renders a fixed-position canvas at z-index: -10 that fills the viewport
7
+ * and ignores pointer events. Drop it once at the root of your page:
8
+ *
9
+ * <JimboBackground />
10
+ * <YourAppContent />
11
+ *
12
+ * Resizes automatically. Disposes the animation frame + shader on unmount.
13
+ */
14
+ export declare function JimboBackground(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,155 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from 'react';
4
+ /**
5
+ * Fullscreen WebGL CRT/spin background — the authentic Balatro hypnotic
6
+ * swirl, pixelated and animated. Ported from weejoker.app's
7
+ * BackgroundShader.tsx; no config required.
8
+ *
9
+ * Renders a fixed-position canvas at z-index: -10 that fills the viewport
10
+ * and ignores pointer events. Drop it once at the root of your page:
11
+ *
12
+ * <JimboBackground />
13
+ * <YourAppContent />
14
+ *
15
+ * Resizes automatically. Disposes the animation frame + shader on unmount.
16
+ */
17
+ export function JimboBackground() {
18
+ const canvasRef = useRef(null);
19
+ useEffect(() => {
20
+ const canvas = canvasRef.current;
21
+ if (!canvas)
22
+ return;
23
+ const gl = canvas.getContext('webgl');
24
+ if (!gl)
25
+ return;
26
+ const vsSource = `
27
+ attribute vec2 position;
28
+ void main() {
29
+ gl_Position = vec4(position, 0.0, 1.0);
30
+ }
31
+ `;
32
+ const fsSource = `
33
+ precision mediump float;
34
+
35
+ uniform float u_time;
36
+ uniform vec2 u_resolution;
37
+
38
+ const float SPIN_ROTATION = -2.0;
39
+ const float SPIN_SPEED = 4.5;
40
+ const vec4 COLOUR_1 = vec4(1.0, 0.2, 0.2, 1.0);
41
+ const vec4 COLOUR_2 = vec4(0.0, 0.5, 1.0, 1.0);
42
+ const vec4 COLOUR_3 = vec4(0.05, 0.08, 0.1, 1.0);
43
+ const float CONTRAST = 4.5;
44
+ const float LIGTHING = 0.5;
45
+ const float SPIN_AMOUNT = 0.35;
46
+ const float PIXEL_FILTER = 1024.0;
47
+ const float PI = 3.14159265359;
48
+
49
+ void main() {
50
+ vec2 screenSize = u_resolution;
51
+ float pixel_size = length(screenSize.xy) / PIXEL_FILTER;
52
+ vec2 uv = (floor(gl_FragCoord.xy*(1.0/pixel_size))*pixel_size - 0.5*screenSize.xy)/length(screenSize.xy);
53
+ float uv_len = length(uv);
54
+
55
+ float speed = (SPIN_ROTATION * 0.2) + 302.2;
56
+ float new_pixel_angle = atan(uv.y, uv.x) + speed - 20.0*(1.0*SPIN_AMOUNT*uv_len + (1.0 - 1.0*SPIN_AMOUNT));
57
+
58
+ vec2 mid = (screenSize.xy/length(screenSize.xy))/2.0;
59
+ uv = (vec2((uv_len * cos(new_pixel_angle) + mid.x), (uv_len * sin(new_pixel_angle) + mid.y)) - mid);
60
+
61
+ uv *= 30.0;
62
+ speed = u_time * SPIN_SPEED;
63
+ vec2 uv2 = vec2(uv.x, uv.y);
64
+
65
+ for(int i=0; i < 5; i++) {
66
+ uv2 += sin(max(uv.x, uv.y)) + uv;
67
+ uv += 0.5*vec2(cos(5.1123314 + 0.353*uv2.y + speed*0.131121), sin(uv2.x - 0.113*speed));
68
+ uv -= 1.0*cos(uv.x + uv.y) - 1.0*sin(uv.x*0.711 - uv.y);
69
+ }
70
+
71
+ float contrast_mod = (0.25*CONTRAST + 0.5*SPIN_AMOUNT + 1.2);
72
+ float paint_res = min(2.0, max(0.0, length(uv)*(0.035)*contrast_mod));
73
+ float c1p = max(0.0, 1.0 - contrast_mod*abs(1.0 - paint_res));
74
+ float c2p = max(0.0, 1.0 - contrast_mod*abs(paint_res));
75
+ float c3p = 1.0 - min(1.0, c1p + c2p);
76
+ float light = (LIGTHING - 0.2)*max(c1p*5.0 - 4.0, 0.0) + LIGTHING*max(c2p*5.0 - 4.0, 0.0);
77
+
78
+ vec4 finalCol = (0.3/CONTRAST)*COLOUR_1 + (1.0 - 0.3/CONTRAST)*(COLOUR_1*c1p + COLOUR_2*c2p + vec4(c3p*COLOUR_3.rgb, c3p*COLOUR_1.a)) + light;
79
+
80
+ gl_FragColor = finalCol;
81
+ }
82
+ `;
83
+ const createShader = (type, source) => {
84
+ const shader = gl.createShader(type);
85
+ if (!shader)
86
+ return null;
87
+ gl.shaderSource(shader, source);
88
+ gl.compileShader(shader);
89
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
90
+ console.error('[JimboBackground] shader compile error:', gl.getShaderInfoLog(shader));
91
+ gl.deleteShader(shader);
92
+ return null;
93
+ }
94
+ return shader;
95
+ };
96
+ const vertexShader = createShader(gl.VERTEX_SHADER, vsSource);
97
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fsSource);
98
+ if (!vertexShader || !fragmentShader)
99
+ return;
100
+ const program = gl.createProgram();
101
+ if (!program)
102
+ return;
103
+ gl.attachShader(program, vertexShader);
104
+ gl.attachShader(program, fragmentShader);
105
+ gl.linkProgram(program);
106
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
107
+ console.error('[JimboBackground] program link error:', gl.getProgramInfoLog(program));
108
+ return;
109
+ }
110
+ gl.useProgram(program);
111
+ const positionBuffer = gl.createBuffer();
112
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
113
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
114
+ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
115
+ -1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
116
+ ]), gl.STATIC_DRAW);
117
+ const positionLocation = gl.getAttribLocation(program, 'position');
118
+ gl.enableVertexAttribArray(positionLocation);
119
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
120
+ const timeLocation = gl.getUniformLocation(program, 'u_time');
121
+ const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
122
+ const startTime = Date.now();
123
+ let animationFrameId;
124
+ const render = () => {
125
+ const displayWidth = canvas.clientWidth;
126
+ const displayHeight = canvas.clientHeight;
127
+ if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
128
+ canvas.width = displayWidth;
129
+ canvas.height = displayHeight;
130
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
131
+ }
132
+ const currentTime = (Date.now() - startTime) / 1000.0;
133
+ gl.uniform1f(timeLocation, currentTime);
134
+ gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
135
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
136
+ animationFrameId = requestAnimationFrame(render);
137
+ };
138
+ render();
139
+ return () => {
140
+ cancelAnimationFrame(animationFrameId);
141
+ gl.deleteProgram(program);
142
+ gl.deleteShader(vertexShader);
143
+ gl.deleteShader(fragmentShader);
144
+ gl.deleteBuffer(positionBuffer);
145
+ };
146
+ }, []);
147
+ return (_jsx("canvas", { ref: canvasRef, "aria-hidden": true, style: {
148
+ position: 'fixed',
149
+ inset: 0,
150
+ width: '100%',
151
+ height: '100%',
152
+ zIndex: -10,
153
+ pointerEvents: 'none',
154
+ } }));
155
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export interface JimboFilterSortOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ export interface JimboFilterBarProps {
7
+ search?: string;
8
+ onSearchChange?: (query: string) => void;
9
+ searchPlaceholder?: string;
10
+ searchLabel?: string;
11
+ sort?: string;
12
+ onSortChange?: (value: string) => void;
13
+ sortLabel?: string;
14
+ sortOptions?: JimboFilterSortOption[];
15
+ className?: string;
16
+ style?: React.CSSProperties;
17
+ }
18
+ /**
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.
24
+ */
25
+ export declare function JimboFilterBar({ search, onSearchChange, searchPlaceholder, searchLabel, sort, onSortChange, sortLabel, sortOptions, className, style, }: JimboFilterBarProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { JimboColorOption } from './tokens.js';
4
+ import { JimboText } from './jimboText.js';
5
+ /**
6
+ * 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.
11
+ */
12
+ 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
+ textTransform: 'uppercase',
39
+ outline: 'none',
40
+ } })] }) })) : 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: {
41
+ appearance: 'none',
42
+ WebkitAppearance: 'none',
43
+ MozAppearance: 'none',
44
+ backgroundColor: JimboColorOption.ORANGE,
45
+ color: JimboColorOption.WHITE,
46
+ border: 'none',
47
+ borderBottom: `4px solid ${JimboColorOption.DARK_ORANGE}`,
48
+ borderRadius: 8,
49
+ cursor: 'pointer',
50
+ fontFamily: "'m6x11plus', 'Courier New', monospace",
51
+ fontSize: 18,
52
+ letterSpacing: 2,
53
+ textTransform: 'uppercase',
54
+ padding: '14px 48px 14px 24px',
55
+ minWidth: 200,
56
+ textAlign: 'center',
57
+ outline: 'none',
58
+ }, 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] }));
59
+ }
60
+ function FloatingLabelField({ label, children }) {
61
+ return (_jsxs("div", { style: { flex: 1, minWidth: 200, position: 'relative', marginTop: 10 }, children: [_jsx("div", { style: {
62
+ position: 'absolute',
63
+ top: -14,
64
+ left: 16,
65
+ backgroundColor: JimboColorOption.RED,
66
+ border: `2px solid ${JimboColorOption.DARK_RED}`,
67
+ borderRadius: 6,
68
+ padding: '4px 12px',
69
+ zIndex: 2,
70
+ }, children: _jsx(JimboText, { size: "xs", uppercase: true, children: label }) }), children] }));
71
+ }
72
+ function SearchIcon() {
73
+ 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 })] }));
74
+ }
75
+ function SortIcon() {
76
+ return (_jsxs("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("polyline", { points: "7 4 7 20" }), _jsx("polyline", { points: "3 8 7 4 11 8" }), _jsx("polyline", { points: "17 20 17 4" }), _jsx("polyline", { points: "21 16 17 20 13 16" })] }));
77
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ export interface JimboFlankNavProps {
3
+ onPrev: () => void;
4
+ onNext: () => void;
5
+ canPrev?: boolean;
6
+ canNext?: boolean;
7
+ prevLabel?: string;
8
+ nextLabel?: string;
9
+ children: React.ReactNode;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ }
13
+ /**
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).
17
+ */
18
+ export declare function JimboFlankNav({ onPrev, onNext, canPrev, canNext, prevLabel, nextLabel, children, className, style, }: JimboFlankNavProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { JimboColorOption, JIMBO_ANIMATIONS } from './tokens.js';
5
+ /**
6
+ * 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).
9
+ */
10
+ 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 })] }));
20
+ }
21
+ function NavButton({ direction, onClick, disabled, 'aria-label': ariaLabel, }) {
22
+ 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 ? 'not-allowed' : 'pointer',
29
+ opacity: disabled ? 0.35 : 1,
30
+ backgroundColor: 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 ? '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`,
38
+ }, children: _jsx(ChevronSvg, { direction: direction }) }));
39
+ }
40
+ 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 }) }));
43
+ }