@open-slot-ui/pixi 0.7.1 → 0.8.2

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ControlView, ButtonView, SliderView, buildBlockColumn, OpenUIPixi } from './chunk-PYXZIGXG.js';
2
2
  export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, ReadoutView, SelectView, SliderView, SpinView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, svgSpinSkin } from './chunk-PYXZIGXG.js';
3
3
  import { Text, Graphics, Container, Rectangle } from 'pixi.js';
4
- import { createUI, composeMenu, buildPanel, formatAmount } from '@open-slot-ui/core';
4
+ import { LOCALE_LABELS, createUI, composeMenu, buildPanel, formatAmount } from '@open-slot-ui/core';
5
5
 
6
6
  var RISE = 26;
7
7
  var STAGGER = 38;
@@ -278,9 +278,294 @@ var PanelBodyView = class extends ControlView {
278
278
  super.dispose();
279
279
  }
280
280
  };
281
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
282
+ var rich = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
283
+ function renderBlocks(blocks, tr) {
284
+ const out = [];
285
+ for (const b of blocks) {
286
+ switch (b.kind) {
287
+ case "text":
288
+ out.push(`<p>${rich(tr(b.text))}</p>`);
289
+ break;
290
+ case "heading":
291
+ out.push(`<div class="ohm-sec"><span>${esc(tr(b.text))}</span></div>`);
292
+ break;
293
+ case "subheading":
294
+ out.push(`<h4 class="ohm-subh">${esc(tr(b.text))}</h4>`);
295
+ break;
296
+ case "legal":
297
+ out.push(`<p class="ohm-legal">${rich(tr(b.text))}</p>`);
298
+ break;
299
+ case "divider":
300
+ out.push('<hr class="ohm-hr">');
301
+ break;
302
+ case "image":
303
+ out.push(`<img class="ohm-feature" alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`);
304
+ break;
305
+ case "media": {
306
+ const img = `<img alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`;
307
+ const body = `<div class="ohm-media-body">${b.title ? `<h4>${esc(tr(b.title))}</h4>` : ""}<p>${rich(tr(b.text))}</p></div>`;
308
+ out.push(`<div class="ohm-media ohm-media--${b.side ?? "left"}">${img}${body}</div>`);
309
+ break;
310
+ }
311
+ case "cards": {
312
+ const cards = b.items.map((it) => `<div class="ohm-fcard">${it.icon ? `<img src="${it.icon}" alt="" loading="lazy">` : ""}<h5>${esc(tr(it.title))}</h5>${it.text ? `<p>${rich(tr(it.text))}</p>` : ""}</div>`).join("");
313
+ out.push(`<div class="ohm-cards">${cards}</div>`);
314
+ break;
315
+ }
316
+ case "paytable":
317
+ out.push(`<div class="ohm-grid">${renderPaytable(b.rows, tr)}</div>`);
318
+ break;
319
+ case "table": {
320
+ const head = b.columns?.length ? `<thead><tr>${b.columns.map((c) => `<th>${esc(tr(c))}</th>`).join("")}</tr></thead>` : "";
321
+ const body = b.rows.map((r) => `<tr>${r.map((c) => `<td>${esc(tr(c))}</td>`).join("")}</tr>`).join("");
322
+ out.push(`<table class="ohm-table">${head}<tbody>${body}</tbody></table>`);
323
+ break;
324
+ }
325
+ case "stat-grid": {
326
+ const rows = b.items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
327
+ out.push(`<dl class="ohm-stats">${rows}</dl>`);
328
+ break;
329
+ }
330
+ case "steps": {
331
+ const items = b.items.map((s) => `<li>${rich(tr(s))}</li>`).join("");
332
+ out.push(b.ordered ? `<ol class="ohm-steps">${items}</ol>` : `<ul class="ohm-steps">${items}</ul>`);
333
+ break;
334
+ }
335
+ case "callout":
336
+ out.push(`<div class="ohm-callout ohm-callout--${b.tone ?? "info"}">${b.title ? `<b>${esc(tr(b.title))}</b>` : ""}<p>${rich(tr(b.text))}</p></div>`);
337
+ break;
338
+ case "group":
339
+ out.push(`<div class="ohm-group">${b.title ? `<h4 class="ohm-subh">${esc(tr(b.title))}</h4>` : ""}${renderBlocks(b.children, tr)}</div>`);
340
+ break;
341
+ }
342
+ }
343
+ return out.join("\n");
344
+ }
345
+ function renderPaytable(rows, tr) {
346
+ return rows.map((r) => {
347
+ const icon = r.icon ? `<img class="ohm-symimg" src="${r.icon}" alt="" loading="lazy">` : `<span class="ohm-emoji">${esc(tr(r.symbol ?? ""))}</span>`;
348
+ const lines = r.payouts.split("\n").map((line) => {
349
+ const i = line.indexOf(":");
350
+ return i >= 0 ? `<div><b>${esc(line.slice(0, i))}</b><span>${esc(line.slice(i + 1))}</span></div>` : `<div><span>${esc(line)}</span></div>`;
351
+ }).join("");
352
+ return `<div class="ohm-sym">${icon}<div class="ohm-pay">${lines}</div></div>`;
353
+ }).join("");
354
+ }
355
+ function renderSettingBlock(b, tr) {
356
+ const hint = "hint" in b && b.hint ? `<div class="ohm-hint">${esc(tr(b.hint))}</div>` : "";
357
+ const label = "label" in b && b.label ? esc(tr(b.label)) : "";
358
+ switch (b.kind) {
359
+ case "toggle":
360
+ return `<div class="ohm-setting"><label class="ohm-row ohm-check"><span>${label}</span><span class="ohm-ctl"><input data-set="${b.id}" type="checkbox"${b.on ? " checked" : ""}></span></label>${hint}</div>`;
361
+ case "slider":
362
+ return `<div class="ohm-setting"><label class="ohm-row"><span>${label}</span><input data-set="${b.id}" type="range" min="0" max="1" step="0.01" value="${b.initial ?? 1}"></label>${hint}</div>`;
363
+ case "select": {
364
+ const opts = b.options.map((o, i) => `<option value="${esc(o.value)}"${i === (b.index ?? 0) ? " selected" : ""}>${esc(tr(o.label))}</option>`).join("");
365
+ return `<div class="ohm-setting"><label class="ohm-row"><span>${label}</span><select data-set="${b.id}">${opts}</select></label>${hint}</div>`;
366
+ }
367
+ default:
368
+ return "";
369
+ }
370
+ }
371
+ function mountInfoMenu(app, ui) {
372
+ const menu = ui.spec?.menu ?? {};
373
+ const tr = (k) => ui.t(k);
374
+ const disposers = [];
375
+ const host2 = document.createElement("div");
376
+ host2.className = "ohm-root";
377
+ const vars = {
378
+ "--accent": "#d99000",
379
+ "--accent-text": "#1a1200",
380
+ "--surface": "#ffffff",
381
+ "--surface-alt": "#eef1f6",
382
+ "--text": "#181b20",
383
+ "--text-dim": "#5b6472",
384
+ "--card-radius": "8px",
385
+ "--font": ui.theme.type.family
386
+ };
387
+ for (const [k, v] of Object.entries(vars)) host2.style.setProperty(k, v);
388
+ const locales = ui.spec?.locale ? Array.from(/* @__PURE__ */ new Set([ui.spec.locale.locale, ...Object.keys(ui.spec.locale.messages)])) : [];
389
+ const langRow = locales.length > 1 ? `<div class="ohm-setting"><label class="ohm-row"><span>${tr("Language")}</span><select id="ohm-lang">${locales.map((c) => `<option value="${c}"${c === ui.locale.get() ? " selected" : ""}>${esc(LOCALE_LABELS[c] ?? c)}</option>`).join("")}</select></label></div>` : "";
390
+ const turbo = ui.turbo;
391
+ const cap = (m) => m.charAt(0).toUpperCase() + m.slice(1);
392
+ const turboCtl = turbo.modeCount <= 2 ? `<span class="ohm-ctl"><input id="ohm-turbo" type="checkbox"></span>` : `<span class="ohm-ctl"><div class="ohm-segmented" id="ohm-turbo-seg">${turbo.modes.map((m, i) => `<button class="ohm-seg" data-i="${i}">${esc(tr(cap(m)))}</button>`).join("")}</div></span>`;
393
+ const customSettings = (menu.settings ?? []).map((b) => renderSettingBlock(b, tr)).join("");
394
+ const soundMode = menu.sound ?? "sliders";
395
+ const volRow = (id, label, hint, val) => `<div class="ohm-setting"><label class="ohm-row"><span>${esc(tr(label))}</span><input class="ohm-slider" data-vol="${id}" type="range" min="0" max="1" step="0.01" value="${val}"></label><div class="ohm-hint">${esc(tr(hint))}</div></div>`;
396
+ const soundSliders = soundMode === "master" ? volRow("master", "Volume", "Adjust the overall game volume.", ui.sfxSlider.value.get()) : soundMode === "sliders" ? volRow("music", "Music", "Adjust the background music volume.", ui.musicSlider.value.get()) + volRow("sfx", "Effects", "Adjust the sound-effects volume.", ui.sfxSlider.value.get()) : "";
397
+ const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr) : "";
398
+ const rulesHtml = menu.rules ? renderBlocks(menu.rules, tr) : "";
399
+ const banner = menu.banner ? `<img class="ohm-logo" alt="" src="${menu.banner.src}"${menu.banner.width ? ` width="${menu.banner.width}"` : ""}${menu.banner.height ? ` height="${menu.banner.height}"` : ""}>` : ui.gameInfo.name ? `<h1 class="ohm-logo-text">${esc(ui.gameInfo.name)}</h1>` : "";
400
+ host2.innerHTML = `
401
+ <div class="ohm-backdrop" data-close></div>
402
+ <button class="ohm-x" data-close aria-label="Close">\u2715</button>
403
+ <div class="ohm-card" role="dialog" aria-modal="true">
404
+ <div class="ohm-body">
405
+ ${banner}
406
+ <div class="ohm-sec"><span>${tr("Settings")}</span></div>
407
+ <div class="ohm-setting"><label class="ohm-row ohm-check"><span>${tr("Sound")}</span><span class="ohm-ctl"><input id="ohm-sound" type="checkbox" checked></span></label><div class="ohm-hint">${tr("Turn all game sound and music on or off.")}</div></div>
408
+ ${soundSliders}
409
+ ${langRow}
410
+ <div class="ohm-setting"><div class="ohm-row ohm-check"><span>${tr("Quick spin")}</span>${turboCtl}</div><div class="ohm-hint">${tr("Speed up rounds by shortening the animation. The result is identical.")}</div></div>
411
+ ${customSettings}
412
+ ${paytableHtml ? `<div class="ohm-sec"><span>${tr("Paytable")}</span></div>${paytableHtml}` : ""}
413
+ ${rulesHtml ? `<div class="ohm-sec"><span>${tr("Rules")}</span></div><div class="ohm-rules" id="ohm-rules">${rulesHtml}</div>` : ""}
414
+ </div>
415
+ </div>`;
416
+ const style = document.createElement("style");
417
+ style.textContent = OHM_CSS;
418
+ host2.appendChild(style);
419
+ document.body.appendChild(host2);
420
+ const $ = (sel) => host2.querySelector(sel);
421
+ const sound = $("#ohm-sound");
422
+ if (sound) {
423
+ sound.checked = !ui.muted.get();
424
+ sound.addEventListener("change", () => ui.setMuted(!sound.checked));
425
+ disposers.push(ui.muted.subscribe((m) => {
426
+ sound.checked = !m;
427
+ }));
428
+ }
429
+ host2.querySelectorAll("[data-vol]").forEach((el) => {
430
+ const which = el.dataset.vol;
431
+ el.addEventListener("input", () => {
432
+ const v = Number(el.value);
433
+ if (which === "music") ui.musicSlider.setNormalized(v);
434
+ else if (which === "sfx") ui.sfxSlider.setNormalized(v);
435
+ else {
436
+ ui.musicSlider.setNormalized(v);
437
+ ui.sfxSlider.setNormalized(v);
438
+ }
439
+ });
440
+ const src = which === "music" ? ui.musicSlider : ui.sfxSlider;
441
+ disposers.push(src.value.subscribe((v) => {
442
+ el.value = String(v);
443
+ }));
444
+ });
445
+ const lang = $("#ohm-lang");
446
+ if (lang) lang.addEventListener("change", () => ui.setLocale(lang.value));
447
+ if (turbo.modeCount <= 2) {
448
+ const tg = $("#ohm-turbo");
449
+ if (tg) {
450
+ tg.checked = turbo.isOn;
451
+ tg.addEventListener("change", () => turbo.set(tg.checked));
452
+ disposers.push(turbo.index.subscribe(() => {
453
+ tg.checked = turbo.isOn;
454
+ }));
455
+ }
456
+ } else {
457
+ const segs = Array.from(host2.querySelectorAll(".ohm-seg"));
458
+ segs.forEach((b) => b.addEventListener("click", () => turbo.setIndex(Number(b.dataset.i))));
459
+ const sync = () => segs.forEach((b, i) => b.classList.toggle("active", i === turbo.index.get()));
460
+ disposers.push(turbo.index.subscribe(sync));
461
+ sync();
462
+ }
463
+ host2.querySelectorAll("[data-set]").forEach((el) => {
464
+ const id = el.dataset.set;
465
+ if (el instanceof HTMLInputElement && el.type === "checkbox") el.addEventListener("change", () => ui.bus.emit("toggled", { id, on: el.checked }));
466
+ else if (el instanceof HTMLInputElement && el.type === "range") el.addEventListener("input", () => ui.bus.emit("valueChanged", { id, value: Number(el.value) }));
467
+ else if (el instanceof HTMLSelectElement) el.addEventListener("change", () => ui.bus.emit("optionSelected", { id, value: el.value, index: el.selectedIndex }));
468
+ });
469
+ host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", () => ui.settingsPanel.closePanel()));
470
+ disposers.push(
471
+ ui.locale.subscribe(() => {
472
+ const body = host2.querySelector(".ohm-body");
473
+ if (body) {
474
+ const rulesEl = host2.querySelector("#ohm-rules");
475
+ if (rulesEl && menu.rules) rulesEl.innerHTML = renderBlocks(menu.rules, tr);
476
+ if (lang) lang.value = ui.locale.get();
477
+ }
478
+ })
479
+ );
480
+ disposers.push(ui.settingsPanel.state.subscribe(() => host2.classList.toggle("open", ui.settingsPanel.isOpen)));
481
+ return () => {
482
+ for (const d of disposers.splice(0)) d();
483
+ host2.remove();
484
+ };
485
+ }
486
+ var OHM_CSS = `
487
+ .ohm-root { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
488
+ .ohm-root.open { opacity: 1; pointer-events: auto; }
489
+ .ohm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }
490
+ .ohm-root.open .ohm-backdrop { background: rgba(8,6,4,.34); backdrop-filter: blur(6px) saturate(1.1); -webkit-backdrop-filter: blur(6px) saturate(1.1); }
491
+ .ohm-card { position: relative; width: min(92%, 1100px); max-height: 86vh; display: flex; flex-direction: column; background: var(--surface); color: var(--text); border: 1.5px solid #000; border-radius: var(--card-radius); box-shadow: 0 30px 80px rgba(0,0,0,.5); overflow: hidden; transform: translateY(8px) scale(.99); transition: transform .18s ease; }
492
+ .ohm-root.open .ohm-card { transform: none; }
493
+ .ohm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }
494
+ .ohm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
495
+ .ohm-body { padding: 24px 26px 26px; overflow-y: scroll; }
496
+ .ohm-body::-webkit-scrollbar { width: 18px; }
497
+ .ohm-body::-webkit-scrollbar-track { background: transparent; margin: 12px 0; }
498
+ .ohm-body::-webkit-scrollbar-thumb { background-color: #111; border: 6px solid transparent; background-clip: padding-box; border-radius: 999px; min-height: 44px; }
499
+ .ohm-body::-webkit-scrollbar-thumb:hover { background-color: #000; }
500
+ .ohm-logo { display: block; margin: 6px auto 18px; max-width: 64%; height: auto; }
501
+ .ohm-logo-text { margin: 6px 0 18px; text-align: center; font-size: 30px; font-weight: 900; letter-spacing: 1px; color: var(--text); }
502
+ .ohm-sec { display: flex; align-items: center; gap: 14px; margin: 26px 0 14px; color: var(--text); font-weight: 800; letter-spacing: 1px; }
503
+ .ohm-sec::before, .ohm-sec::after { content: ""; flex: 1; height: 2px; background: color-mix(in srgb, var(--text) 80%, transparent); border-radius: 2px; }
504
+ .ohm-root *, .ohm-root *::before, .ohm-root *::after { box-sizing: border-box; }
505
+ .ohm-row { display: flex; align-items: center; gap: 16px; margin: 14px 0; font-weight: 700; }
506
+ .ohm-setting { margin: 14px 0; }
507
+ .ohm-setting .ohm-row { margin: 0; }
508
+ .ohm-hint { margin: 3px 0 0; font-size: 12.5px; font-weight: 500; color: var(--text-dim); }
509
+ .ohm-row > span:first-child { flex: none; min-width: 110px; }
510
+ .ohm-row input[type=range] { flex: 1; min-width: 0; max-width: min(440px, 60dvw); accent-color: var(--accent); height: 6px; }
511
+ .ohm-row select { flex: 1; min-width: 0; max-width: min(440px, 60dvw); padding: 11px 14px; border-radius: 4px; border: 2px solid var(--accent); background: var(--surface-alt); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer; }
512
+ .ohm-row select option { background: var(--surface); color: var(--text); font-weight: 600; }
513
+ .ohm-row select option:checked { background: var(--accent); color: var(--accent-text); }
514
+ .ohm-ctl { flex: 1; min-width: 0; max-width: min(440px, 60dvw); display: flex; align-items: center; justify-content: flex-end; }
515
+ .ohm-check input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 50px; height: 28px; border-radius: 999px; background: color-mix(in srgb, var(--text-dim) 38%, transparent); position: relative; cursor: pointer; transition: background .15s; flex: none; }
516
+ .ohm-check input[type=checkbox]:checked { background: var(--accent); }
517
+ .ohm-check input[type=checkbox]::before { content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 999px; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
518
+ .ohm-check input[type=checkbox]:checked::before { left: 25px; }
519
+ .ohm-segmented { display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-alt); border-radius: 999px; border: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); }
520
+ .ohm-seg { border: 0; background: transparent; color: var(--text-dim); font-weight: 700; font-size: 14px; padding: 8px 18px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }
521
+ .ohm-seg.active { background: var(--accent); color: var(--accent-text); }
522
+ .ohm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
523
+ .ohm-sym { display: flex; align-items: center; gap: 16px; padding: 6px 4px; }
524
+ .ohm-emoji { font-size: 48px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }
525
+ .ohm-symimg { width: 56px; height: 56px; object-fit: contain; flex: none; }
526
+ .ohm-pay { font-size: 13px; line-height: 1.6; }
527
+ .ohm-pay div { display: flex; gap: 6px; }
528
+ .ohm-pay b { min-width: 42px; }
529
+ .ohm-pay span { color: var(--accent); font-weight: 700; }
530
+ .ohm-body p { color: var(--text-dim); line-height: 1.6; }
531
+ .ohm-body p b { color: var(--text); }
532
+ .ohm-feature { display: block; width: 100%; height: auto; margin: 10px 0; }
533
+ .ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
534
+ .ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }
535
+ .ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }
536
+ .ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }
537
+ .ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }
538
+ .ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }
539
+ .ohm-callout--warning b { color: #b07d09; }
540
+ .ohm-subh { margin: 22px 0 8px; font-size: 15px; font-weight: 800; letter-spacing: .5px; color: var(--text); }
541
+ .ohm-legal { font-size: 12px; line-height: 1.6; color: var(--text-dim); opacity: .85; }
542
+ .ohm-hr { border: 0; border-top: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); margin: 18px 0; }
543
+ .ohm-media { display: flex; align-items: center; gap: 18px; margin: 14px 0; }
544
+ .ohm-media--right { flex-direction: row-reverse; }
545
+ .ohm-media > img { width: 40%; max-width: 320px; height: auto; flex: none; }
546
+ .ohm-media-body { flex: 1; }
547
+ .ohm-media-body h4 { margin: 0 0 6px; font-size: 16px; color: var(--text); }
548
+ .ohm-media-body p { margin: 0; }
549
+ .ohm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }
550
+ .ohm-fcard { padding: 8px 6px; text-align: center; }
551
+ .ohm-fcard img { display: block; width: 48px; height: 48px; margin: 0 auto 8px; }
552
+ .ohm-fcard h5 { margin: 0 0 4px; font-size: 14px; color: var(--text); }
553
+ .ohm-fcard p { margin: 0; font-size: 12px; line-height: 1.5; }
554
+ .ohm-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
555
+ .ohm-table th { text-align: left; padding: 8px 10px; font-weight: 800; color: var(--text); border-bottom: 2px solid color-mix(in srgb, var(--text-dim) 35%, transparent); }
556
+ .ohm-table td { padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 18%, transparent); }
557
+ .ohm-table td:first-child { color: var(--text); font-weight: 700; }
558
+ .ohm-table td:not(:first-child) { color: var(--accent); font-weight: 700; }
559
+ .ohm-steps { margin: 12px 0; padding-left: 22px; color: var(--text-dim); line-height: 1.7; }
560
+ .ohm-steps li { margin: 5px 0; }
561
+ .ohm-steps b { color: var(--text); }
562
+ .ohm-group { margin: 8px 0; }
563
+ `;
564
+
565
+ // src/mountHud.ts
281
566
  function showReplayModal(ui, info, buttonKey, onSelect) {
282
567
  const cur = info.currency ?? ui.balance.currency.get();
283
- const money = (n) => formatAmount(n, cur);
568
+ const money2 = (n) => formatAmount(n, cur);
284
569
  ui.showNotice(
285
570
  [
286
571
  { kind: "heading", id: "openui-replay-h", text: "openui.replay.title" },
@@ -288,10 +573,10 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
288
573
  kind: "stat-grid",
289
574
  id: "openui-replay-g",
290
575
  items: [
291
- { label: "openui.replay.baseBet", value: money(info.baseBet) },
576
+ { label: "openui.replay.baseBet", value: money2(info.baseBet) },
292
577
  { label: "openui.replay.costMultiplier", value: `${info.costMultiplier}\xD7` },
293
578
  { label: "openui.replay.payoutMultiplier", value: `${info.payoutMultiplier}\xD7` },
294
- { label: "openui.replay.amount", value: money(info.amount) }
579
+ { label: "openui.replay.amount", value: money2(info.amount) }
295
580
  ]
296
581
  }
297
582
  ],
@@ -303,15 +588,17 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
303
588
  );
