@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,115 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Football Scoreboard</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
+ .scoreboard {
25
+ position: absolute;
26
+ top: 5%;
27
+ left: 50%;
28
+ transform: translateX(-50%);
29
+ display: grid;
30
+ grid-template-columns: 16rem 5rem 5rem 16rem;
31
+ align-items: stretch;
32
+ background: rgba(8, 17, 27, .94);
33
+ border-radius: .8rem;
34
+ overflow: hidden;
35
+ box-shadow: 0 1rem 3rem rgba(0, 0, 0, .35);
36
+ border: 1px solid rgba(255, 255, 255, .18);
37
+ }
38
+
39
+ .team {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: .8rem;
43
+ padding: 1rem 1.2rem;
44
+ font-weight: 800;
45
+ letter-spacing: .04em;
46
+ }
47
+
48
+ .team.away {
49
+ justify-content: flex-end;
50
+ text-align: right;
51
+ }
52
+
53
+ .badge {
54
+ width: 2rem;
55
+ height: 2rem;
56
+ display: grid;
57
+ place-items: center;
58
+ border-radius: 50%;
59
+ background: #19b76b;
60
+ font-size: .72rem;
61
+ }
62
+
63
+ .away .badge {
64
+ background: #ea4b35;
65
+ order: 2;
66
+ }
67
+
68
+ .score {
69
+ display: grid;
70
+ place-items: center;
71
+ background: #fff;
72
+ color: #0b1722;
73
+ font-size: 2rem;
74
+ font-weight: 900;
75
+ }
76
+
77
+ .clock {
78
+ grid-column: 1 / -1;
79
+ background: #19b76b;
80
+ padding: .3rem;
81
+ text-align: center;
82
+ font-size: .85rem;
83
+ font-weight: 800;
84
+ letter-spacing: .14em;
85
+ }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <section class="scoreboard">
90
+ <div class="team"><span class="badge">H</span><span id="home">HOME</span></div>
91
+ <div class="score" id="homeScore">0</div>
92
+ <div class="score" id="awayScore">0</div>
93
+ <div class="team away"><span class="badge">A</span><span id="away">AWAY</span></div>
94
+ <div class="clock" id="clock">00:00</div>
95
+ </section>
96
+ <script>
97
+
98
+ const $ = (name, value) => {
99
+ const element = document.getElementById(name);
100
+ if (element) {
101
+ if (value !== undefined) element.textContent = String(value);
102
+ else return element;
103
+ }
104
+ }
105
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
106
+ if (detail.home?.name != null) $("home", detail.home.name);
107
+ if (detail.home?.score != null) $("homeScore", detail.home.score);
108
+ if (detail.away?.name != null) $("away", detail.away.name);
109
+ if (detail.away?.score != null) $("awayScore", detail.away.score);
110
+ if (detail.clock != null) $("clock", detail.clock);
111
+ window.invalidate?.()
112
+ });
113
+ </script>
114
+ </body>
115
+ </html>
@@ -0,0 +1,54 @@
1
+ {
2
+ "instructions": "Enter both team names, update their scores and set the match clock. Press Update whenever the score or clock changes; omitted fields retain their current values.",
3
+ "meta": {
4
+ "home/name": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Home team", "hint": "Enter team name",
8
+ "default": "HOME"
9
+ },
10
+ "home/playName": {
11
+ "type": "string",
12
+ "phase": "play",
13
+ "label": "Home team", "hint": "Home-team name displayed during play",
14
+ "source": "home/name",
15
+ "readonly": true
16
+ },
17
+ "home/score": {
18
+ "type": "number",
19
+ "phase": "play",
20
+ "label": "Home score", "hint": "Enter score",
21
+ "min": 0,
22
+ "default": 0,
23
+ "break": true
24
+ },
25
+ "away/name": {
26
+ "type": "string",
27
+ "phase": "edit",
28
+ "label": "Away team", "hint": "Enter team name",
29
+ "default": "AWAY"
30
+ },
31
+ "away/playName": {
32
+ "type": "string",
33
+ "phase": "play",
34
+ "label": "Away team", "hint": "Away-team name displayed during play",
35
+ "source": "away/name",
36
+ "readonly": true
37
+ },
38
+ "away/score": {
39
+ "type": "number",
40
+ "phase": "play",
41
+ "label": "Away score", "hint": "Enter score",
42
+ "min": 0,
43
+ "default": 0,
44
+ "break": true
45
+ },
46
+ "clock": {
47
+ "type": "string",
48
+ "phase": "play",
49
+ "label": "Match clock", "hint": "e.g. 45:00",
50
+ "pattern": "^[0-9]{1,3}:[0-5][0-9]$",
51
+ "default": "00:00"
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,120 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Tennis Scoreboard</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: white;
21
+ font-family: Inter, system-ui, sans-serif;
22
+ }
23
+
24
+ .board {
25
+ position: absolute;
26
+ left: 4%;
27
+ bottom: 7%;
28
+ width: 34rem;
29
+ border-radius: .7rem;
30
+ overflow: hidden;
31
+ box-shadow: 0 1rem 3rem #0007;
32
+ }
33
+
34
+ .heading {
35
+ display: flex;
36
+ justify-content: space-between;
37
+ padding: .55rem 1rem;
38
+ background: #b7d62b;
39
+ color: #17200c;
40
+ font-size: .75rem;
41
+ font-weight: 900;
42
+ letter-spacing: .12em;
43
+ }
44
+
45
+ .player {
46
+ display: grid;
47
+ grid-template-columns: 1fr repeat(3, 3.6rem);
48
+ align-items: center;
49
+ min-height: 3.6rem;
50
+ background: rgba(15, 23, 31, .95);
51
+ border-top: 1px solid rgba(255, 255, 255, .13);
52
+ }
53
+
54
+ .name {
55
+ padding: 0 1rem;
56
+ font-weight: 800;
57
+ }
58
+
59
+ .cell {
60
+ height: 100%;
61
+ display: grid;
62
+ place-items: center;
63
+ border-left: 1px solid rgba(255, 255, 255, .13);
64
+ font-size: 1.25rem;
65
+ font-weight: 900;
66
+ }
67
+
68
+ .points {
69
+ background: #fff;
70
+ color: #14200d;
71
+ font-size: 1.55rem;
72
+ }
73
+
74
+ .serving::before {
75
+ content: "●";
76
+ color: #b7d62b;
77
+ margin-right: .55rem;
78
+ }
79
+ </style>
80
+ </head>
81
+ <body>
82
+
83
+ <section class="board">
84
+ <header class="heading"><span id="event">CENTRE COURT</span><span>SETS · GAMES · POINTS</span></header>
85
+ <div class="player">
86
+ <div class="name serving" id="playerOne">PLAYER ONE</div>
87
+ <div class="cell" id="playerOneSets">0</div>
88
+ <div class="cell" id="playerOneGames">0</div>
89
+ <div class="cell points" id="playerOnePoints">0</div>
90
+ </div>
91
+ <div class="player">
92
+ <div class="name" id="playerTwo">PLAYER TWO</div>
93
+ <div class="cell" id="playerTwoSets">0</div>
94
+ <div class="cell" id="playerTwoGames">0</div>
95
+ <div class="cell points" id="playerTwoPoints">0</div>
96
+ </div>
97
+ </section>
98
+ <script>
99
+
100
+ const $ = (name, value) => {
101
+ const element = document.getElementById(name);
102
+ if (element) element.textContent = value;
103
+ }
104
+
105
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
106
+ if (detail.event != null) $("event", detail.event);
107
+ if (detail.playerOne?.name != null) $("playerOne", detail.playerOne.name);
108
+ if (detail.playerOne?.sets != null) $("playerOneSets", detail.playerOne.sets);
109
+ if (detail.playerOne?.games != null) $("playerOneGames", detail.playerOne.games);
110
+ if (detail.playerOne?.points != null) $("playerOnePoints", detail.playerOne.points);
111
+ if (detail.playerTwo?.name != null) $("playerTwo", detail.playerTwo.name);
112
+ if (detail.playerTwo?.sets != null) $("playerTwoSets", detail.playerTwo.sets);
113
+ if (detail.playerTwo?.games != null) $("playerTwoGames", detail.playerTwo.games);
114
+ if (detail.playerTwo?.points != null) $("playerTwoPoints", detail.playerTwo.points);
115
+ window.invalidate?.()
116
+ });
117
+
118
+ </script>
119
+ </body>
120
+ </html>
@@ -0,0 +1,92 @@
1
+ {
2
+ "instructions": "Enter the event and player names, then update sets, games and points as play progresses. Use the point selectors for 0, 15, 30, 40 and advantage, and press Update after each scoring change.",
3
+ "meta": {
4
+ "event": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Court or event", "hint": "Enter court or event",
8
+ "default": "CENTRE COURT"
9
+ },
10
+ "playerOne/name": {
11
+ "type": "string",
12
+ "phase": "edit",
13
+ "label": "Player one", "hint": "Enter player name",
14
+ "default": "PLAYER ONE"
15
+ },
16
+ "playerTwo/name": {
17
+ "type": "string",
18
+ "phase": "edit",
19
+ "label": "Player two", "hint": "Enter player name",
20
+ "default": "PLAYER TWO"
21
+ },
22
+ "playerOne/playName": {
23
+ "type": "string",
24
+ "phase": "play",
25
+ "label": "Player one", "hint": "First player's name displayed during play",
26
+ "source": "playerOne/name",
27
+ "readonly": true
28
+ },
29
+ "playerOne/sets": {
30
+ "type": "number",
31
+ "phase": "play",
32
+ "label": "Player one sets", "hint": "Enter sets",
33
+ "min": 0,
34
+ "default": 0
35
+ },
36
+ "playerOne/games": {
37
+ "type": "number",
38
+ "phase": "play",
39
+ "label": "Player one games", "hint": "Enter games",
40
+ "min": 0,
41
+ "default": 0
42
+ },
43
+ "playerOne/points": {
44
+ "type": "choose",
45
+ "phase": "play",
46
+ "label": "Player one points", "hint": "Current-game points for the first player",
47
+ "break": true,
48
+ "default": "0",
49
+ "choose": [
50
+ "0",
51
+ "15",
52
+ "30",
53
+ "40",
54
+ "A"
55
+ ]
56
+ },
57
+ "playerTwo/playName": {
58
+ "type": "string",
59
+ "phase": "play",
60
+ "label": "Player two", "hint": "Second player's name displayed during play",
61
+ "source": "playerTwo/name",
62
+ "readonly": true
63
+ },
64
+ "playerTwo/sets": {
65
+ "type": "number",
66
+ "phase": "play",
67
+ "label": "Player two sets", "hint": "Enter sets",
68
+ "min": 0,
69
+ "default": 0
70
+ },
71
+ "playerTwo/games": {
72
+ "type": "number",
73
+ "phase": "play",
74
+ "label": "Player two games", "hint": "Enter games",
75
+ "min": 0,
76
+ "default": 0
77
+ },
78
+ "playerTwo/points": {
79
+ "type": "choose",
80
+ "phase": "play",
81
+ "label": "Player two points", "hint": "Current-game points for the second player",
82
+ "default": "0",
83
+ "choose": [
84
+ "0",
85
+ "15",
86
+ "30",
87
+ "40",
88
+ "A"
89
+ ]
90
+ }
91
+ }
92
+ }