jaml-ui 0.6.1 → 0.7.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/README.md CHANGED
@@ -1,15 +1,6 @@
1
1
  # jaml-ui
2
2
 
3
- React components and utilities for Balatro/JAML. Pair with **`motely-wasm`** when you need the engine (peer). Update both in consumers with normal **`pnpm update`** / semver like any other deps.
4
-
5
- ## Package shape
6
-
7
- - `jaml-ui`
8
- - React/client entry for rendered components and hooks
9
- - `jaml-ui/core`
10
- - Pure asset helpers, sprite metadata, and decode utilities that do not depend on `motely-wasm`
11
- - `jaml-ui/motely`
12
- - Optional plain `motely-wasm` helpers for decoding Motely item enums
3
+ React components, UI tokens, sprites, and utilities for Balatro/JAML apps.
13
4
 
14
5
  ## Install
15
6
 
@@ -17,129 +8,128 @@ React components and utilities for Balatro/JAML. Pair with **`motely-wasm`** whe
17
8
  npm install jaml-ui react react-dom
18
9
  ```
19
10
 
20
- If you want the Motely-specific helpers too:
11
+ ## Package exports
21
12
 
22
- ```bash
23
- npm install jaml-ui motely-wasm react react-dom
24
- ```
13
+ | Entry | Contents |
14
+ |-------|----------|
15
+ | `jaml-ui` | Game card components, JAML IDE, Analyzer Explorer, hooks |
16
+ | `jaml-ui/ui` | Jimbo design system — JimboPanel, JimboButton, JimboModal, tokens |
17
+ | `jaml-ui/core` | Pure asset helpers, sprite metadata, decode utilities (no React) |
18
+ | `jaml-ui/motely` | motely-wasm decode helpers (requires `motely-wasm` peer) |
19
+ | `jaml-ui/r3f` | 3D card component via React Three Fiber (requires r3f peers) |
25
20
 
26
21
  ## Quick start
27
22
 
28
23
  ```tsx
29
- "use client";
24
+ import { JamlGameCard, AnalyzerExplorer, JamlIde } from "jaml-ui";
25
+ import { JimboPanel, JimboButton } from "jaml-ui/ui";
26
+ ```
27
+
28
+ ### Game card
30
29
 
30
+ ```tsx
31
31
  import { JamlGameCard } from "jaml-ui";
32
32
 
33
- export function Example() {
34
- return (
35
- <JamlGameCard
36
- type="joker"
37
- card={{
38
- name: "Blueprint",
39
- edition: "Foil",
40
- isEternal: true,
41
- scale: 1.5,
42
- }}
43
- />
44
- );
45
- }
33
+ <JamlGameCard
34
+ type="joker"
35
+ card={{ name: "Blueprint", edition: "Foil", isEternal: true, scale: 1.5 }}
36
+ />
46
37
  ```
47
38
 
48
- ## JAML preview
39
+ ### Jimbo UI (Balatro design system)
49
40
 
50
41
  ```tsx
51
- "use client";
52
-
53
- import { JamlMapPreview } from "jaml-ui";
42
+ import { JimboPanel, JimboButton, JimboModal } from "jaml-ui/ui";
43
+ import { JimboColorOption } from "jaml-ui/ui";
54
44
 
55
- export function PreviewExample({ jaml }: { jaml: string }) {
56
- return <JamlMapPreview jaml={jaml} title="JAML Intent Preview" />;
57
- }
45
+ <JimboPanel sway onBack={() => setOpen(false)}>
46
+ <JimboButton variant="primary" onClick={handleSearch}>Search</JimboButton>
47
+ </JimboPanel>
58
48
  ```
59
49
 
60
- ## Lightweight JAML IDE shell
50
+ Available variants: `primary`, `secondary`, `danger`, `back`, `ghost`
61
51
 
62
- ```tsx
63
- "use client";
52
+ ### JAML IDE
64
53
 
65
- import { useState } from "react";
54
+ ```tsx
66
55
  import { JamlIde } from "jaml-ui";
67
56
 
68
- export function IdeExample() {
69
- const [jaml, setJaml] = useState("must:\n joker: Blueprint");
70
-
71
- return (
72
- <JamlIde
73
- jaml={jaml}
74
- onChange={setJaml}
75
- results={[]}
76
- />
77
- );
78
- }
57
+ <JamlIde
58
+ jaml={jaml}
59
+ onChange={setJaml}
60
+ searchResults={results}
61
+ onSearch={handleSearch}
62
+ isSearching={isSearching}
63
+ />
79
64
  ```
80
65
 
81
- ## Asset handling
66
+ ### Analyzer Explorer
82
67
 
83
- By default, `jaml-ui` resolves its packaged sprite assets from the package `assets/` directory using `import.meta.url`.
68
+ ```tsx
69
+ import { AnalyzerExplorer } from "jaml-ui";
70
+
71
+ // antes: AnalyzerAnteView[] — stream from motely-wasm createSearchContext
72
+ <AnalyzerExplorer antes={antes} totalAntes={8} highlights={highlights} />
73
+ ```
84
74
 
85
- If you want to host the assets yourself, set a custom base URL once during app startup:
75
+ ### JAML Map Preview
86
76
 
87
77
  ```tsx
88
- "use client";
89
-
90
- import { setJamlAssetBaseUrl } from "jaml-ui";
78
+ import { JamlMapPreview } from "jaml-ui";
91
79
 
92
- setJamlAssetBaseUrl("/vendor/jaml-ui/");
80
+ <JamlMapPreview jaml={jaml} />
93
81
  ```
94
82
 
95
- Reset back to packaged assets with:
83
+ ## Asset handling
84
+
85
+ By default sprites resolve from the package `assets/` directory via `import.meta.url`.
86
+
87
+ Override at app startup:
96
88
 
