@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,382 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Broadcast Weather</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
+ .weather-card {
25
+ --accent: #53d8ff;
26
+ --panel: #102743;
27
+ position: absolute;
28
+ right: 4.5%;
29
+ bottom: 7%;
30
+ width: 61rem;
31
+ height: 18rem;
32
+ display: grid;
33
+ grid-template-columns: 21rem 1fr;
34
+ overflow: hidden;
35
+ border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
36
+ border-radius: 1.4rem;
37
+ background: linear-gradient(125deg,
38
+ color-mix(in srgb, var(--panel) 97%, #000),
39
+ color-mix(in srgb, var(--panel) 84%, #07101d));
40
+ box-shadow: 0 1.8rem 5rem #0009, inset 0 1px #fff1;
41
+ filter: drop-shadow(0 .3rem 1rem #0004)
42
+ }
43
+
44
+ .weather-card:before {
45
+ content: "";
46
+ position: absolute;
47
+ width: 25rem;
48
+ height: 25rem;
49
+ left: -8rem;
50
+ top: -14rem;
51
+ border-radius: 50%;
52
+ background: var(--accent);
53
+ opacity: .16;
54
+ filter: blur(1rem)
55
+ }
56
+
57
+ .current {
58
+ position: relative;
59
+ display: grid;
60
+ grid-template-columns: 8.2rem 1fr;
61
+ grid-template-rows: auto 1fr auto;
62
+ padding: 1.35rem 1.5rem;
63
+ border-right: 1px solid #fff1
64
+ }
65
+
66
+ .eyebrow {
67
+ grid-column: 1 / 3;
68
+ color: var(--accent);
69
+ font-size: .85rem;
70
+ font-weight: 950;
71
+ letter-spacing: .17em
72
+ }
73
+
74
+ .main-icon {
75
+ align-self: center;
76
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
77
+ font-size: 7rem;
78
+ line-height: 1;
79
+ filter: drop-shadow(0 .8rem 1rem #0005)
80
+ }
81
+
82
+ .temperature-wrap {
83
+ align-self: center;
84
+ min-width: 0
85
+ }
86
+
87
+ .temperature {
88
+ font-size: 5rem;
89
+ font-weight: 950;
90
+ line-height: .95;
91
+ letter-spacing: -.08em
92
+ }
93
+
94
+ .unit {
95
+ margin-left: .12em;
96
+ color: var(--accent);
97
+ font-size: 1.55rem;
98
+ letter-spacing: 0;
99
+ vertical-align: top
100
+ }
101
+
102
+ .condition {
103
+ margin-top: .55rem;
104
+ overflow: hidden;
105
+ color: #eaf8ff;
106
+ font-size: 1.15rem;
107
+ font-weight: 800;
108
+ text-overflow: ellipsis;
109
+ white-space: nowrap
110
+ }
111
+
112
+ .location {
113
+ grid-column: 1 / 3;
114
+ display: flex;
115
+ align-items: baseline;
116
+ justify-content: space-between;
117
+ gap: 1rem;
118
+ padding-top: .9rem;
119
+ border-top: 1px solid #fff1
120
+ }
121
+
122
+ .city {
123
+ overflow: hidden;
124
+ font-size: 1.65rem;
125
+ font-weight: 950;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap
128
+ }
129
+
130
+ .local-time {
131
+ color: #a8c7d7;
132
+ font-size: 1rem;
133
+ font-weight: 800;
134
+ white-space: nowrap
135
+ }
136
+
137
+ .detail-panel {
138
+ position: relative;
139
+ display: grid;
140
+ grid-template-rows: 1fr auto;
141
+ min-width: 0;
142
+ padding: 1.25rem 1.4rem 1.35rem
143
+ }
144
+
145
+ .forecast {
146
+ display: grid;
147
+ grid-template-columns: repeat(3, minmax(0, 1fr));
148
+ gap: .75rem
149
+ }
150
+
151
+ .forecast-day {
152
+ display: grid;
153
+ grid-template-columns: 1fr auto;
154
+ grid-template-rows: auto 1fr;
155
+ min-width: 0;
156
+ padding: 1rem;
157
+ border: 1px solid #fff1;
158
+ border-radius: .9rem;
159
+ background: linear-gradient(145deg, #ffffff12, #ffffff05)
160
+ }
161
+
162
+ .day {
163
+ color: color-mix(in srgb, var(--accent) 84%, #fff);
164
+ font-size: .85rem;
165
+ font-weight: 950;
166
+ letter-spacing: .1em
167
+ }
168
+
169
+ .forecast-icon {
170
+ grid-row: 1 / 3;
171
+ grid-column: 2;
172
+ align-self: center;
173
+ padding-left: .25rem;
174
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
175
+ font-size: 3.25rem;
176
+ line-height: 1
177
+ }
178
+
179
+ .temperatures {
180
+ align-self: end;
181
+ margin-top: .5rem;
182
+ font-size: 1.65rem;
183
+ font-weight: 950;
184
+ white-space: nowrap
185
+ }
186
+
187
+ .low {
188
+ margin-left: .35rem;
189
+ color: #829eae;
190
+ font-size: 1.1rem
191
+ }
192
+
193
+ .readings {
194
+ display: grid;
195
+ grid-template-columns: repeat(3, 1fr);
196
+ margin-top: 1rem;
197
+ border-top: 1px solid #fff1
198
+ }
199
+
200
+ .reading {
201
+ padding: .85rem .8rem 0
202
+ }
203
+
204
+ .reading + .reading {
205
+ border-left: 1px solid #fff1
206
+ }
207
+
208
+ .reading span {
209
+ display: block;
210
+ color: #7899aa;
211
+ font-size: .7rem;
212
+ font-weight: 900;
213
+ letter-spacing: .1em
214
+ }
215
+
216
+ .reading strong {
217
+ display: block;
218
+ margin-top: .25rem;
219
+ overflow: hidden;
220
+ color: #f2fbff;
221
+ font-size: 1.15rem;
222
+ font-weight: 900;
223
+ text-overflow: ellipsis;
224
+ white-space: nowrap
225
+ }
226
+
227
+ .weather-card.refresh .main-icon {
228
+ animation: weather-pop .75s cubic-bezier(.2, .9, .25, 1)
229
+ }
230
+
231
+ .weather-card.refresh .temperature {
232
+ animation: temperature-pop .65s cubic-bezier(.2, .9, .25, 1)
233
+ }
234
+
235
+ @keyframes weather-pop {
236
+ 0% { transform: translateY(.7rem) scale(.82); opacity: .5 }
237
+ 55% { transform: translateY(-.35rem) scale(1.08); opacity: 1 }
238
+ 100% { transform: translateY(0) scale(1); opacity: 1 }
239
+ }
240
+
241
+ @keyframes temperature-pop {
242
+ 0% { transform: scale(.88); filter: brightness(1.8) }
243
+ 60% { transform: scale(1.06); filter: brightness(1.25) }
244
+ 100% { transform: scale(1); filter: brightness(1) }
245
+ }
246
+
247
+ @media (prefers-reduced-motion: reduce) {
248
+ .weather-card.refresh .main-icon,
249
+ .weather-card.refresh .temperature {
250
+ animation: none
251
+ }
252
+ }
253
+ </style>
254
+ </head>
255
+ <body>
256
+ <section class="weather-card" id="card">
257
+ <div class="current">
258
+ <div class="eyebrow" id="eyebrow">LIVE WEATHER</div>
259
+ <div class="main-icon" id="mainIcon">🌤️</div>
260
+ <div class="temperature-wrap">
261
+ <div class="temperature"><span id="temperature">23</span><span class="unit" id="unit">°C</span></div>
262
+ <div class="condition" id="condition">Bright intervals</div>
263
+ </div>
264
+ <div class="location">
265
+ <strong class="city" id="city">Barcelona</strong>
266
+ <span class="local-time" id="localTime">08:45</span>
267
+ </div>
268
+ </div>
269
+ <div class="detail-panel">
270
+ <div class="forecast" id="forecast"></div>
271
+ <div class="readings">
272
+ <div class="reading"><span>FEELS LIKE</span><strong id="feelsLike">24°C</strong></div>
273
+ <div class="reading"><span>WIND</span><strong id="wind">SE 12 km/h</strong></div>
274
+ <div class="reading"><span>HUMIDITY</span><strong id="humidity">68%</strong></div>
275
+ </div>
276
+ </div>
277
+ </section>
278
+ <script>
279
+ const dom = {
280
+ card: document.getElementById("card"),
281
+ eyebrow: document.getElementById("eyebrow"),
282
+ mainIcon: document.getElementById("mainIcon"),
283
+ temperature: document.getElementById("temperature"),
284
+ unit: document.getElementById("unit"),
285
+ condition: document.getElementById("condition"),
286
+ city: document.getElementById("city"),
287
+ localTime: document.getElementById("localTime"),
288
+ forecast: document.getElementById("forecast"),
289
+ feelsLike: document.getElementById("feelsLike"),
290
+ wind: document.getElementById("wind"),
291
+ humidity: document.getElementById("humidity")
292
+ };
293
+
294
+ let unit = "°C";
295
+ let feelsLike = 24;
296
+ let refreshTimer;
297
+ let forecast = [
298
+ {day: "TODAY", icon: "🌤️", high: 25, low: 18},
299
+ {day: "TUE", icon: "🌧️", high: 21, low: 16},
300
+ {day: "WED", icon: "☀️", high: 27, low: 19}
301
+ ];
302
+
303
+ function temperatureText(value) {
304
+ return `${value ?? "—"}°`;
305
+ }
306
+
307
+ function renderForecast() {
308
+ dom.forecast.replaceChildren(...forecast.slice(0, 3).map(item => {
309
+ const tile = document.createElement("div");
310
+ tile.className = "forecast-day";
311
+
312
+ const day = document.createElement("div");
313
+ day.className = "day";
314
+ day.textContent = item.day ?? "—";
315
+
316
+ const icon = document.createElement("div");
317
+ icon.className = "forecast-icon";
318
+ icon.textContent = item.icon ?? "☁️";
319
+
320
+ const temperatures = document.createElement("div");
321
+ temperatures.className = "temperatures";
322
+ temperatures.textContent = temperatureText(item.high);
323
+
324
+ const low = document.createElement("span");
325
+ low.className = "low";
326
+ low.textContent = temperatureText(item.low);
327
+ temperatures.append(low);
328
+ tile.append(day, icon, temperatures);
329
+ return tile
330
+ }))
331
+ }
332
+
333
+ function refresh() {
334
+ clearTimeout(refreshTimer);
335
+ dom.card.classList.remove("refresh");
336
+ void dom.card.offsetWidth;
337
+ dom.card.classList.add("refresh");
338
+ refreshTimer = setTimeout(() => dom.card.classList.remove("refresh"), 800)
339
+ }
340
+
341
+ function renderUnitValues() {
342
+ dom.unit.textContent = unit;
343
+ dom.feelsLike.textContent = `${feelsLike}${unit}`
344
+ }
345
+
346
+ window.addEventListener("nebular-overlay:update", ({detail = {}}) => {
347
+ let currentChanged = false;
348
+
349
+ if (detail.content?.eyebrow != null) dom.eyebrow.textContent = detail.content.eyebrow;
350
+ if (detail.location?.city != null) dom.city.textContent = detail.location.city;
351
+ if (detail.location?.time != null) dom.localTime.textContent = detail.location.time;
352
+ if (detail.weather?.icon != null) {
353
+ dom.mainIcon.textContent = detail.weather.icon;
354
+ currentChanged = true
355
+ }
356
+ if (detail.weather?.condition != null) {
357
+ dom.condition.textContent = detail.weather.condition;
358
+ currentChanged = true
359
+ }
360
+ if (detail.weather?.temperature != null) {
361
+ dom.temperature.textContent = detail.weather.temperature;
362
+ currentChanged = true
363
+ }
364
+ if (detail.weather?.unit != null) unit = detail.weather.unit;
365
+ if (detail.weather?.feelsLike != null) feelsLike = detail.weather.feelsLike;
366
+ if (detail.weather?.wind != null) dom.wind.textContent = detail.weather.wind;
367
+ if (detail.weather?.humidity != null) dom.humidity.textContent = `${detail.weather.humidity}%`;
368
+ if (detail.weather?.unit != null || detail.weather?.feelsLike != null) renderUnitValues();
369
+ if (Array.isArray(detail.forecast)) {
370
+ forecast = detail.forecast;
371
+ renderForecast()
372
+ }
373
+ if (detail.style?.accent != null) dom.card.style.setProperty("--accent", detail.style.accent);
374
+ if (detail.style?.panel != null) dom.card.style.setProperty("--panel", detail.style.panel);
375
+ if (currentChanged) refresh()
376
+ });
377
+
378
+ renderForecast();
379
+ renderUnitValues();
380
+ </script>
381
+ </body>
382
+ </html>
@@ -0,0 +1,104 @@
1
+ {
2
+ "instructions": "Set the location, units, colours and forecast before going live. During the broadcast, update the current conditions and forecast values, then press Update. Use short condition and wind descriptions so the card remains easy to read on television.",
3
+ "meta": {
4
+ "content/eyebrow": {
5
+ "type": "string",
6
+ "phase": "edit",
7
+ "label": "Label", "hint": "Enter short label",
8
+ "default": "LIVE WEATHER"
9
+ },
10
+ "location/city": {
11
+ "type": "string",
12
+ "phase": "play",
13
+ "label": "Location", "hint": "Enter location",
14
+ "default": "Barcelona"
15
+ },
16
+ "location/time": {
17
+ "type": "string",
18
+ "phase": "play",
19
+ "label": "Local time", "hint": "e.g. 08:45",
20
+ "default": "08:45"
21
+ },
22
+ "weather/icon": {
23
+ "type": "choose",
24
+ "phase": "play",
25
+ "label": "Conditions icon", "hint": "Icon representing the current weather conditions",
26
+ "choose": ["☀️", "🌤️", "⛅", "☁️", "🌧️", "⛈️", "🌨️", "🌫️", "🌬️"],
27
+ "default": "🌤️"
28
+ },
29
+ "weather/condition": {
30
+ "type": "string",
31
+ "phase": "play",
32
+ "label": "Conditions", "hint": "e.g. Bright intervals",
33
+ "default": "Bright intervals"
34
+ },
35
+ "weather/temperature": {
36
+ "type": "number",
37
+ "phase": "play",
38
+ "label": "Temperature", "hint": "e.g. 23",
39
+ "default": 23
40
+ },
41
+ "weather/unit": {
42
+ "type": "choose",
43
+ "phase": "edit",
44
+ "label": "Temperature unit", "hint": "Unit used for every temperature in the graphic",
45
+ "choose": ["°C", "°F"],
46
+ "default": "°C"
47
+ },
48
+ "weather/feelsLike": {
49
+ "type": "number",
50
+ "phase": "play",
51
+ "label": "Feels like", "hint": "e.g. 24",
52
+ "default": 24
53
+ },
54
+ "weather/wind": {
55
+ "type": "string",
56
+ "phase": "play",
57
+ "label": "Wind", "hint": "e.g. SE 12 km/h",
58
+ "default": "SE 12 km/h"
59
+ },
60
+ "weather/humidity": {
61
+ "type": "number",
62
+ "phase": "play",
63
+ "label": "Humidity", "hint": "0–100",
64
+ "min": 0,
65
+ "max": 100,
66
+ "default": 68
67
+ },
68
+ "forecast": {
69
+ "type": "set",
70
+ "phase": "play",
71
+ "container": "big",
72
+ "label": "Forecast", "hint": "Forecast periods displayed alongside current conditions",
73
+ "max": 3,
74
+ "default": [
75
+ {"day": "TODAY", "icon": "🌤️", "high": 25, "low": 18},
76
+ {"day": "TUE", "icon": "🌧️", "high": 21, "low": 16},
77
+ {"day": "WED", "icon": "☀️", "high": 27, "low": 19}
78
+ ],
79
+ "meta": {
80
+ "day": {"type": "string", "label": "Day", "hint": "e.g. TODAY"},
81
+ "icon": {
82
+ "type": "choose",
83
+ "label": "Icon",
84
+ "hint": "Icon representing the forecast conditions",
85
+ "choose": ["☀️", "🌤️", "⛅", "☁️", "🌧️", "⛈️", "🌨️", "🌫️", "🌬️"]
86
+ },
87
+ "high": {"type": "number", "label": "High", "hint": "Enter temperature"},
88
+ "low": {"type": "number", "label": "Low", "hint": "Enter temperature"}
89
+ }
90
+ },
91
+ "style/accent": {
92
+ "type": "color",
93
+ "phase": "edit",
94
+ "label": "Accent", "hint": "Accent colour used for weather highlights",
95
+ "default": "#53d8ff"
96
+ },
97
+ "style/panel": {
98
+ "type": "color",
99
+ "phase": "edit",
100
+ "label": "Panel colour", "hint": "Background colour of the weather panel",
101
+ "default": "#102743"
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,116 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Viewer Call to Action</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
+ .card {
25
+ position: absolute;
26
+ right: 5%;
27
+ bottom: 8%;
28
+ width: 48rem;
29
+ padding: 2rem 2.3rem;
30
+ background: linear-gradient(135deg, #111826f5, #070b13f5);
31
+ border: 1px solid #ffffff29;
32
+ border-radius: 1rem;
33
+ box-shadow: 0 1.2rem 3rem #0006
34
+ }
35
+
36
+ .eyebrow {
37
+ color: #ffad3d;
38
+ font-size: 1.5rem;
39
+ font-weight: 850;
40
+ letter-spacing: .16em;
41
+ text-transform: uppercase
42
+ }
43
+
44
+ .title {
45
+ margin-top: .6rem;
46
+ font-size: 3rem;
47
+ font-weight: 900
48
+ }
49
+
50
+ .subtitle {
51
+ margin-top: .55rem;
52
+ color: #c8d0dc;
53
+ font-size: 1.8rem;
54
+ line-height: 1.35
55
+ }
56
+
57
+ .progress {
58
+ margin-top: 1rem;
59
+ height: 1rem;
60
+ overflow: hidden;
61
+ background: #ffffff1f;
62
+ border-radius: 1rem
63
+ }
64
+
65
+ .progress span {
66
+ display: block;
67
+ width: 72%;
68
+ height: 100%;
69
+ background: linear-gradient(90deg, #ff9818, #ffd052);
70
+ transition: width .4s ease
71
+ }
72
+
73
+ .footer {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ margin-top: .9rem;
77
+ font-size: 1.6rem;
78
+ font-weight: 750
79
+ }
80
+
81
+ .count {
82
+ color: #ffc45f
83
+ }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <section class="card">
88
+ <div class="eyebrow" id="eyebrow">Viewer goal</div>
89
+ <div class="title" id="title">Help us reach 10,000 followers</div>
90
+ <div class="subtitle" id="subtitle">Follow the channel and join the Nebular community.</div>
91
+ <div class="progress"><span id="progress"></span></div>
92
+ <div class="footer"><span id="action">FOLLOW NOW</span><span class="count" id="count">7,240 / 10,000</span></div>
93
+ </section>
94
+ <script>
95
+ const $ = id => document.getElementById(id);
96
+ const progressState = {current: 7240, target: 10000};
97
+
98
+ function renderProgress() {
99
+ const current = Math.max(0, Number(progressState.current) || 0),
100
+ target = Math.max(1, Number(progressState.target) || 1);
101
+ $("progress").style.width = `${Math.min(100, current / target * 100)}%`;
102
+ $("count").textContent = `${current.toLocaleString()} / ${target.toLocaleString()}`
103
+ }
104
+
105
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
106
+ if (detail.content?.eyebrow != null) $("eyebrow").textContent = detail.content.eyebrow;
107
+ if (detail.content?.title != null) $("title").textContent = detail.content.title;
108
+ if (detail.content?.subtitle != null) $("subtitle").textContent = detail.content.subtitle;
109
+ if (detail.content?.action != null) $("action").textContent = detail.content.action;
110
+ if (detail.goal?.current != null) progressState.current = detail.goal.current;
111
+ if (detail.goal?.target != null) progressState.target = detail.goal.target;
112
+ if (detail.goal?.current != null || detail.goal?.target != null) renderProgress();
113
+ });
114
+ </script>
115
+ </body>
116
+ </html>
@@ -0,0 +1,12 @@
1
+ {
2
+ "instructions": "Configure the campaign message and call to action, then set the current and target values to update the progress indicator. Press Update whenever the campaign copy or progress changes.",
3
+ "meta": {
4
+ "content/eyebrow": {"type": "string", "phase": "edit", "label": "Eyebrow", "hint": "Enter short label", "default": "VIEWER GOAL"},
5
+ "content/title": {"type": "string", "phase": "edit", "label": "Title", "hint": "Enter title", "default": "Help us reach 10,000 followers"},
6
+ "content/subtitle": {"type": "string", "phase": "edit", "label": "Description", "hint": "Enter description", "default": "Follow the channel and join the Nebular community."},
7
+ "content/action": {"type": "string", "phase": "edit", "label": "Action", "hint": "e.g. FOLLOW NOW", "default": "FOLLOW NOW"},
8
+ "goal/playTarget": {"type": "number", "phase": "play", "label": "Target", "hint": "Goal currently used by the live progress display", "source": "goal/target", "readonly": true},
9
+ "goal/current": {"type": "number", "phase": "play", "label": "Current", "hint": "Enter current value", "min": 0, "default": 7240},
10
+ "goal/target": {"type": "number", "phase": "edit", "label": "Target", "hint": "Enter target", "min": 1, "default": 10000}
11
+ }
12
+ }