@open-slot-ui/core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -252,6 +252,15 @@ interface LayoutSpec {
252
252
  scale?: number;
253
253
  /** Rotation in degrees, clockwise. Default 0. */
254
254
  rotation?: number;
255
+ /**
256
+ * Which rect the anchor resolves against. `'stage'` (default) = the uniformly-scaled
257
+ * design FRAME, so the HUD scales as one homogeneous unit and inter-control gaps never
258
+ * change with aspect ratio. `'screen'` = the RAW viewport, so a control hugs the true
259
+ * screen edge even when the frame is letterboxed off it — for the corner utility buttons
260
+ * (mute / fullscreen / RTP / timers), which must sit in the viewport corner at every
261
+ * aspect (otherwise a bottom-anchored letterboxed frame drops a "top" control mid-screen).
262
+ */
263
+ origin?: 'stage' | 'screen';
255
264
  }
256
265
  interface Placement {
257
266
  x: number;
@@ -1747,6 +1756,12 @@ type BlockSpec = {
1747
1756
  payouts: string;
1748
1757
  icon?: string;
1749
1758
  }>;
1759
+ } | {
1760
+ kind: 'paylines';
1761
+ id: string;
1762
+ reels: number;
1763
+ rows: number;
1764
+ lines: number[][];
1750
1765
  } | {
1751
1766
  kind: 'image';
1752
1767
  id: string;
@@ -1786,7 +1801,7 @@ type BlockSpec = {
1786
1801
  children: BlockSpec[];
1787
1802
  });
1788
1803
  /** All block kinds, for runtime validation of untyped host data. */