97
89
  ```ts
98
- import { clearJamlAssetBaseUrl } from "jaml-ui";
90
+ import { setJamlAssetBaseUrl, clearJamlAssetBaseUrl } from "jaml-ui";
99
91
 
100
- clearJamlAssetBaseUrl();
92
+ setJamlAssetBaseUrl("/vendor/jaml-ui/"); // custom CDN
93
+ clearJamlAssetBaseUrl(); // back to default
101
94
  ```
102
95
 
103
96
  ## Core utilities
104
97
 
105
98
  ```ts
106
99
  import { SPRITE_SHEETS, getSpriteData, resolveJamlAssetUrl } from "jaml-ui/core";
107
-
108
- const jokerSheetUrl = SPRITE_SHEETS.jokers.src;
109
- const blueprintSprite = getSpriteData("Blueprint");
110
- const vouchersUrl = resolveJamlAssetUrl("vouchers");
111
100
  ```
112
101
 
113
- ## Motely helpers
102
+ ## Motely decode helpers
114
103
 
115
104
  ```ts
116
- "use client";
117
-
118
105
  import { decodeMotelyItemName, motelyItemTypeName } from "jaml-ui/motely";
106
+ ```
107
+
108
+ ## 3D card (optional)
119
109
 
120
- const itemName = decodeMotelyItemName(0x5001);
121
- const enumKey = motelyItemTypeName(0x5001);
110
+ ```bash
111
+ npm install three @react-three/fiber @react-three/drei @react-spring/three
122
112
  ```
123
113
 
124
- ## Next.js notes
114
+ ```tsx
115
+ import { Card3D } from "jaml-ui/r3f";
125
116
 
126
- - The root `jaml-ui` entry is client-oriented and preserves the `"use client"` boundary for component consumers.
127
- - Import pure helpers from `jaml-ui/core` when you want server-safe metadata and asset utilities.
128
- - If you are consuming `jaml-ui` from a local workspace package in a Next.js app, you may need:
117
+ <Card3D itemName="Blueprint" />
118
+ ```
129
119
 
130
- ```ts
131
- // next.config.ts
132
- import type { NextConfig } from "next";
120
+ ## Next.js
133
121
 
134
- const nextConfig: NextConfig = {
135
- transpilePackages: ["jaml-ui"],
136
- };
122
+ Import pure helpers from `jaml-ui/core` for server components. For local workspace installs add:
137
123
 
138
- export default nextConfig;
124
+ ```ts
125
+ // next.config.ts
126
+ const nextConfig = { transpilePackages: ["jaml-ui"] };
139
127
  ```
140
128
 
141
- ## Browser-first runtime direction
142
-
143
- `jaml-ui` is designed for browser/React consumers. The optional `jaml-ui/motely` entry targets plain `motely-wasm` and does not assume threaded WASM, SAB, or COEP setup.
129
+ ## Peer dependencies
144
130
 
145
- The built-in `JamlIde` intentionally stays lightweight. Rich editor integrations like Monaco, custom language servers, or extension-host-specific tooling should live in app-level packages on top of `jaml-ui`, not in the base renderer package.
131
+ | Peer | Required for |
132
+ |------|-------------|
133
+ | `react`, `react-dom` | All components |
134
+ | `motely-wasm ^10 \|\| ^11 \|\| ^12` | `jaml-ui/motely`, `AnalyzerExplorer` data |
135
+ | `three`, `@react-three/fiber`, `@react-three/drei`, `@react-spring/three` | `jaml-ui/r3f` only |
package/dist/assets.js CHANGED
@@ -11,17 +11,20 @@ export const JAML_ASSET_FILES = {
11
11
  tags: "tags.png",
12
12
  };
13
13
  const assetKeyByFileName = Object.fromEntries(Object.entries(JAML_ASSET_FILES).map(([key, fileName]) => [fileName, key]));
14
+ // Keep in lockstep with package.json version. Upload assets to this path when publishing.
15
+ const JAML_UI_VERSION = "0.7.0";
16
+ const CDN_BASE = `https://cdn.seedfinder.app/jaml-ui/${JAML_UI_VERSION}/assets/`;
14
17
  const defaultAssetUrls = {
15
- deck: new URL("../assets/8BitDeck.png", import.meta.url).href,
16
- blinds: new URL("../assets/BlindChips.png", import.meta.url).href,
17
- boosters: new URL("../assets/Boosters.png", import.meta.url).href,
18
- editions: new URL("../assets/Editions.png", import.meta.url).href,
19
- enhancers: new URL("../assets/Enhancers.png", import.meta.url).href,
20
- jokers: new URL("../assets/Jokers.png", import.meta.url).href,
21
- tarots: new URL("../assets/Tarots.png", import.meta.url).href,
22
- vouchers: new URL("../assets/Vouchers.png", import.meta.url).href,
23
- stickers: new URL("../assets/stickers.png", import.meta.url).href,
24
- tags: new URL("../assets/tags.png", import.meta.url).href,
18
+ deck: `${CDN_BASE}${JAML_ASSET_FILES.deck}`,
19
+ blinds: `${CDN_BASE}${JAML_ASSET_FILES.blinds}`,
20
+ boosters: `${CDN_BASE}${JAML_ASSET_FILES.boosters}`,
21
+ editions: `${CDN_BASE}${JAML_ASSET_FILES.editions}`,
22
+ enhancers: `${CDN_BASE}${JAML_ASSET_FILES.enhancers}`,
23
+ jokers: `${CDN_BASE}${JAML_ASSET_FILES.jokers}`,
24
+ tarots: `${CDN_BASE}${JAML_ASSET_FILES.tarots}`,
25
+ vouchers: `${CDN_BASE}${JAML_ASSET_FILES.vouchers}`,
26
+ stickers: `${CDN_BASE}${JAML_ASSET_FILES.stickers}`,
27
+ tags: `${CDN_BASE}${JAML_ASSET_FILES.tags}`,
25
28
  };
