@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.
- package/package.json +44 -0
- package/preview/assets/images/favicon.ico +0 -0
- package/preview/assets/images/favicon.png +0 -0
- package/preview/assets/images/login-right.webp +0 -0
- package/preview/assets/images/nebular.color.100h.png +0 -0
- package/preview/assets/images/nebular.color.white.100h.png +0 -0
- package/preview/assets/images/nebular.medallion.transparent..png +0 -0
- package/preview/assets/images/testcard.png +0 -0
- package/preview/assets/webfonts/fa-brands-400.woff2 +0 -0
- package/preview/assets/webfonts/fa-solid-900.woff2 +0 -0
- package/preview/bundle.min.js +6 -0
- package/preview/index.css +28 -0
- package/preview/index.html +23 -0
- package/readme.md +132 -0
- package/skill/nebular-overlay/SKILL.md +56 -0
- package/skill/nebular-overlay/agents/openai.yaml +4 -0
- package/skill/nebular-overlay/references/examples.md +327 -0
- package/skill/nebular-overlay/references/manifest.md +153 -0
- package/skill/nebular-overlay/references/runtime-and-design.md +88 -0
- package/skill/nebular-overlay/scripts/validate-templates.mjs +244 -0
- package/templates/all/broadcast/breaking-news/index.html +100 -0
- package/templates/all/broadcast/breaking-news/manifest.json +9 -0
- package/templates/all/broadcast/empty/index.html +46 -0
- package/templates/all/broadcast/empty/manifest.json +7 -0
- package/templates/all/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/all/broadcast/event-countdown/index.html +160 -0
- package/templates/all/broadcast/event-countdown/logo.png +0 -0
- package/templates/all/broadcast/event-countdown/manifest.json +33 -0
- package/templates/all/broadcast/lower-third/index.html +77 -0
- package/templates/all/broadcast/lower-third/manifest.json +8 -0
- package/templates/all/broadcast/newsticker/index.html +153 -0
- package/templates/all/broadcast/newsticker/manifest.json +21 -0
- package/templates/all/broadcast/newsticker-frames/background-label-green.png +0 -0
- package/templates/all/broadcast/newsticker-frames/background-label-orange.png +0 -0
- package/templates/all/broadcast/newsticker-frames/banner.png +0 -0
- package/templates/all/broadcast/newsticker-frames/index.html +229 -0
- package/templates/all/broadcast/newsticker-frames/manifest.json +20 -0
- package/templates/all/broadcast/weather/index.html +382 -0
- package/templates/all/broadcast/weather/manifest.json +104 -0
- package/templates/all/engagement/call-to-action/index.html +116 -0
- package/templates/all/engagement/call-to-action/manifest.json +12 -0
- package/templates/all/engagement/chat/index.html +121 -0
- package/templates/all/engagement/chat/manifest.json +32 -0
- package/templates/all/engagement/post/index.html +132 -0
- package/templates/all/engagement/post/manifest.json +31 -0
- package/templates/all/gambling/bingo/index.html +227 -0
- package/templates/all/gambling/bingo/manifest.json +144 -0
- package/templates/all/gambling/keno/index.html +202 -0
- package/templates/all/gambling/keno/manifest.json +129 -0
- package/templates/all/gambling/lottery/index.html +159 -0
- package/templates/all/gambling/lottery/manifest.json +95 -0
- package/templates/all/gambling/prize-wall/index.html +256 -0
- package/templates/all/gambling/prize-wall/manifest.json +108 -0
- package/templates/all/gambling/roulette/index.html +200 -0
- package/templates/all/gambling/roulette/manifest.json +78 -0
- package/templates/all/gaming/battleship/index.html +578 -0
- package/templates/all/gaming/battleship/manifest.json +279 -0
- package/templates/all/gaming/gta/mission/index.html +121 -0
- package/templates/all/gaming/gta/mission/manifest.json +38 -0
- package/templates/all/sports/football/scoreboard/index.html +115 -0
- package/templates/all/sports/football/scoreboard/manifest.json +54 -0
- package/templates/all/sports/tennis/scoreboard/index.html +120 -0
- package/templates/all/sports/tennis/scoreboard/manifest.json +92 -0
- package/templates/all/trivia/audience-poll/index.html +60 -0
- package/templates/all/trivia/audience-poll/manifest.json +26 -0
- package/templates/all/trivia/classic-quiz/index.html +49 -0
- package/templates/all/trivia/classic-quiz/manifest.json +15 -0
- package/templates/all/trivia/lightning-round/index.html +126 -0
- package/templates/all/trivia/lightning-round/manifest.json +27 -0
- package/templates/all/trivia/pasapalabra/index.html +249 -0
- package/templates/all/trivia/pasapalabra/manifest.json +68 -0
- package/templates/all/trivia/quiz-deck/index.html +58 -0
- package/templates/all/trivia/quiz-deck/manifest.json +25 -0
- package/templates/all/trivia/true-or-false/index.html +81 -0
- package/templates/all/trivia/true-or-false/manifest.json +27 -0
- package/templates/basic/broadcast/empty/index.html +46 -0
- package/templates/basic/broadcast/empty/manifest.json +7 -0
- package/templates/basic/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/basic/broadcast/event-countdown/index.html +160 -0
- package/templates/basic/broadcast/event-countdown/logo.png +0 -0
- package/templates/basic/broadcast/event-countdown/manifest.json +33 -0
- package/templates/basic/broadcast/lower-third/index.html +77 -0
- package/templates/basic/broadcast/lower-third/manifest.json +8 -0
- package/templates/basic/broadcast/newsticker/index.html +153 -0
- package/templates/basic/broadcast/newsticker/manifest.json +21 -0
- package/templates/basic/broadcast/weather/index.html +382 -0
- package/templates/basic/broadcast/weather/manifest.json +104 -0
- package/templates/basic/engagement/call-to-action/index.html +116 -0
- package/templates/basic/engagement/call-to-action/manifest.json +12 -0
- package/templates/basic/engagement/chat/index.html +121 -0
- package/templates/basic/engagement/chat/manifest.json +32 -0
- package/templates/basic/engagement/post/index.html +132 -0
- package/templates/basic/engagement/post/manifest.json +31 -0
- package/templates/basic/sports/football/scoreboard/index.html +115 -0
- package/templates/basic/sports/football/scoreboard/manifest.json +54 -0
- package/templates/basic/sports/tennis/scoreboard/index.html +120 -0
- package/templates/basic/sports/tennis/scoreboard/manifest.json +92 -0
- package/templates/debug/testing/security-probe/forbidden.js +1 -0
- package/templates/debug/testing/security-probe/forbidden.json +1 -0
- package/templates/debug/testing/security-probe/forbidden.mp4 +1 -0
- package/templates/debug/testing/security-probe/forbidden.svg +1 -0
- package/templates/debug/testing/security-probe/index.html +367 -0
- package/templates/debug/testing/security-probe/manifest.json +13 -0
- package/templates/empty/broadcast/empty/index.html +46 -0
- package/templates/empty/broadcast/empty/manifest.json +7 -0
- package/templates/empty/broadcast/empty/nebular.medallion.transparent.png +0 -0
- package/templates/games/gambling/bingo/index.html +227 -0
- package/templates/games/gambling/bingo/manifest.json +56 -0
- package/templates/games/gambling/keno/index.html +202 -0
- package/templates/games/gambling/keno/manifest.json +129 -0
- package/templates/games/gambling/lottery/index.html +159 -0
- package/templates/games/gambling/lottery/manifest.json +95 -0
- package/templates/games/gambling/prize-wall/index.html +256 -0
- package/templates/games/gambling/prize-wall/manifest.json +108 -0
- package/templates/games/gambling/roulette/index.html +200 -0
- package/templates/games/gambling/roulette/manifest.json +78 -0
- package/templates/games/gaming/battleship/index.html +578 -0
- package/templates/games/gaming/battleship/manifest.json +279 -0
- package/templates/games/gaming/gta/mission/index.html +121 -0
- package/templates/games/gaming/gta/mission/manifest.json +38 -0
- package/templates/games/trivia/audience-poll/index.html +60 -0
- package/templates/games/trivia/audience-poll/manifest.json +26 -0
- package/templates/games/trivia/classic-quiz/index.html +49 -0
- package/templates/games/trivia/classic-quiz/manifest.json +15 -0
- package/templates/games/trivia/lightning-round/index.html +126 -0
- package/templates/games/trivia/lightning-round/manifest.json +27 -0
- package/templates/games/trivia/pasapalabra/index.html +249 -0
- package/templates/games/trivia/pasapalabra/manifest.json +68 -0
- package/templates/games/trivia/quiz-deck/index.html +58 -0
- package/templates/games/trivia/quiz-deck/manifest.json +25 -0
- package/templates/games/trivia/true-or-false/index.html +81 -0
- package/templates/games/trivia/true-or-false/manifest.json +27 -0
- package/tool/cli.cjs +7 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
## Typical Examples
|
|
2
|
+
|
|
3
|
+
### A Bingo
|
|
4
|
+
|
|
5
|
+
Note how Phase is used so the operator toggles between play mode and edit mode and the auto-generated UI editor is not clogged
|
|
6
|
+
|
|
7
|
+
- In Edit Mode it allows to configure the branding-related fields
|
|
8
|
+
- In Play Mode it just displays the card to click on called numbers
|
|
9
|
+
|
|
10
|
+
**manifest.json**
|
|
11
|
+
```
|
|
12
|
+
{
|
|
13
|
+
"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.",
|
|
14
|
+
"meta": {
|
|
15
|
+
"content/eyebrow": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"phase": "edit",
|
|
18
|
+
"label": "Current number label", "hint": "Enter short label",
|
|
19
|
+
"default": "JUST CALLED"
|
|
20
|
+
},
|
|
21
|
+
"content/title": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"phase": "edit",
|
|
24
|
+
"label": "Event title", "hint": "Enter event title",
|
|
25
|
+
"default": "Friday Night Bingo"
|
|
26
|
+
},
|
|
27
|
+
"content/subtitle": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"phase": "edit",
|
|
30
|
+
"label": "Subtitle", "hint": "Enter subtitle",
|
|
31
|
+
"default": "Eyes down — good luck!"
|
|
32
|
+
},
|
|
33
|
+
"game/called": {
|
|
34
|
+
"type": "multi",
|
|
35
|
+
"phase": "play",
|
|
36
|
+
"label": "Called numbers", "hint": "Numbers called so far; the latest selection is highlighted",
|
|
37
|
+
"container": "big",
|
|
38
|
+
"choose": [
|
|
39
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
|
40
|
+
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
|
41
|
+
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
|
42
|
+
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
|
43
|
+
41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
|
44
|
+
51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
|
|
45
|
+
61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
|
|
46
|
+
71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
|
|
47
|
+
81, 82, 83, 84, 85, 86, 87, 88, 89, 90
|
|
48
|
+
],
|
|
49
|
+
"cols": 10,
|
|
50
|
+
"rows": 9,
|
|
51
|
+
"responsive": true,
|
|
52
|
+
"default": [3, 8, 17, 24, 31, 42, 56, 7]
|
|
53
|
+
},
|
|
54
|
+
"game/reset": {
|
|
55
|
+
"type": "reset",
|
|
56
|
+
"phase": "play",
|
|
57
|
+
"icon": "undo",
|
|
58
|
+
"text": "Reset balls", "hint": "Clear every called number",
|
|
59
|
+
"values": {
|
|
60
|
+
"game/called": []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
**index.html**
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<body>
|
|
70
|
+
<main class="bingo">
|
|
71
|
+
<section class="caller">
|
|
72
|
+
<div class="eyebrow" id="eyebrow">Just called</div>
|
|
73
|
+
<div class="ball" id="ball">73</div>
|
|
74
|
+
<div class="title" id="title">Friday Night Bingo</div>
|
|
75
|
+
<div class="subtitle" id="subtitle">Eyes down — good luck!</div>
|
|
76
|
+
<div class="count" id="count">8 numbers called</div>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<section class="board">
|
|
80
|
+
<div class="board-header">
|
|
81
|
+
<strong>CALLED NUMBERS</strong>
|
|
82
|
+
<span class="legend">WHITE · CALLED ORANGE · CURRENT</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="numbers" id="numbers"></div>
|
|
85
|
+
</section>
|
|
86
|
+
</main>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
const
|
|
90
|
+
MAX_NUMBER = 90,
|
|
91
|
+
$ = (id) => document.getElementById(id),
|
|
92
|
+
cells = new Map();
|
|
93
|
+
|
|
94
|
+
let called = [];
|
|
95
|
+
|
|
96
|
+
for (let number = 1; number <= MAX_NUMBER; number++) {
|
|
97
|
+
const cell = document.createElement("div");
|
|
98
|
+
cell.className = "number";
|
|
99
|
+
cell.textContent = number;
|
|
100
|
+
$("numbers").append(cell);
|
|
101
|
+
cells.set(number, cell);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateBoard() {
|
|
105
|
+
const
|
|
106
|
+
values = (Array.isArray(called) ? called : []).map(Number).filter(number => number >= 1 && number <= MAX_NUMBER),
|
|
107
|
+
currentNumber = values.at(-1),
|
|
108
|
+
calledNumbers = new Set(values);
|
|
109
|
+
|
|
110
|
+
cells.forEach((cell, number) => {
|
|
111
|
+
cell.classList.toggle("called", calledNumbers.has(number));
|
|
112
|
+
cell.classList.toggle("current", number === currentNumber);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const ballValue = currentNumber ?? "—";
|
|
116
|
+
if ($("ball").textContent !== String(ballValue)) {
|
|
117
|
+
const replacement = $("ball").cloneNode(false);
|
|
118
|
+
replacement.textContent = ballValue;
|
|
119
|
+
$("ball").replaceWith(replacement);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$("count").textContent = `${calledNumbers.size} ${calledNumbers.size === 1 ? "number" : "numbers"} called`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
window.addEventListener("nebular-overlay:update", ({detail}) => {
|
|
126
|
+
|
|
127
|
+
if (detail.content?.eyebrow != null) $("eyebrow").textContent = detail.content.eyebrow;
|
|
128
|
+
if (detail.content?.title != null) $("title").textContent = detail.content.title;
|
|
129
|
+
if (detail.content?.subtitle != null) $("subtitle").textContent = detail.content.subtitle;
|
|
130
|
+
if (detail.game?.called != null) called = detail.game.called;
|
|
131
|
+
if (detail.game?.called != null) updateBoard();
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
</script>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### An automatic news ticker
|
|
138
|
+
**manifest.json**
|
|
139
|
+
```
|
|
140
|
+
{
|
|
141
|
+
"instructions": "Build the running news list in playback order. Each item remains on screen for the selected interval; its headline slides upward to the next item, while the topic changes only when the next item belongs to a different topic.",
|
|
142
|
+
"meta": {
|
|
143
|
+
"content/brand": {"type": "string", "phase": "edit", "label": "Brand text", "hint": "US OPEN", "default": "US OPEN"},
|
|
144
|
+
"news": {
|
|
145
|
+
"type": "set", "phase": "edit", "label": "News items", "hint": "Headlines shown in their playback order", "container": "big", "max": 30,
|
|
146
|
+
"default": [
|
|
147
|
+
{"topic": "MEN'S DOUBLES", "tagline": "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6-4 6–4 6–2"},
|
|
148
|
+
{"topic": "MEN'S DOUBLES", "tagline": "The quarter-final line-up is now complete in New York"},
|
|
149
|
+
{"topic": "WOMEN'S SINGLES", "tagline": "A dramatic final set sends the defending champion through"},
|
|
150
|
+
{"topic": "AROUND THE GROUNDS", "tagline": "Evening play begins under the lights at Arthur Ashe Stadium"}
|
|
151
|
+
],
|
|
152
|
+
"meta": {
|
|
153
|
+
"topic": {"type": "string", "phase": "edit", "label": "News topic", "hint": "MEN'S DOUBLES"},
|
|
154
|
+
"tagline": {"type": "string", "phase": "edit", "label": "News tagline", "hint": "Enter headline"}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"timing/interval": {"type": "number", "phase": "edit", "label": "News interval", "hint": "Seconds between headlines", "default": 5, "min": 2, "max": 30},
|
|
158
|
+
"style/background": {"type": "color", "phase": "edit", "label": "Background", "hint": "Background colour used by the ticker", "default": "#0759a6"}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**index.html**
|
|
164
|
+
(excluding styles, etc)
|
|
165
|
+
```
|
|
166
|
+
<body>
|
|
167
|
+
<main class="ticker" id="ticker">
|
|
168
|
+
<section class="zone topic"><div class="viewport" id="topicSlot">
|
|
169
|
+
<div class="line">MEN'S DOUBLES</div><div class="line waiting"></div>
|
|
170
|
+
</div></section>
|
|
171
|
+
<section class="zone headline"><div class="viewport" id="headlineSlot">
|
|
172
|
+
<div class="line">Carlos Alcaraz defeats Tommy Paul, 7–6 7–6 6–4</div><div class="line waiting"></div>
|
|
173
|
+
</div></section>
|
|
174
|
+
<section class="zone brand"><div class="viewport"><div class="line" id="brand">US OPEN</div></div></section>
|
|
175
|
+
</main>
|
|
176
|
+
<script>
|
|
177
|
+
const defaults = [
|
|
178
|
+
{topic: "MEN'S DOUBLES", tagline: "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6–4 6–4 6–2"},
|
|
179
|
+
{topic: "MEN'S DOUBLES", tagline: "The quarter-final line-up is now complete in New York"},
|
|
180
|
+
{topic: "WOMEN'S SINGLES", tagline: "A dramatic final set sends the defending champion through"},
|
|
181
|
+
{topic: "AROUND THE GROUNDS", tagline: "Evening play begins under the lights at Arthur Ashe Stadium"}
|
|
182
|
+
];
|
|
183
|
+
const TRANSITION_MS = 580;
|
|
184
|
+
const ticker = document.getElementById("ticker");
|
|
185
|
+
const brand = document.getElementById("brand");
|
|
186
|
+
|
|
187
|
+
function makeSlot(id) {
|
|
188
|
+
const lines = document.getElementById(id).querySelectorAll(".line");
|
|
189
|
+
return {current: lines[0], next: lines[1], timer: null};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const topicSlot = makeSlot("topicSlot");
|
|
193
|
+
const headlineSlot = makeSlot("headlineSlot");
|
|
194
|
+
let items = defaults;
|
|
195
|
+
let currentIndex = 0;
|
|
196
|
+
let intervalSeconds = 5;
|
|
197
|
+
let cycleTimer = null;
|
|
198
|
+
|
|
199
|
+
function cleanItems(value) {
|
|
200
|
+
if (!Array.isArray(value)) return [];
|
|
201
|
+
return value.map(item => ({
|
|
202
|
+
topic: String(item?.topic ?? "").trim(),
|
|
203
|
+
tagline: String(item?.tagline ?? "").trim()
|
|
204
|
+
})).filter(item => item.topic || item.tagline);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setSlot(slot, text) {
|
|
208
|
+
clearTimeout(slot.timer);
|
|
209
|
+
slot.current.textContent = text;
|
|
210
|
+
slot.current.className = "line";
|
|
211
|
+
slot.next.textContent = "";
|
|
212
|
+
slot.next.className = "line waiting";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function animateSlot(slot, text) {
|
|
216
|
+
clearTimeout(slot.timer);
|
|
217
|
+
slot.next.textContent = text;
|
|
218
|
+
slot.next.className = "line waiting";
|
|
219
|
+
void slot.next.offsetHeight;
|
|
220
|
+
slot.current.className = "line leaving";
|
|
221
|
+
slot.next.className = "line";
|
|
222
|
+
slot.timer = setTimeout(() => {
|
|
223
|
+
const previous = slot.current;
|
|
224
|
+
slot.current = slot.next;
|
|
225
|
+
slot.next = previous;
|
|
226
|
+
slot.next.textContent = "";
|
|
227
|
+
slot.next.className = "line waiting";
|
|
228
|
+
}, TRANSITION_MS);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function showCurrent() {
|
|
232
|
+
const item = items[currentIndex];
|
|
233
|
+
ticker.classList.toggle("empty", !item);
|
|
234
|
+
if (!item) return;
|
|
235
|
+
setSlot(topicSlot, item.topic);
|
|
236
|
+
setSlot(headlineSlot, item.tagline);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scheduleCycle() {
|
|
240
|
+
clearTimeout(cycleTimer);
|
|
241
|
+
if (items.length > 1) cycleTimer = setTimeout(nextItem, intervalSeconds * 1000);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function nextItem() {
|
|
245
|
+
if (items.length < 2) return;
|
|
246
|
+
const previous = items[currentIndex];
|
|
247
|
+
currentIndex = (currentIndex + 1) % items.length;
|
|
248
|
+
const next = items[currentIndex];
|
|
249
|
+
animateSlot(headlineSlot, next.tagline);
|
|
250
|
+
if (next.topic !== previous.topic) animateSlot(topicSlot, next.topic);
|
|
251
|
+
scheduleCycle();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function replaceItems(value) {
|
|
255
|
+
items = cleanItems(value);
|
|
256
|
+
currentIndex = 0;
|
|
257
|
+
showCurrent();
|
|
258
|
+
scheduleCycle();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
window.addEventListener("nebular-overlay:update", ({detail}) => {
|
|
262
|
+
if (detail.content?.brand != null) brand.textContent = detail.content.brand;
|
|
263
|
+
if (detail.style?.background) ticker.style.setProperty("--background", detail.style.background);
|
|
264
|
+
if (detail.timing?.interval != null) {
|
|
265
|
+
const interval = Number(detail.timing.interval);
|
|
266
|
+
if (Number.isFinite(interval)) intervalSeconds = Math.min(30, Math.max(2, interval));
|
|
267
|
+
scheduleCycle();
|
|
268
|
+
}
|
|
269
|
+
if (detail.news != null) replaceItems(detail.news);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
showCurrent();
|
|
273
|
+
scheduleCycle();
|
|
274
|
+
</script>
|
|
275
|
+
</body>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### A simple call-to-action
|
|
279
|
+
|
|
280
|
+
**manifest.json**
|
|
281
|
+
```
|
|
282
|
+
{
|
|
283
|
+
"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.",
|
|
284
|
+
"meta": {
|
|
285
|
+
"content/eyebrow": {"type": "string", "phase": "edit", "label": "Eyebrow", "hint": "Enter short label", "default": "VIEWER GOAL"},
|
|
286
|
+
"content/title": {"type": "string", "phase": "edit", "label": "Title", "hint": "Enter title", "default": "Help us reach 10,000 followers"},
|
|
287
|
+
"content/subtitle": {"type": "string", "phase": "edit", "label": "Description", "hint": "Enter description", "default": "Follow the channel and join the Nebular community."},
|
|
288
|
+
"content/action": {"type": "string", "phase": "edit", "label": "Action", "hint": "e.g. FOLLOW NOW", "default": "FOLLOW NOW"},
|
|
289
|
+
"goal/playTarget": {"type": "number", "phase": "play", "label": "Target", "hint": "Goal currently used by the live progress display", "source": "goal/target", "readonly": true},
|
|
290
|
+
"goal/current": {"type": "number", "phase": "play", "label": "Current", "hint": "Enter current value", "min": 0, "default": 7240},
|
|
291
|
+
"goal/target": {"type": "number", "phase": "edit", "label": "Target", "hint": "Enter target", "min": 1, "default": 10000}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
**index.html**
|
|
296
|
+
```
|
|
297
|
+
<body>
|
|
298
|
+
<section class="card">
|
|
299
|
+
<div class="eyebrow" id="eyebrow">Viewer goal</div>
|
|
300
|
+
<div class="title" id="title">Help us reach 10,000 followers</div>
|
|
301
|
+
<div class="subtitle" id="subtitle">Follow the channel and join the Nebular community.</div>
|
|
302
|
+
<div class="progress"><span id="progress"></span></div>
|
|
303
|
+
<div class="footer"><span id="action">FOLLOW NOW</span><span class="count" id="count">7,240 / 10,000</span></div>
|
|
304
|
+
</section>
|
|
305
|
+
<script>
|
|
306
|
+
const $ = id => document.getElementById(id);
|
|
307
|
+
const progressState = {current: 7240, target: 10000};
|
|
308
|
+
|
|
309
|
+
function renderProgress() {
|
|
310
|
+
const current = Math.max(0, Number(progressState.current) || 0),
|
|
311
|
+
target = Math.max(1, Number(progressState.target) || 1);
|
|
312
|
+
$("progress").style.width = `${Math.min(100, current / target * 100)}%`;
|
|
313
|
+
$("count").textContent = `${current.toLocaleString()} / ${target.toLocaleString()}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
window.addEventListener("nebular-overlay:update", ({detail}) => {
|
|
317
|
+
if (detail.content?.eyebrow != null) $("eyebrow").textContent = detail.content.eyebrow;
|
|
318
|
+
if (detail.content?.title != null) $("title").textContent = detail.content.title;
|
|
319
|
+
if (detail.content?.subtitle != null) $("subtitle").textContent = detail.content.subtitle;
|
|
320
|
+
if (detail.content?.action != null) $("action").textContent = detail.content.action;
|
|
321
|
+
if (detail.goal?.current != null) progressState.current = detail.goal.current;
|
|
322
|
+
if (detail.goal?.target != null) progressState.target = detail.goal.target;
|
|
323
|
+
if (detail.goal?.current != null || detail.goal?.target != null) renderProgress();
|
|
324
|
+
});
|
|
325
|
+
</script>
|
|
326
|
+
</body>
|
|
327
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Manifest contract
|
|
2
|
+
|
|
3
|
+
Each template has a `manifest.json` object with an optional concise `instructions` string and a required `meta` object.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"instructions": "Configure the title before going live. Update the score during play.",
|
|
8
|
+
"meta": {
|
|
9
|
+
"content/title": {"type": "string", "phase": "edit", "label": "Title", "hint": "Enter title", "default": "Championship Final"},
|
|
10
|
+
"game/score": {"type": "number", "phase": "play", "label": "Score", "min": 0, "default": 0}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Slash-separated keys become nested update values: `content/title` arrives as `detail.content.title`. Path segments should contain only letters, numbers, hyphens, and underscores.
|
|
16
|
+
|
|
17
|
+
## Shared fields
|
|
18
|
+
|
|
19
|
+
- `type`: required control type.
|
|
20
|
+
- `label`: short operator-facing caption. Buttons may use `text` instead.
|
|
21
|
+
- `default`: initial Studio value. It must agree with the initial HTML presentation.
|
|
22
|
+
- `phase`: optional `edit` or `play`. Omit only when a field genuinely belongs in both modes.
|
|
23
|
+
- `when`: optional conditional visibility rule in the form `{"key": "path", "is": value}`. The control is shown only while the current value at `key` strictly equals `is`.
|
|
24
|
+
- `hint`: short placeholder for `string` and `date`; descriptive hover help for other controls.
|
|
25
|
+
- `readonly`: displays the value without allowing edits.
|
|
26
|
+
- `source`: makes this control a UI alias for another canonical value path. The alias path is not persisted.
|
|
27
|
+
- `size`: optional editor column width; `break` starts a new editor row.
|
|
28
|
+
- `container`: `"big"` or `"small"` editor-space preference.
|
|
29
|
+
- `class`: optional Studio presentation class; use only when an established repository convention requires it.
|
|
30
|
+
|
|
31
|
+
## Conditional controls
|
|
32
|
+
|
|
33
|
+
Every control type supports `when`. Use it to keep a crowded Edit or Play panel focused when one value selects among several screens or operating states:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"screen": {
|
|
38
|
+
"type": "choose",
|
|
39
|
+
"phase": "play",
|
|
40
|
+
"label": "Screen",
|
|
41
|
+
"default": "scores",
|
|
42
|
+
"choose": [
|
|
43
|
+
{"value": "scores", "caption": "Hi-Scores"},
|
|
44
|
+
{"value": "playing", "caption": "Now Playing"},
|
|
45
|
+
{"value": "idle", "caption": "Game Idle"}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"current/name": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"phase": "play",
|
|
51
|
+
"label": "Current player",
|
|
52
|
+
"default": "VEX",
|
|
53
|
+
"when": {"key": "screen", "is": "playing"}
|
|
54
|
+
},
|
|
55
|
+
"idle/headline": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"phase": "play",
|
|
58
|
+
"label": "Announcement",
|
|
59
|
+
"default": "ROOK ENTERS THE ARENA",
|
|
60
|
+
"when": {"key": "screen", "is": "idle"}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `key` is a slash-separated value path and must identify another canonical control in the same `meta` object.
|
|
66
|
+
- `is` is compared with strict equality, so its JSON type must match the controlling value. For example, `1` and `"1"` are different.
|
|
67
|
+
- `phase` and `when` are cumulative: a control is visible only when both filters pass.
|
|
68
|
+
- Conditional visibility affects only the generated editor. A hidden value remains initialized, persisted, and available to the template; hiding it never clears or resets it.
|
|
69
|
+
- Prefer one clear controlling field such as `screen` over chains of interdependent conditions. `when` supports one equality test, not compound expressions.
|
|
70
|
+
- In a nested `set` record, `key` refers to a sibling field in that record's nested `meta`.
|
|
71
|
+
|
|
72
|
+
## Types
|
|
73
|
+
|
|
74
|
+
### `string`
|
|
75
|
+
|
|
76
|
+
A text input. `default` is a string. Supports `maxLength` and `pattern`. Keep `hint` short because it appears inside the empty input.
|
|
77
|
+
|
|
78
|
+
### `number` and `range`
|
|
79
|
+
|
|
80
|
+
Numeric controls supporting `min`, `max`, and (for `range`) `step`. Studio may emit numeric input values as strings, so page logic should convert with `Number(...)` when arithmetic matters.
|
|
81
|
+
|
|
82
|
+
### `date`
|
|
83
|
+
|
|
84
|
+
A local date-time picker whose persisted value is a finite Unix epoch in milliseconds. Use a numeric default such as `1795046400000`. The page can compare it directly with `Date.now()`.
|
|
85
|
+
|
|
86
|
+
### `color`
|
|
87
|
+
|
|
88
|
+
A color picker. The default is a CSS color string. Optional fields include `noalpha`, `remove`, and `caption`. If `remove` is enabled, the update may contain `null`.
|
|
89
|
+
|
|
90
|
+
### `choose` and `select`
|
|
91
|
+
|
|
92
|
+
Single-choice controls with a `choose` array and scalar default. `choose` renders buttons; `select` renders a dropdown. Entries may be primitives or captioned objects:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{"value": "car", "caption": "Car"}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Keep the default's type consistent with the choice values.
|
|
99
|
+
|
|
100
|
+
### `multi`
|
|
101
|
+
|
|
102
|
+
A multiple-choice grid. `choose` is an array and `default` is an array. Useful options are `cols`, `rows`, `responsive`, `minCellWidth`, and `container`.
|
|
103
|
+
|
|
104
|
+
### `set`
|
|
105
|
+
|
|
106
|
+
An editable array of records. Define record fields in nested `meta`, give `default` an array of matching objects, and usually set a practical `max`.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
"news": {
|
|
110
|
+
"type": "set", "phase": "edit", "label": "News items", "container": "big", "max": 20,
|
|
111
|
+
"default": [{"topic": "SPORT", "headline": "Final begins tonight"}],
|
|
112
|
+
"meta": {
|
|
113
|
+
"topic": {
|
|
114
|
+
"type": "string", "label": "Topic", "hint": "SPORT"},
|
|
115
|
+
"headline": {"type": "string", "label": "Headline", "hint": "Enter headline"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Nested sets are supported, but prefer a flat record when it expresses the workflow; deep editors are cumbersome for operators.
|
|
122
|
+
|
|
123
|
+
### `selector`
|
|
124
|
+
|
|
125
|
+
A one-based index selector for a set, normally used in `play` mode. `set` is required and identifies the target value path. `include` optionally lists record fields to display beneath the selector.
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"control/current": {"type": "selector", "phase": "play", "label": "Current question", "set": "questions", "include": ["question"]}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `action`
|
|
132
|
+
|
|
133
|
+
A button that emits `Date.now()` at its value path. Use `text`, `icon`, and `hint`. Treat each new timestamp as a trigger; do not depend on a particular timestamp value.
|
|
134
|
+
|
|
135
|
+
### `reset`
|
|
136
|
+
|
|
137
|
+
A button that assigns several declared paths at once. Its `values` object maps canonical manifest paths to reset values.
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
"game/reset": {
|
|
141
|
+
"type": "reset", "phase": "play", "text": "Reset game", "icon": "undo",
|
|
142
|
+
"hint": "Return the game to its initial state", "values": {"game/score": 0, "game/history": []
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Defaults and aliases
|
|
148
|
+
|
|
149
|
+
- Do not use truthiness to read defaults or updates: `0`, `false`, `""`, and `[]` are meaningful.
|
|
150
|
+
- A `source` control reads and writes the canonical source path. Do not also persist the alias key.
|
|
151
|
+
- A hidden `when` control retains its value; do not rely on visibility to clear state.
|
|
152
|
+
- Set and multi updates replace that entire parameter value; fields absent from an event remain unchanged.
|
|
153
|
+
- Keep reset targets and selector set paths aligned with declared canonical paths.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Runtime and design guidance
|
|
2
|
+
|
|
3
|
+
## Page baseline
|
|
4
|
+
|
|
5
|
+
Use a transparent, fixed render surface:
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
* { box-sizing: border-box }
|
|
9
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent; }
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Design primarily for 1920×1080. Use percentages, viewport units, grids, flex layout, and `clamp()` where they improve scaling. Keep essential graphics inside sensible title-safe margins unless the design intentionally touches an edge.
|
|
13
|
+
|
|
14
|
+
Broadcast copy must survive motion and detailed video beneath it. Prefer:
|
|
15
|
+
|
|
16
|
+
- large type for the primary value (roughly 40–70 px at 1080p, often larger for scores or countdowns);
|
|
17
|
+
- high contrast, restrained shadows, translucent panels, or local backplates;
|
|
18
|
+
- short line lengths and deliberate truncation/wrapping behavior;
|
|
19
|
+
- clear hierarchy rather than many equally prominent elements;
|
|
20
|
+
- tabular numerals for clocks and scores (`font-variant-numeric: tabular-nums`).
|
|
21
|
+
|
|
22
|
+
## Incremental updates
|
|
23
|
+
|
|
24
|
+
Every event is a patch, not a complete state replacement:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
window.addEventListener("nebular-overlay:update", ({detail = {}}) => {
|
|
28
|
+
if (detail.content?.title != null) title.textContent = detail.content.title;
|
|
29
|
+
if (detail.style?.accent != null) root.style.setProperty("--accent", detail.style.accent);
|
|
30
|
+
if (Array.isArray(detail.items)) { state.items = detail.items; renderItems(); }
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `!= null`, explicit property checks, or type checks. Do not write `if (detail.game?.score)` when zero is valid. Do not reset unrelated fields on every event.
|
|
35
|
+
|
|
36
|
+
Treat update data as untrusted. Use `textContent`, DOM properties, and validated style values; avoid `innerHTML`. The page should already display useful sample content before the first event, and manifest defaults should match it.
|
|
37
|
+
|
|
38
|
+
## State and animations
|
|
39
|
+
|
|
40
|
+
Keep state local and small. Update existing nodes instead of rebuilding the whole page. For transitions, prefer opacity and transforms; avoid expensive layout animation, filters over large areas, and perpetual high-frequency work.
|
|
41
|
+
|
|
42
|
+
Cancel or replace outstanding timers when relevant values change. Handle rapid consecutive updates without leaving duplicated nodes or stale classes.
|
|
43
|
+
|
|
44
|
+
Clocks and countdowns must derive their display from an absolute epoch:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
function renderClock() {
|
|
48
|
+
const remaining = Math.max(0, state.startsAt - Date.now());
|
|
49
|
+
// derive days/hours/minutes/seconds from remaining
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Do not decrement a stored number once per second: redraws, throttling, dropped frames, and page suspension cause drift. A timestamp also keeps independent renderers synchronized closely enough for display.
|
|
54
|
+
|
|
55
|
+
Call `window.invalidate?.()` only if a small static DOM change is not captured automatically; normal updates should not need it.
|
|
56
|
+
|
|
57
|
+
## Assets and isolation
|
|
58
|
+
|
|
59
|
+
Allowed local asset formats are CSS (`.css`), images (`.jpg`, `.jpeg`, `.png`, `.webp`), and fonts (`.otf`, `.ttf`, `.woff`, `.woff2`). Each served file is limited to 5 MiB. Reference assets with relative URLs. Keep JavaScript inline in `index.html`; local JavaScript files are not served.
|
|
60
|
+
|
|
61
|
+
Do not use external URLs or attempt network access. The production renderer blocks external scripts, styles, fonts, images, `fetch`, XHR, WebSocket, EventSource, audio/video resources, frames, embedded objects, forms, workers, popups, storage, WebGL, canvas rendering, WASM, WebAudio, and device APIs.
|
|
62
|
+
|
|
63
|
+
Avoid large DOMs, large assets, tight loops, aggressive timers, and expensive effects. Templates are reviewed for render cost even when their APIs are technically permitted.
|
|
64
|
+
|
|
65
|
+
## Useful implementation patterns
|
|
66
|
+
|
|
67
|
+
- Put editable colors into CSS custom properties and update the property.
|
|
68
|
+
- Use `object-fit: contain` for optional organization-supplied logos.
|
|
69
|
+
- Give missing assets a restrained fallback rather than breaking the composition.
|
|
70
|
+
- For a `selector`, clamp the one-based value to the current set length before indexing.
|
|
71
|
+
- When a set becomes empty, render a deliberate empty state or hide the graphic.
|
|
72
|
+
- For repeated ticker items, maintain the current index and restart scheduling only when the list or timing changes.
|
|
73
|
+
- If a topic repeats across consecutive ticker items, animate only the changing headline.
|
|
74
|
+
|
|
75
|
+
## Local test events
|
|
76
|
+
|
|
77
|
+
Exercise both full and partial patches from the browser console:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
window.dispatchEvent(new CustomEvent("nebular-overlay:update", {
|
|
81
|
+
detail: {content: {title: "Local preview"}, style: {accent: "#ff9818"}}
|
|
82
|
+
}));
|
|
83
|
+
window.dispatchEvent(new CustomEvent("nebular-overlay:update", {
|
|
84
|
+
detail: {content: {title: "Only the title changes"}}
|
|
85
|
+
}));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Verify that the second event does not reset the accent or any internal play state.
|