@livepeer-frameworks/streamcrafter-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 (81) hide show
  1. package/dist/cjs/components/fw-sc-advanced.js +198 -0
  2. package/dist/cjs/components/fw-sc-advanced.js.map +1 -0
  3. package/dist/cjs/components/fw-sc-compositor.js +116 -0
  4. package/dist/cjs/components/fw-sc-compositor.js.map +1 -0
  5. package/dist/cjs/components/fw-sc-layer-list.js +253 -0
  6. package/dist/cjs/components/fw-sc-layer-list.js.map +1 -0
  7. package/dist/cjs/components/fw-sc-scene-switcher.js +164 -0
  8. package/dist/cjs/components/fw-sc-scene-switcher.js.map +1 -0
  9. package/dist/cjs/components/fw-sc-volume.js +183 -0
  10. package/dist/cjs/components/fw-sc-volume.js.map +1 -0
  11. package/dist/cjs/components/fw-streamcrafter.js +508 -0
  12. package/dist/cjs/components/fw-streamcrafter.js.map +1 -0
  13. package/dist/cjs/controllers/ingest-controller-host.js +236 -0
  14. package/dist/cjs/controllers/ingest-controller-host.js.map +1 -0
  15. package/dist/cjs/define.js +25 -0
  16. package/dist/cjs/define.js.map +1 -0
  17. package/dist/cjs/icons/index.js +283 -0
  18. package/dist/cjs/icons/index.js.map +1 -0
  19. package/dist/cjs/index.js +38 -0
  20. package/dist/cjs/index.js.map +1 -0
  21. 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
  22. 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
  23. package/dist/cjs/styles/shared-styles.js +2019 -0
  24. package/dist/cjs/styles/shared-styles.js.map +1 -0
  25. package/dist/cjs/styles/utility-styles.js +182 -0
  26. package/dist/cjs/styles/utility-styles.js.map +1 -0
  27. package/dist/esm/components/fw-sc-advanced.js +198 -0
  28. package/dist/esm/components/fw-sc-advanced.js.map +1 -0
  29. package/dist/esm/components/fw-sc-compositor.js +116 -0
  30. package/dist/esm/components/fw-sc-compositor.js.map +1 -0
  31. package/dist/esm/components/fw-sc-layer-list.js +253 -0
  32. package/dist/esm/components/fw-sc-layer-list.js.map +1 -0
  33. package/dist/esm/components/fw-sc-scene-switcher.js +164 -0
  34. package/dist/esm/components/fw-sc-scene-switcher.js.map +1 -0
  35. package/dist/esm/components/fw-sc-volume.js +183 -0
  36. package/dist/esm/components/fw-sc-volume.js.map +1 -0
  37. package/dist/esm/components/fw-streamcrafter.js +508 -0
  38. package/dist/esm/components/fw-streamcrafter.js.map +1 -0
  39. package/dist/esm/controllers/ingest-controller-host.js +234 -0
  40. package/dist/esm/controllers/ingest-controller-host.js.map +1 -0
  41. package/dist/esm/define.js +23 -0
  42. package/dist/esm/define.js.map +1 -0
  43. package/dist/esm/icons/index.js +253 -0
  44. package/dist/esm/icons/index.js.map +1 -0
  45. package/dist/esm/index.js +8 -0
  46. package/dist/esm/index.js.map +1 -0
  47. 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
  48. 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
  49. package/dist/esm/styles/shared-styles.js +2017 -0
  50. package/dist/esm/styles/shared-styles.js.map +1 -0
  51. package/dist/esm/styles/utility-styles.js +180 -0
  52. package/dist/esm/styles/utility-styles.js.map +1 -0
  53. package/dist/fw-streamcrafter.iife.js +3121 -0
  54. package/dist/fw-streamcrafter.iife.js.map +1 -0
  55. package/dist/types/components/fw-sc-advanced.d.ts +20 -0
  56. package/dist/types/components/fw-sc-compositor.d.ts +19 -0
  57. package/dist/types/components/fw-sc-layer-list.d.ts +30 -0
  58. package/dist/types/components/fw-sc-scene-switcher.d.ts +23 -0
  59. package/dist/types/components/fw-sc-volume.d.ts +30 -0
  60. package/dist/types/components/fw-streamcrafter.d.ts +49 -0
  61. package/dist/types/controllers/ingest-controller-host.d.ts +77 -0
  62. package/dist/types/define.d.ts +1 -0
  63. package/dist/types/icons/index.d.ts +29 -0
  64. package/dist/types/iife-entry.d.ts +11 -0
  65. package/dist/types/index.d.ts +12 -0
  66. package/dist/types/styles/shared-styles.d.ts +1 -0
  67. package/dist/types/styles/utility-styles.d.ts +1 -0
  68. package/package.json +55 -0
  69. package/src/components/fw-sc-advanced.ts +221 -0
  70. package/src/components/fw-sc-compositor.ts +162 -0
  71. package/src/components/fw-sc-layer-list.ts +251 -0
  72. package/src/components/fw-sc-scene-switcher.ts +163 -0
  73. package/src/components/fw-sc-volume.ts +171 -0
  74. package/src/components/fw-streamcrafter.ts +515 -0
  75. package/src/controllers/ingest-controller-host.ts +358 -0
  76. package/src/define.ts +23 -0
  77. package/src/icons/index.ts +291 -0
  78. package/src/iife-entry.ts +11 -0
  79. package/src/index.ts +15 -0
  80. package/src/styles/shared-styles.ts +2014 -0
  81. package/src/styles/utility-styles.ts +177 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * <fw-sc-compositor> — Compact floating compositor controls overlay.