1789
- declare const BLOCK_KINDS: readonly ["slider", "toggle", "button", "select", "stepper", "value", "text", "heading", "subheading", "callout", "stat-grid", "mode-stats", "steps", "table", "paytable", "image", "media", "cards", "legal", "divider", "group"];
1804
+ declare const BLOCK_KINDS: readonly ["slider", "toggle", "button", "select", "stepper", "value", "text", "heading", "subheading", "callout", "stat-grid", "mode-stats", "steps", "table", "paytable", "paylines", "image", "media", "cards", "legal", "divider", "group"];
1790
1805
  interface PanelSpec {
1791
1806
  id: string;
1792
1807
  variant: PanelVariant;
package/dist/index.d.ts CHANGED
@@ -252,6 +252,15 @@ interface LayoutSpec {
252
252
  scale?: number;
253
253
  /** Rotation in degrees, clockwise. Default 0. */
254
254
  rotation?: number;
255
+ /**
256
+ * Which rect the anchor resolves against. `'stage'` (default) = the uniformly-scaled
257
+ * design FRAME, so the HUD scales as one homogeneous unit and inter-control gaps never
258
+ * change with aspect ratio. `'screen'` = the RAW viewport, so a control hugs the true
259
+ * screen edge even when the frame is letterboxed off it — for the corner utility buttons
260
+ * (mute / fullscreen / RTP / timers), which must sit in the viewport corner at every
261
+ * aspect (otherwise a bottom-anchored letterboxed frame drops a "top" control mid-screen).
262
+ */
263
+ origin?: 'stage' | 'screen';
255
264
  }
256
265
  interface Placement {
257
266
  x: number;
@@ -1747,6 +1756,12 @@ type BlockSpec = {
1747
1756
  payouts: string;
1748
1757
  icon?: string;
1749
1758
  }>;
1759
+ } | {
1760
+ kind: 'paylines';
1761
+ id: string;
1762
+ reels: number;
1763
+ rows: number;
1764
+ lines: number[][];
1750
1765
  } | {
1751
1766
  kind: 'image';
1752
1767
  id: string;
@@ -1786,7 +1801,7 @@ type BlockSpec = {
1786
1801
  children: BlockSpec[];
1787
1802
  });
1788
1803
  /** All block kinds, for runtime validation of untyped host data. */
1789
- declare const BLOCK_KINDS: readonly ["slider", "toggle", "button", "select", "stepper", "value", "text", "heading", "subheading", "callout", "stat-grid", "mode-stats", "steps", "table", "paytable", "image", "media", "cards", "legal", "divider", "group"];
1804
+ declare const BLOCK_KINDS: readonly ["slider", "toggle", "button", "select", "stepper", "value", "text", "heading", "subheading", "callout", "stat-grid", "mode-stats", "steps", "table", "paytable", "paylines", "image", "media", "cards", "legal", "divider", "group"];
1790
1805
  interface PanelSpec {
1791
1806
  id: string;
1792
1807
  variant: PanelVariant;
package/dist/index.js CHANGED
@@ -131,10 +131,10 @@ function anchorFactors(anchor) {
131
131
  function resolvePlacement(spec, screen) {
132
132
  const [ax, ay] = anchorFactors(spec.anchor);
133
133
  const [ox, oy] = spec.offset ?? [0, 0];
134
- const st = screen.stage;
134
+ const rect = spec.origin === "screen" ? { x: 0, y: 0, width: screen.width, height: screen.height } : screen.stage;
135
135
  return {
136
- x: st.x + ax * st.width + ox * screen.scale,
137
- y: st.y + ay * st.height + oy * screen.scale,
136
+ x: rect.x + ax * rect.width + ox * screen.scale,
137
+ y: rect.y + ay * rect.height + oy * screen.scale,
138
138
  scale: screen.scale * (spec.scale ?? 1),
139
139
  rotation: (spec.rotation ?? 0) * Math.PI / 180
140
140
  };
@@ -158,11 +158,13 @@ var landscapeDefaultLayouts = Object.freeze({
158
158
  // Right-anchored so a long localized caption grows LEFT and never clips the edge.
159
159
  "total-win": { anchor: "bottom-right", offset: [-150, -300] },
160
160
  settings: { anchor: "bottom-center", offset: [-740, -320], rotation: -5 },
161
- mute: { anchor: "top-right", offset: [-122, 46] },
162
- fullscreen: { anchor: "top-right", offset: [-46, 46] },
163
- rtp: { anchor: "top-left", offset: [20, 18] },
164
- "session-timer": { anchor: "top-left", offset: [20, 46] },
165
- "net-position": { anchor: "top-left", offset: [20, 74] }
161
+ // Corner utilities anchor to the SCREEN (not the letterboxed frame) so they hug the true
162
+ // viewport corners at every aspect see `LayoutSpec.origin`.
163
+ mute: { anchor: "top-right", offset: [-122, 46], origin: "screen" },
164
+ fullscreen: { anchor: "top-right", offset: [-46, 46], origin: "screen" },
165
+ rtp: { anchor: "top-left", offset: [20, 18], origin: "screen" },
166
+ "session-timer": { anchor: "top-left", offset: [20, 46], origin: "screen" },
167
+ "net-position": { anchor: "top-left", offset: [20, 74], origin: "screen" }
166
168
  });
167
169
  var portraitDefaultLayouts = Object.freeze({
168
170
  // Mobile sizes run ~1.5× the desktop base (the Figma mobile frame draws the
@@ -177,19 +179,21 @@ var portraitDefaultLayouts = Object.freeze({
177
179
  settings: { anchor: "bottom-center", offset: [438, -480], scale: 1.5, rotation: -5 },
178
180
  "bet-minus": { anchor: "bottom-center", offset: [-105, -267], scale: 1.5 },
179
181
  "bet-plus": { anchor: "bottom-center", offset: [105, -267], scale: 1.5 },
180
- mute: { anchor: "top-right", offset: [-159, 57], scale: 1.5 },
181
- fullscreen: { anchor: "top-right", offset: [-57, 57], scale: 1.5 },
182
+ // Corner utilities anchor to the SCREEN (see `LayoutSpec.origin`) so they stay in the true
183
+ // viewport corner even when the portrait frame is letterboxed.
184
+ mute: { anchor: "top-right", offset: [-159, 57], scale: 1.5, origin: "screen" },
185
+ fullscreen: { anchor: "top-right", offset: [-57, 57], scale: 1.5, origin: "screen" },
182
186
  // Compliance readouts scale up ~2× on mobile with wider line spacing.
183
- rtp: { anchor: "top-left", offset: [16, 16], scale: 2 },
184
- "session-timer": { anchor: "top-left", offset: [16, 66], scale: 2 },
185
- "net-position": { anchor: "top-left", offset: [16, 116], scale: 2 },
187
+ rtp: { anchor: "top-left", offset: [16, 16], scale: 2, origin: "screen" },
188
+ "session-timer": { anchor: "top-left", offset: [16, 66], scale: 2, origin: "screen" },
189
+ "net-position": { anchor: "top-left", offset: [16, 116], scale: 2, origin: "screen" },
186
190
  balance: { anchor: "bottom-left", offset: [36, -110], rotation: -5 },
187
191
  bet: { anchor: "bottom-right", offset: [-36, -110], rotation: 5 }
188
192
  });
189
193
  function defaultLayout(id) {
190
194
  const l = landscapeDefaultLayouts[id];
191
195
  if (!l) return { anchor: "center" };
192
- return { anchor: l.anchor, offset: l.offset ? [l.offset[0], l.offset[1]] : void 0, scale: l.scale, rotation: l.rotation };
196
+ return { anchor: l.anchor, offset: l.offset ? [l.offset[0], l.offset[1]] : void 0, scale: l.scale, rotation: l.rotation, origin: l.origin };
193
197
  }
194
198
 
195
199
  // src/transition/Transition.ts
@@ -1273,6 +1277,7 @@ var BLOCK_KINDS = [
1273
1277
  "steps",
1274
1278
  "table",
1275
1279
  "paytable",
1280
+ "paylines",
1276
1281
  "image",
1277
1282
  "media",
1278
1283
  "cards",
@@ -1434,6 +1439,10 @@ function validateSpec(spec) {
1434
1439
  if (b.kind === "paytable" && (!b.rows || b.rows.length === 0)) {
1435
1440
  add("warn", `${p}.rows`, "empty-paytable", "a paytable block has no rows");
1436
1441
  }
1442
+ if (b.kind === "paylines") {
1443
+ if (!(b.reels > 0) || !(b.rows > 0)) add("error", `${p}.reels`, "paylines-shape", "a paylines block needs reels > 0 and rows > 0");
1444
+ if (!b.lines || b.lines.length === 0) add("warn", `${p}.lines`, "empty-paylines", "a paylines block has no lines");
1445
+ }
1437
1446
  if (b.kind === "image" && (!b.src || !b.src.trim())) {
1438
1447
  add("error", `${p}.src`, "image-src", "an image block needs a src");
1439
1448
  }
@@ -2457,7 +2466,7 @@ var OpenUI = class {
2457
2466
 
2458
2467
  // src/spec/responsive.ts
2459
2468
  function cloneLayout(l) {
2460
- return { anchor: l.anchor, offset: l.offset ? [l.offset[0], l.offset[1]] : void 0, scale: l.scale, rotation: l.rotation };
2469
+ return { anchor: l.anchor, offset: l.offset ? [l.offset[0], l.offset[1]] : void 0, scale: l.scale, rotation: l.rotation, origin: l.origin };
2461
2470
  }
2462
2471
  function activeKeys(screen) {
2463
2472
  return [screen.orientation, screen.breakpoint];
@@ -2915,6 +2924,15 @@ function renderBlocksHtml(blocks, tr, facts) {
2915
2924
  case "paytable":
2916
2925
  out.push(`<div class="ohm-grid">${renderPaytable(b.rows, tr)}</div>`);
2917
2926
  break;
2927
+ case "paylines": {
2928
+ const masks = b.lines.map((line, i) => {
2929
+ const cells = [];
2930
+ for (let row = 0; row < b.rows; row++) for (let reel = 0; reel < b.reels; reel++) cells.push(`<i class="${line[reel] === row ? "on" : ""}"></i>`);
2931
+ return `<div class="ohm-line"><div class="ohm-linegrid" style="grid-template-columns:repeat(${b.reels},1fr)">${cells.join("")}</div><span>${i + 1}</span></div>`;
2932
+ }).join("");
2933
+ out.push(`<div class="ohm-lines">${masks}</div>`);
2934
+ break;
2935
+ }
2918
2936
  case "table": {
2919
2937
  const head = b.columns?.length ? `<thead><tr>${b.columns.map((c) => `<th>${esc(tr(c))}</th>`).join("")}</tr></thead>` : "";
2920
2938
  const body = b.rows.map((r) => `<tr>${r.map((c) => `<td>${esc(tr(c))}</td>`).join("")}</tr>`).join("");