@livepeer-frameworks/player-wc 0.1.2 → 0.1.3

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 (116) hide show
  1. package/dist/cjs/components/fw-dev-mode-panel.js +845 -212
  2. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
  3. package/dist/cjs/components/fw-dvd-logo.js +211 -0
  4. package/dist/cjs/components/fw-dvd-logo.js.map +1 -0
  5. package/dist/cjs/components/fw-idle-screen.js +641 -97
  6. package/dist/cjs/components/fw-idle-screen.js.map +1 -1
  7. package/dist/cjs/components/fw-loading-screen.js +513 -0
  8. package/dist/cjs/components/fw-loading-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-player-controls.js +347 -173
  10. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  11. package/dist/cjs/components/fw-player.js +460 -60
  12. package/dist/cjs/components/fw-player.js.map +1 -1
  13. package/dist/cjs/components/fw-seek-bar.js +292 -142
  14. package/dist/cjs/components/fw-seek-bar.js.map +1 -1
  15. package/dist/cjs/components/fw-settings-menu.js +191 -81
  16. package/dist/cjs/components/fw-settings-menu.js.map +1 -1
  17. package/dist/cjs/components/fw-stats-panel.js +134 -70
  18. package/dist/cjs/components/fw-stats-panel.js.map +1 -1
  19. package/dist/cjs/components/fw-stream-state-overlay.js +338 -0
  20. package/dist/cjs/components/fw-stream-state-overlay.js.map +1 -0
  21. package/dist/cjs/components/fw-subtitle-renderer.js +174 -27
  22. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -1
  23. package/dist/cjs/components/fw-thumbnail-overlay.js +161 -0
  24. package/dist/cjs/components/fw-thumbnail-overlay.js.map +1 -0
  25. package/dist/cjs/components/fw-volume-control.js +150 -69
  26. package/dist/cjs/components/fw-volume-control.js.map +1 -1
  27. package/dist/cjs/components/shared/hitmarker-audio.js +76 -0
  28. package/dist/cjs/components/shared/hitmarker-audio.js.map +1 -0
  29. package/dist/cjs/constants/media-assets.js +11 -0
  30. package/dist/cjs/constants/media-assets.js.map +1 -0
  31. package/dist/cjs/controllers/player-controller-host.js +28 -1
  32. package/dist/cjs/controllers/player-controller-host.js.map +1 -1
  33. package/dist/cjs/define.js +8 -0
  34. package/dist/cjs/define.js.map +1 -1
  35. package/dist/cjs/icons/index.js +27 -0
  36. package/dist/cjs/icons/index.js.map +1 -1
  37. package/dist/cjs/index.js +20 -0
  38. package/dist/cjs/index.js.map +1 -1
  39. package/dist/esm/components/fw-dev-mode-panel.js +846 -213
  40. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  41. package/dist/esm/components/fw-dvd-logo.js +211 -0
  42. package/dist/esm/components/fw-dvd-logo.js.map +1 -0
  43. package/dist/esm/components/fw-idle-screen.js +643 -99
  44. package/dist/esm/components/fw-idle-screen.js.map +1 -1
  45. package/dist/esm/components/fw-loading-screen.js +513 -0
  46. package/dist/esm/components/fw-loading-screen.js.map +1 -0
  47. package/dist/esm/components/fw-player-controls.js +348 -174
  48. package/dist/esm/components/fw-player-controls.js.map +1 -1
  49. package/dist/esm/components/fw-player.js +460 -60
  50. package/dist/esm/components/fw-player.js.map +1 -1
  51. package/dist/esm/components/fw-seek-bar.js +293 -143
  52. package/dist/esm/components/fw-seek-bar.js.map +1 -1
  53. package/dist/esm/components/fw-settings-menu.js +192 -82
  54. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  55. package/dist/esm/components/fw-stats-panel.js +135 -71
  56. package/dist/esm/components/fw-stats-panel.js.map +1 -1
  57. package/dist/esm/components/fw-stream-state-overlay.js +338 -0
  58. package/dist/esm/components/fw-stream-state-overlay.js.map +1 -0
  59. package/dist/esm/components/fw-subtitle-renderer.js +175 -28
  60. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -1
  61. package/dist/esm/components/fw-thumbnail-overlay.js +161 -0
  62. package/dist/esm/components/fw-thumbnail-overlay.js.map +1 -0
  63. package/dist/esm/components/fw-volume-control.js +150 -69
  64. package/dist/esm/components/fw-volume-control.js.map +1 -1
  65. package/dist/esm/components/shared/hitmarker-audio.js +74 -0
  66. package/dist/esm/components/shared/hitmarker-audio.js.map +1 -0
  67. package/dist/esm/constants/media-assets.js +8 -0
  68. package/dist/esm/constants/media-assets.js.map +1 -0
  69. package/dist/esm/controllers/player-controller-host.js +28 -1
  70. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  71. package/dist/esm/define.js +8 -0
  72. package/dist/esm/define.js.map +1 -1
  73. package/dist/esm/icons/index.js +26 -2
  74. package/dist/esm/icons/index.js.map +1 -1
  75. package/dist/esm/index.js +4 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/fw-player.iife.js +2072 -880
  78. package/dist/types/components/fw-dev-mode-panel.d.ts +36 -9
  79. package/dist/types/components/fw-dvd-logo.d.ts +29 -0
  80. package/dist/types/components/fw-idle-screen.d.ts +36 -0
  81. package/dist/types/components/fw-loading-screen.d.ts +36 -0
  82. package/dist/types/components/fw-player-controls.d.ts +21 -6
  83. package/dist/types/components/fw-player.d.ts +28 -1
  84. package/dist/types/components/fw-seek-bar.d.ts +31 -14
  85. package/dist/types/components/fw-settings-menu.d.ts +15 -1
  86. package/dist/types/components/fw-stats-panel.d.ts +4 -4
  87. package/dist/types/components/fw-stream-state-overlay.d.ts +20 -0
  88. package/dist/types/components/fw-subtitle-renderer.d.ts +33 -2
  89. package/dist/types/components/fw-thumbnail-overlay.d.ts +17 -0
  90. package/dist/types/components/fw-volume-control.d.ts +11 -4
  91. package/dist/types/components/shared/hitmarker-audio.d.ts +1 -0
  92. package/dist/types/constants/media-assets.d.ts +5 -0
  93. package/dist/types/controllers/player-controller-host.d.ts +14 -1
  94. package/dist/types/iife-entry.d.ts +4 -0
  95. package/dist/types/index.d.ts +4 -0
  96. package/package.json +2 -2
  97. package/src/components/fw-dev-mode-panel.ts +929 -228
  98. package/src/components/fw-dvd-logo.ts +233 -0
  99. package/src/components/fw-idle-screen.ts +680 -100
  100. package/src/components/fw-loading-screen.ts +540 -0
  101. package/src/components/fw-player-controls.ts +435 -176
  102. package/src/components/fw-player.ts +505 -57
  103. package/src/components/fw-seek-bar.ts +336 -143
  104. package/src/components/fw-settings-menu.ts +208 -85
  105. package/src/components/fw-stats-panel.ts +150 -77
  106. package/src/components/fw-stream-state-overlay.ts +331 -0
  107. package/src/components/fw-subtitle-renderer.ts +216 -28
  108. package/src/components/fw-thumbnail-overlay.ts +148 -0
  109. package/src/components/fw-volume-control.ts +166 -66
  110. package/src/components/shared/hitmarker-audio.ts +92 -0
  111. package/src/constants/media-assets.ts +7 -0
  112. package/src/controllers/player-controller-host.ts +29 -2
  113. package/src/define.ts +8 -0
  114. package/src/iife-entry.ts +4 -0
  115. package/src/index.ts +4 -0
  116. package/dist/fw-player.iife.js.map +0 -1
