@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,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>
|
|
Binary file
|
|
@@ -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
|
+
}
|