@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,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
+ }
@@ -0,0 +1,229 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>News Ticker</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
+ --hue: 0deg;
21
+ color: #000;
22
+ background-image: url(background-label-green.png);
23
+ font-family: Inter, Arial, Helvetica, sans-serif;
24
+ background-size: 100% 100%;
25
+ backdrop-filter: hue-rotate(var(--hue));
26
+ }
27
+
28
+ .ticker {
29
+ background-image: url(banner.png);
30
+ position: absolute;
31
+ left: 8%;
32
+ right: 8%;
33
+ bottom: 8%;
34
+ height: 8.75rem;
35
+ display: flex;
36
+ grid-template-columns: minmax(15rem, 20%) minmax(0, 1fr) minmax(13rem, 17%);
37
+ overflow: hidden;
38
+ border-radius: .35rem;
39
+ box-shadow: 0 1.1rem 3rem #0007;
40
+ background-size: 100% 100%;
41
+ }
42
+
43
+ .zone {
44
+ min-width: 0;
45
+ position: relative;
46
+ display: flex;
47
+ align-items: center;
48
+ overflow: hidden
49
+ }
50
+
51
+ .topic {
52
+ justify-content: center;
53
+ padding: 0 1.6rem;
54
+ background: #0002
55
+ }
56
+
57
+ .topic {
58
+ border-right: 1px solid #fff5;
59
+ width: 17rem;
60
+ }
61
+
62
+ .viewport {
63
+ position: relative;
64
+ width: 100%;
65
+ height: 100%;
66
+ overflow: hidden
67
+ }
68
+
69
+ .line {
70
+ position: absolute;
71
+ inset: 0;
72
+ display: flex;
73
+ align-items: center;
74
+ min-width: 0;
75
+ transition: transform .58s cubic-bezier(.7, 0, .25, 1), opacity .58s ease;
76
+ will-change: transform, opacity
77
+ }
78
+
79
+ section.zone.headline {
80
+ flex-grow: 1;
81
+ }
82
+
83
+ .topic .line {
84
+ justify-content: center;
85
+ text-align: center;
86
+ font-size: 2.35rem;
87
+ line-height: 1.05;
88
+ font-weight: 900;
89
+ letter-spacing: .055em;
90
+ text-transform: uppercase
91
+ }
92
+
93
+ .headline .line {
94
+ padding: 0 2.4rem;
95
+ font-size: 2.75rem;
96
+ line-height: 1.08;
97
+ font-weight: 760;
98
+ letter-spacing: -.025em;
99
+ }
100
+
101
+ .line.waiting {
102
+ transform: translateY(110%);
103
+ opacity: 0
104
+ }
105
+
106
+ .line.leaving {
107
+ transform: translateY(-110%);
108
+ opacity: 0
109
+ }
110
+
111
+ .ticker.empty {
112
+ opacity: 0
113
+ }
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <main class="ticker" id="ticker">
118
+ <section class="zone topic">
119
+ <div class="viewport" id="topicSlot">
120
+ <div class="line">MEN'S DOUBLES</div>
121
+ <div class="line waiting"></div>
122
+ </div>
123
+ </section>
124
+ <section class="zone headline">
125
+ <div class="viewport" id="headlineSlot">
126
+ <div class="line">Carlos Alcaraz defeats Tommy Paul, 7–6 7–6 6–4</div>
127
+ <div class="line waiting"></div>
128
+ </div>
129
+ </section>
130
+ </main>
131
+ <script>
132
+ const defaults = [
133
+ {topic: "MEN'S DOUBLES", tagline: "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6–4 6–4 6–2"},
134
+ {topic: "MEN'S DOUBLES", tagline: "The quarter-final line-up is now complete in New York"},
135
+ {topic: "WOMEN'S SINGLES", tagline: "A dramatic final set sends the defending champion through"},
136
+ {topic: "AROUND THE GROUNDS", tagline: "Evening play begins under the lights at Arthur Ashe Stadium"}
137
+ ];
138
+ const TRANSITION_MS = 580;
139
+ const ticker = document.getElementById("ticker");
140
+
141
+ function makeSlot(id) {
142
+ const lines = document.getElementById(id).querySelectorAll(".line");
143
+ return {current: lines[0], next: lines[1], timer: null};
144
+ }
145
+
146
+ const topicSlot = makeSlot("topicSlot");
147
+ const headlineSlot = makeSlot("headlineSlot");
148
+ let items = defaults;
149
+ let currentIndex = 0;
150
+ let intervalSeconds = 5;
151
+ let cycleTimer = null;
152
+
153
+ function cleanItems(value) {
154
+ if (!Array.isArray(value)) return [];
155
+ return value.map(item => ({
156
+ topic: String(item?.topic ?? "").trim(),
157
+ tagline: String(item?.tagline ?? "").trim()
158
+ })).filter(item => item.topic || item.tagline);
159
+ }
160
+
161
+ function setSlot(slot, text) {
162
+ clearTimeout(slot.timer);
163
+ slot.current.textContent = text;
164
+ slot.current.className = "line";
165
+ slot.next.textContent = "";
166
+ slot.next.className = "line waiting";
167
+ }
168
+
169
+ function animateSlot(slot, text) {
170
+ clearTimeout(slot.timer);
171
+ slot.next.textContent = text;
172
+ slot.next.className = "line waiting";
173
+ void slot.next.offsetHeight;
174
+ slot.current.className = "line leaving";
175
+ slot.next.className = "line";
176
+ slot.timer = setTimeout(() => {
177
+ const previous = slot.current;
178
+ slot.current = slot.next;
179
+ slot.next = previous;
180
+ slot.next.textContent = "";
181
+ slot.next.className = "line waiting";
182
+ }, TRANSITION_MS);
183
+ }
184
+
185
+ function showCurrent() {
186
+ const item = items[currentIndex];
187
+ ticker.classList.toggle("empty", !item);
188
+ if (!item) return;
189
+ setSlot(topicSlot, item.topic);
190
+ setSlot(headlineSlot, item.tagline);
191
+ }
192
+
193
+ function scheduleCycle() {
194
+ clearTimeout(cycleTimer);
195
+ if (items.length > 1) cycleTimer = setTimeout(nextItem, intervalSeconds * 1000);
196
+ }
197
+
198
+ function nextItem() {
199
+ if (items.length < 2) return;
200
+ const previous = items[currentIndex];
201
+ currentIndex = (currentIndex + 1) % items.length;
202
+ const next = items[currentIndex];
203
+ animateSlot(headlineSlot, next.tagline);
204
+ if (next.topic !== previous.topic) animateSlot(topicSlot, next.topic);
205
+ scheduleCycle();
206
+ }
207
+
208
+ function replaceItems(value) {
209
+ items = cleanItems(value);
210
+ currentIndex = 0;
211
+ showCurrent();
212
+ scheduleCycle();
213
+ }
214
+
215
+ window.addEventListener("nebular-overlay:update", ({detail}) => {
216
+ if (detail.timing?.interval != null) {
217
+ const interval = Number(detail.timing.interval);
218
+ if (Number.isFinite(interval)) intervalSeconds = Math.min(30, Math.max(2, interval));
219
+ scheduleCycle();
220
+ }
221
+ if (detail.news != null) replaceItems(detail.news);
222
+ if (detail.style?.hue > 0) document.body.style.setProperty("--hue", detail.style.hue + "deg");
223
+ });
224
+
225
+ showCurrent();
226
+ scheduleCycle();
227
+ </script>
228
+ </body>
229
+ </html>
@@ -0,0 +1,20 @@
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
+ "news": {
5
+ "type": "set", "phase": "edit", "label": "News items", "hint": "Headlines shown in their playback order", "container": "big", "max": 30,
6
+ "default": [
7
+ {"topic": "MEN'S DOUBLES", "tagline": "Carlos Alcaraz defeats Tommy Paul in memorable performance, 6-4 6–4 6–2"},
8
+ {"topic": "MEN'S DOUBLES", "tagline": "The quarter-final line-up is now complete in New York"},
9
+ {"topic": "WOMEN'S SINGLES", "tagline": "A dramatic final set sends the defending champion through"},
10
+ {"topic": "AROUND THE GROUNDS", "tagline": "Evening play begins under the lights at Arthur Ashe Stadium"}
11
+ ],
12
+ "meta": {
13
+ "topic": {"type": "string", "phase": "edit", "label": "News topic", "hint": "MEN'S DOUBLES"},
14
+ "tagline": {"type": "string", "phase": "edit", "label": "News tagline", "hint": "Enter headline"}
15
+ }
16
+ },
17
+ "timing/interval": {"type": "number", "phase": "edit", "label": "News interval", "hint": "Seconds between headlines", "default": 5, "min": 2, "max": 30},
18
+ "style/hue": {"type": "range", "phase": "edit", "label": "Color Theme", "min": 0, "max": 360, "hint": "General theme color used in the framer", "default": 0}
19
+ }
20
+ }