@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.cjs CHANGED
@@ -183,6 +183,16 @@ var SpinView = class extends ControlView {
183
183
  this.fsFace.addChild(ring, this.fsCount, this.fsLabel);
184
184
  this.fsFace.visible = false;
185
185
  this.art.addChild(this.fsFace);
186
+ const stopRing = new pixi_js.Graphics().circle(0, 0, 96).fill({ color: 16777215 }).stroke({ width: 7, color: 657930 });
187
+ this.stopLabel = new pixi_js.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 } });
188
+ this.stopLabel.anchor.set(0.5);
189
+ this.stopLabel.y = -38;
190
+ this.stopCount = new pixi_js.Text({ text: "0", style: { fontFamily: ui.theme.type.family, fontSize: 64, fill: 657930, fontWeight: "900" } });
191
+ this.stopCount.anchor.set(0.5);
192
+ this.stopCount.y = 16;
193
+ this.stopFace.addChild(stopRing, this.stopLabel, this.stopCount);
194
+ this.stopFace.visible = false;
195
+ this.art.addChild(this.stopFace);
186
196
  this.addChild(this.art);
187
197
  this.hitArea = new pixi_js.Circle(0, 0, 118);
188
198
  this.eventMode = "static";
@@ -201,13 +211,19 @@ var SpinView = class extends ControlView {
201
211
  this.spin.allowSlamStop.subscribe(() => this.updateInteractive()),
202
212
  this.spin.onTransition((t) => this.play(t)),
203
213
  // free-spins face: switch between the normal skin and the "N FS" counter
204
- this.spin.freeSpins.subscribe(() => this.updateFreeSpins()),
214
+ this.spin.freeSpins.subscribe(() => this.updateFaces()),
215
+ // autoplay: while active the spin button becomes the STOP-count button
216
+ this.ui.autoplay.state.subscribe(() => {
217
+ this.updateFaces();
218
+ this.updateInteractive();
219
+ }),
220
+ this.ui.autoplay.count.subscribe(() => this.updateFaces()),
205
221
  this.ui.locale.subscribe(() => {
206
222
  if (!this.destroyed) this.fsLabel.text = this.ui.t("openui.freeSpins");
207
223
  })
208
224
  );
209
225
  this.skin.update(this.spin.current);
210
- this.updateFreeSpins();
226
+ this.updateFaces();
211
227
  this.updateInteractive();
212
228
  }
213
229
  spin;