26
29
  let customAssetBaseUrl = null;
27
30
  function normalizeBaseUrl(baseUrl) {
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { JimboButton } from "../ui/panel.js";
3
4
  import { JimboColorOption } from "../ui/tokens.js";
4
5
  const TABS = [
5
6
  { id: "visual", label: "Visual" },
@@ -16,40 +17,5 @@ export function JamlIdeToolbar({ mode, onModeChange, resultCount = 0, className
16
17
  padding: "6px 10px",
17
18
  borderBottom: `1px solid ${JimboColorOption.PANEL_EDGE}`,
18
19
  background: JimboColorOption.DARKEST,
19
- }, children: [_jsx("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: TABS.map((tab) => {
20
- const selected = mode === tab.id;
21
- return (_jsxs("button", { type: "button", onClick: () => onModeChange(tab.id), style: {
22
- cursor: "pointer",
23
- borderRadius: 6,
24
- border: selected ? `1px solid ${JimboColorOption.GOLD}` : "1px solid transparent",
25
- background: selected ? `${JimboColorOption.GOLD}22` : "transparent",
26
- color: selected ? JimboColorOption.GOLD_TEXT : JimboColorOption.GREY,
27
- padding: "5px 10px",
28
- fontSize: 11,
29
- fontWeight: 700,
30
- fontFamily: "m6x11plus, monospace",
31
- transition: "background 120ms, color 120ms",
32
- }, children: [tab.label, tab.id === "results" && resultCount > 0 ? (_jsx("span", { style: {
33
- marginLeft: 6,
34
- borderRadius: 999,
35
- background: `${JimboColorOption.GOLD}33`,
36
- color: JimboColorOption.GOLD_TEXT,
37
- padding: "1px 6px",
38
- fontSize: 10,
39
- }, children: resultCount })) : null] }, tab.id));
40
- }) }), onSearch ? (_jsx("button", { type: "button", onClick: onSearch, style: {
41
- cursor: "pointer",
42
- borderRadius: 6,
43
- border: isSearching
44
- ? `1px solid ${JimboColorOption.DARK_RED}`
45
- : `1px solid ${JimboColorOption.GREEN}`,
46
- background: isSearching
47
- ? `${JimboColorOption.RED}22`
48
- : `${JimboColorOption.GREEN}22`,
49
- color: isSearching ? JimboColorOption.RED : JimboColorOption.GREEN_TEXT,
50
- padding: "5px 14px",
51
- fontSize: 11,
52
- fontWeight: 700,
53
- fontFamily: "m6x11plus, monospace",
54
- }, children: isSearching ? "Stop" : "Search" })) : null] }));
20
+ }, children: [_jsx("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: TABS.map((tab) => (_jsxs(JimboButton, { tone: mode === tab.id ? "gold" : "grey", size: "xs", onClick: () => onModeChange(tab.id), children: [tab.label, tab.id === "results" && resultCount > 0 ? (_jsx("span", { style: { marginLeft: 6, borderRadius: 999, background: "rgba(228,182,67,0.2)", color: JimboColorOption.GOLD_TEXT, padding: "1px 6px", fontSize: 10 }, children: resultCount })) : null] }, tab.id))) }), onSearch ? (_jsx(JimboButton, { tone: isSearching ? "red" : "blue", size: "xs", onClick: onSearch, children: isSearching ? "Stop" : "Search" })) : null] }));
55
21
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
4
  import { JimboColorOption } from "../ui/tokens.js";
5
- import { BalSprite } from "../ui/sprites.js";
5
+ import { JimboSprite } from "../ui/sprites.js";
6
6
  const ZONE_META = {
7
7
  must: { label: "MUST", color: JimboColorOption.BLUE },
8
8
  should: { label: "SHOULD", color: JimboColorOption.RED },
@@ -25,7 +25,7 @@ function ClauseSprite({ clause, size = 26 }) {
25
25
  const sheet = clauseSpriteSheet(clause.type);
26
26
  if (!sheet)
27
27
  return null;
28
- return _jsx(BalSprite, { name: clause.value, sheet: sheet, width: size });
28
+ return _jsx(JimboSprite, { name: clause.value, sheet: sheet, width: size });
29
29
  }
30
30
  function DragClausePill({ clause, zone, onDragStart, }) {
31
31
  const z = ZONE_META[zone];
@@ -0,0 +1,7 @@
1
+ interface MotelyModules {
2
+ MotelyWasm: any;
3
+ MotelyWasmEvents: any;
4
+ Motely: any;
5
+ }
6
+ export declare function loadMotelyWasm(url: string): Promise<MotelyModules>;
7
+ export {};
@@ -0,0 +1,16 @@
1
+ // Module-level cache so multiple hooks share a single boot per URL.
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ const cache = new Map();
4
+ export function loadMotelyWasm(url) {
5
+ if (!cache.has(url)) {
6
+ cache.set(url, (async () => {
7
+ const mod = await import(/* @vite-ignore */ url);
8
+ await mod.default.boot();
9
+ return { MotelyWasm: mod.MotelyWasm, MotelyWasmEvents: mod.MotelyWasmEvents, Motely: mod.Motely };
10
+ })().catch((err) => {
11
+ cache.delete(url);
12
+ throw err;
13
+ }));
14
+ }
15
+ return cache.get(url);
16
+ }
@@ -0,0 +1 @@
1
+ export declare const SEARCH_WORKER_CODE = "\nlet MotelyWasm = null;\nlet MotelyWasmEvents = null;\nlet activeSearch = null;\n\nself.addEventListener('message', async function(e) {\n const msg = e.data;\n\n if (msg.type === 'init') {\n try {\n const mod = await import(msg.url);\n await mod.default.boot();\n MotelyWasm = mod.MotelyWasm;\n MotelyWasmEvents = mod.MotelyWasmEvents;\n self.postMessage({ type: 'ready' });\n } catch (err) {\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'start') {\n if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }\n const validation = MotelyWasm.validateJaml(msg.jaml);\n if (validation !== 'valid') { self.postMessage({ type: 'error', message: validation }); return; }\n\n let rId, pId, cId;\n function cleanup() {\n MotelyWasmEvents.onResult.unsubscribeById(rId);\n MotelyWasmEvents.onProgress.unsubscribeById(pId);\n MotelyWasmEvents.onComplete.unsubscribeById(cId);\n activeSearch = null;\n }\n\n rId = MotelyWasmEvents.onResult.subscribe(function(seed, score) {\n self.postMessage({ type: 'result', seed, score });\n });\n pId = MotelyWasmEvents.onProgress.subscribe(function(searched, matching) {\n self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });\n });\n cId = MotelyWasmEvents.onComplete.subscribe(function(status, searched, matched) {\n cleanup();\n self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });\n });\n\n try {\n activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);\n } catch (err) {\n cleanup();\n self.postMessage({ type: 'error', message: String(err) });\n }\n return;\n }\n\n if (msg.type === 'stop') {\n if (activeSearch) { activeSearch.cancel(); activeSearch = null; }\n self.postMessage({ type: 'cancelled' });\n }\n});\n";
@@ -0,0 +1,62 @@
1
+ // Worker code as an inline string — created as a Blob URL at runtime.
2
+ // This avoids bundler/import.meta.url issues when shipped as an npm package.
3
+ export const SEARCH_WORKER_CODE = `
4
+ let MotelyWasm = null;
5
+ let MotelyWasmEvents = null;
6
+ let activeSearch = null;
7
+
8
+ self.addEventListener('message', async function(e) {
9
+ const msg = e.data;
10
+
11
+ if (msg.type === 'init') {
12
+ try {
13
+ const mod = await import(msg.url);
14
+ await mod.default.boot();
15
+ MotelyWasm = mod.MotelyWasm;
16
+ MotelyWasmEvents = mod.MotelyWasmEvents;
17
+ self.postMessage({ type: 'ready' });
18
+ } catch (err) {
19
+ self.postMessage({ type: 'error', message: String(err) });
20
+ }
21
+ return;
22
+ }
23
+
24
+ if (msg.type === 'start') {
25
+ if (!MotelyWasm) { self.postMessage({ type: 'error', message: 'Not initialized' }); return; }
26
+ const validation = MotelyWasm.validateJaml(msg.jaml);
27
+ if (validation !== 'valid') { self.postMessage({ type: 'error', message: validation }); return; }
28
+
29
+ let rId, pId, cId;
30
+ function cleanup() {
31
+ MotelyWasmEvents.onResult.unsubscribeById(rId);
32
+ MotelyWasmEvents.onProgress.unsubscribeById(pId);
33
+ MotelyWasmEvents.onComplete.unsubscribeById(cId);
34
+ activeSearch = null;
35
+ }
36
+
37
+ rId = MotelyWasmEvents.onResult.subscribe(function(seed, score) {
38
+ self.postMessage({ type: 'result', seed, score });
39
+ });
40
+ pId = MotelyWasmEvents.onProgress.subscribe(function(searched, matching) {
41
+ self.postMessage({ type: 'progress', searched: searched.toString(), matching: matching.toString() });
42
+ });
43
+ cId = MotelyWasmEvents.onComplete.subscribe(function(status, searched, matched) {
44
+ cleanup();
45
+ self.postMessage({ type: 'complete', status, searched: searched.toString(), matched: matched.toString() });
46
+ });
47
+
48
+ try {
49
+ activeSearch = MotelyWasm.startRandomSearch(msg.jaml, msg.count);
50
+ } catch (err) {
51
+ cleanup();
52
+ self.postMessage({ type: 'error', message: String(err) });
53
+ }
54
+ return;
55
+ }
56
+
57
+ if (msg.type === 'stop') {
58
+ if (activeSearch) { activeSearch.cancel(); activeSearch = null; }
59
+ self.postMessage({ type: 'cancelled' });
60
+ }
61
+ });
62
+ `;
@@ -0,0 +1,8 @@
1
+ import type { AnalyzerAnteView } from "../components/AnalyzerExplorer.js";
2
+ export type AnalyzerStatus = "idle" | "running" | "done" | "error";
3
+ export declare function useAnalyzer(motelyWasmUrl: string): {
4
+ antes: AnalyzerAnteView[];
5
+ status: AnalyzerStatus;
6
+ error: string | null;
7
+ analyze: (seed: string, deck: string, stake: string, jaml?: string) => Promise<void>;
8
+ };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { useState, useCallback } from "react";
3
+ import { loadMotelyWasm } from "./loadMotelyWasm.js";
4
+ import { extractVisualJamlItems } from "../utils/jamlMapPreview.js";
5
+ import { motelyItemDisplayNameFromValue } from "../motelyDisplay.js";
6
+ export function useAnalyzer(motelyWasmUrl) {
7
+ const [antes, setAntes] = useState([]);
8
+ const [status, setStatus] = useState("idle");
9
+ const [error, setError] = useState(null);
10
+ const analyze = useCallback(async (seed, deck, stake, jaml) => {
11
+ setAntes([]);
12
+ setStatus("running");
13
+ setError(null);
14
+ try {
15
+ const { MotelyWasm, Motely } = await loadMotelyWasm(motelyWasmUrl);
16
+ const deckEnum = Motely.MotelyDeck[deck] ?? Motely.MotelyDeck.Red;
17
+ const stakeEnum = Motely.MotelyStake[stake] ?? Motely.MotelyStake.White;
18
+ const desiredNames = new Set();
19
+ if (jaml) {
20
+ const groups = extractVisualJamlItems(jaml);
21
+ for (const item of [...groups.must, ...groups.should]) {
22
+ desiredNames.add(item.value.toLowerCase());
23
+ }
24
+ }
25
+ const ctx = MotelyWasm.createSearchContext(seed, deckEnum, stakeEnum);
26
+ const bossStream = ctx.createBossStream();
27
+ let runState = { voucherBitfield: 0, bossBitfield: 0 };
28
+ const results = [];
29
+ for (let ante = 1; ante <= 8; ante++) {
30
+ const bossResult = ctx.getNextBossForAnte(bossStream, ante, runState);
31
+ const bossName = Motely.MotelyBossBlind[bossResult.boss] ?? `Unknown(${bossResult.boss})`;
32
+ runState = bossResult.runState;
33
+ const voucherResult = ctx.getAnteFirstVoucher(ante, runState);
34
+ const voucherName = Motely.MotelyVoucher[voucherResult.voucher] ?? `Unknown(${voucherResult.voucher})`;
35
+ runState = voucherResult.runState;
36
+ const tagStream = ctx.createTagStream(ante);
37
+ const tag1 = ctx.getNextTag(tagStream);
38
+ const tag2 = ctx.getNextTag(tagStream);
39
+ const packStream = ctx.createBoosterPackStream(ante);
40
+ const packs = [];
41
+ for (let p = 0; p < 2; p++) {
42
+ const packResult = ctx.getNextBoosterPack(packStream);
43
+ packs.push(Motely.MotelyBoosterPack[packResult.pack] ?? `Unknown(${packResult.pack})`);
44
+ }
45
+ const shopStream = ctx.createShopItemStream(ante, runState, Motely.MotelyShopStreamFlags.Default, Motely.MotelyJokerStreamFlags.Default);
46
+ const shop = [];
47
+ for (let i = 0; i < 4; i++) {
48
+ const itemResult = ctx.getNextShopItem(shopStream);
49
+ const name = motelyItemDisplayNameFromValue(itemResult.item.value);
50
+ const desired = desiredNames.size > 0 && desiredNames.has(name.toLowerCase());
51
+ shop.push({ id: `${ante}-shop-${i}`, name, value: itemResult.item.value, desired });
52
+ }
53
+ results.push({
54
+ ante,
55
+ boss: bossName,
56
+ voucher: voucherName,
57
+ smallBlindTag: Motely.MotelyTag[tag1.tag] ?? `Unknown(${tag1.tag})`,
58
+ bigBlindTag: Motely.MotelyTag[tag2.tag] ?? `Unknown(${tag2.tag})`,
59
+ packs,
60
+ shop,
61
+ });
62
+ }
63
+ setAntes(results);
64
+ setStatus("done");
65
+ }
66
+ catch (e) {
67
+ setError(e instanceof Error ? e.message : String(e));
68
+ setStatus("error");
69
+ }
70
+ }, [motelyWasmUrl]);
71
+ return { antes, status, error, analyze };
72
+ }
@@ -0,0 +1,21 @@
1
+ export interface SearchResult {
2
+ seed: string;
3
+ score: number;
4
+ }
5
+ export type SearchStatus = "idle" | "booting" | "running" | "completed" | "cancelled" | "error";
6
+ export interface UseSearchState {
7
+ results: SearchResult[];
8
+ totalSearched: bigint;
9
+ matchingSeeds: bigint;
10
+ status: SearchStatus;
11
+ error: string | null;
12
+ }
13
+ export declare function useSearch(motelyWasmUrl: string): {
14
+ start: (jaml: string, count: number) => void;
15
+ cancel: () => void;
16
+ results: SearchResult[];
17
+ totalSearched: bigint;
18
+ matchingSeeds: bigint;
19
+ status: SearchStatus;
20
+ error: string | null;
21
+ };
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { useState, useCallback, useRef, useEffect } from "react";
3
+ import { SEARCH_WORKER_CODE } from "./searchWorkerCode.js";
4
+ function createWorker(motelyWasmUrl) {
5
+ const blob = new Blob([SEARCH_WORKER_CODE], { type: "text/javascript" });
6
+ const blobUrl = URL.createObjectURL(blob);
7
+ const worker = new Worker(blobUrl);
8
+ worker.postMessage({ type: "init", url: motelyWasmUrl });
9
+ return worker;
10
+ }
11
+ export function useSearch(motelyWasmUrl) {
12
+ const [state, setState] = useState({
13
+ results: [],
14
+ totalSearched: 0n,
15
+ matchingSeeds: 0n,
16
+ status: "idle",
17
+ error: null,
18
+ });
19
+ const workerRef = useRef(null);
20
+ const readyRef = useRef(false);
21
+ useEffect(() => {
22
+ setState((s) => ({ ...s, status: "booting" }));
23
+ const worker = createWorker(motelyWasmUrl);
24
+ workerRef.current = worker;
25
+ readyRef.current = false;
26
+ worker.onmessage = (e) => {
27
+ const msg = e.data;
28
+ if (msg.type === "ready") {
29
+ readyRef.current = true;
30
+ setState((s) => s.status === "booting" ? { ...s, status: "idle" } : s);
31
+ }
32
+ else if (msg.type === "result") {
33
+ setState((s) => ({ ...s, results: [...s.results, { seed: msg.seed, score: msg.score }] }));
34
+ }
35
+ else if (msg.type === "progress") {
36
+ setState((s) => ({ ...s, totalSearched: BigInt(msg.searched), matchingSeeds: BigInt(msg.matching) }));
37
+ }
38
+ else if (msg.type === "complete") {
39
+ setState((s) => ({ ...s, status: msg.status === "Completed" ? "completed" : "error", error: msg.status !== "Completed" ? msg.status : null, totalSearched: BigInt(msg.searched), matchingSeeds: BigInt(msg.matched) }));
40
+ }
41
+ else if (msg.type === "cancelled") {
42
+ setState((s) => ({ ...s, status: "cancelled" }));
43
+ }
44
+ else if (msg.type === "error") {
45
+ setState((s) => ({ ...s, status: "error", error: msg.message }));
46
+ }
47
+ };
48
+ return () => {
49
+ worker.terminate();
50
+ workerRef.current = null;
51
+ };
52
+ }, [motelyWasmUrl]);
53
+ const start = useCallback((jaml, count) => {
54
+ const worker = workerRef.current;
55
+ if (!worker)
56
+ return;
57
+ setState({ results: [], totalSearched: 0n, matchingSeeds: 0n, status: "running", error: null });
58
+ if (readyRef.current) {
59
+ worker.postMessage({ type: "start", jaml, count });
60
+ }
61
+ else {
62
+ const orig = worker.onmessage;
63
+ worker.onmessage = (e) => {
64
+ orig?.call(worker, e);
65
+ if (e.data.type === "ready") {
66
+ worker.onmessage = orig;
67
+ worker.postMessage({ type: "start", jaml, count });
68
+ }
69
+ };
70
+ }
71
+ }, []);
72
+ const cancel = useCallback(() => {
73
+ workerRef.current?.postMessage({ type: "stop" });
74
+ }, []);
75
+ return { ...state, start, cancel };
76
+ }
@@ -1,36 +1,25 @@
1
1
  import React from 'react';
2
- import { type ButtonVariant } from './tokens.js';
3
2
  export interface JimboPanelProps extends React.HTMLAttributes<HTMLDivElement> {
4
3
  sway?: boolean;
5
4
  onBack?: () => void;
6
- backLabel?: string;
7
5
  hideBack?: boolean;
8
6
  }
9
- export declare const JimboPanel: React.MemoExoticComponent<({ children, className, sway, onBack, backLabel, hideBack, style, ...props }: JimboPanelProps) => import("react/jsx-runtime").JSX.Element>;
7
+ export declare const JimboPanel: React.MemoExoticComponent<({ children, className, sway, onBack, hideBack, style, ...props }: JimboPanelProps) => import("react/jsx-runtime").JSX.Element>;
10
8
  export interface JimboInnerPanelProps extends React.HTMLAttributes<HTMLDivElement> {
11
9
  }
12
10
  export declare const JimboInnerPanel: React.MemoExoticComponent<({ children, className, style, ...props }: JimboInnerPanelProps) => import("react/jsx-runtime").JSX.Element>;
13
- export interface JimboButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
14
- variant?: ButtonVariant;
11
+ export type JimboTone = 'orange' | 'red' | 'blue' | 'green' | 'gold' | 'grey';
12
+ export interface JimboButtonProps {
13
+ tone?: JimboTone;
15
14
  size?: 'xs' | 'sm' | 'md' | 'lg';
16
15
  fullWidth?: boolean;
17
- }
18
- export declare function JimboButton({ children, variant, size, fullWidth, className, style, disabled, ...props }: JimboButtonProps): import("react/jsx-runtime").JSX.Element;
19
- export declare function JimboBackButton({ label, ...props }: Omit<JimboButtonProps, 'variant' | 'children'> & {
20
- label?: string;
21
- }): import("react/jsx-runtime").JSX.Element;
22
- export type BalTone = 'orange' | 'red' | 'blue' | 'green' | 'gold' | 'grey';
23
- export interface BalButtonProps {
24
- tone?: BalTone;
25
- size?: 'sm' | 'md' | 'lg';
26
- fullWidth?: boolean;
27
16
  disabled?: boolean;
28
17
  onClick?: () => void;
29
18
  style?: React.CSSProperties;
30
19
  children?: React.ReactNode;
31
20
  }
32
- export declare function BalButton({ tone, size, fullWidth, disabled, onClick, style, children, }: BalButtonProps): import("react/jsx-runtime").JSX.Element;
33
- export declare function BalBackButton({ onClick }: {
21
+ export declare function JimboButton({ tone, size, fullWidth, disabled, onClick, style, children, }: JimboButtonProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function JimboBackButton({ onClick }: {
34
23
  onClick?: () => void;
35
24
  }): import("react/jsx-runtime").JSX.Element;
36
25
  export interface JimboModalProps {
package/dist/ui/panel.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useRef, memo } from 'react';
4
4
  import { JimboColorOption, JIMBO_ANIMATIONS } from './tokens.js';
5
- export const JimboPanel = memo(({ children, className = '', sway = false, onBack, backLabel = 'Back', hideBack = false, style, ...props }) => {
5
+ export const JimboPanel = memo(({ children, className = '', sway = false, onBack, hideBack = false, style, ...props }) => {
6
6
  const panelRef = useRef(null);
7
7
  useEffect(() => {
8
8
  if (!sway || !panelRef.current)
@@ -23,51 +23,16 @@ export const JimboPanel = memo(({ children, className = '', sway = false, onBack
23
23
  border: `3px solid ${JimboColorOption.BORDER_SILVER}`,
24
24
  boxShadow: `0 3px 0 0 ${JimboColorOption.BORDER_SOUTH}`,
25
25
  ...style,
26
- }, ...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, label: backLabel }) }))] }));
26
+ }, ...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 }) }))] }));
27
27
  });
