@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,160 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Event Countdown</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, Arial, Helvetica, sans-serif;
21
+ }
22
+
23
+ .countdown-card {
24
+ --panel: #fff;
25
+ --ink: #090909;
26
+ position: absolute;
27
+ top: 4.5%;
28
+ right: 4.5%;
29
+ filter: drop-shadow(0 1rem 2.25rem #0007);
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ }
34
+
35
+ .clock {
36
+ display: flex;
37
+ color: var(--ink);
38
+ overflow: hidden;
39
+ font-size: 3rem;
40
+ font-weight: bold;
41
+ line-height: 3rem;
42
+ }
43
+
44
+ .clock span {
45
+ background: var(--panel);
46
+ margin: 0 0.1rem;
47
+ padding: 0 0.1rem;
48
+ }
49
+
50
+ .time {
51
+ min-width: 0;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ white-space: nowrap
56
+ }
57
+
58
+ .days-label {
59
+ background: var(--panel);
60
+ font-variant: all-petite-caps;
61
+ }
62
+
63
+ .logo-frame {
64
+ width: 100%;
65
+ min-height: 9rem;
66
+ max-height: 22rem;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ overflow: hidden;
71
+ background: transparent
72
+ }
73
+
74
+ .logo {
75
+ display: block;
76
+ width: 100%;
77
+ height: auto;
78
+ max-height: 22rem;
79
+ object-fit: contain;
80
+ object-position: center top;
81
+ margin-top: -2rem;
82
+ }
83
+
84
+ .logo-missing {
85
+ display: none;
86
+ width: 100%;
87
+ padding: 2.5rem 1rem;
88
+ color: #fff;
89
+ background: #111d;
90
+ text-align: center;
91
+ font-size: 2rem;
92
+ font-weight: 850;
93
+ letter-spacing: .08em;
94
+ text-transform: uppercase
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <main class="countdown-card" id="card">
100
+ <section class="clock" aria-label="Event countdown">
101
+ <div class="days-number" id="days">00</div>
102
+ <div class="days-label" id="daysLabel">D</div>
103
+ <div class="time" id="time">00:00:00</div>
104
+ </section>
105
+ <div class="logo-frame">
106
+ <img class="logo" id="logo" src="logo.png" alt="Event logo">
107
+ <div class="logo-missing" id="logoMissing">logo.png</div>
108
+ </div>
109
+ </main>
110
+
111
+ <script>
112
+ const card = document.getElementById("card");
113
+ const days = document.getElementById("days");
114
+ const daysLabel = document.getElementById("daysLabel");
115
+ const time = document.getElementById("time");
116
+ const logo = document.getElementById("logo");
117
+ const logoMissing = document.getElementById("logoMissing");
118
+ let startsAt = 1795046400000;
119
+ let timer = null;
120
+
121
+ const
122
+ pad = value => String(value).padStart(2, "0"),
123
+ spanner = (str) => "<span>" + (str.split("").join("</span><span>")) + "</span>";
124
+
125
+ function renderCountdown() {
126
+ const remaining = Math.max(0, startsAt - Date.now());
127
+ const totalSeconds = Math.floor(remaining / 1000);
128
+ const dayCount = Math.floor(totalSeconds / 86400);
129
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
130
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
131
+ const seconds = totalSeconds % 60;
132
+
133
+ days.innerHTML = spanner(pad(dayCount));
134
+ time.innerHTML = spanner(`${pad(hours)}:${pad(minutes)}:${pad(seconds)}`);
135
+
136
+ clearTimeout(timer);
137
+ window.invalidate?.();
138
+ if (remaining > 0) timer = setTimeout(renderCountdown, 1000 - Date.now() % 1000 + 15);
139
+ }
140
+
141
+ logo.addEventListener("error", () => {
142
+ logo.style.display = "none";
143
+ logoMissing.style.display = "block";
144
+ });
145
+
146
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
147
+ if (detail.event?.startsAt != null) {
148
+ const nextStartsAt = Number(detail.event.startsAt);
149
+ if (Number.isFinite(nextStartsAt)) startsAt = nextStartsAt;
150
+ renderCountdown();
151
+ }
152
+ if (detail.content?.days != null) daysLabel.textContent = detail.content.days;
153
+ if (detail.style?.color) card.style.setProperty("--panel", detail.style.color);
154
+ if (detail.style?.textColor) card.style.setProperty("--ink", detail.style.textColor);
155
+ });
156
+
157
+ renderCountdown();
158
+ </script>
159
+ </body>
160
+ </html>
@@ -0,0 +1,33 @@
1
+ {
2
+ "instructions": "Choose the event date and time, then add logo.png to the template directory. The countdown is derived from that fixed moment and stops at zero when the event begins.",
3
+ "meta": {
4
+ "event/startsAt": {
5
+ "type": "date",
6
+ "phase": "edit",
7
+ "label": "Event date and time",
8
+ "hint": "Choose the event date and time",
9
+ "default": 1795046400000
10
+ },
11
+ "content/days": {
12
+ "type": "string",
13
+ "phase": "edit",
14
+ "label": "Days label",
15
+ "hint": "Days",
16
+ "default": "Days"
17
+ },
18
+ "style/color": {
19
+ "type": "color",
20
+ "phase": "edit",
21
+ "label": "Countdown colour",
22
+ "hint": "Background colour behind the countdown",
23
+ "default": "#ffffff"
24
+ },
25
+ "style/textColor": {
26
+ "type": "color",
27
+ "phase": "edit",
28
+ "label": "Text colour",
29
+ "hint": "Countdown numbers and label colour",
30
+ "default": "#090909"
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,77 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Lower Third</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
+ .lower {
25
+ position: absolute;
26
+ left: 5%;
27
+ bottom: 8%;
28
+ display: flex;
29
+ filter: drop-shadow(0 1rem 2rem #0005)
30
+ }
31
+
32
+ .accent {
33
+ width: 1rem;
34
+ background: #ff9818;
35
+ border-radius: .55rem 0 0 .55rem
36
+ }
37
+
38
+ .content {
39
+ min-width: 48rem;
40
+ padding: 1.8rem 2.4rem;
41
+ background: #090e17f0;
42
+ border-radius: 0 .75rem .75rem 0
43
+ }
44
+
45
+ .name {
46
+ font-size: 3.1rem;
47
+ font-weight: 850
48
+ }
49
+
50
+ .role {
51
+ margin-top: .45rem;
52
+ color: #ffb14b;
53
+ font-size: 1.8rem;
54
+ font-weight: 650;
55
+ letter-spacing: .08em;
56
+ text-transform: uppercase
57
+ }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <section class="lower">
62
+ <div class="accent" id="accent"></div>
63
+ <div class="content">
64
+ <div class="name" id="name">Alex Morgan</div>
65
+ <div class="role" id="role">Creative Director · Nebular TV</div>
66
+ </div>
67
+ </section>
68
+ <script>
69
+ const $ = id => document.getElementById(id);
70
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
71
+ if (detail.person?.name != null) $("name").textContent = detail.person.name;
72
+ if (detail.person?.role != null) $("role").textContent = detail.person.role;
73
+ if (detail.style?.accent != null) $("accent").style.backgroundColor = detail.style.accent
74
+ });
75
+ </script>
76
+ </body>
77
+ </html>
@@ -0,0 +1,8 @@
1
+ {
2
+ "instructions": "Enter the guest or presenter name and their role or subtitle, choose an accent colour, then press Update to refresh the lower-third identification.",
3
+ "meta": {
4
+ "person/name": {"type": "string", "phase": "play", "label": "Name", "hint": "Enter name", "default": "Alex Morgan"},
5
+ "person/role": {"type": "string", "phase": "play", "label": "Role or subtitle", "hint": "Enter role or subtitle", "default": "Creative Director · Nebular TV"},
6
+ "style/accent": {"type": "color", "phase": "edit", "label": "label", "hint": "Accent colour used by the lower-third graphic", "default": "#ff9818"}
7
+ }
8
+ }
@@ -0,0 +1,153 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>News Ticker</title>
6
+ <style>
7
+ * { box-sizing: border-box }
8
+ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent }
9
+ body { color: #fff; font-family: Inter, Arial, Helvetica, sans-serif }
10
+ .ticker {
11
+ --background: #0759a6;
12
+ position: absolute; left: 0; right: 0; bottom: 0; height: 8.75rem;
13
+ display: grid; grid-template-columns: minmax(15rem, 20%) minmax(0, 1fr) minmax(13rem, 17%);
14
+ overflow: hidden; background: var(--background); border-radius: .35rem; box-shadow: 0 1.1rem 3rem #0007
15
+ }
16
+ .zone { min-width: 0; position: relative; display: flex; align-items: center; overflow: hidden }
17
+ .topic, .brand { justify-content: center; padding: 0 1.6rem; background: #0002 }
18
+ .topic { border-right: 1px solid #fff5 }
19
+ .brand { border-left: 1px solid #fff5 }
20
+ .viewport { position: relative; width: 100%; height: 100%; overflow: hidden }
21
+ .line {
22
+ position: absolute; inset: 0; display: flex; align-items: center; min-width: 0;
23
+ transition: transform .58s cubic-bezier(.7, 0, .25, 1), opacity .58s ease;
24
+ will-change: transform, opacity
25
+ }
26
+ .topic .line {
27
+ justify-content: center; text-align: center; font-size: 2.35rem; line-height: 1.05;
28
+ font-weight: 900; letter-spacing: .055em; text-transform: uppercase
29
+ }
30
+ .headline .line {
31
+ padding: 0 2.4rem; font-size: 3.25rem; line-height: 1.08;
32
+ font-weight: 760; letter-spacing: -.025em
33
+ }
34
+ .brand .line {
35
+ justify-content: center; text-align: center; font-size: 2.55rem; line-height: 1;
36
+ font-weight: 950; letter-spacing: .08em; text-transform: uppercase
37
+ }
38
+ .line.waiting { transform: translateY(110%); opacity: 0 }
39
+ .line.leaving { transform: translateY(-110%); opacity: 0 }
40
+ .ticker.empty { opacity: 0 }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <main class="ticker" id="ticker">
45
+ <section class="zone topic"><div class="viewport" id="topicSlot">
46
+ <div class="line">MEN'S DOUBLES</div><div class="line waiting"></div>
47
+ </div></section>
48
+ <section class="zone headline"><div class="viewport" id="headlineSlot">
49
+ <div class="line">Carlos Alcaraz defeats Tommy Paul, 7–6 7–6 6–4</div><div class="line waiting"></div>
50
+ </div></section>
51
+ <section class="zone brand"><div class="viewport"><div class="line" id="brand">US OPEN</div></div></section>
52
+ </main>
53
+ <script>
54
+ const defaults = [
55
+ {topic: "MEN'S DOUBLES", tagline: "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6–4 6–4 6–2"},
56
+ {topic: "MEN'S DOUBLES", tagline: "The quarter-final line-up is now complete in New York"},
57
+ {topic: "WOMEN'S SINGLES", tagline: "A dramatic final set sends the defending champion through"},
58
+ {topic: "AROUND THE GROUNDS", tagline: "Evening play begins under the lights at Arthur Ashe Stadium"}
59
+ ];
60
+ const TRANSITION_MS = 580;
61
+ const ticker = document.getElementById("ticker");
62
+ const brand = document.getElementById("brand");
63
+
64
+ function makeSlot(id) {
65
+ const lines = document.getElementById(id).querySelectorAll(".line");
66
+ return {current: lines[0], next: lines[1], timer: null};
67
+ }
68
+
69
+ const topicSlot = makeSlot("topicSlot");
70
+ const headlineSlot = makeSlot("headlineSlot");
71
+ let items = defaults;
72
+ let currentIndex = 0;
73
+ let intervalSeconds = 5;
74
+ let cycleTimer = null;
75
+
76
+ function cleanItems(value) {
77
+ if (!Array.isArray(value)) return [];
78
+ return value.map(item => ({
79
+ topic: String(item?.topic ?? "").trim(),
80
+ tagline: String(item?.tagline ?? "").trim()
81
+ })).filter(item => item.topic || item.tagline);
82
+ }
83
+
84
+ function setSlot(slot, text) {
85
+ clearTimeout(slot.timer);
86
+ slot.current.textContent = text;
87
+ slot.current.className = "line";
88
+ slot.next.textContent = "";
89
+ slot.next.className = "line waiting";
90
+ }
91
+
92
+ function animateSlot(slot, text) {
93
+ clearTimeout(slot.timer);
94
+ slot.next.textContent = text;
95
+ slot.next.className = "line waiting";
96
+ void slot.next.offsetHeight;
97
+ slot.current.className = "line leaving";
98
+ slot.next.className = "line";
99
+ slot.timer = setTimeout(() => {
100
+ const previous = slot.current;
101
+ slot.current = slot.next;
102
+ slot.next = previous;
103
+ slot.next.textContent = "";
104
+ slot.next.className = "line waiting";
105
+ }, TRANSITION_MS);
106
+ }
107
+
108
+ function showCurrent() {
109
+ const item = items[currentIndex];
110
+ ticker.classList.toggle("empty", !item);
111
+ if (!item) return;
112
+ setSlot(topicSlot, item.topic);
113
+ setSlot(headlineSlot, item.tagline);
114
+ }
115
+
116
+ function scheduleCycle() {
117
+ clearTimeout(cycleTimer);
118
+ if (items.length > 1) cycleTimer = setTimeout(nextItem, intervalSeconds * 1000);
119
+ }
120
+
121
+ function nextItem() {
122
+ if (items.length < 2) return;
123
+ const previous = items[currentIndex];
124
+ currentIndex = (currentIndex + 1) % items.length;
125
+ const next = items[currentIndex];
126
+ animateSlot(headlineSlot, next.tagline);
127
+ if (next.topic !== previous.topic) animateSlot(topicSlot, next.topic);
128
+ scheduleCycle();
129
+ }
130
+
131
+ function replaceItems(value) {
132
+ items = cleanItems(value);
133
+ currentIndex = 0;
134
+ showCurrent();
135
+ scheduleCycle();
136
+ }
137
+
138
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
139
+ if (detail.content?.brand != null) brand.textContent = detail.content.brand;
140
+ if (detail.style?.background) ticker.style.setProperty("--background", detail.style.background);
141
+ if (detail.timing?.interval != null) {
142
+ const interval = Number(detail.timing.interval);
143
+ if (Number.isFinite(interval)) intervalSeconds = Math.min(30, Math.max(2, interval));
144
+ scheduleCycle();
145
+ }
146
+ if (detail.news != null) replaceItems(detail.news);
147
+ });
148
+
149
+ showCurrent();
150
+ scheduleCycle();
151
+ </script>
152
+ </body>
153
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "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.",
3
+ "meta": {
4
+ "content/brand": {"type": "string", "phase": "edit", "label": "Brand text", "hint": "US OPEN", "default": "US OPEN"},
5
+ "news": {
6
+ "type": "set", "phase": "edit", "label": "News items", "hint": "Headlines shown in their playback order", "container": "big", "max": 30,
7
+ "default": [
8
+ {"topic": "MEN'S DOUBLES", "tagline": "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6-4 6–4 6–2"},
9
+ {"topic": "MEN'S DOUBLES", "tagline": "The quarter-final line-up is now complete in New York"},
10
+ {"topic": "WOMEN'S SINGLES", "tagline": "A dramatic final set sends the defending champion through"},
11
+ {"topic": "AROUND THE GROUNDS", "tagline": "Evening play begins under the lights at Arthur Ashe Stadium"}
12
+ ],
13
+ "meta": {
14
+ "topic": {"type": "string", "phase": "edit", "label": "News topic", "hint": "MEN'S DOUBLES"},
15
+ "tagline": {"type": "string", "phase": "edit", "label": "News tagline", "hint": "Enter headline"}
16
+ }
17
+ },
18
+ "timing/interval": {"type": "number", "phase": "edit", "label": "News interval", "hint": "Seconds between headlines", "default": 5, "min": 2, "max": 30},
19
+ "style/background": {"type": "color", "phase": "edit", "label": "Background", "hint": "Background colour used by the ticker", "default": "#0759a6"}
20
+ }
21
+ }