@livepeer-frameworks/player-wc 0.1.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 (141) hide show
  1. package/dist/cjs/components/fw-context-menu.js +17 -0
  2. package/dist/cjs/components/fw-context-menu.js.map +1 -0
  3. package/dist/cjs/components/fw-dev-mode-panel.js +273 -0
  4. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -0
  5. package/dist/cjs/components/fw-error-overlay.js +101 -0
  6. package/dist/cjs/components/fw-error-overlay.js.map +1 -0
  7. package/dist/cjs/components/fw-idle-screen.js +182 -0
  8. package/dist/cjs/components/fw-idle-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-loading-spinner.js +62 -0
  10. package/dist/cjs/components/fw-loading-spinner.js.map +1 -0
  11. package/dist/cjs/components/fw-player-controls.js +258 -0
  12. package/dist/cjs/components/fw-player-controls.js.map +1 -0
  13. package/dist/cjs/components/fw-player.js +570 -0
  14. package/dist/cjs/components/fw-player.js.map +1 -0
  15. package/dist/cjs/components/fw-seek-bar.js +233 -0
  16. package/dist/cjs/components/fw-seek-bar.js.map +1 -0
  17. package/dist/cjs/components/fw-settings-menu.js +126 -0
  18. package/dist/cjs/components/fw-settings-menu.js.map +1 -0
  19. package/dist/cjs/components/fw-skip-indicator.js +143 -0
  20. package/dist/cjs/components/fw-skip-indicator.js.map +1 -0
  21. package/dist/cjs/components/fw-speed-indicator.js +61 -0
  22. package/dist/cjs/components/fw-speed-indicator.js.map +1 -0
  23. package/dist/cjs/components/fw-stats-panel.js +141 -0
  24. package/dist/cjs/components/fw-stats-panel.js.map +1 -0
  25. package/dist/cjs/components/fw-subtitle-renderer.js +70 -0
  26. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -0
  27. package/dist/cjs/components/fw-title-overlay.js +72 -0
  28. package/dist/cjs/components/fw-title-overlay.js.map +1 -0
  29. package/dist/cjs/components/fw-toast.js +74 -0
  30. package/dist/cjs/components/fw-toast.js.map +1 -0
  31. package/dist/cjs/components/fw-volume-control.js +140 -0
  32. package/dist/cjs/components/fw-volume-control.js.map +1 -0
  33. package/dist/cjs/controllers/player-controller-host.js +315 -0
  34. package/dist/cjs/controllers/player-controller-host.js.map +1 -0
  35. package/dist/cjs/define.js +45 -0
  36. package/dist/cjs/define.js.map +1 -0
  37. package/dist/cjs/icons/index.js +153 -0
  38. package/dist/cjs/icons/index.js.map +1 -0
  39. package/dist/cjs/index.js +88 -0
  40. package/dist/cjs/index.js.map +1 -0
  41. package/dist/cjs/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 +33 -0
  42. package/dist/cjs/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.map +1 -0
  43. package/dist/cjs/styles/shared-styles.js +1967 -0
  44. package/dist/cjs/styles/shared-styles.js.map +1 -0
  45. package/dist/cjs/styles/utility-styles.js +725 -0
  46. package/dist/cjs/styles/utility-styles.js.map +1 -0
  47. package/dist/esm/components/fw-context-menu.js +17 -0
  48. package/dist/esm/components/fw-context-menu.js.map +1 -0
  49. package/dist/esm/components/fw-dev-mode-panel.js +273 -0
  50. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -0
  51. package/dist/esm/components/fw-error-overlay.js +101 -0
  52. package/dist/esm/components/fw-error-overlay.js.map +1 -0
  53. package/dist/esm/components/fw-idle-screen.js +182 -0
  54. package/dist/esm/components/fw-idle-screen.js.map +1 -0
  55. package/dist/esm/components/fw-loading-spinner.js +62 -0
  56. package/dist/esm/components/fw-loading-spinner.js.map +1 -0
  57. package/dist/esm/components/fw-player-controls.js +258 -0
  58. package/dist/esm/components/fw-player-controls.js.map +1 -0
  59. package/dist/esm/components/fw-player.js +570 -0
  60. package/dist/esm/components/fw-player.js.map +1 -0
  61. package/dist/esm/components/fw-seek-bar.js +233 -0
  62. package/dist/esm/components/fw-seek-bar.js.map +1 -0
  63. package/dist/esm/components/fw-settings-menu.js +126 -0
  64. package/dist/esm/components/fw-settings-menu.js.map +1 -0
  65. package/dist/esm/components/fw-skip-indicator.js +143 -0
  66. package/dist/esm/components/fw-skip-indicator.js.map +1 -0
  67. package/dist/esm/components/fw-speed-indicator.js +61 -0
  68. package/dist/esm/components/fw-speed-indicator.js.map +1 -0
  69. package/dist/esm/components/fw-stats-panel.js +141 -0
  70. package/dist/esm/components/fw-stats-panel.js.map +1 -0
  71. package/dist/esm/components/fw-subtitle-renderer.js +70 -0
  72. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -0
  73. package/dist/esm/components/fw-title-overlay.js +72 -0
  74. package/dist/esm/components/fw-title-overlay.js.map +1 -0
  75. package/dist/esm/components/fw-toast.js +74 -0
  76. package/dist/esm/components/fw-toast.js.map +1 -0
  77. package/dist/esm/components/fw-volume-control.js +140 -0
  78. package/dist/esm/components/fw-volume-control.js.map +1 -0
  79. package/dist/esm/controllers/player-controller-host.js +313 -0
  80. package/dist/esm/controllers/player-controller-host.js.map +1 -0
  81. package/dist/esm/define.js +43 -0
  82. package/dist/esm/define.js.map +1 -0
  83. package/dist/esm/icons/index.js +141 -0
  84. package/dist/esm/icons/index.js.map +1 -0
  85. package/dist/esm/index.js +18 -0
  86. package/dist/esm/index.js.map +1 -0
  87. package/dist/esm/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 +31 -0
  88. package/dist/esm/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.map +1 -0
  89. package/dist/esm/styles/shared-styles.js +1965 -0
  90. package/dist/esm/styles/shared-styles.js.map +1 -0
  91. package/dist/esm/styles/utility-styles.js +723 -0
  92. package/dist/esm/styles/utility-styles.js.map +1 -0
  93. package/dist/fw-player.iife.js +4362 -0
  94. package/dist/fw-player.iife.js.map +1 -0
  95. package/dist/types/components/fw-context-menu.d.ts +15 -0
  96. package/dist/types/components/fw-dev-mode-panel.d.ts +24 -0
  97. package/dist/types/components/fw-error-overlay.d.ts +14 -0
  98. package/dist/types/components/fw-idle-screen.d.ts +13 -0
  99. package/dist/types/components/fw-loading-spinner.d.ts +10 -0
  100. package/dist/types/components/fw-player-controls.d.ts +23 -0
  101. package/dist/types/components/fw-player.d.ts +74 -0
  102. package/dist/types/components/fw-seek-bar.d.ts +33 -0
  103. package/dist/types/components/fw-settings-menu.d.ts +16 -0
  104. package/dist/types/components/fw-skip-indicator.d.ts +18 -0
  105. package/dist/types/components/fw-speed-indicator.d.ts +11 -0
  106. package/dist/types/components/fw-stats-panel.d.ts +18 -0
  107. package/dist/types/components/fw-subtitle-renderer.d.ts +21 -0
  108. package/dist/types/components/fw-title-overlay.d.ts +12 -0
  109. package/dist/types/components/fw-toast.d.ts +12 -0
  110. package/dist/types/components/fw-volume-control.d.ts +18 -0
  111. package/dist/types/controllers/player-controller-host.d.ts +119 -0
  112. package/dist/types/define.d.ts +1 -0
  113. package/dist/types/icons/index.d.ts +23 -0
  114. package/dist/types/iife-entry.d.ts +11 -0
  115. package/dist/types/index.d.ts +25 -0
  116. package/dist/types/styles/shared-styles.d.ts +1 -0
  117. package/dist/types/styles/utility-styles.d.ts +1 -0
  118. package/package.json +65 -0
  119. package/src/components/fw-context-menu.ts +23 -0
  120. package/src/components/fw-dev-mode-panel.ts +285 -0
  121. package/src/components/fw-error-overlay.ts +96 -0
  122. package/src/components/fw-idle-screen.ts +182 -0
  123. package/src/components/fw-loading-spinner.ts +63 -0
  124. package/src/components/fw-player-controls.ts +256 -0
  125. package/src/components/fw-player.ts +557 -0
  126. package/src/components/fw-seek-bar.ts +219 -0
  127. package/src/components/fw-settings-menu.ts +128 -0
  128. package/src/components/fw-skip-indicator.ts +139 -0
  129. package/src/components/fw-speed-indicator.ts +57 -0
  130. package/src/components/fw-stats-panel.ts +154 -0
  131. package/src/components/fw-subtitle-renderer.ts +65 -0
  132. package/src/components/fw-title-overlay.ts +64 -0
  133. package/src/components/fw-toast.ts +70 -0
  134. package/src/components/fw-volume-control.ts +140 -0
  135. package/src/controllers/player-controller-host.ts +457 -0
  136. package/src/define.ts +43 -0
  137. package/src/icons/index.ts +209 -0
  138. package/src/iife-entry.ts +11 -0
  139. package/src/index.ts +31 -0
  140. package/src/styles/shared-styles.ts +1962 -0
  141. package/src/styles/utility-styles.ts +720 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * <fw-dev-mode-panel> — Developer mode panel for forcing player/source selection.
