@editframe/elements 0.26.4-beta.0 → 0.30.0-beta.14

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 (62) hide show
  1. package/dist/elements/EFSourceMixin.js +1 -1
  2. package/dist/elements/EFSourceMixin.js.map +1 -1
  3. package/dist/elements/EFSurface.d.ts +4 -4
  4. package/dist/elements/EFText.d.ts +52 -0
  5. package/dist/elements/EFText.js +319 -0
  6. package/dist/elements/EFText.js.map +1 -0
  7. package/dist/elements/EFTextSegment.d.ts +30 -0
  8. package/dist/elements/EFTextSegment.js +94 -0
  9. package/dist/elements/EFTextSegment.js.map +1 -0
  10. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  11. package/dist/elements/EFWaveform.d.ts +4 -4
  12. package/dist/elements/FetchMixin.js +22 -7
  13. package/dist/elements/FetchMixin.js.map +1 -1
  14. package/dist/elements/easingUtils.js +62 -0
  15. package/dist/elements/easingUtils.js.map +1 -0
  16. package/dist/elements/updateAnimations.js +57 -10
  17. package/dist/elements/updateAnimations.js.map +1 -1
  18. package/dist/gui/ContextMixin.js +11 -2
  19. package/dist/gui/ContextMixin.js.map +1 -1
  20. package/dist/gui/EFConfiguration.d.ts +4 -4
  21. package/dist/gui/EFControls.d.ts +2 -2
  22. package/dist/gui/EFDial.d.ts +4 -4
  23. package/dist/gui/EFDial.js +4 -2
  24. package/dist/gui/EFDial.js.map +1 -1
  25. package/dist/gui/EFFilmstrip.d.ts +32 -6
  26. package/dist/gui/EFFilmstrip.js +314 -50
  27. package/dist/gui/EFFilmstrip.js.map +1 -1
  28. package/dist/gui/EFFitScale.js +39 -15
  29. package/dist/gui/EFFitScale.js.map +1 -1
  30. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  31. package/dist/gui/EFPause.d.ts +4 -4
  32. package/dist/gui/EFPlay.d.ts +4 -4
  33. package/dist/gui/EFPreview.d.ts +4 -4
  34. package/dist/gui/EFPreview.js +2 -2
  35. package/dist/gui/EFPreview.js.map +1 -1
  36. package/dist/gui/EFResizableBox.d.ts +4 -4
  37. package/dist/gui/EFResizableBox.js +6 -3
  38. package/dist/gui/EFResizableBox.js.map +1 -1
  39. package/dist/gui/EFScrubber.d.ts +8 -5
  40. package/dist/gui/EFScrubber.js +64 -12
  41. package/dist/gui/EFScrubber.js.map +1 -1
  42. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  43. package/dist/gui/EFToggleLoop.d.ts +4 -4
  44. package/dist/gui/EFTogglePlay.d.ts +4 -4
  45. package/dist/gui/EFWorkbench.d.ts +4 -4
  46. package/dist/gui/EFWorkbench.js +16 -3
  47. package/dist/gui/EFWorkbench.js.map +1 -1
  48. package/dist/gui/TWMixin.js +1 -1
  49. package/dist/gui/TWMixin.js.map +1 -1
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/style.css +7 -120
  54. package/package.json +3 -3
  55. package/scripts/build-css.js +2 -6
  56. package/test/constants.ts +8 -0
  57. package/test/recordReplayProxyPlugin.js +76 -10
  58. package/test/setup.ts +32 -0
  59. package/test/useMSW.ts +3 -0
  60. package/test/useTranscodeMSW.ts +191 -0
  61. package/tsdown.config.ts +6 -4
  62. package/types.json +1 -1
@@ -21,7 +21,7 @@ function EFSourceMixin(superClass, options) {
21
21
  });
22
22
  }
23
23
  get apiHost() {
24
- return (this.closest("ef-configuration")?.apiHost ?? this.closest("ef-workbench")?.apiHost ?? this.closest("ef-preview")?.apiHost) || "https://editframe.dev";
24
+ return (this.closest("ef-configuration")?.apiHost ?? this.closest("ef-workbench")?.apiHost ?? this.closest("ef-preview")?.apiHost) || "https://editframe.com";
25
25
  }