304
589
  }
305
590
  function mountHud(app, spec = {}, opts = {}) {
306
- const { hooks, ...pixiOpts } = opts;
591
+ const { hooks, infoMenu, ...pixiOpts } = opts;
307
592
  const ui = createUI(spec, hooks);
593
+ const useInfoMenu = infoMenu !== false && pixiOpts.menu !== false;
308
594
  const locales = spec.locale ? Array.from(/* @__PURE__ */ new Set([spec.locale.locale, ...Object.keys(spec.locale.messages)])) : [];
309
- const menu = pixiOpts.menu === false ? false : composeMenu(spec.menu, { locales, localeSelectId: spec.localeSelectId, rulesFallback: spec.rules });
595
+ const menu = pixiOpts.menu === false || useInfoMenu ? false : composeMenu(spec.menu, { locales, localeSelectId: spec.localeSelectId, rulesFallback: spec.rules });
310
596
  if (menu && spec.game && (spec.game.name || spec.game.version)) {
311
597
  menu.push({ kind: "legal", id: "openui-game-info", text: [spec.game.name, spec.game.version ? `v${spec.game.version}` : ""].filter(Boolean).join(" \xB7 ") });
312
598
  }
313
599
  const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu });
314
600
  pixi.mount(app);
601
+ const disposeInfoMenu = useInfoMenu ? mountInfoMenu(app, ui) : void 0;
315
602
  const extra = [];
