@nebularstreams/overlay-sdk 1.0.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.
Files changed (133) hide show
  1. package/package.json +44 -0
  2. package/preview/assets/images/favicon.ico +0 -0
  3. package/preview/assets/images/favicon.png +0 -0
  4. package/preview/assets/images/login-right.webp +0 -0
  5. package/preview/assets/images/nebular.color.100h.png +0 -0
  6. package/preview/assets/images/nebular.color.white.100h.png +0 -0
  7. package/preview/assets/images/nebular.medallion.transparent..png +0 -0
  8. package/preview/assets/images/testcard.png +0 -0
  9. package/preview/assets/webfonts/fa-brands-400.woff2 +0 -0
  10. package/preview/assets/webfonts/fa-solid-900.woff2 +0 -0
  11. package/preview/bundle.min.js +6 -0
  12. package/preview/index.css +28 -0
  13. package/preview/index.html +23 -0
  14. package/readme.md +132 -0
  15. package/skill/nebular-overlay/SKILL.md +56 -0
  16. package/skill/nebular-overlay/agents/openai.yaml +4 -0
  17. package/skill/nebular-overlay/references/examples.md +327 -0
  18. package/skill/nebular-overlay/references/manifest.md +153 -0
  19. package/skill/nebular-overlay/references/runtime-and-design.md +88 -0
  20. package/skill/nebular-overlay/scripts/validate-templates.mjs +244 -0
  21. package/templates/all/broadcast/breaking-news/index.html +100 -0
  22. package/templates/all/broadcast/breaking-news/manifest.json +9 -0
  23. package/templates/all/broadcast/empty/index.html +46 -0
  24. package/templates/all/broadcast/empty/manifest.json +7 -0
  25. package/templates/all/broadcast/empty/nebular.medallion.transparent.png +0 -0
  26. package/templates/all/broadcast/event-countdown/index.html +160 -0
  27. package/templates/all/broadcast/event-countdown/logo.png +0 -0
  28. package/templates/all/broadcast/event-countdown/manifest.json +33 -0
  29. package/templates/all/broadcast/lower-third/index.html +77 -0
  30. package/templates/all/broadcast/lower-third/manifest.json +8 -0
  31. package/templates/all/broadcast/newsticker/index.html +153 -0
  32. package/templates/all/broadcast/newsticker/manifest.json +21 -0
  33. package/templates/all/broadcast/newsticker-frames/background-label-green.png +0 -0
  34. package/templates/all/broadcast/newsticker-frames/background-label-orange.png +0 -0
  35. package/templates/all/broadcast/newsticker-frames/banner.png +0 -0
  36. package/templates/all/broadcast/newsticker-frames/index.html +229 -0
  37. package/templates/all/broadcast/newsticker-frames/manifest.json +20 -0
  38. package/templates/all/broadcast/weather/index.html +382 -0
  39. package/templates/all/broadcast/weather/manifest.json +104 -0
  40. package/templates/all/engagement/call-to-action/index.html +116 -0
  41. package/templates/all/engagement/call-to-action/manifest.json +12 -0
  42. package/templates/all/engagement/chat/index.html +121 -0
  43. package/templates/all/engagement/chat/manifest.json +32 -0
  44. package/templates/all/engagement/post/index.html +132 -0
  45. package/templates/all/engagement/post/manifest.json +31 -0
  46. package/templates/all/gambling/bingo/index.html +227 -0
  47. package/templates/all/gambling/bingo/manifest.json +144 -0
  48. package/templates/all/gambling/keno/index.html +202 -0
  49. package/templates/all/gambling/keno/manifest.json +129 -0
  50. package/templates/all/gambling/lottery/index.html +159 -0
  51. package/templates/all/gambling/lottery/manifest.json +95 -0
  52. package/templates/all/gambling/prize-wall/index.html +256 -0
  53. package/templates/all/gambling/prize-wall/manifest.json +108 -0
  54. package/templates/all/gambling/roulette/index.html +200 -0
  55. package/templates/all/gambling/roulette/manifest.json +78 -0
  56. package/templates/all/gaming/battleship/index.html +578 -0
  57. package/templates/all/gaming/battleship/manifest.json +279 -0
  58. package/templates/all/gaming/gta/mission/index.html +121 -0
  59. package/templates/all/gaming/gta/mission/manifest.json +38 -0
  60. package/templates/all/sports/football/scoreboard/index.html +115 -0
  61. package/templates/all/sports/football/scoreboard/manifest.json +54 -0
  62. package/templates/all/sports/tennis/scoreboard/index.html +120 -0
  63. package/templates/all/sports/tennis/scoreboard/manifest.json +92 -0
  64. package/templates/all/trivia/audience-poll/index.html +60 -0
  65. package/templates/all/trivia/audience-poll/manifest.json +26 -0
  66. package/templates/all/trivia/classic-quiz/index.html +49 -0
  67. package/templates/all/trivia/classic-quiz/manifest.json +15 -0
  68. package/templates/all/trivia/lightning-round/index.html +126 -0
  69. package/templates/all/trivia/lightning-round/manifest.json +27 -0
  70. package/templates/all/trivia/pasapalabra/index.html +249 -0
  71. package/templates/all/trivia/pasapalabra/manifest.json +68 -0
  72. package/templates/all/trivia/quiz-deck/index.html +58 -0
  73. package/templates/all/trivia/quiz-deck/manifest.json +25 -0
  74. package/templates/all/trivia/true-or-false/index.html +81 -0
  75. package/templates/all/trivia/true-or-false/manifest.json +27 -0
  76. package/templates/basic/broadcast/empty/index.html +46 -0
  77. package/templates/basic/broadcast/empty/manifest.json +7 -0
  78. package/templates/basic/broadcast/empty/nebular.medallion.transparent.png +0 -0
  79. package/templates/basic/broadcast/event-countdown/index.html +160 -0
  80. package/templates/basic/broadcast/event-countdown/logo.png +0 -0
  81. package/templates/basic/broadcast/event-countdown/manifest.json +33 -0
  82. package/templates/basic/broadcast/lower-third/index.html +77 -0
  83. package/templates/basic/broadcast/lower-third/manifest.json +8 -0
  84. package/templates/basic/broadcast/newsticker/index.html +153 -0
  85. package/templates/basic/broadcast/newsticker/manifest.json +21 -0
  86. package/templates/basic/broadcast/weather/index.html +382 -0
  87. package/templates/basic/broadcast/weather/manifest.json +104 -0
  88. package/templates/basic/engagement/call-to-action/index.html +116 -0
  89. package/templates/basic/engagement/call-to-action/manifest.json +12 -0
  90. package/templates/basic/engagement/chat/index.html +121 -0
  91. package/templates/basic/engagement/chat/manifest.json +32 -0
  92. package/templates/basic/engagement/post/index.html +132 -0
  93. package/templates/basic/engagement/post/manifest.json +31 -0
  94. package/templates/basic/sports/football/scoreboard/index.html +115 -0
  95. package/templates/basic/sports/football/scoreboard/manifest.json +54 -0
  96. package/templates/basic/sports/tennis/scoreboard/index.html +120 -0
  97. package/templates/basic/sports/tennis/scoreboard/manifest.json +92 -0
  98. package/templates/debug/testing/security-probe/forbidden.js +1 -0
  99. package/templates/debug/testing/security-probe/forbidden.json +1 -0
  100. package/templates/debug/testing/security-probe/forbidden.mp4 +1 -0
  101. package/templates/debug/testing/security-probe/forbidden.svg +1 -0
  102. package/templates/debug/testing/security-probe/index.html +367 -0
  103. package/templates/debug/testing/security-probe/manifest.json +13 -0
  104. package/templates/empty/broadcast/empty/index.html +46 -0
  105. package/templates/empty/broadcast/empty/manifest.json +7 -0
  106. package/templates/empty/broadcast/empty/nebular.medallion.transparent.png +0 -0
  107. package/templates/games/gambling/bingo/index.html +227 -0
  108. package/templates/games/gambling/bingo/manifest.json +56 -0
  109. package/templates/games/gambling/keno/index.html +202 -0
  110. package/templates/games/gambling/keno/manifest.json +129 -0
  111. package/templates/games/gambling/lottery/index.html +159 -0
  112. package/templates/games/gambling/lottery/manifest.json +95 -0
  113. package/templates/games/gambling/prize-wall/index.html +256 -0
  114. package/templates/games/gambling/prize-wall/manifest.json +108 -0
  115. package/templates/games/gambling/roulette/index.html +200 -0
  116. package/templates/games/gambling/roulette/manifest.json +78 -0
  117. package/templates/games/gaming/battleship/index.html +578 -0
  118. package/templates/games/gaming/battleship/manifest.json +279 -0
  119. package/templates/games/gaming/gta/mission/index.html +121 -0
  120. package/templates/games/gaming/gta/mission/manifest.json +38 -0
  121. package/templates/games/trivia/audience-poll/index.html +60 -0
  122. package/templates/games/trivia/audience-poll/manifest.json +26 -0
  123. package/templates/games/trivia/classic-quiz/index.html +49 -0
  124. package/templates/games/trivia/classic-quiz/manifest.json +15 -0
  125. package/templates/games/trivia/lightning-round/index.html +126 -0
  126. package/templates/games/trivia/lightning-round/manifest.json +27 -0
  127. package/templates/games/trivia/pasapalabra/index.html +249 -0
  128. package/templates/games/trivia/pasapalabra/manifest.json +68 -0
  129. package/templates/games/trivia/quiz-deck/index.html +58 -0
  130. package/templates/games/trivia/quiz-deck/manifest.json +25 -0
  131. package/templates/games/trivia/true-or-false/index.html +81 -0
  132. package/templates/games/trivia/true-or-false/manifest.json +27 -0
  133. package/tool/cli.cjs +7 -0