3
+ * Port of CompositorControls.tsx from streamcrafter-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 {
11
+ soloIcon,
12
+ pipBRIcon,
13
+ pipBLIcon,
14
+ pipTRIcon,
15
+ pipTLIcon,
16
+ splitHIcon,
17
+ splitVIcon,
18
+ focusLIcon,
19
+ focusRIcon,
20
+ gridIcon,
21
+ stackIcon,
22
+ dualPipIcon,
23
+ splitPipIcon,
24
+ featuredIcon,
25
+ featuredRIcon,
26
+ letterboxIcon,
27
+ cropIcon,
28
+ stretchIcon,
29
+ } from "../icons/index.js";
30
+ import type { IngestControllerHost } from "../controllers/ingest-controller-host.js";
31
+ import type {
32
+ LayoutMode,
33
+ LayoutConfig,
34
+ ScalingMode,
35
+ } from "@livepeer-frameworks/streamcrafter-core";
36
+ import { isLayoutAvailable } from "@livepeer-frameworks/streamcrafter-core";
37
+
38
+ interface LayoutPresetUI {
39
+ mode: LayoutMode;
40
+ label: string;
41
+ icon: () => ReturnType<typeof soloIcon>;
42
+ minSources: number;
43
+ }
44
+
45
+ const LAYOUT_PRESETS_UI: LayoutPresetUI[] = [
46
+ { mode: "solo", label: "Solo", icon: soloIcon, minSources: 1 },
47
+ { mode: "pip-br", label: "PiP ↘", icon: pipBRIcon, minSources: 2 },
48
+ { mode: "pip-bl", label: "PiP ↙", icon: pipBLIcon, minSources: 2 },
49
+ { mode: "pip-tr", label: "PiP ↗", icon: pipTRIcon, minSources: 2 },
50
+ { mode: "pip-tl", label: "PiP ↖", icon: pipTLIcon, minSources: 2 },
51
+ { mode: "split-h", label: "Split ⬌", icon: splitHIcon, minSources: 2 },
52
+ { mode: "split-v", label: "Split ⬍", icon: splitVIcon, minSources: 2 },
53
+ { mode: "focus-l", label: "Focus ◀", icon: focusLIcon, minSources: 2 },
54
+ { mode: "focus-r", label: "Focus ▶", icon: focusRIcon, minSources: 2 },
55
+ { mode: "pip-dual-br", label: "Main+2 PiP", icon: dualPipIcon, minSources: 3 },
56
+ { mode: "split-pip-r", label: "Split+PiP", icon: splitPipIcon, minSources: 3 },
57
+ { mode: "featured", label: "Featured", icon: featuredIcon, minSources: 3 },
58
+ { mode: "featured-r", label: "Featured ▶", icon: featuredRIcon, minSources: 3 },
59
+ { mode: "grid", label: "Grid", icon: gridIcon, minSources: 2 },
60
+ { mode: "stack", label: "Stack", icon: stackIcon, minSources: 2 },
61
+ ];
62
+
63
+ const SCALING_MODES: {
64
+ mode: ScalingMode;
65
+ icon: () => ReturnType<typeof letterboxIcon>;
66
+ label: string;
67
+ }[] = [
68
+ { mode: "letterbox", icon: letterboxIcon, label: "Letterbox (fit)" },
69
+ { mode: "crop", icon: cropIcon, label: "Crop (fill)" },
70
+ { mode: "stretch", icon: stretchIcon, label: "Stretch" },
71
+ ];
72
+
73
+ @customElement("fw-sc-compositor")
74
+ export class FwScCompositor extends LitElement {
75
+ @property({ attribute: false }) ic!: IngestControllerHost;
76
+
77
+ @state() private _tooltipText = "";
78
+ @state() private _tooltipTarget: Element | null = null;
79
+
80
+ static styles = [
81
+ sharedStyles,
82
+ utilityStyles,
83
+ css`
84
+ :host {
85
+ display: contents;
86
+ }
87
+ `,
88
+ ];
89
+
90
+ protected render() {
91
+ // Compositor uses controller compositor API — for now render layout bar from CSS classes
92
+ const sources = this.ic.s.sources;
93
+ const visibleSourceCount = sources.length;
94
+
95
+ const availableLayouts = LAYOUT_PRESETS_UI.filter((preset) =>
96
+ isLayoutAvailable(preset.mode, visibleSourceCount)
97
+ );
98
+
99
+ return html`
100
+ <div class="fw-sc-layout-overlay">
101
+ <div class="fw-sc-layout-bar">
102
+ <div class="fw-sc-layout-section">
103
+ <span class="fw-sc-layout-label">Layout</span>
104
+ <div class="fw-sc-layout-icons">
105
+ ${availableLayouts.map(
106
+ (preset) => html`
107
+ <button
108
+ type="button"
109
+ class="fw-sc-layout-icon"
110
+ @click=${(e: MouseEvent) => {
111
+ e.stopPropagation();
112
+ this._handleLayoutSelect(preset.mode);
113
+ }}
114
+ title=${preset.label}
115
+ >
116
+ ${preset.icon()}
117
+ </button>
118
+ `
119
+ )}
120
+ </div>
121
+ </div>
122
+ <div class="fw-sc-layout-separator"></div>
123
+ <div class="fw-sc-layout-section">
124
+ <span class="fw-sc-layout-label">Display</span>
125
+ <div class="fw-sc-scaling-icons">
126
+ ${SCALING_MODES.map(
127
+ (sm) => html`
128
+ <button
129
+ type="button"
130
+ class="fw-sc-layout-icon"
131
+ @click=${(e: MouseEvent) => {
132
+ e.stopPropagation();
133
+ }}
134
+ title=${sm.label}
135
+ >
136
+ ${sm.icon()}
137
+ </button>
138
+ `
139
+ )}
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ `;
145
+ }
146
+
147
+ private _handleLayoutSelect(mode: LayoutMode) {
148
+ this.dispatchEvent(
149
+ new CustomEvent("fw-sc-layout-select", {
150
+ detail: { mode },
151
+ bubbles: true,
152
+ composed: true,
153
+ })
154
+ );
155
+ }
156
+ }
157
+
158
+ declare global {
159
+ interface HTMLElementTagNameMap {
160
+ "fw-sc-compositor": FwScCompositor;
161
+ }
162
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * <fw-sc-layer-list> — Drag-to-reorder layer list.
3
+ * Port of LayerList.tsx from streamcrafter-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 { eyeIcon, eyeOffIcon, cameraIcon, monitorIcon, videoIcon } from "../icons/index.js";
11
+ import type { Layer, MediaSource, LayerTransform } from "@livepeer-frameworks/streamcrafter-core";
12
+
13
+ @customElement("fw-sc-layer-list")
14
+ export class FwScLayerList extends LitElement {
15
+ @property({ attribute: false }) layers: Layer[] = [];
16
+ @property({ attribute: false }) sources: MediaSource[] = [];
17
+ @property({ type: String, attribute: "selected-layer-id" }) selectedLayerId: string | null = null;
18
+
19
+ @state() private _draggedId: string | null = null;
20
+ @state() private _dragOverId: string | null = null;
21
+ @state() private _editingLayerId: string | null = null;
22
+
23
+ static styles = [
24
+ sharedStyles,
25
+ utilityStyles,
26
+ css`
27
+ :host {
28
+ display: block;
29
+ }
30
+ `,
31
+ ];
32
+
33
+ private get _sortedLayers(): Layer[] {
34
+ return [...this.layers].sort((a, b) => b.zIndex - a.zIndex);
35
+ }
36
+
37
+ private _getSourceLabel(sourceId: string): string {
38
+ const source = this.sources.find((s) => s.id === sourceId);
39
+ return source?.label || sourceId;
40
+ }
41
+
42
+ private _getSourceIcon(sourceId: string) {
43
+ const source = this.sources.find((s) => s.id === sourceId);
44
+ switch (source?.type) {
45
+ case "camera":
46
+ return cameraIcon(14);
47
+ case "screen":
48
+ return monitorIcon(14);
49
+ default:
50
+ return videoIcon(14);
51
+ }
52
+ }
53
+
54
+ protected render() {
55
+ const sorted = this._sortedLayers;
56
+ return html`
57
+ <div class="fw-sc-layer-list">
58
+ <div class="fw-sc-layer-list-header">
59
+ <span class="fw-sc-layer-list-title">Layers</span>
60
+ <span class="fw-sc-layer-count">${this.layers.length}</span>
61
+ </div>
62
+
63
+ <div class="fw-sc-layer-items">
64
+ ${sorted.length === 0
65
+ ? html` <div class="fw-sc-layer-empty">No layers. Add a source to get started.</div> `
66
+ : sorted.map(
67
+ (layer, index) => html`
68
+ <div
69
+ class=${classMap({
70
+ "fw-sc-layer-item": true,
71
+ "fw-sc-layer-item--selected": layer.id === this.selectedLayerId,
72
+ "fw-sc-layer-item--dragging": layer.id === this._draggedId,
73
+ "fw-sc-layer-item--drag-over": layer.id === this._dragOverId,
74
+ "fw-sc-layer-item--hidden": !layer.visible,
75
+ })}
76
+ draggable="true"
77
+ @dragstart=${(e: DragEvent) => this._handleDragStart(e, layer.id)}
78
+ @dragover=${(e: DragEvent) => this._handleDragOver(e, layer.id)}
79
+ @dragleave=${() => {
80
+ this._dragOverId = null;
81
+ }}
82
+ @drop=${(e: DragEvent) => this._handleDrop(e, layer.id)}
83
+ @dragend=${() => {
84
+ this._draggedId = null;
85
+ this._dragOverId = null;
86
+ }}
87
+ @click=${() =>
88
+ this._dispatch("fw-sc-layer-select", {
89
+ layerId: layer.id === this.selectedLayerId ? null : layer.id,
90
+ })}
91
+ >
92
+ <button
93
+ class=${classMap({
94
+ "fw-sc-layer-visibility": true,
95
+ "fw-sc-layer-visibility--visible": layer.visible,
96
+ })}
97
+ @click=${(e: Event) => {
98
+ e.stopPropagation();
99
+ this._dispatch("fw-sc-visibility-toggle", {
100
+ layerId: layer.id,
101
+ visible: !layer.visible,
102
+ });
103
+ }}
104
+ title=${layer.visible ? "Hide layer" : "Show layer"}
105
+ >
106
+ ${layer.visible ? eyeIcon(14) : eyeOffIcon(14)}
107
+ </button>
108
+ <span class="fw-sc-layer-icon">${this._getSourceIcon(layer.sourceId)}</span>
109
+ <span class="fw-sc-layer-name">${this._getSourceLabel(layer.sourceId)}</span>
110
+
111
+ ${this._editingLayerId === layer.id
112
+ ? html`
113
+ <div class="fw-sc-layer-opacity">
114
+ <input
115
+ type="range"
116
+ min="0"
117
+ max="1"
118
+ step="0.1"
119
+ .value=${String(layer.transform.opacity)}
120
+ @input=${(e: Event) =>
121
+ this._dispatch("fw-sc-transform-edit", {
122
+ layerId: layer.id,
123
+ transform: {
124
+ opacity: Number((e.target as HTMLInputElement).value),
125
+ },
126
+ })}
127
+ @click=${(e: Event) => e.stopPropagation()}
128
+ />
129
+ <span>${Math.round(layer.transform.opacity * 100)}%</span>
130
+ </div>
131
+ `
132
+ : nothing}
133
+
134
+ <div class="fw-sc-layer-controls">
135
+ <button
136
+ class="fw-sc-layer-btn"
137
+ @click=${(e: Event) => {
138
+ e.stopPropagation();
139
+ this._moveUp(layer.id);
140
+ }}
141
+ ?disabled=${index === 0}
142
+ title="Move up"
143
+ >
144
+
145
+ </button>
146
+ <button
147
+ class="fw-sc-layer-btn"
148
+ @click=${(e: Event) => {
149
+ e.stopPropagation();
150
+ this._moveDown(layer.id);
151
+ }}
152
+ ?disabled=${index === sorted.length - 1}
153
+ title="Move down"
154
+ >
155
+
156
+ </button>
157
+ <button
158
+ class=${classMap({
159
+ "fw-sc-layer-btn": true,
160
+ "fw-sc-layer-btn--active": this._editingLayerId === layer.id,
161
+ })}
162
+ @click=${(e: Event) => {
163
+ e.stopPropagation();
164
+ this._editingLayerId =
165
+ this._editingLayerId === layer.id ? null : layer.id;
166
+ }}
167
+ title="Edit opacity"
168
+ >
169
+
170
+ </button>
171
+ <button
172
+ class="fw-sc-layer-btn fw-sc-layer-btn--danger"
173
+ @click=${(e: Event) => {
174
+ e.stopPropagation();
175
+ this._dispatch("fw-sc-layer-remove", { layerId: layer.id });
176
+ }}
177
+ title="Remove layer"
178
+ >
179
+ ×
180
+ </button>
181
+ </div>
182
+ </div>
183
+ `
184
+ )}
185
+ </div>
186
+ </div>
187
+ `;
188
+ }
189
+
190
+ private _dispatch(name: string, detail: unknown) {
191
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
192
+ }
193
+
194
+ private _handleDragStart(e: DragEvent, layerId: string) {
195
+ this._draggedId = layerId;
196
+ e.dataTransfer!.effectAllowed = "move";
197
+ e.dataTransfer!.setData("text/plain", layerId);
198
+ }
199
+
200
+ private _handleDragOver(e: DragEvent, layerId: string) {
201
+ e.preventDefault();
202
+ e.dataTransfer!.dropEffect = "move";
203
+ this._dragOverId = layerId;
204
+ }
205
+
206
+ private _handleDrop(e: DragEvent, targetLayerId: string) {
207
+ e.preventDefault();
208
+ this._dragOverId = null;
209
+ if (!this._draggedId || this._draggedId === targetLayerId) {
210
+ this._draggedId = null;
211
+ return;
212
+ }
213
+ const sorted = this._sortedLayers;
214
+ const currentIds = sorted.map((l) => l.id);
215
+ const fromIndex = currentIds.indexOf(this._draggedId);
216
+ const toIndex = currentIds.indexOf(targetLayerId);
217
+ if (fromIndex === -1 || toIndex === -1) {
218
+ this._draggedId = null;
219
+ return;
220
+ }
221
+ const newOrder = [...currentIds];
222
+ newOrder.splice(fromIndex, 1);
223
+ newOrder.splice(toIndex, 0, this._draggedId);
224
+ this._dispatch("fw-sc-reorder", { layerIds: newOrder });
225
+ this._draggedId = null;
226
+ }
227
+
228
+ private _moveUp(layerId: string) {
229
+ const sorted = this._sortedLayers;
230
+ const ids = sorted.map((l) => l.id);
231
+ const idx = ids.indexOf(layerId);
232
+ if (idx <= 0) return;
233
+ [ids[idx - 1], ids[idx]] = [ids[idx], ids[idx - 1]];
234
+ this._dispatch("fw-sc-reorder", { layerIds: ids });
235
+ }
236
+
237
+ private _moveDown(layerId: string) {
238
+ const sorted = this._sortedLayers;
239
+ const ids = sorted.map((l) => l.id);
240
+ const idx = ids.indexOf(layerId);
241
+ if (idx >= ids.length - 1) return;
242
+ [ids[idx], ids[idx + 1]] = [ids[idx + 1], ids[idx]];
243
+ this._dispatch("fw-sc-reorder", { layerIds: ids });
244
+ }
245
+ }
246
+
247
+ declare global {
248
+ interface HTMLElementTagNameMap {
249
+ "fw-sc-layer-list": FwScLayerList;
250
+ }
251
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * <fw-sc-scene-switcher> — Horizontal scene selector with transition controls.
3
+ * Port of SceneSwitcher.tsx from streamcrafter-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 type {
11
+ Scene,
12
+ TransitionConfig,
13
+ TransitionType,
14
+ } from "@livepeer-frameworks/streamcrafter-core";
15
+
16
+ @customElement("fw-sc-scene-switcher")
17
+ export class FwScSceneSwitcher extends LitElement {
18
+ @property({ attribute: false }) scenes: Scene[] = [];
19
+ @property({ type: String, attribute: "active-scene-id" }) activeSceneId: string | null = null;
20
+ @property({ type: Boolean, attribute: "show-transition-controls" }) showTransitionControls = true;
21
+
22
+ @state() private _selectedTransition: TransitionType = "fade";
23
+ @state() private _transitionDuration = 500;
24
+ @state() private _isTransitioning = false;
25
+
26
+ static styles = [
27
+ sharedStyles,
28
+ utilityStyles,
29
+ css`
30
+ :host {
31
+ display: block;
32
+ }
33
+ `,
34
+ ];
35
+
36
+ protected render() {
37
+ return html`
38
+ <div class="fw-sc-scene-switcher">
39
+ <div class="fw-sc-scene-switcher-header">
40
+ <span class="fw-sc-scene-switcher-title">Scenes</span>
41
+ ${this.showTransitionControls
42
+ ? html`
43
+ <div class="fw-sc-transition-controls">
44
+ <select
45
+ class="fw-sc-transition-select"
46
+ .value=${this._selectedTransition}
47
+ @change=${(e: Event) => {
48
+ this._selectedTransition = (e.target as HTMLSelectElement)
49
+ .value as TransitionType;
50
+ }}
51
+ >
52
+ <option value="cut">Cut</option>
53
+ <option value="fade">Fade</option>
54
+ <option value="slide-left">Slide Left</option>
55
+ <option value="slide-right">Slide Right</option>
56
+ <option value="slide-up">Slide Up</option>
57
+ <option value="slide-down">Slide Down</option>
58
+ </select>
59
+ <input
60
+ type="number"
61
+ class="fw-sc-transition-duration"
62
+ .value=${String(this._transitionDuration)}
63
+ @change=${(e: Event) => {
64
+ this._transitionDuration = Number((e.target as HTMLInputElement).value);
65
+ }}
66
+ min="0"
67
+ max="3000"
68
+ step="100"
69
+ title="Transition duration (ms)"
70
+ />
71
+ <span class="fw-sc-transition-unit">ms</span>
72
+ </div>
73
+ `
74
+ : nothing}
75
+ </div>
76
+
77
+ <div class="fw-sc-scene-list">
78
+ ${this.scenes.map(
79
+ (scene) => html`
80
+ <div
81
+ class=${classMap({
82
+ "fw-sc-scene-item": true,
83
+ "fw-sc-scene-item--active": scene.id === this.activeSceneId,
84
+ "fw-sc-scene-item--transitioning": this._isTransitioning,
85
+ })}
86
+ @click=${() => this._handleSceneClick(scene.id)}
87
+ style="background-color:${scene.backgroundColor}"
88
+ >
89
+ <span class="fw-sc-scene-name">${scene.name}</span>
90
+ <span class="fw-sc-scene-layer-count">${scene.layers.length} layers</span>
91
+ ${this.scenes.length > 1 && scene.id !== this.activeSceneId
92
+ ? html`
93
+ <button
94
+ class="fw-sc-scene-delete"
95
+ @click=${(e: Event) => {
96
+ e.stopPropagation();
97
+ this._handleDelete(scene.id);
98
+ }}
99
+ title="Delete scene"
100
+ >
101
+ ×
102
+ </button>
103
+ `
104
+ : nothing}
105
+ </div>
106
+ `
107
+ )}
108
+
109
+ <button
110
+ class="fw-sc-scene-add"
111
+ @click=${() =>
112
+ this.dispatchEvent(
113
+ new CustomEvent("fw-sc-scene-create", { bubbles: true, composed: true })
114
+ )}
115
+ title="Create new scene"
116
+ >
117
+ +
118
+ </button>
119
+ </div>
120
+ </div>
121
+ `;
122
+ }
123
+
124
+ private async _handleSceneClick(sceneId: string) {
125
+ if (sceneId === this.activeSceneId || this._isTransitioning) return;
126
+ this._isTransitioning = true;
127
+ try {
128
+ this.dispatchEvent(
129
+ new CustomEvent("fw-sc-scene-select", {
130
+ detail: {
131
+ sceneId,
132
+ transition: {
133
+ type: this._selectedTransition,
134
+ durationMs: this._transitionDuration,
135
+ easing: "ease-in-out" as const,
136
+ } satisfies TransitionConfig,
137
+ },
138
+ bubbles: true,
139
+ composed: true,
140
+ })
141
+ );
142
+ } finally {
143
+ this._isTransitioning = false;
144
+ }
145
+ }
146
+
147
+ private _handleDelete(sceneId: string) {
148
+ if (this.scenes.length <= 1) return;
149
+ this.dispatchEvent(
150
+ new CustomEvent("fw-sc-scene-delete", {
151
+ detail: { sceneId },
152
+ bubbles: true,
153
+ composed: true,
154
+ })
155
+ );
156
+ }
157
+ }
158
+
159
+ declare global {
160
+ interface HTMLElementTagNameMap {
161
+ "fw-sc-scene-switcher": FwScSceneSwitcher;
162
+ }
163
+ }