@@ -1,42 +1,388 @@
1
1
  import { __decorate } from '../node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js';
2
- import { css, LitElement, nothing, html } from 'lit';
3
- import { property, customElement } from 'lit/decorators.js';
2
+ import { css, LitElement, html, nothing } from 'lit';
3
+ import { property, query, state, customElement } from 'lit/decorators.js';
4
4
  import { sharedStyles } from '../styles/shared-styles.js';
5
5
  import { utilityStyles } from '../styles/utility-styles.js';
6
+ import { LOGOMARK_DATA_URL } from '../constants/media-assets.js';
7
+ import { playHitmarkerSound } from './shared/hitmarker-audio.js';
8
+ import './fw-dvd-logo.js';
6
9
 
10
+ const BUBBLE_COLORS = [
11
+ "rgba(122, 162, 247, 0.2)",
12
+ "rgba(187, 154, 247, 0.2)",
13
+ "rgba(158, 206, 106, 0.2)",
14
+ "rgba(115, 218, 202, 0.2)",
15
+ "rgba(125, 207, 255, 0.2)",
16
+ "rgba(247, 118, 142, 0.2)",
17
+ "rgba(224, 175, 104, 0.2)",
18
+ "rgba(42, 195, 222, 0.2)",
19
+ ];
20
+ const PARTICLE_COLORS = [
21
+ "#7aa2f7",
22
+ "#bb9af7",
23
+ "#9ece6a",
24
+ "#73daca",
25
+ "#7dcfff",
26
+ "#f7768e",
27
+ "#e0af68",
28
+ "#2ac3de",
29
+ ];
7
30
  let FwIdleScreen = class FwIdleScreen extends LitElement {
31
+ constructor() {
32
+ super(...arguments);
33
+ this.retryEnabled = false;
34
+ this._logoSize = 100;
35
+ this._logoOffset = { x: 0, y: 0 };
36
+ this._isLogoHovered = false;
37
+ this._bubbles = this._createBubbles();
38
+ this._hitmarkers = [];
39
+ this._particles = this._createParticles();
40
+ this._bubbleTimers = new Set();
41
+ this._handleMouseMove = (event) => {
42
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
43
+ if (rect.width === 0 || rect.height === 0) {
44
+ return;
45
+ }
46
+ const centerX = rect.left + rect.width / 2;
47
+ const centerY = rect.top + rect.height / 2;
48
+ const deltaX = event.clientX - centerX;
49
+ const deltaY = event.clientY - centerY;
50
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
51
+ const maxDistance = this._logoSize * 1.5;
52
+ if (distance < maxDistance && distance > 0) {
53
+ const pushStrength = (maxDistance - distance) / maxDistance;
54
+ const pushDistance = 50 * pushStrength;
55
+ this._logoOffset = {
56
+ x: -(deltaX / distance) * pushDistance,
57
+ y: -(deltaY / distance) * pushDistance,
58
+ };
59
+ this._isLogoHovered = true;
60
+ return;
61
+ }
62
+ this._logoOffset = { x: 0, y: 0 };
63
+ this._isLogoHovered = false;
64
+ };
65
+ this._handleMouseLeave = () => {
66
+ this._logoOffset = { x: 0, y: 0 };
67
+ this._isLogoHovered = false;
68
+ };
69
+ this._handleLogoClick = (event) => {
70
+ event.stopPropagation();
71
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
72
+ const hitmarker = {
73
+ id: Date.now() + Math.random(),
74
+ x: event.clientX - rect.left,
75
+ y: event.clientY - rect.top,
76
+ };
77
+ this._hitmarkers = [...this._hitmarkers, hitmarker];
78
+ playHitmarkerSound();
79
+ this._setManagedTimer(() => {
80
+ this._hitmarkers = this._hitmarkers.filter((h) => h.id !== hitmarker.id);
81
+ }, 600);
82
+ };
83
+ this._handleRetry = () => {
84
+ if (this.onRetry) {
85
+ this.onRetry();
86
+ return;
87
+ }
88
+ this.dispatchEvent(new CustomEvent("fw-retry", {
89
+ bubbles: true,
90
+ composed: true,
91
+ }));
92
+ };
93
+ }
94
+ connectedCallback() {
95
+ super.connectedCallback();
96
+ this._clearBubbleTimers();
97
+ this._startBubbleAnimations();
98
+ }
99
+ disconnectedCallback() {
100
+ super.disconnectedCallback();
101
+ this._clearBubbleTimers();
102
+ this._resizeObserver?.disconnect();
103
+ this._resizeObserver = undefined;
104
+ }
105
+ firstUpdated() {
106
+ this._updateLogoSize();
107
+ if (typeof ResizeObserver !== "undefined") {
108
+ this._resizeObserver = new ResizeObserver(() => {
109
+ this._updateLogoSize();
110
+ });
111
+ if (this._containerEl) {
112
+ this._resizeObserver.observe(this._containerEl);
113
+ }
114
+ }
115
+ }
116
+ _createParticles() {
117
+ return Array.from({ length: 12 }, (_, i) => ({
118
+ left: Math.random() * 100,
119
+ size: Math.random() * 4 + 2,
120
+ color: PARTICLE_COLORS[i % PARTICLE_COLORS.length],
121
+ duration: 8 + Math.random() * 4,
122
+ delay: Math.random() * 8,
123
+ }));
124
+ }
125
+ _createBubbles() {
126
+ return Array.from({ length: 8 }, (_, i) => ({
127
+ top: Math.random() * 80 + 10,
128
+ left: Math.random() * 80 + 10,
129
+ size: Math.random() * 60 + 30,
130
+ opacity: 0,
131
+ color: BUBBLE_COLORS[i % BUBBLE_COLORS.length],
132
+ }));
133
+ }
134
+ _setManagedTimer(callback, delayMs) {
135
+ const timer = setTimeout(() => {
136
+ this._bubbleTimers.delete(timer);
137
+ callback();
138
+ }, delayMs);
139
+ this._bubbleTimers.add(timer);
140
+ }
141
+ _clearBubbleTimers() {
142
+ this._bubbleTimers.forEach((timer) => clearTimeout(timer));
143
+ this._bubbleTimers.clear();
144
+ }
145
+ _updateBubble(index, nextState) {
146
+ if (index < 0 || index >= this._bubbles.length) {
147
+ return;
148
+ }
149
+ const next = [...this._bubbles];
150
+ next[index] = { ...next[index], ...nextState };
151
+ this._bubbles = next;
152
+ }
153
+ _animateBubble(index) {
154
+ this._updateBubble(index, { opacity: 0.15 });
155
+ const visibleDuration = 4000 + Math.random() * 3000;
156
+ this._setManagedTimer(() => {
157
+ this._updateBubble(index, { opacity: 0 });
158
+ this._setManagedTimer(() => {
159
+ this._updateBubble(index, {
160
+ top: Math.random() * 80 + 10,
161
+ left: Math.random() * 80 + 10,
162
+ size: Math.random() * 60 + 30,
163
+ });
164
+ this._setManagedTimer(() => this._animateBubble(index), 200);
165
+ }, 1500);
166
+ }, visibleDuration);
167
+ }
168
+ _startBubbleAnimations() {
169
+ this._bubbles.forEach((_, index) => {
170
+ this._setManagedTimer(() => this._animateBubble(index), index * 500);
171
+ });
172
+ }
173
+ _updateLogoSize() {
174
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
175
+ const minDimension = Math.min(rect.width, rect.height);
176
+ if (!Number.isFinite(minDimension) || minDimension <= 0) {
177
+ return;
178
+ }
179
+ this._logoSize = minDimension * 0.2;
180
+ }
181
+ get _isLoading() {
182
+ return (this.status === "INITIALIZING" ||
183
+ this.status === "BOOTING" ||
184
+ this.status === "WAITING_FOR_DATA" ||
185
+ !this.status);
186
+ }
187
+ get _isOffline() {
188
+ return this.status === "OFFLINE";
189
+ }
190
+ get _isError() {
191
+ return this.status === "ERROR" || this.status === "INVALID";
192
+ }
193
+ get _showProgress() {
194
+ return this.status === "INITIALIZING" && this.percentage != null;
195
+ }
196
+ get _showRetry() {
197
+ return this._isError && (this.retryEnabled || typeof this.onRetry === "function");
198
+ }
199
+ get _displayMessage() {
200
+ return this.error || this.message || "Waiting for stream...";
201
+ }
202
+ _renderStatusIcon() {
203
+ if (this._isLoading) {
204
+ return html `
205
+ <svg
206
+ class="status-icon spinning"
207
+ fill="none"
208
+ viewBox="0 0 24 24"
209
+ style="color: hsl(var(--tn-yellow, 40 95% 64%));"
210
+ >
211
+ <circle
212
+ style="opacity: 0.25;"
213
+ cx="12"
214
+ cy="12"
215
+ r="10"
216
+ stroke="currentColor"
217
+ stroke-width="4"
218
+ />
219
+ <path
220
+ style="opacity: 0.75;"
221
+ fill="currentColor"
222
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
223
+ ></path>
224
+ </svg>
225
+ `;
226
+ }
227
+ if (this._isOffline) {
228
+ return html `
229
+ <svg
230
+ class="status-icon"
231
+ fill="none"
232
+ viewBox="0 0 24 24"
233
+ stroke="currentColor"
234
+ style="color: hsl(var(--tn-red, 348 100% 72%));"
235
+ >
236
+ <path
237
+ stroke-linecap="round"
238
+ stroke-linejoin="round"
239
+ stroke-width="2"
240
+ d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
241
+ ></path>
242
+ </svg>
243
+ `;
244
+ }
245
+ if (this._isError) {
246
+ return html `
247
+ <svg
248
+ class="status-icon"
249
+ fill="none"
250
+ viewBox="0 0 24 24"
251
+ stroke="currentColor"
252
+ style="color: hsl(var(--tn-red, 348 100% 72%));"
253
+ >
254
+ <path
255
+ stroke-linecap="round"
256
+ stroke-linejoin="round"
257
+ stroke-width="2"
258
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
259
+ ></path>
260
+ </svg>
261
+ `;
262
+ }
263
+ return html `
264
+ <svg
265
+ class="status-icon spinning"
266
+ fill="none"
267
+ viewBox="0 0 24 24"
268
+ style="color: hsl(var(--tn-cyan, 193 100% 75%));"
269
+ >
270
+ <circle
271
+ style="opacity: 0.25;"
272
+ cx="12"
273
+ cy="12"
274
+ r="10"
275
+ stroke="currentColor"
276
+ stroke-width="4"
277
+ />
278
+ <path
279
+ style="opacity: 0.75;"
280
+ fill="currentColor"
281
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
282
+ ></path>
283
+ </svg>
284
+ `;
285
+ }
8
286
  render() {
9
- const isOffline = this.status === "OFFLINE";
10
- const isInitializing = this.status === "INITIALIZING" || this.status === "STARTING";
287
+ const progress = Math.min(100, Math.max(0, this.percentage ?? 0));
288
+ const logoSrc = this.logoSrc || LOGOMARK_DATA_URL;
11
289
  return html `
12
- <div class="idle">
290
+ <div
291
+ class="idle-container fw-player-root"
292
+ role="status"
293
+ aria-label="Stream status"
294
+ @mousemove=${this._handleMouseMove}
295
+ @mouseleave=${this._handleMouseLeave}
296
+ >
297
+ ${this._hitmarkers.map((hitmarker) => html `
298
+ <div class="hitmarker" style="left: ${hitmarker.x}px; top: ${hitmarker.y}px;">
299
+ <div class="hitmarker-line tl"></div>
300
+ <div class="hitmarker-line tr"></div>
301
+ <div class="hitmarker-line bl"></div>
302
+ <div class="hitmarker-line br"></div>
303
+ </div>
304
+ `)}
305
+
13
306
  <div class="particles">
14
- ${[0, 1, 2, 3, 4, 5, 6, 7].map((i) => html `
307
+ ${this._particles.map((particle) => html `
15
308
  <div
16
309
  class="particle"
17
- style="left: ${10 + i * 11}%; top: ${20 + (i % 3) * 25}%; --dur: ${6 +
18
- i * 0.8}s; background: hsl(${217 + i * 15} 70% 60%); animation-delay: ${i * -1}s;"
310
+ style="
311
+ left: ${particle.left}%;
312
+ width: ${particle.size}px;
313
+ height: ${particle.size}px;
314
+ background: ${particle.color};
315
+ animation-duration: ${particle.duration}s;
316
+ animation-delay: ${particle.delay}s;
317
+ "
318
+ ></div>
319
+ `)}
320
+ </div>
321
+
322
+ <div class="bubbles">
323
+ ${this._bubbles.map((bubble) => html `
324
+ <div
325
+ class="bubble"
326
+ style="
327
+ top: ${bubble.top}%;
328
+ left: ${bubble.left}%;
329
+ width: ${bubble.size}px;
330
+ height: ${bubble.size}px;
331
+ background: ${bubble.color};
332
+ opacity: ${bubble.opacity};
333
+ "
19
334
  ></div>
20
335
  `)}
21
336
  </div>
22
- <div class="card">
23
- <div class="status-icon">
24
- ${isOffline ? html `<div class="offline-dot"></div>` : html `<div class="spinner"></div>`}
337
+
338
+ <div
339
+ class="center-logo"
340
+ style="transform: translate(-50%, -50%) translate(${this._logoOffset.x}px, ${this
341
+ ._logoOffset.y}px);"
342
+ >
343
+ <div
344
+ class="logo-pulse ${this._isLogoHovered ? "hovered" : ""}"
345
+ style="width: ${this._logoSize * 1.4}px; height: ${this._logoSize * 1.4}px;"
346
+ ></div>
347
+ <button
348
+ type="button"
349
+ class="logo-button"
350
+ @click=${this._handleLogoClick}
351
+ aria-label="FrameWorks logo"
352
+ >
353
+ <img
354
+ src=${logoSrc}
355
+ alt=""
356
+ class="logo-image ${this._isLogoHovered ? "hovered" : ""}"
357
+ style="width: ${this._logoSize}px; height: ${this._logoSize}px;"
358
+ draggable="false"
359
+ />
360
+ </button>
361
+ </div>
362
+
363
+ <fw-dvd-logo .parentRef=${this._containerEl ?? null} .scale=${0.08}></fw-dvd-logo>
364
+
365
+ <div class="status-overlay">
366
+ <div class="status-indicator">
367
+ ${this._renderStatusIcon()}
368
+ <span>${this._displayMessage}</span>
25
369
  </div>