28
28
  JimboPanel.displayName = 'JimboPanel';
29
29
  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 })));
30
30
  JimboInnerPanel.displayName = 'JimboInnerPanel';
31
- // ─── Button ──────────────────────────────────────────────────────────────────
32
- const VARIANT_COLORS = {
33
- primary: { bg: JimboColorOption.RED, hover: JimboColorOption.DARK_RED, text: '#fff' },
34
- secondary: { bg: JimboColorOption.BLUE, hover: JimboColorOption.DARK_BLUE, text: '#fff' },
35
- danger: { bg: JimboColorOption.RED, hover: JimboColorOption.DARK_RED, text: '#fff' },
36
- back: { bg: JimboColorOption.ORANGE, hover: JimboColorOption.DARK_ORANGE, text: '#fff' },
37
- ghost: { bg: 'transparent', hover: 'rgba(255,255,255,0.1)', text: '#fff' },
38
- };
39
- export function JimboButton({ children, variant = 'primary', size = 'md', fullWidth = false, className = '', style, disabled, ...props }) {
40
- const [hovered, setHovered] = useState(false);
41
- const [pressed, setPressed] = useState(false);
42
- const c = VARIANT_COLORS[variant];
43
- const pad = { xs: '0.2rem 0.5rem', sm: '0.25rem 0.75rem', md: '0.375rem 1rem', lg: '0.5rem 1.5rem' }[size];
44
- return (_jsx("button", { disabled: disabled, onMouseEnter: () => { if (!disabled)
45
- setHovered(true); }, onMouseLeave: () => { setHovered(false); setPressed(false); }, onMouseDown: () => { if (!disabled)
46
- setPressed(true); }, onMouseUp: () => setPressed(false), className: className, style: {
47
- fontFamily: 'm6x11plus, monospace',
48
- backgroundColor: hovered ? c.hover : c.bg,
49
- color: c.text,
50
- padding: pad,
51
- borderRadius: '0.5rem',
52
- border: 'none',
53
- cursor: disabled ? 'not-allowed' : 'pointer',
54
- width: fullWidth ? '100%' : undefined,
55
- opacity: disabled ? 0.5 : 1,
56
- transform: pressed ? 'translateY(3px)' : 'none',
57
- boxShadow: pressed ? 'none' : '0 3px 0 0 rgba(0,0,0,0.5)',
58
- textShadow: '1px 1px 0 rgba(0,0,0,0.8)',
59
- userSelect: 'none',
60
- ...style,
61
- }, ...props, children: children }));
62
- }
63
- export function JimboBackButton({ label = 'Back', ...props }) {
64
- return _jsx(JimboButton, { variant: "back", size: "sm", fullWidth: true, ...props, children: label });
65
- }
66
- // ─── BalButton ────────────────────────────────────────────────────────────────
67
- // Canonical Balatro-style flat 2D button.
31
+ // ─── JimboButton ──────────────────────────────────────────────────────────────
32
+ // Canonical flat 2D Balatro-style button.
68
33
  // Two-layer: separate shadow div (3px south + 1px east) that disappears on press.
