jaml-ui 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/DESIGN.md +7 -3
  2. package/dist/components/JamlAnalyzerFullscreen.d.ts +4 -1
  3. package/dist/components/JamlAnalyzerFullscreen.js +2 -2
  4. package/dist/components/JamlCurator.d.ts +4 -0
  5. package/dist/components/JamlCurator.js +63 -0
  6. package/dist/components/JamlCurator.stories.d.ts +6 -0
  7. package/dist/components/JamlCurator.stories.js +14 -0
  8. package/dist/components/JamlIde.js +1 -1
  9. package/dist/components/JamlIdeVisual.js +12 -20
  10. package/dist/components/jamlMap/CategoryPicker.js +38 -120
  11. package/dist/components/jamlMap/JamlMapEditor.d.ts +11 -0
  12. package/dist/components/jamlMap/JamlMapEditor.js +170 -0
  13. package/dist/components/jamlMap/JamlMapEditor.stories.d.ts +7 -0
  14. package/dist/components/jamlMap/JamlMapEditor.stories.js +26 -0
  15. package/dist/components/jamlMap/JokerPicker.js +28 -157
  16. package/dist/components/jamlMap/MysterySlot.js +32 -5
  17. package/dist/components/jamlMap/MysterySlot.stories.d.ts +7 -0
  18. package/dist/components/jamlMap/MysterySlot.stories.js +31 -0
  19. package/dist/components/jamlMap/index.d.ts +1 -1
  20. package/dist/components/jamlMap/index.js +1 -1
  21. package/dist/hooks/useAnalyzer.d.ts +4 -8
  22. package/dist/hooks/useAnalyzer.js +3 -6
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +1 -1
  25. package/dist/stories/Button.d.ts +15 -0
  26. package/dist/stories/Button.js +7 -0
  27. package/dist/stories/Button.stories.d.ts +24 -0
  28. package/dist/stories/Button.stories.js +50 -0
  29. package/dist/stories/Header.d.ts +12 -0
  30. package/dist/stories/Header.js +4 -0
  31. package/dist/stories/Header.stories.d.ts +18 -0
  32. package/dist/stories/Header.stories.js +26 -0
  33. package/dist/stories/Page.d.ts +3 -0
  34. package/dist/stories/Page.js +8 -0
  35. package/dist/stories/Page.stories.d.ts +12 -0
  36. package/dist/stories/Page.stories.js +24 -0
  37. package/dist/ui/Jimbo.stories.d.ts +7 -0
  38. package/dist/ui/Jimbo.stories.js +28 -0
  39. package/dist/ui/jimbo.css +20 -11
  40. package/dist/ui/jimboText.d.ts +1 -1
  41. package/dist/ui/panel.d.ts +1 -1
  42. package/dist/ui/panel.js +7 -5
  43. package/package.json +16 -3
package/DESIGN.md CHANGED
@@ -147,9 +147,11 @@ Must-clause items glow blue. Should-clause items glow gold/green. Non-matching i
147
147
 
148
148
  ## Typography
149
149
 
150
- m6x11plus (m6x11plusplus.otf) is the ONLY font. It is a single-weight pixel font. NEVER apply font-weight bold, semibold, or any weight other than 400. Bold makes it look muddy. Use size and letter-spacing for hierarchy instead.
150
+ m6x11plus (m6x11plusplus.otf) is the ONLY font. It is a single-weight pixel font. NEVER apply font-weight bold, semibold, or any weight other than 400. Bold makes it look muddy. NEVER USE HEAVY!
151
151
 
