@dawcore/components 0.0.2 → 0.0.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.
package/dist/index.mjs CHANGED
@@ -2179,6 +2179,13 @@ var ClipPointerHandler = class {
2179
2179
  this._isDragging = false;
2180
2180
  this._lastDeltaPx = 0;
2181
2181
  this._cumulativeDeltaSamples = 0;
2182
+ if (this._host.engine) {
2183
+ this._host.engine.beginTransaction();
2184
+ } else {
2185
+ console.warn(
2186
+ "[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
2187
+ );
2188
+ }
2182
2189
  if (mode === "trim-left" || mode === "trim-right") {
2183
2190
  const container = this._host.shadowRoot?.querySelector(
2184
2191
  `.clip-container[data-clip-id="${clipId}"]`
@@ -2217,8 +2224,8 @@ var ClipPointerHandler = class {
2217
2224
  const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
2218
2225
  this._lastDeltaPx = totalDeltaPx;
2219
2226
  const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
2220
- this._cumulativeDeltaSamples += incrementalDeltaSamples;
2221
- engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2227
+ const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2228
+ this._cumulativeDeltaSamples += applied;
2222
2229
  } else {
2223
2230
  const boundary = this._mode === "trim-left" ? "left" : "right";
2224
2231
  const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
@@ -2307,9 +2314,18 @@ var ClipPointerHandler = class {
2307
2314
  }
2308
2315
  })
2309
2316
  );
2317
+ } else {
2318
+ console.warn(
2319
+ "[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
2320
+ );
2310
2321
  }
2311
2322
  }
2312
2323
  } finally {
2324
+ if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
2325
+ this._host.engine?.commitTransaction();
2326
+ } else {
2327
+ this._host.engine?.abortTransaction();
2328
+ }
2313
2329
  this._reset();
2314
2330
  }
2315
2331
  }
@@ -2545,6 +2561,35 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
2545
2561
 
2546
2562
  // src/interactions/split-handler.ts
