@open-slot-ui/core 0.4.2 → 0.5.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
@@ -101,7 +101,9 @@ var defaultLayoutConfig = {
101
101
  refLandscape: [1920, 1080],
102
102
  refPortrait: [1080, 2337],
103
103
  portraitBelowAspect: 0.85,
104
- breakpoints: { mobile: 480, tablet: 840 }
104
+ breakpoints: { mobile: 480, tablet: 840 },
105
+ // Bottom bar hugs the screen bottom; extra height opens above (reel area).
106
+ stageAnchor: [0.5, 1]
105
107
  };
106
108
  function breakpointFor(width, height, cfg) {
107
109
  const short = Math.min(Math.max(width, 1), Math.max(height, 1));
@@ -115,7 +117,11 @@ function computeScreen(width, height, cfg) {
115
117
  const orientation = w / h < cfg.portraitBelowAspect ? "portrait" : "landscape";
116
118
  const [rw, rh] = orientation === "portrait" ? cfg.refPortrait : cfg.refLandscape;
117
119
  const scale = Math.min(w / rw, h / rh);
118
- return { width: w, height: h, orientation, breakpoint: breakpointFor(w, h, cfg), scale };
120
+ const sw = rw * scale;
121
+ const sh = rh * scale;
122
+ const [ax, ay] = cfg.stageAnchor ?? [0.5, 1];
123
+ const stage = { x: (w - sw) * ax, y: (h - sh) * ay, width: sw, height: sh };
124
+ return { width: w, height: h, orientation, breakpoint: breakpointFor(w, h, cfg), scale, stage };
119
125
  }
120
126
 
121
127
  // src/layout/anchor.ts
@@ -127,9 +133,10 @@ function anchorFactors(anchor) {
127
133
  function resolvePlacement(spec, screen) {
128
134
  const [ax, ay] = anchorFactors(spec.anchor);
129
135
  const [ox, oy] = spec.offset ?? [0, 0];
136
+ const st = screen.stage;
130
137
  return {
131
- x: ax * screen.width + ox * screen.scale,
132
- y: ay * screen.height + oy * screen.scale,
138
+ x: st.x + ax * st.width + ox * screen.scale,
139
+ y: st.y + ay * st.height + oy * screen.scale,
133
140
  scale: screen.scale * (spec.scale ?? 1),
134
141
  rotation: (spec.rotation ?? 0) * Math.PI / 180
135
142
  };
@@ -137,14 +144,21 @@ function resolvePlacement(spec, screen) {
137
144
 
138
145
  // src/layout/defaultLayouts.ts
139
146
  var landscapeDefaultLayouts = Object.freeze({
140
- spin: { anchor: "bottom-center", offset: [0, -140], scale: 0.9 },
141
- autoplay: { anchor: "bottom-center", offset: [-204, -112] },
142
- turbo: { anchor: "bottom-center", offset: [204, -112] },
147
+ // Measured 1:1 from the Figma "Desk DEF" frame (1920×1080): spin ~0.95 of the base
148
+ // 220px coin, dropped to −124, autoplay/turbo flanking at ±204. (The homogeneous
149
+ // stage keeps this off the reels, so it no longer needs shrinking.)
150
+ spin: { anchor: "bottom-center", offset: [0, -124], scale: 0.95 },
151
+ autoplay: { anchor: "bottom-center", offset: [-204, -100] },
152
+ turbo: { anchor: "bottom-center", offset: [204, -100] },
143
153
  balance: { anchor: "bottom-left", offset: [135, -104], rotation: -5 },
144
154
  bet: { anchor: "bottom-right", offset: [-186, -104], rotation: 5 },
145
155
  "bet-plus": { anchor: "bottom-right", offset: [-120, -188], rotation: 5 },
146
156
  "bet-minus": { anchor: "bottom-right", offset: [-120, -104], rotation: 5 },
147
157
  bonus: { anchor: "bottom-center", offset: [740, -320] },
158
+ // Bonus "total win" readout — occupies the buy-feature corner; the renderer swaps
159
+ // the two so only one shows at a time (buy in base play, total-win in a bonus round).
160
+ // Right-anchored so a long localized caption grows LEFT and never clips the edge.
161
+ "total-win": { anchor: "bottom-right", offset: [-150, -300] },
148
162
  settings: { anchor: "bottom-center", offset: [-740, -320], rotation: -5 },
149
163
  mute: { anchor: "top-right", offset: [-122, 46] },
150
164
  fullscreen: { anchor: "top-right", offset: [-46, 46] },
@@ -159,6 +173,9 @@ var portraitDefaultLayouts = Object.freeze({
159
173
  autoplay: { anchor: "bottom-center", offset: [-258, -510], scale: 1.5 },
160
174
  turbo: { anchor: "bottom-center", offset: [258, -510], scale: 1.5 },
161
175
  bonus: { anchor: "bottom-center", offset: [-438, -480], scale: 1.3 },
176
+ // Figma "Mobile FS": the total-win sits on the LEFT at the spin's level (where the
177
+ // buy coin lives in base play), left-aligned + tilted −10° — NOT centred over spin.
178
+ "total-win": { anchor: "bottom-left", offset: [30, -520], rotation: -10 },
162
179
  settings: { anchor: "bottom-center", offset: [438, -480], scale: 1.5, rotation: -5 },
163
180
  "bet-minus": { anchor: "bottom-center", offset: [-105, -267], scale: 1.5 },
164
181
  "bet-plus": { anchor: "bottom-center", offset: [105, -267], scale: 1.5 },
@@ -864,6 +881,10 @@ var ReadoutControl = class extends Control {
864
881
  value = new Signal(0);
865
882
  /** Whether a `'duration'` readout is currently advancing. */
866
883
  running = new Signal(false);
884
+ /** Accent emphasis: the renderer tints the value in the theme accent while on — for
885
+ * a MODIFIED readout (e.g. net position while a bet boost is active). Mirrors
886
+ * {@link ValueDisplay.emphasized}. */
887
+ emphasized = new Signal(false);
867
888
  kind;
868
889
  label;
869
890
  decimals;
@@ -883,6 +904,10 @@ var ReadoutControl = class extends Control {
883
904
  set(v) {
884
905
  this.value.set(safeAmount(v, this.value.get()));
885
906
  }
907
+ /** Accent-tint the value ("this readout is modified"). Strict boolean. */
908
+ setEmphasis(on) {
909
+ this.emphasized.set(on === true);
910
+ }
886
911
  get() {
887
912
  return this.value.get();
888
913
  }
@@ -1409,6 +1434,7 @@ var openuiDefaults = {
1409
1434
  "openui.bet": "Bet",
1410
1435
  "openui.balance": "Balance",
1411
1436
  "openui.win": "Win",
1437
+ "openui.totalWin": "Total Win",
1412
1438
  "openui.menu": "Menu",
1413
1439
  "openui.rules": "Rules",
1414
1440
  "openui.paytable": "Paytable",
@@ -1458,6 +1484,13 @@ var openuiSocialDefaults = {
1458
1484
  "openui.bet": "Play",
1459
1485
  "openui.balance": "Balance",
1460
1486
  "openui.win": "Prize",
1487
+ "openui.totalWin": "Total Prize",
1488
+ // "Paytable" (contains "pay") and "Insufficient funds" ("funds") are restricted in
1489
+ // social mode → their compliant equivalents. The literal "Paytable" is overridden too
1490
+ // because the menu SECTION heading uses it as its key (DEFAULT_MENU_TITLES).
1491
+ "openui.paytable": "Prizes",
1492
+ Paytable: "Prizes",
1493
+ "openui.err.insufficient.title": "Insufficient balance",
1461
1494
  "openui.buyFeature.title": "Play bonus",
1462
1495
  "openui.buyFeature.message": "Play this bonus now?",
1463
1496
  "openui.err.insufficient.message": "You don't have enough balance for this play.",
@@ -1506,27 +1539,67 @@ function dictionary(messages, locale, socialMessages) {
1506
1539
  }
1507
1540
 
1508
1541
  // src/format/currency.ts
1509
- var ZERO_DECIMAL = ["JPY", "KRW", "VND", "CLP", "ISK", "HUF", "TWD", "UGX", "XAF", "XOF", "PYG", "RWF"];
1510
- var CRYPTO = { BTC: 8, ETH: 8, LTC: 8, BCH: 8, DOGE: 8, XRP: 6, SOL: 6, TRX: 6, USDT: 2, USDC: 2 };
1511
1542
  var CURRENCY_TABLE = Object.freeze({
1512
- ...Object.fromEntries(ZERO_DECIMAL.map((c) => [c, { decimals: 0 }])),
1513
- ...Object.fromEntries(Object.entries(CRYPTO).map(([c, d]) => [c, { decimals: d }])),
1514
- // Stake social currencies: Gold Coins + Stake Cash — shown as GC/SC, not fiat.
1515
- XGC: { decimals: 2, display: "GC", social: true },
1516
- XSC: { decimals: 2, display: "SC", social: true },
1517
- GC: { decimals: 2, display: "GC", social: true },
1518
- SC: { decimals: 2, display: "SC", social: true }
1543
+ // ── fiat symbol BEFORE the amount ──────────────────────────────────────────
1544
+ USD: { symbol: "$", decimals: 2 },
1545
+ CAD: { symbol: "CA$", decimals: 2 },
1546
+ JPY: { symbol: "\xA5", decimals: 0 },
1547
+ EUR: { symbol: "\u20AC", decimals: 2 },
1548
+ RUB: { symbol: "\u20BD", decimals: 2 },
1549
+ CNY: { symbol: "CN\xA5", decimals: 2 },
1550
+ PHP: { symbol: "\u20B1", decimals: 2 },
1551
+ INR: { symbol: "\u20B9", decimals: 2 },
1552
+ IDR: { symbol: "Rp", decimals: 0 },
1553
+ KRW: { symbol: "\u20A9", decimals: 0 },
1554
+ BRL: { symbol: "R$", decimals: 2 },
1555
+ MXN: { symbol: "MX$", decimals: 2 },
1556
+ TRY: { symbol: "\u20BA", decimals: 2 },
1557
+ GBP: { symbol: "\xA3", decimals: 2 },
1558
+ // ── fiat — symbol AFTER the amount ───────────────────────────────────────────
1559
+ DKK: { symbol: "kr", decimals: 2, symbolAfter: true },
1560
+ PLN: { symbol: "z\u0142", decimals: 2, symbolAfter: true },
1561
+ VND: { symbol: "\u20AB", decimals: 0, symbolAfter: true },
1562
+ CLP: { symbol: "CLP", decimals: 0, symbolAfter: true },
1563
+ ARS: { symbol: "ARS", decimals: 2, symbolAfter: true },
1564
+ PEN: { symbol: "S/", decimals: 2, symbolAfter: true },
1565
+ // ── crypto (8-dp, so sub-cent + high precision format correctly) ─────────────
1566
+ BTC: { symbol: "\u20BF", decimals: 8 },
1567
+ ETH: { symbol: "\u039E", decimals: 8 },
1568
+ LTC: { symbol: "\u0141", decimals: 8 },
1569
+ BCH: { symbol: "BCH", decimals: 8 },
1570
+ DOGE: { symbol: "\xD0", decimals: 8 },
1571
+ USDT: { symbol: "\u20AE", decimals: 2 },
1572
+ USDC: { symbol: "USDC", decimals: 2 },
1573
+ // ── Stake social coins — Gold Coins / Stake Cash (shown as GC / SC) ───────────
1574
+ XGC: { symbol: "GC", decimals: 2, social: true },
1575
+ XSC: { symbol: "SC", decimals: 2, social: true },
1576
+ GC: { symbol: "GC", decimals: 2, social: true },
1577
+ SC: { symbol: "SC", decimals: 2, social: true }
1519
1578
  });
1520
1579
  var upper = (code) => typeof code === "string" ? code.toUpperCase() : "";
1521
1580
  function isSocialCurrency(code) {
1522
1581
  return CURRENCY_TABLE[upper(code)]?.social === true;
1523
1582
  }
1524
1583
  function resolveCurrency(code, overrides = {}) {
1525
- const info = CURRENCY_TABLE[upper(code)] ?? { decimals: 2 };
1584
+ const info = CURRENCY_TABLE[upper(code)];
1585
+ if (!info) {
1586
+ return {
1587
+ code: overrides.code ?? code,
1588
+ decimals: clampDecimals(overrides.decimals ?? 2),
1589
+ position: overrides.position ?? "suffix",
1590
+ separator: overrides.separator ?? ",",
1591
+ decimalChar: overrides.decimalChar ?? ".",
1592
+ ...overrides.symbol ? { symbol: overrides.symbol, display: overrides.display ?? "symbol" } : {}
1593
+ };
1594
+ }
1526
1595
  return {
1527
- code: overrides.code ?? info.display ?? code,
1596
+ // Social coins carry their DISPLAY code (XGC → "GC", XSC → "SC"); fiat/crypto keep
1597
+ // their ISO code (the symbol is what's actually rendered under `display:'symbol'`).
1598
+ code: overrides.code ?? (info.social ? info.symbol : code),
1599
+ symbol: overrides.symbol ?? info.symbol,
1600
+ display: overrides.display ?? "symbol",
1601
+ position: overrides.position ?? (info.symbolAfter ? "suffix" : "prefix"),
1528
1602
  decimals: clampDecimals(overrides.decimals ?? info.decimals),
1529
- position: overrides.position ?? "suffix",
1530
1603
  separator: overrides.separator ?? ",",
1531
1604
  decimalChar: overrides.decimalChar ?? "."
1532
1605
  };
@@ -1542,7 +1615,11 @@ function formatAmount(value, spec, opts = {}) {
1542
1615
  const body = frac ? `${grouped}${dot}${frac}` : grouped;
1543
1616
  const sign = n < 0 ? "-" : opts.signed ? "+" : "";
1544
1617
  const number = `${sign}${body}`;
1545
- return (spec.position ?? "suffix") === "prefix" ? `${spec.code} ${number}` : `${number} ${spec.code}`;
1618
+ const symbolMode = spec.display === "symbol" && !!spec.symbol;
1619
+ const affix = symbolMode ? spec.symbol : spec.code;
1620
+ const prefix = (spec.position ?? "suffix") === "prefix";
1621
+ const gap = symbolMode && prefix ? "" : " ";
1622
+ return prefix ? `${affix}${gap}${number}` : `${number}${gap}${affix}`;
1546
1623
  }
1547
1624
 
1548
1625
  // src/OpenUI.ts
@@ -1604,6 +1681,9 @@ var OpenUI = class {
1604
1681
  spin;
1605
1682
  balance;
1606
1683
  bet;
1684
+ /** Bonus "total win" readout — shown in the buy-feature slot during a bonus/free-
1685
+ * spins round (the renderer swaps it for the buy button). Same money logic as `bet`. */
1686
+ totalWin;
1607
1687
  settingsButton;
1608
1688
  settingsPanel;
1609
1689
  musicSlider;
@@ -1652,6 +1732,13 @@ var OpenUI = class {
1652
1732
  currency: { code: "USD", decimals: 2 },
1653
1733
  initial: 1
1654
1734
  });
1735
+ this.totalWin = new ValueDisplay({
1736
+ id: "total-win",
1737
+ label: "openui.totalWin",
1738
+ layout: defaultLayout("total-win"),
1739
+ currency: { code: "USD", decimals: 2 },
1740
+ initial: 0
1741
+ });
1655
1742
  this.settingsButton = new ButtonControl(
1656
1743
  { id: "settings", layout: defaultLayout("settings") },
1657
1744
  this.bus
@@ -1681,6 +1768,7 @@ var OpenUI = class {
1681
1768
  this.spin,
1682
1769
  this.balance,
1683
1770
  this.bet,
1771
+ this.totalWin,
1684
1772
  this.settingsButton,
1685
1773
  this.settingsPanel,
1686
1774
  this.musicSlider,
@@ -2057,6 +2145,130 @@ function installResponsive(ui, responsive) {
2057
2145
  };
2058
2146
  }
2059
2147
 
2148
+ // src/spec/socialPhrases.ts
2149
+ var FORBIDDEN = [
2150
+ { re: /\bpay\w*/i, term: "pay*", replacement: 'prize / award (e.g. "paytable" \u2192 "prizes")' },
2151
+ { re: /\bpayout\w*/i, term: "payout", replacement: "award" },
2152
+ { re: /\bbets?\b|\bbetting\b/i, term: "bet", replacement: "play" },
2153
+ { re: /\bwager\w*/i, term: "wager", replacement: "play" },
2154
+ { re: /\bgambl\w*/i, term: "gamble", replacement: "play" },
2155
+ { re: /\bcash\w*/i, term: "cash", replacement: "coins" },
2156
+ { re: /\bfunds?\b/i, term: "funds", replacement: "coins / balance" },
2157
+ { re: /\bmoney\b/i, term: "money", replacement: "coins" },
2158
+ { re: /\bdeposit\w*/i, term: "deposit", replacement: "purchase" },
2159
+ { re: /\bwithdraw\w*/i, term: "withdraw", replacement: "redeem" },
2160
+ { re: /\bcredits?\b/i, term: "credit", replacement: "coins / balance" },
2161
+ { re: /\bwinnings?\b/i, term: "winnings", replacement: "prizes" },
2162
+ { re: /\bjackpots?\b/i, term: "jackpot", replacement: "grand prize" },
2163
+ { re: /\breal[-\s]?money\b/i, term: "real money", replacement: "coins" }
2164
+ ];
2165
+ function findForbiddenPhrases(text) {
2166
+ if (typeof text !== "string" || !text) return [];
2167
+ const seen = /* @__PURE__ */ new Set();
2168
+ const out = [];
2169
+ for (const r of FORBIDDEN) {
2170
+ if (r.re.test(text) && !seen.has(r.term)) {
2171
+ seen.add(r.term);
2172
+ out.push({ term: r.term, replacement: r.replacement });
2173
+ }
2174
+ }
2175
+ return out;
2176
+ }
2177
+ function walkBlock(b, src, out) {
2178
+ const add = (t, s = src) => {
2179
+ if (typeof t === "string" && t) out.push({ source: s, text: t });
2180
+ };
2181
+ const anyB = b;
2182
+ switch (b.kind) {
2183
+ case "text":
2184
+ case "heading":
2185
+ case "subheading":
2186
+ case "legal":
2187
+ add(b.text);
2188
+ break;
2189
+ case "callout":
2190
+ add(b.title);
2191
+ add(b.text);
2192
+ break;
2193
+ case "media":
2194
+ add(b.title);
2195
+ add(b.text);
2196
+ break;
2197
+ case "slider":
2198
+ case "toggle":
2199
+ case "button":
2200
+ case "select":
2201
+ case "stepper":
2202
+ case "value":
2203
+ add(anyB.label);
2204
+ add(anyB.hint);
2205
+ break;
2206
+ case "steps":
2207
+ b.items.forEach((s, i) => add(s, `${src}.items[${i}]`));
2208
+ break;
2209
+ case "table":
2210
+ (b.columns ?? []).forEach((c) => add(c));
2211
+ b.rows.forEach((row) => row.forEach((c) => add(c)));
2212
+ break;
2213
+ case "paytable":
2214
+ b.rows.forEach((r) => {
2215
+ add(r.symbol);
2216
+ add(r.payouts);
2217
+ });
2218
+ break;
2219
+ case "cards":
2220
+ b.items.forEach((it) => {
2221
+ add(it.title);
2222
+ add(it.text);
2223
+ });
2224
+ break;
2225
+ case "stat-grid":
2226
+ b.items.forEach((it) => {
2227
+ add(it.label);
2228
+ add(it.value);
2229
+ });
2230
+ break;
2231
+ case "group":
2232
+ add(b.title);
2233
+ b.children.forEach((c, i) => walkBlock(c, `${src}.children[${i}]`, out));
2234
+ break;
2235
+ }
2236
+ }
2237
+ function collectSocialCopy(spec) {
2238
+ const out = [];
2239
+ const menu = spec.menu;
2240
+ if (menu) {
2241
+ const sections = [
2242
+ ["settings", menu.settings],
2243
+ ["paytable", menu.paytable],
2244
+ ["rules", menu.rules]
2245
+ ];
2246
+ for (const [sec, blocks] of sections) (blocks ?? []).forEach((b, i) => walkBlock(b, `menu.${sec}[${i}]`, out));
2247
+ if (menu.titles) {
2248
+ for (const [k, v] of Object.entries(menu.titles)) if (v) out.push({ source: `menu.titles.${k}`, text: v });
2249
+ }
2250
+ if (menu.banner?.src) ;
2251
+ }
2252
+ (spec.rules ?? []).forEach((b, i) => walkBlock(b, `rules[${i}]`, out));
2253
+ if (spec.game?.name) out.push({ source: "game.name", text: spec.game.name });
2254
+ const en = spec.locale?.messages?.en;
2255
+ if (en) {
2256
+ for (const [k, v] of Object.entries(en)) if (typeof v === "string") out.push({ source: `locale.en[${JSON.stringify(k)}]`, text: v, key: k });
2257
+ }
2258
+ return out;
2259
+ }
2260
+ function checkSocialPhrases(spec) {
2261
+ const socialEn = spec.locale?.socialMessages?.en ?? {};
2262
+ const render = (e) => socialEn[e.key ?? e.text] ?? openuiSocialDefaults[e.key ?? e.text] ?? e.text;
2263
+ const issues = [];
2264
+ for (const e of collectSocialCopy(spec)) {
2265
+ const text = render(e);
2266
+ const matches = findForbiddenPhrases(text);
2267
+ if (matches.length) issues.push({ source: e.source, text, matches });
2268
+ }
2269
+ return issues;
2270
+ }
2271
+
2060
2272
  // src/spec/createUI.ts
2061
2273
  function resolveTurboModes(modes) {
2062
2274
  if (Array.isArray(modes)) return modes.length ? modes : ["off", "on"];
@@ -2086,12 +2298,27 @@ function createUI(spec = {}, hooks = {}) {
2086
2298
  const cur = typeof spec.currency === "string" ? resolveCurrency(spec.currency) : spec.currency;
2087
2299
  ui.balance.setCurrency(cur);
2088
2300
  ui.bet.setCurrency(cur);
2301
+ ui.totalWin.setCurrency(cur);
2089
2302
  ui.netPosition.setCurrency(cur);
2090
2303
  }
2091
2304
  if (spec.social) {
2092
2305
  if (typeof spec.social === "object") ui.setSocial(true, spec.social.coin);
2093
2306
  else ui.setSocial(true);
2094
2307
  }
2308
+ if (spec.social || spec.jurisdiction?.socialCasino) {
2309
+ const found = checkSocialPhrases(spec);
2310
+ for (const issue of found) {
2311
+ hooks.onDataIssue?.({
2312
+ level: "warn",
2313
+ path: issue.source,
2314
+ code: "social-forbidden-phrase",
2315
+ message: `Social mode restricted wording in "${issue.text}": ${issue.matches.map((m) => `\u201C${m.term}\u201D \u2192 ${m.replacement}`).join("; ")}`
2316
+ });
2317
+ }
2318
+ if (found.length && typeof console !== "undefined") {
2319
+ console.warn(`[open-ui] social mode: ${found.length} string(s) contain restricted phrases (see onDataIssue). First: ${found[0].source} \u2192 "${found[0].text}"`);
2320
+ }
2321
+ }
2095
2322
  if (typeof spec.rtp === "number") ui.rtp.set(spec.rtp);
2096
2323
  if (spec.game) ui.gameInfo = { name: spec.game.name, version: spec.game.version };
2097
2324
  if (spec.betLadder?.levels?.length) {
@@ -2354,10 +2581,12 @@ exports.buildBetLadder = buildBetLadder;
2354
2581
  exports.buildBlocks = buildBlocks;
2355
2582
  exports.buildPanel = buildPanel;
2356
2583
  exports.buttonBlocks = buttonBlocks;
2584
+ exports.checkSocialPhrases = checkSocialPhrases;
2357
2585
  exports.clampBet = clampBet;
2358
2586
  exports.clampDecimals = clampDecimals;
2359
2587
  exports.clampDigits = clampDigits;
2360
2588
  exports.clampMinDigits = clampMinDigits;
2589
+ exports.collectSocialCopy = collectSocialCopy;
2361
2590
  exports.composeMenu = composeMenu;
2362
2591
  exports.computeScreen = computeScreen;
2363
2592
  exports.createUI = createUI;
@@ -2371,6 +2600,7 @@ exports.displayDigits = displayDigits;
2371
2600
  exports.effect = effect;
2372
2601
  exports.errorBlocks = errorBlocks;
2373
2602
  exports.extendTheme = extendTheme;
2603
+ exports.findForbiddenPhrases = findForbiddenPhrases;
2374
2604
  exports.formatAmount = formatAmount;
2375
2605
  exports.installResponsive = installResponsive;
2376
2606
  exports.integerDigits = integerDigits;