@open-slot-ui/core 0.9.0 → 0.11.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
@@ -1652,6 +1652,10 @@ interface ResponsiveOverride {
1652
1652
  type BlockSpec = {
1653
1653
  /** Rules-audit topics this block covers (see `auditRules`). */
1654
1654
  covers?: string[];
1655
+ /** The declared mode id this block EXPLAINS (e.g. `'bonus'`). Every declared mode
1656
+ * must have its own rules section — a heading naming it, or blocks tagged with
1657
+ * `explains` — with real prose; the audit enforces it (see `auditRules`). */
1658
+ explains?: string;
1655
1659
  } & ({
1656
1660
  kind: 'slider';
1657
1661
  id: string;
@@ -2087,6 +2091,41 @@ declare function auditRulesDoc(doc: RulesDoc, opts?: {
2087
2091
  facts?: GameFacts;
2088
2092
  }): RulesAuditIssue[];
2089
2093
 
2094
+ /**
2095
+ * The ONE in-game rules/menu HTML renderer — the white-card-with-gold-accents
2096
+ * design the info menu shows in every game — as PURE string builders (zero deps,
2097
+ * no DOM required), so the SAME renderer that draws the menu in-game also powers
2098
+ * external tooling (the stakeplate rules editor's WYSIWYG preview, docs, CI
2099
+ * snapshots). `@open-slot-ui/pixi`'s `mountInfoMenu` consumes these — there is
2100
+ * exactly one source of truth for how a rules block looks.
2101
+ */
2102
+
2103
+ /** Escape text for safe HTML interpolation. */
2104
+ declare const escapeHtml: (s: string) => string;
2105
+ /** Escape, then turn `**bold**` runs into `<b>` — the shared inline syntax. */
2106
+ declare const richHtml: (s: string) => string;
2107
+ /**
2108
+ * Render a declarative rules `BlockSpec[]` to the in-game HTML — the full modular
2109
+ * palette. `tr` is the translate function (pass a translate-and-interpolate so
2110
+ * `{{rtp.base}}`-style tokens resolve — see `factsVars`); `facts` feeds the auto
2111
+ * `mode-stats` block.
2112
+ */
2113
+ declare function renderBlocksHtml(blocks: BlockSpec[], tr: (s: string) => string, facts?: GameFacts): string;
2114
+ /**
2115
+ * The "⚠ Rules incomplete" warning card the info menu shows at the top of Rules
2116
+ * when the audit finds forgotten declarations. `tr` localizes the chrome strings
2117
+ * (`openui.rulesAudit.*`) + the finding messages. Empty issues → ''.
2118
+ */
2119
+ declare function renderRulesAuditHtml(issues: RulesAuditIssue[], tr: (s: string) => string): string;
2120
+ /**
2121
+ * The in-game info-menu stylesheet (the white card + gold accents design). One
2122
+ * copy, used by `mountInfoMenu` AND any external preview. Expects the CSS custom
2123
+ * properties `--accent --accent-text --surface --surface-alt --text --text-dim
2124
+ * --card-radius --font` on the `.ohm-root` (see {@link INFO_MENU_VARS}).
2125
+ */
2126
+ declare const INFO_MENU_VARS: Readonly<Record<string, string>>;
2127
+ declare const INFO_MENU_CSS = "\n.ohm-root { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }\n.ohm-root.open { opacity: 1; pointer-events: auto; }\n.ohm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }\n.ohm-root.open .ohm-backdrop { background: rgba(8,6,4,.34); backdrop-filter: blur(6px) saturate(1.1); -webkit-backdrop-filter: blur(6px) saturate(1.1); }\n.ohm-card { position: relative; width: min(92%, 1100px); max-height: 86vh; display: flex; flex-direction: column; background: var(--surface); color: var(--text); border: 1.5px solid #000; border-radius: var(--card-radius); box-shadow: 0 30px 80px rgba(0,0,0,.5); overflow: hidden; transform: translateY(8px) scale(.99); transition: transform .18s ease; }\n.ohm-root.open .ohm-card { transform: none; }\n.ohm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }\n.ohm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }\n.ohm-body { padding: 24px 26px 26px; overflow-y: scroll; }\n.ohm-body::-webkit-scrollbar { width: 18px; }\n.ohm-body::-webkit-scrollbar-track { background: transparent; margin: 12px 0; }\n.ohm-body::-webkit-scrollbar-thumb { background-color: #111; border: 6px solid transparent; background-clip: padding-box; border-radius: 999px; min-height: 44px; }\n.ohm-body::-webkit-scrollbar-thumb:hover { background-color: #000; }\n.ohm-logo { display: block; margin: 6px auto 18px; max-width: 64%; height: auto; }\n.ohm-logo-text { margin: 6px 0 18px; text-align: center; font-size: 30px; font-weight: 900; letter-spacing: 1px; color: var(--text); }\n.ohm-sec { display: flex; align-items: center; gap: 14px; margin: 26px 0 14px; color: var(--text); font-weight: 800; letter-spacing: 1px; }\n.ohm-sec::before, .ohm-sec::after { content: \"\"; flex: 1; height: 2px; background: color-mix(in srgb, var(--text) 80%, transparent); border-radius: 2px; }\n.ohm-root *, .ohm-root *::before, .ohm-root *::after { box-sizing: border-box; }\n.ohm-row { display: flex; align-items: center; gap: 16px; margin: 14px 0; font-weight: 700; }\n.ohm-setting { margin: 14px 0; }\n.ohm-setting .ohm-row { margin: 0; }\n.ohm-hint { margin: 3px 0 0; font-size: 12.5px; font-weight: 500; color: var(--text-dim); }\n.ohm-row > span:first-child { flex: none; min-width: 110px; }\n.ohm-row input[type=range] { flex: 1; min-width: 0; max-width: min(440px, 60dvw); accent-color: var(--accent); height: 6px; }\n.ohm-row select { flex: 1; min-width: 0; max-width: min(440px, 60dvw); padding: 11px 14px; border-radius: 4px; border: 2px solid var(--accent); background: var(--surface-alt); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer; }\n.ohm-row select option { background: var(--surface); color: var(--text); font-weight: 600; }\n.ohm-row select option:checked { background: var(--accent); color: var(--accent-text); }\n.ohm-ctl { flex: 1; min-width: 0; max-width: min(440px, 60dvw); display: flex; align-items: center; justify-content: flex-end; }\n.ohm-check input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 50px; height: 28px; border-radius: 999px; background: color-mix(in srgb, var(--text-dim) 38%, transparent); position: relative; cursor: pointer; transition: background .15s; flex: none; }\n.ohm-check input[type=checkbox]:checked { background: var(--accent); }\n.ohm-check input[type=checkbox]::before { content: \"\"; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 999px; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }\n.ohm-check input[type=checkbox]:checked::before { left: 25px; }\n.ohm-segmented { display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-alt); border-radius: 999px; border: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); }\n.ohm-seg { border: 0; background: transparent; color: var(--text-dim); font-weight: 700; font-size: 14px; padding: 8px 18px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }\n.ohm-seg.active { background: var(--accent); color: var(--accent-text); }\n.ohm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }\n.ohm-sym { display: flex; align-items: center; gap: 16px; padding: 6px 4px; }\n.ohm-emoji { font-size: 48px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }\n.ohm-symimg { width: 56px; height: 56px; object-fit: contain; flex: none; }\n.ohm-pay { font-size: 13px; line-height: 1.6; }\n.ohm-pay div { display: flex; gap: 6px; }\n.ohm-pay b { min-width: 42px; }\n.ohm-pay span { color: var(--accent); font-weight: 700; }\n.ohm-body p { color: var(--text-dim); line-height: 1.6; }\n.ohm-body p b { color: var(--text); }\n.ohm-feature { display: block; width: 100%; height: auto; margin: 10px 0; }\n.ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }\n.ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }\n.ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }\n.ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }\n.ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }\n.ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }\n.ohm-audit-sub--rec { color: #b07d09; }\n.ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }\n.ohm-audit li { margin: 3px 0; }\n.ohm-audit-rec ul { color: var(--text-dim); }\n.ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }\n.ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }\n.ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }\n.ohm-callout--warning b { color: #b07d09; }\n.ohm-subh { margin: 22px 0 8px; font-size: 15px; font-weight: 800; letter-spacing: .5px; color: var(--text); }\n.ohm-legal { font-size: 12px; line-height: 1.6; color: var(--text-dim); opacity: .85; }\n.ohm-hr { border: 0; border-top: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); margin: 18px 0; }\n.ohm-media { display: flex; align-items: center; gap: 18px; margin: 14px 0; }\n.ohm-media--right { flex-direction: row-reverse; }\n.ohm-media > img { width: 40%; max-width: 320px; height: auto; flex: none; }\n.ohm-media-body { flex: 1; }\n.ohm-media-body h4 { margin: 0 0 6px; font-size: 16px; color: var(--text); }\n.ohm-media-body p { margin: 0; }\n.ohm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }\n.ohm-fcard { padding: 8px 6px; text-align: center; }\n.ohm-fcard img { display: block; width: 48px; height: 48px; margin: 0 auto 8px; }\n.ohm-fcard h5 { margin: 0 0 4px; font-size: 14px; color: var(--text); }\n.ohm-fcard p { margin: 0; font-size: 12px; line-height: 1.5; }\n.ohm-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }\n.ohm-table th { text-align: left; padding: 8px 10px; font-weight: 800; color: var(--text); border-bottom: 2px solid color-mix(in srgb, var(--text-dim) 35%, transparent); }\n.ohm-table td { padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 18%, transparent); }\n.ohm-table td:first-child { color: var(--text); font-weight: 700; }\n.ohm-table td:not(:first-child) { color: var(--accent); font-weight: 700; }\n.ohm-steps { margin: 12px 0; padding-left: 22px; color: var(--text-dim); line-height: 1.7; }\n.ohm-steps li { margin: 5px 0; }\n.ohm-steps b { color: var(--text); }\n.ohm-group { margin: 8px 0; }\n";
2128
+
2090
2129
  /**
2091
2130
  * The reference HUD expressed as data — the knobs `new OpenUI()` ships with.
2092
2131
  * `createUI(defaultHudSpec)` is byte-equivalent to zero-arg `createUI()` (a test
@@ -2243,4 +2282,4 @@ declare function resolveBetLadder(availableBets: number[] | undefined, defaultBe
2243
2282
  /** Clamp a stake (major units) to the RGS min/max and snap to `stepBet`. Pure. */
2244
2283
  declare function clampBet(amount: number, cfg: RgsBetConfig, divisor?: number): number;
2245
2284
 
2246
- export { API_AMOUNT_DIVISOR, type Anchor, AutoplayControl, type AutoplayLimits, type AutoplayMode, type AutoplayOptions, BLOCK_KINDS, type BlockFactory, type BlockSpec, type Breakpoint, type BuiltPanel, ButtonControl, type ButtonOptions, CURRENCY_TABLE, CardControl, type CardOptions, type ComposeMenuOptions, Control, type ControlOptions, type ControlOverride, ControlRegistry, type ControlSnapshot, type CurrencyInfo, type CurrencySpec, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, type DeepPartial, DictionaryTranslator, type Dispose, EventBus, EventLog, type EventLogOptions, type EventMap, Fade, type ForbiddenMatch, type FreeSpinsFact, type GameFacts, type GameModeFact, type HostHooks, type JurisdictionConfig, type KnownControlId, LOCALE_LABELS, type LayoutConfig, type LayoutSpec, type LoggedEvent, type MenuSpec, type NoticeAction, type NoticeOptions, OpenUI, type OpenUIEvents, type OpenUIOptions, type Orientation, PanelControl, type PanelOptions, type PanelSpec, type PanelVariant, Parallel, type Placement, Pulse, RGS_ERROR_KEYS, ReadoutControl, type ReadoutKind, type ReadoutOptions, type RealityCheckOptions, type Rect, type ResponsiveKey, type ResponsiveOverride, type RgsBetConfig, type RgsErrorCode, type RgsErrorOptions, type RulesAuditIssue, type RulesDoc, type ScreenState, SelectControl, type SelectControlOptions, type SelectOption, Sequence, Signal, SliderControl, type SliderOptions, type SocialPhraseIssue, type SoundControls, type SpecIssue, SpinControl, type SpinPress, type SpinSpec, Squish, type StageRect, type StateDef, type StateMap, StepperControl, type StepperOptions, type Subscribable, type Theme, type ThemeChoice, type ThemeIssue, type ThemeOverrides, type ThemePreset, ToggleControl, type ToggleOptions, type Transition, type Translator, TurboControl, type TurboOptions, type TurboSpec, Turn, type UISpec, ValueDisplay, type ValueDisplayOptions, type ViewInspect, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
2285
+ export { API_AMOUNT_DIVISOR, type Anchor, AutoplayControl, type AutoplayLimits, type AutoplayMode, type AutoplayOptions, BLOCK_KINDS, type BlockFactory, type BlockSpec, type Breakpoint, type BuiltPanel, ButtonControl, type ButtonOptions, CURRENCY_TABLE, CardControl, type CardOptions, type ComposeMenuOptions, Control, type ControlOptions, type ControlOverride, ControlRegistry, type ControlSnapshot, type CurrencyInfo, type CurrencySpec, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, type DeepPartial, DictionaryTranslator, type Dispose, EventBus, EventLog, type EventLogOptions, type EventMap, Fade, type ForbiddenMatch, type FreeSpinsFact, type GameFacts, type GameModeFact, type HostHooks, INFO_MENU_CSS, INFO_MENU_VARS, type JurisdictionConfig, type KnownControlId, LOCALE_LABELS, type LayoutConfig, type LayoutSpec, type LoggedEvent, type MenuSpec, type NoticeAction, type NoticeOptions, OpenUI, type OpenUIEvents, type OpenUIOptions, type Orientation, PanelControl, type PanelOptions, type PanelSpec, type PanelVariant, Parallel, type Placement, Pulse, RGS_ERROR_KEYS, ReadoutControl, type ReadoutKind, type ReadoutOptions, type RealityCheckOptions, type Rect, type ResponsiveKey, type ResponsiveOverride, type RgsBetConfig, type RgsErrorCode, type RgsErrorOptions, type RulesAuditIssue, type RulesDoc, type ScreenState, SelectControl, type SelectControlOptions, type SelectOption, Sequence, Signal, SliderControl, type SliderOptions, type SocialPhraseIssue, type SoundControls, type SpecIssue, SpinControl, type SpinPress, type SpinSpec, Squish, type StageRect, type StateDef, type StateMap, StepperControl, type StepperOptions, type Subscribable, type Theme, type ThemeChoice, type ThemeIssue, type ThemeOverrides, type ThemePreset, ToggleControl, type ToggleOptions, type Transition, type Translator, TurboControl, type TurboOptions, type TurboSpec, Turn, type UISpec, ValueDisplay, type ValueDisplayOptions, type ViewInspect, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, escapeHtml, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, renderBlocksHtml, renderRulesAuditHtml, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, richHtml, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
package/dist/index.d.ts CHANGED
@@ -1652,6 +1652,10 @@ interface ResponsiveOverride {
1652
1652
  type BlockSpec = {
1653
1653
  /** Rules-audit topics this block covers (see `auditRules`). */
1654
1654
  covers?: string[];
1655
+ /** The declared mode id this block EXPLAINS (e.g. `'bonus'`). Every declared mode
1656
+ * must have its own rules section — a heading naming it, or blocks tagged with
1657
+ * `explains` — with real prose; the audit enforces it (see `auditRules`). */
1658
+ explains?: string;
1655
1659
  } & ({
1656
1660
  kind: 'slider';
1657
1661
  id: string;
@@ -2087,6 +2091,41 @@ declare function auditRulesDoc(doc: RulesDoc, opts?: {
2087
2091
  facts?: GameFacts;
2088
2092
  }): RulesAuditIssue[];
2089
2093
 
2094
+ /**
2095
+ * The ONE in-game rules/menu HTML renderer — the white-card-with-gold-accents
2096
+ * design the info menu shows in every game — as PURE string builders (zero deps,
2097
+ * no DOM required), so the SAME renderer that draws the menu in-game also powers
2098
+ * external tooling (the stakeplate rules editor's WYSIWYG preview, docs, CI
2099
+ * snapshots). `@open-slot-ui/pixi`'s `mountInfoMenu` consumes these — there is
2100
+ * exactly one source of truth for how a rules block looks.
2101
+ */
2102
+
2103
+ /** Escape text for safe HTML interpolation. */
2104
+ declare const escapeHtml: (s: string) => string;
2105
+ /** Escape, then turn `**bold**` runs into `<b>` — the shared inline syntax. */
2106
+ declare const richHtml: (s: string) => string;
2107
+ /**
2108
+ * Render a declarative rules `BlockSpec[]` to the in-game HTML — the full modular
2109
+ * palette. `tr` is the translate function (pass a translate-and-interpolate so
2110
+ * `{{rtp.base}}`-style tokens resolve — see `factsVars`); `facts` feeds the auto
2111
+ * `mode-stats` block.
2112
+ */
2113
+ declare function renderBlocksHtml(blocks: BlockSpec[], tr: (s: string) => string, facts?: GameFacts): string;
2114
+ /**
2115
+ * The "⚠ Rules incomplete" warning card the info menu shows at the top of Rules
2116
+ * when the audit finds forgotten declarations. `tr` localizes the chrome strings
2117
+ * (`openui.rulesAudit.*`) + the finding messages. Empty issues → ''.
2118
+ */
2119
+ declare function renderRulesAuditHtml(issues: RulesAuditIssue[], tr: (s: string) => string): string;
2120
+ /**
2121
+ * The in-game info-menu stylesheet (the white card + gold accents design). One
2122
+ * copy, used by `mountInfoMenu` AND any external preview. Expects the CSS custom
2123
+ * properties `--accent --accent-text --surface --surface-alt --text --text-dim
2124
+ * --card-radius --font` on the `.ohm-root` (see {@link INFO_MENU_VARS}).
2125
+ */
2126
+ declare const INFO_MENU_VARS: Readonly<Record<string, string>>;
2127
+ declare const INFO_MENU_CSS = "\n.ohm-root { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }\n.ohm-root.open { opacity: 1; pointer-events: auto; }\n.ohm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }\n.ohm-root.open .ohm-backdrop { background: rgba(8,6,4,.34); backdrop-filter: blur(6px) saturate(1.1); -webkit-backdrop-filter: blur(6px) saturate(1.1); }\n.ohm-card { position: relative; width: min(92%, 1100px); max-height: 86vh; display: flex; flex-direction: column; background: var(--surface); color: var(--text); border: 1.5px solid #000; border-radius: var(--card-radius); box-shadow: 0 30px 80px rgba(0,0,0,.5); overflow: hidden; transform: translateY(8px) scale(.99); transition: transform .18s ease; }\n.ohm-root.open .ohm-card { transform: none; }\n.ohm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }\n.ohm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }\n.ohm-body { padding: 24px 26px 26px; overflow-y: scroll; }\n.ohm-body::-webkit-scrollbar { width: 18px; }\n.ohm-body::-webkit-scrollbar-track { background: transparent; margin: 12px 0; }\n.ohm-body::-webkit-scrollbar-thumb { background-color: #111; border: 6px solid transparent; background-clip: padding-box; border-radius: 999px; min-height: 44px; }\n.ohm-body::-webkit-scrollbar-thumb:hover { background-color: #000; }\n.ohm-logo { display: block; margin: 6px auto 18px; max-width: 64%; height: auto; }\n.ohm-logo-text { margin: 6px 0 18px; text-align: center; font-size: 30px; font-weight: 900; letter-spacing: 1px; color: var(--text); }\n.ohm-sec { display: flex; align-items: center; gap: 14px; margin: 26px 0 14px; color: var(--text); font-weight: 800; letter-spacing: 1px; }\n.ohm-sec::before, .ohm-sec::after { content: \"\"; flex: 1; height: 2px; background: color-mix(in srgb, var(--text) 80%, transparent); border-radius: 2px; }\n.ohm-root *, .ohm-root *::before, .ohm-root *::after { box-sizing: border-box; }\n.ohm-row { display: flex; align-items: center; gap: 16px; margin: 14px 0; font-weight: 700; }\n.ohm-setting { margin: 14px 0; }\n.ohm-setting .ohm-row { margin: 0; }\n.ohm-hint { margin: 3px 0 0; font-size: 12.5px; font-weight: 500; color: var(--text-dim); }\n.ohm-row > span:first-child { flex: none; min-width: 110px; }\n.ohm-row input[type=range] { flex: 1; min-width: 0; max-width: min(440px, 60dvw); accent-color: var(--accent); height: 6px; }\n.ohm-row select { flex: 1; min-width: 0; max-width: min(440px, 60dvw); padding: 11px 14px; border-radius: 4px; border: 2px solid var(--accent); background: var(--surface-alt); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer; }\n.ohm-row select option { background: var(--surface); color: var(--text); font-weight: 600; }\n.ohm-row select option:checked { background: var(--accent); color: var(--accent-text); }\n.ohm-ctl { flex: 1; min-width: 0; max-width: min(440px, 60dvw); display: flex; align-items: center; justify-content: flex-end; }\n.ohm-check input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 50px; height: 28px; border-radius: 999px; background: color-mix(in srgb, var(--text-dim) 38%, transparent); position: relative; cursor: pointer; transition: background .15s; flex: none; }\n.ohm-check input[type=checkbox]:checked { background: var(--accent); }\n.ohm-check input[type=checkbox]::before { content: \"\"; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 999px; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }\n.ohm-check input[type=checkbox]:checked::before { left: 25px; }\n.ohm-segmented { display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-alt); border-radius: 999px; border: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); }\n.ohm-seg { border: 0; background: transparent; color: var(--text-dim); font-weight: 700; font-size: 14px; padding: 8px 18px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }\n.ohm-seg.active { background: var(--accent); color: var(--accent-text); }\n.ohm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }\n.ohm-sym { display: flex; align-items: center; gap: 16px; padding: 6px 4px; }\n.ohm-emoji { font-size: 48px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }\n.ohm-symimg { width: 56px; height: 56px; object-fit: contain; flex: none; }\n.ohm-pay { font-size: 13px; line-height: 1.6; }\n.ohm-pay div { display: flex; gap: 6px; }\n.ohm-pay b { min-width: 42px; }\n.ohm-pay span { color: var(--accent); font-weight: 700; }\n.ohm-body p { color: var(--text-dim); line-height: 1.6; }\n.ohm-body p b { color: var(--text); }\n.ohm-feature { display: block; width: 100%; height: auto; margin: 10px 0; }\n.ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }\n.ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }\n.ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }\n.ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }\n.ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }\n.ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }\n.ohm-audit-sub--rec { color: #b07d09; }\n.ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }\n.ohm-audit li { margin: 3px 0; }\n.ohm-audit-rec ul { color: var(--text-dim); }\n.ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }\n.ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }\n.ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }\n.ohm-callout--warning b { color: #b07d09; }\n.ohm-subh { margin: 22px 0 8px; font-size: 15px; font-weight: 800; letter-spacing: .5px; color: var(--text); }\n.ohm-legal { font-size: 12px; line-height: 1.6; color: var(--text-dim); opacity: .85; }\n.ohm-hr { border: 0; border-top: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); margin: 18px 0; }\n.ohm-media { display: flex; align-items: center; gap: 18px; margin: 14px 0; }\n.ohm-media--right { flex-direction: row-reverse; }\n.ohm-media > img { width: 40%; max-width: 320px; height: auto; flex: none; }\n.ohm-media-body { flex: 1; }\n.ohm-media-body h4 { margin: 0 0 6px; font-size: 16px; color: var(--text); }\n.ohm-media-body p { margin: 0; }\n.ohm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }\n.ohm-fcard { padding: 8px 6px; text-align: center; }\n.ohm-fcard img { display: block; width: 48px; height: 48px; margin: 0 auto 8px; }\n.ohm-fcard h5 { margin: 0 0 4px; font-size: 14px; color: var(--text); }\n.ohm-fcard p { margin: 0; font-size: 12px; line-height: 1.5; }\n.ohm-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }\n.ohm-table th { text-align: left; padding: 8px 10px; font-weight: 800; color: var(--text); border-bottom: 2px solid color-mix(in srgb, var(--text-dim) 35%, transparent); }\n.ohm-table td { padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 18%, transparent); }\n.ohm-table td:first-child { color: var(--text); font-weight: 700; }\n.ohm-table td:not(:first-child) { color: var(--accent); font-weight: 700; }\n.ohm-steps { margin: 12px 0; padding-left: 22px; color: var(--text-dim); line-height: 1.7; }\n.ohm-steps li { margin: 5px 0; }\n.ohm-steps b { color: var(--text); }\n.ohm-group { margin: 8px 0; }\n";
2128
+
2090
2129
  /**
2091
2130
  * The reference HUD expressed as data — the knobs `new OpenUI()` ships with.
2092
2131
  * `createUI(defaultHudSpec)` is byte-equivalent to zero-arg `createUI()` (a test
@@ -2243,4 +2282,4 @@ declare function resolveBetLadder(availableBets: number[] | undefined, defaultBe
2243
2282
  /** Clamp a stake (major units) to the RGS min/max and snap to `stepBet`. Pure. */
2244
2283
  declare function clampBet(amount: number, cfg: RgsBetConfig, divisor?: number): number;
2245
2284
 
2246
- export { API_AMOUNT_DIVISOR, type Anchor, AutoplayControl, type AutoplayLimits, type AutoplayMode, type AutoplayOptions, BLOCK_KINDS, type BlockFactory, type BlockSpec, type Breakpoint, type BuiltPanel, ButtonControl, type ButtonOptions, CURRENCY_TABLE, CardControl, type CardOptions, type ComposeMenuOptions, Control, type ControlOptions, type ControlOverride, ControlRegistry, type ControlSnapshot, type CurrencyInfo, type CurrencySpec, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, type DeepPartial, DictionaryTranslator, type Dispose, EventBus, EventLog, type EventLogOptions, type EventMap, Fade, type ForbiddenMatch, type FreeSpinsFact, type GameFacts, type GameModeFact, type HostHooks, type JurisdictionConfig, type KnownControlId, LOCALE_LABELS, type LayoutConfig, type LayoutSpec, type LoggedEvent, type MenuSpec, type NoticeAction, type NoticeOptions, OpenUI, type OpenUIEvents, type OpenUIOptions, type Orientation, PanelControl, type PanelOptions, type PanelSpec, type PanelVariant, Parallel, type Placement, Pulse, RGS_ERROR_KEYS, ReadoutControl, type ReadoutKind, type ReadoutOptions, type RealityCheckOptions, type Rect, type ResponsiveKey, type ResponsiveOverride, type RgsBetConfig, type RgsErrorCode, type RgsErrorOptions, type RulesAuditIssue, type RulesDoc, type ScreenState, SelectControl, type SelectControlOptions, type SelectOption, Sequence, Signal, SliderControl, type SliderOptions, type SocialPhraseIssue, type SoundControls, type SpecIssue, SpinControl, type SpinPress, type SpinSpec, Squish, type StageRect, type StateDef, type StateMap, StepperControl, type StepperOptions, type Subscribable, type Theme, type ThemeChoice, type ThemeIssue, type ThemeOverrides, type ThemePreset, ToggleControl, type ToggleOptions, type Transition, type Translator, TurboControl, type TurboOptions, type TurboSpec, Turn, type UISpec, ValueDisplay, type ValueDisplayOptions, type ViewInspect, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
2285
+ export { API_AMOUNT_DIVISOR, type Anchor, AutoplayControl, type AutoplayLimits, type AutoplayMode, type AutoplayOptions, BLOCK_KINDS, type BlockFactory, type BlockSpec, type Breakpoint, type BuiltPanel, ButtonControl, type ButtonOptions, CURRENCY_TABLE, CardControl, type CardOptions, type ComposeMenuOptions, Control, type ControlOptions, type ControlOverride, ControlRegistry, type ControlSnapshot, type CurrencyInfo, type CurrencySpec, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, type DeepPartial, DictionaryTranslator, type Dispose, EventBus, EventLog, type EventLogOptions, type EventMap, Fade, type ForbiddenMatch, type FreeSpinsFact, type GameFacts, type GameModeFact, type HostHooks, INFO_MENU_CSS, INFO_MENU_VARS, type JurisdictionConfig, type KnownControlId, LOCALE_LABELS, type LayoutConfig, type LayoutSpec, type LoggedEvent, type MenuSpec, type NoticeAction, type NoticeOptions, OpenUI, type OpenUIEvents, type OpenUIOptions, type Orientation, PanelControl, type PanelOptions, type PanelSpec, type PanelVariant, Parallel, type Placement, Pulse, RGS_ERROR_KEYS, ReadoutControl, type ReadoutKind, type ReadoutOptions, type RealityCheckOptions, type Rect, type ResponsiveKey, type ResponsiveOverride, type RgsBetConfig, type RgsErrorCode, type RgsErrorOptions, type RulesAuditIssue, type RulesDoc, type ScreenState, SelectControl, type SelectControlOptions, type SelectOption, Sequence, Signal, SliderControl, type SliderOptions, type SocialPhraseIssue, type SoundControls, type SpecIssue, SpinControl, type SpinPress, type SpinSpec, Squish, type StageRect, type StateDef, type StateMap, StepperControl, type StepperOptions, type Subscribable, type Theme, type ThemeChoice, type ThemeIssue, type ThemeOverrides, type ThemePreset, ToggleControl, type ToggleOptions, type Transition, type Translator, TurboControl, type TurboOptions, type TurboSpec, Turn, type UISpec, ValueDisplay, type ValueDisplayOptions, type ViewInspect, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, escapeHtml, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, renderBlocksHtml, renderRulesAuditHtml, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, richHtml, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
package/dist/index.js CHANGED
@@ -1410,6 +1410,9 @@ function validateSpec(spec) {
1410
1410
  return;
1411
1411
  }
1412
1412
  seeId(b.id, `${p}.id`);
1413
+ if (b.explains && b.explains !== "freeSpins" && spec.facts?.modes?.length && !spec.facts.modes.some((m) => m.id === b.explains)) {
1414
+ add("warn", `${p}.explains`, "explains-unknown-mode", `explains "${b.explains}" matches no declared mode id (facts.modes)`);
1415
+ }
1413
1416
  if (b.kind === "select" && (!b.options || b.options.length === 0)) {
1414
1417
  add("error", `${p}.options`, "select-empty", "a select block needs at least one option");
1415
1418
  }
@@ -1762,14 +1765,29 @@ function factsVars(facts, extra) {
1762
1765
  return extra ? { ...vars, ...extra } : vars;
1763
1766
  }
1764
1767
  function scanBlocks(blocks, resolve = (s) => s) {
1765
- const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false };
1768
+ const scan = { text: "", headings: "", covers: /* @__PURE__ */ new Set(), hasModeStats: false, hasLegal: false, sections: [] };
1769
+ let section = { heading: "", text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1770
+ scan.sections.push(section);
1766
1771
  const addText = (t) => {
1767
- if (typeof t === "string" && t) scan.text += `
1768
- ${resolve(t)}`;
1772
+ if (typeof t === "string" && t) {
1773
+ const r = resolve(t);
1774
+ scan.text += `
1775
+ ${r}`;
1776
+ section.text += `
1777
+ ${r}`;
1778
+ }
1769
1779
  };
1770
1780
  const walk = (list) => {
1771
1781
  for (const b of list ?? []) {
1772
- for (const c of b.covers ?? []) scan.covers.add(c);
1782
+ if (b.kind === "heading" || b.kind === "subheading") {
1783
+ section = { heading: resolve(b.text), text: "", explains: /* @__PURE__ */ new Set(), covers: /* @__PURE__ */ new Set() };
1784
+ scan.sections.push(section);
1785
+ }
1786
+ for (const c of b.covers ?? []) {
1787
+ scan.covers.add(c);
1788
+ section.covers.add(c);
1789
+ }
1790
+ if (b.explains) section.explains.add(b.explains);
1773
1791
  const anyB = b;
1774
1792
  switch (b.kind) {
1775
1793
  case "mode-stats":
@@ -1784,11 +1802,14 @@ ${resolve(t)}`;
1784
1802
  addText(b.text);
1785
1803
  break;
1786
1804
  case "heading":
1787
- case "subheading":
1805
+ case "subheading": {
1806
+ const r = resolve(b.text);
1788
1807
  scan.headings += `
1789
- ${resolve(b.text)}`;
1790
- addText(b.text);
1808
+ ${r}`;
1809
+ scan.text += `
1810
+ ${r}`;
1791
1811
  break;
1812
+ }
1792
1813
  case "text":
1793
1814
  addText(b.text);
1794
1815
  break;
@@ -1857,6 +1878,17 @@ function mentionsTimes(text, x) {
1857
1878
  return new RegExp(`\\b${grouped}${frac}\\s*[x\xD7]`, "i").test(text);
1858
1879
  }
1859
1880
  var mentionsName = (text, name) => text.toLowerCase().includes(name.toLowerCase());
1881
+ function mentionsCost(text, cost) {
1882
+ if (mentionsTimes(text, cost) || mentionsTimes(text, cost + 1)) return true;
1883
+ if (cost > 0 && cost < 1) return new RegExp(`\\b${Math.round(cost * 100)}\\s*%`).test(text);
1884
+ return false;
1885
+ }
1886
+ function findModeSection(sections, m) {
1887
+ return sections.find(
1888
+ (s) => s.explains.has(m.id) || s.covers.has(`feature:${m.id}`) || s.heading !== "" && m.name !== "" && mentionsName(s.heading, m.name) || (m.kind === "base" || !m.kind) && /how to play|about the game/i.test(s.heading)
1889
+ );
1890
+ }
1891
+ var MIN_SECTION_PROSE = 40;
1860
1892
  function auditRules(facts, rules, opts = {}) {
1861
1893
  const out = [];
1862
1894
  try {
@@ -1877,8 +1909,17 @@ function auditRules(facts, rules, opts = {}) {
1877
1909
  } else if (!(scan.hasModeStats || covered(`maxwin:${m.id}`) || nameIn && mentionsTimes(scan.text, m.maxWinX))) {
1878
1910
  add("required", "rules-missing-maxwin", `maxwin:${m.id}`, `The max win of \u201C${m.name}\u201D (${formatTimes(m.maxWinX)}) is not stated in the rules.`);
1879
1911
  }
1880
- if (m.kind && m.kind !== "base" && !(covered(`feature:${m.id}`) || nameIn)) {
1881
- add("required", "rules-missing-feature", `feature:${m.id}`, `The configured feature \u201C${m.name}\u201D is not described in the rules.`);
1912
+ if (!m.id) continue;
1913
+ const sec = findModeSection(scan.sections, m);
1914
+ if (!sec) {
1915
+ add("required", "rules-missing-mode-section", `section:${m.id}`, `\u201C${m.name}\u201D has no rules section of its own \u2014 add a heading naming it (or tag its blocks with explains: "${m.id}") and explain how the mode plays.`);
1916
+ } else {
1917
+ if (sec.text.trim().length < MIN_SECTION_PROSE) {
1918
+ add("required", "rules-mode-section-empty", `section:${m.id}`, `The \u201C${m.name}\u201D section has a heading but no real explanation \u2014 describe how the mode actually plays.`);
1919
+ }
1920
+ if (m.kind && m.kind !== "base" && m.cost != null && !(sec.covers.has(`cost:${m.id}`) || mentionsCost(sec.text, m.cost))) {
1921
+ add("required", "rules-mode-missing-cost", `cost:${m.id}`, `State what \u201C${m.name}\u201D costs (${formatTimes(m.cost)} the bet \u2014 e.g. via {{cost.${m.id}}}) inside its own section.`);
1922
+ }
1882
1923
  }
1883
1924
  }
1884
1925
  const fs = facts?.freeSpins;
@@ -2819,6 +2860,201 @@ function auditRulesDoc(doc, opts = {}) {
2819
2860
  return auditRules(facts, doc.blocks, { resolve });
2820
2861
  }
2821
2862
 
2863
+ // src/spec/rulesHtml.ts
2864
+ var escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2865
+ var richHtml = (s) => escapeHtml(s).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
2866
+ var esc = escapeHtml;
2867
+ var rich = richHtml;
2868
+ function statGridHtml(items, tr) {
2869
+ const rows = items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
2870
+ return `<dl class="ohm-stats">${rows}</dl>`;
2871
+ }
2872
+ function renderPaytable(rows, tr) {
2873
+ return rows.map((r) => {
2874
+ const icon = r.icon ? `<img class="ohm-symimg" src="${r.icon}" alt="" loading="lazy">` : `<span class="ohm-emoji">${esc(tr(r.symbol ?? ""))}</span>`;
2875
+ const lines = r.payouts.split("\n").map((line) => {
2876
+ const i = line.indexOf(":");
2877
+ return i >= 0 ? `<div><b>${esc(line.slice(0, i))}</b><span>${esc(line.slice(i + 1))}</span></div>` : `<div><span>${esc(line)}</span></div>`;
2878
+ }).join("");
2879
+ return `<div class="ohm-sym">${icon}<div class="ohm-pay">${lines}</div></div>`;
2880
+ }).join("");
2881
+ }
2882
+ function renderBlocksHtml(blocks, tr, facts) {
2883
+ const out = [];
2884
+ for (const b of blocks) {
2885
+ switch (b.kind) {
2886
+ case "text":
2887
+ out.push(`<p>${rich(tr(b.text))}</p>`);
2888
+ break;
2889
+ case "heading":
2890
+ out.push(`<div class="ohm-sec"><span>${esc(tr(b.text))}</span></div>`);
2891
+ break;
2892
+ case "subheading":
2893
+ out.push(`<h4 class="ohm-subh">${esc(tr(b.text))}</h4>`);
2894
+ break;
2895
+ case "legal":
2896
+ out.push(`<p class="ohm-legal">${rich(tr(b.text))}</p>`);
2897
+ break;
2898
+ case "divider":
2899
+ out.push('<hr class="ohm-hr">');
2900
+ break;
2901
+ case "image":
2902
+ out.push(`<img class="ohm-feature" alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`);
2903
+ break;
2904
+ case "media": {
2905
+ const img = `<img alt="${esc(tr(b.alt ?? ""))}" src="${b.src}" loading="lazy">`;
2906
+ const body = `<div class="ohm-media-body">${b.title ? `<h4>${esc(tr(b.title))}</h4>` : ""}<p>${rich(tr(b.text))}</p></div>`;
2907
+ out.push(`<div class="ohm-media ohm-media--${b.side ?? "left"}">${img}${body}</div>`);
2908
+ break;
2909
+ }
2910
+ case "cards": {
2911
+ const cards = b.items.map((it) => `<div class="ohm-fcard">${it.icon ? `<img src="${it.icon}" alt="" loading="lazy">` : ""}<h5>${esc(tr(it.title))}</h5>${it.text ? `<p>${rich(tr(it.text))}</p>` : ""}</div>`).join("");
2912
+ out.push(`<div class="ohm-cards">${cards}</div>`);
2913
+ break;
2914
+ }
2915
+ case "paytable":
2916
+ out.push(`<div class="ohm-grid">${renderPaytable(b.rows, tr)}</div>`);
2917
+ break;
2918
+ case "table": {
2919
+ const head = b.columns?.length ? `<thead><tr>${b.columns.map((c) => `<th>${esc(tr(c))}</th>`).join("")}</tr></thead>` : "";
2920
+ const body = b.rows.map((r) => `<tr>${r.map((c) => `<td>${esc(tr(c))}</td>`).join("")}</tr>`).join("");
2921
+ out.push(`<table class="ohm-table">${head}<tbody>${body}</tbody></table>`);
2922
+ break;
2923
+ }
2924
+ case "stat-grid":
2925
+ out.push(statGridHtml(b.items, tr));
2926
+ break;
2927
+ // The AUTO per-mode RTP / Max-win grid — rendered straight from the declared
2928
+ // game facts (+ any host extras), so the table can never drift from the config.
2929
+ // `modeStatsItems` localizes the label PARTS itself ("Max win" · the mode name),
2930
+ // so the grid renderer gets identity-tr — no double translation.
2931
+ case "mode-stats": {
2932
+ const extras = (b.extras ?? []).map((e) => ({ label: tr(e.label), value: tr(e.value) }));
2933
+ out.push(statGridHtml([...modeStatsItems(facts, tr), ...extras], (s) => s));
2934
+ break;
2935
+ }
2936
+ case "steps": {
2937
+ const items = b.items.map((s) => `<li>${rich(tr(s))}</li>`).join("");
2938
+ out.push(b.ordered ? `<ol class="ohm-steps">${items}</ol>` : `<ul class="ohm-steps">${items}</ul>`);
2939
+ break;
2940
+ }
2941
+ case "callout":
2942
+ out.push(`<div class="ohm-callout ohm-callout--${b.tone ?? "info"}">${b.title ? `<b>${esc(tr(b.title))}</b>` : ""}<p>${rich(tr(b.text))}</p></div>`);
2943
+ break;
2944
+ case "group":
2945
+ out.push(`<div class="ohm-group">${b.title ? `<h4 class="ohm-subh">${esc(tr(b.title))}</h4>` : ""}${renderBlocksHtml(b.children, tr, facts)}</div>`);
2946
+ break;
2947
+ }
2948
+ }
2949
+ return out.join("\n");
2950
+ }
2951
+ function renderRulesAuditHtml(issues, tr) {
2952
+ if (!issues.length) return "";
2953
+ const req = issues.filter((i) => i.level === "required");
2954
+ const rec = issues.filter((i) => i.level === "recommended");
2955
+ const list = (items) => `<ul>${items.map((i) => `<li>${esc(tr(i.message))}</li>`).join("")}</ul>`;
2956
+ return `<div class="ohm-audit" role="alert">
2957
+ <div class="ohm-audit-title">\u26A0 ${esc(tr("openui.rulesAudit.title"))}</div>
2958
+ ${req.length ? `<div class="ohm-audit-sub">${esc(tr("openui.rulesAudit.required"))}</div>${list(req)}` : ""}
2959
+ ${rec.length ? `<div class="ohm-audit-sub ohm-audit-sub--rec">${esc(tr("openui.rulesAudit.recommended"))}</div><div class="ohm-audit-rec">${list(rec)}</div>` : ""}
2960
+ </div>`;
2961
+ }
2962
+ var INFO_MENU_VARS = Object.freeze({
2963
+ "--accent": "#d99000",
2964
+ "--accent-text": "#1a1200",
2965
+ "--surface": "#ffffff",
2966
+ "--surface-alt": "#eef1f6",
2967
+ "--text": "#181b20",
2968
+ "--text-dim": "#5b6472",
2969
+ "--card-radius": "8px",
2970
+ "--font": "system-ui, sans-serif"
2971
+ });
2972
+ var INFO_MENU_CSS = `
2973
+ .ohm-root { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
2974
+ .ohm-root.open { opacity: 1; pointer-events: auto; }
2975
+ .ohm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }
2976
+ .ohm-root.open .ohm-backdrop { background: rgba(8,6,4,.34); backdrop-filter: blur(6px) saturate(1.1); -webkit-backdrop-filter: blur(6px) saturate(1.1); }
2977
+ .ohm-card { position: relative; width: min(92%, 1100px); max-height: 86vh; display: flex; flex-direction: column; background: var(--surface); color: var(--text); border: 1.5px solid #000; border-radius: var(--card-radius); box-shadow: 0 30px 80px rgba(0,0,0,.5); overflow: hidden; transform: translateY(8px) scale(.99); transition: transform .18s ease; }
2978
+ .ohm-root.open .ohm-card { transform: none; }
2979
+ .ohm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }
2980
+ .ohm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
2981
+ .ohm-body { padding: 24px 26px 26px; overflow-y: scroll; }
2982
+ .ohm-body::-webkit-scrollbar { width: 18px; }
2983
+ .ohm-body::-webkit-scrollbar-track { background: transparent; margin: 12px 0; }
2984
+ .ohm-body::-webkit-scrollbar-thumb { background-color: #111; border: 6px solid transparent; background-clip: padding-box; border-radius: 999px; min-height: 44px; }
2985
+ .ohm-body::-webkit-scrollbar-thumb:hover { background-color: #000; }
2986
+ .ohm-logo { display: block; margin: 6px auto 18px; max-width: 64%; height: auto; }
2987
+ .ohm-logo-text { margin: 6px 0 18px; text-align: center; font-size: 30px; font-weight: 900; letter-spacing: 1px; color: var(--text); }
2988
+ .ohm-sec { display: flex; align-items: center; gap: 14px; margin: 26px 0 14px; color: var(--text); font-weight: 800; letter-spacing: 1px; }
2989
+ .ohm-sec::before, .ohm-sec::after { content: ""; flex: 1; height: 2px; background: color-mix(in srgb, var(--text) 80%, transparent); border-radius: 2px; }
2990
+ .ohm-root *, .ohm-root *::before, .ohm-root *::after { box-sizing: border-box; }
2991
+ .ohm-row { display: flex; align-items: center; gap: 16px; margin: 14px 0; font-weight: 700; }
2992
+ .ohm-setting { margin: 14px 0; }
2993
+ .ohm-setting .ohm-row { margin: 0; }
2994
+ .ohm-hint { margin: 3px 0 0; font-size: 12.5px; font-weight: 500; color: var(--text-dim); }
2995
+ .ohm-row > span:first-child { flex: none; min-width: 110px; }
2996
+ .ohm-row input[type=range] { flex: 1; min-width: 0; max-width: min(440px, 60dvw); accent-color: var(--accent); height: 6px; }
2997
+ .ohm-row select { flex: 1; min-width: 0; max-width: min(440px, 60dvw); padding: 11px 14px; border-radius: 4px; border: 2px solid var(--accent); background: var(--surface-alt); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer; }
2998
+ .ohm-row select option { background: var(--surface); color: var(--text); font-weight: 600; }
2999
+ .ohm-row select option:checked { background: var(--accent); color: var(--accent-text); }
3000
+ .ohm-ctl { flex: 1; min-width: 0; max-width: min(440px, 60dvw); display: flex; align-items: center; justify-content: flex-end; }
3001
+ .ohm-check input[type=checkbox] { appearance: none; -webkit-appearance: none; width: 50px; height: 28px; border-radius: 999px; background: color-mix(in srgb, var(--text-dim) 38%, transparent); position: relative; cursor: pointer; transition: background .15s; flex: none; }
3002
+ .ohm-check input[type=checkbox]:checked { background: var(--accent); }
3003
+ .ohm-check input[type=checkbox]::before { content: ""; position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 999px; background: #fff; transition: left .15s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
3004
+ .ohm-check input[type=checkbox]:checked::before { left: 25px; }
3005
+ .ohm-segmented { display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-alt); border-radius: 999px; border: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); }
3006
+ .ohm-seg { border: 0; background: transparent; color: var(--text-dim); font-weight: 700; font-size: 14px; padding: 8px 18px; border-radius: 999px; cursor: pointer; transition: background .12s, color .12s; }
3007
+ .ohm-seg.active { background: var(--accent); color: var(--accent-text); }
3008
+ .ohm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
3009
+ .ohm-sym { display: flex; align-items: center; gap: 16px; padding: 6px 4px; }
3010
+ .ohm-emoji { font-size: 48px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }
3011
+ .ohm-symimg { width: 56px; height: 56px; object-fit: contain; flex: none; }
3012
+ .ohm-pay { font-size: 13px; line-height: 1.6; }
3013
+ .ohm-pay div { display: flex; gap: 6px; }
3014
+ .ohm-pay b { min-width: 42px; }
3015
+ .ohm-pay span { color: var(--accent); font-weight: 700; }
3016
+ .ohm-body p { color: var(--text-dim); line-height: 1.6; }
3017
+ .ohm-body p b { color: var(--text); }
3018
+ .ohm-feature { display: block; width: 100%; height: auto; margin: 10px 0; }
3019
+ .ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
3020
+ .ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }
3021
+ .ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }
3022
+ .ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }
3023
+ .ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }
3024
+ .ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }
3025
+ .ohm-audit-sub--rec { color: #b07d09; }
3026
+ .ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }
3027
+ .ohm-audit li { margin: 3px 0; }
3028
+ .ohm-audit-rec ul { color: var(--text-dim); }
3029
+ .ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }
3030
+ .ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }
3031
+ .ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }
3032
+ .ohm-callout--warning b { color: #b07d09; }
3033
+ .ohm-subh { margin: 22px 0 8px; font-size: 15px; font-weight: 800; letter-spacing: .5px; color: var(--text); }
3034
+ .ohm-legal { font-size: 12px; line-height: 1.6; color: var(--text-dim); opacity: .85; }
3035
+ .ohm-hr { border: 0; border-top: 1px solid color-mix(in srgb, var(--text-dim) 30%, transparent); margin: 18px 0; }
3036
+ .ohm-media { display: flex; align-items: center; gap: 18px; margin: 14px 0; }
3037
+ .ohm-media--right { flex-direction: row-reverse; }
3038
+ .ohm-media > img { width: 40%; max-width: 320px; height: auto; flex: none; }
3039
+ .ohm-media-body { flex: 1; }
3040
+ .ohm-media-body h4 { margin: 0 0 6px; font-size: 16px; color: var(--text); }
3041
+ .ohm-media-body p { margin: 0; }
3042
+ .ohm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }
3043
+ .ohm-fcard { padding: 8px 6px; text-align: center; }
3044
+ .ohm-fcard img { display: block; width: 48px; height: 48px; margin: 0 auto 8px; }
3045
+ .ohm-fcard h5 { margin: 0 0 4px; font-size: 14px; color: var(--text); }
3046
+ .ohm-fcard p { margin: 0; font-size: 12px; line-height: 1.5; }
3047
+ .ohm-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
3048
+ .ohm-table th { text-align: left; padding: 8px 10px; font-weight: 800; color: var(--text); border-bottom: 2px solid color-mix(in srgb, var(--text-dim) 35%, transparent); }
3049
+ .ohm-table td { padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 18%, transparent); }
3050
+ .ohm-table td:first-child { color: var(--text); font-weight: 700; }
3051
+ .ohm-table td:not(:first-child) { color: var(--accent); font-weight: 700; }
3052
+ .ohm-steps { margin: 12px 0; padding-left: 22px; color: var(--text-dim); line-height: 1.7; }
3053
+ .ohm-steps li { margin: 5px 0; }
3054
+ .ohm-steps b { color: var(--text); }
3055
+ .ohm-group { margin: 8px 0; }
3056
+ `;
3057
+
2822
3058
  // src/spec/defaultHudSpec.ts
2823
3059
  var defaultHudSpec = Object.freeze({
2824
3060
  meta: { id: "open-ui-reference-hud", version: 1 },
@@ -2923,6 +3159,6 @@ function clampBet(amount, cfg, divisor = API_AMOUNT_DIVISOR) {
2923
3159
  return Math.min(max, Math.max(min, v));
2924
3160
  }
2925
3161
 
2926
- export { API_AMOUNT_DIVISOR, AutoplayControl, BLOCK_KINDS, ButtonControl, CURRENCY_TABLE, CardControl, Control, ControlRegistry, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, DictionaryTranslator, EventBus, EventLog, Fade, LOCALE_LABELS, OpenUI, PanelControl, Parallel, Pulse, RGS_ERROR_KEYS, ReadoutControl, SelectControl, Sequence, Signal, SliderControl, SpinControl, Squish, StepperControl, ToggleControl, TurboControl, Turn, ValueDisplay, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
3162
+ export { API_AMOUNT_DIVISOR, AutoplayControl, BLOCK_KINDS, ButtonControl, CURRENCY_TABLE, CardControl, Control, ControlRegistry, DEFAULT_MENU_TITLES, DEFAULT_NOTICE_ACTION, DictionaryTranslator, EventBus, EventLog, Fade, INFO_MENU_CSS, INFO_MENU_VARS, LOCALE_LABELS, OpenUI, PanelControl, Parallel, Pulse, RGS_ERROR_KEYS, ReadoutControl, SelectControl, Sequence, Signal, SliderControl, SpinControl, Squish, StepperControl, ToggleControl, TurboControl, Turn, ValueDisplay, applyJurisdiction, applyRulesDoc, auditRules, auditRulesDoc, breakpointFor, buildBetLadder, buildBlocks, buildPanel, buttonBlocks, checkSocialPhrases, clampBet, clampDecimals, clampDigits, clampMinDigits, collectSocialCopy, composeMenu, computeScreen, createUI, defaultHudSpec, defaultLayout, defaultLayoutConfig, defaultTheme, defineUI, dictionary, displayDigits, effect, errorBlocks, escapeHtml, extendTheme, factsVars, findForbiddenPhrases, formatAmount, formatAmountPrecise, formatRtp, formatTimes, installResponsive, integerDigits, isRulesDoc, isSafeColor, isSocialCurrency, landscapeDefaultLayouts, mergeFacts, modeStatsItems, neededDecimals, openuiDefaults, openuiSocialDefaults, portraitDefaultLayouts, renderBlocksHtml, renderRulesAuditHtml, resolveBetLadder, resolveCurrency, resolvePlacement, resolveTheme, resolveTurboModes, richHtml, safeAmount, sanitizeThemeOverrides, themePresets, validateSpec, valueFitMaxWidth };
2927
3163
  //# sourceMappingURL=index.js.map
2928
3164
  //# sourceMappingURL=index.js.map