2547
2563
  function splitAtPlayhead(host) {
2564
+ const wasPlaying = host.isPlaying;
2565
+ const time = host.currentTime;
2566
+ if (!canSplitAtTime(host, time)) return false;
2567
+ if (wasPlaying) {
2568
+ host.stop();
2569
+ }
2570
+ let result;
2571
+ try {
2572
+ result = performSplit(host, time);
2573
+ } catch (err) {
2574
+ console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
2575
+ result = false;
2576
+ }
2577
+ if (wasPlaying) {
2578
+ host.play(time);
2579
+ }
2580
+ return result;
2581
+ }
2582
+ function canSplitAtTime(host, time) {
2583
+ const { engine } = host;
2584
+ if (!engine) return false;
2585
+ const state5 = engine.getState();
2586
+ if (!state5.selectedTrackId) return false;
2587
+ const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
2588
+ if (!track) return false;
2589
+ const atSample = Math.round(time * host.effectiveSampleRate);
2590
+ return !!findClipAtSample(track.clips, atSample);
2591
+ }
2592
+ function performSplit(host, time) {
2548
2593
  const { engine } = host;
2549
2594
  if (!engine) return false;
2550
2595
  const stateBefore = engine.getState();
@@ -2552,7 +2597,7 @@ function splitAtPlayhead(host) {
2552
2597
  if (!selectedTrackId) return false;
2553
2598
  const track = tracks.find((t) => t.id === selectedTrackId);
2554
2599
  if (!track) return false;
2555
- const atSample = Math.round(host.currentTime * host.effectiveSampleRate);
2600
+ const atSample = Math.round(time * host.effectiveSampleRate);
2556
2601
  const clip = findClipAtSample(track.clips, atSample);
2557
2602
  if (!clip) return false;
2558
2603
  const originalClipId = clip.id;
@@ -2759,6 +2804,19 @@ var DawEditorElement = class extends LitElement8 {
2759
2804
  this._onTrackControl = (e) => {
2760
2805
  const { trackId, prop, value } = e.detail ?? {};
2761
2806
  if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
2807
+ if (this._selectedTrackId !== trackId) {
2808
+ this._setSelectedTrackId(trackId);
2809
+ if (this._engine) {
2810
+ this._engine.selectTrack(trackId);
2811
+ }
2812
+ this.dispatchEvent(
2813
+ new CustomEvent("daw-track-select", {
2814
+ bubbles: true,
2815
+ composed: true,
2816
+ detail: { trackId }
2817
+ })
2818
+ );
2819
+ }
2762
2820
  const oldDescriptor = this._tracks.get(trackId);
2763
2821
  if (oldDescriptor) {
2764
2822
  const descriptor = { ...oldDescriptor, [prop]: value };
@@ -2816,17 +2874,6 @@ var DawEditorElement = class extends LitElement8 {
2816
2874
  );
2817
2875
  }
2818
2876
  };
2819
- this._onKeyDown = (e) => {
2820
- if (!this.interactiveClips) return;
2821
- if (e.key === "s" || e.key === "S") {
2822
- if (e.ctrlKey || e.metaKey || e.altKey) return;
2823
- const tag = e.target?.tagName;
2824
- if (tag === "INPUT" || tag === "TEXTAREA") return;
2825
- if (e.target?.isContentEditable) return;
2826
- e.preventDefault();
2827
- this.splitAtPlayhead();
2828
- }
2829
- };
2830
2877
  // --- Recording ---
2831
2878
  this.recordingStream = null;
2832
2879
  }
@@ -2892,10 +2939,6 @@ var DawEditorElement = class extends LitElement8 {
2892
2939
  // --- Lifecycle ---
2893
2940
  connectedCallback() {
2894
2941
  super.connectedCallback();
2895
- if (!this.hasAttribute("tabindex")) {
2896
- this.setAttribute("tabindex", "0");
2897
- }
2898
- this.addEventListener("keydown", this._onKeyDown);
2899
2942
  this.addEventListener("daw-track-connected", this._onTrackConnected);
2900
2943
  this.addEventListener("daw-track-update", this._onTrackUpdate);
2901
2944
  this.addEventListener("daw-track-control", this._onTrackControl);
@@ -2921,7 +2964,6 @@ var DawEditorElement = class extends LitElement8 {
2921
2964
  }
2922
2965
  disconnectedCallback() {
2923
2966
  super.disconnectedCallback();
2924
- this.removeEventListener("keydown", this._onKeyDown);
2925
2967
  this.removeEventListener("daw-track-connected", this._onTrackConnected);
2926
2968
  this.removeEventListener("daw-track-update", this._onTrackUpdate);
2927
2969
  this.removeEventListener("daw-track-control", this._onTrackControl);
@@ -3213,18 +3255,71 @@ var DawEditorElement = class extends LitElement8 {
3213
3255
  this._stopPlayhead();
3214
3256
  this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
3215
3257
  }
3258
+ /** Toggle between play and pause. */
3259
+ togglePlayPause() {
3260
+ if (this._isPlaying) {
3261
+ this.pause();
3262
+ } else {
3263
+ this.play();
3264
+ }
3265
+ }
3216
3266
  seekTo(time) {
3217
- if (!this._engine) return;
3218
- this._engine.seek(time);
3219
- this._currentTime = time;
3267
+ if (!this._engine) {
3268
+ console.warn("[dawcore] seekTo: engine not ready, call ignored");
3269
+ return;
3270
+ }
3271
+ if (this._isPlaying) {
3272
+ this.stop();
3273
+ this.play(time);
3274
+ } else {
3275
+ this._engine.seek(time);
3276
+ this._currentTime = time;
3277
+ this._stopPlayhead();
3278
+ }
3279
+ }
3280
+ /** Undo the last structural edit. */
3281
+ undo() {
3282
+ if (!this._engine) {
3283
+ console.warn("[dawcore] undo: engine not ready, call ignored");
3284
+ return;
3285
+ }
3286
+ this._engine.undo();
3287
+ }
3288
+ /** Redo the last undone edit. */
3289
+ redo() {
3290
+ if (!this._engine) {
3291
+ console.warn("[dawcore] redo: engine not ready, call ignored");
3292
+ return;
3293
+ }
3294
+ this._engine.redo();
3295
+ }
3296
+ /** Whether undo is available. */
3297
+ get canUndo() {
3298
+ return this._engine?.canUndo ?? false;
3299
+ }
3300
+ /** Whether redo is available. */
3301
+ get canRedo() {
3302
+ return this._engine?.canRedo ?? false;
3220
3303
  }
3221
3304
  /** Split the clip under the playhead on the selected track. */
3222
3305
  splitAtPlayhead() {
3223
3306
  return splitAtPlayhead({
3224
3307
  effectiveSampleRate: this.effectiveSampleRate,
3225
3308
  currentTime: this._currentTime,
3309
+ isPlaying: this._isPlaying,
3226
3310
  engine: this._engine,
3227
- dispatchEvent: (e) => this.dispatchEvent(e)
3311
+ dispatchEvent: (e) => this.dispatchEvent(e),
3312
+ stop: () => {
3313
+ this._engine?.stop();
3314
+ this._stopPlayhead();
3315
+ },
3316
+ // Call engine.play directly (synchronous) — not the async editor play()
3317
+ // which yields to microtask queue via await engine.init(). Engine is
3318
+ // already initialized at split time; the async gap causes audio desync.
3319
+ play: (time) => {
3320
+ this._engine?.play(time);
3321
+ this._startPlayhead();
3322
+ }
3228
3323
  });
3229
3324
  }
3230
3325
  get currentTime() {
@@ -3824,11 +3919,182 @@ __decorateClass([
3824
3919
  DawRecordButtonElement = __decorateClass([
3825
3920
  customElement13("daw-record-button")
3826
3921
  ], DawRecordButtonElement);
3922
+
3923
+ // src/elements/daw-keyboard-shortcuts.ts
3924
+ import { LitElement as LitElement11 } from "lit";
3925
+ import { customElement as customElement14, property as property9 } from "lit/decorators.js";
3926
+ import { handleKeyboardEvent } from "@waveform-playlist/core";
3927
+ var DawKeyboardShortcutsElement = class extends LitElement11 {
3928
+ constructor() {
3929
+ super(...arguments);
3930
+ this.playback = false;
3931
+ this.splitting = false;
3932
+ this.undo = false;
3933
+ // --- JS properties for remapping ---
3934
+ this.playbackShortcuts = null;
3935
+ this.splittingShortcuts = null;
3936
+ this.undoShortcuts = null;
3937
+ /** Additional custom shortcuts. */
3938
+ this.customShortcuts = [];
3939
+ this._editor = null;
3940
+ this._cachedShortcuts = null;
3941
+ // --- Event handler ---
3942
+ this._onKeyDown = (e) => {
3943
+ const shortcuts = this.shortcuts;
3944
+ if (shortcuts.length === 0) return;
3945
+ try {
3946
+ handleKeyboardEvent(e, shortcuts, true);
3947
+ } catch (err) {
3948
+ console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
3949
+ const target = this._editor ?? this;
3950
+ target.dispatchEvent(
3951
+ new CustomEvent("daw-error", {
3952
+ bubbles: true,
3953
+ composed: true,
3954
+ detail: { operation: "keyboard-shortcut", key: e.key, error: err }
3955
+ })
3956
+ );
3957
+ }
3958
+ };
3959
+ }
3960
+ /** All active shortcuts (read-only, cached). */
3961
+ get shortcuts() {
3962
+ if (!this._cachedShortcuts) {
3963
+ this._cachedShortcuts = this._buildShortcuts();
3964
+ }
3965
+ return this._cachedShortcuts;
3966
+ }
3967
+ /** Invalidate cached shortcuts when Lit properties change. */
3968
+ updated() {
3969
+ this._cachedShortcuts = null;
3970
+ }
3971
+ // --- Lifecycle ---
3972
+ connectedCallback() {
3973
+ super.connectedCallback();
3974
+ this._editor = this.closest("daw-editor");
3975
+ if (!this._editor) {
3976
+ console.warn(
3977
+ "[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
3978
+ );
3979
+ }
3980
+ document.addEventListener("keydown", this._onKeyDown);
3981
+ }
3982
+ disconnectedCallback() {
3983
+ super.disconnectedCallback();
3984
+ document.removeEventListener("keydown", this._onKeyDown);
3985
+ this._editor = null;
3986
+ }
3987
+ // No shadow DOM — render-less element
3988
+ createRenderRoot() {
3989
+ return this;
3990
+ }
3991
+ // --- Shortcut building ---
3992
+ _buildShortcuts() {
3993
+ const editor = this._editor;
3994
+ if (!editor) return this.customShortcuts;
3995
+ const result = [];
3996
+ if (this.playback) {
3997
+ const map = this.playbackShortcuts;
3998
+ result.push(
3999
+ this._makeShortcut(
4000
+ map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
4001
+ () => editor.togglePlayPause(),
4002
+ "Play/Pause"
4003
+ ),
4004
+ this._makeShortcut(
4005
+ map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
4006
+ () => editor.stop(),
4007
+ "Stop"
4008
+ ),
4009
+ this._makeShortcut(
4010
+ map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
4011
+ () => editor.seekTo(0),
4012
+ "Rewind to start"
4013
+ )
4014
+ );
4015
+ }
4016
+ if (this.splitting) {
4017
+ const map = this.splittingShortcuts;
4018
+ const binding = map?.splitAtPlayhead ?? {
4019
+ key: "s",
4020
+ ctrlKey: false,
4021
+ metaKey: false,
4022
+ altKey: false
4023
+ };
4024
+ result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
4025
+ }
4026
+ if (this.undo) {
4027
+ const map = this.undoShortcuts;
4028
+ const undoBinding = map?.undo ?? { key: "z" };
4029
+ const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
4030
+ if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
4031
+ const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
4032
+ result.push(
4033
+ this._makeShortcut(
4034
+ { ...undoBinding, ctrlKey: true, ...undoShift },
4035
+ () => editor.undo(),
4036
+ "Undo"
4037
+ ),
4038
+ this._makeShortcut(
4039
+ { ...undoBinding, metaKey: true, ...undoShift },
4040
+ () => editor.undo(),
4041
+ "Undo"
4042
+ )
4043
+ );
4044
+ } else {
4045
+ result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
4046
+ }
4047
+ if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
4048
+ const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
4049
+ result.push(
4050
+ this._makeShortcut(
4051
+ { ...redoBinding, ctrlKey: true, ...redoShift },
4052
+ () => editor.redo(),
4053
+ "Redo"
4054
+ ),
4055
+ this._makeShortcut(
4056
+ { ...redoBinding, metaKey: true, ...redoShift },
4057
+ () => editor.redo(),
4058
+ "Redo"
4059
+ )
4060
+ );
4061
+ } else {
4062
+ result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
4063
+ }
4064
+ }
4065
+ result.push(...this.customShortcuts);
4066
+ return result;
4067
+ }
4068
+ _makeShortcut(binding, action, description) {
4069
+ return {
4070
+ key: binding.key,
4071
+ ...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
4072
+ ...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
4073
+ ...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
4074
+ ...binding.altKey !== void 0 && { altKey: binding.altKey },
4075
+ action,
4076
+ description
4077
+ };
4078
+ }
4079
+ };
4080
+ __decorateClass([
4081
+ property9({ type: Boolean })
4082
+ ], DawKeyboardShortcutsElement.prototype, "playback", 2);
4083
+ __decorateClass([
4084
+ property9({ type: Boolean })
4085
+ ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
4086
+ __decorateClass([
4087
+ property9({ type: Boolean })
4088
+ ], DawKeyboardShortcutsElement.prototype, "undo", 2);
4089
+ DawKeyboardShortcutsElement = __decorateClass([
4090
+ customElement14("daw-keyboard-shortcuts")
4091
+ ], DawKeyboardShortcutsElement);
3827
4092
  export {
3828
4093
  AudioResumeController,
3829
4094
  ClipPointerHandler,
3830
4095
  DawClipElement,
3831
4096
  DawEditorElement,
4097
+ DawKeyboardShortcutsElement,
3832
4098
  DawPauseButtonElement,
3833
4099
  DawPlayButtonElement,
3834
4100
  DawPlayheadElement,