152
- All text is uppercase with generous letter-spacing (0.04em-0.1em) for labels and micro text. Seed codes use the display size (26px) in gold (#e4b643) with 0.04em tracking.
152
+ Text is NEVER ALL CAPS. Use proper Title Case or Sentence case. Seed codes use the display size (26px) in gold (#e4b643).
153
+
154
+ Contrast is critical. NEVER make grey text on top of a grey background. If using a dark grey background, use white or light contrasting text.
153
155
 
154
156
  ## Layout
155
157
 
@@ -188,7 +190,9 @@ JAML-hit items get a GlowRing: `box-shadow: 0 0 0 2px [color], 0 0 10px [color]`
188
190
  - DO use m6x11plus for everything except code/monospace.
189
191
  - DO design for 375px portrait.
190
192
  - DO use translateY + box-shadow for button depth. Not CSS 3D transforms.
191
- - DON'T use font-weight bold. m6x11plus is single-weight. Bold = muddy.
193
+ - DON'T use font-weight bold or heavy. m6x11plus is single-weight.
194
+ - DON'T use ALL CAPS. It is considered an embellishment and ruins the aesthetic.
195
+ - DON'T put grey text on top of a grey background.
192
196
  - DON'T use fat padding or margins. Balatro UI is dense and cozy.
193
197
  - DON'T add horizontal scroll. Vertical snap-scroll + horizontal swipe only.
194
198
  - DON'T use rounded corners larger than 10px. Balatro is chunky, not bubbly.
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import type { AnalyzerAnteView, AnalyzerItem } from "./AnalyzerExplorer.js";
2
3
  import type { AnalyzerLive } from "../hooks/useAnalyzer.js";
3
4
  import { type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
@@ -21,7 +22,9 @@ export interface JamlAnalyzerFullscreenProps {
21
22
  /** Pull size on each lazy load. */
22
23
  chunkSize?: number;
23
24
  className?: string;
25
+ /** Custom top page to render as Slide 0 */
26
+ topPage?: React.ReactNode;
24
27
  }
25
- export declare function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker, chunkSize, className, topPage, }: JamlAnalyzerFullscreenProps): import("react/jsx-runtime").JSX.Element;
26
29
  export type { AnalyzerItem };
27
30
  export { ANALYZER_STREAM_META, type AnalyzerStreamKey } from "../hooks/analyzerStreamRegistry.js";
@@ -17,7 +17,7 @@ const TONE_COLORS = {
17
17
  default: C.GOLD_TEXT,
18
18
  };
19
19
  import { JamlMapPreview } from "./JamlMapPreview.js";
20
- export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", }) {
20
+ export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyLabels, enabledStreams, onEnabledStreamsChange, hidePicker = false, chunkSize = 12, className = "", topPage, }) {
21
21
  const [internalEnabled, setInternalEnabled] = useState(enabledStreams ?? DEFAULT_ENABLED_STREAMS);
22
22
  const effectiveEnabled = enabledStreams ?? internalEnabled;
23
23
  const setEnabled = useCallback((next) => {
@@ -26,7 +26,7 @@ export function JamlAnalyzerFullscreen({ antes, live, jaml, tallyColumns, tallyL
26
26
  }, [onEnabledStreamsChange]);
27
27
  const { currentAnte, scrollRef, scrollToAnte, registerAnteRef } = useAnteTracker(antes);
28
28
  const [pickerOpen, setPickerOpen] = useState(false);
29
- return (_jsxs("div", { className: className, style: styles.root, children: [_jsxs("div", { ref: scrollRef, style: styles.scroller, children: [jaml && (_jsxs("section", { style: { ...styles.section, scrollSnapAlign: "start", justifyContent: 'center' }, children: [_jsxs("div", { style: { marginBottom: 20 }, children: [_jsx("div", { style: styles.anteLabel, children: "JAML" }), _jsx("div", { style: styles.anteNumber, children: "MAP" })] }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns, tallyLabels: tallyLabels }), _jsx("div", { style: { marginTop: 24, textAlign: 'center', opacity: 0.6 }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Scroll down to explore seed details" }) })] })), antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => registerAnteRef(ante.ante, el) }, ante.ante)))] }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
29
+ return (_jsxs("div", { className: className, style: styles.root, children: [_jsxs("div", { ref: scrollRef, style: styles.scroller, children: [topPage ? topPage : jaml && (_jsxs("section", { style: { ...styles.section, scrollSnapAlign: "start", justifyContent: 'center' }, children: [_jsxs("div", { style: { marginBottom: 20 }, children: [_jsx("div", { style: styles.anteLabel, children: "JAML" }), _jsx("div", { style: styles.anteNumber, children: "MAP" })] }), _jsx(JamlMapPreview, { jaml: jaml, tallyColumns: tallyColumns, tallyLabels: tallyLabels }), _jsx("div", { style: { marginTop: 24, textAlign: 'center', opacity: 0.6 }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: "Scroll down to explore seed details" }) })] })), antes.map((ante) => (_jsx(AnteSection, { ante: ante, live: live, enabledStreams: effectiveEnabled, chunkSize: chunkSize, registerRef: (el) => registerAnteRef(ante.ante, el) }, ante.ante)))] }), _jsx(SideRail, { antes: antes.map((a) => a.ante), currentAnte: currentAnte, onJump: scrollToAnte }), !hidePicker && (_jsx(StreamPicker, { enabled: effectiveEnabled, onChange: setEnabled, open: pickerOpen, onToggle: () => setPickerOpen((v) => !v) }))] }));
30
30
  }
31
31
  function AnteSection({ ante, live, enabledStreams, chunkSize, registerRef }) {
32
32
  return (_jsxs("section", { ref: registerRef, "data-ante": ante.ante, style: styles.section, children: [_jsxs("header", { style: styles.header, children: [_jsxs("div", { children: [_jsx("div", { style: styles.anteLabel, children: "Ante" }), _jsx("div", { style: styles.anteNumber, children: ante.ante })] }), ante.voucher && (_jsxs("div", { style: styles.voucherBlock, children: [_jsx(JamlVoucher, { voucherName: ante.voucher, scale: 0.85 }), _jsx("div", { style: styles.voucherCaption, children: ante.voucher })] }))] }), _jsxs("div", { style: styles.blindRow, children: [_jsx(BlindCell, { label: "Small", tag: ante.smallBlindTag }), _jsx(BlindCell, { label: "Big", tag: ante.bigBlindTag }), ante.boss && (_jsxs("div", { style: styles.bossCell, children: [_jsx("div", { style: styles.cellLabel, children: "Boss" }), _jsx(JamlBoss, { bossName: ante.boss, scale: 0.7 }), _jsx("div", { style: styles.cellCaption, children: ante.boss })] }))] }), ante.packs && ante.packs.length > 0 && (_jsxs("div", { style: styles.streamLane, children: [_jsx("div", { style: styles.streamLabel, children: "Packs" }), _jsx("div", { style: styles.packRow, children: ante.packs.map((pack, i) => (_jsx("div", { style: styles.packPill, children: pack }, `${ante.ante}-pack-${i}`))) })] })), enabledStreams.map((key) => {
@@ -0,0 +1,4 @@
1
+ export interface JamlCuratorProps {
2
+ motelyWasmUrl: string;
3
+ }
4
+ export declare function JamlCurator({ motelyWasmUrl }: JamlCuratorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { JimboButton, JimboPanel } from "../ui/panel.js";
5
+ import { JimboText } from "../ui/jimboText.js";
6
+ import { JimboColorOption } from "../ui/tokens.js";
7
+ import { JimboFlankNav } from "../ui/jimboFlankNav.js";
8
+ import { JamlMapEditor } from "./jamlMap/JamlMapEditor.js";
9
+ import { JamlAnalyzerFullscreen } from "./JamlAnalyzerFullscreen.js";
10
+ import { useSearch } from "../hooks/useSearch.js";
11
+ import { useAnalyzer } from "../hooks/useAnalyzer.js";
12
+ import { JamlSpeedometer } from "./JamlSpeedometer.js";
13
+ const C = JimboColorOption;
14
+ export function JamlCurator({ motelyWasmUrl }) {
15
+ // Use map editor by default to generate JAML
16
+ const [jamlText, setJamlText] = useState("");
17
+ const search = useSearch(motelyWasmUrl);
18
+ const analyzer = useAnalyzer();
19
+ // Search results pagination
20
+ const [resultIndex, setResultIndex] = useState(0);
21
+ const isSearching = search.status === "running";
22
+ const handleSearch = () => {
23
+ if (isSearching) {
24
+ search.cancel();
25
+ }
26
+ else {
27
+ setResultIndex(0);
28
+ search.start(jamlText, 1_000_000);
29
+ }
30
+ };
31
+ const currentSeed = search.results[resultIndex]?.seed;
32
+ useEffect(() => {
33
+ if (currentSeed) {
34
+ analyzer.analyze(currentSeed, "Red", "White", jamlText);
35
+ }
36
+ }, [currentSeed, jamlText]); // eslint-disable-line react-hooks/exhaustive-deps
37
+ // Map Editor changes
38
+ const handleMapChange = (jamlString) => {
39
+ setJamlText(jamlString);
40
+ };
41
+ return (_jsx("div", { style: {
42
+ width: "100%",
43
+ maxWidth: 375,
44
+ height: "100svh",
45
+ margin: "0 auto",
46
+ position: "relative",
47
+ background: C.DARKEST,
48
+ overflow: "hidden",
49
+ borderLeft: `1px solid ${C.PANEL_EDGE}`,
50
+ borderRight: `1px solid ${C.PANEL_EDGE}`,
51
+ boxShadow: `0 0 20px rgba(0,0,0,0.5)`,
52
+ }, children: _jsx(JamlAnalyzerFullscreen, { antes: analyzer.antes, live: analyzer.live, hidePicker: true, topPage: _jsxs("section", { style: {
53
+ width: "100%",
54
+ height: "100svh",
55
+ scrollSnapAlign: "start",
56
+ display: "flex",
57
+ flexDirection: "column",
58
+ gap: 12,
59
+ padding: "16px 12px 24px",
60
+ boxSizing: "border-box",
61
+ borderBottom: `2px solid ${C.GOLD}`,
62
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(JimboText, { size: "lg", tone: "gold", children: "JAML Curator" }), _jsx(JimboButton, { tone: isSearching ? "red" : "green", size: "sm", onClick: handleSearch, children: isSearching ? "STOP" : "SEARCH" })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto' }, className: "hide-scrollbar", children: _jsx(JamlMapEditor, { onChange: handleMapChange }) }), _jsx("div", { style: { flexShrink: 0 }, children: _jsx(JamlSpeedometer, { status: search.status, seedsPerSecond: search.seedsPerSecond, totalSearched: search.totalSearched, matchingSeeds: search.matchingSeeds }) }), _jsx("div", { style: { flexShrink: 0 }, children: _jsx(JimboPanel, { children: search.results.length === 0 ? (_jsx(JimboText, { size: "sm", tone: "grey", className: "j-text-center", children: isSearching ? "Searching..." : "No results yet." })) : (_jsxs("div", { className: "j-flex-col j-gap-sm", children: [_jsxs("div", { className: "j-flex j-items-center j-justify-between", children: [_jsx(JimboText, { size: "xs", tone: "grey", children: "SEED MATCHES" }), _jsxs(JimboText, { size: "xs", tone: "gold", children: [search.matchingSeeds, " FOUND"] })] }), _jsx(JimboFlankNav, { canPrev: resultIndex > 0, canNext: resultIndex < search.results.length - 1, onPrev: () => setResultIndex(i => Math.max(0, i - 1)), onNext: () => setResultIndex(i => Math.min(search.results.length - 1, i + 1)), children: _jsxs("div", { className: "j-flex-col j-items-center j-gap-xs", children: [_jsx(JimboText, { size: "lg", tone: "gold", style: { letterSpacing: 2 }, children: currentSeed }), _jsx(JimboButton, { tone: "grey", size: "xs", children: "Copy Seed" })] }) }), _jsx(JimboText, { size: "micro", tone: "grey", className: "j-text-center", style: { opacity: 0.7, marginTop: 8 }, children: "\u25BC SWIPE DOWN FOR ANTES \u25BC" })] })) }) })] }) }) }));
63
+ }
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { JamlCurator } from "./JamlCurator.js";
3
+ declare const meta: Meta<typeof JamlCurator>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof JamlCurator>;
6
+ export declare const Default: Story;
@@ -0,0 +1,14 @@
1
+ import { JamlCurator } from "./JamlCurator.js";
2
+ const meta = {
3
+ title: "JAML/JamlCurator",
4
+ component: JamlCurator,
5
+ parameters: {
6
+ layout: "fullscreen",
7
+ },
8
+ };
9
+ export default meta;
10
+ export const Default = {
11
+ args: {
12
+ motelyWasmUrl: "https://unpkg.com/@nims11/motely@0.2.2/motely_bg.wasm",
13
+ },
14
+ };
@@ -69,7 +69,7 @@ function ResultsView({ results, jaml }) {
69
69
  display: "flex",
70
70
  flexDirection: "column",
71
71
  gap: 8,
72
- }, children: [_jsx(JamlMapPreview, { jaml: jaml, tallyColumns: result.tallyColumns, tallyLabels: result.tallyLabels }), _jsxs("div", { style: { padding: "4px 8px 8px", display: "flex", flexDirection: "column", gap: 5 }, children: [_jsx("span", { style: { fontSize: 8, color: JimboColorOption.GREY, letterSpacing: "0.08em", textTransform: "uppercase" }, children: "RAW TALLY DATA" }), (result.tallyLabels ?? []).map((label, i) => {
72
+ }, children: [_jsx(JamlMapPreview, { jaml: jaml, tallyColumns: result.tallyColumns, tallyLabels: result.tallyLabels }), _jsxs("div", { style: { padding: "4px 8px 8px", display: "flex", flexDirection: "column", gap: 5 }, children: [_jsx("span", { style: { fontSize: 9, color: JimboColorOption.WHITE, opacity: 0.8 }, children: "Raw Tally Data" }), (result.tallyLabels ?? []).map((label, i) => {
73
73
  const val = result.tallyColumns[i] ?? 0;
74
74
  if (val === 0)
75
75
  return null;
@@ -6,9 +6,9 @@ import { JimboColorOption } from "../ui/tokens.js";
6
6
  import { JimboSprite } from "../ui/sprites.js";
7
7
  const C = JimboColorOption;
8
8
  const ZONE_META = {
9
- must: { label: "MUST", hint: "Seed must contain all of these.", color: C.BLUE, accent: "#4db5ff" },
10
- should: { label: "SHOULD", hint: "Bonus points per match.", color: C.RED, accent: "#ff8076" },
11
- mustnot: { label: "MUST NOT", hint: "Seed is rejected if any appear.", color: C.ORANGE, accent: "#ffb84d" },
9
+ must: { label: "Must", hint: "Seed must contain all of these.", color: C.BLUE, accent: "#4db5ff" },
10
+ should: { label: "Should", hint: "Bonus points per match.", color: C.RED, accent: "#ff8076" },
11
+ mustnot: { label: "Must Not", hint: "Rejected if any appear.", color: C.ORANGE, accent: "#ffb84d" },
12
12
  };
13
13
  function clauseSpriteSheet(type) {
14
14
  if (type === "joker" ||
@@ -127,8 +127,7 @@ function MysteryAddTile({ zone, onTap }) {
127
127
  fontFamily: "m6x11plus, ui-monospace, monospace",
128
128
  fontSize: 12,
129
129
  color: z.accent,
130
- letterSpacing: 2,
131
- }, children: ["ADD TO ", z.label] })] }));
130
+ }, children: ["Add to ", z.label] })] }));
132
131
  }
133
132
  function ZoneRail({ zone, clauses, onAdd, onRemove, onEdit, onDragStart, highlight, }) {
134
133
  const z = ZONE_META[zone];
@@ -140,23 +139,16 @@ function ZoneRail({ zone, clauses, onAdd, onRemove, onEdit, onDragStart, highlig
140
139
  transition: "background 100ms, border-color 100ms",
141
140
  }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }, children: [_jsx("div", { style: {
142
141
  fontFamily: "m6x11plus, ui-monospace, monospace",
143
- fontSize: 11,
144
- padding: "2px 8px",
142
+ fontSize: 12,
143
+ padding: "2px 6px",
145
144
  background: z.color,
146
145
  color: C.WHITE,
147
146
  borderRadius: 3,
148
- letterSpacing: 2,
149
147
  boxShadow: `0 2px 0 ${C.BLACK}`,
150
148
  }, children: z.label }), _jsx("div", { style: { flex: 1, height: 2, background: `${z.color}55`, borderRadius: 1 } }), _jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 8, color: C.GREY }, children: clauses.length })] }), _jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 9, color: C.GREY, letterSpacing: 0.5, marginBottom: 8 }, children: z.hint }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [clauses.map((c) => (_jsx(ClauseCard, { clause: c, zone: zone, onRemove: () => onRemove(c.id), onEdit: () => onEdit(c), onDragStart: onDragStart }, c.id))), _jsx(MysteryAddTile, { zone: zone, onTap: onAdd })] })] }));
151
149
  }
152
150
  function TopMatter({ filter, onChange, }) {
153
- return (_jsxs("div", { style: {
154
- background: C.DARK_GREY,
155
- borderRadius: 6,
156
- padding: 10,
157
- border: `2px solid ${C.PANEL_EDGE}`,
158
- boxShadow: `0 2px 0 ${C.BLACK}`,
159
- }, children: [_jsx("input", { value: filter.name ?? "", placeholder: "Untitled", onChange: (e) => onChange({ ...filter, name: e.target.value }), style: {
151
+ return (_jsxs("div", { className: "j-inner-panel", style: { padding: 10 }, children: [_jsx("input", { value: filter.name ?? "", placeholder: "Untitled", onChange: (e) => onChange({ ...filter, name: e.target.value }), style: {
160
152
  display: "block",
161
153
  width: "100%",
162
154
  background: "transparent",
@@ -168,7 +160,7 @@ function TopMatter({ filter, onChange, }) {
168
160
  letterSpacing: 1,
169
161
  padding: 0,
170
162
  marginBottom: 4,
171
- } }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 9, color: C.GREY, letterSpacing: 2 }, children: "BY" }), _jsx("input", { value: filter.author ?? "", placeholder: "anonymous", onChange: (e) => onChange({ ...filter, author: e.target.value }), style: {
163
+ } }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { style: { fontFamily: "m6x11plus, ui-monospace, monospace", fontSize: 10, color: C.WHITE }, children: "By" }), _jsx("input", { value: filter.author ?? "", placeholder: "anonymous", onChange: (e) => onChange({ ...filter, author: e.target.value }), style: {
172
164
  flex: 1,
173
165
  background: "transparent",
174
166
  border: "none",
@@ -176,7 +168,6 @@ function TopMatter({ filter, onChange, }) {
176
168
  fontFamily: "m6x11plus, ui-monospace, monospace",
177
169
  fontSize: 12,
178
170
  color: C.GOLD_TEXT,
179
- letterSpacing: 1,
180
171
  padding: 0,
181
172
  } })] }), _jsx("input", { value: filter.description ?? "", placeholder: "description", onChange: (e) => onChange({ ...filter, description: e.target.value }), style: {
182
173
  display: "block",
@@ -186,8 +177,9 @@ function TopMatter({ filter, onChange, }) {
186
177
  border: "none",
187
178
  outline: "none",
188
179
  fontFamily: "m6x11plus, ui-monospace, monospace",
189
- fontSize: 10,
190
- color: C.GREY,
180
+ fontSize: 11,
181
+ color: C.WHITE,
182
+ opacity: 0.8,
191
183
  lineHeight: 1.35,
192
184
  padding: 0,
193
185
  } })] }));
@@ -205,7 +197,7 @@ export function JamlIdeVisual({ filter, onChange, onEditClause, onAddClause }) {
205
197
  padding: 10,
206
198
  background: C.DARKEST,
207
199
  color: C.WHITE,
208
- }, children: [_jsx(TopMatter, { filter: filter, onChange: onChange }), ["must", "should", "mustnot"].map((zone) => (_jsx(ZoneRail, { zone: zone, clauses: filter[zone], onAdd: onAddClause ? () => onAddClause(zone) : undefined, onRemove: (id) => removeClause(zone, id), onEdit: (c) => onEditClause?.(zone, c), onDragStart: onDragStart, highlight: hoverZone === zone }, zone))), drag && (_jsx("div", { style: {
200
+ }, children: [_jsx(TopMatter, { filter: filter, onChange: onChange }), _jsxs("div", { style: { display: "flex", gap: 10 }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(ZoneRail, { zone: "must", clauses: filter.must, onAdd: onAddClause ? () => onAddClause("must") : undefined, onRemove: (id) => removeClause("must", id), onEdit: (c) => onEditClause?.("must", c), onDragStart: onDragStart, highlight: hoverZone === "must" }) }), _jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(ZoneRail, { zone: "mustnot", clauses: filter.mustnot, onAdd: onAddClause ? () => onAddClause("mustnot") : undefined, onRemove: (id) => removeClause("mustnot", id), onEdit: (c) => onEditClause?.("mustnot", c), onDragStart: onDragStart, highlight: hoverZone === "mustnot" }) })] }), _jsx(ZoneRail, { zone: "should", clauses: filter.should, onAdd: onAddClause ? () => onAddClause("should") : undefined, onRemove: (id) => removeClause("should", id), onEdit: (c) => onEditClause?.("should", c), onDragStart: onDragStart, highlight: hoverZone === "should" }), drag && (_jsx("div", { style: {
209
201
  position: "fixed",
210
202
  left: drag.x - drag.offX,
211
203
  top: drag.y - drag.offY,
@@ -2,8 +2,11 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useMemo } from "react";
4
4
  import { JimboSprite } from "../../ui/sprites.js";
5
- import { JimboColorOption, withAlpha } from "../../ui/tokens.js";
5
+ import { JimboColorOption } from "../../ui/tokens.js";
6
+ import { JimboButton } from "../../ui/panel.js";
7
+ import { JimboText } from "../../ui/jimboText.js";
6
8
  // ─── Component ───────────────────────────────────────────────────────────────
9
+ const C = JimboColorOption;
7
10
  export function CategoryPicker({ config, onSelect, onCancel }) {
8
11
  const [search, setSearch] = useState("");
9
12
  const filtered = useMemo(() => {
@@ -12,6 +15,19 @@ export function CategoryPicker({ config, onSelect, onCancel }) {
12
15
  const q = search.toLowerCase();
13
16
  return config.items.filter((item) => item.name.toLowerCase().includes(q));
14
17
  }, [config.items, search]);
18
+ const pairedVouchers = useMemo(() => {
19
+ if (config.category !== "voucher")
20
+ return null;
21
+ const bases = config.items.filter((item) => item.pos.y % 2 === 0);
22
+ const pairs = bases.map((base) => {
23
+ const upgrade = config.items.find((u) => u.pos.x === base.pos.x && u.pos.y === base.pos.y + 1);
24
+ return { base, upgrade };
25
+ });
26
+ if (!search)
27
+ return pairs;
28
+ const q = search.toLowerCase();
29
+ return pairs.filter(p => p.base.name.toLowerCase().includes(q) || p.upgrade?.name.toLowerCase().includes(q));
30
+ }, [config.items, search, config.category]);
15
31
  const handleSelect = useCallback((item) => {
16
32
  onSelect({
17
33
  category: config.category,
@@ -26,8 +42,26 @@ export function CategoryPicker({ config, onSelect, onCancel }) {
26
42
  clauseKey: config.clauseKey,
27
43
  });
28
44
  }, [onSelect, config]);
29
- const C = JimboColorOption;
30
- return (_jsxs("div", { style: styles.container, children: [_jsxs("div", { style: { ...styles.header, borderBottomColor: withAlpha(config.accent, 0.3) }, children: [_jsx("button", { onClick: onCancel, style: styles.backBtn, children: "\u2715" }), _jsx("span", { style: { ...styles.title, color: config.accent }, children: config.title }), _jsx("div", { style: { width: 44 } })] }), _jsxs("div", { style: styles.searchRow, children: [_jsx("input", { type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: styles.searchInput }), _jsx("button", { onClick: handleAny, style: styles.anyBtn, children: "Any" })] }), config.hint && (_jsxs("div", { style: styles.hint, children: [_jsx("span", { children: "\uD83D\uDCA1" }), _jsx("span", { children: config.hint })] })), _jsxs("div", { style: styles.grid, children: [filtered.map((item) => (_jsxs("div", { onClick: () => handleSelect(item), title: item.name, style: styles.cell, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx("span", { style: styles.label, children: item.name })] }, item.name))), filtered.length === 0 && (_jsxs("div", { style: styles.emptyState, children: ["No matches for \"", search, "\""] }))] })] }));
45
+ const renderItem = (item, isMuted = false) => (_jsxs("div", { onClick: () => handleSelect(item), title: item.name, style: {
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ alignItems: "center",
49
+ gap: 3,
50
+ padding: 4,
51
+ borderRadius: 4,
52
+ cursor: "pointer",
53
+ opacity: isMuted ? 0.3 : 1,
54
+ }, children: [_jsx(JimboSprite, { name: item.name, sheet: config.sheet, width: 48 }), _jsx(JimboText, { size: "micro", tone: "grey", style: { maxWidth: 60, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: item.name })] }, item.name));
55
+ return (_jsxs("div", { style: { padding: 0, maxWidth: 420, maxHeight: "80vh", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { className: "j-flex j-gap-sm", style: { padding: "8px 10px 4px" }, children: [_jsx("input", { className: "j-seed-input__field", type: "text", placeholder: `Search ${config.title.toLowerCase()}...`, value: search, onChange: (e) => setSearch(e.target.value), style: { fontSize: 13, padding: "6px 10px", textTransform: "none", letterSpacing: "0.04em" } }), _jsx(JimboButton, { tone: "gold", size: "sm", onClick: handleAny, children: "Any" })] }), config.hint && (_jsx("div", { className: "j-inner-panel", style: { margin: "4px 10px 6px", padding: "6px 10px" }, children: _jsx(JimboText, { size: "xs", tone: "grey", children: config.hint }) })), _jsxs("div", { style: {
56
+ display: "flex",
57
+ flexWrap: "wrap",
58
+ gap: 6,
59
+ padding: "8px 10px 10px",
60
+ overflowY: "auto",
61
+ flex: 1,
62
+ alignContent: "flex-start",
63
+ }, children: [config.category === "voucher" && pairedVouchers ? (pairedVouchers.map((pair) => (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, width: 64 }, children: [renderItem(pair.base, search ? !pair.base.name.toLowerCase().includes(search.toLowerCase()) : false), pair.upgrade && renderItem(pair.upgrade, search ? !pair.upgrade.name.toLowerCase().includes(search.toLowerCase()) : false)] }, pair.base.name)))) : (filtered.map((item) => (_jsx("div", { style: { width: 64 }, children: renderItem(item) }, item.name)))), ((config.category === "voucher" && pairedVouchers?.length === 0) ||
64
+ (config.category !== "voucher" && filtered.length === 0)) && (_jsx("div", { style: { width: "100%", padding: 20, textAlign: "center" }, children: _jsxs(JimboText, { size: "sm", tone: "grey", children: ["No matches for \"", search, "\""] }) }))] })] }));
31
65
  }
32
66
  // ─── Pre-built configs ───────────────────────────────────────────────────────
33
67
  import { VOUCHERS, TAGS, BOSSES, BOOSTER_PACKS, TAROTS_AND_PLANETS } from "../../sprites/spriteData.js";
@@ -36,18 +70,16 @@ const TAROT_CARDS = TAROTS_AND_PLANETS.filter((c) => {
36
70
  const y = c.pos.y;
37
71
  return y <= 2 && c.name !== "The Soul" && c.name !== "Black Hole";
38
72
  }).filter((c) => {
39
- // Tarots are y=0..1 + Judgement (y=2,x=0) + The World (y=2,x=1)
40
73
  return c.pos.y <= 1 || (c.pos.y === 2 && c.pos.x <= 1);
41
74
  });
42
75
  const PLANET_CARDS = TAROTS_AND_PLANETS.filter((c) => {
43
- return c.pos.y === 3 || // Main planets row
76
+ return c.pos.y === 3 ||
44
77
  c.name === "Planet X" || c.name === "Ceres" || c.name === "Eris" ||
45
78
  c.name === "Black Hole";
46
79
  });
47
80
  const SPECTRAL_CARDS = TAROTS_AND_PLANETS.filter((c) => {
48
81
  return c.pos.y >= 4 || c.name === "The Soul";
49
82
  });
50
- const C = JimboColorOption;
51
83
  export const VOUCHER_PICKER_CONFIG = {
52
84
  title: "Vouchers",
53
85
  category: "voucher",
@@ -108,117 +140,3 @@ export const PACK_PICKER_CONFIG = {
108
140
  items: BOOSTER_PACKS,
109
141
  accent: C.ORANGE,
110
142
  };
111
- // ─── Styles ──────────────────────────────────────────────────────────────────
112
- const styles = {
113
- container: {
114
- background: JimboColorOption.DARKEST,
115
- border: `2px solid ${JimboColorOption.TEAL_GREY}`,
116
- borderRadius: 8,
117
- padding: 0,
118
- maxWidth: 420,
119
- maxHeight: "80vh",
120
- overflow: "hidden",
121
- display: "flex",
122
- flexDirection: "column",
123
- fontFamily: "m6x11plus, ui-monospace, monospace",
124
- boxShadow: `0 8px 32px ${withAlpha(JimboColorOption.BLACK, 0.6)}`,
125
- },
126
- header: {
127
- display: "flex",
128
- alignItems: "center",
129
- justifyContent: "space-between",
130
- padding: "10px 12px",
131
- borderBottom: `1px solid ${JimboColorOption.TEAL_GREY}`,
132
- background: withAlpha(JimboColorOption.DARK_GREY, 0.5),
133
- },
134
- backBtn: {
135
- background: "none",
136
- border: "none",
137
- color: JimboColorOption.GREY,
138
- fontFamily: "m6x11plus, ui-monospace, monospace",
139
- fontSize: 14,
140
- cursor: "pointer",
141
- padding: "4px 8px",
142
- },
143
- title: {
144
- fontSize: 16,
145
- fontWeight: "bold",
146
- letterSpacing: 0.5,
147
- },
148
- searchRow: {
149
- display: "flex",
150
- gap: 8,
151
- padding: "10px 12px 6px",
152
- },
153
- searchInput: {
154
- flex: 1,
155
- padding: "6px 10px",
156
- borderRadius: 4,
157
- border: `1px solid ${JimboColorOption.TEAL_GREY}`,
158
- background: withAlpha(JimboColorOption.DARK_GREY, 0.8),
159
- color: JimboColorOption.WHITE,
160
- fontSize: 13,
161
- fontFamily: "m6x11plus, ui-monospace, monospace",
162
- outline: "none",
163
- },
164
- anyBtn: {
165
- padding: "6px 14px",
166
- borderRadius: 4,
167
- border: `1px solid ${JimboColorOption.GOLD}`,
168
- background: withAlpha(JimboColorOption.GOLD, 0.15),
169
- color: JimboColorOption.GOLD,
170
- fontSize: 13,
171
- fontFamily: "m6x11plus, ui-monospace, monospace",
172
- cursor: "pointer",
173
- fontWeight: "bold",
174
- },
175
- hint: {
176
- display: "flex",
177
- alignItems: "flex-start",
178
- gap: 6,
179
- margin: "4px 12px 8px",
180
- padding: "8px 10px",
181
- borderRadius: 4,
182
- background: withAlpha(JimboColorOption.TEAL_GREY, 0.12),
183
- border: `1px solid ${withAlpha(JimboColorOption.TEAL_GREY, 0.3)}`,
184
- color: JimboColorOption.GREY,
185
- fontSize: 11,
186
- lineHeight: "1.4",
187
- },
188
- grid: {
189
- display: "grid",
190
- gridTemplateColumns: "repeat(auto-fill, minmax(64px, 1fr))",
191
- gap: 6,
192
- padding: "8px 12px 12px",
193
- overflowY: "auto",
194
- flex: 1,
195
- },
196
- cell: {
197
- display: "flex",
198
- flexDirection: "column",
199
- alignItems: "center",
200
- gap: 3,
201
- padding: 4,
202
- borderRadius: 4,
203
- cursor: "pointer",
204
- transition: "background 120ms",
205
- background: "transparent",
206
- },
207
- label: {
208
- fontSize: 9,
209
- color: JimboColorOption.GREY,
210
- textAlign: "center",
211
- lineHeight: "1.2",
212
- maxWidth: 60,
213
- overflow: "hidden",
214
- textOverflow: "ellipsis",
215
- whiteSpace: "nowrap",
216
- },
217
- emptyState: {
218
- gridColumn: "1 / -1",
219
- textAlign: "center",
220
- color: JimboColorOption.GREY,
221
- fontSize: 13,
222
- padding: 20,
223
- },
224
- };
@@ -0,0 +1,11 @@
1
+ import { type SlotSelection, type JamlZone } from "./MysterySlot.js";
2
+ export interface JamlMapEditorProps {
3
+ /** Initial zone for the demo. */
4
+ zone?: JamlZone;
5
+ /** Callback when selections change. Returns JAML string. */
6
+ onChange?: (jamlString: string) => void;
7
+ }
8
+ export interface MapSlotSelection extends SlotSelection {
9
+ zone: JamlZone;
10
+ }
11
+ export declare function JamlMapEditor({ zone: initialZone, onChange, }: JamlMapEditorProps): import("react/jsx-runtime").JSX.Element;