69
34
  // Press translates the face onto the shadow. No gradients, no hover color change.
70
- const BAL_PAIRS = {
35
+ const JIMBO_TONE_PAIRS = {
71
36
  orange: [JimboColorOption.ORANGE, JimboColorOption.DARK_ORANGE],
72
37
  red: [JimboColorOption.RED, JimboColorOption.DARK_RED],
73
38
  blue: [JimboColorOption.BLUE, JimboColorOption.DARK_BLUE],
@@ -75,11 +40,11 @@ const BAL_PAIRS = {
75
40
  gold: [JimboColorOption.GOLD, '#8a6a1e'],
76
41
  grey: [JimboColorOption.DARK_GREY, JimboColorOption.DARKEST],
77
42
  };
78
- export function BalButton({ tone = 'orange', size = 'md', fullWidth = false, disabled = false, onClick, style, children, }) {
43
+ export function JimboButton({ tone = 'orange', size = 'md', fullWidth = false, disabled = false, onClick, style, children, }) {
79
44
  const [pressed, setPressed] = useState(false);
80
- const [fg, sh] = BAL_PAIRS[tone] ?? BAL_PAIRS.orange;
81
- const pad = size === 'sm' ? '4px 10px' : size === 'lg' ? '14px 18px' : '9px 14px';
82
- const fs = size === 'sm' ? 12 : size === 'lg' ? 18 : 14;
45
+ const [fg, sh] = JIMBO_TONE_PAIRS[tone] ?? JIMBO_TONE_PAIRS.orange;
46
+ const pad = size === 'xs' ? '2px 8px' : size === 'sm' ? '4px 10px' : size === 'lg' ? '14px 18px' : '9px 14px';
47
+ const fs = size === 'xs' ? 10 : size === 'sm' ? 12 : size === 'lg' ? 18 : 14;
83
48
  return (_jsxs("div", { onMouseDown: () => { if (!disabled)
84
49
  setPressed(true); }, onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onTouchStart: () => { if (!disabled)
85
50
  setPressed(true); }, onTouchEnd: () => setPressed(false), onClick: () => { if (!disabled)
@@ -93,8 +58,8 @@ export function BalButton({ tone = 'orange', size = 'md', fullWidth = false, dis
93
58
  textTransform: 'uppercase', lineHeight: 1.1,
94
59
  }, children: children })] }));
95
60
  }
96
- export function BalBackButton({ onClick }) {
97
- return (_jsx("div", { style: { display: 'flex', justifyContent: 'center', width: '100%', padding: '8px 10px 10px' }, children: _jsx(BalButton, { tone: "orange", size: "md", onClick: onClick, style: { width: '66.666%' }, children: "Back" }) }));
61
+ export function JimboBackButton({ onClick }) {
62
+ return (_jsx("div", { style: { display: 'flex', justifyContent: 'center', width: '100%', padding: '8px 10px 10px' }, children: _jsx(JimboButton, { tone: "orange", size: "md", onClick: onClick, style: { width: '66.666%' }, children: "Back" }) }));
98
63
  }
99
64
  export function JimboModal({ children, open, onClose, title, className }) {
100
65
  const [visible, setVisible] = useState(open);
@@ -112,5 +77,5 @@ export function JimboModal({ children, open, onClose, title, className }) {
112
77
  }, [open]);
113
78
  if (!visible)
114
79
  return null;
115
- 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, backLabel: "Close", className: 'w-full flex flex-col max-h-[90vh] ' + (className ?? 'max-w-lg'), onClick: (e) => e.stopPropagation(), children: [title && _jsx("h2", { style: { fontFamily: 'm6x11plus, monospace', color: '#fff', textAlign: 'center', margin: '0 0 1rem', fontSize: '1.25rem' }, children: title }), children] }) }));
80
+ 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("h2", { style: { fontFamily: 'm6x11plus, monospace', color: '#fff', textAlign: 'center', margin: '0 0 1rem', fontSize: '1.25rem' }, children: title }), children] }) }));
116
81
  }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { JimboColorOption } from './tokens.js';
