@open-slot-ui/pixi 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -181,6 +181,16 @@ var SpinView = class extends ControlView {
181
181
  this.fsFace.addChild(ring, this.fsCount, this.fsLabel);
182
182
  this.fsFace.visible = false;
183
183
  this.art.addChild(this.fsFace);
184
+ const stopRing = new Graphics().circle(0, 0, 96).fill({ color: 16777215 }).stroke({ width: 7, color: 657930 });
185
+ this.stopLabel = new Text({ text: ui.t("openui.stop") === "openui.stop" ? "STOP" : ui.t("openui.stop"), style: { fontFamily: ui.theme.type.family, fontSize: 26, fill: 657930, fontWeight: "900", letterSpacing: 3 } });
186
+ this.stopLabel.anchor.set(0.5);
187
+ this.stopLabel.y = -38;
188
+ this.stopCount = new Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 64, fill: 657930, fontWeight: "900" } });
189
+ this.stopCount.anchor.set(0.5);
190
+ this.stopCount.y = 16;
191
+ this.stopFace.addChild(stopRing, this.stopLabel, this.stopCount);
192
+ this.stopFace.visible = false;
193
+ this.art.addChild(this.stopFace);
184
194
  this.addChild(this.art);
185
195
  this.hitArea = new Circle(0, 0, 118);
186
196
  this.eventMode = "static";
@@ -199,13 +209,19 @@ var SpinView = class extends ControlView {
199
209
  this.spin.allowSlamStop.subscribe(() => this.updateInteractive()),
200
210
  this.spin.onTransition((t) => this.play(t)),
201
211
  // free-spins face: switch between the normal skin and the "N FS" counter
202
- this.spin.freeSpins.subscribe(() => this.updateFreeSpins()),
212
+ this.spin.freeSpins.subscribe(() => this.updateFaces()),
213
+ // autoplay: while active the spin button becomes the STOP-count button
214
+ this.ui.autoplay.state.subscribe(() => {
215
+ this.updateFaces();
216
+ this.updateInteractive();
217
+ }),
218
+ this.ui.autoplay.count.subscribe(() => this.updateFaces()),
203
219
  this.ui.locale.subscribe(() => {
204
220
  if (!this.destroyed) this.fsLabel.text = this.ui.t("openui.freeSpins");
205
221
  })
206
222
  );
207
223
  this.skin.update(this.spin.current);
208
- this.updateFreeSpins();
224
+ this.updateFaces();
209
225
  this.updateInteractive();
210
226
  }
211
227
  spin;