26
- ${this.status ? html `<div class="label">${this.status}</div>` : nothing}
27
- ${this.message ? html `<div class="msg">${this.message}</div>` : nothing}
28
- ${isInitializing && this.percentage != null
370
+
371
+ ${this._showProgress
29
372
  ? html `
30
- <div class="progress-wrap">
31
- <div
32
- class="progress-bar"
33
- style="width: ${Math.min(100, Math.max(0, this.percentage))}%"
34
- ></div>
373
+ <div class="progress-bar">
374
+ <div class="progress-fill" style="width: ${progress}%;"></div>
35
375
  </div>
36
- <div class="pct">${Math.round(this.percentage)}%</div>
37
376
  `
38
377
  : nothing}
378
+ ${this._showRetry
379
+ ? html `<button type="button" class="retry-btn" @click=${this._handleRetry}>
380
+ Retry
381
+ </button>`
382
+ : nothing}
39
383
  </div>
384
+
385
+ <div class="overlay-texture"></div>
40
386
  </div>
41
387
  `;
42
388
  }
@@ -48,119 +394,287 @@ FwIdleScreen.styles = [
48
394
  :host {
49
395
  display: contents;
50
396
  }
51
- .idle {
397
+ .idle-container {
52
398
  position: absolute;
53
399
  inset: 0;
400
+ z-index: 5;
401
+ background: linear-gradient(
402
+ 135deg,
403
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
404
+ hsl(var(--tn-bg, 233 23% 17%)) 25%,
405
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 50%,
406
+ hsl(var(--tn-bg, 233 23% 17%)) 75%,
407
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
408
+ );
409
+ background-size: 400% 400%;
410
+ animation: _fw-gradient-shift 16s ease-in-out infinite;
54
411
  display: flex;
412
+ flex-direction: column;
55
413
  align-items: center;
56
414
  justify-content: center;
57
- z-index: 15;
58
- background: linear-gradient(135deg, rgb(15 23 42), rgb(2 6 23), rgb(15 23 42));
59
415
  overflow: hidden;
416
+ user-select: none;
417
+ -webkit-user-select: none;
60
418
  }
61
- .card {
62
- position: relative;
63
- z-index: 10;
64
- max-width: 280px;
65
- width: 100%;
66
- text-align: center;
419
+
420
+ .particles,
421
+ .bubbles {
422
+ position: absolute;
423
+ inset: 0;
424
+ pointer-events: none;
67
425
  }
68
- .status-icon {
69
- margin: 0 auto 0.75rem;
70
- width: 2.5rem;
71
- height: 2.5rem;
426
+
427
+ .particle {
428
+ position: absolute;
429
+ border-radius: 50%;
430
+ opacity: 0;
431
+ animation: _fw-float-up linear infinite;
432
+ }
433
+
434
+ .bubble {
435
+ position: absolute;
436
+ border-radius: 50%;
437
+ transition: opacity 1s ease-in-out;
438
+ }
439
+
440
+ .center-logo {
441
+ position: absolute;
442
+ top: 50%;
443
+ left: 50%;
72
444
  display: flex;
73
445
  align-items: center;
74
446
  justify-content: center;
447
+ z-index: 10;
448
+ transition: transform 0.3s ease-out;
75
449
  }
76
- .spinner {
77
- width: 1.5rem;
78
- height: 1.5rem;
79
- border: 2px solid rgb(255 255 255 / 0.2);
80
- border-top-color: hsl(var(--tn-blue, 217 89% 61%));
450
+
451
+ .logo-pulse {
452
+ position: absolute;
81
453
  border-radius: 50%;
82
- animation: _fw-spin 1s linear infinite;
454
+ background: rgba(122, 162, 247, 0.15);
455
+ animation: _fw-logo-pulse 3s ease-in-out infinite;
456
+ pointer-events: none;
457
+ transition: transform 0.3s ease-out;
83
458
  }
84
- @keyframes _fw-spin {
85
- to {
86
- transform: rotate(360deg);
87
- }
459
+
460
+ .logo-pulse.hovered {
461
+ animation: _fw-logo-pulse 1s ease-in-out infinite;
462
+ transform: scale(1.2);
88
463
  }
89
- .offline-dot {
90
- width: 0.75rem;
91
- height: 0.75rem;
92
- border-radius: 50%;
93
- background: hsl(var(--tn-red, 348 74% 64%));
94
- animation: _fw-blink 2s ease-in-out infinite;
464
+
465
+ .logo-button {
466
+ all: unset;
467
+ cursor: pointer;
468
+ display: block;
95
469
  }
96
- @keyframes _fw-blink {
97
- 0%,
98
- 100% {
99
- opacity: 1;
100
- }
101
- 50% {
102
- opacity: 0.3;
103
- }
104
- }
105
- .label {
106
- font-size: 0.6875rem;
107
- font-weight: 600;
108
- text-transform: uppercase;
109
- letter-spacing: 0.05em;
110
- color: rgb(255 255 255 / 0.5);
111
- margin-bottom: 0.5rem;
112
- }
113
- .msg {
114
- font-size: 0.8125rem;
115
- color: rgb(255 255 255 / 0.7);
116
- margin-bottom: 0.75rem;
117
- }
118
- .progress-wrap {
119
- width: 100%;
120
- height: 0.25rem;
121
- background: rgb(255 255 255 / 0.1);
470
+
471
+ .logo-image {
472
+ position: relative;
473
+ z-index: 1;
474
+ display: block;
475
+ filter: drop-shadow(0 4px 8px rgb(36 40 59 / 0.3));
476
+ transition: all 0.3s ease-out;
477
+ cursor: default;
478
+ user-select: none;
479
+ -webkit-user-drag: none;
480
+ }
481
+
482
+ .logo-image.hovered {
483
+ transform: scale(1.1);
484
+ filter: drop-shadow(0 6px 12px rgb(36 40 59 / 0.4)) brightness(1.1);
485
+ }
486
+
487
+ .status-overlay {
488
+ position: absolute;
489
+ bottom: 16px;
490
+ left: 50%;
491
+ transform: translateX(-50%);
492
+ z-index: 20;
493
+ display: flex;
494
+ flex-direction: column;
495
+ align-items: center;
496
+ gap: 8px;
497
+ max-width: 280px;
498
+ text-align: center;
499
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
500
+ }
501
+
502
+ .status-indicator {
503
+ display: flex;
504
+ align-items: center;
505
+ gap: 8px;
506
+ color: #787c99;
507
+ font-size: 13px;
508
+ }
509
+
510
+ .status-icon {
511
+ width: 20px;
512
+ height: 20px;
513
+ flex: 0 0 auto;
514
+ }
515
+
516
+ .status-icon.spinning {
517
+ animation: _fw-spin 1s linear infinite;
518
+ }
519
+
520
+ .progress-bar {
521
+ width: 160px;
522
+ height: 4px;
523
+ background: rgb(65 72 104 / 0.4);
122
524
  border-radius: 2px;
123
525
  overflow: hidden;
124
- margin-bottom: 0.5rem;
125
526
  }
126
- .progress-bar {
527
+
528
+ .progress-fill {
127
529
  height: 100%;
128
- background: hsl(var(--tn-blue, 217 89% 61%));
129
- border-radius: 2px;
130
- transition: width 300ms ease;
530
+ background: hsl(var(--tn-cyan, 193 100% 75%));
531
+ transition: width 0.3s ease-out;
131
532
  }
132
- .pct {
133
- font-size: 0.6875rem;
134
- color: rgb(255 255 255 / 0.4);
135
- font-variant-numeric: tabular-nums;
533
+
534
+ .retry-btn {
535
+ padding: 6px 16px;
536
+ background: transparent;
537
+ border: 1px solid rgb(122 162 247 / 0.4);
538
+ border-radius: 4px;
539
+ color: #7aa2f7;
540
+ font-size: 11px;
541
+ font-weight: 500;
542
+ cursor: pointer;
543
+ transition: all 0.2s ease;
544
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
545
+ }
546
+
547
+ .retry-btn:hover {
548
+ background: rgb(122 162 247 / 0.1);
136
549
  }
137
- .particles {
550
+
551
+ .overlay-texture {
138
552
  position: absolute;
139
553
  inset: 0;
140
- overflow: hidden;
554
+ background:
555
+ radial-gradient(circle at 20% 80%, rgb(122 162 247 / 0.03) 0%, transparent 50%),
556
+ radial-gradient(circle at 80% 20%, rgb(187 154 247 / 0.03) 0%, transparent 50%),
557
+ radial-gradient(circle at 40% 40%, rgb(158 206 106 / 0.02) 0%, transparent 50%);
141
558
  pointer-events: none;
142
559
  }
143
- .particle {
560
+
561
+ .hitmarker {
144
562
  position: absolute;
145
- width: 4px;
146
- height: 4px;
147
- border-radius: 50%;
148
- opacity: 0.3;
149
- animation: _fw-float var(--dur, 8s) ease-in-out infinite;
563
+ transform: translate(-50%, -50%);
564
+ pointer-events: none;
565
+ z-index: 100;
566
+ width: 40px;
567
+ height: 40px;
568
+ }
569
+
570
+ .hitmarker-line {
571
+ position: absolute;
572
+ width: 12px;
573
+ height: 3px;
574
+ background-color: #fff;
575
+ box-shadow: 0 0 8px rgb(255 255 255 / 0.8);
576
+ border-radius: 1px;
577
+ }
578
+
579
+ .hitmarker-line.tl {
580
+ top: 25%;
581
+ left: 25%;
582
+ animation: _fw-hitmarker-fade-45 0.6s ease-out forwards;
583
+ }
584
+
585
+ .hitmarker-line.tr {
586
+ top: 25%;
587
+ left: 75%;
588
+ animation: _fw-hitmarker-fade-neg-45 0.6s ease-out forwards;
150
589
  }
151
- @keyframes _fw-float {
590
+
591
+ .hitmarker-line.bl {
592
+ top: 75%;
593
+ left: 25%;
594
+ animation: _fw-hitmarker-fade-neg-45 0.6s ease-out forwards;
595
+ }
596
+
597
+ .hitmarker-line.br {
598
+ top: 75%;
599
+ left: 75%;
600
+ animation: _fw-hitmarker-fade-45 0.6s ease-out forwards;
601
+ }
602
+
603
+ @keyframes _fw-spin {
604
+ from {
605
+ transform: rotate(0deg);
606
+ }
607
+ to {
608
+ transform: rotate(360deg);
609
+ }
610
+ }
611
+
612
+ @keyframes _fw-logo-pulse {
152
613
  0%,
153
614
  100% {
154
- transform: translateY(0) translateX(0);
615
+ opacity: 0.15;
616
+ transform: scale(1);
617
+ }
618
+ 50% {
619
+ opacity: 0.25;
620
+ transform: scale(1.05);
621
+ }
622
+ }
623
+
624
+ @keyframes _fw-float-up {
625
+ 0% {
626
+ transform: translateY(100vh) rotate(0deg);
627
+ opacity: 0;
628
+ }
629
+ 10% {
630
+ opacity: 0.6;
631
+ }
632
+ 90% {
633
+ opacity: 0.6;
634
+ }
635
+ 100% {
636
+ transform: translateY(-100px) rotate(360deg);
637
+ opacity: 0;
155
638
  }
156
- 25% {
157
- transform: translateY(-20px) translateX(10px);
639
+ }
640
+
641
+ @keyframes _fw-gradient-shift {
642
+ 0%,
643
+ 100% {
644
+ background-position: 0% 50%;
158
645
  }
159
646
  50% {
160
- transform: translateY(-10px) translateX(-5px);
647
+ background-position: 100% 50%;
161
648
  }
162
- 75% {
163
- transform: translateY(-30px) translateX(15px);
649
+ }
650
+
651
+ @keyframes _fw-hitmarker-fade-45 {
652
+ 0% {
653
+ opacity: 1;
654
+ transform: translate(-50%, -50%) rotate(45deg) scale(0.5);
655
+ }
656
+ 20% {
657
+ opacity: 1;
658
+ transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
659
+ }
660
+ 100% {
661
+ opacity: 0;
662
+ transform: translate(-50%, -50%) rotate(45deg) scale(1);
663
+ }
664
+ }
665
+
666
+ @keyframes _fw-hitmarker-fade-neg-45 {
667
+ 0% {
668
+ opacity: 1;
669
+ transform: translate(-50%, -50%) rotate(-45deg) scale(0.5);
670
+ }
671
+ 20% {
672
+ opacity: 1;
673
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1.2);
674
+ }
675
+ 100% {
676
+ opacity: 0;
677
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1);
164
678
  }
165
679
  }
166
680
  `,
@@ -174,6 +688,36 @@ __decorate([
174
688
  __decorate([
175
689
  property({ type: Number })
176
690
  ], FwIdleScreen.prototype, "percentage", void 0);
691
+ __decorate([
692
+ property({ type: String })
693
+ ], FwIdleScreen.prototype, "error", void 0);
694
+ __decorate([
695
+ property({ type: String, attribute: "logo-src" })
696
+ ], FwIdleScreen.prototype, "logoSrc", void 0);
697
+ __decorate([
698
+ property({ type: Boolean, attribute: "retry-enabled" })
699
+ ], FwIdleScreen.prototype, "retryEnabled", void 0);
700
+ __decorate([
701
+ property({ attribute: false })
702
+ ], FwIdleScreen.prototype, "onRetry", void 0);
703
+ __decorate([
704
+ query(".idle-container")
705
+ ], FwIdleScreen.prototype, "_containerEl", void 0);
706
+ __decorate([
707
+ state()
708
+ ], FwIdleScreen.prototype, "_logoSize", void 0);
709
+ __decorate([
710
+ state()
711
+ ], FwIdleScreen.prototype, "_logoOffset", void 0);
712
+ __decorate([
713
+ state()
714
+ ], FwIdleScreen.prototype, "_isLogoHovered", void 0);
715
+ __decorate([
716
+ state()
717
+ ], FwIdleScreen.prototype, "_bubbles", void 0);
718
+ __decorate([
719
+ state()
720
+ ], FwIdleScreen.prototype, "_hitmarkers", void 0);
177
721
  FwIdleScreen = __decorate([
178
722
  customElement("fw-idle-screen")
179
723
  ], FwIdleScreen);