@editframe/elements 0.8.0-beta.1 → 0.8.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,7 @@ export declare class EFMedia extends EFMedia_base {
29
29
  }[] | undefined], Record<string, {
30
30
  segment: TrackSegment;
31
31
  track: MP4Box.TrackInfo;
32
+ nextSegment?: TrackSegment;
32
33
  }> | undefined>;
33
34
  fetchSeekTask: Task<readonly [{
34
35
  trackId: string;
@@ -37,6 +38,7 @@ export declare class EFMedia extends EFMedia_base {
37
38
  }[] | undefined, Record<string, {
38
39
  segment: TrackSegment;
39
40
  track: MP4Box.TrackInfo;
41
+ nextSegment?: TrackSegment;
40
42
  }> | undefined, typeof fetch], Record<string, File> | undefined>;
41
43
  videoAssetTask: Task<readonly [Record<string, File> | undefined], VideoAsset | undefined>;
42
44
  desiredSeekTimeMs: number;
@@ -48,6 +50,7 @@ export declare class EFMedia extends EFMedia_base {
48
50
  audioBufferTask: Task<readonly [Record<string, File> | undefined, Record<string, {
49
51
  segment: TrackSegment;
50
52
  track: MP4Box.TrackInfo;
53
+ nextSegment?: TrackSegment;
51
54
  }> | undefined], {
52
55
  buffer: AudioBuffer;
53
56
  startOffsetMs: number;
@@ -7,11 +7,14 @@ export declare const timegroupContext: {
7
7
  };
8
8
  export declare class TemporalMixinInterface {
9
9
  get hasOwnDuration(): boolean;
10
+ get trimStartMs(): number;
11
+ get trimEndMs(): number;
10
12
  get durationMs(): number;
11
13
  get startTimeMs(): number;
12
14
  get startTimeWithinParentMs(): number;
13
15
  get endTimeMs(): number;
14
16
  get ownCurrentTimeMs(): number;
17
+ get trimAdjustedOwnCurrentTimeMs(): number;
15
18
  set duration(value: string);
16
19
  get duration(): string;
17
20
  parentTimegroup?: EFTimegroup;
@@ -8,17 +8,15 @@ export declare class EFTimegroup extends EFTimegroup_base {
8
8
  static styles: import('lit').CSSResult;
9
9
  _timeGroupContext: this;
10
10
  mode: "fixed" | "sequence" | "contain";
11
+ overlapMs: number;
11
12
  set currentTime(time: number);
12
13
  get currentTime(): number;
13
14
  get currentTimeMs(): number;
14
15
  set currentTimeMs(ms: number);
15
- crossoverMs: number;
16
16
  render(): import('lit-html').TemplateResult<1>;
17
17
  maybeLoadTimeFromLocalStorage(): number;
18
18
  connectedCallback(): void;
19
19
  get storageKey(): string;
20
- get crossoverStartMs(): number;
21
- get crossoverEndMs(): number;
22
20
  get durationMs(): number;
23
21
  waitForMediaDurations(): Promise<(Record<number, import('packages/assets/dist/Probe.js').TrackFragmentIndex> | undefined)[]>;
24
22
  get childTemporals(): import('./EFTemporal.ts').TemporalMixinInterface[];
@@ -2,3 +2,11 @@ export declare const durationConverter: {
2
2
  fromAttribute: (value: string) => number;
3
3
  toAttribute: (value: number) => string;
4
4
  };
5
+ export declare const trimDurationConverter: {
6
+ fromAttribute: (value: string) => number;
7
+ toAttribute: (value: number) => string;
8
+ };
9
+ export declare const imageDurationConverter: {
10
+ fromAttribute: (value: string) => number;
11
+ toAttribute: (value: number) => string;
12
+ };
@@ -120,9 +120,9 @@ let EFCaptions = class extends EFSourceMixin(
120
120
  let startMs = 0;
121
121
  let endMs = 0;
122
122
  for (const segment of caption.segments) {
123
- if (this.targetElement.ownCurrentTimeMs >= segment.start * 1e3 && this.targetElement.ownCurrentTimeMs <= segment.end * 1e3) {
123
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs >= segment.start * 1e3 && this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1e3) {
124
124
  for (const word of segment.words) {
125
- if (this.targetElement.ownCurrentTimeMs >= word.start * 1e3 && this.targetElement.ownCurrentTimeMs <= word.end * 1e3) {
125
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs >= word.start * 1e3 && this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1e3) {
126
126
  words.push(word.text);
127
127
  startMs = word.start * 1e3;
128
128
  endMs = word.end * 1e3;
@@ -101,10 +101,13 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
101
101
  const segment = index.segments.toReversed().find((segment2) => {
102
102
  return segment2.dts / track.timescale * 1e3 <= seekToMs;
103
103
  });
104
+ const nextSegment = index.segments.find((segment2) => {
105
+ return segment2.dts / track.timescale * 1e3 > seekToMs;
106
+ });
104
107
  if (!segment) {
105
108
  return;
106
109
  }
107
- result[index.track] = { segment, track };
110
+ result[index.track] = { segment, track, nextSegment };
108
111
  }
109
112
  return result;
110
113
  }
@@ -121,13 +124,27 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
121
124
  return;
122
125
  }
123
126
  const files = {};
124
- for (const [trackId, { segment, track }] of Object.entries(seekResult)) {
127
+ for (const [trackId, { segment, track, nextSegment }] of Object.entries(
128
+ seekResult
129
+ )) {
125
130
  const start = segment.offset;
126
131
  const end = segment.offset + segment.size;
127
132
  const response = await fetch(this.fragmentTrackPath(trackId), {
128
133
  signal,
129
134
  headers: { Range: `bytes=${start}-${end}` }
130
135
  });
136
+ if (nextSegment) {
137
+ const nextStart = nextSegment.offset;
138
+ const nextEnd = nextSegment.offset + nextSegment.size;
139
+ fetch(this.fragmentTrackPath(trackId), {
140
+ signal,
141
+ headers: { Range: `bytes=${nextStart}-${nextEnd}` }
142
+ }).then(() => {
143
+ log("Prefetched next segment");
144
+ }).catch((error) => {
145
+ log("Failed to prefetch next segment", error);
146
+ });
147
+ }
131
148
  const initSegment = Object.values(initSegments).find(
132
149
  (initSegment2) => initSegment2.trackId === String(track.id)
133
150
  );
@@ -238,7 +255,7 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
238
255
  }
239
256
  updated(changedProperties) {
240
257
  if (changedProperties.has("ownCurrentTimeMs")) {
241
- this.executeSeek(this.ownCurrentTimeMs);
258
+ this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
242
259
  }
243
260
  }
244
261
  get hasOwnDuration() {
@@ -256,15 +273,15 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
256
273
  if (durations.length === 0) {
257
274
  return 0;
258
275
  }
259
- return Math.max(...durations);
276
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
260
277
  }
261
278
  get startTimeMs() {
262
279
  return getStartTimeMs(this);
263
280
  }
264
281
  #audioContext;
265
282
  async fetchAudioSpanningTime(fromMs, toMs) {
266
- fromMs -= this.startTimeMs;
267
- toMs -= this.startTimeMs;
283
+ fromMs -= this.startTimeMs - this.trimStartMs;
284
+ toMs -= this.startTimeMs - this.trimStartMs;
268
285
  await this.trackFragmentIndexLoader.taskComplete;
269
286
  const audioTrackId = this.defaultAudioTrackId;
270
287
  if (!audioTrackId) {
@@ -284,6 +301,7 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
284
301
  headers: { Range: `bytes=${start}-${end}` }
285
302
  }
286
303
  );
304
+ console.log({ fromMs, toMs });
287
305
  const fragments = Object.values(audioTrackIndex.segments).filter(
288
306
  (segment) => {
289
307
  const segmentStartsBeforeEnd = segment.dts <= toMs * audioTrackIndex.timescale / 1e3;
@@ -318,8 +336,8 @@ class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
318
336
  });
319
337
  return {
320
338
  blob: audioBlob,
321
- startMs: firstFragment.dts / audioTrackIndex.timescale * 1e3,
322
- endMs: lastFragment.dts / audioTrackIndex.timescale * 1e3 + lastFragment.duration / audioTrackIndex.timescale * 1e3
339
+ startMs: firstFragment.dts / audioTrackIndex.timescale * 1e3 - this.trimStartMs,
340
+ endMs: lastFragment.dts / audioTrackIndex.timescale * 1e3 + lastFragment.duration / audioTrackIndex.timescale * 1e3 - this.trimEndMs
323
341
  };
324
342
  }
325
343
  }
@@ -85,6 +85,9 @@ const EFTemporal = (superClass) => {
85
85
  constructor() {
86
86
  super(...arguments);
87
87
  this._offsetMs = 0;
88
+ this._trimStartMs = 0;
89
+ this._trimEndMs = 0;
90
+ this._startOffsetMs = 0;
88
91
  this.rootTimegroup = this.getRootTimegroup();
89
92
  this.frameTask = new Task(this, {
90
93
  autoRun: EF_INTERACTIVE,
@@ -112,6 +115,15 @@ const EFTemporal = (superClass) => {
112
115
  get parentTimegroup() {
113
116
  return this.#parentTimegroup;
114
117
  }
118
+ get trimStartMs() {
119
+ return this._trimStartMs;
120
+ }
121
+ get trimEndMs() {
122
+ return this._trimEndMs;
123
+ }
124
+ get startOffsetMs() {
125
+ return this._startOffsetMs;
126
+ }
115
127
  getRootTimegroup() {
116
128
  let parent = this.tagName === "EF-TIMEGROUP" ? this : this.parentTimegroup;
117
129
  while (parent?.parentTimegroup) {
@@ -169,9 +181,9 @@ const EFTemporal = (superClass) => {
169
181
  }
170
182
  startTimeMsCache.set(
171
183
  this,
172
- previous.startTimeMs + previous.durationMs
184
+ previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs
173
185
  );
174
- return previous.startTimeMs + previous.durationMs;
186
+ return previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs;
175
187
  }
176
188
  case "contain":
177
189
  case "fixed":
@@ -196,6 +208,22 @@ const EFTemporal = (superClass) => {
196
208
  }
197
209
  return 0;
198
210
  }
211
+ /**
212
+ * Used to calculate the internal currentTimeMs of the element. This is useful
213
+ * for mapping to internal media time codes for audio/video elements.
214
+ */
215
+ get trimAdjustedOwnCurrentTimeMs() {
216
+ if (this.rootTimegroup) {
217
+ return Math.min(
218
+ Math.max(
219
+ 0,
220
+ this.rootTimegroup.currentTimeMs - this.startTimeMs + this.trimStartMs
221
+ ),
222
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs
223
+ );
224
+ }
225
+ return 0;
226
+ }
199
227
  }
200
228
  __decorateClass([
201
229
  consume({ context: timegroupContext, subscribe: true }),
@@ -215,6 +243,27 @@ const EFTemporal = (superClass) => {
215
243
  converter: durationConverter
216
244
  })
217
245
  ], TemporalMixinClass.prototype, "_durationMs", 2);
246
+ __decorateClass([
247
+ property({
248
+ type: Number,
249
+ attribute: "trimstart",
250
+ converter: durationConverter
251
+ })
252
+ ], TemporalMixinClass.prototype, "_trimStartMs", 2);
253
+ __decorateClass([
254
+ property({
255
+ type: Number,
256
+ attribute: "trimend",
257
+ converter: durationConverter
258
+ })
259
+ ], TemporalMixinClass.prototype, "_trimEndMs", 2);
260
+ __decorateClass([
261
+ property({
262
+ type: Number,
263
+ attribute: "startoffset",
264
+ converter: durationConverter
265
+ })
266
+ ], TemporalMixinClass.prototype, "_startOffsetMs", 2);
218
267
  __decorateClass([
219
268
  state()
220
269
  ], TemporalMixinClass.prototype, "rootTimegroup", 2);
@@ -7,6 +7,7 @@ import { EFTemporal, shallowGetTemporalElements, isEFTemporal, timegroupContext
7
7
  import { TimegroupController } from "./TimegroupController.js";
8
8
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
9
9
  import { deepGetMediaElements } from "./EFMedia.js";
10
+ import { durationConverter } from "./durationConverter.js";
10
11
  var __defProp = Object.defineProperty;
11
12
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
13
  var __typeError = (msg) => {
@@ -44,7 +45,7 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
44
45
  this._timeGroupContext = this;
45
46
  __privateAdd(this, _currentTime, 0);
46
47
  this.mode = "sequence";
47
- this.crossoverMs = 0;
48
+ this.overlapMs = 0;
48
49
  this.frameTask = new Task(this, {
49
50
  autoRun: EF_INTERACTIVE,
50
51
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
@@ -110,29 +111,18 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
110
111
  }
111
112
  return `ef-timegroup-${this.id}`;
112
113
  }
113
- get crossoverStartMs() {
114
- const parentTimeGroup = this.parentTimegroup;
115
- if (!parentTimeGroup || !this.previousElementSibling) {
116
- return 0;
117
- }
118
- return parentTimeGroup.crossoverMs;
119
- }
120
- get crossoverEndMs() {
121
- const parentTimeGroup = this.parentTimegroup;
122
- if (!parentTimeGroup || !this.nextElementSibling) {
123
- return 0;
124
- }
125
- return parentTimeGroup.crossoverMs;
126
- }
127
114
  get durationMs() {
128
115
  switch (this.mode) {
129
116
  case "fixed":
130
117
  return super.durationMs;
131
118
  case "sequence": {
132
119
  let duration = 0;
133
- for (const node of this.childTemporals) {
120
+ this.childTemporals.forEach((node, index) => {
121
+ if (index > 0) {
122
+ duration -= this.overlapMs;
123
+ }
134
124
  duration += node.durationMs;
135
- }
125
+ });
136
126
  return duration;
137
127
  }
138
128
  case "contain": {
@@ -168,9 +158,14 @@ let EFTimegroup = class extends EFTemporal(LitElement) {
168
158
  }
169
159
  this.style.display = "";
170
160
  const animations = this.getAnimations({ subtree: true });
161
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
162
+ this.style.setProperty(
163
+ "--ef-transition--duration",
164
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`
165
+ );
171
166
  this.style.setProperty(
172
- "--ef-duration",
173
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`
167
+ "--ef-transition-out-start",
168
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`
174
169
  );
175
170
  for (const animation of animations) {
176
171
  if (animation.playState === "running") {
@@ -311,7 +306,7 @@ EFTimegroup.styles = css`
311
306
  display: block;
312
307
  width: 100%;
313
308
  height: 100%;
314
- position: relative;
309
+ position: absolute;
315
310
  top: 0;
316
311
  }
317
312
  `;
@@ -324,28 +319,16 @@ __decorateClass([
324
319
  attribute: "mode"
325
320
  })
326
321
  ], EFTimegroup.prototype, "mode", 2);
327
- __decorateClass([
328
- property({ type: Number })
329
- ], EFTimegroup.prototype, "currentTime", 1);
330
322
  __decorateClass([
331
323
  property({
332
- attribute: "crossover",
333
- converter: {
334
- fromAttribute: (value) => {
335
- if (value.endsWith("ms")) {
336
- return Number.parseFloat(value);
337
- }
338
- if (value.endsWith("s")) {
339
- return Number.parseFloat(value) * 1e3;
340
- }
341
- throw new Error(
342
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)"
343
- );
344
- },
345
- toAttribute: (value) => `${value}ms`
346
- }
324
+ type: Number,
325
+ converter: durationConverter,
326
+ attribute: "overlap"
347
327
  })
348
- ], EFTimegroup.prototype, "crossoverMs", 2);
328
+ ], EFTimegroup.prototype, "overlapMs", 2);
329
+ __decorateClass([
330
+ property({ type: Number })
331
+ ], EFTimegroup.prototype, "currentTime", 1);
349
332
  EFTimegroup = __decorateClass([
350
333
  customElement("ef-timegroup")
351
334
  ], EFTimegroup);
@@ -148,7 +148,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
148
148
  if (!this.targetElement.audioBufferTask.value) {
149
149
  return;
150
150
  }
151
- if (this.targetElement.ownCurrentTimeMs > 0) {
151
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
152
152
  const audioContext = new OfflineAudioContext(2, 48e3 / 25, 48e3);
153
153
  const audioBufferSource = audioContext.createBufferSource();
154
154
  audioBufferSource.buffer = this.targetElement.audioBufferTask.value.buffer;
@@ -159,7 +159,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
159
159
  0,
160
160
  Math.max(
161
161
  0,
162
- (this.targetElement.ownCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3
162
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs - this.targetElement.audioBufferTask.value.startOffsetMs) / 1e3
163
163
  ),
164
164
  48e3 / 1e3
165
165
  );
@@ -1,4 +1,5 @@
1
1
  const parseTimeToMs = (time) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -32,25 +32,27 @@ function ContextMixin(superClass) {
32
32
  });
33
33
  }
34
34
  if (this.signingURL) {
35
- if (!this.#signedURLs[url]) {
36
- this.#signedURLs[url] = fetch(this.signingURL, {
35
+ if (!this.#URLTokens[url]) {
36
+ this.#URLTokens[url] = fetch(this.signingURL, {
37
37
  method: "POST",
38
38
  body: JSON.stringify({ url })
39
39
  }).then(async (response) => {
40
40
  if (response.ok) {
41
- return (await response.json()).url;
41
+ return (await response.json()).token;
42
42
  }
43
43
  throw new Error(
44
44
  `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`
45
45
  );
46
46
  });
47
47
  }
48
- const signedURL = await this.#signedURLs[url];
49
- return fetch(signedURL, init);
48
+ const urlToken = await this.#URLTokens[url];
49
+ Object.assign(init.headers, {
50
+ authorization: `Bearer ${urlToken}`
51
+ });
50
52
  }
51
53
  return fetch(url, init);
52
54
  };
53
- this.#signedURLs = {};
55
+ this.#URLTokens = {};
54
56
  this.apiHost = "";
55
57
  this.playing = false;
56
58
  this.loop = false;
@@ -93,7 +95,7 @@ function ContextMixin(superClass) {
93
95
  this.#playbackAnimationFrameRequest = null;
94
96
  this.#AUDIO_PLAYBACK_SLICE_MS = 1e3;
95
97
  }
96
- #signedURLs;
98
+ #URLTokens;
97
99
  connectedCallback() {
98
100
  super.connectedCallback();
99
101
  requestAnimationFrame(this.setStageScale);
@@ -73,17 +73,23 @@ class FilmstripItem extends TWMixin(LitElement) {
73
73
  get isFocused() {
74
74
  return this.element && this.focusContext?.focusedElement === this.element;
75
75
  }
76
- get styles() {
76
+ get gutterStyles() {
77
77
  return {
78
78
  position: "relative",
79
- left: `${this.pixelsPerMs * this.element.startTimeWithinParentMs}px`,
80
- width: `${this.pixelsPerMs * this.element.durationMs}px`
79
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
80
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`
81
+ };
82
+ }
83
+ get trimPortionStyles() {
84
+ return {
85
+ width: `${this.pixelsPerMs * this.element.durationMs}px`,
86
+ left: `${this.pixelsPerMs * this.element.trimStartMs}px`
81
87
  };
82
88
  }
83
89
  render() {
84
- return html` <div class="" style=${styleMap(this.styles)}>
90
+ return html`<div style=${styleMap(this.gutterStyles)}>
85
91
  <div
86
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 hover:bg-blue-400 text-sm data-[focused]:bg-slate-400"
92
+ class="bg-slate-300"
87
93
  ?data-focused=${this.isFocused}
88
94
  @mouseenter=${() => {
89
95
  if (this.focusContext) {
@@ -96,7 +102,13 @@ class FilmstripItem extends TWMixin(LitElement) {
96
102
  }
97
103
  }}
98
104
  >
99
- ${this.animations()}
105
+ <div
106
+ ?data-focused=${this.isFocused}
107
+ class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400"
108
+ style=${styleMap(this.trimPortionStyles)}
109
+ >
110
+ ${this.animations()}
111
+ </div>
100
112
  </div>
101
113
  ${this.renderChildren()}
102
114
  </div>`;
@@ -74,7 +74,6 @@ let EFWorkbench = class extends ContextMixin(TWMixin(LitElement)) {
74
74
  >
75
75
  <slot
76
76
  ${ref(this.canvasRef)}
77
- class="inline-block"
78
77
  name="canvas"
79
78
  ></slot>
80
79
  <div
@@ -1,4 +1,4 @@
1
- const twStyle = "/*\n! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com\n*//*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n\n::before,\n::after {\n --tw-content: '';\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\n\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n -o-tab-size: 4;\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n letter-spacing: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\ninput:where([type='button']),\ninput:where([type='reset']),\ninput:where([type='submit']) {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/*\nReset default styling for dialogs.\n*/\ndialog {\n padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::-moz-placeholder, textarea::-moz-placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n:disabled {\n cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/* Make elements with the HTML hidden attribute stay hidden by default */\n[hidden] {\n display: none;\n}\n\n*, ::before, ::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n\n::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n.container {\n width: 100%;\n}\n@media (min-width: 640px) {\n\n .container {\n max-width: 640px;\n }\n}\n@media (min-width: 768px) {\n\n .container {\n max-width: 768px;\n }\n}\n@media (min-width: 1024px) {\n\n .container {\n max-width: 1024px;\n }\n}\n@media (min-width: 1280px) {\n\n .container {\n max-width: 1280px;\n }\n}\n@media (min-width: 1536px) {\n\n .container {\n max-width: 1536px;\n }\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.static {\n position: static;\n}\n.fixed {\n position: fixed;\n}\n.absolute {\n position: absolute;\n}\n.relative {\n position: relative;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-0 {\n top: 0px;\n}\n.z-10 {\n z-index: 10;\n}\n.z-20 {\n z-index: 20;\n}\n.col-span-2 {\n grid-column: span 2 / span 2;\n}\n.mb-\\[1px\\] {\n margin-bottom: 1px;\n}\n.block {\n display: block;\n}\n.inline-block {\n display: inline-block;\n}\n.inline {\n display: inline;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.contents {\n display: contents;\n}\n.hidden {\n display: none;\n}\n.h-\\[1\\.1rem\\] {\n height: 1.1rem;\n}\n.h-\\[5px\\] {\n height: 5px;\n}\n.h-full {\n height: 100%;\n}\n.w-1 {\n width: 0.25rem;\n}\n.w-\\[2px\\] {\n width: 2px;\n}\n.w-full {\n width: 100%;\n}\n.transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.cursor-crosshair {\n cursor: crosshair;\n}\n.resize {\n resize: both;\n}\n.place-content-center {\n place-content: center;\n}\n.place-items-center {\n place-items: center;\n}\n.items-center {\n align-items: center;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.text-nowrap {\n text-wrap: nowrap;\n}\n.border {\n border-width: 1px;\n}\n.border-r-2 {\n border-right-width: 2px;\n}\n.border-blue-500 {\n --tw-border-opacity: 1;\n border-color: rgb(59 130 246 / var(--tw-border-opacity));\n}\n.border-red-700 {\n --tw-border-opacity: 1;\n border-color: rgb(185 28 28 / var(--tw-border-opacity));\n}\n.border-slate-500 {\n --tw-border-opacity: 1;\n border-color: rgb(100 116 139 / var(--tw-border-opacity));\n}\n.border-b-slate-600 {\n --tw-border-opacity: 1;\n border-bottom-color: rgb(71 85 105 / var(--tw-border-opacity));\n}\n.bg-blue-200 {\n --tw-bg-opacity: 1;\n background-color: rgb(191 219 254 / var(--tw-bg-opacity));\n}\n.bg-blue-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(59 130 246 / var(--tw-bg-opacity));\n}\n.bg-red-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(239 68 68 / var(--tw-bg-opacity));\n}\n.bg-slate-100 {\n --tw-bg-opacity: 1;\n background-color: rgb(241 245 249 / var(--tw-bg-opacity));\n}\n.bg-slate-200 {\n --tw-bg-opacity: 1;\n background-color: rgb(226 232 240 / var(--tw-bg-opacity));\n}\n.bg-opacity-20 {\n --tw-bg-opacity: 0.2;\n}\n.object-fill {\n -o-object-fit: fill;\n object-fit: fill;\n}\n.p-\\[1px\\] {\n padding: 1px;\n}\n.pb-0 {\n padding-bottom: 0px;\n}\n.pl-1 {\n padding-left: 0.25rem;\n}\n.pl-2 {\n padding-left: 0.5rem;\n}\n.pr-0 {\n padding-right: 0px;\n}\n.pr-1 {\n padding-right: 0.25rem;\n}\n.pt-2 {\n padding-top: 0.5rem;\n}\n.font-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n.text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n.opacity-50 {\n opacity: 0.5;\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-slate-300 {\n --tw-shadow-color: #cbd5e1;\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-slate-600 {\n --tw-shadow-color: #475569;\n --tw-shadow: var(--tw-shadow-colored);\n}\n.filter {\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.transition {\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.hover\\:bg-blue-400:hover {\n --tw-bg-opacity: 1;\n background-color: rgb(96 165 250 / var(--tw-bg-opacity));\n}\n.hover\\:bg-slate-400:hover {\n --tw-bg-opacity: 1;\n background-color: rgb(148 163 184 / var(--tw-bg-opacity));\n}\n.peer:hover ~ .peer-hover\\:border-slate-400 {\n --tw-border-opacity: 1;\n border-color: rgb(148 163 184 / var(--tw-border-opacity));\n}\n.peer:hover ~ .peer-hover\\:bg-slate-300 {\n --tw-bg-opacity: 1;\n background-color: rgb(203 213 225 / var(--tw-bg-opacity));\n}\n.data-\\[focused\\]\\:bg-slate-400[data-focused] {\n --tw-bg-opacity: 1;\n background-color: rgb(148 163 184 / var(--tw-bg-opacity));\n}\n.peer[data-focused] ~ .peer-data-\\[focused\\]\\:border-slate-400 {\n --tw-border-opacity: 1;\n border-color: rgb(148 163 184 / var(--tw-border-opacity));\n}\n.peer[data-focused] ~ .peer-data-\\[focused\\]\\:bg-slate-300 {\n --tw-bg-opacity: 1;\n background-color: rgb(203 213 225 / var(--tw-bg-opacity));\n}\n";
1
+ const twStyle = "/*\n! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com\n*//*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n\n::before,\n::after {\n --tw-content: '';\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\n\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n -o-tab-size: 4;\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n letter-spacing: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\ninput:where([type='button']),\ninput:where([type='reset']),\ninput:where([type='submit']) {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/*\nReset default styling for dialogs.\n*/\ndialog {\n padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::-moz-placeholder, textarea::-moz-placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n:disabled {\n cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/* Make elements with the HTML hidden attribute stay hidden by default */\n[hidden] {\n display: none;\n}\n\n*, ::before, ::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n\n::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n.container {\n width: 100%;\n}\n@media (min-width: 640px) {\n\n .container {\n max-width: 640px;\n }\n}\n@media (min-width: 768px) {\n\n .container {\n max-width: 768px;\n }\n}\n@media (min-width: 1024px) {\n\n .container {\n max-width: 1024px;\n }\n}\n@media (min-width: 1280px) {\n\n .container {\n max-width: 1280px;\n }\n}\n@media (min-width: 1536px) {\n\n .container {\n max-width: 1536px;\n }\n}\n.pointer-events-none {\n pointer-events: none;\n}\n.static {\n position: static;\n}\n.fixed {\n position: fixed;\n}\n.absolute {\n position: absolute;\n}\n.relative {\n position: relative;\n}\n.inset-0 {\n inset: 0px;\n}\n.top-0 {\n top: 0px;\n}\n.z-10 {\n z-index: 10;\n}\n.z-20 {\n z-index: 20;\n}\n.col-span-2 {\n grid-column: span 2 / span 2;\n}\n.mb-\\[1px\\] {\n margin-bottom: 1px;\n}\n.block {\n display: block;\n}\n.inline-block {\n display: inline-block;\n}\n.inline {\n display: inline;\n}\n.flex {\n display: flex;\n}\n.grid {\n display: grid;\n}\n.contents {\n display: contents;\n}\n.hidden {\n display: none;\n}\n.h-\\[1\\.1rem\\] {\n height: 1.1rem;\n}\n.h-\\[5px\\] {\n height: 5px;\n}\n.h-full {\n height: 100%;\n}\n.w-1 {\n width: 0.25rem;\n}\n.w-\\[2px\\] {\n width: 2px;\n}\n.w-full {\n width: 100%;\n}\n.transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.cursor-crosshair {\n cursor: crosshair;\n}\n.resize {\n resize: both;\n}\n.place-content-center {\n place-content: center;\n}\n.place-items-center {\n place-items: center;\n}\n.items-center {\n align-items: center;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.text-nowrap {\n text-wrap: nowrap;\n}\n.border {\n border-width: 1px;\n}\n.border-r-2 {\n border-right-width: 2px;\n}\n.border-blue-500 {\n --tw-border-opacity: 1;\n border-color: rgb(59 130 246 / var(--tw-border-opacity));\n}\n.border-red-700 {\n --tw-border-opacity: 1;\n border-color: rgb(185 28 28 / var(--tw-border-opacity));\n}\n.border-slate-500 {\n --tw-border-opacity: 1;\n border-color: rgb(100 116 139 / var(--tw-border-opacity));\n}\n.border-b-slate-600 {\n --tw-border-opacity: 1;\n border-bottom-color: rgb(71 85 105 / var(--tw-border-opacity));\n}\n.bg-blue-200 {\n --tw-bg-opacity: 1;\n background-color: rgb(191 219 254 / var(--tw-bg-opacity));\n}\n.bg-blue-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(59 130 246 / var(--tw-bg-opacity));\n}\n.bg-red-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(239 68 68 / var(--tw-bg-opacity));\n}\n.bg-slate-100 {\n --tw-bg-opacity: 1;\n background-color: rgb(241 245 249 / var(--tw-bg-opacity));\n}\n.bg-slate-200 {\n --tw-bg-opacity: 1;\n background-color: rgb(226 232 240 / var(--tw-bg-opacity));\n}\n.bg-slate-300 {\n --tw-bg-opacity: 1;\n background-color: rgb(203 213 225 / var(--tw-bg-opacity));\n}\n.bg-opacity-20 {\n --tw-bg-opacity: 0.2;\n}\n.object-fill {\n -o-object-fit: fill;\n object-fit: fill;\n}\n.p-\\[1px\\] {\n padding: 1px;\n}\n.pb-0 {\n padding-bottom: 0px;\n}\n.pl-1 {\n padding-left: 0.25rem;\n}\n.pl-2 {\n padding-left: 0.5rem;\n}\n.pr-0 {\n padding-right: 0px;\n}\n.pr-1 {\n padding-right: 0.25rem;\n}\n.pt-2 {\n padding-top: 0.5rem;\n}\n.font-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n.text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n.opacity-50 {\n opacity: 0.5;\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.shadow-slate-300 {\n --tw-shadow-color: #cbd5e1;\n --tw-shadow: var(--tw-shadow-colored);\n}\n.shadow-slate-600 {\n --tw-shadow-color: #475569;\n --tw-shadow: var(--tw-shadow-colored);\n}\n.filter {\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.transition {\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.hover\\:bg-slate-400:hover {\n --tw-bg-opacity: 1;\n background-color: rgb(148 163 184 / var(--tw-bg-opacity));\n}\n.peer:hover ~ .peer-hover\\:border-slate-400 {\n --tw-border-opacity: 1;\n border-color: rgb(148 163 184 / var(--tw-border-opacity));\n}\n.peer:hover ~ .peer-hover\\:bg-slate-300 {\n --tw-bg-opacity: 1;\n background-color: rgb(203 213 225 / var(--tw-bg-opacity));\n}\n.data-\\[focused\\]\\:bg-slate-400[data-focused] {\n --tw-bg-opacity: 1;\n background-color: rgb(148 163 184 / var(--tw-bg-opacity));\n}\n.peer[data-focused] ~ .peer-data-\\[focused\\]\\:border-slate-400 {\n --tw-border-opacity: 1;\n border-color: rgb(148 163 184 / var(--tw-border-opacity));\n}\n.peer[data-focused] ~ .peer-data-\\[focused\\]\\:bg-slate-300 {\n --tw-bg-opacity: 1;\n background-color: rgb(203 213 225 / var(--tw-bg-opacity));\n}\n";
2
2
  export {
3
3
  twStyle as default
4
4
  };
@@ -23,11 +23,15 @@ declare class FilmstripItem extends FilmstripItem_base {
23
23
  get isFocused(): boolean;
24
24
  element: TemporalMixinInterface & LitElement;
25
25
  pixelsPerMs: number;
26
- get styles(): {
26
+ get gutterStyles(): {
27
27
  position: string;
28
28
  left: string;
29
29
  width: string;
30
30
  };
31
+ get trimPortionStyles(): {
32
+ width: string;
33
+ left: string;
34
+ };
31
35
  render(): TemplateResult<1>;
32
36
  renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing;
33
37
  contents(): TemplateResult<1>;
package/dist/style.css CHANGED
@@ -675,6 +675,10 @@ video {
675
675
  --tw-bg-opacity: 1;
676
676
  background-color: rgb(226 232 240 / var(--tw-bg-opacity));
677
677
  }
678
+ .bg-slate-300 {
679
+ --tw-bg-opacity: 1;
680
+ background-color: rgb(203 213 225 / var(--tw-bg-opacity));
681
+ }
678
682
  .bg-opacity-20 {
679
683
  --tw-bg-opacity: 0.2;
680
684
  }
@@ -764,11 +768,6 @@ body {
764
768
  -webkit-text-size-adjust: 100%;
765
769
  }
766
770
 
767
- .hover\:bg-blue-400:hover {
768
- --tw-bg-opacity: 1;
769
- background-color: rgb(96 165 250 / var(--tw-bg-opacity));
770
- }
771
-
772
771
  .hover\:bg-slate-400:hover {
773
772
  --tw-bg-opacity: 1;
774
773
  background-color: rgb(148 163 184 / var(--tw-bg-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.8.0-beta.1",
3
+ "version": "0.8.0-beta.3",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -20,7 +20,7 @@
20
20
  "author": "",
21
21
  "license": "UNLICENSED",
22
22
  "dependencies": {
23
- "@editframe/assets": "0.8.0-beta.1",
23
+ "@editframe/assets": "0.8.0-beta.3",
24
24
  "@lit/context": "^1.1.2",
25
25
  "@lit/task": "^1.0.1",
26
26
  "d3": "^7.9.0",
@@ -149,13 +149,15 @@ export class EFCaptions extends EFSourceMixin(
149
149
  let endMs = 0;
150
150
  for (const segment of caption.segments) {
151
151
  if (
152
- this.targetElement.ownCurrentTimeMs >= segment.start * 1000 &&
153
- this.targetElement.ownCurrentTimeMs <= segment.end * 1000
152
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
153
+ segment.start * 1000 &&
154
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= segment.end * 1000
154
155
  ) {
155
156
  for (const word of segment.words) {
156
157
  if (
157
- this.targetElement.ownCurrentTimeMs >= word.start * 1000 &&
158
- this.targetElement.ownCurrentTimeMs <= word.end * 1000
158
+ this.targetElement.trimAdjustedOwnCurrentTimeMs >=
159
+ word.start * 1000 &&
160
+ this.targetElement.trimAdjustedOwnCurrentTimeMs <= word.end * 1000
159
161
  ) {
160
162
  words.push(word.text);
161
163
  startMs = word.start * 1000;
@@ -145,7 +145,11 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
145
145
 
146
146
  const result: Record<
147
147
  string,
148
- { segment: TrackSegment; track: MP4Box.TrackInfo }
148
+ {
149
+ segment: TrackSegment;
150
+ track: MP4Box.TrackInfo;
151
+ nextSegment?: TrackSegment;
152
+ }
149
153
  > = {};
150
154
 
151
155
  for (const index of Object.values(fragmentIndex)) {
@@ -161,11 +165,15 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
161
165
  return (segment.dts / track.timescale) * 1000 <= seekToMs;
162
166
  });
163
167
 
168
+ const nextSegment = index.segments.find((segment) => {
169
+ return (segment.dts / track.timescale) * 1000 > seekToMs;
170
+ });
171
+
164
172
  if (!segment) {
165
173
  return;
166
174
  }
167
175
 
168
- result[index.track] = { segment, track };
176
+ result[index.track] = { segment, track, nextSegment };
169
177
  }
170
178
 
171
179
  return result;
@@ -187,7 +195,9 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
187
195
 
188
196
  const files: Record<string, File> = {};
189
197
 
190
- for (const [trackId, { segment, track }] of Object.entries(seekResult)) {
198
+ for (const [trackId, { segment, track, nextSegment }] of Object.entries(
199
+ seekResult,
200
+ )) {
191
201
  const start = segment.offset;
192
202
  const end = segment.offset + segment.size;
193
203
 
@@ -196,6 +206,21 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
196
206
  headers: { Range: `bytes=${start}-${end}` },
197
207
  });
198
208
 
209
+ if (nextSegment) {
210
+ const nextStart = nextSegment.offset;
211
+ const nextEnd = nextSegment.offset + nextSegment.size;
212
+ fetch(this.fragmentTrackPath(trackId), {
213
+ signal,
214
+ headers: { Range: `bytes=${nextStart}-${nextEnd}` },
215
+ })
216
+ .then(() => {
217
+ log("Prefetched next segment");
218
+ })
219
+ .catch((error) => {
220
+ log("Failed to prefetch next segment", error);
221
+ });
222
+ }
223
+
199
224
  const initSegment = Object.values(initSegments).find(
200
225
  (initSegment) => initSegment.trackId === String(track.id),
201
226
  );
@@ -253,7 +278,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
253
278
  changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
254
279
  ): void {
255
280
  if (changedProperties.has("ownCurrentTimeMs")) {
256
- this.executeSeek(this.ownCurrentTimeMs);
281
+ this.executeSeek(this.trimAdjustedOwnCurrentTimeMs);
257
282
  }
258
283
  }
259
284
 
@@ -274,7 +299,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
274
299
  if (durations.length === 0) {
275
300
  return 0;
276
301
  }
277
- return Math.max(...durations);
302
+ return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
278
303
  }
279
304
 
280
305
  get startTimeMs() {
@@ -315,8 +340,8 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
315
340
 
316
341
  async fetchAudioSpanningTime(fromMs: number, toMs: number) {
317
342
  // Adjust range for track's own time
318
- fromMs -= this.startTimeMs;
319
- toMs -= this.startTimeMs;
343
+ fromMs -= this.startTimeMs - this.trimStartMs;
344
+ toMs -= this.startTimeMs - this.trimStartMs;
320
345
 
321
346
  await this.trackFragmentIndexLoader.taskComplete;
322
347
  const audioTrackId = this.defaultAudioTrackId;
@@ -340,6 +365,7 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
340
365
  },
341
366
  );
342
367
 
368
+ console.log({ fromMs, toMs });
343
369
  const fragments = Object.values(audioTrackIndex.segments).filter(
344
370
  (segment) => {
345
371
  const segmentStartsBeforeEnd =
@@ -383,10 +409,13 @@ export class EFMedia extends EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
383
409
 
384
410
  return {
385
411
  blob: audioBlob,
386
- startMs: (firstFragment.dts / audioTrackIndex.timescale) * 1000,
412
+ startMs:
413
+ (firstFragment.dts / audioTrackIndex.timescale) * 1000 -
414
+ this.trimStartMs,
387
415
  endMs:
388
416
  (lastFragment.dts / audioTrackIndex.timescale) * 1000 +
389
- (lastFragment.duration / audioTrackIndex.timescale) * 1000,
417
+ (lastFragment.duration / audioTrackIndex.timescale) * 1000 -
418
+ this.trimEndMs,
390
419
  };
391
420
  }
392
421
  }
@@ -13,11 +13,14 @@ export const timegroupContext = createContext<EFTimegroup>(
13
13
 
14
14
  export declare class TemporalMixinInterface {
15
15
  get hasOwnDuration(): boolean;
16
+ get trimStartMs(): number;
17
+ get trimEndMs(): number;
16
18
  get durationMs(): number;
17
19
  get startTimeMs(): number;
18
20
  get startTimeWithinParentMs(): number;
19
21
  get endTimeMs(): number;
20
22
  get ownCurrentTimeMs(): number;
23
+ get trimAdjustedOwnCurrentTimeMs(): number;
21
24
 
22
25
  set duration(value: string);
23
26
  get duration(): string;
@@ -153,6 +156,36 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
153
156
  })
154
157
  private _durationMs?: number;
155
158
 
159
+ @property({
160
+ type: Number,
161
+ attribute: "trimstart",
162
+ converter: durationConverter,
163
+ })
164
+ private _trimStartMs = 0;
165
+ public get trimStartMs(): number {
166
+ return this._trimStartMs;
167
+ }
168
+
169
+ @property({
170
+ type: Number,
171
+ attribute: "trimend",
172
+ converter: durationConverter,
173
+ })
174
+ private _trimEndMs = 0;
175
+ public get trimEndMs(): number {
176
+ return this._trimEndMs;
177
+ }
178
+
179
+ @property({
180
+ type: Number,
181
+ attribute: "startoffset",
182
+ converter: durationConverter,
183
+ })
184
+ private _startOffsetMs = 0;
185
+ public get startOffsetMs(): number {
186
+ return this._startOffsetMs;
187
+ }
188
+
156
189
  @state()
157
190
  rootTimegroup?: EFTimegroup = this.getRootTimegroup();
158
191
 
@@ -220,9 +253,15 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
220
253
  }
221
254
  startTimeMsCache.set(
222
255
  this,
223
- previous.startTimeMs + previous.durationMs,
256
+ previous.startTimeMs +
257
+ previous.durationMs -
258
+ parentTimegroup.overlapMs,
259
+ );
260
+ return (
261
+ previous.startTimeMs +
262
+ previous.durationMs -
263
+ parentTimegroup.overlapMs
224
264
  );
225
- return previous.startTimeMs + previous.durationMs;
226
265
  }
227
266
  case "contain":
228
267
  case "fixed":
@@ -250,6 +289,25 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
250
289
  return 0;
251
290
  }
252
291
 
292
+ /**
293
+ * Used to calculate the internal currentTimeMs of the element. This is useful
294
+ * for mapping to internal media time codes for audio/video elements.
295
+ */
296
+ get trimAdjustedOwnCurrentTimeMs() {
297
+ if (this.rootTimegroup) {
298
+ return Math.min(
299
+ Math.max(
300
+ 0,
301
+ this.rootTimegroup.currentTimeMs -
302
+ this.startTimeMs +
303
+ this.trimStartMs,
304
+ ),
305
+ this.durationMs + Math.abs(this.startOffsetMs) + this.trimStartMs,
306
+ );
307
+ }
308
+ return 0;
309
+ }
310
+
253
311
  frameTask = new Task(this, {
254
312
  autoRun: EF_INTERACTIVE,
255
313
  args: () => [this.ownCurrentTimeMs] as const,
@@ -13,6 +13,7 @@ import {
13
13
  import { TimegroupController } from "./TimegroupController.ts";
14
14
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
15
  import { deepGetMediaElements } from "./EFMedia.ts";
16
+ import { durationConverter } from "./durationConverter.ts";
16
17
 
17
18
  const log = debug("ef:elements:EFTimegroup");
18
19
 
@@ -37,7 +38,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
37
38
  display: block;
38
39
  width: 100%;
39
40
  height: 100%;
40
- position: relative;
41
+ position: absolute;
41
42
  top: 0;
42
43
  }
43
44
  `;
@@ -53,6 +54,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
53
54
  })
54
55
  mode: "fixed" | "sequence" | "contain" = "sequence";
55
56
 
57
+ @property({
58
+ type: Number,
59
+ converter: durationConverter,
60
+ attribute: "overlap",
61
+ })
62
+ overlapMs = 0;
63
+
56
64
  @property({ type: Number })
57
65
  set currentTime(time: number) {
58
66
  this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
@@ -76,25 +84,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
76
84
  this.currentTime = ms / 1000;
77
85
  }
78
86
 
79
- @property({
80
- attribute: "crossover",
81
- converter: {
82
- fromAttribute: (value: string): number => {
83
- if (value.endsWith("ms")) {
84
- return Number.parseFloat(value);
85
- }
86
- if (value.endsWith("s")) {
87
- return Number.parseFloat(value) * 1000;
88
- }
89
- throw new Error(
90
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
91
- );
92
- },
93
- toAttribute: (value: number) => `${value}ms`,
94
- },
95
- })
96
- crossoverMs = 0;
97
-
98
87
  render() {
99
88
  return html`<slot></slot> `;
100
89
  }
@@ -134,32 +123,18 @@ export class EFTimegroup extends EFTemporal(LitElement) {
134
123
  return `ef-timegroup-${this.id}`;
135
124
  }
136
125
 
137
- get crossoverStartMs() {
138
- const parentTimeGroup = this.parentTimegroup;
139
- if (!parentTimeGroup || !this.previousElementSibling) {
140
- return 0;
141
- }
142
-
143
- return parentTimeGroup.crossoverMs;
144
- }
145
- get crossoverEndMs() {
146
- const parentTimeGroup = this.parentTimegroup;
147
- if (!parentTimeGroup || !this.nextElementSibling) {
148
- return 0;
149
- }
150
-
151
- return parentTimeGroup.crossoverMs;
152
- }
153
-
154
126
  get durationMs() {
155
127
  switch (this.mode) {
156
128
  case "fixed":
157
129
  return super.durationMs;
158
130
  case "sequence": {
159
131
  let duration = 0;
160
- for (const node of this.childTemporals) {
132
+ this.childTemporals.forEach((node, index) => {
133
+ if (index > 0) {
134
+ duration -= this.overlapMs;
135
+ }
161
136
  duration += node.durationMs;
162
- }
137
+ });
163
138
  return duration;
164
139
  }
165
140
  case "contain": {
@@ -207,9 +182,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
207
182
  this.style.display = "";
208
183
 
209
184
  const animations = this.getAnimations({ subtree: true });
185
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
186
+ this.style.setProperty(
187
+ "--ef-transition--duration",
188
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`,
189
+ );
210
190
  this.style.setProperty(
211
- "--ef-duration",
212
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
191
+ "--ef-transition-out-start",
192
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
213
193
  );
214
194
 
215
195
  for (const animation of animations) {
@@ -351,7 +351,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
351
351
  if (!this.targetElement.audioBufferTask.value) {
352
352
  return;
353
353
  }
354
- if (this.targetElement.ownCurrentTimeMs > 0) {
354
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
355
355
  const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
356
356
  const audioBufferSource = audioContext.createBufferSource();
357
357
  audioBufferSource.buffer =
@@ -364,7 +364,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
364
364
  0,
365
365
  Math.max(
366
366
  0,
367
- (this.targetElement.ownCurrentTimeMs -
367
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs -
368
368
  this.targetElement.audioBufferTask.value.startOffsetMs) /
369
369
  1000,
370
370
  ),
@@ -4,3 +4,23 @@ export const durationConverter = {
4
4
  fromAttribute: (value: string): number => parseTimeToMs(value),
5
5
  toAttribute: (value: number) => `${value}s`,
6
6
  };
7
+
8
+ const positiveDurationConverter = (error: string) => {
9
+ return {
10
+ fromAttribute: (value: string): number => {
11
+ if (value.startsWith("-")) {
12
+ throw new Error(error);
13
+ }
14
+ return parseTimeToMs(value);
15
+ },
16
+ toAttribute: (value: number) => `${value}s`,
17
+ };
18
+ };
19
+
20
+ export const trimDurationConverter = positiveDurationConverter(
21
+ "Trimstart & trimend must be a positive value in milliseconds or seconds (1s, 1000ms)",
22
+ );
23
+
24
+ export const imageDurationConverter = positiveDurationConverter(
25
+ "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
+ );
@@ -1,4 +1,5 @@
1
1
  export const parseTimeToMs = (time: string) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -51,13 +51,13 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
51
51
  }
52
52
 
53
53
  if (this.signingURL) {
54
- if (!this.#signedURLs[url]) {
55
- this.#signedURLs[url] = fetch(this.signingURL, {
54
+ if (!this.#URLTokens[url]) {
55
+ this.#URLTokens[url] = fetch(this.signingURL, {
56
56
  method: "POST",
57
57
  body: JSON.stringify({ url }),
58
58
  }).then(async (response) => {
59
59
  if (response.ok) {
60
- return (await response.json()).url;
60
+ return (await response.json()).token;
61
61
  }
62
62
  throw new Error(
63
63
  `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
@@ -65,15 +65,17 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
65
65
  });
66
66
  }
67
67
 
68
- const signedURL = await this.#signedURLs[url];
68
+ const urlToken = await this.#URLTokens[url];
69
69
 
70
- return fetch(signedURL, init);
70
+ Object.assign(init.headers, {
71
+ authorization: `Bearer ${urlToken}`,
72
+ });
71
73
  }
72
74
 
73
75
  return fetch(url, init);
74
76
  };
75
77
 
76
- #signedURLs: Record<string, Promise<string>> = {};
78
+ #URLTokens: Record<string, Promise<string>> = {};
77
79
 
78
80
  @property({ type: String })
79
81
  signingURL?: string;
@@ -86,18 +86,25 @@ class FilmstripItem extends TWMixin(LitElement) {
86
86
  @property({ type: Number })
87
87
  pixelsPerMs = 0.04;
88
88
 
89
- get styles() {
89
+ get gutterStyles() {
90
90
  return {
91
91
  position: "relative",
92
- left: `${this.pixelsPerMs * this.element.startTimeWithinParentMs}px`,
92
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
93
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`,
94
+ };
95
+ }
96
+
97
+ get trimPortionStyles() {
98
+ return {
93
99
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
100
+ left: `${this.pixelsPerMs * this.element.trimStartMs}px`,
94
101
  };
95
102
  }
96
103
 
97
104
  render() {
98
- return html` <div class="" style=${styleMap(this.styles)}>
105
+ return html`<div style=${styleMap(this.gutterStyles)}>
99
106
  <div
100
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 hover:bg-blue-400 text-sm data-[focused]:bg-slate-400"
107
+ class="bg-slate-300"
101
108
  ?data-focused=${this.isFocused}
102
109
  @mouseenter=${() => {
103
110
  if (this.focusContext) {
@@ -110,7 +117,13 @@ class FilmstripItem extends TWMixin(LitElement) {
110
117
  }
111
118
  }}
112
119
  >
113
- ${this.animations()}
120
+ <div
121
+ ?data-focused=${this.isFocused}
122
+ class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400"
123
+ style=${styleMap(this.trimPortionStyles)}
124
+ >
125
+ ${this.animations()}
126
+ </div>
114
127
  </div>
115
128
  ${this.renderChildren()}
116
129
  </div>`;
@@ -83,7 +83,6 @@ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
83
83
  >
84
84
  <slot
85
85
  ${ref(this.canvasRef)}
86
- class="inline-block"
87
86
  name="canvas"
88
87
  ></slot>
89
88
  <div