@@ -221,13 +237,25 @@ var SpinView = class extends ControlView {
221
237
  fsFace = new pixi_js.Container();
222
238
  fsCount;
223
239
  fsLabel;
224
- /** Swap the spin face: free-spins counter when `freeSpins > 0`, else the skin. */
225
- updateFreeSpins() {
240
+ /** Autoplay STOP face a "STOP" label over the remaining count (or ∞); shown while
241
+ * autoplay is active. Tapping the spin button then STOPS autoplay. */
242
+ stopFace = new pixi_js.Container();
243
+ stopCount;
244
+ stopLabel;
245
+ /** Pick the spin face: autoplay STOP-count (top priority) → free-spins counter →
246
+ * the normal skin. */
247
+ updateFaces() {
248
+ const auto = this.ui.autoplay.isActive;
226
249
  const n = this.spin.freeSpins.get();
227
- const fs = n > 0;
228
- this.fsCount.text = String(n);
250
+ const fs = n > 0 && !auto;
251
+ if (auto) {
252
+ const c = this.ui.autoplay.count.get();
253
+ this.stopCount.text = c === Infinity ? "\u221E" : String(c);
254
+ }
255
+ if (fs) this.fsCount.text = String(n);
256
+ this.stopFace.visible = auto;
229
257
  this.fsFace.visible = fs;
230
- this.skin.view.visible = !fs;
258
+ this.skin.view.visible = !auto && !fs;
231
259
  }
232
260
  onOver = () => {
233
261
  if (isDesktop() && this.spin.current === "idle") this.spin.setState("hover");
@@ -255,6 +283,10 @@ var SpinView = class extends ControlView {
255
283
  onUp = () => {
256
284
  if (this.holding) return this.endHold();
257
285
  this.clearHoldTimer();
286
+ if (this.ui.autoplay.isActive) {
287
+ this.ui.autoplay.stop();
288
+ return;
289
+ }
258
290
  if (this.spin.current === "pressed") {
259
291
  this.spin.setState("idle");
260
292
  this.spin.activate();
@@ -283,7 +315,7 @@ var SpinView = class extends ControlView {
283
315
  if (this.spin.current === "pressed") this.spin.setState("idle");
284
316
  }
285
317
  updateInteractive() {
286
- const ok = this.spin.interactable;
318
+ const ok = this.spin.interactable || this.ui.autoplay.isActive;
287
319
  this.eventMode = ok ? "static" : "none";
288
320
  this.cursor = ok ? "pointer" : "default";
289
321
  const inSpin = this.spin.current === "spinning" || this.spin.current === "auto" || this.spin.current === "stop";
@@ -1140,9 +1172,9 @@ var TextCellRenderer = class {
1140
1172
  };
1141
1173
 
1142
1174
  // src/views/ValueDisplayView.ts
1143
- var DIGIT_W = 26;
1144
- var DIGIT_H = 46;
1145
- var FONT_SIZE = 40;
1175
+ var DIGIT_W = 39;
1176
+ var DIGIT_H = 68;
1177
+ var FONT_SIZE = 48;
1146
1178
  var FIT_BUDGET_COLS = 9;
1147
1179
  var ValueDisplayView = class extends ControlView {
1148
1180
  constructor(vd, ui, ticker, fitGsap) {
@@ -1176,6 +1208,7 @@ var ValueDisplayView = class extends ControlView {
1176
1208
  fitGsap;
1177
1209
  counter;
1178
1210
  caption;
1211
+ captionPill;
1179
1212
  side() {
1180
1213
  const a = this.vd.layout.anchor;
1181
1214
  return a.includes("left") ? "left" : a.includes("right") ? "right" : "center";
@@ -1185,13 +1218,20 @@ var ValueDisplayView = class extends ControlView {
1185
1218
  const side = this.side();
1186
1219
  if (this.vd.label) {
1187
1220
  this.caption = new pixi_js.Text({
1188
- text: this.ui.t(this.vd.label).toUpperCase(),
1189
- style: { fontFamily: theme.type.family, fontSize: 13, fill: theme.color.text, fontWeight: "700", letterSpacing: 2 }
1221
+ // Figma: the label is NOT uppercased — "Баланс" / "Ставка" as authored.
1222
+ text: this.ui.t(this.vd.label),
1223
+ style: { fontFamily: theme.type.family, fontSize: 28, fill: 16777215, fontWeight: "900", letterSpacing: 0 }
1190
1224
  });
1191
- this.caption.alpha = 0.55;
1192
- this.caption.anchor.set(side === "left" ? 0 : side === "right" ? 1 : 0.5, 1);
1193
- this.caption.y = -DIGIT_H / 2 - 3;
1194
- this.addChild(this.caption);
1225
+ this.caption.anchor.set(0.5, 0.5);
1226
+ const padX = 24;
1227
+ const padY = 8;
1228
+ const pillW = Math.ceil(this.caption.width) + padX * 2;
1229
+ const pillH = Math.ceil(this.caption.height) + padY * 2;
1230
+ const pillX = side === "left" ? 0 : side === "right" ? -pillW : -pillW / 2;
1231
+ const pillY = -DIGIT_H / 2 - pillH - 14;
1232
+ this.captionPill = new pixi_js.Graphics().roundRect(pillX, pillY, pillW, pillH, 12).fill({ color: 0 });
1233
+ this.caption.position.set(pillX + pillW / 2, pillY + pillH / 2);
1234
+ this.addChild(this.captionPill, this.caption);
1195
1235
  }
1196
1236
  const cur = this.vd.currency.get();
1197
1237
  this.buildCounter(core.displayDigits(this.vd.get(), this.vd.digits, cur.decimals), this.vd.minorUnits);
@@ -1203,7 +1243,16 @@ var ValueDisplayView = class extends ControlView {
1203
1243
  const cur = this.vd.currency.get();
1204
1244
  const side = this.side();
1205
1245
  const renderer = new TextCellRenderer({
1206
- style: { fontFamily: theme.type.family, fontSize: FONT_SIZE, fill: theme.color.text, fontWeight: "700" },
1246
+ // Figma "default" readout: Montserrat Black, white, with a bold BLACK OUTLINE
1247
+ // plus a soft drop shadow (0px 4px, 25% black) under it.
1248
+ style: {
1249
+ fontFamily: theme.type.family,
1250
+ fontSize: FONT_SIZE,
1251
+ fill: 16777215,
1252
+ fontWeight: "900",
1253
+ stroke: { color: 0, width: 8, join: "round" },
1254
+ dropShadow: { color: 0, alpha: 0.25, blur: 2, distance: 4, angle: Math.PI / 2 }
1255
+ },
1207
1256
  digitWidth: DIGIT_W,
1208
1257
  digitHeight: DIGIT_H
1209
1258
  // no fillerChar → rolling cells show real digits (crisp, no blur, no blank flicker)
@@ -1248,8 +1297,10 @@ var ValueDisplayView = class extends ControlView {
1248
1297
  rebuild() {
1249
1298
  this.counter?.destroy();
1250
1299
  this.caption?.destroy();
1300
+ this.captionPill?.destroy();
1251
1301
  this.counter = void 0;
1252
1302
  this.caption = void 0;
1303
+ this.captionPill = void 0;
1253
1304
  this.removeChildren();
1254
1305
  this.build();
1255
1306
  }
@@ -1258,6 +1309,12 @@ var ValueDisplayView = class extends ControlView {
1258
1309
  super.dispose();
1259
1310
  }
1260
1311
  };
1312
+ var GLYPH_SVG = {
1313
+ 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>',
1314
+ "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>',
1315
+ 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>',
1316
+ "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>'
1317
+ };
1261
1318
  var ButtonView = class extends ControlView {
1262
1319
  constructor(btn, ui, ticker, opts = {}) {
1263
1320
  super(btn, ui);
@@ -1269,6 +1326,7 @@ var ButtonView = class extends ControlView {
1269
1326
  this.glyph = opts.glyph ?? "none";
1270
1327
  this.iconTarget = opts.iconTarget ?? 84;
1271
1328
  this.mono = opts.mono ?? false;
1329
+ this.dark = opts.dark ?? false;
1272
1330
  this.addChild(this.art);
1273
1331
  if (opts.iconTexture) {
1274
1332
  this.sprite = new pixi_js.Sprite(opts.iconTexture);
@@ -1276,7 +1334,7 @@ var ButtonView = class extends ControlView {
1276
1334
  this.fitSprite();
1277
1335
  this.art.addChild(this.sprite);
1278
1336
  } else {
1279
- this.art.addChild(this.bg);
1337
+ this.art.addChild(this.bg, this.glyphG);
1280
1338
  if (btn.label) {
1281
1339
  this.labelText = new pixi_js.Text({
1282
1340
  text: ui.t(btn.label),
@@ -1288,8 +1346,6 @@ var ButtonView = class extends ControlView {
1288
1346
  }
1289
1347
  this.eventMode = "static";
1290
1348
  this.cursor = "pointer";
1291
- this.on("pointerover", this.onOver);
1292
- this.on("pointerout", this.onOut);
1293
1349
  this.on("pointerdown", this.onDown);
1294
1350
  this.on("pointerup", this.onUp);
1295
1351
  this.on("pointerupoutside", this.onUpOutside);
@@ -1299,6 +1355,12 @@ var ButtonView = class extends ControlView {
1299
1355
  this.updateInteractive();
1300
1356
  }),
1301
1357
  this.btn.onTransition((t) => this.play(t)),
1358
+ // The host input lock gates `interactable` without a state change, so react to
1359
+ // it directly: a locked/disabled button dims to a semi-transparent state.
1360
+ this.ui.locked.subscribe(() => {
1361
+ this.redraw();
1362
+ this.updateInteractive();
1363
+ }),
1302
1364
  this.ui.locale.subscribe(() => {
1303
1365
  if (this.destroyed) return;
1304
1366
  if (this.labelText && this.btn.label) {
@@ -1314,6 +1376,7 @@ var ButtonView = class extends ControlView {
1314
1376
  btn;
1315
1377
  art = new pixi_js.Container();
1316
1378
  bg = new pixi_js.Graphics();
1379
+ glyphG = new pixi_js.Graphics();
1317
1380
  sprite;
1318
1381
  labelText;
1319
1382
  tween;
@@ -1323,6 +1386,7 @@ var ButtonView = class extends ControlView {
1323
1386
  glyph;
1324
1387
  iconTarget;
1325
1388
  mono;
1389
+ dark;
1326
1390
  /** Swap the displayed art (e.g. ☰ → ✕). */
1327
1391
  setIconTexture(tex) {
1328
1392
  if (!this.sprite) return;
@@ -1342,12 +1406,6 @@ var ButtonView = class extends ControlView {
1342
1406
  this.sprite.scale.set(1);
1343
1407
  this.sprite.scale.set(this.iconTarget / this.sprite.height);
1344
1408
  }
1345
- onOver = () => {
1346
- if (isDesktop() && this.btn.current === "idle") this.btn.setState("hover");
1347
- };
1348
- onOut = () => {
1349
- if (this.btn.current === "hover") this.btn.setState("idle");
1350
- };
1351
1409
  onDown = () => {
1352
1410
  if (this.btn.interactable) this.btn.setState("pressed");
1353
1411
  };
@@ -1376,6 +1434,7 @@ var ButtonView = class extends ControlView {
1376
1434
  const ok = this.btn.interactable;
1377
1435
  this.eventMode = ok ? "static" : "none";
1378
1436
  this.cursor = ok ? "pointer" : "default";
1437
+ this.alpha = ok ? 1 : 0.4;
1379
1438
  }
1380
1439
  redraw() {
1381
1440
  if (this.sprite) {
@@ -1386,31 +1445,37 @@ var ButtonView = class extends ControlView {
1386
1445
  const g = this.bg;
1387
1446
  g.clear();
1388
1447
  const disabled = this.btn.current === "disabled";
1389
- const fillC = this.mono ? "#ffffff" : t.color.surface;
1448
+ const fillC = this.dark ? "#0a0a0a" : this.mono ? "#ffffff" : t.color.surface;
1390
1449
  const lineC = this.mono ? "#0a0a0a" : t.color.accent;
1391
- const ink = this.mono ? disabled ? "#9aa0a6" : "#0a0a0a" : disabled ? t.color.textDim : t.color.text;
1450
+ const ink = this.dark ? "#ffffff" : this.mono ? disabled ? "#9aa0a6" : "#0a0a0a" : disabled ? t.color.textDim : t.color.text;
1451
+ this.glyphG.clear();
1452
+ this.glyphG.visible = false;
1392
1453
  if (this.shape === "circle") {
1393
1454
  const r = this.radius;
1394
- g.circle(0, 0, r).fill({ color: fillC, alpha: this.mono && disabled ? 0.6 : 1 });
1395
- g.circle(0, 0, r).stroke({ width: this.mono ? 5 : 4, color: lineC });
1396
- if (this.glyph === "menu") {
1397
- for (const dy of [-8, 0, 8]) g.moveTo(-13, dy).lineTo(13, dy).stroke({ width: 4, color: ink });
1455
+ if (this.mono) {
1456
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: disabled ? 0.08 : 0.18 });
1457
+ g.circle(0, 0, r).fill({ color: "#ffffff", alpha: disabled ? 0.6 : 1 });
1458
+ g.circle(0, 0, r * 0.925).stroke({ width: r * 0.15, color: "#0a0a0a", alpha: disabled ? 0.5 : 1 });
1459
+ } else if (this.dark) {
1460
+ g.circle(0, r * 0.16, r).fill({ color: 0, alpha: 0.18 });
1461
+ g.circle(0, 0, r).fill({ color: "#0a0a0a" });
1462
+ } else {
1463
+ g.circle(0, 0, r).fill({ color: fillC }).circle(0, 0, r).stroke({ width: 4, color: lineC });
1464
+ }
1465
+ const svg = GLYPH_SVG[this.glyph];
1466
+ if (svg) {
1467
+ const s = r / 15;
1468
+ this.glyphG.svg(svg);
1469
+ this.glyphG.scale.set(s);
1470
+ this.glyphG.position.set(-11.25 * s, -11.25 * s);
1471
+ this.glyphG.alpha = disabled ? 0.5 : 1;
1472
+ this.glyphG.visible = true;
1473
+ } else if (this.glyph === "menu") {
1474
+ const barW = this.dark ? 6 : 4;
1475
+ const spread = this.dark ? 9 : 8;
1476
+ for (const dy of [-spread, 0, spread]) g.moveTo(-14, dy).lineTo(14, dy).stroke({ width: barW, color: ink, cap: "round" });
1398
1477
  } else if (this.glyph === "close") {
1399
1478
  g.moveTo(-10, -10).lineTo(10, 10).moveTo(10, -10).lineTo(-10, 10).stroke({ width: 4, color: ink });
1400
- } else if (this.glyph === "speaker" || this.glyph === "speaker-mute") {
1401
- g.poly([-12, -4, -6, -4, 1, -11, 1, 11, -6, 4, -12, 4]).fill({ color: ink });
1402
- if (this.glyph === "speaker") {
1403
- g.moveTo(6, -6).lineTo(11, 0).lineTo(6, 6).stroke({ width: 2.5, color: ink });
1404
- g.moveTo(11, -9).lineTo(17, 0).lineTo(11, 9).stroke({ width: 2.5, color: ink });
1405
- } else {
1406
- g.moveTo(7, -6).lineTo(17, 6).moveTo(17, -6).lineTo(7, 6).stroke({ width: 2.5, color: ink });
1407
- }
1408
- } else if (this.glyph === "fullscreen") {
1409
- const a = 13, b = 6;
1410
- 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 });
1411
- } else if (this.glyph === "fullscreen-exit") {
1412
- const c = 4, d = 12;
1413
- 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 });
1414
1479
  }
1415
1480
  } else {
1416
1481
  const w = this.labelText ? this.labelText.width + 48 : 120;
@@ -1655,24 +1720,30 @@ var AutoplayView = class extends ControlView {
1655
1720
  }
1656
1721
  drawButton() {
1657
1722
  const st = this.auto.current;
1658
- this.countText.visible = st === "active";
1659
- if (st === "active") this.countText.text = this.fmt(this.auto.count.get());
1660
- if (st === "active") {
1661
- if (this.sprite) this.sprite.visible = false;
1662
- const r = this.sprite ? this.target / 2 : this.radius;
1663
- this.bg.clear();
1664
- 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 });
1723
+ this.countText.visible = false;
1724
+ const th = this.ui.theme;
1725
+ const r = this.sprite ? this.target / 2 : this.radius;
1726
+ const active = st === "active";
1727
+ this.eventMode = active ? "none" : "static";
1728
+ this.cursor = active ? "default" : "pointer";
1729
+ if (active) {
1730
+ this.bg.clear().circle(0, 0, r).fill({ color: th.color.accent }).stroke({ width: 4, color: th.color.accentText });
1731
+ if (this.sprite) {
1732
+ this.sprite.visible = true;
1733
+ this.sprite.tint = th.color.accentText;
1734
+ this.sprite.alpha = 0.55;
1735
+ }
1665
1736
  return;
1666
1737
  }
1667
1738
  if (this.sprite) {
1668
1739
  this.sprite.visible = true;
1740
+ this.sprite.tint = 16777215;
1669
1741
  this.bg.clear();
1670
1742
  const tex = this.idleTex ?? this.activeTex;
1671
1743
  if (tex) this.sprite.texture = tex;
1672
1744
  this.fit();
1673
1745
  this.sprite.alpha = st === "disabled" ? 0.5 : 1;
1674
1746
  } else {
1675
- const th = this.ui.theme;
1676
1747
  this.bg.clear();
1677
1748
  this.bg.circle(0, 0, this.radius).fill({ color: th.color.surface }).stroke({ width: 4, color: th.color.accent });
1678
1749
  }
@@ -1843,8 +1914,8 @@ var AutoplayDrawerView = class extends pixi_js.Container {
1843
1914
  this.startBtn.position.set(W / 2, startY);
1844
1915
  this.startBtn.hitArea = new pixi_js.Rectangle(-startW / 2, -startH / 2, startW, startH);
1845
1916
  this.sheetH = startY + startH / 2 + 20 * k;
1846
- this.sheetBg.clear().rect(0, 0, W, this.sheetH + 40).fill({ color: DARK });
1847
- this.handle.clear().rect(W / 2 - 30 * k, 13 * k, 60 * k, 5 * k).fill({ color: DIM });
1917
+ this.sheetBg.clear().rect(-1, 0, W + 2, this.sheetH + 40 + 600).fill({ color: DARK });
1918
+ this.handle.clear().roundRect(W / 2 - 30 * k, 13 * k, 60 * k, 5 * k, 2.5 * k).fill({ color: DIM });
1848
1919
  return this.sheetH + 40;
1849
1920
  }
1850
1921
  drawGroup(g) {
@@ -2908,7 +2979,27 @@ var ReadoutView = class extends ControlView {
2908
2979
  this.ticker = ticker;
2909
2980
  const t = ui.theme;
2910
2981
  const inline = opts.inline ?? false;
2911
- const fill = opts.mono ? "#ffffff" : t.color.text;
2982
+ this.prefix = opts.prefix ?? false;
2983
+ const fill = opts.fill ?? (opts.mono ? "#ffffff" : t.color.text);
2984
+ if (this.prefix) {
2985
+ this.valueText = new pixi_js.Text({ text: "", style: { fontFamily: t.type.family, fontSize: 12, fill: opts.fill ?? "#ffffff", fontWeight: "400" } });
2986
+ this.valueText.alpha = opts.fill ? 1 : 0.5;
2987
+ this.valueText.anchor.set(0, 0);
2988
+ this.addChild(this.valueText);
2989
+ this.disposers.push(
2990
+ this.ro.value.subscribe(() => this.render()),
2991
+ this.ui.locale.subscribe(() => {
2992
+ if (!this.destroyed) this.render();
2993
+ })
2994
+ );
2995
+ if (this.ro.currency) this.disposers.push(this.ro.currency.subscribe(() => this.render()));
2996
+ if (this.ro.kind === "duration") {
2997
+ this.tick = (tk) => this.ro.tick(tk.deltaMS / 1e3);
2998
+ this.ticker.add(this.tick);
2999
+ }
3000
+ this.render();
3001
+ return;
3002
+ }
2912
3003
  if (ro.label) {
2913
3004
  this.caption = new pixi_js.Text({
2914
3005
  text: ui.t(ro.label).toUpperCase(),
@@ -2951,6 +3042,7 @@ var ReadoutView = class extends ControlView {
2951
3042
  caption;
2952
3043
  valueText;
2953
3044
  tick;
3045
+ prefix;
2954
3046
  render() {
2955
3047
  const v = this.ro.value.get();
2956
3048
  let text;
@@ -2967,7 +3059,7 @@ var ReadoutView = class extends ControlView {
2967
3059
  default:
2968
3060
  text = String(v);
2969
3061
  }
2970
- this.valueText.text = text;
3062
+ this.valueText.text = this.prefix && this.ro.label ? `${this.ui.t(this.ro.label)}: ${text}` : text;
2971
3063
  }
2972
3064
  dispose() {
2973
3065
  if (this.tick) this.ticker.remove(this.tick);
@@ -2975,9 +3067,18 @@ var ReadoutView = class extends ControlView {
2975
3067
  }
2976
3068
  };
2977
3069
  var INSET2 = 24;
2978
- var BTN_H = 50;
2979
- var BTN_GAP = 12;
3070
+ var BTN_H = 52;
3071
+ var BTN_GAP = 14;
2980
3072
  var ROW_GAP = 16;
3073
+ var LIGHT = {
3074
+ surface: "#ffffff",
3075
+ surfaceAlt: "#eef1f6",
3076
+ text: "#181b20",
3077
+ textDim: "#5b6472",
3078
+ border: "#000000",
3079
+ primary: "#0a0a0a",
3080
+ primaryText: "#ffffff"
3081
+ };
2981
3082
  var DialogView = class extends ControlView {
2982
3083
  constructor(panel, blocks, actions, ui, ticker, opts = {}) {
2983
3084
  super(panel, ui);
@@ -2988,6 +3089,8 @@ var DialogView = class extends ControlView {
2988
3089
  this.opts = opts;
2989
3090
  this.zIndex = 130;
2990
3091
  this.maxWidth = opts.maxWidth ?? 520;
3092
+ const lightTheme = { ...ui.theme, color: { ...ui.theme.color, surface: LIGHT.surface, surfaceAlt: LIGHT.surfaceAlt, text: LIGHT.text, textDim: LIGHT.textDim } };
3093
+ this.lightUi = new Proxy(ui, { get: (t, p) => p === "theme" ? lightTheme : Reflect.get(t, p) });
2991
3094
  this.backdrop.eventMode = "static";
2992
3095
  this.backdrop.on("pointertap", () => {
2993
3096
  if (!this.ui.noticeBlocking.get()) this.panel.closePanel();
@@ -3027,11 +3130,13 @@ var DialogView = class extends ControlView {
3027
3130
  childViews = [];
3028
3131
  screen;
3029
3132
  maxWidth;
3133
+ /** `ui` proxy with a light theme — so the shared block renderer draws dark-on-white
3134
+ * content inside the white modal card (Figma "default" dialog look). */
3135
+ lightUi;
3030
3136
  buildClose() {
3031
- const t = this.ui.theme;
3032
- const r = 18;
3033
- const bg = new pixi_js.Graphics().circle(0, 0, r).fill({ color: t.color.surfaceAlt }).stroke({ width: 2, color: t.color.textDim });
3034
- const x = new pixi_js.Graphics().moveTo(-6, -6).lineTo(6, 6).moveTo(6, -6).lineTo(-6, 6).stroke({ width: 3, color: t.color.text });
3137
+ const r = 22;
3138
+ const bg = new pixi_js.Graphics().circle(0, 0, r).fill({ color: LIGHT.primary });
3139
+ const x = new pixi_js.Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
3035
3140
  this.closeBtn.addChild(bg, x);
3036
3141
  this.closeBtn.eventMode = "static";
3037
3142
  this.closeBtn.cursor = "pointer";
@@ -3050,13 +3155,12 @@ var DialogView = class extends ControlView {
3050
3155
  if (!this.panel.isOpen) return;
3051
3156
  const W = s.width;
3052
3157
  const H = s.height;
3053
- const t = this.ui.theme;
3054
3158
  for (const v of this.childViews) v.dispose();
3055
3159
  this.childViews.length = 0;
3056
3160
  for (const ch of this.content.removeChildren()) ch.destroy();
3057
3161
  const cardW = Math.min(W - 48, this.maxWidth);
3058
3162
  const innerW = cardW - INSET2 * 2;
3059
- const col = buildBlockColumn(this.blocks.get(), [], this.ui, this.ticker, innerW, { controlSkins: this.opts.controlSkins });
3163
+ const col = buildBlockColumn(this.blocks.get(), [], this.lightUi, this.ticker, innerW, { controlSkins: this.opts.controlSkins });
3060
3164
  this.childViews = col.views;
3061
3165
  const kids = col.content.removeChildren();
3062
3166
  if (kids.length) this.content.addChild(...kids);
@@ -3066,15 +3170,15 @@ var DialogView = class extends ControlView {
3066
3170
  const cardH = Math.min(H - 48, wantH);
3067
3171
  const cx = (W - cardW) / 2;
3068
3172
  const cy = (H - cardH) / 2;
3069
- this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.62 });
3173
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: 0, alpha: 0.5 });
3070
3174
  this.backdrop.hitArea = new pixi_js.Rectangle(0, 0, W, H);
3071
- this.card.clear().roundRect(cx, cy, cardW, cardH, t.radius.card).fill({ color: t.color.surface }).stroke({ width: 2, color: t.color.accent });
3175
+ this.card.clear().roundRect(cx, cy, cardW, cardH, 14).fill({ color: LIGHT.surface }).stroke({ width: 2.5, color: LIGHT.border });
3072
3176
  const bodyH = cardH - (buttonsH ? ROW_GAP + buttonsH + INSET2 : INSET2);
3073
3177
  this.content.x = cx + cardW / 2;
3074
3178
  this.content.y = cy;
3075
3179
  this.maskG.clear().rect(cx, cy, cardW, Math.max(0, bodyH)).fill({ color: 16777215 });
3076
3180
  this.buttons.position.set(cx + cardW / 2, cy + cardH - INSET2 / 2 - buttonsH / 2);
3077
- this.closeBtn.position.set(cx + cardW - 20, cy + 20);
3181
+ this.closeBtn.position.set(cx + cardW - 8, cy + 8);
3078
3182
  }
3079
3183
  /** Build the footer buttons from `noticeActions` into a centered row (shrunk to
3080
3184
  * fit if needed). Returns the row height (0 when there are no actions). */
@@ -3107,7 +3211,7 @@ var DialogView = class extends ControlView {
3107
3211
  const bg = new pixi_js.Graphics();
3108
3212
  const label = new pixi_js.Text({
3109
3213
  text: this.ui.t(action.label),
3110
- style: { fontFamily: t.type.family, fontSize: 18, fontWeight: "800", fill: variant === "primary" ? t.color.accentText : t.color.text, letterSpacing: 0.5 }
3214
+ style: { fontFamily: t.type.family, fontSize: 18, fontWeight: "800", fill: variant === "primary" ? LIGHT.primaryText : LIGHT.text, letterSpacing: 0.5 }
3111
3215
  });
3112
3216
  label.anchor.set(0.5);
3113
3217
  node.addChild(bg, label);
@@ -3124,12 +3228,11 @@ var DialogView = class extends ControlView {
3124
3228
  return { node, bg, width, variant };
3125
3229
  }
3126
3230
  drawButton(bg, variant, width) {
3127
- const t = this.ui.theme;
3128
3231
  bg.clear();
3129
3232
  if (variant === "primary") {
3130
- bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: t.color.accent });
3233
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT.primary });
3131
3234
  } else {
3132
- 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 });
3235
+ bg.roundRect(-width / 2, -BTN_H / 2, width, BTN_H, BTN_H / 2).fill({ color: LIGHT.surface }).stroke({ width: 2.5, color: LIGHT.border });
3133
3236
  }
3134
3237
  }
3135
3238
  applyOpen(open) {
@@ -3144,73 +3247,6 @@ var DialogView = class extends ControlView {
3144
3247
  super.dispose();
3145
3248
  }
3146
3249
  };
3147
- var BAR_H = 26;
3148
- var StatusBarView = class _StatusBarView extends pixi_js.Container {
3149
- constructor(controls, ui, ticker, side) {
3150
- super();
3151
- this.ui = ui;
3152
- this.side = side;
3153
- this.zIndex = 300;
3154
- this.addChild(this.bg);
3155
- for (const c of controls) {
3156
- const view = new ReadoutView(c, ui, ticker, { inline: true, mono: true });
3157
- this.addChild(view);
3158
- this.items.push({ id: c.id, view });
3159
- }
3160
- this.disposers.push(
3161
- this.ui.on("visibilityChanged", ({ id }) => {
3162
- if (this.items.some((it) => it.id === id)) this.relayout();
3163
- })
3164
- );
3165
- }
3166
- ui;
3167
- side;
3168
- bg = new pixi_js.Graphics();
3169
- items = [];
3170
- screen;
3171
- disposers = [];
3172
- /** The strip's pixel height for the current screen (used for the HUD margin too). */
3173
- static heightFor(screen) {
3174
- const scale = Math.max(0.7, Math.min(1.4, screen.scale));
3175
- return Math.round(BAR_H * scale);
3176
- }
3177
- applyLayout(screen) {
3178
- this.screen = screen;
3179
- this.relayout();
3180
- }
3181
- relayout() {
3182
- const s = this.screen;
3183
- if (!s) return;
3184
- const W = s.width;
3185
- const H = s.height;
3186
- const scale = Math.max(0.7, Math.min(1.4, s.scale));
3187
- const barH = _StatusBarView.heightFor(s);
3188
- const y = this.side === "top" ? 0 : H - barH;
3189
- this.position.set(0, 0);
3190
- const visible = this.items.filter((it) => !this.ui.hidden.has(it.id));
3191
- for (const it of this.items) it.view.visible = !this.ui.hidden.has(it.id);
3192
- this.bg.clear();
3193
- this.visible = visible.length > 0;
3194
- if (!visible.length) return;
3195
- const edgeY = this.side === "top" ? barH - 1 : y;
3196
- this.bg.rect(0, y, W, barH).fill({ color: 789776, alpha: 0.92 }).rect(0, edgeY, W, 1).fill({ color: 16777215, alpha: 0.18 });
3197
- const n = visible.length;
3198
- const cy = y + barH / 2;
3199
- const pad = 18 * scale;
3200
- const usableW = Math.max(160, W - pad * 2);
3201
- visible.forEach((it, i) => {
3202
- it.view.position.set(pad + usableW * ((i + 0.5) / n), cy);
3203
- it.view.scale.set(scale);
3204
- });
3205
- }
3206
- dispose() {
3207
- for (const it of this.items) it.view.dispose();
3208
- this.items.length = 0;
3209
- for (const d of this.disposers) d();
3210
- this.disposers.length = 0;
3211
- if (!this.destroyed) this.destroy({ children: true });
3212
- }
3213
- };
3214
3250
 
3215
3251
  // src/OpenUIPixi.ts
3216
3252
  function easeInOutCubic(p) {
@@ -3263,8 +3299,8 @@ var OpenUIPixi = class {
3263
3299
  glyph: "menu",
3264
3300
  iconTexture: ic.settingsIdle,
3265
3301
  iconTarget: 88,
3266
- mono: true
3267
- // b&w like the turbo button (used on the drawn glyph path)
3302
+ dark: true
3303
+ // Figma ☰: solid black circle, white bars, no ring
3268
3304
  });
3269
3305
  settingsView.zIndex = 5;
3270
3306
  const menuView = this.opts.menu === false ? void 0 : this.buildMenu(ticker);
@@ -3272,7 +3308,7 @@ var OpenUIPixi = class {
3272
3308
  offTexture: ic.turboOff,
3273
3309
  onTexture: ic.turboOn,
3274
3310
  modeTextures: ic.turboModes,
3275
- target: 88
3311
+ target: 79
3276
3312
  });
3277
3313
  const spinOff = this.ui.spin.layout.offset ?? [0, 0];
3278
3314
  const autoOff = this.ui.autoplay.layout.offset ?? [0, 0];
@@ -3280,23 +3316,23 @@ var OpenUIPixi = class {
3280
3316
  const autoplayView = new AutoplayView(this.ui.autoplay, this.ui, ticker, {
3281
3317
  idleTexture: ic.autoIdle,
3282
3318
  activeTexture: ic.autoActive,
3283
- target: 88,
3319
+ target: 73,
3284
3320
  picker,
3285
3321
  arcCenter: { x: spinOff[0] - autoOff[0], y: spinOff[1] - autoOff[1] },
3286
3322
  arcStepDeg: 22
3287
3323
  });
3288
3324
  autoplayView.zIndex = 20;
3289
- const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 44, iconTexture: ic.bonus, iconTarget: 110 });
3325
+ const bonusView = new ButtonView(this.ui.bonusButton, this.ui, ticker, { shape: "circle", radius: 60, iconTexture: ic.bonus, iconTarget: 130 });
3290
3326
  const betPlusView = new ButtonView(this.ui.betPlus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betPlus, iconTarget: 64 });
3291
3327
  const betMinusView = new ButtonView(this.ui.betMinus, this.ui, ticker, { shape: "circle", radius: 30, iconTexture: ic.betMinus, iconTarget: 64 });
3292
- const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 22, glyph: "speaker", iconTarget: 44, mono: true });
3293
- const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 22, glyph: "fullscreen", iconTarget: 44, mono: true });
3328
+ const muteView = new ButtonView(this.ui.muteButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "speaker", iconTarget: 60, mono: true });
3329
+ const fullscreenView = new ButtonView(this.ui.fullscreenButton, this.ui, ticker, { shape: "circle", radius: 30, glyph: "fullscreen", iconTarget: 60, mono: true });
3294
3330
  muteView.zIndex = 65;
3295
3331
  fullscreenView.zIndex = 65;
3296
- const statusBarSide = this.opts.statusBar;
3297
- const rtpView = statusBarSide ? void 0 : new ReadoutView(this.ui.rtp, this.ui, ticker);
3298
- const netView = statusBarSide ? void 0 : new ReadoutView(this.ui.netPosition, this.ui, ticker);
3299
- const timerView = statusBarSide ? void 0 : new ReadoutView(this.ui.sessionTimer, this.ui, ticker);
3332
+ const roOpts = { prefix: true, fill: this.opts.readoutColor };
3333
+ const rtpView = new ReadoutView(this.ui.rtp, this.ui, ticker, roOpts);
3334
+ const netView = new ReadoutView(this.ui.netPosition, this.ui, ticker, roOpts);
3335
+ const timerView = new ReadoutView(this.ui.sessionTimer, this.ui, ticker, roOpts);
3300
3336
  const entries = [
3301
3337
  [this.ui.spin.id, spinView],
3302
3338
  [this.ui.balance.id, balanceView],
@@ -3310,9 +3346,9 @@ var OpenUIPixi = class {
3310
3346
  [this.ui.muteButton.id, muteView],
3311
3347
  [this.ui.fullscreenButton.id, fullscreenView]
3312
3348
  ];
3313
- if (rtpView) entries.push([this.ui.rtp.id, rtpView]);
3314
- if (netView) entries.push([this.ui.netPosition.id, netView]);
3315
- if (timerView) entries.push([this.ui.sessionTimer.id, timerView]);
3349
+ entries.push([this.ui.rtp.id, rtpView]);
3350
+ entries.push([this.ui.netPosition.id, netView]);
3351
+ entries.push([this.ui.sessionTimer.id, timerView]);
3316
3352
  const viewById = /* @__PURE__ */ new Map();
3317
3353
  const idByView = /* @__PURE__ */ new Map();
3318
3354
  for (const [id, view] of entries) {
@@ -3373,11 +3409,22 @@ var OpenUIPixi = class {
3373
3409
  const dialog = new DialogView(this.ui.noticePanel, this.ui.noticeBlocks, this.ui.noticeActions, this.ui, ticker, { controlSkins: this.opts.controlSkins });
3374
3410
  this.root.addChild(dialog);
3375
3411
  this.overlays.push(dialog);
3376
- if (statusBarSide) {
3377
- const bar = new StatusBarView([this.ui.netPosition, this.ui.rtp, this.ui.sessionTimer], this.ui, ticker, statusBarSide);
3378
- this.root.addChild(bar);
3379
- this.overlays.push(bar);
3380
- }
3412
+ const vignette = new pixi_js.Graphics();
3413
+ vignette.zIndex = -1;
3414
+ this.root.addChild(vignette);
3415
+ this.overlays.push({
3416
+ applyLayout: (s) => {
3417
+ const r = 200 * s.scale;
3418
+ vignette.clear();
3419
+ for (let i = 0; i < 14; i++) {
3420
+ const t = i / 14;
3421
+ vignette.circle(0, 0, r * (1 - t)).fill({ color: 0, alpha: 0.5 / 14 });
3422
+ }
3423
+ },
3424
+ dispose: () => {
3425
+ if (!vignette.destroyed) vignette.destroy();
3426
+ }
3427
+ });
3381
3428
  const replayBadge = new pixi_js.Container();
3382
3429
  const replayBg = new pixi_js.Graphics();
3383
3430
  const replayText = new pixi_js.Text({ text: this.ui.t("openui.replay").toUpperCase(), style: { fontFamily: this.ui.theme.type.family, fontSize: 16, fontWeight: "800", fill: 16777215, letterSpacing: 2 } });
@@ -3391,8 +3438,7 @@ var OpenUIPixi = class {
3391
3438
  const w = replayText.width + 36;
3392
3439
  const h = 32;
3393
3440
  replayBg.clear().roundRect(-w / 2, -h / 2, w, h, h / 2).fill({ color: 789776 }).stroke({ width: 2, color: 16777215 });
3394
- const top = statusBarSide === "top" ? StatusBarView.heightFor(s) + 28 : 40;
3395
- replayBadge.position.set(s.width / 2, top);
3441
+ replayBadge.position.set(s.width / 2, 40);
3396
3442
  },
3397
3443
  dispose: () => {
3398
3444
  if (!replayBadge.destroyed) replayBadge.destroy({ children: true });
@@ -3406,33 +3452,13 @@ var OpenUIPixi = class {
3406
3452
  if (!replayBadge.destroyed) replayText.text = this.ui.t("openui.replay").toUpperCase();
3407
3453
  })
3408
3454
  );
3409
- if (ic.settingsIdle && ic.settingsActive) {
3410
- const idle = ic.settingsIdle;
3411
- const active = ic.settingsActive;
3412
- this.disposers.push(
3413
- this.ui.settingsPanel.state.subscribe(() => {
3414
- settingsView.setIconTexture(this.ui.settingsPanel.isOpen ? active : idle);
3415
- })
3416
- );
3417
- } else {
3418
- this.disposers.push(
3419
- this.ui.settingsPanel.state.subscribe(() => {
3420
- settingsView.setGlyph(this.ui.settingsPanel.isOpen ? "close" : "menu");
3421
- })
3422
- );
3423
- }
3424
3455
  const applyLayout = () => {
3425
3456
  const screen = this.ui.screen.get();
3426
- const barH = statusBarSide ? StatusBarView.heightFor(screen) : 0;
3427
3457
  this.slideBaseY.clear();
3428
3458
  this.slideSign.clear();
3429
3459
  for (const v of this.views) {
3430
3460
  v.applyLayout(screen);
3431
3461
  const anchor = this.ui.control(idByView.get(v) ?? "")?.layout.anchor ?? "bottom-center";
3432
- if (barH) {
3433
- if (statusBarSide === "top" && anchor.startsWith("top")) v.y += barH;
3434
- else if (statusBarSide === "bottom" && anchor.startsWith("bottom")) v.y -= barH;
3435
- }
3436
3462
  this.slideBaseY.set(v, v.y);
3437
3463
  this.slideSign.set(v, anchor.startsWith("top") ? -1 : 1);
3438
3464
  }
@@ -3845,7 +3871,7 @@ function mountHud(app, spec = {}, opts = {}) {
3845
3871
  if (menu && spec.game && (spec.game.name || spec.game.version)) {
3846
3872
  menu.push({ kind: "legal", id: "openui-game-info", text: [spec.game.name, spec.game.version ? `v${spec.game.version}` : ""].filter(Boolean).join(" \xB7 ") });
3847
3873
  }
3848
- const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu, statusBar: pixiOpts.statusBar ?? spec.statusBar });
3874
+ const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu });
3849
3875
  pixi.mount(app);
3850
3876
  const extra = [];
3851
3877
  if (spec.panels?.length) {
@@ -3965,7 +3991,6 @@ exports.ReadoutView = ReadoutView;
3965
3991
  exports.SelectView = SelectView;
3966
3992
  exports.SliderView = SliderView;
3967
3993
  exports.SpinView = SpinView;
3968
- exports.StatusBarView = StatusBarView;
3969
3994
  exports.StepperView = StepperView;
3970
3995
  exports.TextCellRenderer = TextCellRenderer;
3971
3996
  exports.ToggleView = ToggleView;