3
+ * Port of DevModePanel.tsx from player-react.
4
+ */
5
+ import { LitElement, html, css, nothing } from "lit";
6
+ import { customElement, property, state } from "lit/decorators.js";
7
+ import { classMap } from "lit/directives/class-map.js";
8
+ import { sharedStyles } from "../styles/shared-styles.js";
9
+ import { utilityStyles } from "../styles/utility-styles.js";
10
+ import { closeIcon } from "../icons/index.js";
11
+ import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
12
+ import type { PlaybackMode } from "@livepeer-frameworks/player-core";
13
+
14
+ @customElement("fw-dev-mode-panel")
15
+ export class FwDevModePanel extends LitElement {
16
+ @property({ attribute: false }) pc!: PlayerControllerHost;
17
+ @property({ type: String }) playbackMode: PlaybackMode = "auto";
18
+
19
+ @state() private _activeTab: "config" | "stats" = "config";
20
+
21
+ static styles = [
22
+ sharedStyles,
23
+ utilityStyles,
24
+ css`
25
+ :host {
26
+ display: block;
27
+ }
28
+ .panel {
29
+ width: 320px;
30
+ height: 100%;
31
+ border-left: 1px solid rgb(255 255 255 / 0.1);
32
+ background: rgb(15 23 42);
33
+ overflow: auto;
34
+ font-size: 0.75rem;
35
+ color: rgb(255 255 255 / 0.7);
36
+ }
37
+ .header {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ padding: 0.5rem 0.75rem;
42
+ border-bottom: 1px solid rgb(255 255 255 / 0.1);
43
+ }
44
+ .tabs {
45
+ display: flex;
46
+ gap: 0.5rem;
47
+ }
48
+ .tab {
49
+ padding: 0.25rem 0.5rem;
50
+ border: none;
51
+ background: none;
52
+ color: rgb(255 255 255 / 0.5);
53
+ font-size: 0.6875rem;
54
+ font-weight: 600;
55
+ cursor: pointer;
56
+ border-radius: 0.25rem;
57
+ }
58
+ .tab--active {
59
+ color: white;
60
+ background: rgb(255 255 255 / 0.1);
61
+ }
62
+ .close {
63
+ display: flex;
64
+ background: none;
65
+ border: none;
66
+ color: rgb(255 255 255 / 0.5);
67
+ cursor: pointer;
68
+ padding: 0;
69
+ }
70
+ .close:hover {
71
+ color: white;
72
+ }
73
+ .body {
74
+ padding: 0.75rem;
75
+ }
76
+ .section {
77
+ margin-bottom: 0.75rem;
78
+ }
79
+ .label {
80
+ font-size: 0.625rem;
81
+ font-weight: 600;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.05em;
84
+ color: rgb(255 255 255 / 0.4);
85
+ margin-bottom: 0.375rem;
86
+ }
87
+ .value {
88
+ color: rgb(255 255 255 / 0.9);
89
+ font-family: ui-monospace, monospace;
90
+ }
91
+ .mode-group {
92
+ display: flex;
93
+ gap: 0.25rem;
94
+ flex-wrap: wrap;
95
+ }
96
+ .mode-btn {
97
+ padding: 0.25rem 0.5rem;
98
+ border: 1px solid rgb(255 255 255 / 0.15);
99
+ background: none;
100
+ color: rgb(255 255 255 / 0.6);
101
+ font-size: 0.6875rem;
102
+ cursor: pointer;
103
+ border-radius: 0.25rem;
104
+ transition: all 150ms;
105
+ }
106
+ .mode-btn:hover {
107
+ border-color: rgb(255 255 255 / 0.3);
108
+ color: white;
109
+ }
110
+ .mode-btn--active {
111
+ border-color: hsl(var(--tn-blue, 217 89% 61%));
112
+ color: hsl(var(--tn-blue, 217 89% 61%));
113
+ background: hsl(var(--tn-blue, 217 89% 61%) / 0.1);
114
+ }
115
+ .actions {
116
+ display: flex;
117
+ gap: 0.5rem;
118
+ margin-top: 0.5rem;
119
+ }
120
+ .action-btn {
121
+ padding: 0.375rem 0.75rem;
122
+ border: 1px solid rgb(255 255 255 / 0.15);
123
+ background: none;
124
+ color: rgb(255 255 255 / 0.7);
125
+ font-size: 0.6875rem;
126
+ cursor: pointer;
127
+ border-radius: 0.25rem;
128
+ }
129
+ .action-btn:hover {
130
+ border-color: rgb(255 255 255 / 0.3);
131
+ color: white;
132
+ }
133
+ .stat-row {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ padding: 0.125rem 0;
137
+ }
138
+ .stat-label {
139
+ color: rgb(255 255 255 / 0.4);
140
+ }
141
+ .stat-value {
142
+ color: rgb(255 255 255 / 0.8);
143
+ font-family: ui-monospace, monospace;
144
+ font-variant-numeric: tabular-nums;
145
+ }
146
+ `,
147
+ ];
148
+
149
+ private _modes: PlaybackMode[] = ["auto", "low-latency", "quality"];
150
+
151
+ protected render() {
152
+ const s = this.pc.s;
153
+
154
+ return html`
155
+ <div class="panel fw-dev-panel">
156
+ <div class="header fw-dev-header">
157
+ <div class="tabs">
158
+ <button
159
+ class=${classMap({ tab: true, "tab--active": this._activeTab === "config" })}
160
+ @click=${() => {
161
+ this._activeTab = "config";
162
+ }}
163
+ >
164
+ Config
165
+ </button>
166
+ <button
167
+ class=${classMap({ tab: true, "tab--active": this._activeTab === "stats" })}
168
+ @click=${() => {
169
+ this._activeTab = "stats";
170
+ }}
171
+ >
172
+ Stats
173
+ </button>
174
+ </div>
175
+ <button
176
+ class="close"
177
+ @click=${() =>
178
+ this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }))}
179
+ aria-label="Close panel"
180
+ >
181
+ ${closeIcon()}
182
+ </button>
183
+ </div>
184
+
185
+ <div class="body fw-dev-body">
186
+ ${this._activeTab === "config" ? this._renderConfig(s) : this._renderStats(s)}
187
+ </div>
188
+ </div>
189
+ `;
190
+ }
191
+
192
+ private _renderConfig(s: typeof this.pc.s) {
193
+ return html`
194
+ <div class="section">
195
+ <div class="label">Current Player</div>
196
+ <div class="value">${s.currentPlayerInfo?.name ?? "—"}</div>
197
+ </div>
198
+ <div class="section">
199
+ <div class="label">Current Source</div>
200
+ <div class="value">${s.currentSourceInfo?.type ?? "—"}</div>
201
+ </div>
202
+ <div class="section">
203
+ <div class="label">Playback Mode</div>
204
+ <div class="mode-group fw-dev-mode-group">
205
+ ${this._modes.map(
206
+ (mode) => html`
207
+ <button
208
+ class=${classMap({
209
+ "mode-btn": true,
210
+ "fw-dev-mode-btn": true,
211
+ "mode-btn--active": this.playbackMode === mode,
212
+ "fw-dev-mode-btn--active": this.playbackMode === mode,
213
+ })}
214
+ @click=${() => this.pc.setDevModeOptions({ playbackMode: mode })}
215
+ >
216
+ ${mode}
217
+ </button>
218
+ `
219
+ )}
220
+ </div>
221
+ </div>
222
+ <div class="actions fw-dev-actions">
223
+ <button
224
+ class="action-btn fw-dev-action-btn"
225
+ @click=${() => {
226
+ this.pc.clearError();
227
+ this.pc.reload();
228
+ }}
229
+ >
230
+ Reload
231
+ </button>
232
+ </div>
233
+ `;
234
+ }
235
+
236
+ private _renderStats(s: typeof this.pc.s) {
237
+ const q = s.playbackQuality;
238
+ return html`
239
+ <div class="section">
240
+ <div class="label">Playback</div>
241
+ ${this._row("State", s.state)}
242
+ ${this._row(
243
+ "Time",
244
+ `${s.currentTime.toFixed(1)}s / ${isFinite(s.duration) ? s.duration.toFixed(1) + "s" : "∞"}`
245
+ )}
246
+ ${this._row("Volume", `${Math.round(s.volume * 100)}%${s.isMuted ? " (muted)" : ""}`)}
247
+ </div>
248
+ ${q
249
+ ? html`
250
+ <div class="section">
251
+ <div class="label">Quality</div>
252
+ ${this._row("Resolution", this._resolution())}
253
+ ${this._row("Bitrate", q.bitrate ? `${Math.round(q.bitrate / 1000)} kbps` : "—")}
254
+ ${this._row("Latency", q.latency != null ? `${q.latency.toFixed(2)}s` : "—")}
255
+ ${this._row(
256
+ "Buffer",
257
+ q.bufferedAhead != null ? `${q.bufferedAhead.toFixed(1)}s` : "—"
258
+ )}
259
+ ${this._row("Score", q.score != null ? `${q.score.toFixed(0)}` : "—")}
260
+ ${this._row("Drops", `${q.frameDropRate?.toFixed(1) ?? "0"}%`)}
261
+ ${this._row("Stalls", `${q.stallCount ?? 0}`)}
262
+ </div>
263
+ `
264
+ : nothing}
265
+ `;
266
+ }
267
+
268
+ private _resolution(): string {
269
+ const video = this.pc.s.videoElement;
270
+ if (!video || !video.videoWidth || !video.videoHeight) return "—";
271
+ return `${video.videoWidth}×${video.videoHeight}`;
272
+ }
273
+
274
+ private _row(label: string, value: string) {
275
+ return html`<div class="stat-row">
276
+ <span class="stat-label">${label}</span><span class="stat-value">${value}</span>
277
+ </div>`;
278
+ }
279
+ }
280
+
281
+ declare global {
282
+ interface HTMLElementTagNameMap {
283
+ "fw-dev-mode-panel": FwDevModePanel;
284
+ }
285
+ }
@@ -0,0 +1,96 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { sharedStyles } from "../styles/shared-styles.js";
5
+ import { utilityStyles } from "../styles/utility-styles.js";
6
+ import { closeIcon } from "../icons/index.js";
7
+
8
+ @customElement("fw-error-overlay")
9
+ export class FwErrorOverlay extends LitElement {
10
+ @property({ type: String }) error: string | null = null;
11
+ @property({ type: Boolean, attribute: "is-passive" }) isPassive = false;
12
+
13
+ static styles = [
14
+ sharedStyles,
15
+ utilityStyles,
16
+ css`
17
+ :host {
18
+ display: contents;
19
+ }
20
+ `,
21
+ ];
22
+
23
+ protected render() {
24
+ return html`
25
+ <div
26
+ role="alert"
27
+ aria-live="assertive"
28
+ class=${classMap({
29
+ "fw-error-overlay": true,
30
+ "fw-error-overlay--passive": this.isPassive,
31
+ "fw-error-overlay--fullscreen": !this.isPassive,
32
+ })}
33
+ >
34
+ <div
35
+ class=${classMap({
36
+ "fw-error-popup": true,
37
+ "fw-error-popup--passive": this.isPassive,
38
+ "fw-error-popup--fullscreen": !this.isPassive,
39
+ })}
40
+ >
41
+ <div
42
+ class=${classMap({
43
+ "fw-error-header": true,
44
+ "fw-error-header--warning": this.isPassive,
45
+ "fw-error-header--error": !this.isPassive,
46
+ })}
47
+ >
48
+ <span
49
+ class=${classMap({
50
+ "fw-error-title": true,
51
+ "fw-error-title--warning": this.isPassive,
52
+ "fw-error-title--error": !this.isPassive,
53
+ })}
54
+ >${this.isPassive ? "Warning" : "Error"}</span
55
+ >
56
+ <button
57
+ type="button"
58
+ class="fw-error-close"
59
+ @click=${this._clearError}
60
+ aria-label="Dismiss"
61
+ >
62
+ ${closeIcon()}
63
+ </button>
64
+ </div>
65
+ <div class="fw-error-body">
66
+ <p class="fw-error-message">Playback issue</p>
67
+ </div>
68
+ <div class="fw-error-actions">
69
+ <button
70
+ type="button"
71
+ class="fw-error-btn"
72
+ aria-label="Retry playback"
73
+ @click=${this._retry}
74
+ >
75
+ Retry
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ `;
81
+ }
82
+
83
+ private _clearError() {
84
+ this.dispatchEvent(new CustomEvent("fw-clear-error", { bubbles: true, composed: true }));
85
+ }
86
+
87
+ private _retry() {
88
+ this.dispatchEvent(new CustomEvent("fw-retry", { bubbles: true, composed: true }));
89
+ }
90
+ }
91
+
92
+ declare global {
93
+ interface HTMLElementTagNameMap {
94
+ "fw-error-overlay": FwErrorOverlay;
95
+ }
96
+ }
@@ -0,0 +1,182 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { sharedStyles } from "../styles/shared-styles.js";
5
+ import { utilityStyles } from "../styles/utility-styles.js";
6
+
7
+ @customElement("fw-idle-screen")
8
+ export class FwIdleScreen extends LitElement {
9
+ @property({ type: String }) status?: string;
10
+ @property({ type: String }) message?: string;
11
+ @property({ type: Number }) percentage?: number;
12
+
13
+ static styles = [
14
+ sharedStyles,
15
+ utilityStyles,
16
+ css`
17
+ :host {
18
+ display: contents;
19
+ }
20
+ .idle {
21
+ position: absolute;
22
+ inset: 0;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ z-index: 15;
27
+ background: linear-gradient(135deg, rgb(15 23 42), rgb(2 6 23), rgb(15 23 42));
28
+ overflow: hidden;
29
+ }
30
+ .card {
31
+ position: relative;
32
+ z-index: 10;
33
+ max-width: 280px;
34
+ width: 100%;
35
+ text-align: center;
36
+ }
37
+ .status-icon {
38
+ margin: 0 auto 0.75rem;
39
+ width: 2.5rem;
40
+ height: 2.5rem;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+ .spinner {
46
+ width: 1.5rem;
47
+ height: 1.5rem;
48
+ border: 2px solid rgb(255 255 255 / 0.2);
49
+ border-top-color: hsl(var(--tn-blue, 217 89% 61%));
50
+ border-radius: 50%;
51
+ animation: _fw-spin 1s linear infinite;
52
+ }
53
+ @keyframes _fw-spin {
54
+ to {
55
+ transform: rotate(360deg);
56
+ }
57
+ }
58
+ .offline-dot {
59
+ width: 0.75rem;
60
+ height: 0.75rem;
61
+ border-radius: 50%;
62
+ background: hsl(var(--tn-red, 348 74% 64%));
63
+ animation: _fw-blink 2s ease-in-out infinite;
64
+ }
65
+ @keyframes _fw-blink {
66
+ 0%,
67
+ 100% {
68
+ opacity: 1;
69
+ }
70
+ 50% {
71
+ opacity: 0.3;
72
+ }
73
+ }
74
+ .label {
75
+ font-size: 0.6875rem;
76
+ font-weight: 600;
77
+ text-transform: uppercase;
78
+ letter-spacing: 0.05em;
79
+ color: rgb(255 255 255 / 0.5);
80
+ margin-bottom: 0.5rem;
81
+ }
82
+ .msg {
83
+ font-size: 0.8125rem;
84
+ color: rgb(255 255 255 / 0.7);
85
+ margin-bottom: 0.75rem;
86
+ }
87
+ .progress-wrap {
88
+ width: 100%;
89
+ height: 0.25rem;
90
+ background: rgb(255 255 255 / 0.1);
91
+ border-radius: 2px;
92
+ overflow: hidden;
93
+ margin-bottom: 0.5rem;
94
+ }
95
+ .progress-bar {
96
+ height: 100%;
97
+ background: hsl(var(--tn-blue, 217 89% 61%));
98
+ border-radius: 2px;
99
+ transition: width 300ms ease;
100
+ }
101
+ .pct {
102
+ font-size: 0.6875rem;
103
+ color: rgb(255 255 255 / 0.4);
104
+ font-variant-numeric: tabular-nums;
105
+ }
106
+ .particles {
107
+ position: absolute;
108
+ inset: 0;
109
+ overflow: hidden;
110
+ pointer-events: none;
111
+ }
112
+ .particle {
113
+ position: absolute;
114
+ width: 4px;
115
+ height: 4px;
116
+ border-radius: 50%;
117
+ opacity: 0.3;
118
+ animation: _fw-float var(--dur, 8s) ease-in-out infinite;
119
+ }
120
+ @keyframes _fw-float {
121
+ 0%,
122
+ 100% {
123
+ transform: translateY(0) translateX(0);
124
+ }
125
+ 25% {
126
+ transform: translateY(-20px) translateX(10px);
127
+ }
128
+ 50% {
129
+ transform: translateY(-10px) translateX(-5px);
130
+ }
131
+ 75% {
132
+ transform: translateY(-30px) translateX(15px);
133
+ }
134
+ }
135
+ `,
136
+ ];
137
+
138
+ protected render() {
139
+ const isOffline = this.status === "OFFLINE";
140
+ const isInitializing = this.status === "INITIALIZING" || this.status === "STARTING";
141
+
142
+ return html`
143
+ <div class="idle">
144
+ <div class="particles">
145
+ ${[0, 1, 2, 3, 4, 5, 6, 7].map(
146
+ (i) => html`
147
+ <div
148
+ class="particle"
149
+ style="left: ${10 + i * 11}%; top: ${20 + (i % 3) * 25}%; --dur: ${6 +
150
+ i * 0.8}s; background: hsl(${217 + i * 15} 70% 60%); animation-delay: ${i * -1}s;"
151
+ ></div>
152
+ `
153
+ )}
154
+ </div>
155
+ <div class="card">
156
+ <div class="status-icon">
157
+ ${isOffline ? html`<div class="offline-dot"></div>` : html`<div class="spinner"></div>`}
158
+ </div>
159
+ ${this.status ? html`<div class="label">${this.status}</div>` : nothing}
160
+ ${this.message ? html`<div class="msg">${this.message}</div>` : nothing}
161
+ ${isInitializing && this.percentage != null
162
+ ? html`
163
+ <div class="progress-wrap">
164
+ <div
165
+ class="progress-bar"
166
+ style="width: ${Math.min(100, Math.max(0, this.percentage))}%"
167
+ ></div>
168
+ </div>
169
+ <div class="pct">${Math.round(this.percentage)}%</div>
170
+ `
171
+ : nothing}
172
+ </div>
173
+ </div>
174
+ `;
175
+ }
176
+ }
177
+
178
+ declare global {
179
+ interface HTMLElementTagNameMap {
180
+ "fw-idle-screen": FwIdleScreen;
181
+ }
182
+ }
@@ -0,0 +1,63 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+
4
+ @customElement("fw-loading-spinner")
5
+ export class FwLoadingSpinner extends LitElement {
6
+ static styles = css`
7
+ :host {
8
+ display: contents;
9
+ }
10
+ .overlay {
11
+ position: absolute;
12
+ inset: 0;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ background: rgb(0 0 0 / 0.4);
17
+ backdrop-filter: blur(4px);
18
+ z-index: 20;
19
+ }
20
+ .pill {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 0.75rem;
24
+ border-radius: 0.5rem;
25
+ border: 1px solid rgb(255 255 255 / 0.1);
26
+ background: rgb(0 0 0 / 0.7);
27
+ padding: 0.75rem 1rem;
28
+ font-size: 0.875rem;
29
+ color: white;
30
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
31
+ }
32
+ .spinner {
33
+ width: 1rem;
34
+ height: 1rem;
35
+ border: 2px solid rgb(255 255 255 / 0.3);
36
+ border-top-color: white;
37
+ border-radius: 50%;
38
+ animation: _fw-spin 1s linear infinite;
39
+ }
40
+ @keyframes _fw-spin {
41
+ to {
42
+ transform: rotate(360deg);
43
+ }
44
+ }
45
+ `;
46
+
47
+ protected render() {
48
+ return html`
49
+ <div class="overlay" role="status" aria-live="polite">
50
+ <div class="pill">
51
+ <div class="spinner"></div>
52
+ <span>Buffering...</span>
53
+ </div>
54
+ </div>
55
+ `;
56
+ }
57
+ }
58
+
59
+ declare global {
60
+ interface HTMLElementTagNameMap {
61
+ "fw-loading-spinner": FwLoadingSpinner;
62
+ }
63
+ }