@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,121 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Side Chat</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
+ .chat {
25
+ position: absolute;
26
+ right: 3%;
27
+ top: 8%;
28
+ width: 40rem;
29
+ max-height: 84%;
30
+ padding: 1.5rem;
31
+ background: #080c14e8;
32
+ border: 1px solid #ffffff29;
33
+ border-radius: 1rem;
34
+ box-shadow: 0 1.2rem 3rem #0006;
35
+ overflow: hidden
36
+ }
37
+
38
+ .header {
39
+ display: flex;
40
+ justify-content: space-between;
41
+ padding: .2rem .25rem .85rem;
42
+ border-bottom: 1px solid #ffffff1f;
43
+ font-size: 1.7rem;
44
+ font-weight: 900
45
+ }
46
+
47
+ .live {
48
+ color: #52e39c;
49
+ font-size: 1.2rem;
50
+ font-weight: 900;
51
+ letter-spacing: .12em
52
+ }
53
+
54
+ .messages {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 1rem;
58
+ padding-top: 1.2rem
59
+ }
60
+
61
+ .message {
62
+ padding: 1.15rem 1.3rem;
63
+ background: #ffffff13;
64
+ border-radius: .8rem;
65
+ border-left: .22rem solid #ff9d18;
66
+ animation: enter .35s ease both
67
+ }
68
+
69
+ .author {
70
+ color: #ffb449;
71
+ font-size: 1.45rem;
72
+ font-weight: 900;
73
+ margin-bottom: .35rem
74
+ }
75
+
76
+ .text {
77
+ font-size: 1.75rem;
78
+ line-height: 1.35;
79
+ overflow-wrap: anywhere
80
+ }
81
+
82
+ @keyframes enter {
83
+ from {
84
+ opacity: 0;
85
+ transform: translateX(2rem)
86
+ }
87
+ }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <aside class="chat">
92
+ <div class="header"><span id="title">LIVE CHAT</span><span class="live">● LIVE</span></div>
93
+ <div class="messages" id="messages"></div>
94
+ </aside>
95
+ <script>
96
+ const $ = id => document.getElementById(id);
97
+ let lastMessage = "", limit = 5;
98
+
99
+ function addMessage(author, text) {
100
+ const signature = `${author}\u0000${text}`;
101
+ if (!text || signature === lastMessage) return;
102
+ lastMessage = signature;
103
+ const item = document.createElement("div"), name = document.createElement("div"), body = document.createElement("div");
104
+ item.className = "message";
105
+ name.className = "author";
106
+ body.className = "text";
107
+ name.textContent = author || "Viewer";
108
+ body.textContent = text;
109
+ item.append(name, body);
110
+ $("messages").append(item);
111
+ while ($("messages").children.length > limit) $("messages").firstElementChild.remove()
112
+ }
113
+
114
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
115
+ if (detail.chat?.title != null) $("title").textContent = detail.chat.title;
116
+ if (detail.chat?.maxMessages != null) limit = Math.max(1, Math.min(8, Number(detail.chat.maxMessages) || 5));
117
+ if (detail.message?.text != null) addMessage(detail.message.author, detail.message.text)
118
+ });
119
+ </script>
120
+ </body>
121
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "instructions": "Enter an author and message, then press Update to append it to the live chat. Set the maximum message count to control how many recent messages remain visible in the side panel.",
3
+ "meta": {
4
+ "chat/title": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Panel title", "hint": "Enter panel title",
8
+ "default": "LIVE CHAT"
9
+ },
10
+ "chat/maxMessages": {
11
+ "type": "number",
12
+ "phase": "edit",
13
+ "label": "Messages shown", "hint": "Enter maximum",
14
+ "min": 1,
15
+ "max": 8,
16
+ "default": 5
17
+ },
18
+ "message/author": {
19
+ "type": "string",
20
+ "phase": "play",
21
+ "label": "Author", "hint": "Enter author name",
22
+ "default": "Nebular viewer"
23
+ },
24
+ "message/text": {
25
+ "type": "string",
26
+ "phase": "play",
27
+ "label": "New message", "hint": "Enter message",
28
+ "default": "",
29
+ "clear": true
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,132 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Social Post</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
+ color: #16202a;
21
+ font-family: Inter, system-ui, sans-serif
22
+ }
23
+
24
+ .post {
25
+ position: absolute;
26
+ left: 50%;
27
+ bottom: 8%;
28
+ width: 64rem;
29
+ padding: 2rem 2.4rem;
30
+ border-radius: 1.1rem;
31
+ background: #fffffff7;
32
+ border: 1px solid #d9e1e8;
33
+ box-shadow: 0 1.4rem 4rem #00000047;
34
+ opacity: 0;
35
+ transform: translate(-50%, 2rem) scale(.97);
36
+ transition: opacity .35s ease, transform .35s ease
37
+ }
38
+
39
+ .post.visible {
40
+ opacity: 1;
41
+ transform: translate(-50%, 0) scale(1)
42
+ }
43
+
44
+ .top {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 1.3rem
48
+ }
49
+
50
+ .avatar {
51
+ width: 5.5rem;
52
+ height: 5.5rem;
53
+ border-radius: 50%;
54
+ display: grid;
55
+ place-items: center;
56
+ background: linear-gradient(135deg, #ff9d18, #ff4b55);
57
+ color: #fff;
58
+ font-size: 1.8rem;
59
+ font-weight: 900
60
+ }
61
+
62
+ .identity {
63
+ flex: 1
64
+ }
65
+
66
+ .name {
67
+ font-size: 2rem;
68
+ font-weight: 850
69
+ }
70
+
71
+ .handle {
72
+ color: #657786;
73
+ font-size: 1.5rem
74
+ }
75
+
76
+ .mark {
77
+ color: #1d9bf0;
78
+ font-size: 2.6rem;
79
+ font-weight: 900
80
+ }
81
+
82
+ .message {
83
+ margin-top: 1.4rem;
84
+ font-size: 2.35rem;
85
+ line-height: 1.45;
86
+ overflow-wrap: anywhere
87
+ }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <article class="post" id="post">
92
+ <div class="top">
93
+ <div class="avatar" id="avatar">NS</div>
94
+ <div class="identity">
95
+ <div class="name" id="name">Nebular Streams</div>
96
+ <div class="handle" id="handle">@nebularstreams</div>
97
+ </div>
98
+ <div class="mark">✦</div>
99
+ </div>
100
+ <div class="message" id="message">Broadcast graphics, beautifully simple.</div>
101
+ </article>
102
+ <script>
103
+
104
+ let timer, lastMessage = "";
105
+
106
+ const
107
+ $ = id => document.getElementById(id),
108
+ initials = name => String(name || "NS").trim().split(/\s+/).slice(0, 2).map(word => word[0]).join("").toUpperCase();
109
+
110
+ function show(duration) {
111
+ clearTimeout(timer);
112
+ $("post").classList.remove("visible");
113
+ requestAnimationFrame(() => requestAnimationFrame(() => $("post").classList.add("visible")));
114
+ timer = setTimeout(() => $("post").classList.remove("visible"), Math.max(1, duration) * 1000)
115
+ }
116
+
117
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
118
+ if (detail.author?.name != null) {
119
+ $("name").textContent = detail.author.name;
120
+ $("avatar").textContent = initials(detail.author.name)
121
+ }
122
+ if (detail.author?.handle != null) $("handle").textContent = detail.author.handle;
123
+ if (detail.post?.message != null) $("message").textContent = detail.post.message;
124
+ const signature = `${detail.author?.handle || ""}\u0000${detail.post?.message || ""}`;
125
+ if (detail.post?.message != null && signature !== lastMessage) {
126
+ lastMessage = signature;
127
+ show(Number(detail.post?.duration) || 8)
128
+ }
129
+ });
130
+ </script>
131
+ </body>
132
+ </html>
@@ -0,0 +1,31 @@
1
+ {
2
+ "instructions": "Enter the author, handle and post text, choose how long it should remain visible, then press Update. Each update presents the post and hides it automatically after the selected duration.",
3
+ "meta": {
4
+ "author/name": {
5
+ "type": "string",
6
+ "phase": "play",
7
+ "label": "Display name", "hint": "Enter display name",
8
+ "default": "Nebular Streams"
9
+ },
10
+ "author/handle": {
11
+ "type": "string",
12
+ "phase": "play",
13
+ "label": "Handle", "hint": "e.g. @channel",
14
+ "default": "@nebularstreams"
15
+ },
16
+ "post/message": {
17
+ "type": "string",
18
+ "phase": "play",
19
+ "label": "Post", "hint": "Enter post text",
20
+ "default": "Broadcast graphics, beautifully simple."
21
+ },
22
+ "post/duration": {
23
+ "type": "number",
24
+ "phase": "edit",
25
+ "label": "Visible seconds", "hint": "Enter seconds",
26
+ "min": 1,
27
+ "max": 60,
28
+ "default": 8
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,227 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Bingo Caller Board</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
+ color: #fff;
21
+ font-family: Inter, system-ui, sans-serif;
22
+ }
23
+
24
+ .bingo {
25
+ position: absolute;
26
+ inset: auto 5% 9% 5%;
27
+ display: grid;
28
+ grid-template-columns: 22rem minmax(0, 1fr);
29
+ gap: 1rem;
30
+ filter: drop-shadow(0 1.4rem 3rem rgba(0, 0, 0, .34));
31
+ }
32
+
33
+ .caller,
34
+ .board {
35
+ background: rgba(8, 13, 22, .94);
36
+ border: 1px solid rgba(255, 255, 255, .16);
37
+ border-radius: 1.2rem;
38
+ }
39
+
40
+ .caller {
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: center;
44
+ justify-content: center;
45
+ min-height: 38rem;
46
+ padding: 1.5rem;
47
+ text-align: center;
48
+ }
49
+
50
+ .eyebrow {
51
+ color: #ffb33f;
52
+ font-size: 1.2rem;
53
+ font-weight: 850;
54
+ letter-spacing: .18em;
55
+ text-transform: uppercase;
56
+ }
57
+
58
+ .ball {
59
+ width: 12rem;
60
+ height: 12rem;
61
+ margin: 1.4rem 0;
62
+ display: grid;
63
+ place-items: center;
64
+ border-radius: 50%;
65
+ background: radial-gradient(circle at 34% 28%, #fff8b3 0 8%, #ffc848 34%, #f18b12 72%);
66
+ border: .55rem solid rgba(255, 255, 255, .85);
67
+ box-shadow: inset -.8rem -1rem 1.5rem rgba(132, 51, 0, .3), 0 1rem 2.5rem rgba(255, 151, 21, .25);
68
+ color: #172033;
69
+ font-size: 5.2rem;
70
+ font-weight: 950;
71
+ animation: called .45s cubic-bezier(.18, .89, .32, 1.28);
72
+ }
73
+
74
+ .title {
75
+ font-size: 2rem;
76
+ font-weight: 900;
77
+ }
78
+
79
+ .subtitle {
80
+ margin-top: .35rem;
81
+ color: #aeb8c8;
82
+ font-size: 1.3rem;
83
+ line-height: 1.3;
84
+ }
85
+
86
+ .count {
87
+ margin-top: 1.4rem;
88
+ color: #ffbd53;
89
+ font-size: 1.2rem;
90
+ font-weight: 800;
91
+ letter-spacing: .08em;
92
+ text-transform: uppercase;
93
+ }
94
+
95
+ .board {
96
+ min-height: 38rem;
97
+ padding: 1rem;
98
+ }
99
+
100
+ .board-header {
101
+ display: flex;
102
+ justify-content: space-between;
103
+ align-items: center;
104
+ padding: .15rem .25rem .9rem;
105
+ }
106
+
107
+ .board-header strong {
108
+ font-size: 1.4rem;
109
+ letter-spacing: .08em;
110
+ }
111
+
112
+ .legend {
113
+ color: #aeb8c8;
114
+ font-size: 1.05rem;
115
+ font-weight: 750;
116
+ }
117
+
118
+ .numbers {
119
+ display: grid;
120
+ grid-template-columns: repeat(10, minmax(0, 1fr));
121
+ gap: .45rem;
122
+ }
123
+
124
+ .number {
125
+ display: grid;
126
+ place-items: center;
127
+ border-radius: .55rem;
128
+ background: rgba(255, 255, 255, .06);
129
+ border: 1px solid rgba(255, 255, 255, .1);
130
+ color: rgba(255, 255, 255, .4);
131
+ font-size: 2rem;
132
+ font-weight: 850;
133
+ transition: background .25s ease, color .25s ease, transform .25s ease;
134
+ aspect-ratio: 1.9;
135
+ }
136
+
137
+ .number.called {
138
+ background: #fff;
139
+ border-color: #fff;
140
+ color: #172033;
141
+ }
142
+
143
+ .number.current {
144
+ z-index: 1;
145
+ background: #ff9d18;
146
+ border-color: #ffd486;
147
+ color: #111827;
148
+ transform: scale(1.14);
149
+ box-shadow: 0 0 0 .22rem rgba(255, 157, 24, .25), 0 .6rem 1.4rem rgba(0, 0, 0, .35);
150
+ }
151
+
152
+ @keyframes called {
153
+ from {
154
+ opacity: 0;
155
+ transform: scale(.55) rotate(-12deg);
156
+ }
157
+ }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <main class="bingo">
162
+ <section class="caller">
163
+ <div class="eyebrow" id="eyebrow">Just called</div>
164
+ <div class="ball" id="ball">73</div>
165
+ <div class="title" id="title">Friday Night Bingo</div>
166
+ <div class="subtitle" id="subtitle">Eyes down — good luck!</div>
167
+ <div class="count" id="count">8 numbers called</div>
168
+ </section>
169
+
170
+ <section class="board">
171
+ <div class="board-header">
172
+ <strong>CALLED NUMBERS</strong>
173
+ <span class="legend">WHITE · CALLED&nbsp;&nbsp;&nbsp; ORANGE · CURRENT</span>
174
+ </div>
175
+ <div class="numbers" id="numbers"></div>
176
+ </section>
177
+ </main>
178
+
179
+ <script>
180
+ const
181
+ MAX_NUMBER = 90,
182
+ $ = (id) => document.getElementById(id),
183
+ cells = new Map();
184
+
185
+ let called = [];
186
+
187
+ for (let number = 1; number <= MAX_NUMBER; number++) {
188
+ const cell = document.createElement("div");
189
+ cell.className = "number";
190
+ cell.textContent = number;
191
+ $("numbers").append(cell);
192
+ cells.set(number, cell);
193
+ }
194
+
195
+ function updateBoard() {
196
+ const
197
+ values = (Array.isArray(called) ? called : []).map(Number).filter(number => number >= 1 && number <= MAX_NUMBER),
198
+ currentNumber = values.at(-1),
199
+ calledNumbers = new Set(values);
200
+
201
+ cells.forEach((cell, number) => {
202
+ cell.classList.toggle("called", calledNumbers.has(number));
203
+ cell.classList.toggle("current", number === currentNumber);
204
+ });
205
+
206
+ const ballValue = currentNumber ?? "—";
207
+ if ($("ball").textContent !== String(ballValue)) {
208
+ const replacement = $("ball").cloneNode(false);
209
+ replacement.textContent = ballValue;
210
+ $("ball").replaceWith(replacement);
211
+ }
212
+
213
+ $("count").textContent = `${calledNumbers.size} ${calledNumbers.size === 1 ? "number" : "numbers"} called`;
214
+ }
215
+
216
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
217
+
218
+ if (detail.content?.eyebrow != null) $("eyebrow").textContent = detail.content.eyebrow;
219
+ if (detail.content?.title != null) $("title").textContent = detail.content.title;
220
+ if (detail.content?.subtitle != null) $("subtitle").textContent = detail.content.subtitle;
221
+ if (detail.game?.called != null) called = detail.game.called;
222
+ if (detail.game?.called != null) updateBoard();
223
+ })
224
+
225
+ </script>
226
+ </body>
227
+ </html>
@@ -0,0 +1,144 @@
1
+ {
2
+ "instructions": "Select each number as it is called, then press Update. The most recently selected number is highlighted and displayed automatically. Use Reset balls to clear the selection, then press Update to publish the reset. Update the event copy before play if desired.",
3
+ "meta": {
4
+ "content/eyebrow": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Current number label", "hint": "Enter short label",
8
+ "default": "JUST CALLED"
9
+ },
10
+ "content/title": {
11
+ "type": "string",
12
+ "phase": "edit",
13
+ "label": "Event title", "hint": "Enter event title",
14
+ "default": "Friday Night Bingo"
15
+ },
16
+ "content/subtitle": {
17
+ "type": "string",
18
+ "phase": "edit",
19
+ "label": "Subtitle", "hint": "Enter subtitle",
20
+ "default": "Eyes down — good luck!"
21
+ },
22
+ "game/called": {
23
+ "type": "multi",
24
+ "phase": "play",
25
+ "label": "Called numbers", "hint": "Numbers called so far; the latest selection is highlighted",
26
+ "container": "big",
27
+ "choose": [
28
+ 1,
29
+ 2,
30
+ 3,
31
+ 4,
32
+ 5,
33
+ 6,
34
+ 7,
35
+ 8,
36
+ 9,
37
+ 10,
38
+ 11,
39
+ 12,
40
+ 13,
41
+ 14,
42
+ 15,
43
+ 16,
44
+ 17,
45
+ 18,
46
+ 19,
47
+ 20,
48
+ 21,
49
+ 22,
50
+ 23,
51
+ 24,
52
+ 25,
53
+ 26,
54
+ 27,
55
+ 28,
56
+ 29,
57
+ 30,
58
+ 31,
59
+ 32,
60
+ 33,
61
+ 34,
62
+ 35,
63
+ 36,
64
+ 37,
65
+ 38,
66
+ 39,
67
+ 40,
68
+ 41,
69
+ 42,
70
+ 43,
71
+ 44,
72
+ 45,
73
+ 46,
74
+ 47,
75
+ 48,
76
+ 49,
77
+ 50,
78
+ 51,
79
+ 52,
80
+ 53,
81
+ 54,
82
+ 55,
83
+ 56,
84
+ 57,
85
+ 58,
86
+ 59,
87
+ 60,
88
+ 61,
89
+ 62,
90
+ 63,
91
+ 64,
92
+ 65,
93
+ 66,
94
+ 67,
95
+ 68,
96
+ 69,
97
+ 70,
98
+ 71,
99
+ 72,
100
+ 73,
101
+ 74,
102
+ 75,
103
+ 76,
104
+ 77,
105
+ 78,
106
+ 79,
107
+ 80,
108
+ 81,
109
+ 82,
110
+ 83,
111
+ 84,
112
+ 85,
113
+ 86,
114
+ 87,
115
+ 88,
116
+ 89,
117
+ 90
118
+ ],
119
+ "cols": 10,
120
+ "rows": 9,
121
+ "responsive": true,
122
+ "minCellWidth": "2.54rem",
123
+ "default": [
124
+ 3,
125
+ 8,
126
+ 17,
127
+ 24,
128
+ 31,
129
+ 42,
130
+ 56,
131
+ 73
132
+ ]
133
+ },
134
+ "game/reset": {
135
+ "type": "reset",
136
+ "phase": "play",
137
+ "icon": "undo",
138
+ "text": "Reset balls", "hint": "Clear every called number",
139
+ "values": {
140
+ "game/called": []
141
+ }
142
+ }
143
+ }
144
+ }