26
26
  productionSrc() {
27
27
  if (!this.md5SumLoader.value) throw new Error(`MD5 sum not available for ${this}. Cannot generate production URL`);
@@ -1 +1 @@
1
- {"version":3,"file":"EFSourceMixin.js","names":[],"sources":["../../src/elements/EFSourceMixin.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport type { LitElement } from \"lit\";\nimport { property } from \"lit/decorators/property.js\";\n\nexport declare class EFSourceMixinInterface {\n apiHost?: string;\n productionSrc(): string;\n src: string;\n}\n\ninterface EFSourceMixinOptions {\n assetType: string;\n}\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function EFSourceMixin<T extends Constructor<LitElement>>(\n superClass: T,\n options: EFSourceMixinOptions,\n) {\n class EFSourceElement extends superClass {\n get apiHost() {\n const apiHost =\n this.closest(\"ef-configuration\")?.apiHost ??\n this.closest(\"ef-workbench\")?.apiHost ??\n this.closest(\"ef-preview\")?.apiHost;\n\n return apiHost || \"https://editframe.dev\";\n }\n\n @property({ type: String })\n src = \"\";\n\n productionSrc() {\n if (!this.md5SumLoader.value) {\n throw new Error(\n `MD5 sum not available for ${this}. Cannot generate production URL`,\n );\n }\n\n if (!this.apiHost) {\n throw new Error(\n `apiHost not available for ${this}. Cannot generate production URL`,\n );\n }\n\n return `${this.apiHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;\n }\n\n md5SumLoader = new Task(this, {\n autoRun: false,\n args: () => [this.src] as const,\n task: async ([src], { signal }) => {\n const md5Path = `/@ef-asset/${src}`;\n const response = await fetch(md5Path, { method: \"HEAD\", signal });\n return response.headers.get(\"etag\") ?? undefined;\n },\n });\n }\n\n return EFSourceElement as Constructor<EFSourceMixinInterface> & T;\n}\n"],"mappings":";;;;;AAcA,SAAgB,cACd,YACA,SACA;CACA,MAAM,wBAAwB,WAAW;;;cAWjC;uBAkBS,IAAI,KAAK,MAAM;IAC5B,SAAS;IACT,YAAY,CAAC,KAAK,IAAI;IACtB,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa;KACjC,MAAM,UAAU,cAAc;AAE9B,aADiB,MAAM,MAAM,SAAS;MAAE,QAAQ;MAAQ;MAAQ,CAAC,EACjD,QAAQ,IAAI,OAAO,IAAI;;IAE1C,CAAC;;EApCF,IAAI,UAAU;AAMZ,WAJE,KAAK,QAAQ,mBAAmB,EAAE,WAClC,KAAK,QAAQ,eAAe,EAAE,WAC9B,KAAK,QAAQ,aAAa,EAAE,YAEZ;;EAMpB,gBAAgB;AACd,OAAI,CAAC,KAAK,aAAa,MACrB,OAAM,IAAI,MACR,6BAA6B,KAAK,kCACnC;AAGH,OAAI,CAAC,KAAK,QACR,OAAM,IAAI,MACR,6BAA6B,KAAK,kCACnC;AAGH,UAAO,GAAG,KAAK,QAAQ,UAAU,QAAQ,UAAU,GAAG,KAAK,aAAa;;;aAhBzE,SAAS,EAAE,MAAM,QAAQ,CAAC;AA8B7B,QAAO"}
1
+ {"version":3,"file":"EFSourceMixin.js","names":[],"sources":["../../src/elements/EFSourceMixin.ts"],"sourcesContent":["import { Task } from \"@lit/task\";\nimport type { LitElement } from \"lit\";\nimport { property } from \"lit/decorators/property.js\";\n\nexport declare class EFSourceMixinInterface {\n apiHost?: string;\n productionSrc(): string;\n src: string;\n}\n\ninterface EFSourceMixinOptions {\n assetType: string;\n}\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function EFSourceMixin<T extends Constructor<LitElement>>(\n superClass: T,\n options: EFSourceMixinOptions,\n) {\n class EFSourceElement extends superClass {\n get apiHost() {\n const apiHost =\n this.closest(\"ef-configuration\")?.apiHost ??\n this.closest(\"ef-workbench\")?.apiHost ??\n this.closest(\"ef-preview\")?.apiHost;\n\n return apiHost || \"https://editframe.com\";\n }\n\n @property({ type: String })\n src = \"\";\n\n productionSrc() {\n if (!this.md5SumLoader.value) {\n throw new Error(\n `MD5 sum not available for ${this}. Cannot generate production URL`,\n );\n }\n\n if (!this.apiHost) {\n throw new Error(\n `apiHost not available for ${this}. Cannot generate production URL`,\n );\n }\n\n return `${this.apiHost}/api/v1/${options.assetType}/${this.md5SumLoader.value}`;\n }\n\n md5SumLoader = new Task(this, {\n autoRun: false,\n args: () => [this.src] as const,\n task: async ([src], { signal }) => {\n const md5Path = `/@ef-asset/${src}`;\n const response = await fetch(md5Path, { method: \"HEAD\", signal });\n return response.headers.get(\"etag\") ?? undefined;\n },\n });\n }\n\n return EFSourceElement as Constructor<EFSourceMixinInterface> & T;\n}\n"],"mappings":";;;;;AAcA,SAAgB,cACd,YACA,SACA;CACA,MAAM,wBAAwB,WAAW;;;cAWjC;uBAkBS,IAAI,KAAK,MAAM;IAC5B,SAAS;IACT,YAAY,CAAC,KAAK,IAAI;IACtB,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa;KACjC,MAAM,UAAU,cAAc;AAE9B,aADiB,MAAM,MAAM,SAAS;MAAE,QAAQ;MAAQ;MAAQ,CAAC,EACjD,QAAQ,IAAI,OAAO,IAAI;;IAE1C,CAAC;;EApCF,IAAI,UAAU;AAMZ,WAJE,KAAK,QAAQ,mBAAmB,EAAE,WAClC,KAAK,QAAQ,eAAe,EAAE,WAC9B,KAAK,QAAQ,aAAa,EAAE,YAEZ;;EAMpB,gBAAgB;AACd,OAAI,CAAC,KAAK,aAAa,MACrB,OAAM,IAAI,MACR,6BAA6B,KAAK,kCACnC;AAGH,OAAI,CAAC,KAAK,QACR,OAAM,IAAI,MACR,6BAA6B,KAAK,kCACnC;AAGH,UAAO,GAAG,KAAK,QAAQ,UAAU,QAAQ,UAAU,GAAG,KAAK,aAAa;;;aAhBzE,SAAS,EAAE,MAAM,QAAQ,CAAC;AA8B7B,QAAO"}
@@ -1,18 +1,18 @@
1
1
  import { ContextMixinInterface } from "../gui/ContextMixin.js";
2
2
  import { Task } from "@lit/task";
3
- import * as lit24 from "lit";
3
+ import * as lit26 from "lit";
4
4
  import { LitElement } from "lit";
5
- import * as lit_html21 from "lit-html";
5
+ import * as lit_html23 from "lit-html";
6
6
  import * as lit_html_directives_ref5 from "lit-html/directives/ref";
7
7
 
8
8
  //#region src/elements/EFSurface.d.ts
9
9
  declare class EFSurface extends LitElement {
10
10
  #private;
11
- static styles: lit24.CSSResult[];
11
+ static styles: lit26.CSSResult[];
12
12
  canvasRef: lit_html_directives_ref5.Ref<HTMLCanvasElement>;
13
13
  targetElement: ContextMixinInterface | null;
14
14
  target: string;
15
- render(): lit_html21.TemplateResult<1>;
15
+ render(): lit_html23.TemplateResult<1>;
16
16
  get rootTimegroup(): any;
17
17
  get currentTimeMs(): number;
18
18
  get durationMs(): number;
@@ -0,0 +1,52 @@
1
+ import { TemporalMixinInterface } from "./EFTemporal.js";
2
+ import { EFTextSegment } from "./EFTextSegment.js";
3
+ import * as lit8 from "lit";
4
+ import { LitElement, PropertyValueMap } from "lit";
5
+ import * as lit_html8 from "lit-html";
6
+
7
+ //#region src/elements/EFText.d.ts
8
+ type SplitMode = "line" | "word" | "char";
9
+ declare const EFText_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
10
+ declare class EFText extends EFText_base {
11
+ static styles: lit8.CSSResult[];
12
+ split: SplitMode;
13
+ private validateSplit;
14
+ staggerMs?: number;
15
+ private validateStagger;
16
+ easing: string;
17
+ private mutationObserver?;
18
+ private lastTextContent;
19
+ private _textContent;
20
+ private _templateElement;
21
+ private _segmentsReadyResolvers;
22
+ render(): lit_html8.TemplateResult<1>;
23
+ set textContent(value: string | null);
24
+ get textContent(): string;
25
+ /**
26
+ * Get all ef-text-segment elements directly
27
+ * @public
28
+ */
29
+ get segments(): EFTextSegment[];
30
+ /**
31
+ * Returns a promise that resolves when segments are ready (created and connected)
32
+ * Use this to wait for segments after setting textContent or other properties
33
+ * @public
34
+ */
35
+ whenSegmentsReady(): Promise<EFTextSegment[]>;
36
+ connectedCallback(): void;
37
+ disconnectedCallback(): void;
38
+ protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
39
+ private setupMutationObserver;
40
+ private getTextContent;
41
+ private splitText;
42
+ private splitTextIntoSegments;
43
+ get intrinsicDurationMs(): number | undefined;
44
+ }
45
+ declare global {
46
+ interface HTMLElementTagNameMap {
47
+ "ef-text": EFText;
48
+ }
49
+ }
50
+ //#endregion
51
+ export { EFText };
52
+ //# sourceMappingURL=EFText.d.ts.map
@@ -0,0 +1,319 @@
1
+ import { durationConverter } from "./durationConverter.js";
2
+ import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
3
+ import { EFTemporal } from "./EFTemporal.js";
4
+ import { updateAnimations } from "./updateAnimations.js";
5
+ import { evaluateEasing } from "./easingUtils.js";
6
+ import { LitElement, css, html } from "lit";
7
+ import { customElement, property } from "lit/decorators.js";
8
+
9
+ //#region src/elements/EFText.ts
10
+ let EFText = class EFText$1 extends EFTemporal(LitElement) {
11
+ constructor(..._args) {
12
+ super(..._args);
13
+ this.split = "word";
14
+ this.easing = "linear";
15
+ this.lastTextContent = "";
16
+ this._textContent = null;
17
+ this._templateElement = null;
18
+ this._segmentsReadyResolvers = [];
19
+ }
20
+ static {
21
+ this.styles = [css`
22
+ :host {
23
+ display: inline-flex;
24
+ white-space: normal;
25
+ line-height: 1;
26
+ gap: 0;
27
+ }
28
+ :host([split="char"]) {
29
+ white-space: pre;
30
+ }
31
+ :host([split="line"]) {
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+ ::slotted(*) {
36
+ display: inline-block;
37
+ margin: 0;
38
+ padding: 0;
39
+ }
40
+ `];
41
+ }
42
+ validateSplit(value) {
43
+ if (value === "line" || value === "word" || value === "char") return value;
44
+ console.warn(`Invalid split value "${value}". Must be "line", "word", or "char". Defaulting to "word".`);
45
+ return "word";
46
+ }
47
+ validateStagger(value) {
48
+ if (value === void 0) return void 0;
49
+ if (value < 0) {
50
+ console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);
51
+ return 0;
52
+ }
53
+ return value;
54
+ }
55
+ render() {
56
+ return html`<slot></slot>`;
57
+ }
58
+ set textContent(value) {
59
+ const newValue = value || "";
60
+ if (this._textContent !== newValue) {
61
+ this._textContent = newValue;
62
+ if (!this._templateElement && this.isConnected) this._templateElement = this.querySelector("template");
63
+ const textNodes = [];
64
+ for (const node of Array.from(this.childNodes)) if (node.nodeType === Node.TEXT_NODE) textNodes.push(node);
65
+ for (const node of textNodes) node.remove();
66
+ if (newValue) {
67
+ const textNode = document.createTextNode(newValue);
68
+ this.appendChild(textNode);
69
+ }
70
+ if (this.isConnected) this.splitText();
71
+ }
72
+ }
73
+ get textContent() {
74
+ if (this._textContent === null) return this.getTextContent();
75
+ return this._textContent;
76
+ }
77
+ /**
78
+ * Get all ef-text-segment elements directly
79
+ * @public
80
+ */
81
+ get segments() {
82
+ return Array.from(this.querySelectorAll("ef-text-segment[data-segment-created]"));
83
+ }
84
+ /**
85
+ * Returns a promise that resolves when segments are ready (created and connected)
86
+ * Use this to wait for segments after setting textContent or other properties
87
+ * @public
88
+ */
89
+ async whenSegmentsReady() {
90
+ await this.updateComplete;
91
+ const text = this._textContent !== null ? this._textContent : this.getTextContent();
92
+ if (!text || text.trim().length === 0) return [];
93
+ await new Promise((resolve) => requestAnimationFrame(resolve));
94
+ let segments = this.segments;
95
+ if (segments.length > 0) {
96
+ await Promise.all(segments.map((seg) => seg.updateComplete));
97
+ await new Promise((resolve) => requestAnimationFrame(resolve));
98
+ await new Promise((resolve) => requestAnimationFrame(resolve));
99
+ return this.segments;
100
+ }
101
+ return new Promise((resolve) => {
102
+ let attempts = 0;
103
+ const maxAttempts = 100;
104
+ const checkSegments = () => {
105
+ segments = this.segments;
106
+ if (segments.length > 0) Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {
107
+ requestAnimationFrame(() => {
108
+ requestAnimationFrame(() => {
109
+ resolve(this.segments);
110
+ });
111
+ });
112
+ });
113
+ else if (attempts < maxAttempts) {
114
+ attempts++;
115
+ requestAnimationFrame(checkSegments);
116
+ } else resolve([]);
117
+ };
118
+ checkSegments();
119
+ });
120
+ }
121
+ connectedCallback() {
122
+ super.connectedCallback();
123
+ this._templateElement = this.querySelector("template");
124
+ if (this._textContent === null) {
125
+ this._textContent = this.getTextContent();
126
+ this.lastTextContent = this._textContent;
127
+ }
128
+ requestAnimationFrame(() => {
129
+ this.setupMutationObserver();
130
+ this.splitText();
131
+ });
132
+ }
133
+ disconnectedCallback() {
134
+ super.disconnectedCallback();
135
+ this.mutationObserver?.disconnect();
136
+ }
137
+ updated(changedProperties) {
138
+ if (changedProperties.has("split") || changedProperties.has("staggerMs") || changedProperties.has("easing") || changedProperties.has("durationMs")) this.splitText();
139
+ }
140
+ setupMutationObserver() {
141
+ this.mutationObserver = new MutationObserver(() => {
142
+ const currentText = this._textContent || this.getTextContent();
143
+ if (currentText !== this.lastTextContent) {
144
+ this._textContent = currentText;
145
+ this.lastTextContent = currentText;
146
+ this.splitText();
147
+ }
148
+ });
149
+ this.mutationObserver.observe(this, {
150
+ childList: true,
151
+ characterData: true,
152
+ subtree: true
153
+ });
154
+ }
155
+ getTextContent() {
156
+ let text = "";
157
+ for (const node of Array.from(this.childNodes)) if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";
158
+ else if (node.nodeType === Node.ELEMENT_NODE) {
159
+ const element = node;
160
+ if (element.tagName === "EF-TEXT-SEGMENT") continue;
161
+ if (element.tagName === "TEMPLATE") continue;
162
+ text += element.textContent || "";
163
+ }
164
+ return text;
165
+ }
166
+ splitText() {
167
+ const validatedSplit = this.validateSplit(this.split);
168
+ if (validatedSplit !== this.split) {
169
+ this.split = validatedSplit;
170
+ return;
171
+ }
172
+ const validatedStagger = this.validateStagger(this.staggerMs);
173
+ if (validatedStagger !== this.staggerMs) {
174
+ this.staggerMs = validatedStagger;
175
+ return;
176
+ }
177
+ const text = this._textContent !== null ? this._textContent : this.getTextContent();
178
+ if (!text || text.trim().length === 0) {
179
+ const existingSegments = Array.from(this.querySelectorAll("ef-text-segment"));
180
+ for (const segment of existingSegments) segment.remove();
181
+ const textNodes = [];
182
+ for (const node of Array.from(this.childNodes)) if (node.nodeType === Node.TEXT_NODE) textNodes.push(node);
183
+ for (const node of textNodes) node.remove();
184
+ this.lastTextContent = "";
185
+ this._segmentsReadyResolvers.forEach((resolve) => {
186
+ resolve();
187
+ });
188
+ this._segmentsReadyResolvers = [];
189
+ return;
190
+ }
191
+ const segments = this.splitTextIntoSegments(text);
192
+ const durationMs = this.durationMs || 1e3;
193
+ const fragment = document.createDocumentFragment();
194
+ if (!this._templateElement) this._templateElement = this.querySelector("template");
195
+ const templateContent = this._templateElement?.content;
196
+ const templateSegments = templateContent ? Array.from(templateContent.querySelectorAll("ef-text-segment")) : [];
197
+ const useTemplate = templateSegments.length > 0;
198
+ const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;
199
+ segments.forEach((segmentText, textIndex) => {
200
+ let staggerOffset;
201
+ if (this.staggerMs !== void 0) {
202
+ const totalSegments = segments.length;
203
+ const normalizedProgress = totalSegments > 1 ? textIndex / (totalSegments - 1) : 0;
204
+ staggerOffset = evaluateEasing(this.easing, normalizedProgress) * ((totalSegments - 1) * this.staggerMs);
205
+ }
206
+ if (useTemplate && templateContent) {
207
+ const clonedContent = templateContent.cloneNode(true);
208
+ Array.from(clonedContent.querySelectorAll("ef-text-segment")).forEach((segment, templateIndex) => {
209
+ segment.segmentText = segmentText;
210
+ segment.segmentIndex = textIndex * segmentsPerTextSegment + templateIndex;
211
+ segment.segmentStartMs = 0;
212
+ segment.segmentEndMs = durationMs;
213
+ segment.staggerOffsetMs = staggerOffset ?? 0;
214
+ if (this.split === "line") segment.setAttribute("data-line-segment", "true");
215
+ segment.setAttribute("data-segment-created", "true");
216
+ fragment.appendChild(segment);
217
+ });
218
+ } else {
219
+ const segment = document.createElement("ef-text-segment");
220
+ segment.segmentText = segmentText;
221
+ segment.segmentIndex = textIndex;
222
+ segment.segmentStartMs = 0;
223
+ segment.segmentEndMs = durationMs;
224
+ segment.staggerOffsetMs = staggerOffset ?? 0;
225
+ if (this.split === "line") segment.setAttribute("data-line-segment", "true");
226
+ segment.setAttribute("data-segment-created", "true");
227
+ fragment.appendChild(segment);
228
+ }
229
+ });
230
+ const templateToPreserve = this._templateElement;
231
+ while (this.firstChild) {
232
+ const child = this.firstChild;
233
+ if (child === templateToPreserve) {
234
+ this.removeChild(child);
235
+ continue;
236
+ }
237
+ this.removeChild(child);
238
+ }
239
+ this.appendChild(fragment);
240
+ if (templateToPreserve) this.appendChild(templateToPreserve);
241
+ requestAnimationFrame(() => {
242
+ const segmentElements = this.segments;
243
+ Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {
244
+ requestAnimationFrame(() => {
245
+ const rootTimegroup = this.rootTimegroup;
246
+ if (rootTimegroup) updateAnimations(rootTimegroup);
247
+ else updateAnimations(this);
248
+ });
249
+ });
250
+ });
251
+ this.lastTextContent = text;
252
+ this._textContent = text;
253
+ requestAnimationFrame(() => {
254
+ this._segmentsReadyResolvers.forEach((resolve) => {
255
+ resolve();
256
+ });
257
+ this._segmentsReadyResolvers = [];
258
+ });
259
+ }
260
+ splitTextIntoSegments(text) {
261
+ switch (this.split) {
262
+ case "line": return text.split(/\r?\n/).filter((line) => line.length > 0);
263
+ case "word": {
264
+ const words = [];
265
+ const parts = text.split(/(\S+)/);
266
+ for (const part of parts) if (part.trim().length > 0) words.push(part);
267
+ else if (part.length > 0) {
268
+ if (words.length > 0) words[words.length - 1] += part;
269
+ }
270
+ return words.length > 0 ? words : [text];
271
+ }
272
+ case "char": return Array.from(text);
273
+ default: return [text];
274
+ }
275
+ }
276
+ get intrinsicDurationMs() {
277
+ if (this.hasExplicitDuration) return;
278
+ const text = this._textContent !== null ? this._textContent : this.getTextContent();
279
+ if (!text || text.trim().length === 0) return 0;
280
+ let segmentCount = 1;
281
+ switch (this.split) {
282
+ case "line":
283
+ segmentCount = text.split(/\r?\n/).filter((line) => line.trim().length > 0).length || 1;
284
+ break;
285
+ case "word": {
286
+ const words = [];
287
+ const parts = text.split(/(\S+)/);
288
+ for (const part of parts) if (part.trim().length > 0) words.push(part);
289
+ else if (part.length > 0) {
290
+ if (words.length > 0) words[words.length - 1] += part;
291
+ }
292
+ segmentCount = words.length > 0 ? words.length : 1;
293
+ break;
294
+ }
295
+ case "char":
296
+ segmentCount = text.length || 1;
297
+ break;
298
+ }
299
+ return segmentCount * 1e3;
300
+ }
301
+ };
302
+ __decorate([property({
303
+ type: String,
304
+ reflect: true
305
+ })], EFText.prototype, "split", void 0);
306
+ __decorate([property({
307
+ type: Number,
308
+ attribute: "stagger",
309
+ converter: durationConverter
310
+ })], EFText.prototype, "staggerMs", void 0);
311
+ __decorate([property({
312
+ type: String,
313
+ reflect: true
314
+ })], EFText.prototype, "easing", void 0);
315
+ EFText = __decorate([customElement("ef-text")], EFText);
316
+
317
+ //#endregion
318
+ export { EFText };
319
+ //# sourceMappingURL=EFText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","staggerOffset: number | undefined","words: string[]"],"sources":["../../src/elements/EFText.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport { evaluateEasing } from \"./easingUtils.js\";\nimport type { EFTextSegment } from \"./EFTextSegment.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\n\nexport type SplitMode = \"line\" | \"word\" | \"char\";\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-flex;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n :host([split=\"char\"]) {\n white-space: pre;\n }\n :host([split=\"line\"]) {\n display: flex;\n flex-direction: column;\n }\n ::slotted(*) {\n display: inline-block;\n margin: 0;\n padding: 0;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n split: SplitMode = \"word\";\n\n private validateSplit(value: string): SplitMode {\n if (value === \"line\" || value === \"word\" || value === \"char\") {\n return value as SplitMode;\n }\n console.warn(\n `Invalid split value \"${value}\". Must be \"line\", \"word\", or \"char\". Defaulting to \"word\".`,\n );\n return \"word\";\n }\n\n @property({\n type: Number,\n attribute: \"stagger\",\n converter: durationConverter,\n })\n staggerMs?: number;\n\n private validateStagger(value: number | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value < 0) {\n console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);\n return 0;\n }\n return value;\n }\n\n @property({ type: String, reflect: true })\n easing = \"linear\";\n\n private mutationObserver?: MutationObserver;\n private lastTextContent = \"\";\n private _textContent: string | null = null; // null means not initialized, \"\" means explicitly empty\n private _templateElement: HTMLTemplateElement | null = null;\n private _segmentsReadyResolvers: Array<() => void> = [];\n\n render() {\n return html`<slot></slot>`;\n }\n\n // Store text content so we can use it even after DOM is cleared\n set textContent(value: string | null) {\n const newValue = value || \"\";\n // Only update if value actually changed\n if (this._textContent !== newValue) {\n this._textContent = newValue;\n\n // Find template element if not already stored\n if (!this._templateElement && this.isConnected) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Clear any existing text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n // Add new text node if value is not empty\n if (newValue) {\n const textNode = document.createTextNode(newValue);\n this.appendChild(textNode);\n }\n // Trigger re-split\n if (this.isConnected) {\n this.splitText();\n }\n }\n }\n\n get textContent(): string {\n // If _textContent is null, it hasn't been initialized - read from DOM\n if (this._textContent === null) {\n return this.getTextContent();\n }\n // Otherwise use stored value (even if empty string)\n return this._textContent;\n }\n\n /**\n * Get all ef-text-segment elements directly\n * @public\n */\n get segments(): EFTextSegment[] {\n return Array.from(\n this.querySelectorAll(\"ef-text-segment[data-segment-created]\"),\n ) as EFTextSegment[];\n }\n\n /**\n * Returns a promise that resolves when segments are ready (created and connected)\n * Use this to wait for segments after setting textContent or other properties\n * @public\n */\n async whenSegmentsReady(): Promise<EFTextSegment[]> {\n // Wait for text element to be updated first\n await this.updateComplete;\n\n // If no text content, segments will be empty - return immediately\n // Use same logic as splitText to read text content\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return [];\n }\n\n // Wait a frame for splitText to run\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // If segments already exist and are connected, wait for updates\n let segments = this.segments;\n if (segments.length > 0) {\n // Wait for all segments to be updated\n await Promise.all(segments.map((seg) => seg.updateComplete));\n // Wait one more frame to ensure connectedCallback has run and properties are set\n await new Promise((resolve) => requestAnimationFrame(resolve));\n // Wait one more frame to ensure properties are fully processed\n await new Promise((resolve) => requestAnimationFrame(resolve));\n return this.segments;\n }\n\n // Otherwise, wait for segments to be created (with timeout)\n return new Promise<EFTextSegment[]>((resolve) => {\n let attempts = 0;\n const maxAttempts = 100; // 100 frames = ~1.6 seconds at 60fps\n\n const checkSegments = () => {\n segments = this.segments;\n if (segments.length > 0) {\n // Wait for all segments to be updated\n Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {\n // Wait frames to ensure connectedCallback has run and properties are set\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n resolve(this.segments);\n });\n });\n });\n } else if (attempts < maxAttempts) {\n attempts++;\n requestAnimationFrame(checkSegments);\n } else {\n // Timeout - return empty array (might be empty text)\n resolve([]);\n }\n };\n checkSegments();\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Find and store template element before any modifications\n this._templateElement = this.querySelector(\"template\");\n\n // Initialize _textContent from DOM if not already set (for declarative usage)\n if (this._textContent === null) {\n this._textContent = this.getTextContent();\n this.lastTextContent = this._textContent;\n }\n\n // Use requestAnimationFrame to ensure DOM is ready\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n if (\n changedProperties.has(\"split\") ||\n changedProperties.has(\"staggerMs\") ||\n changedProperties.has(\"easing\") ||\n changedProperties.has(\"durationMs\")\n ) {\n this.splitText();\n }\n }\n\n private setupMutationObserver() {\n this.mutationObserver = new MutationObserver(() => {\n // Only react to changes that aren't from our own segment creation\n const currentText = this._textContent || this.getTextContent();\n if (currentText !== this.lastTextContent) {\n this._textContent = currentText;\n this.lastTextContent = currentText;\n this.splitText();\n }\n });\n\n this.mutationObserver.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n private getTextContent(): string {\n // Get text content, handling both text nodes and HTML content\n let text = \"\";\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent || \"\";\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Skip ef-text-segment elements (they're created by us)\n if (element.tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n // Skip template elements (they're templates, not content)\n if (element.tagName === \"TEMPLATE\") {\n continue;\n }\n text += element.textContent || \"\";\n }\n }\n return text;\n }\n\n private splitText() {\n // Validate split mode\n const validatedSplit = this.validateSplit(this.split);\n if (validatedSplit !== this.split) {\n this.split = validatedSplit;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Validate stagger\n const validatedStagger = this.validateStagger(this.staggerMs);\n if (validatedStagger !== this.staggerMs) {\n this.staggerMs = validatedStagger;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Read text content - use stored _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n // Clear segments if no text\n const existingSegments = Array.from(\n this.querySelectorAll(\"ef-text-segment\"),\n );\n for (const segment of existingSegments) {\n segment.remove();\n }\n // Clear text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n this.lastTextContent = \"\";\n // Resolve any waiting promises\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n return;\n }\n\n const segments = this.splitTextIntoSegments(text);\n const durationMs = this.durationMs || 1000; // Default 1 second if no duration\n\n // Clear ALL child nodes (text nodes and segments) by replacing innerHTML\n // This ensures we don't have any leftover text nodes\n const fragment = document.createDocumentFragment();\n\n // Find template element if not already stored\n if (!this._templateElement) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Get template content structure\n // If template exists, clone it; otherwise create default ef-text-segment\n const templateContent = this._templateElement?.content;\n const templateSegments = templateContent\n ? Array.from(templateContent.querySelectorAll(\"ef-text-segment\"))\n : [];\n\n // If no template segments found, we'll create a default one\n const useTemplate = templateSegments.length > 0;\n const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;\n\n // Create new segments in a fragment first\n segments.forEach((segmentText, textIndex) => {\n // Calculate stagger offset if stagger is set\n let staggerOffset: number | undefined;\n if (this.staggerMs !== undefined) {\n // Apply easing to the stagger offset\n // Normalize index to 0-1 range (0 for first segment, 1 for last segment)\n const totalSegments = segments.length;\n const normalizedProgress =\n totalSegments > 1 ? textIndex / (totalSegments - 1) : 0;\n\n // Apply easing function to get eased progress\n const easedProgress = evaluateEasing(this.easing, normalizedProgress);\n\n // Calculate total stagger duration (last segment gets full stagger)\n const totalStaggerDuration = (totalSegments - 1) * this.staggerMs;\n\n // Apply eased progress to total stagger duration\n staggerOffset = easedProgress * totalStaggerDuration;\n }\n\n if (useTemplate && templateContent) {\n // Clone template content for each text segment\n // This allows multiple ef-text-segment elements per character/word/line\n const clonedContent = templateContent.cloneNode(\n true,\n ) as DocumentFragment;\n const clonedSegments = Array.from(\n clonedContent.querySelectorAll(\"ef-text-segment\"),\n ) as EFTextSegment[];\n\n clonedSegments.forEach((segment, templateIndex) => {\n // Set properties - Lit will process these when element is connected\n segment.segmentText = segmentText;\n // Calculate segment index accounting for multiple segments per text segment\n segment.segmentIndex =\n textIndex * segmentsPerTextSegment + templateIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n fragment.appendChild(segment);\n });\n } else {\n // No template - create default ef-text-segment\n const segment = document.createElement(\n \"ef-text-segment\",\n ) as EFTextSegment;\n\n segment.segmentText = segmentText;\n segment.segmentIndex = textIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n fragment.appendChild(segment);\n }\n });\n\n // Ensure segments are connected to DOM before checking for animations\n // Append fragment first, then trigger updates\n\n // Replace all children with the fragment (this clears text nodes and old segments)\n // But preserve the template element if it exists\n const templateToPreserve = this._templateElement;\n while (this.firstChild) {\n const child = this.firstChild;\n // Don't remove the template element\n if (child === templateToPreserve) {\n // Skip template, but we need to move it after the fragment\n // So we'll remove it temporarily and re-add it after\n this.removeChild(child);\n continue;\n }\n this.removeChild(child);\n }\n this.appendChild(fragment);\n // Re-add template element if it existed\n if (templateToPreserve) {\n this.appendChild(templateToPreserve);\n }\n\n // Segments will pause their own animations in connectedCallback\n // Lit will automatically update them when they're connected to the DOM\n // Ensure segments are updated after being connected\n requestAnimationFrame(() => {\n const segmentElements = this.segments;\n Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {\n // Wait an additional frame to ensure animations are paused in connectedCallback\n // Then trigger updateAnimations to set correct state\n // This ensures animations are positioned correctly on first load\n requestAnimationFrame(() => {\n const rootTimegroup = this.rootTimegroup;\n if (rootTimegroup) {\n updateAnimations(rootTimegroup);\n } else {\n updateAnimations(this);\n }\n });\n });\n });\n\n this.lastTextContent = text;\n this._textContent = text;\n\n // Resolve any waiting promises after segments are connected\n requestAnimationFrame(() => {\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n });\n }\n\n private splitTextIntoSegments(text: string): string[] {\n switch (this.split) {\n case \"line\": {\n // Split on newlines\n const lines = text.split(/\\r?\\n/);\n // Filter out empty lines but keep the structure\n return lines.filter((line) => line.length > 0);\n }\n case \"word\": {\n // Split on whitespace boundaries, preserving spaces after words\n const words: string[] = [];\n // Split by word boundaries, but keep spaces after words\n const parts = text.split(/(\\S+)/);\n for (const part of parts) {\n if (part.trim().length > 0) {\n // This is a word - check if there's a space after it in the original text\n words.push(part);\n } else if (part.length > 0) {\n // This is whitespace - attach it to the previous word if exists, otherwise skip\n if (words.length > 0) {\n words[words.length - 1] += part;\n }\n }\n }\n // If no words found, return original text\n return words.length > 0 ? words : [text];\n }\n case \"char\": {\n // Split every character, preserving whitespace\n return Array.from(text);\n }\n default:\n return [text];\n }\n }\n\n get intrinsicDurationMs(): number | undefined {\n // If explicit duration is set, use it\n if (this.hasExplicitDuration) {\n return undefined; // Let explicit duration take precedence\n }\n\n // Otherwise, calculate from content\n // Use _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return 0;\n }\n\n // Default to 1 second per segment (can be overridden with explicit duration)\n // Use the same splitting logic as splitTextIntoSegments\n let segmentCount = 1;\n switch (this.split) {\n case \"line\": {\n const lines = text\n .split(/\\r?\\n/)\n .filter((line) => line.trim().length > 0);\n segmentCount = lines.length || 1;\n break;\n }\n case \"word\": {\n // Use same logic as splitTextIntoSegments for consistency\n const words: string[] = [];\n const parts = text.split(/(\\S+)/);\n for (const part of parts) {\n if (part.trim().length > 0) {\n words.push(part);\n } else if (part.length > 0) {\n if (words.length > 0) {\n words[words.length - 1] += part;\n }\n }\n }\n segmentCount = words.length > 0 ? words.length : 1;\n break;\n }\n case \"char\": {\n segmentCount = text.length || 1;\n break;\n }\n }\n\n return segmentCount * 1000;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text\": EFText;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { TemporalMixinInterface } from "./EFTemporal.js";
2
+ import * as lit9 from "lit";
3
+ import { LitElement } from "lit";
4
+ import * as lit_html9 from "lit-html";
5
+
6
+ //#region src/elements/EFTextSegment.d.ts
7
+ declare const EFTextSegment_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
8
+ declare class EFTextSegment extends EFTextSegment_base {
9
+ static styles: lit9.CSSResult[];
10
+ render(): lit_html9.TemplateResult<1>;
11
+ private _animationsPaused;
12
+ connectedCallback(): void;
13
+ segmentText: string;
14
+ segmentIndex: number;
15
+ staggerOffsetMs?: number;
16
+ segmentStartMs: number;
17
+ segmentEndMs: number;
18
+ hidden: boolean;
19
+ get startTimeMs(): number;
20
+ get endTimeMs(): number;
21
+ get durationMs(): number;
22
+ }
23
+ declare global {
24
+ interface HTMLElementTagNameMap {
25
+ "ef-text-segment": EFTextSegment;
26
+ }
27
+ }
28
+ //#endregion
29
+ export { EFTextSegment };
30
+ //# sourceMappingURL=EFTextSegment.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
2
+ import { EFTemporal } from "./EFTemporal.js";
3
+ import { LitElement, css, html } from "lit";
4
+ import { customElement, property } from "lit/decorators.js";
5
+
6
+ //#region src/elements/EFTextSegment.ts
7
+ let EFTextSegment = class EFTextSegment$1 extends EFTemporal(LitElement) {
8
+ constructor(..._args) {
9
+ super(..._args);
10
+ this._animationsPaused = false;
11
+ this.segmentText = "";
12
+ this.segmentIndex = 0;
13
+ this.segmentStartMs = 0;
14
+ this.segmentEndMs = 0;
15
+ this.hidden = false;
16
+ }
17
+ static {
18
+ this.styles = [css`
19
+ :host {
20
+ display: inline-block;
21
+ white-space: pre;
22
+ line-height: 1;
23
+ }
24
+ :host([data-line-segment]) {
25
+ display: block;
26
+ white-space: normal;
27
+ }
28
+ :host([hidden]) {
29
+ opacity: 0;
30
+ pointer-events: none;
31
+ }
32
+ `];
33
+ }
34
+ render() {
35
+ const seedValue = this.segmentIndex * 9007 % 233 / 233;
36
+ this.style.setProperty("--ef-seed", seedValue.toString());
37
+ const offsetMs = this.staggerOffsetMs ?? 0;
38
+ this.style.setProperty("--ef-stagger-offset", `${offsetMs}ms`);
39
+ this.style.setProperty("--ef-index", this.segmentIndex.toString());
40
+ return html`${this.segmentText}`;
41
+ }
42
+ connectedCallback() {
43
+ super.connectedCallback();
44
+ if (!this._animationsPaused) this.updateComplete.then(() => {
45
+ requestAnimationFrame(() => {
46
+ const animations = this.getAnimations();
47
+ for (const animation of animations) if (animation.playState === "finished") {
48
+ animation.cancel();
49
+ animation.play();
50
+ animation.pause();
51
+ } else if (animation.playState === "running") animation.pause();
52
+ this._animationsPaused = true;
53
+ });
54
+ });
55
+ }
56
+ get startTimeMs() {
57
+ return (this.closest("ef-text")?.startTimeMs || 0) + (this.segmentStartMs || 0);
58
+ }
59
+ get endTimeMs() {
60
+ return (this.closest("ef-text")?.startTimeMs || 0) + (this.segmentEndMs || 0);
61
+ }
62
+ get durationMs() {
63
+ return this.segmentEndMs - this.segmentStartMs;
64
+ }
65
+ };
66
+ __decorate([property({
67
+ type: String,
68
+ attribute: false
69
+ })], EFTextSegment.prototype, "segmentText", void 0);
70
+ __decorate([property({
71
+ type: Number,
72
+ attribute: false
73
+ })], EFTextSegment.prototype, "segmentIndex", void 0);
74
+ __decorate([property({
75
+ type: Number,
76
+ attribute: false
77
+ })], EFTextSegment.prototype, "staggerOffsetMs", void 0);
78
+ __decorate([property({
79
+ type: Number,
80
+ attribute: false
81
+ })], EFTextSegment.prototype, "segmentStartMs", void 0);
82
+ __decorate([property({
83
+ type: Number,
84
+ attribute: false
85
+ })], EFTextSegment.prototype, "segmentEndMs", void 0);
86
+ __decorate([property({
87
+ type: Boolean,
88
+ reflect: true
89
+ })], EFTextSegment.prototype, "hidden", void 0);
90
+ EFTextSegment = __decorate([customElement("ef-text-segment")], EFTextSegment);
91
+
92
+ //#endregion
93
+ export { EFTextSegment };
94
+ //# sourceMappingURL=EFTextSegment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EFTextSegment.js","names":["EFTextSegment"],"sources":["../../src/elements/EFTextSegment.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport type { EFText } from \"./EFText.js\";\n\n@customElement(\"ef-text-segment\")\nexport class EFTextSegment extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-block;\n white-space: pre;\n line-height: 1;\n }\n :host([data-line-segment]) {\n display: block;\n white-space: normal;\n }\n :host([hidden]) {\n opacity: 0;\n pointer-events: none;\n }\n `,\n ];\n\n render() {\n // Set deterministic --ef-seed value based on segment index\n const seed = (this.segmentIndex * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-seed\", seedValue.toString());\n\n // Set stagger offset CSS variable\n // staggerOffsetMs is always set (defaults to 0), so we can always set the CSS variable\n const offsetMs = this.staggerOffsetMs ?? 0;\n this.style.setProperty(\"--ef-stagger-offset\", `${offsetMs}ms`);\n\n // Set index CSS variable\n this.style.setProperty(\"--ef-index\", this.segmentIndex.toString());\n\n return html`${this.segmentText}`;\n }\n\n private _animationsPaused = false;\n\n connectedCallback() {\n super.connectedCallback();\n // CRITICAL: Pause all animations once when segment is first connected\n // CSS animations start automatically and must be paused so updateAnimations can control them\n // Only do this once on initial load\n if (!this._animationsPaused) {\n // Wait for segment to be fully updated before pausing animations\n this.updateComplete.then(() => {\n requestAnimationFrame(() => {\n const animations = this.getAnimations();\n for (const animation of animations) {\n // Ensure animation is in a playable state\n // If it's finished, reset it\n if (animation.playState === \"finished\") {\n animation.cancel();\n animation.play();\n animation.pause();\n } else if (animation.playState === \"running\") {\n // Pause if running, preserving current visual state\n animation.pause();\n }\n // Don't reset currentTime here - let updateAnimations set it based on timeline\n // This preserves any visual state that was already applied\n }\n this._animationsPaused = true;\n // Note: updateAnimations is called from parent EFText after all segments are created\n // This avoids calling it multiple times (once per segment)\n });\n });\n }\n }\n\n @property({ type: String, attribute: false })\n segmentText = \"\";\n\n @property({ type: Number, attribute: false })\n segmentIndex = 0;\n\n @property({ type: Number, attribute: false })\n staggerOffsetMs?: number;\n\n @property({ type: Number, attribute: false })\n segmentStartMs = 0;\n\n @property({ type: Number, attribute: false })\n segmentEndMs = 0;\n\n @property({ type: Boolean, reflect: true })\n hidden = false;\n\n get startTimeMs() {\n // Get parent text element's absolute start time, then add our local offset\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentStartMs || 0);\n }\n\n get endTimeMs() {\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentEndMs || 0);\n }\n\n get durationMs(): number {\n return this.segmentEndMs - this.segmentStartMs;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text-segment\": EFTextSegment;\n }\n}\n"],"mappings":";;;;;;AAMO,0BAAMA,wBAAsB,WAAW,WAAW,CAAC;;;2BAoC5B;qBAmCd;sBAGC;wBAME;sBAGF;gBAGN;;;gBArFO,CACd,GAAG;;;;;;;;;;;;;;MAeJ;;CAED,SAAS;EAGP,MAAM,YADQ,KAAK,eAAe,OAAQ,MACjB;AACzB,OAAK,MAAM,YAAY,aAAa,UAAU,UAAU,CAAC;EAIzD,MAAM,WAAW,KAAK,mBAAmB;AACzC,OAAK,MAAM,YAAY,uBAAuB,GAAG,SAAS,IAAI;AAG9D,OAAK,MAAM,YAAY,cAAc,KAAK,aAAa,UAAU,CAAC;AAElE,SAAO,IAAI,GAAG,KAAK;;CAKrB,oBAAoB;AAClB,QAAM,mBAAmB;AAIzB,MAAI,CAAC,KAAK,kBAER,MAAK,eAAe,WAAW;AAC7B,+BAA4B;IAC1B,MAAM,aAAa,KAAK,eAAe;AACvC,SAAK,MAAM,aAAa,WAGtB,KAAI,UAAU,cAAc,YAAY;AACtC,eAAU,QAAQ;AAClB,eAAU,MAAM;AAChB,eAAU,OAAO;eACR,UAAU,cAAc,UAEjC,WAAU,OAAO;AAKrB,SAAK,oBAAoB;KAGzB;IACF;;CAsBN,IAAI,cAAc;AAIhB,UAFmB,KAAK,QAAQ,UAAU,EACN,eAAe,MACzB,KAAK,kBAAkB;;CAGnD,IAAI,YAAY;AAGd,UAFmB,KAAK,QAAQ,UAAU,EACN,eAAe,MACzB,KAAK,gBAAgB;;CAGjD,IAAI,aAAqB;AACvB,SAAO,KAAK,eAAe,KAAK;;;YAhCjC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAS,SAAS;CAAM,CAAC;4BAtF5C,cAAc,kBAAkB"}
@@ -1,12 +1,12 @@
1
1
  import { EFVideo } from "./EFVideo.js";
2
- import * as lit25 from "lit";
2
+ import * as lit27 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html22 from "lit-html";
4
+ import * as lit_html24 from "lit-html";
5
5
  import * as lit_html_directives_ref6 from "lit-html/directives/ref";
6
6
 
7
7
  //#region src/elements/EFThumbnailStrip.d.ts
8
8
  declare class EFThumbnailStrip extends LitElement {
9
- static styles: lit25.CSSResult[];
9
+ static styles: lit27.CSSResult[];
10
10
  canvasRef: lit_html_directives_ref6.Ref<HTMLCanvasElement>;
11
11
  private _targetController;
12
12
  private _targetElement;
@@ -82,7 +82,7 @@ declare class EFThumbnailStrip extends LitElement {
82
82
  * Convert Canvas to ImageData for caching
83
83
  */
84
84
  private canvasToImageData;
85
- render(): lit_html22.TemplateResult<1>;
85
+ render(): lit_html24.TemplateResult<1>;
86
86
  }
87
87
  declare global {
88
88
  interface HTMLElementTagNameMap {