@@ -0,0 +1,256 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Prize Wall</title>
6
+ <style>
7
+ * {
8
+ box-sizing: border-box
9
+ }
10
+
11
+ html, body {
12
+ width: 100%;
13
+ height: 100%;
14
+ margin: 0;
15
+ overflow: hidden;
16
+ background: transparent
17
+ }
18
+
19
+ body {
20
+ font-family: Inter, system-ui, sans-serif;
21
+ color: #fff
22
+ }
23
+
24
+ .wall {
25
+ position: absolute;
26
+ inset: auto 5% 9% 5%;
27
+ display: grid;
28
+ grid-template-columns:1fr 21rem;
29
+ gap: 1rem
30
+ }
31
+
32
+ .board, .reveal {
33
+ background: #16121ff2;
34
+ border: 1px solid #ffffff2b;
35
+ border-radius: 1.2rem;
36
+ box-shadow: 0 1.5rem 4rem #0006
37
+ }
38
+
39
+ .board {
40
+ padding: 1.2rem
41
+ }
42
+
43
+ .head {
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ margin-bottom: 1rem
48
+ }
49
+
50
+ .head strong {
51
+ font-size: 1.7rem;
52
+ letter-spacing: .1em
53
+ }
54
+
55
+ .head span {
56
+ color: #baadc8;
57
+ font-size: 1.1rem;
58
+ font-weight: 750
59
+ }
60
+
61
+ .doors {
62
+ display: grid;
63
+ grid-template-columns:repeat(6, minmax(0, 1fr));
64
+ gap: .65rem
65
+ }
66
+
67
+ .door {
68
+ height: 6rem;
69
+ display: grid;
70
+ place-items: center;
71
+ border-radius: .6rem;
72
+ background: linear-gradient(145deg, #57306e, #27152f);
73
+ border: 2px solid #ce9cff44;
74
+ color: #e7d3ff;
75
+ font-size: 1.9rem;
76
+ font-weight: 950;
77
+ box-shadow: inset 0 0 0 .25rem #ffffff08
78
+ }
79
+
80
+ .door-icon {
81
+ display: none;
82
+ font-size: 2.65rem;
83
+ line-height: 1;
84
+ filter: grayscale(.2)
85
+ }
86
+
87
+ .door.open {
88
+ background: #141119;
89
+ color: #fff;
90
+ border-color: #ffffff18
91
+ }
92
+
93
+ .door.open .door-number,
94
+ .door.revealed .door-number {
95
+ display: none
96
+ }
97
+
98
+ .door.open .door-icon,
99
+ .door.revealed .door-icon {
100
+ display: block
101
+ }
102
+
103
+ .door.revealed {
104
+ background: #211929;
105
+ border-color: #8d729f55;
106
+ color: #cdbed7;
107
+ opacity: .78
108
+ }
109
+
110
+ .door.latest {
111
+ background: linear-gradient(145deg, #ffd95d, #e98914);
112
+ border-color: #fff0a6;
113
+ color: #2a1820;
114
+ transform: scale(1.08);
115
+ box-shadow: 0 .8rem 1.8rem #0008;
116
+ animation: open .45s ease
117
+ }
118
+
119
+ .reveal {
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ justify-content: center;
124
+ padding: 1.5rem;
125
+ text-align: center;
126
+ background: radial-gradient(circle at 50% 25%, #56336a, #17121f 68%)
127
+ }
128
+
129
+ .eyebrow {
130
+ color: #ffcf55;
131
+ font-size: 1.15rem;
132
+ font-weight: 850;
133
+ letter-spacing: .16em
134
+ }
135
+
136
+ .prize-number {
137
+ font-size: 5.5rem;
138
+ font-weight: 950;
139
+ line-height: 1;
140
+ margin: 1.1rem
141
+ }
142
+
143
+ .prize {
144
+ font-size: 2.2rem;
145
+ font-weight: 900
146
+ }
147
+
148
+ .reveal-icon {
149
+ margin-top: .5rem;
150
+ font-size: 3.2rem;
151
+ line-height: 1
152
+ }
153
+
154
+ .count {
155
+ margin-top: 1.5rem;
156
+ color: #ffc958;
157
+ font-size: 1.2rem;
158
+ font-weight: 800
159
+ }
160
+
161
+ @keyframes open {
162
+ 0% {
163
+ transform: perspective(20rem) rotateY(85deg)
164
+ }
165
+ 100% {
166
+ transform: scale(1.08)
167
+ }
168
+ }
169
+ </style>
170
+ </head>
171
+ <body>
172
+ <main class="wall">
173
+ <section class="board">
174
+ <div class="head"><strong id="title">THE PRIZE WALL</strong><span>SELECT A DOOR TO REVEAL IT</span></div>
175
+ <div class="doors" id="doors"></div>
176
+ </section>
177
+ <aside class="reveal">
178
+ <div class="eyebrow">LATEST REVEAL</div>
179
+ <div class="prize-number" id="number">—</div>
180
+ <div class="prize" id="prize">No Prize</div>
181
+ <div class="reveal-icon" id="revealIcon">🕸️</div>
182
+ <div class="count" id="count">0 of 30 opened</div>
183
+ </aside>
184
+ </main>
185
+ <script>
186
+ const
187
+ ALL = Array.from({length: 30}, (_, i) => i + 1),
188
+ ICONS = {
189
+ none: "🕸️",
190
+ gift: "🎁",
191
+ car: "🚘",
192
+ plane: "✈️",
193
+ money: "💰",
194
+ gem: "💎",
195
+ tv: "📺",
196
+ phone: "📱",
197
+ ticket: "🎟️",
198
+ suitcase: "🧳",
199
+ star: "⭐"
200
+ },
201
+ $ = id => document.getElementById(id),
202
+ doors = new Map(),
203
+ state = {prizes: [], opened: [], reveal: "hidden"};
204
+
205
+ ALL.forEach(number => {
206
+ const
207
+ door = document.createElement("div"),
208
+ doorNumber = document.createElement("span"),
209
+ doorIcon = document.createElement("span");
210
+ door.className = "door";
211
+ doorNumber.className = "door-number";
212
+ doorNumber.textContent = number;
213
+ doorIcon.className = "door-icon";
214
+ door.append(doorNumber, doorIcon);
215
+ $("doors").append(door);
216
+ doors.set(number, {door, icon: doorIcon})
217
+ });
218
+
219
+ function render() {
220
+ const
221
+ opened = (Array.isArray(state.opened) ? state.opened : []).map(Number).filter(n => n >= 1 && n <= 30),
222
+ openedSet = new Set(opened),
223
+ revealAll = state.reveal === "revealed",
224
+ latest = opened.at(-1),
225
+ prizes = new Map((Array.isArray(state.prizes) ? state.prizes : [])
226
+ .map(prize => [Number(prize?.position), prize])
227
+ .filter(([position]) => position >= 1 && position <= 30)),
228
+ latestPrize = prizes.get(latest),
229
+ latestWon = latestPrize?.icon !== "none" && Boolean(latestPrize?.prize?.trim());
230
+
231
+ doors.forEach(({door, icon}, position) => {
232
+ const
233
+ prize = prizes.get(position),
234
+ won = prize?.icon !== "none" && Boolean(prize?.prize?.trim());
235
+ door.classList.toggle("open", openedSet.has(position));
236
+ door.classList.toggle("revealed", revealAll && !openedSet.has(position));
237
+ door.classList.toggle("latest", position === latest);
238
+ icon.textContent = won ? (ICONS[prize.icon] || ICONS.gift) : ICONS.none;
239
+ door.title = won ? prize.prize : "No Prize"
240
+ });
241
+ $("number").textContent = latest ?? "—";
242
+ $("prize").textContent = latestWon ? latestPrize.prize : "No Prize";
243
+ $("revealIcon").textContent = latestWon ? (ICONS[latestPrize.icon] || ICONS.gift) : ICONS.none;
244
+ $("count").textContent = `${openedSet.size} of 30 opened`
245
+ }
246
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
247
+ if (detail.content?.title != null) $("title").textContent = detail.content.title;
248
+ if (detail.game?.prizes != null) state.prizes = detail.game.prizes;
249
+ if (detail.game?.opened != null) state.opened = detail.game.opened;
250
+ if (detail.game?.reveal != null) state.reveal = detail.game.reveal;
251
+ if (detail.game?.prizes != null || detail.game?.opened != null || detail.game?.reveal != null) render()
252
+ });
253
+
254
+ </script>
255
+ </body>
256
+ </html>
@@ -0,0 +1,108 @@
1
+ {
2
+ "instructions": "Configure each prize with its wall position, name and icon before play. During the game, select every opened door and press Update; the latest selected door displays its assigned prize automatically. When the game ends, reveal the remaining doors to show where every prize was hidden. Use Reset wall to close every door and hide the remaining prizes, then press Update.",
3
+ "meta": {
4
+ "content/title": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Wall title", "hint": "Enter wall title",
8
+ "default": "THE PRIZE WALL"
9
+ },
10
+ "game/prizes": {
11
+ "type": "set",
12
+ "phase": "edit",
13
+ "label": "Prizes", "hint": "Assign prizes to positions on the wall",
14
+ "container": "big",
15
+ "max": 30,
16
+ "default": [
17
+ {"position": 2, "prize": "Buick Limousine", "icon": "car"},
18
+ {"position": 7, "prize": "Tropical Holiday", "icon": "plane"},
19
+ {"position": 12, "prize": "$10,000 Cash", "icon": "money"},
20
+ {"position": 19, "prize": "Diamond Necklace", "icon": "gem"},
21
+ {"position": 25, "prize": "Home Cinema", "icon": "tv"}
22
+ ],
23
+ "meta": {
24
+ "position": {"size": 2, "type": "number", "label": "Position", "hint": "1–30", "min": 1, "max": 30},
25
+ "prize": {"size": 6, "type": "string", "label": "Prize", "hint": "Enter prize name"},
26
+ "icon": {
27
+ "size": 3,
28
+ "type": "select",
29
+ "label": "Icon", "hint": "Symbol displayed inside the opened door",
30
+ "choose": [
31
+ {"value": "none", "caption": "No prize"},
32
+ {"value": "gift", "caption": "Gift"},
33
+ {"value": "car", "caption": "Car"},
34
+ {"value": "plane", "caption": "Holiday"},
35
+ {"value": "money", "caption": "Cash"},
36
+ {"value": "gem", "caption": "Jewellery"},
37
+ {"value": "tv", "caption": "Television"},
38
+ {"value": "phone", "caption": "Phone"},
39
+ {"value": "ticket", "caption": "Tickets"},
40
+ {"value": "suitcase", "caption": "Luggage"},
41
+ {"value": "star", "caption": "Special prize"}
42
+ ],
43
+ "default": "none"
44
+ }
45
+ }
46
+ },
47
+ "game/opened": {
48
+ "type": "multi",
49
+ "phase": "play",
50
+ "label": "Opened doors", "hint": "Doors that have already been opened",
51
+ "container": "big",
52
+ "choose": [
53
+ 1,
54
+ 2,
55
+ 3,
56
+ 4,
57
+ 5,
58
+ 6,
59
+ 7,
60
+ 8,
61
+ 9,
62
+ 10,
63
+ 11,
64
+ 12,
65
+ 13,
66
+ 14,
67
+ 15,
68
+ 16,
69
+ 17,
70
+ 18,
71
+ 19,
72
+ 20,
73
+ 21,
74
+ 22,
75
+ 23,
76
+ 24,
77
+ 25,
78
+ 26,
79
+ 27,
80
+ 28,
81
+ 29,
82
+ 30
83
+ ],
84
+ "cols": 6,
85
+ "rows": 5,
86
+ "responsive": true,
87
+ "minCellWidth": "2.54rem",
88
+ "default": []
89
+ },
90
+ "game/reveal": {
91
+ "type": "choose",
92
+ "phase": "play",
93
+ "label": "Remaining prizes", "hint": "Reveal every unopened position when the game ends",
94
+ "choose": ["hidden", "revealed"],
95
+ "default": "hidden"
96
+ },
97
+ "game/reset": {
98
+ "type": "reset",
99
+ "phase": "play",
100
+ "icon": "undo",
101
+ "text": "Reset wall", "hint": "Close every door and reset the wall",
102
+ "values": {
103
+ "game/opened": [],
104
+ "game/reveal": "hidden"
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,200 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Roulette Hit Map</title>
6
+ <style>
7
+ * {
8
+ box-sizing: border-box
9
+ }
10
+
11
+ html, body {
12
+ width: 100%;
13
+ height: 100%;
14
+ margin: 0;
15
+ overflow: hidden;
16
+ background: transparent
17
+ }
18
+
19
+ body {
20
+ font-family: Inter, system-ui, sans-serif;
21
+ color: #fff
22
+ }
23
+
24
+ .panel {
25
+ position: absolute;
26
+ left: 5%;
27
+ right: 5%;
28
+ bottom: 7%;
29
+ display: grid;
30
+ grid-template-columns:16rem 1fr;
31
+ background: #071713f2;
32
+ border: 1px solid #ffffff2b;
33
+ border-radius: 1.2rem;
34
+ overflow: hidden;
35
+ box-shadow: 0 1.5rem 4rem #0007
36
+ }
37
+
38
+ .hero {
39
+ display: grid;
40
+ place-items: center;
41
+ align-content: center;
42
+ padding: 1.5rem;
43
+ background: radial-gradient(circle at 50% 35%, #194d3b, #06130f 68%);
44
+ border-right: 1px solid #ffffff1c
45
+ }
46
+
47
+ .eyebrow {
48
+ color: #d9b95f;
49
+ font-size: .76rem;
50
+ font-weight: 850;
51
+ letter-spacing: .18em
52
+ }
53
+
54
+ .ball {
55
+ width: 9rem;
56
+ height: 9rem;
57
+ margin: 1.2rem;
58
+ display: grid;
59
+ place-items: center;
60
+ border-radius: 50%;
61
+ background: #16191d;
62
+ border: .6rem solid #e7d08a;
63
+ font-size: 4rem;
64
+ font-weight: 950;
65
+ box-shadow: 0 1rem 2.5rem #0008
66
+ }
67
+
68
+ .ball.red {
69
+ background: #bd2733
70
+ }
71
+
72
+ .ball.black {
73
+ background: #17191d
74
+ }
75
+
76
+ .ball.green {
77
+ background: #087b4e
78
+ }
79
+
80
+ .title {
81
+ font-size: 1.2rem;
82
+ font-weight: 900
83
+ }
84
+
85
+ .count {
86
+ margin-top: .35rem;
87
+ color: #b4beb9;
88
+ font-size: .8rem
89
+ }
90
+
91
+ .table {
92
+ padding: 1rem
93
+ }
94
+
95
+ .header {
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ margin-bottom: .8rem
100
+ }
101
+
102
+ .header strong {
103
+ letter-spacing: .12em
104
+ }
105
+
106
+ .legend {
107
+ color: #aab6b0;
108
+ font-size: .72rem
109
+ }
110
+
111
+ .numbers {
112
+ display: grid;
113
+ grid-template-columns:repeat(13, minmax(0, 1fr));
114
+ gap: .42rem
115
+ }
116
+
117
+ .number {
118
+ height: 3.7rem;
119
+ display: grid;
120
+ place-items: center;
121
+ border-radius: .42rem;
122
+ background: #17191d;
123
+ border: 1px solid #ffffff20;
124
+ color: #ffffff69;
125
+ font-weight: 850
126
+ }
127
+
128
+ .number.zero {
129
+ grid-row: 1/span 3;
130
+ height: auto;
131
+ background: #087b4e
132
+ }
133
+
134
+ .number.hit {
135
+ color: #fff;
136
+ border-color: #e5cb78;
137
+ box-shadow: inset 0 0 0 .12rem #e5cb7855
138
+ }
139
+
140
+ .number.red.hit {
141
+ background: #8d202a
142
+ }
143
+
144
+ .number.black.hit {
145
+ background: #090b0e
146
+ }
147
+
148
+ .number.latest {
149
+ color: #172019;
150
+ background: #e7cf80 !important;
151
+ transform: scale(1.08);
152
+ box-shadow: 0 .6rem 1.3rem #0008;
153
+ z-index: 2
154
+ }
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <main class="panel">
159
+ <section class="hero">
160
+ <div class="eyebrow">LATEST HIT</div>
161
+ <div class="ball" id="ball">17</div>
162
+ <div class="title" id="title">Roulette Heat Map</div>
163
+ <div class="count" id="count">8 unique numbers</div>
164
+ </section>
165
+ <section class="table">
166
+ <div class="header"><strong>TABLE HISTORY</strong><span class="legend">GOLD · LATEST&nbsp;&nbsp; COLOURED · PREVIOUS</span></div>
167
+ <div class="numbers" id="numbers"></div>
168
+ </section>
169
+ </main>
170
+ <script>
171
+ const RED = new Set([1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36]), ALL = Array.from({length: 37}, (_, i) => i);
172
+ const $ = id => document.getElementById(id), cells = new Map();
173
+ ALL.forEach(number => {
174
+ const cell = document.createElement("div");
175
+ cell.className = `number ${number === 0 ? "zero green" : RED.has(number) ? "red" : "black"}`;
176
+ cell.textContent = number;
177
+ $("numbers").append(cell);
178
+ cells.set(number, cell)
179
+ });
180
+
181
+ function update(history) {
182
+ const values = (Array.isArray(history) ? history : []).map(Number).filter(n => n >= 0 && n <= 36), hits = new Set(values), latest = values.at(-1) ?? 0;
183
+ cells.forEach((cell, n) => {
184
+ cell.classList.toggle("hit", hits.has(n));
185
+ cell.classList.toggle("latest", n === latest)
186
+ });
187
+ const ball = $("ball");
188
+ ball.textContent = latest;
189
+ ball.className = `ball ${latest === 0 ? "green" : RED.has(latest) ? "red" : "black"}`;
190
+ $("count").textContent = `${hits.size} unique ${hits.size === 1 ? "number" : "numbers"}`
191
+ }
192
+
193
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
194
+ if (detail.content?.title != null) $("title").textContent = detail.content.title;
195
+ if (detail.game?.history != null) update(detail.game.history)
196
+ });
197
+
198
+ </script>
199
+ </body>
200
+ </html>
@@ -0,0 +1,78 @@
1
+ {
2
+ "instructions": "Select the roulette numbers represented in the current table history and edit the title if desired. Press Update after the latest result to refresh the heat-map display. Use Reset table to clear the history, then press Update to publish the reset.",
3
+ "meta": {
4
+ "content/title": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Title", "hint": "Enter title",
8
+ "default": "Roulette Heat Map"
9
+ },
10
+ "game/history": {
11
+ "type": "multi",
12
+ "phase": "play",
13
+ "container": "big",
14
+ "label": "Numbers hit", "hint": "Winning numbers in the order they were called",
15
+ "choose": [
16
+ 0,
17
+ 1,
18
+ 2,
19
+ 3,
20
+ 4,
21
+ 5,
22
+ 6,
23
+ 7,
24
+ 8,
25
+ 9,
26
+ 10,
27
+ 11,
28
+ 12,
29
+ 13,
30
+ 14,
31
+ 15,
32
+ 16,
33
+ 17,
34
+ 18,
35
+ 19,
36
+ 20,
37
+ 21,
38
+ 22,
39
+ 23,
40
+ 24,
41
+ 25,
42
+ 26,
43
+ 27,
44
+ 28,
45
+ 29,
46
+ 30,
47
+ 31,
48
+ 32,
49
+ 33,
50
+ 34,
51
+ 35,
52
+ 36
53
+ ],
54
+ "cols": 12,
55
+ "rows": 4,
56
+ "responsive": true,
57
+ "minCellWidth": "2.54rem",
58
+ "default": [
59
+ 32,
60
+ 15,
61
+ 0,
62
+ 19,
63
+ 4,
64
+ 21,
65
+ 17
66
+ ]
67
+ },
68
+ "game/reset": {
69
+ "type": "reset",
70
+ "phase": "play",
71
+ "icon": "undo",
72
+ "text": "Reset table", "hint": "Clear the winning-number history",
73
+ "values": {
74
+ "game/history": []
75
+ }
76
+ }
77
+ }
78
+ }