316
603
  if (spec.panels?.length) {
317
604
  for (const ps of spec.panels) {
@@ -332,6 +619,7 @@ function mountHud(app, spec = {}, opts = {}) {
332
619
  });
333
620
  const teardown = () => {
334
621
  offRelayout();
622
+ disposeInfoMenu?.();
335
623
  for (const v of extra) v.dispose();
336
624
  extra.length = 0;
337
625
  pixi.unmount();
@@ -416,10 +704,237 @@ function mountHud(app, spec = {}, opts = {}) {
416
704
  dispose: teardown
417
705
  };
418
706
  }
707
+ function money(amount, cur) {
708
+ return formatAmount(amount, cur);
709
+ }
710
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
711
+ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
712
+ const ui = hud.ui;
713
+ const tr = (k) => ui.t(k);
714
+ const list = features.slice(0, 4);
715
+ const boosts = /* @__PURE__ */ new Set();
716
+ const activation = opts.activation ?? "multi";
717
+ const blocksBuy = opts.activationBlocksBuy ?? false;
718
+ const disposers = [];
719
+ const host2 = document.createElement("div");
720
+ host2.className = "bfm-root";
721
+ const vars = {
722
+ "--accent": "#d99000",
723
+ "--accent-text": "#1a1200",
724
+ "--surface": "#ffffff",
725
+ "--surface-alt": "#eef1f6",
726
+ "--text": "#181b20",
727
+ "--text-dim": "#5b6472",
728
+ "--font": ui.theme.type.family
729
+ };
730
+ for (const [k, v] of Object.entries(vars)) host2.style.setProperty(k, v);
731
+ host2.innerHTML = `
732
+ <div class="bfm-backdrop" data-close></div>
733
+ <button class="bfm-x" data-close aria-label="Close">\u2715</button>
734
+ <div class="bfm-panel" role="dialog" aria-modal="true">
735
+ <div class="bfm-fit" id="bfm-fit">
736
+ <h2 class="bfm-title" data-t="Buy Feature">${esc2(tr("Buy Feature"))}</h2>
737
+ <div class="bfm-bet">
738
+ <button class="bfm-step" id="bfm-minus" aria-label="Decrease">\u2212</button>
739
+ <div class="bfm-betbox"><span class="bfm-betlabel" data-t="Bet">${esc2(tr("Bet"))}</span><b id="bfm-betval">\u2014</b></div>
740
+ <button class="bfm-step" id="bfm-plus" aria-label="Increase">+</button>
741
+ </div>
742
+ <div class="bfm-cards" id="bfm-cards"></div>
743
+ </div>
744
+ </div>
745
+ <div class="bfm-confirm" id="bfm-confirm">
746
+ <div class="bfm-confirm-card">
747
+ <p class="bfm-confirm-msg" id="bfm-confirm-msg"></p>
748
+ <div class="bfm-confirm-row">
749
+ <button class="bfm-confirm-btn bfm-confirm-no" id="bfm-confirm-no" data-t="openui.cancel">${esc2(tr("openui.cancel"))}</button>
750
+ <button class="bfm-confirm-btn bfm-confirm-yes" id="bfm-confirm-yes" data-t="openui.confirm">${esc2(tr("openui.confirm"))}</button>
751
+ </div>
752
+ </div>
753
+ </div>`;
754
+ const style = document.createElement("style");
755
+ style.textContent = BFM_CSS;
756
+ host2.appendChild(style);
757
+ document.body.appendChild(host2);
758
+ const $ = (sel) => host2.querySelector(sel);
759
+ const panel = $(".bfm-panel");
760
+ const fitEl = $("#bfm-fit");
761
+ const cardsEl = $("#bfm-cards");
762
+ const betValEl = $("#bfm-betval");
763
+ const confirmEl = $("#bfm-confirm");
764
+ const confirmMsg = $("#bfm-confirm-msg");
765
+ const confirmYes = $("#bfm-confirm-yes");
766
+ const confirmNo = $("#bfm-confirm-no");
767
+ let pendingConfirm = null;
768
+ const hideConfirm = () => {
769
+ confirmEl.classList.remove("show");
770
+ pendingConfirm = null;
771
+ };
772
+ confirmNo.addEventListener("click", hideConfirm);
773
+ confirmYes.addEventListener("click", () => {
774
+ const fn = pendingConfirm;
775
+ hideConfirm();
776
+ fn?.();
777
+ });
778
+ const askConfirm = (totalCost, name, price, onYes) => {
779
+ if (!hud.shouldConfirmBuy(totalCost)) {
780
+ onYes();
781
+ return;
782
+ }
783
+ confirmMsg.textContent = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
784
+ pendingConfirm = onYes;
785
+ confirmEl.classList.add("show");
786
+ };
787
+ const layout = () => {
788
+ const vw = window.innerWidth;
789
+ const vh = window.innerHeight;
790
+ const cols = vh <= 540 || vw >= 900 ? Math.min(4, list.length) : Math.min(2, list.length);
791
+ cardsEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
792
+ const fitW = Math.min(vw * 0.96, cols >= 4 ? 1180 : cols === 1 ? 380 : 760);
793
+ fitEl.style.width = `${fitW}px`;
794
+ fitEl.style.transform = "none";
795
+ const natH = fitEl.offsetHeight;
796
+ const s = Math.min(1, vh * 0.95 / natH);
797
+ fitEl.style.transform = `translateX(-50%) scale(${s})`;
798
+ panel.style.width = `${Math.ceil(fitW * s)}px`;
799
+ panel.style.height = `${Math.ceil(natH * s)}px`;
800
+ };
801
+ const renderCards = () => {
802
+ const bet = opts.getBet ? opts.getBet() : ui.bet.get();
803
+ const cur = ui.bet.currency.get();
804
+ cardsEl.innerHTML = list.map((f) => {
805
+ const active = boosts.has(f.id);
806
+ const buyBlocked = f.variant === "buy" && blocksBuy && boosts.size > 0;
807
+ const price = f.variant === "buy" ? money(f.cost * bet, cur) : `+${money(f.cost * bet, cur)}`;
808
+ const label = f.variant === "buy" ? tr("Buy") : active ? tr("Activated") : tr("Activate");
809
+ const cls = `bfm-action bfm-action--${f.variant}${active ? " is-active" : ""}${buyBlocked ? " is-blocked" : ""}`;
810
+ const img = f.image ? ` style="background-image:url('${f.image}')"` : "";
811
+ return `
812
+ <div class="bfm-cell">
813
+ <div class="bfm-card">
814
+ <div class="bfm-cardimg"${img}></div>
815
+ <div class="bfm-strip"></div>
816
+ <div class="bfm-cardbody">
817
+ <span class="bfm-name">${esc2(tr(f.name))}</span>
818
+ <b class="bfm-price">${esc2(price)}</b>
819
+ </div>
820
+ </div>
821
+ <button class="${cls}" data-id="${f.id}" data-variant="${f.variant}"${buyBlocked ? " disabled" : ""}>${esc2(label)}</button>
822
+ </div>`;
823
+ }).join("");
824
+ betValEl.textContent = money(bet, cur);
825
+ cardsEl.querySelectorAll(".bfm-action").forEach((b) => {
826
+ b.addEventListener("click", () => onAction(b.dataset.id, b.dataset.variant));
827
+ });
828
+ layout();
829
+ };
830
+ const onAction = (id, variant) => {
831
+ const f = list.find((x) => x.id === id);
832
+ const bet = opts.getBet ? opts.getBet() : ui.bet.get();
833
+ const cur = ui.bet.currency.get();
834
+ if (variant === "boost") {
835
+ const wasActive = boosts.has(id);
836
+ const commit = () => {
837
+ if (activation === "single") boosts.clear();
838
+ if (wasActive) boosts.delete(id);
839
+ else boosts.add(id);
840
+ ui.bus.emit("cardActivated", { id });
841
+ renderCards();
842
+ opts.onActivate?.([...boosts], id, boosts.has(id));
843
+ };
844
+ if (wasActive) {
845
+ commit();
846
+ return;
847
+ }
848
+ const total = 1 + (f?.cost ?? 0);
849
+ askConfirm(total, f?.name ?? id, money(total * bet, cur), commit);
850
+ } else {
851
+ if (blocksBuy && boosts.size > 0) return;
852
+ const cost = (f?.cost ?? 0) * bet;
853
+ askConfirm(f?.cost ?? 0, f?.name ?? id, money(cost, cur), () => {
854
+ ui.bus.emit("cardActivated", { id });
855
+ close();
856
+ opts.onBuy?.(id, cost);
857
+ });
858
+ }
859
+ };
860
+ $("#bfm-minus").addEventListener("click", () => ui.betStepper.dec());
861
+ $("#bfm-plus").addEventListener("click", () => ui.betStepper.inc());
862
+ disposers.push(ui.bet.value.subscribe(() => renderCards()));
863
+ const open = () => {
864
+ renderCards();
865
+ host2.classList.add("open");
866
+ };
867
+ const close = () => host2.classList.remove("open");
868
+ host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", close));
869
+ disposers.push(ui.on("buttonActivated", ({ id }) => {
870
+ if (id === "bonus") open();
871
+ }));
872
+ const onResize = () => {
873
+ if (host2.classList.contains("open")) layout();
874
+ };
875
+ window.addEventListener("resize", onResize);
876
+ disposers.push(
877
+ ui.locale.subscribe(() => {
878
+ host2.querySelectorAll("[data-t]").forEach((n) => n.textContent = tr(n.dataset.t));
879
+ renderCards();
880
+ })
881
+ );
882
+ renderCards();
883
+ return () => {
884
+ window.removeEventListener("resize", onResize);
885
+ for (const d of disposers.splice(0)) d();
886
+ host2.remove();
887
+ };
888
+ }
889
+ var BFM_CSS = `
890
+ .bfm-root { position: fixed; inset: 0; z-index: 11000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
891
+ .bfm-root.open { opacity: 1; pointer-events: auto; }
892
+ .bfm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }
893
+ .bfm-root.open .bfm-backdrop { background: rgba(8,6,4,.5); backdrop-filter: blur(10px) saturate(1.1); -webkit-backdrop-filter: blur(10px) saturate(1.1); }
894
+ .bfm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }
895
+ .bfm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
896
+ .bfm-root *, .bfm-root *::before, .bfm-root *::after { box-sizing: border-box; }
897
+ .bfm-panel { position: relative; transform: translateY(8px) scale(.985); transition: transform .18s ease; }
898
+ .bfm-root.open .bfm-panel { transform: none; }
899
+ .bfm-fit { position: absolute; top: 0; left: 50%; transform: translateX(-50%); transform-origin: top center; }
900
+ .bfm-title { margin: 0 0 14px; text-align: center; color: #fff; font-size: 30px; font-weight: 800; letter-spacing: 1px; text-shadow: 0 2px 12px rgba(0,0,0,.6); }
901
+ .bfm-bet { display: flex; align-items: center; justify-content: center; gap: 16px; margin: 0 0 24px; }
902
+ .bfm-betbox { min-width: 200px; padding: 10px 22px; border-radius: 12px; background: var(--surface); border: 3px solid #000; display: flex; flex-direction: column; align-items: center; line-height: 1.1; }
903
+ .bfm-betlabel { font-size: 12px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
904
+ .bfm-betbox b { font-size: 24px; color: var(--text); }
905
+ .bfm-step { flex: none; width: 54px; height: 54px; border-radius: 999px; border: 3px solid #000; background: var(--surface); color: var(--text); font-size: 28px; font-weight: 800; cursor: pointer; display: grid; place-items: center; line-height: 1; transition: transform .1s, background .12s; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
906
+ .bfm-step:hover { background: var(--surface-alt); }
907
+ .bfm-step:active { transform: scale(.92); }
908
+ .bfm-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; align-items: start; }
909
+ .bfm-cell { display: flex; flex-direction: column; min-width: 0; }
910
+ .bfm-card { background: var(--surface); border: 4px solid #000; border-radius: 14px; overflow: hidden; box-shadow: 0 14px 34px rgba(0,0,0,.45); }
911
+ .bfm-cardimg { width: 100%; aspect-ratio: 16 / 10; background-size: cover; background-position: center; background-color: var(--surface-alt); background-image: linear-gradient(135deg, #e7ebf2, #cfd6e2); }
912
+ .bfm-strip { height: 8px; background: linear-gradient(90deg, #f0a500, #ffd166, #f0a500); }
913
+ .bfm-cardbody { padding: 12px 14px 16px; text-align: center; }
914
+ .bfm-name { display: block; font-size: 15px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
915
+ .bfm-price { display: block; margin-top: 2px; font-size: 22px; font-weight: 800; color: var(--text); }
916
+ .bfm-action { display: block; width: 100%; margin-top: 12px; padding: 14px 10px; border-radius: 12px; border: 4px solid #000; background: var(--surface); color: var(--text); font-size: 15px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: transform .1s, background .12s, color .12s; box-shadow: 0 5px 14px rgba(0,0,0,.35); white-space: nowrap; }
917
+ .bfm-action:hover { background: var(--surface-alt); }
918
+ .bfm-action:active { transform: scale(.96); }
919
+ .bfm-action.is-active { background: var(--accent); color: var(--accent-text); border-color: #000; }
920
+ .bfm-action.is-blocked, .bfm-action:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
921
+ .bfm-action.is-blocked:hover, .bfm-action:disabled:hover { background: var(--surface); }
922
+ .bfm-confirm { position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; opacity: 0; pointer-events: none; transition: opacity .16s ease; }
923
+ .bfm-confirm.show { opacity: 1; pointer-events: auto; }
924
+ .bfm-confirm::before { content: ""; position: absolute; inset: 0; background: rgba(8,6,4,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
925
+ .bfm-confirm-card { position: relative; width: min(90%, 420px); background: var(--surface); color: var(--text); border: 3px solid #000; border-radius: 14px; padding: 26px 24px 22px; box-shadow: 0 24px 60px rgba(0,0,0,.55); text-align: center; }
926
+ .bfm-confirm-msg { margin: 0 0 22px; font-size: 19px; font-weight: 700; line-height: 1.4; color: var(--text); }
927
+ .bfm-confirm-row { display: flex; gap: 14px; }
928
+ .bfm-confirm-btn { flex: 1; padding: 14px 10px; border-radius: 12px; border: 3px solid #000; font-size: 15px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: transform .1s, background .12s; }
929
+ .bfm-confirm-btn:active { transform: scale(.96); }
930
+ .bfm-confirm-no { background: var(--surface); color: var(--text); }
931
+ .bfm-confirm-no:hover { background: var(--surface-alt); }
932
+ .bfm-confirm-yes { background: var(--accent); color: var(--accent-text); }
933
+ `;
419
934
 