@@ -219,13 +235,25 @@ var SpinView = class extends ControlView {
219
235
  fsFace = new Container();
220
236
  fsCount;
221
237
  fsLabel;
222
- /** Swap the spin face: free-spins counter when `freeSpins > 0`, else the skin. */
223
- updateFreeSpins() {
238
+ /** Autoplay STOP face a "STOP" label over the remaining count (or ∞); shown while
239
+ * autoplay is active. Tapping the spin button then STOPS autoplay. */
240
+ stopFace = new Container();
241
+ stopCount;
242
+ stopLabel;
243
+ /** Pick the spin face: autoplay STOP-count (top priority) → free-spins counter →
244
+ * the normal skin. */
245
+ updateFaces() {
246
+ const auto = this.ui.autoplay.isActive;
224
247
  const n = this.spin.freeSpins.get();
225
- const fs = n > 0;
226
- this.fsCount.text = String(n);
248
+ const fs = n > 0 && !auto;
249
+ if (auto) {
250
+ const c = this.ui.autoplay.count.get();
251
+ this.stopCount.text = c === Infinity ? "\u221E" : String(c);
252
+ }
253
+ if (fs) this.fsCount.text = String(n);
254
+ this.stopFace.visible = auto;
227
255
  this.fsFace.visible = fs;
228
- this.skin.view.visible = !fs;
256
+ this.skin.view.visible = !auto && !fs;
229
257
  }
230
258
  onOver = () => {
231
259
  if (isDesktop() && this.spin.current === "idle") this.spin.setState("hover");
@@ -253,6 +281,10 @@ var SpinView = class extends ControlView {
253
281
  onUp = () => {
254
282
  if (this.holding) return this.endHold();
255
283
  this.clearHoldTimer();
284
+ if (this.ui.autoplay.isActive) {
285
+ this.ui.autoplay.stop();
286
+ return;
287
+ }
256
288
  if (this.spin.current === "pressed") {
257
289
  this.spin.setState("idle");
258
290
  this.spin.activate();
@@ -281,7 +313,7 @@ var SpinView = class extends ControlView {
281
313
  if (this.spin.current === "pressed") this.spin.setState("idle");
282
314
  }
283
315
  updateInteractive() {
284
- const ok = this.spin.interactable;
316
+ const ok = this.spin.interactable || this.ui.autoplay.isActive;
285
317
  this.eventMode = ok ? "static" : "none";
286
318
  this.cursor = ok ? "pointer" : "default";
287
319
  const inSpin = this.spin.current === "spinning" || this.spin.current === "auto" || this.spin.current === "stop";
@@ -1138,9 +1170,9 @@ var TextCellRenderer = class {
1138
1170
  };
1139
1171
 
1140
1172
  // src/views/ValueDisplayView.ts
1141
- var DIGIT_W = 26;
1142
- var DIGIT_H = 46;
1143
- var FONT_SIZE = 40;
1173
+ var DIGIT_W = 39;
1174
+ var DIGIT_H = 68;
1175
+ var FONT_SIZE = 48;
1144
1176
  var FIT_BUDGET_COLS = 9;
1145
1177
  var ValueDisplayView = class extends ControlView {
1146
1178
  constructor(vd, ui, ticker, fitGsap) {
@@ -1174,6 +1206,7 @@ var ValueDisplayView = class extends ControlView {
1174
1206
  fitGsap;
1175
1207
  counter;
1176
1208
  caption;
1209
+ captionPill;
1177
1210
  side() {
1178
1211
  const a = this.vd.layout.anchor;
1179
1212
  return a.includes("left") ? "left" : a.includes("right") ? "right" : "center";
@@ -1183,13 +1216,20 @@ var ValueDisplayView = class extends ControlView {
1183
1216
  const side = this.side();
1184
1217
  if (this.vd.label) {
1185
1218
  this.caption = new Text({
1186
- text: this.ui.t(this.vd.label).toUpperCase(),
1187
- style: { fontFamily: theme.type.family, fontSize: 13, fill: theme.color.text, fontWeight: "700", letterSpacing: 2 }
1219
+ // Figma: the label is NOT uppercased — "Баланс" / "Ставка" as authored.
1220
+ text: this.ui.t(this.vd.label),
1221
+ style: { fontFamily: theme.type.family, fontSize: 28, fill: 16777215, fontWeight: "900", letterSpacing: 0 }
1188
1222
  });
1189
- this.caption.alpha = 0.55;
1190
- this.caption.anchor.set(side === "left" ? 0 : side === "right" ? 1 : 0.5, 1);
1191
- this.caption.y = -DIGIT_H / 2 - 3;
1192
- this.addChild(this.caption);
1223
+ this.caption.anchor.set(0.5, 0.5);
1224
+ const padX = 24;
1225
+ const padY = 8;
1226
+ const pillW = Math.ceil(this.caption.width) + padX * 2;
1227
+ const pillH = Math.ceil(this.caption.height) + padY * 2;
1228
+ const pillX = side === "left" ? 0 : side === "right" ? -pillW : -pillW / 2;
1229
+ const pillY = -DIGIT_H / 2 - pillH - 14;
1230
+ this.captionPill = new Graphics().roundRect(pillX, pillY, pillW, pillH, 12).fill({ color: 0 });
1231
+ this.caption.position.set(pillX + pillW / 2, pillY + pillH / 2);
1232
+ this.addChild(this.captionPill, this.caption);
1193
1233
  }
1194
1234
  const cur = this.vd.currency.get();
1195
1235
  this.buildCounter(displayDigits(this.vd.get(), this.vd.digits, cur.decimals), this.vd.minorUnits);
@@ -1201,7 +1241,16 @@ var ValueDisplayView = class extends ControlView {
1201
1241
  const cur = this.vd.currency.get();
1202
1242
  const side = this.side();
1203
1243
  const renderer = new TextCellRenderer({
1204
- style: { fontFamily: theme.type.family, fontSize: FONT_SIZE, fill: theme.color.text, fontWeight: "700" },
1244
+ // Figma "default" readout: Montserrat Black, white, with a bold BLACK OUTLINE
1245
+ // plus a soft drop shadow (0px 4px, 25% black) under it.
1246
+ style: {
1247
+ fontFamily: theme.type.family,
1248
+ fontSize: FONT_SIZE,
1249
+ fill: 16777215,
1250
+ fontWeight: "900",
1251
+ stroke: { color: 0, width: 8, join: "round" },
1252
+ dropShadow: { color: 0, alpha: 0.25, blur: 2, distance: 4, angle: Math.PI / 2 }
1253
+ },
1205
1254
  digitWidth: DIGIT_W,
1206
1255
  digitHeight: DIGIT_H
1207
1256
  // no fillerChar → rolling cells show real digits (crisp, no blur, no blank flicker)
@@ -1246,8 +1295,10 @@ var ValueDisplayView = class extends ControlView {
1246
1295
  rebuild() {
1247
1296
  this.counter?.destroy();
1248
1297
  this.caption?.destroy();
1298
+ this.captionPill?.destroy();
1249
1299
  this.counter = void 0;
1250
1300
  this.caption = void 0;
1301
+ this.captionPill = void 0;
1251
1302
  this.removeChildren();
1252
1303
  this.build();
1253
1304
  }
@@ -1256,6 +1307,12 @@ var ValueDisplayView = class extends ControlView {
1256
1307
  super.dispose();
1257
1308
  }
1258
1309
  };
1310
+ var GLYPH_SVG = {
1311
+ speaker: '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M16.1933 9.375C16.6252 10.4652 16.6015 11.654 16.1257 12.75" fill="none" stroke="black" stroke-width="3" stroke-linecap="round"/></svg>',
1312
+ "speaker-mute": '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M15.5 8.5L19.5 13.5M19.5 8.5L15.5 13.5" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/></svg>',
1313
+ fullscreen: '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M7.875 18H7.5C5.84315 18 4.5 16.6569 4.5 15V14.25M7.875 4.5H7.5C5.84315 4.5 4.5 5.84315 4.5 7.5V8.25M14.25 4.5H15C16.6569 4.5 18 5.84315 18 7.5V8.25M14.25 18H15C16.6569 18 18 16.6569 18 15V14.25" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1314
+ "fullscreen-exit": '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 8.25H5.25C6.9 8.25 8.25 6.9 8.25 5.25V4.5M14.25 4.5V5.25C14.25 6.9 15.6 8.25 17.25 8.25H18M18 14.25H17.25C15.6 14.25 14.25 15.6 14.25 17.25V18M8.25 18V17.25C8.25 15.6 6.9 14.25 5.25 14.25H4.5" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
1315
+ };
1259
1316
  var ButtonView = class extends ControlView {
1260
1317
  constructor(btn, ui, ticker, opts = {}) {
1261
1318
  super(btn, ui);
@@ -1267,6 +1324,7 @@ var ButtonView = class extends ControlView {
1267
1324
  this.glyph = opts.glyph ?? "none";
1268
1325
  this.iconTarget = opts.iconTarget ?? 84;
1269
1326
  this.mono = opts.mono ?? false;
1327
+ this.dark = opts.dark ?? false;
1270
1328
  this.addChild(this.art);
1271
1329
  if (opts.iconTexture) {
1272
1330
  this.sprite = new Sprite(opts.iconTexture);
@@ -1274,7 +1332,7 @@ var ButtonView = class extends ControlView {
1274
1332
  this.fitSprite();
1275
1333
  this.art.addChild(this.sprite);
1276
1334
  } else {
1277
- this.art.addChild(this.bg);
1335
+ this.art.addChild(this.bg, this.glyphG);
1278
1336
  if (btn.label) {
1279
1337
  this.labelText = new Text({
1280
1338
  text: ui.t(btn.label),
@@ -1286,8 +1344,6 @@ var ButtonView = class extends ControlView {
1286
1344
  }
1287
1345
  this.eventMode = "static";
1288
1346
  this.cursor = "pointer";
1289
- this.on("pointerover", this.onOver);
1290
- this.on("pointerout", this.onOut);
1291
1347
  this.on("pointerdown", this.onDown);
1292
1348
  this.on("pointerup", this.onUp);
1293
1349
  this.on("pointerupoutside", this.onUpOutside);
@@ -1297,6 +1353,12 @@ var ButtonView = class extends ControlView {
1297
1353
  this.updateInteractive();
1298
1354
  }),
1299
1355
  this.btn.onTransition((t) => this.play(t)),
1356
+ // The host input lock gates `interactable` without a state change, so react to
1357
+ // it directly: a locked/disabled button dims to a semi-transparent state.
1358
+ this.ui.locked.subscribe(() => {
1359
+ this.redraw();
1360
+ this.updateInteractive();
1361
+ }),
1300
1362
  this.ui.locale.subscribe(() => {
1301
1363
  if (this.destroyed) return;
1302
1364
  if (this.labelText && this.btn.label) {
@@ -1312,6 +1374,7 @@ var ButtonView = class extends ControlView {
1312
1374
  btn;
1313
1375
  art = new Container();
1314
1376
  bg = new Graphics();
1377
+ glyphG = new Graphics();
1315
1378
  sprite;
1316
1379
  labelText;
1317
1380
  tween;
@@ -1321,6 +1384,7 @@ var ButtonView = class extends ControlView {
1321
1384
  glyph;
1322
1385
  iconTarget;
1323
1386
  mono;
1387
+ dark;
1324
1388
  /** Swap the displayed art (e.g. ☰ → ✕). */
1325
1389
  setIconTexture(tex) {
1326
1390
  if (!this.sprite) return;
@@ -1340,12 +1404,6 @@ var ButtonView = class extends ControlView {
1340
1404
  this.sprite.scale.set(1);
1341
1405
  this.sprite.scale.set(this.iconTarget / this.sprite.height);
1342
1406
  }
1343
- onOver = () => {
1344
- if (isDesktop() && this.btn.current === "idle") this.btn.setState("hover");
1345
- };
1346
- onOut = () => {
1347
- if (this.btn.current === "hover") this.btn.setState("idle");
1348
- };
1349
1407
  onDown = () => {
1350
1408
  if (this.btn.interactable) this.btn.setState("pressed");
1351
1409
  };
@@ -1374,6 +1432,7 @@ var ButtonView = class extends ControlView {
1374
1432
  const ok = this.btn.interactable;
1375
1433
  this.eventMode = ok ? "static" : "none";
1376
1434
  this.cursor = ok ? "pointer" : "default";
1435
+ this.alpha = ok ? 1 : 0.4;
1377
1436
  }
1378
1437
  redraw() {
1379
1438
  if (this.sprite) {
@@ -1384,31 +1443,37 @@ var ButtonView = class extends ControlView {
1384
1443
  const g = this.bg;
1385
1444
  g.clear();
1386
1445
  const disabled = this.btn.current === "disabled";
1387
- const fillC = this.mono ? "#ffffff" : t.color.surface;
1446
+ const fillC = this.dark ? "#0a0a0a" : this.mono ? "#ffffff" : t.color.surface;
1388
1447
  const lineC = this.mono ? "#0a0a0a" : t.color.accent;
1389
- const ink = this.mono ? disabled ? "#9aa0a6" : "#0a0a0a" : disabled ? t.color.textDim : t.color.text;
1448
+ const ink = this.dark ? "#ffffff" : this.mono ? disabled ? "#9aa0a6" : "#0a0a0a" : disabled ? t.color.textDim : t.color.text;
1449
+ this.glyphG.clear();
1450
+ this.glyphG.visible = false;
1390
1451
  if (this.shape === "circle") {
1391
1452
  const r = this.radius;
1392
- g.circle(0, 0, r).fill({ color: fillC, alpha: this.mono && disabled ? 0.6 : 1 });
1393
- g.circle(0, 0, r).stroke({ width: this.mono ? 5 : 4, color: lineC });
1394
- if (this.glyph === "menu") {
1395
- for (const dy of [-8, 0, 8]) g.moveTo(-13, dy).lineTo(13, dy).stroke({ width: 4, color: ink });
1453
+ if (this.mono) {
1454
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: disabled ? 0.08 : 0.18 });
1455
+ g.circle(0, 0, r).fill({ color: "#ffffff", alpha: disabled ? 0.6 : 1 });
1456
+ g.circle(0, 0, r * 0.925).stroke({ width: r * 0.15, color: "#0a0a0a", alpha: disabled ? 0.5 : 1 });
1457
+ } else if (this.dark) {
1458
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: 0.18 });
1459
+ g.circle(0, 0, r).fill({ color: "#0a0a0a" });
1460
+ } else {
1461
+ g.circle(0, 0, r).fill({ color: fillC }).circle(0, 0, r).stroke({ width: 4, color: lineC });
1462
+ }
1463
+ const svg = GLYPH_SVG[this.glyph];
1464
+ if (svg) {
1465
+ const s = r / 15;
1466
+ this.glyphG.svg(svg);
1467
+ this.glyphG.scale.set(s);
1468
+ this.glyphG.position.set(-11.25 * s, -11.25 * s);
1469
+ this.glyphG.alpha = disabled ? 0.5 : 1;
1470
+ this.glyphG.visible = true;
1471
+ } else if (this.glyph === "menu") {
1472
+ const barW = this.dark ? 6 : 4;
1473
+ const spread = this.dark ? 9 : 8;
1474
+ for (const dy of [-spread, 0, spread]) g.moveTo(-14, dy).lineTo(14, dy).stroke({ width: barW, color: ink, cap: "round" });
1396
1475
  } else if (this.glyph === "close") {
1397
1476
  g.moveTo(-10, -10).lineTo(10, 10).moveTo(10, -10).lineTo(-10, 10).stroke({ width: 4, color: ink });
1398
- } else if (this.glyph === "speaker" || this.glyph === "speaker-mute") {
1399
- g.poly([-12, -4, -6, -4, 1, -11, 1, 11, -6, 4, -12, 4]).fill({ color: ink });
1400
- if (this.glyph === "speaker") {
1401
- g.moveTo(6, -6).lineTo(11, 0).lineTo(6, 6).stroke({ width: 2.5, color: ink });
1402
- g.moveTo(11, -9).lineTo(17, 0).lineTo(11, 9).stroke({ width: 2.5, color: ink });
1403
- } else {
1404
- g.moveTo(7, -6).lineTo(17, 6).moveTo(17, -6).lineTo(7, 6).stroke({ width: 2.5, color: ink });
1405
- }
1406
- } else if (this.glyph === "fullscreen") {
1407
- const a = 13, b = 6;
1408
- g.moveTo(-a, -a + b).lineTo(-a, -a).lineTo(-a + b, -a).moveTo(a - b, -a).lineTo(a, -a).lineTo(a, -a + b).moveTo(-a, a - b).lineTo(-a, a).lineTo(-a + b, a).moveTo(a - b, a).lineTo(a, a).lineTo(a, a - b).stroke({ width: 3, color: ink });
1409
- } else if (this.glyph === "fullscreen-exit") {
1410
- const c = 4, d = 12;
1411
- g.moveTo(-d, -c).lineTo(-c, -c).lineTo(-c, -d).moveTo(d, -c).lineTo(c, -c).lineTo(c, -d).moveTo(-d, c).lineTo(-c, c).lineTo(-c, d).moveTo(d, c).lineTo(c, c).lineTo(c, d).stroke({ width: 3, color: ink });
1412
1477
  }
1413
1478
  } else {
1414
1479
  const w = this.labelText ? this.labelText.width + 48 : 120;
@@ -1653,24 +1718,30 @@ var AutoplayView = class extends ControlView {
1653
1718
  }
1654
1719
  drawButton() {
1655
1720
  const st = this.auto.current;
1656
- this.countText.visible = st === "active";
1657
- if (st === "active") this.countText.text = this.fmt(this.auto.count.get());
1658
- if (st === "active") {
1659
- if (this.sprite) this.sprite.visible = false;
1660
- const r = this.sprite ? this.target / 2 : this.radius;
1661
- this.bg.clear();
1662
- this.bg.roundRect(-r, -r, 2 * r, 2 * r, r * 0.35).fill({ color: this.ui.theme.color.accent }).stroke({ width: 4, color: this.ui.theme.color.accentText });
1721
+ this.countText.visible = false;
1722
+ const th = this.ui.theme;
1723
+ const r = this.sprite ? this.target / 2 : this.radius;
1724
+ const active = st === "active";
1725
+ this.eventMode = active ? "none" : "static";
1726
+ this.cursor = active ? "default" : "pointer";
1727
+ if (active) {
1728
+ this.bg.clear().circle(0, 0, r).fill({ color: th.color.accent }).stroke({ width: 4, color: th.color.accentText });
1729
+ if (this.sprite) {
1730
+ this.sprite.visible = true;
1731
+ this.sprite.tint = th.color.accentText;
1732
+ this.sprite.alpha = 0.55;
1733
+ }
1663
1734
  return;
1664
1735
  }
1665
1736
  if (this.sprite) {
1666
1737
  this.sprite.visible = true;
1738
+ this.sprite.tint = 16777215;
1667
1739
  this.bg.clear();
1668
1740
  const tex = this.idleTex ?? this.activeTex;
1669
1741
  if (tex) this.sprite.texture = tex;
1670
1742
  this.fit();
1671
1743
  this.sprite.alpha = st === "disabled" ? 0.5 : 1;
1672
1744
  } else {
1673
- const th = this.ui.theme;
1674
1745
  this.bg.clear();
1675
1746
  this.bg.circle(0, 0, this.radius).fill({ color: th.color.surface }).stroke({ width: 4, color: th.color.accent });
1676
1747
  }
@@ -1841,8 +1912,8 @@ var AutoplayDrawerView = class extends Container {
1841
1912
  this.startBtn.position.set(W / 2, startY);
1842
1913
  this.startBtn.hitArea = new Rectangle(-startW / 2, -startH / 2, startW, startH);
1843
1914
  this.sheetH = startY + startH / 2 + 20 * k;
1844
- this.sheetBg.clear().rect(0, 0, W, this.sheetH + 40).fill({ color: DARK });
1845
- this.handle.clear().rect(W / 2 - 30 * k, 13 * k, 60 * k, 5 * k).fill({ color: DIM });
1915
+ this.sheetBg.clear().rect(-1, 0, W + 2, this.sheetH + 40 + 600).fill({ color: DARK });
1916
+ this.handle.clear().roundRect(W / 2 - 30 * k, 13 * k, 60 * k, 5 * k, 2.5 * k).fill({ color: DIM });
1846
1917
  return this.sheetH + 40;
1847
1918
  }
1848
1919
  drawGroup(g) {
@@ -2906,7 +2977,27 @@ var ReadoutView = class extends ControlView {
2906
2977
  this.ticker = ticker;
2907
2978
  const t = ui.theme;
2908
2979
  const inline = opts.inline ?? false;
2909
- const fill = opts.mono ? "#ffffff" : t.color.text;
2980
+ this.prefix = opts.prefix ?? false;
2981
+ const fill = opts.fill ?? (opts.mono ? "#ffffff" : t.color.text);
2982
+ if (this.prefix) {
2983
+ this.valueText = new Text({ text: "", style: { fontFamily: t.type.family, fontSize: 12, fill: opts.fill ?? "#ffffff", fontWeight: "400" } });
2984
+ this.valueText.alpha = opts.fill ? 1 : 0.5;
2985
+ this.valueText.anchor.set(0, 0);
2986
+ this.addChild(this.valueText);
2987
+ this.disposers.push(
2988
+ this.ro.value.subscribe(() => this.render()),
2989
+ this.ui.locale.subscribe(() => {
2990
+ if (!this.destroyed) this.render();
2991
+ })
2992
+ );
2993
+ if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
2994
+ if (this.ro.kind === "duration") {
2995
+ this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
2996
+ this.ticker.add(this.tick);
2997
+ }
2998
+ this.render();
2999
+ return;
3000
+ }
2910
3001
  if (ro.label) {
2911
3002
  this.caption = new Text({
2912
3003
  text: ui.t(ro.label).toUpperCase(),
@@ -2949,6 +3040,7 @@ var ReadoutView = class extends ControlView {
2949
3040
  caption;
2950
3041
  valueText;
2951
3042
  tick;
3043
+ prefix;
2952
3044
  render() {
2953
3045
  const v = this.ro.value.get();
2954
3046
  let text;
@@ -2965,7 +3057,7 @@ var ReadoutView = class extends ControlView {
2965
3057
  default:
2966
3058
  text = String(v);
2967
3059
  }
2968
- this.valueText.text = text;
3060
+ this.valueText.text = this.prefix && this.ro.label ? `${this.ui.t(this.ro.label)}: ${text}` : text;
2969
3061
  }
2970
3062
  dispose() {
2971
3063
  if (this.tick) this.ticker.remove(this.tick);
@@ -2973,9 +3065,18 @@ var ReadoutView = class extends ControlView {
2973
3065
  }
2974
3066
  };
2975
3067
  var INSET2 = 24;
2976
- var BTN_H = 50;
2977
- var BTN_GAP = 12;
3068
+ var BTN_H = 52;
3069
+ var BTN_GAP = 14;
2978
3070
  var ROW_GAP = 16;
3071
+ var LIGHT = {
3072
+ surface: "#ffffff",
3073
+ surfaceAlt: "#eef1f6",
3074
+ text: "#181b20",
3075
+ textDim: "#5b6472",
3076
+ border: "#000000",
3077
+ primary: "#0a0a0a",
3078
+ primaryText: "#ffffff"
3079
+ };
2979
3080
  var DialogView = class extends ControlView {
2980
3081
  constructor(panel, blocks, actions, ui, ticker, opts = {}) {
2981
3082
  super(panel, ui);
@@ -2986,6 +3087,8 @@ var DialogView = class extends ControlView {
2986
3087
  this.opts = opts;
2987
3088
  this.zIndex = 130;
2988
3089
  this.maxWidth = opts.maxWidth ?? 520;
3090
+ const lightTheme = { ...ui.theme, color: { ...ui.theme.color, surface: LIGHT.surface, surfaceAlt: LIGHT.surfaceAlt, text: LIGHT.text, textDim: LIGHT.textDim } };
3091
+ this.lightUi = new Proxy(ui, { get: (t, p) => p === "theme" ? lightTheme : Reflect.get(t, p) });
2989
3092
  this.backdrop.eventMode = "static";
2990
3093
  this.backdrop.on("pointertap", () => {
2991
3094
  if (!this.ui.noticeBlocking.get()) this.panel.closePanel();
@@ -3025,11 +3128,13 @@ var DialogView = class extends ControlView {
3025
3128
  childViews = [];
3026
3129
  screen;
3027
3130
  maxWidth;
3131
+ /** `ui` proxy with a light theme — so the shared block renderer draws dark-on-white
3132
+ * content inside the white modal card (Figma "default" dialog look). */
3133
+ lightUi;
3028
3134
  buildClose() {
3029
- const t = this.ui.theme;
3030
- const r = 18;
3031
- const bg = new Graphics().circle(0, 0, r).fill({ color: t.color.surfaceAlt }).stroke({ width: 2, color: t.color.textDim });
3032
- const x = new Graphics().moveTo(-6, -6).lineTo(6, 6).moveTo(6, -6).lineTo(-6, 6).stroke({ width: 3, color: t.color.text });
3135
+ const r = 22;
3136
+ const bg = new Graphics().circle(0, 0, r).fill({ color: LIGHT.primary });
3137
+ const x = new Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
3033
3138
  this.closeBtn.addChild(bg, x);
3034
3139
  this.closeBtn.eventMode = "static";
3035
3140
  this.closeBtn.cursor = "pointer";
@@ -3048,13 +3153,12 @@ var DialogView = class extends ControlView {
3048
3153
  if (!this.panel.isOpen) return;
3049
3154
  const W = s.width;
3050
3155
  const H = s.height;
3051
- const t = this.ui.theme;
3052
3156
  for (const v of this.childViews) v.dispose();
3053
3157
  this.childViews.length = 0;
3054
3158
  for (const ch of this.content.removeChildren()) ch.destroy();
3055
3159
  const cardW = Math.min(W - 48, this.maxWidth);
3056
3160
  const innerW = cardW - INSET2 * 2;
3057
- const col = buildBlockColumn(this.blocks.get(), [], this.ui, this.ticker, innerW, { controlSkins: this.opts.controlSkins });
3161
+ const col = buildBlockColumn(this.blocks.get(), [], this.lightUi, this.ticker, innerW, { controlSkins: this.opts.controlSkins });
3058
3162
  this.childViews = col.views;
3059
3163
  const kids = col.content.removeChildren();
3060
3164
  if (kids.length) this.content.addChild(...kids);
@@ -3064,15 +3168,15 @@ var DialogView = class extends ControlView {
3064
3168
  const cardH = Math.min(H - 48, wantH);
3065
3169
  const cx = (W - cardW) / 2;
3066
3170
  const cy = (H - cardH) / 2;
3067
- this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.62 });
3171
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.5 });
3068
3172
  this.backdrop.hitArea = new Rectangle(0, 0, W, H);
3069
- this.card.clear().roundRect(cx, cy, cardW, cardH, t.radius.card).fill({ color: t.color.surface }).stroke({ width: 2, color: t.color.accent });
3173
+ this.card.clear().roundRect(cx, cy, cardW, cardH, 14).fill({ color: LIGHT.surface }).stroke({ width: 2.5, color: LIGHT.border });
3070
3174
  const bodyH = cardH - (buttonsH ? ROW_GAP + buttonsH + INSET2 : INSET2);
3071
3175
  this.content.x = cx + cardW / 2;
3072
3176
  this.content.y = cy;
3073
3177
  this.maskG.clear().rect(cx, cy, cardW, Math.max(0, bodyH)).fill({ color: 16777215 });
3074
3178
  this.buttons.position.set(cx + cardW / 2, cy + cardH - INSET2 / 2 - buttonsH / 2);
3075
- this.closeBtn.position.set(cx + cardW - 20, cy + 20);
3179
+ this.closeBtn.position.set(cx + cardW - 8, cy + 8);
3076
3180
  }
3077
3181
  /** Build the footer buttons from `noticeActions` into a centered row (shrunk to
3078
3182
  * fit if needed). Returns the row height (0 when there are no actions). */
@@ -3105,7 +3209,7 @@ var DialogView = class extends ControlView {
3105
3209
  const bg = new Graphics();
3106
3210
  const label = new Text({
3107
3211
  text: this.ui.t(action.label),
3108
- style: { fontFamily: t.type.family, fontSize: 18, fontWeight: "800", fill: variant === "primary" ? t.color.accentText : t.color.text, letterSpacing: 0.5 }
3212
+ style: { fontFamily: t.type.family, fontSize: 18, fontWeight: "800", fill: variant === "primary" ? LIGHT.primaryText : LIGHT.text, letterSpacing: 0.5 }
3109
3213
  });
3110
3214
  label.anchor.set(0.5);
3111
3215
  node.addChild(bg, label);
@@ -3122,12 +3226,11 @@ var DialogView = class extends ControlView {
3122
3226
  return { node, bg, width, variant };
3123
3227
  }
3124
3228
  drawButton(bg, variant, width) {
3125
- const t = this.ui.theme;
3126
3229
  bg.clear();
3127
3230
  if (variant === "primary") {
3128
- bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: t.color.accent });
3231
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT.primary });
3129
3232
  } else {
3130
- bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: t.color.surfaceAlt }).stroke({ width: 2, color: t.color.accent });
3233
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT.surface }).stroke({ width: 2.5, color: LIGHT.border });
3131
3234
  }
3132
3235
  }
3133
3236
  applyOpen(open) {
@@ -3142,73 +3245,6 @@ var DialogView = class extends ControlView {
3142
3245
  super.dispose();
3143
3246
  }
3144
3247
  };
3145
- var BAR_H = 26;
3146
- var StatusBarView = class _StatusBarView extends Container {
3147
- constructor(controls, ui, ticker, side) {
3148
- super();
3149
- this.ui = ui;
3150
- this.side = side;
3151
- this.zIndex = 300;
3152
- this.addChild(this.bg);
3153
- for (const c of controls) {
3154
- const view = new ReadoutView(c, ui, ticker, { inline: true, mono: true });
3155
- this.addChild(view);
3156
- this.items.push({ id: c.id, view });
3157
- }
3158
- this.disposers.push(
3159
- this.ui.on("visibilityChanged", ({ id }) => {
3160
- if (this.items.some((it) => it.id === id)) this.relayout();
3161
- })
3162
- );
3163
- }
3164
- ui;
3165
- side;
3166
- bg = new Graphics();
3167
- items = [];
3168
- screen;
3169
- disposers = [];
3170
- /** The strip's pixel height for the current screen (used for the HUD margin too). */
3171
- static heightFor(screen) {
3172
- const scale = Math.max(0.7, Math.min(1.4, screen.scale));
3173
- return Math.round(BAR_H * scale);
3174
- }
3175
- applyLayout(screen) {
3176
- this.screen = screen;
3177
- this.relayout();
3178
- }
3179
- relayout() {
3180
- const s = this.screen;
3181
- if (!s) return;
3182
- const W = s.width;
3183
- const H = s.height;
3184
- const scale = Math.max(0.7, Math.min(1.4, s.scale));
3185
- const barH = _StatusBarView.heightFor(s);
3186
- const y = this.side === "top" ? 0 : H - barH;
3187
- this.position.set(0, 0);
3188
- const visible = this.items.filter((it) => !this.ui.hidden.has(it.id));
3189
- for (const it of this.items) it.view.visible = !this.ui.hidden.has(it.id);
3190
- this.bg.clear();
3191
- this.visible = visible.length > 0;
3192
- if (!visible.length) return;
3193
- const edgeY = this.side === "top" ? barH - 1 : y;
3194
- this.bg.rect(0, y, W, barH).fill({ color: 789776, alpha: 0.92 }).rect(0, edgeY, W, 1).fill({ color: 16777215, alpha: 0.18 });
3195
- const n = visible.length;
3196
- const cy = y + barH / 2;
3197
- const pad = 18 * scale;
3198
- const usableW = Math.max(160, W - pad * 2);
3199
- visible.forEach((it, i) => {
3200
- it.view.position.set(pad + usableW * ((i + 0.5) / n), cy);
3201
- it.view.scale.set(scale);
3202
- });
3203
- }
3204
- dispose() {
3205
- for (const it of this.items) it.view.dispose();
3206
- this.items.length = 0;
3207
- for (const d of this.disposers) d();
3208
- this.disposers.length = 0;
3209
- if (!this.destroyed) this.destroy({ children: true });
3210
- }
3211
- };
3212
3248
 
3213
3249
  // src/OpenUIPixi.ts
3214
3250
  function easeInOutCubic(p) {
@@ -3261,8 +3297,8 @@ var OpenUIPixi = class {
3261
3297
  glyph: "menu",
3262
3298
  iconTexture: ic.settingsIdle,
3263
3299
  iconTarget: 88,
3264
- mono: true
3265
- // b&w like the turbo button (used on the drawn glyph path)
3300
+ dark: true
3301
+ // Figma ☰: solid black circle, white bars, no ring
3266
3302
  });
3267
3303
  settingsView.zIndex = 5;
3268
3304
  const menuView = this.opts.menu === false ? void 0 : this.buildMenu(ticker);
@@ -3270,7 +3306,7 @@ var OpenUIPixi = class {
3270
3306
  offTexture: ic.turboOff,
3271
3307
  onTexture: ic.turboOn,
3272
3308
  modeTextures: ic.turboModes,
3273
- target: 88
3309
+ target: 79
3274
3310
  });
3275
3311
  const spinOff = this.ui.spin.layout.offset ?? [0, 0];
3276
3312
  const autoOff = this.ui.autoplay.layout.offset ?? [0, 0];
@@ -3278,23 +3314,23 @@ var OpenUIPixi = class {
3278
3314
  const autoplayView = new AutoplayView(this.ui.autoplay, this.ui, ticker, {
3279
3315
  idleTexture: ic.autoIdle,
3280
3316
  activeTexture: ic.autoActive,
3281
- target: 88,
3317
+ target: 73,
3282
3318
  picker,
3283
3319
  arcCenter: { x: spinOff[0] - autoOff[0], y: spinOff[1] - autoOff[1] },
3284
3320
  arcStepDeg: 22
3285
3321
  });
3286
3322
  autoplayView.zIndex = 20;
3287
- const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 44, iconTexture: ic.bonus, iconTarget: 110 });
3323
+ const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 60, iconTexture: ic.bonus, iconTarget: 130 });
3288
3324
  const betPlusView = new ButtonView(this.ui.betPlus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betPlus, iconTarget: 64 });
3289
3325
  const betMinusView = new ButtonView(this.ui.betMinus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betMinus, iconTarget: 64 });
3290
- const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 22, glyph: "speaker", iconTarget: 44, mono: true });
3291
- const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 22, glyph: "fullscreen", iconTarget: 44, mono: true });
3326
+ const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "speaker", iconTarget: 60, mono: true });
3327
+ const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "fullscreen", iconTarget: 60, mono: true });
3292
3328
  muteView.zIndex = 65;
3293
3329
  fullscreenView.zIndex = 65;
3294
- const statusBarSide = this.opts.statusBar;
3295
- const rtpView = statusBarSide ? void 0 : new ReadoutView(this.ui.rtp, this.ui, ticker);
3296
- const netView = statusBarSide ? void 0 : new ReadoutView(this.ui.netPosition, this.ui, ticker);
3297
- const timerView = statusBarSide ? void 0 : new ReadoutView(this.ui.sessionTimer, this.ui, ticker);
3330
+ const roOpts = { prefix: true, fill: this.opts.readoutColor };
3331
+ const rtpView = new ReadoutView(this.ui.rtp, this.ui, ticker, roOpts);
3332
+ const netView = new ReadoutView(this.ui.netPosition, this.ui, ticker, roOpts);
3333
+ const timerView = new ReadoutView(this.ui.sessionTimer, this.ui, ticker, roOpts);
3298
3334
  const entries = [
3299
3335
  [this.ui.spin.id, spinView],
3300
3336
  [this.ui.balance.id, balanceView],
@@ -3308,9 +3344,9 @@ var OpenUIPixi = class {
3308
3344
  [this.ui.muteButton.id, muteView],
3309
3345
  [this.ui.fullscreenButton.id, fullscreenView]
3310
3346
  ];
3311
- if (rtpView) entries.push([this.ui.rtp.id, rtpView]);
3312
- if (netView) entries.push([this.ui.netPosition.id, netView]);
3313
- if (timerView) entries.push([this.ui.sessionTimer.id, timerView]);
3347
+ entries.push([this.ui.rtp.id, rtpView]);
3348
+ entries.push([this.ui.netPosition.id, netView]);
3349
+ entries.push([this.ui.sessionTimer.id, timerView]);
3314
3350
  const viewById = /* @__PURE__ */ new Map();
3315
3351
  const idByView = /* @__PURE__ */ new Map();
3316
3352
  for (const [id, view] of entries) {
@@ -3371,11 +3407,22 @@ var OpenUIPixi = class {
3371
3407
  const dialog = new DialogView(this.ui.noticePanel, this.ui.noticeBlocks, this.ui.noticeActions, this.ui, ticker, { controlSkins: this.opts.controlSkins });
3372
3408
  this.root.addChild(dialog);
3373
3409
  this.overlays.push(dialog);
3374
- if (statusBarSide) {
3375
- const bar = new StatusBarView([this.ui.netPosition, this.ui.rtp, this.ui.sessionTimer], this.ui, ticker, statusBarSide);
3376
- this.root.addChild(bar);
3377
- this.overlays.push(bar);
3378
- }
3410
+ const vignette = new Graphics();
3411
+ vignette.zIndex = -1;
3412
+ this.root.addChild(vignette);
3413
+ this.overlays.push({
3414
+ applyLayout: (s) => {
3415
+ const r = 200 * s.scale;
3416
+ vignette.clear();
3417
+ for (let i = 0; i < 14; i++) {
3418
+ const t = i / 14;
3419
+ vignette.circle(0, 0, r * (1 - t)).fill({ color: 0, alpha: 0.5 / 14 });
3420
+ }
3421
+ },
3422
+ dispose: () => {
3423
+ if (!vignette.destroyed) vignette.destroy();
3424
+ }
3425
+ });
3379
3426
  const replayBadge = new Container();
3380
3427
  const replayBg = new Graphics();
3381
3428
  const replayText = new Text({ text: this.ui.t("openui.replay").toUpperCase(), style: { fontFamily: this.ui.theme.type.family, fontSize: 16, fontWeight: "800", fill: 16777215, letterSpacing: 2 } });
@@ -3389,8 +3436,7 @@ var OpenUIPixi = class {
3389
3436
  const w = replayText.width + 36;
3390
3437
  const h = 32;
3391
3438
  replayBg.clear().roundRect(-w / 2, -h / 2, w, h, h / 2).fill({ color: 789776 }).stroke({ width: 2, color: 16777215 });
3392
- const top = statusBarSide === "top" ? StatusBarView.heightFor(s) + 28 : 40;
3393
- replayBadge.position.set(s.width / 2, top);
3439
+ replayBadge.position.set(s.width / 2, 40);
3394
3440
  },
3395
3441
  dispose: () => {
3396
3442
  if (!replayBadge.destroyed) replayBadge.destroy({ children: true });
@@ -3404,33 +3450,13 @@ var OpenUIPixi = class {
3404
3450
  if (!replayBadge.destroyed) replayText.text = this.ui.t("openui.replay").toUpperCase();
3405
3451
  })
3406
3452
  );
3407
- if (ic.settingsIdle && ic.settingsActive) {
3408
- const idle = ic.settingsIdle;
3409
- const active = ic.settingsActive;
3410
- this.disposers.push(
3411
- this.ui.settingsPanel.state.subscribe(() => {
3412
- settingsView.setIconTexture(this.ui.settingsPanel.isOpen ? active : idle);
3413
- })
3414
- );
3415
- } else {
3416
- this.disposers.push(
3417
- this.ui.settingsPanel.state.subscribe(() => {
3418
- settingsView.setGlyph(this.ui.settingsPanel.isOpen ? "close" : "menu");
3419
- })
3420
- );
3421
- }
3422
3453
  const applyLayout = () => {
3423
3454
  const screen = this.ui.screen.get();
3424
- const barH = statusBarSide ? StatusBarView.heightFor(screen) : 0;
3425
3455
  this.slideBaseY.clear();
3426
3456
  this.slideSign.clear();
3427
3457
  for (const v of this.views) {
3428
3458
  v.applyLayout(screen);
3429
3459
  const anchor = this.ui.control(idByView.get(v) ?? "")?.layout.anchor ?? "bottom-center";
3430
- if (barH) {
3431
- if (statusBarSide === "top" && anchor.startsWith("top")) v.y += barH;
3432
- else if (statusBarSide === "bottom" && anchor.startsWith("bottom")) v.y -= barH;
3433
- }
3434
3460
  this.slideBaseY.set(v, v.y);
3435
3461
  this.slideSign.set(v, anchor.startsWith("top") ? -1 : 1);
3436
3462
  }
@@ -3843,7 +3869,7 @@ function mountHud(app, spec = {}, opts = {}) {
3843
3869
  if (menu && spec.game && (spec.game.name || spec.game.version)) {
3844
3870
  menu.push({ kind: "legal", id: "openui-game-info", text: [spec.game.name, spec.game.version ? `v${spec.game.version}` : ""].filter(Boolean).join(" \xB7 ") });
3845
3871
  }
3846
- const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu, statusBar: pixiOpts.statusBar ?? spec.statusBar });
3872
+ const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu });
3847
3873
  pixi.mount(app);
3848
3874
  const extra = [];
3849
3875
  if (spec.panels?.length) {
@@ -3949,6 +3975,6 @@ function svgSpinSkin(textures) {
3949
3975
  };
3950
3976
  }
3951
3977
 
3952
- export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, PanelBodyView, PanelView, PopoverView, ReadoutView, SelectView, SliderView, SpinView, StatusBarView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, mountHud, svgSpinSkin };
3978
+ export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, PanelBodyView, PanelView, PopoverView, ReadoutView, SelectView, SliderView, SpinView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, mountHud, svgSpinSkin };
3953
3979
  //# sourceMappingURL=index.js.map
3954
3980
  //# sourceMappingURL=index.js.map