4
- import { BalButton } from './panel.js';
5
- import { BalSprite } from './sprites.js';
4
+ import { JimboButton } from './panel.js';
5
+ import { JimboSprite } from './sprites.js';
6
6
  const TONE_COLOR = {
7
7
  blue: JimboColorOption.BLUE,
8
8
  red: JimboColorOption.RED,
@@ -34,7 +34,7 @@ export function Showcase({ hotFilters = [], recentFinds = [], stats = DEFAULT_ST
34
34
  background: C.DARK_GREY, borderRadius: 6, padding: 10,
35
35
  border: `2px solid ${tColor}`, boxShadow: `0 2px 0 ${C.BLACK}`,
36
36
  display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
37
- }, children: [_jsx("div", { style: { display: 'flex', gap: 2 }, children: f.sample.map((name, j) => (_jsx("div", { style: { width: 30, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(BalSprite, { name: name, width: 28 }) }, j))) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: {
37
+ }, children: [_jsx("div", { style: { display: 'flex', gap: 2 }, children: f.sample.map((name, j) => (_jsx("div", { style: { width: 30, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(JimboSprite, { name: name, width: 28 }) }, j))) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: {
38
38
  fontSize: 13, color: C.WHITE, letterSpacing: 1,
39
39
  textShadow: '1px 1px 0 rgba(0,0,0,.8)',
40
40
  overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
@@ -50,5 +50,5 @@ export function Showcase({ hotFilters = [], recentFinds = [], stats = DEFAULT_ST
50
50
  }, children: recentFinds.length === 0 ? (_jsx("div", { style: { color: C.GREY }, children: "No recent finds yet." })) : recentFinds.map((r, i) => (_jsxs("div", { children: [_jsx("span", { style: { color: C.GOLD_TEXT }, children: r.seed }), ' · ', r.filterName, r.score > 0 && _jsxs("span", { style: { color: C.GREEN_TEXT }, children: [" +", r.score] })] }, i))) }), _jsx("div", { style: { height: 16 } })] }), _jsxs("div", { style: {
51
51
  padding: '8px 10px 10px', borderTop: `2px solid ${C.BLACK}`, background: C.DARK_GREY,
52
52
  display: 'flex', flexDirection: 'column', gap: 6,
53
- }, children: [_jsx(BalButton, { tone: "green", fullWidth: true, size: "md", onClick: onNewSearch, children: "New Search" }), _jsx(BalButton, { tone: "blue", fullWidth: true, size: "md", onClick: onBrowseFilters, children: "Browse Filters" }), _jsx(BalButton, { tone: "orange", fullWidth: true, size: "md", onClick: onBack, children: "Back" })] })] }));
53
+ }, children: [_jsx(JimboButton, { tone: "green", fullWidth: true, size: "md", onClick: onNewSearch, children: "New Search" }), _jsx(JimboButton, { tone: "blue", fullWidth: true, size: "md", onClick: onBrowseFilters, children: "Browse Filters" }), _jsx(JimboButton, { tone: "orange", fullWidth: true, size: "md", onClick: onBack, children: "Back" })] })] }));
54
54
  }
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import { type SpriteSheetType } from '../sprites/spriteMapper.js';
3
- export interface BalSpriteProps {
3
+ export interface JimboSpriteProps {
4
4
  name: string;
5
5
  sheet?: SpriteSheetType;
6
6
  width?: number;
7
7
  height?: number;
8
8
  style?: React.CSSProperties;
9
9
  }
10
- export declare function BalSprite({ name, sheet, width, height, style }: BalSpriteProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export declare function JimboSprite({ name, sheet, width, height, style }: JimboSpriteProps): import("react/jsx-runtime").JSX.Element | null;
@@ -12,7 +12,7 @@ const SHEET_META = {
12
12
  Enhancers: { cols: 7, rows: 5, assetKey: 'enhancers' },
13
13
  Editions: { cols: 5, rows: 1, assetKey: 'editions' },
14
14
  };
15
- export function BalSprite({ name, sheet, width = 40, height, style }) {
15
+ export function JimboSprite({ name, sheet, width = 40, height, style }) {
16
16
  const sprite = getSpriteData(name);
17
17
  const resolvedSheet = sheet ?? sprite?.type ?? 'Jokers';
18
18
  const meta = SHEET_META[resolvedSheet];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaml-ui",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Balatro rendering components, sprite metadata, and optional Motely helpers for React apps.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,13 +26,11 @@
26
26
  "types": "./dist/r3f.d.ts",
27
27
  "import": "./dist/r3f.js"
28
28
  },
29
- "./assets/*": "./assets/*",
30
29
  "./package.json": "./package.json"
31
30
  },
32
31
  "sideEffects": false,
33
32
  "files": [
34
33
  "dist",
35
- "assets",
36
34
  "README.md",
37
35
  "LICENSE"
38
36
  ],
Binary file
Binary file
Binary file
Binary file
Binary file
package/assets/Jokers.png DELETED
Binary file
package/assets/Tarots.png DELETED
Binary file
Binary file
Binary file
Binary file
package/assets/tags.png DELETED
Binary file