420
935
  // src/bootError.ts
421
936
  var host = null;
422
- var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
937
+ var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
423
938
  function showBootError(opts = {}) {
424
939
  const title = opts.title ?? "Connection lost";
425
940
  const message = opts.message ?? "The game could not reach the game server. Please reload to reconnect and continue.";
@@ -433,10 +948,10 @@ function showBootError(opts = {}) {
433
948
  <div class="openui-be-icon" aria-hidden="true">
434
949
  <svg viewBox="0 0 48 48" width="48" height="48"><path d="M24 4 3 42h42L24 4Z" fill="none" stroke="#ffc935" stroke-width="3" stroke-linejoin="round"/><rect x="22" y="18" width="4" height="12" rx="2" fill="#ffc935"/><circle cx="24" cy="35" r="2.4" fill="#ffc935"/></svg>
435
950
  </div>
436
- <h1 class="openui-be-title">${esc(title)}</h1>
437
- <p class="openui-be-msg">${esc(message)}</p>
438
- ${opts.detail ? `<p class="openui-be-detail">${esc(opts.detail)}</p>` : ""}
439
- ${showReload ? `<button class="openui-be-reload" type="button">${esc(opts.reloadLabel ?? "Reload")}</button>` : ""}
951
+ <h1 class="openui-be-title">${esc3(title)}</h1>
952
+ <p class="openui-be-msg">${esc3(message)}</p>
953
+ ${opts.detail ? `<p class="openui-be-detail">${esc3(opts.detail)}</p>` : ""}
954
+ ${showReload ? `<button class="openui-be-reload" type="button">${esc3(opts.reloadLabel ?? "Reload")}</button>` : ""}
440
955
  </div>
441
956
  <style>
442
957
  .openui-boot-error { position: fixed; inset: 0; z-index: 2147483000; display: grid; place-items: center;
@@ -464,6 +979,6 @@ function hideBootError() {
464
979
  host = null;
465
980
  }
466
981
 
467
- export { PanelBodyView, PanelView, PopoverView, hideBootError, mountHud, showBootError };
982
+ export { PanelBodyView, PanelView, PopoverView, hideBootError, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError };
468
983
  //# sourceMappingURL=index.js.map
469
984
  //# sourceMappingURL=index.js.map