@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.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as lit from 'lit';
2
2
  import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost } from 'lit';
3
- import { Peaks, Bits, FadeType, PeakData, ClipTrack } from '@waveform-playlist/core';
3
+ import { Peaks, Bits, FadeType, PeakData, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
4
  import { PlaylistEngine } from '@waveform-playlist/engine';
5
5
 
6
6
  declare class DawClipElement extends LitElement {
@@ -335,13 +335,19 @@ interface ClipBounds {
335
335
  }
336
336
  /** Narrow engine contract for clip move/trim interactions. */
337
337
  interface ClipEngineContract {
338
- moveClip(trackId: string, clipId: string, deltaSamples: number, skipAdapter?: boolean): void;
338
+ moveClip(trackId: string, clipId: string, deltaSamples: number, skipAdapter?: boolean): number;
339
339
  trimClip(trackId: string, clipId: string, boundary: 'left' | 'right', deltaSamples: number, skipAdapter?: boolean): void;
340
340
  updateTrack(trackId: string): void;
341
341
  /** Get a clip's full bounds for trim constraint computation. */
342
342
  getClipBounds(trackId: string, clipId: string): ClipBounds | null;
343
343
  /** Constrain a trim delta using the engine's collision/bounds logic. */
344
344
  constrainTrimDelta(trackId: string, clipId: string, boundary: 'left' | 'right', deltaSamples: number): number;
345
+ /** Begin a transaction — groups mutations into one undo step. */
346
+ beginTransaction(): void;
347
+ /** Commit the transaction — pushes one undo step for all grouped mutations. */
348
+ commitTransaction(): void;
349
+ /** Abort the transaction — restores pre-transaction state without pushing to undo. */
350
+ abortTransaction(): void;
345
351
  }
346
352
  /** Peak data returned by reextractClipPeaks for imperative waveform updates. */
347
353
  interface ClipPeakSlice {
@@ -593,10 +599,19 @@ declare class DawEditorElement extends LitElement {
593
599
  play(startTime?: number): Promise<void>;
594
600
  pause(): void;
595
601
  stop(): void;
602
+ /** Toggle between play and pause. */
603
+ togglePlayPause(): void;
596
604
  seekTo(time: number): void;
605
+ /** Undo the last structural edit. */
606
+ undo(): void;
607
+ /** Redo the last undone edit. */
608
+ redo(): void;
609
+ /** Whether undo is available. */
610
+ get canUndo(): boolean;
611
+ /** Whether redo is available. */
612
+ get canRedo(): boolean;
597
613
  /** Split the clip under the playhead on the selected track. */
598
614
  splitAtPlayhead(): boolean;
599
- private _onKeyDown;
600
615
  recordingStream: MediaStream | null;
601
616
  get currentTime(): number;
602
617
  get isRecording(): boolean;
@@ -668,6 +683,59 @@ declare global {
668
683
  }
669
684
  }
670
685
 
686
+ /** Key binding for remapping — derived from KeyboardShortcut to stay in sync. */
687
+ type KeyBinding = Pick<KeyboardShortcut, 'key' | 'ctrlKey' | 'shiftKey' | 'metaKey' | 'altKey'>;
688
+ interface PlaybackShortcutMap {
689
+ playPause?: KeyBinding;
690
+ stop?: KeyBinding;
691
+ rewindToStart?: KeyBinding;
692
+ }
693
+ interface SplittingShortcutMap {
694
+ splitAtPlayhead?: KeyBinding;
695
+ }
696
+ interface UndoShortcutMap {
697
+ undo?: KeyBinding;
698
+ redo?: KeyBinding;
699
+ }
700
+ /**
701
+ * Render-less element that enables keyboard shortcuts for a parent <daw-editor>.
702
+ * Place inside the editor element. Boolean attributes enable preset categories;
703
+ * JS properties allow remapping and custom shortcuts.
704
+ *
705
+ * ```html
706
+ * <daw-editor>
707
+ * <daw-keyboard-shortcuts playback splitting undo></daw-keyboard-shortcuts>
708
+ * </daw-editor>
709
+ * ```
710
+ */
711
+ declare class DawKeyboardShortcutsElement extends LitElement {
712
+ playback: boolean;
713
+ splitting: boolean;
714
+ undo: boolean;
715
+ playbackShortcuts: PlaybackShortcutMap | null;
716
+ splittingShortcuts: SplittingShortcutMap | null;
717
+ undoShortcuts: UndoShortcutMap | null;
718
+ /** Additional custom shortcuts. */
719
+ customShortcuts: KeyboardShortcut[];
720
+ private _editor;
721
+ private _cachedShortcuts;
722
+ /** All active shortcuts (read-only, cached). */
723
+ get shortcuts(): KeyboardShortcut[];
724
+ /** Invalidate cached shortcuts when Lit properties change. */
725
+ updated(): void;
726
+ connectedCallback(): void;
727
+ disconnectedCallback(): void;
728
+ createRenderRoot(): this;
729
+ private _buildShortcuts;
730
+ private _makeShortcut;
731
+ private _onKeyDown;
732
+ }
733
+ declare global {
734
+ interface HTMLElementTagNameMap {
735
+ 'daw-keyboard-shortcuts': DawKeyboardShortcutsElement;
736
+ }
737
+ }
738
+
671
739
  declare class AudioResumeController implements ReactiveController {
672
740
  private _host;
673
741
  private _target;
@@ -704,15 +772,20 @@ interface SplitEngineContract {
704
772
  interface SplitHost {
705
773
  readonly effectiveSampleRate: number;
706
774
  readonly currentTime: number;
775
+ readonly isPlaying: boolean;
707
776
  readonly engine: SplitEngineContract | null;
708
777
  dispatchEvent(event: Event): boolean;
778
+ stop(): void;
779
+ play(time: number): void;
709
780
  }
710
781
  /**
711
- * Splits the clip under the playhead on the selected track.
782
+ * Split the clip under the playhead on the selected track.
783
+ * Stops playback before split and resumes after to avoid duplicate audio
784
+ * from Transport rescheduling during playback.
712
785
  *
713
786
  * Returns true if the split occurred and dispatched a daw-clip-split event.
714
787
  * Returns false for any guard failure or engine no-op.
715
788
  */
716
789
  declare function splitAtPlayhead(host: SplitHost): boolean;
717
790
 
718
- export { AudioResumeController, type ClipDescriptor, type ClipEngineContract, ClipPointerHandler, type ClipPointerHost, DawClipElement, type DawClipMoveDetail, type DawClipSplitDetail, type DawClipTrimDetail, DawEditorElement, type DawErrorDetail, type DawEvent, type DawEventMap, type DawFilesLoadErrorDetail, DawPauseButtonElement, DawPlayButtonElement, DawPlayheadElement, DawRecordButtonElement, type DawRecordingCompleteDetail, type DawRecordingErrorDetail, type DawRecordingStartDetail, DawRulerElement, type DawSeekDetail, type DawSelectionDetail, DawSelectionElement, DawStopButtonElement, type DawTrackConnectedDetail, type DawTrackControlDetail, DawTrackControlsElement, DawTrackElement, type DawTrackErrorDetail, type DawTrackIdDetail, type DawTrackRemoveDetail, type DawTrackSelectDetail, DawTransportButton, DawTransportElement, DawWaveformElement, type LoadFilesResult, type PointerEngineContract, RecordingController, type RecordingOptions, type RecordingSession, type SplitEngineContract, type SplitHost, type TrackDescriptor, splitAtPlayhead };
791
+ export { AudioResumeController, type ClipDescriptor, type ClipEngineContract, ClipPointerHandler, type ClipPointerHost, DawClipElement, type DawClipMoveDetail, type DawClipSplitDetail, type DawClipTrimDetail, DawEditorElement, type DawErrorDetail, type DawEvent, type DawEventMap, type DawFilesLoadErrorDetail, DawKeyboardShortcutsElement, DawPauseButtonElement, DawPlayButtonElement, DawPlayheadElement, DawRecordButtonElement, type DawRecordingCompleteDetail, type DawRecordingErrorDetail, type DawRecordingStartDetail, DawRulerElement, type DawSeekDetail, type DawSelectionDetail, DawSelectionElement, DawStopButtonElement, type DawTrackConnectedDetail, type DawTrackControlDetail, DawTrackControlsElement, DawTrackElement, type DawTrackErrorDetail, type DawTrackIdDetail, type DawTrackRemoveDetail, type DawTrackSelectDetail, DawTransportButton, DawTransportElement, DawWaveformElement, type KeyBinding, type LoadFilesResult, type PlaybackShortcutMap, type PointerEngineContract, RecordingController, type RecordingOptions, type RecordingSession, type SplitEngineContract, type SplitHost, type SplittingShortcutMap, type TrackDescriptor, type UndoShortcutMap, splitAtPlayhead };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as lit from 'lit';
2
2
  import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost } from 'lit';
3
- import { Peaks, Bits, FadeType, PeakData, ClipTrack } from '@waveform-playlist/core';
3
+ import { Peaks, Bits, FadeType, PeakData, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
4
  import { PlaylistEngine } from '@waveform-playlist/engine';
5
5
 
6
6
  declare class DawClipElement extends LitElement {
@@ -335,13 +335,19 @@ interface ClipBounds {
335
335
  }
336
336
  /** Narrow engine contract for clip move/trim interactions. */
337
337
  interface ClipEngineContract {
338
- moveClip(trackId: string, clipId: string, deltaSamples: number, skipAdapter?: boolean): void;
338
+ moveClip(trackId: string, clipId: string, deltaSamples: number, skipAdapter?: boolean): number;
339
339
  trimClip(trackId: string, clipId: string, boundary: 'left' | 'right', deltaSamples: number, skipAdapter?: boolean): void;
340
340
  updateTrack(trackId: string): void;
341
341
  /** Get a clip's full bounds for trim constraint computation. */
342
342
  getClipBounds(trackId: string, clipId: string): ClipBounds | null;
343
343
  /** Constrain a trim delta using the engine's collision/bounds logic. */
344
344
  constrainTrimDelta(trackId: string, clipId: string, boundary: 'left' | 'right', deltaSamples: number): number;
345
+ /** Begin a transaction — groups mutations into one undo step. */
346
+ beginTransaction(): void;
347
+ /** Commit the transaction — pushes one undo step for all grouped mutations. */
348
+ commitTransaction(): void;
349
+ /** Abort the transaction — restores pre-transaction state without pushing to undo. */
350
+ abortTransaction(): void;
345
351
  }
346
352
  /** Peak data returned by reextractClipPeaks for imperative waveform updates. */
347
353
  interface ClipPeakSlice {
@@ -593,10 +599,19 @@ declare class DawEditorElement extends LitElement {
593
599
  play(startTime?: number): Promise<void>;
594
600
  pause(): void;
595
601
  stop(): void;
602
+ /** Toggle between play and pause. */
603
+ togglePlayPause(): void;
596
604
  seekTo(time: number): void;
605
+ /** Undo the last structural edit. */
606
+ undo(): void;
607
+ /** Redo the last undone edit. */
608
+ redo(): void;
609
+ /** Whether undo is available. */
610
+ get canUndo(): boolean;
611
+ /** Whether redo is available. */
612
+ get canRedo(): boolean;
597
613
  /** Split the clip under the playhead on the selected track. */
598
614
  splitAtPlayhead(): boolean;
599
- private _onKeyDown;
600
615
  recordingStream: MediaStream | null;
601
616
  get currentTime(): number;
602
617
  get isRecording(): boolean;
@@ -668,6 +683,59 @@ declare global {
668
683
  }
669
684
  }
670
685
 
686
+ /** Key binding for remapping — derived from KeyboardShortcut to stay in sync. */
687
+ type KeyBinding = Pick<KeyboardShortcut, 'key' | 'ctrlKey' | 'shiftKey' | 'metaKey' | 'altKey'>;
688
+ interface PlaybackShortcutMap {
689
+ playPause?: KeyBinding;
690
+ stop?: KeyBinding;
691
+ rewindToStart?: KeyBinding;
692
+ }
693
+ interface SplittingShortcutMap {
694
+ splitAtPlayhead?: KeyBinding;
695
+ }
696
+ interface UndoShortcutMap {
697
+ undo?: KeyBinding;
698
+ redo?: KeyBinding;
699
+ }
700
+ /**
701
+ * Render-less element that enables keyboard shortcuts for a parent <daw-editor>.
702
+ * Place inside the editor element. Boolean attributes enable preset categories;
703
+ * JS properties allow remapping and custom shortcuts.
704
+ *
705
+ * ```html
706
+ * <daw-editor>
707
+ * <daw-keyboard-shortcuts playback splitting undo></daw-keyboard-shortcuts>
708
+ * </daw-editor>
709
+ * ```
710
+ */
711
+ declare class DawKeyboardShortcutsElement extends LitElement {
712
+ playback: boolean;
713
+ splitting: boolean;
714
+ undo: boolean;
715
+ playbackShortcuts: PlaybackShortcutMap | null;
716
+ splittingShortcuts: SplittingShortcutMap | null;
717
+ undoShortcuts: UndoShortcutMap | null;
718
+ /** Additional custom shortcuts. */
719
+ customShortcuts: KeyboardShortcut[];
720
+ private _editor;
721
+ private _cachedShortcuts;
722
+ /** All active shortcuts (read-only, cached). */
723
+ get shortcuts(): KeyboardShortcut[];
724
+ /** Invalidate cached shortcuts when Lit properties change. */
725
+ updated(): void;
726
+ connectedCallback(): void;
727
+ disconnectedCallback(): void;
728
+ createRenderRoot(): this;
729
+ private _buildShortcuts;
730
+ private _makeShortcut;
731
+ private _onKeyDown;
732
+ }
733
+ declare global {
734
+ interface HTMLElementTagNameMap {
735
+ 'daw-keyboard-shortcuts': DawKeyboardShortcutsElement;
736
+ }
737
+ }
738
+
671
739
  declare class AudioResumeController implements ReactiveController {
672
740
  private _host;
673
741
  private _target;
@@ -704,15 +772,20 @@ interface SplitEngineContract {
704
772
  interface SplitHost {
705
773
  readonly effectiveSampleRate: number;
706
774
  readonly currentTime: number;
775
+ readonly isPlaying: boolean;
707
776
  readonly engine: SplitEngineContract | null;
708
777
  dispatchEvent(event: Event): boolean;
778
+ stop(): void;
779
+ play(time: number): void;
709
780
  }
710
781
  /**
711
- * Splits the clip under the playhead on the selected track.
782
+ * Split the clip under the playhead on the selected track.
783
+ * Stops playback before split and resumes after to avoid duplicate audio
784
+ * from Transport rescheduling during playback.
712
785
  *
713
786
  * Returns true if the split occurred and dispatched a daw-clip-split event.
714
787
  * Returns false for any guard failure or engine no-op.
715
788
  */
716
789
  declare function splitAtPlayhead(host: SplitHost): boolean;
717
790
 
718
- export { AudioResumeController, type ClipDescriptor, type ClipEngineContract, ClipPointerHandler, type ClipPointerHost, DawClipElement, type DawClipMoveDetail, type DawClipSplitDetail, type DawClipTrimDetail, DawEditorElement, type DawErrorDetail, type DawEvent, type DawEventMap, type DawFilesLoadErrorDetail, DawPauseButtonElement, DawPlayButtonElement, DawPlayheadElement, DawRecordButtonElement, type DawRecordingCompleteDetail, type DawRecordingErrorDetail, type DawRecordingStartDetail, DawRulerElement, type DawSeekDetail, type DawSelectionDetail, DawSelectionElement, DawStopButtonElement, type DawTrackConnectedDetail, type DawTrackControlDetail, DawTrackControlsElement, DawTrackElement, type DawTrackErrorDetail, type DawTrackIdDetail, type DawTrackRemoveDetail, type DawTrackSelectDetail, DawTransportButton, DawTransportElement, DawWaveformElement, type LoadFilesResult, type PointerEngineContract, RecordingController, type RecordingOptions, type RecordingSession, type SplitEngineContract, type SplitHost, type TrackDescriptor, splitAtPlayhead };
791
+ export { AudioResumeController, type ClipDescriptor, type ClipEngineContract, ClipPointerHandler, type ClipPointerHost, DawClipElement, type DawClipMoveDetail, type DawClipSplitDetail, type DawClipTrimDetail, DawEditorElement, type DawErrorDetail, type DawEvent, type DawEventMap, type DawFilesLoadErrorDetail, DawKeyboardShortcutsElement, DawPauseButtonElement, DawPlayButtonElement, DawPlayheadElement, DawRecordButtonElement, type DawRecordingCompleteDetail, type DawRecordingErrorDetail, type DawRecordingStartDetail, DawRulerElement, type DawSeekDetail, type DawSelectionDetail, DawSelectionElement, DawStopButtonElement, type DawTrackConnectedDetail, type DawTrackControlDetail, DawTrackControlsElement, DawTrackElement, type DawTrackErrorDetail, type DawTrackIdDetail, type DawTrackRemoveDetail, type DawTrackSelectDetail, DawTransportButton, DawTransportElement, DawWaveformElement, type KeyBinding, type LoadFilesResult, type PlaybackShortcutMap, type PointerEngineContract, RecordingController, type RecordingOptions, type RecordingSession, type SplitEngineContract, type SplitHost, type SplittingShortcutMap, type TrackDescriptor, type UndoShortcutMap, splitAtPlayhead };
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  ClipPointerHandler: () => ClipPointerHandler,
43
43
  DawClipElement: () => DawClipElement,
44
44
  DawEditorElement: () => DawEditorElement,
45
+ DawKeyboardShortcutsElement: () => DawKeyboardShortcutsElement,
45
46
  DawPauseButtonElement: () => DawPauseButtonElement,
46
47
  DawPlayButtonElement: () => DawPlayButtonElement,
47
48
  DawPlayheadElement: () => DawPlayheadElement,
@@ -2229,6 +2230,13 @@ var ClipPointerHandler = class {
2229
2230
  this._isDragging = false;
2230
2231
  this._lastDeltaPx = 0;
2231
2232
  this._cumulativeDeltaSamples = 0;
2233
+ if (this._host.engine) {
2234
+ this._host.engine.beginTransaction();
2235
+ } else {
2236
+ console.warn(
2237
+ "[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
2238
+ );
2239
+ }
2232
2240
  if (mode === "trim-left" || mode === "trim-right") {
2233
2241
  const container = this._host.shadowRoot?.querySelector(
2234
2242
  `.clip-container[data-clip-id="${clipId}"]`
@@ -2267,8 +2275,8 @@ var ClipPointerHandler = class {
2267
2275
  const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
2268
2276
  this._lastDeltaPx = totalDeltaPx;
2269
2277
  const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
2270
- this._cumulativeDeltaSamples += incrementalDeltaSamples;
2271
- engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2278
+ const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
2279
+ this._cumulativeDeltaSamples += applied;
2272
2280
  } else {
2273
2281
  const boundary = this._mode === "trim-left" ? "left" : "right";
2274
2282
  const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
@@ -2357,9 +2365,18 @@ var ClipPointerHandler = class {
2357
2365
  }
2358
2366
  })
2359
2367
  );
2368
+ } else {
2369
+ console.warn(
2370
+ "[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
2371
+ );
2360
2372
  }
2361
2373
  }
2362
2374
  } finally {
2375
+ if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
2376
+ this._host.engine?.commitTransaction();
2377
+ } else {
2378
+ this._host.engine?.abortTransaction();
2379
+ }
2363
2380
  this._reset();
2364
2381
  }
2365
2382
  }
@@ -2595,6 +2612,35 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
2595
2612
 
2596
2613
  // src/interactions/split-handler.ts
2597
2614
  function splitAtPlayhead(host) {
2615
+ const wasPlaying = host.isPlaying;
2616
+ const time = host.currentTime;
2617
+ if (!canSplitAtTime(host, time)) return false;
2618
+ if (wasPlaying) {
2619
+ host.stop();
2620
+ }
2621
+ let result;
2622
+ try {
2623
+ result = performSplit(host, time);
2624
+ } catch (err) {
2625
+ console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
2626
+ result = false;
2627
+ }
2628
+ if (wasPlaying) {
2629
+ host.play(time);
2630
+ }
2631
+ return result;
2632
+ }
2633
+ function canSplitAtTime(host, time) {
2634
+ const { engine } = host;
2635
+ if (!engine) return false;
2636
+ const state5 = engine.getState();
2637
+ if (!state5.selectedTrackId) return false;
2638
+ const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
2639
+ if (!track) return false;
2640
+ const atSample = Math.round(time * host.effectiveSampleRate);
2641
+ return !!findClipAtSample(track.clips, atSample);
2642
+ }
2643
+ function performSplit(host, time) {
2598
2644
  const { engine } = host;
2599
2645
  if (!engine) return false;
2600
2646
  const stateBefore = engine.getState();
@@ -2602,7 +2648,7 @@ function splitAtPlayhead(host) {
2602
2648
  if (!selectedTrackId) return false;
2603
2649
  const track = tracks.find((t) => t.id === selectedTrackId);
2604
2650
  if (!track) return false;
2605
- const atSample = Math.round(host.currentTime * host.effectiveSampleRate);
2651
+ const atSample = Math.round(time * host.effectiveSampleRate);
2606
2652
  const clip = findClipAtSample(track.clips, atSample);
2607
2653
  if (!clip) return false;
2608
2654
  const originalClipId = clip.id;
@@ -2809,6 +2855,19 @@ var DawEditorElement = class extends import_lit12.LitElement {
2809
2855
  this._onTrackControl = (e) => {
2810
2856
  const { trackId, prop, value } = e.detail ?? {};
2811
2857
  if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
2858
+ if (this._selectedTrackId !== trackId) {
2859
+ this._setSelectedTrackId(trackId);
2860
+ if (this._engine) {
2861
+ this._engine.selectTrack(trackId);
2862
+ }
2863
+ this.dispatchEvent(
2864
+ new CustomEvent("daw-track-select", {
2865
+ bubbles: true,
2866
+ composed: true,
2867
+ detail: { trackId }
2868
+ })
2869
+ );
2870
+ }
2812
2871
  const oldDescriptor = this._tracks.get(trackId);
2813
2872
  if (oldDescriptor) {
2814
2873
  const descriptor = { ...oldDescriptor, [prop]: value };
@@ -2866,17 +2925,6 @@ var DawEditorElement = class extends import_lit12.LitElement {
2866
2925
  );
2867
2926
  }
2868
2927
  };
2869
- this._onKeyDown = (e) => {
2870
- if (!this.interactiveClips) return;
2871
- if (e.key === "s" || e.key === "S") {
2872
- if (e.ctrlKey || e.metaKey || e.altKey) return;
2873
- const tag = e.target?.tagName;
2874
- if (tag === "INPUT" || tag === "TEXTAREA") return;
2875
- if (e.target?.isContentEditable) return;
2876
- e.preventDefault();
2877
- this.splitAtPlayhead();
2878
- }
2879
- };
2880
2928
  // --- Recording ---
2881
2929
  this.recordingStream = null;
2882
2930
  }
@@ -2942,10 +2990,6 @@ var DawEditorElement = class extends import_lit12.LitElement {
2942
2990
  // --- Lifecycle ---
2943
2991
  connectedCallback() {
2944
2992
  super.connectedCallback();
2945
- if (!this.hasAttribute("tabindex")) {
2946
- this.setAttribute("tabindex", "0");
2947
- }
2948
- this.addEventListener("keydown", this._onKeyDown);
2949
2993
  this.addEventListener("daw-track-connected", this._onTrackConnected);
2950
2994
  this.addEventListener("daw-track-update", this._onTrackUpdate);
2951
2995
  this.addEventListener("daw-track-control", this._onTrackControl);
@@ -2971,7 +3015,6 @@ var DawEditorElement = class extends import_lit12.LitElement {
2971
3015
  }
2972
3016
  disconnectedCallback() {
2973
3017
  super.disconnectedCallback();
2974
- this.removeEventListener("keydown", this._onKeyDown);
2975
3018
  this.removeEventListener("daw-track-connected", this._onTrackConnected);
2976
3019
  this.removeEventListener("daw-track-update", this._onTrackUpdate);
2977
3020
  this.removeEventListener("daw-track-control", this._onTrackControl);
@@ -3263,18 +3306,71 @@ var DawEditorElement = class extends import_lit12.LitElement {
3263
3306
  this._stopPlayhead();
3264
3307
  this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
3265
3308
  }
3309
+ /** Toggle between play and pause. */
3310
+ togglePlayPause() {
3311
+ if (this._isPlaying) {
3312
+ this.pause();
3313
+ } else {
3314
+ this.play();
3315
+ }
3316
+ }
3266
3317
  seekTo(time) {
3267
- if (!this._engine) return;
3268
- this._engine.seek(time);
3269
- this._currentTime = time;
3318
+ if (!this._engine) {
3319
+ console.warn("[dawcore] seekTo: engine not ready, call ignored");
3320
+ return;
3321
+ }
3322
+ if (this._isPlaying) {
3323
+ this.stop();
3324
+ this.play(time);
3325
+ } else {
3326
+ this._engine.seek(time);
3327
+ this._currentTime = time;
3328
+ this._stopPlayhead();
3329
+ }
3330
+ }
3331
+ /** Undo the last structural edit. */
3332
+ undo() {
3333
+ if (!this._engine) {
3334
+ console.warn("[dawcore] undo: engine not ready, call ignored");
3335
+ return;
3336
+ }
3337
+ this._engine.undo();
3338
+ }
3339
+ /** Redo the last undone edit. */
3340
+ redo() {
3341
+ if (!this._engine) {
3342
+ console.warn("[dawcore] redo: engine not ready, call ignored");
3343
+ return;
3344
+ }
3345
+ this._engine.redo();
3346
+ }
3347
+ /** Whether undo is available. */
3348
+ get canUndo() {
3349
+ return this._engine?.canUndo ?? false;
3350
+ }
3351
+ /** Whether redo is available. */
3352
+ get canRedo() {
3353
+ return this._engine?.canRedo ?? false;
3270
3354
  }
3271
3355
  /** Split the clip under the playhead on the selected track. */
3272
3356
  splitAtPlayhead() {
3273
3357
  return splitAtPlayhead({
3274
3358
  effectiveSampleRate: this.effectiveSampleRate,
3275
3359
  currentTime: this._currentTime,
3360
+ isPlaying: this._isPlaying,
3276
3361
  engine: this._engine,
3277
- dispatchEvent: (e) => this.dispatchEvent(e)
3362
+ dispatchEvent: (e) => this.dispatchEvent(e),
3363
+ stop: () => {
3364
+ this._engine?.stop();
3365
+ this._stopPlayhead();
3366
+ },
3367
+ // Call engine.play directly (synchronous) — not the async editor play()
3368
+ // which yields to microtask queue via await engine.init(). Engine is
3369
+ // already initialized at split time; the async gap causes audio desync.
3370
+ play: (time) => {
3371
+ this._engine?.play(time);
3372
+ this._startPlayhead();
3373
+ }
3278
3374
  });
3279
3375
  }
3280
3376
  get currentTime() {
@@ -3874,12 +3970,183 @@ __decorateClass([
3874
3970
  DawRecordButtonElement = __decorateClass([
3875
3971
  (0, import_decorators13.customElement)("daw-record-button")
3876
3972
  ], DawRecordButtonElement);
3973
+
3974
+ // src/elements/daw-keyboard-shortcuts.ts
3975
+ var import_lit16 = require("lit");
3976
+ var import_decorators14 = require("lit/decorators.js");
3977
+ var import_core5 = require("@waveform-playlist/core");
3978
+ var DawKeyboardShortcutsElement = class extends import_lit16.LitElement {
3979
+ constructor() {
3980
+ super(...arguments);
3981
+ this.playback = false;
3982
+ this.splitting = false;
3983
+ this.undo = false;
3984
+ // --- JS properties for remapping ---
3985
+ this.playbackShortcuts = null;
3986
+ this.splittingShortcuts = null;
3987
+ this.undoShortcuts = null;
3988
+ /** Additional custom shortcuts. */
3989
+ this.customShortcuts = [];
3990
+ this._editor = null;
3991
+ this._cachedShortcuts = null;
3992
+ // --- Event handler ---
3993
+ this._onKeyDown = (e) => {
3994
+ const shortcuts = this.shortcuts;
3995
+ if (shortcuts.length === 0) return;
3996
+ try {
3997
+ (0, import_core5.handleKeyboardEvent)(e, shortcuts, true);
3998
+ } catch (err) {
3999
+ console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
4000
+ const target = this._editor ?? this;
4001
+ target.dispatchEvent(
4002
+ new CustomEvent("daw-error", {
4003
+ bubbles: true,
4004
+ composed: true,
4005
+ detail: { operation: "keyboard-shortcut", key: e.key, error: err }
4006
+ })
4007
+ );
4008
+ }
4009
+ };
4010
+ }
4011
+ /** All active shortcuts (read-only, cached). */
4012
+ get shortcuts() {
4013
+ if (!this._cachedShortcuts) {
4014
+ this._cachedShortcuts = this._buildShortcuts();
4015
+ }
4016
+ return this._cachedShortcuts;
4017
+ }
4018
+ /** Invalidate cached shortcuts when Lit properties change. */
4019
+ updated() {
4020
+ this._cachedShortcuts = null;
4021
+ }
4022
+ // --- Lifecycle ---
4023
+ connectedCallback() {
4024
+ super.connectedCallback();
4025
+ this._editor = this.closest("daw-editor");
4026
+ if (!this._editor) {
4027
+ console.warn(
4028
+ "[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
4029
+ );
4030
+ }
4031
+ document.addEventListener("keydown", this._onKeyDown);
4032
+ }
4033
+ disconnectedCallback() {
4034
+ super.disconnectedCallback();
4035
+ document.removeEventListener("keydown", this._onKeyDown);
4036
+ this._editor = null;
4037
+ }
4038
+ // No shadow DOM — render-less element
4039
+ createRenderRoot() {
4040
+ return this;
4041
+ }
4042
+ // --- Shortcut building ---
4043
+ _buildShortcuts() {
4044
+ const editor = this._editor;
4045
+ if (!editor) return this.customShortcuts;
4046
+ const result = [];
4047
+ if (this.playback) {
4048
+ const map = this.playbackShortcuts;
4049
+ result.push(
4050
+ this._makeShortcut(
4051
+ map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
4052
+ () => editor.togglePlayPause(),
4053
+ "Play/Pause"
4054
+ ),
4055
+ this._makeShortcut(
4056
+ map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
4057
+ () => editor.stop(),
4058
+ "Stop"
4059
+ ),
4060
+ this._makeShortcut(
4061
+ map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
4062
+ () => editor.seekTo(0),
4063
+ "Rewind to start"
4064
+ )
4065
+ );
4066
+ }
4067
+ if (this.splitting) {
4068
+ const map = this.splittingShortcuts;
4069
+ const binding = map?.splitAtPlayhead ?? {
4070
+ key: "s",
4071
+ ctrlKey: false,
4072
+ metaKey: false,
4073
+ altKey: false
4074
+ };
4075
+ result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
4076
+ }
4077
+ if (this.undo) {
4078
+ const map = this.undoShortcuts;
4079
+ const undoBinding = map?.undo ?? { key: "z" };
4080
+ const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
4081
+ if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
4082
+ const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
4083
+ result.push(
4084
+ this._makeShortcut(
4085
+ { ...undoBinding, ctrlKey: true, ...undoShift },
4086
+ () => editor.undo(),
4087
+ "Undo"
4088
+ ),
4089
+ this._makeShortcut(
4090
+ { ...undoBinding, metaKey: true, ...undoShift },
4091
+ () => editor.undo(),
4092
+ "Undo"
4093
+ )
4094
+ );
4095
+ } else {
4096
+ result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
4097
+ }
4098
+ if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
4099
+ const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
4100
+ result.push(
4101
+ this._makeShortcut(
4102
+ { ...redoBinding, ctrlKey: true, ...redoShift },
4103
+ () => editor.redo(),
4104
+ "Redo"
4105
+ ),
4106
+ this._makeShortcut(
4107
+ { ...redoBinding, metaKey: true, ...redoShift },
4108
+ () => editor.redo(),
4109
+ "Redo"
4110
+ )
4111
+ );
4112
+ } else {
4113
+ result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
4114
+ }
4115
+ }
4116
+ result.push(...this.customShortcuts);
4117
+ return result;
4118
+ }
4119
+ _makeShortcut(binding, action, description) {
4120
+ return {
4121
+ key: binding.key,
4122
+ ...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
4123
+ ...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
4124
+ ...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
4125
+ ...binding.altKey !== void 0 && { altKey: binding.altKey },
4126
+ action,
4127
+ description
4128
+ };
4129
+ }
4130
+ };
4131
+ __decorateClass([
4132
+ (0, import_decorators14.property)({ type: Boolean })
4133
+ ], DawKeyboardShortcutsElement.prototype, "playback", 2);
4134
+ __decorateClass([
4135
+ (0, import_decorators14.property)({ type: Boolean })
4136
+ ], DawKeyboardShortcutsElement.prototype, "splitting", 2);
4137
+ __decorateClass([
4138
+ (0, import_decorators14.property)({ type: Boolean })
4139
+ ], DawKeyboardShortcutsElement.prototype, "undo", 2);
4140
+ DawKeyboardShortcutsElement = __decorateClass([
4141
+ (0, import_decorators14.customElement)("daw-keyboard-shortcuts")
4142
+ ], DawKeyboardShortcutsElement);
3877
4143
  // Annotate the CommonJS export names for ESM import in node:
3878
4144
  0 && (module.exports = {
3879
4145
  AudioResumeController,
3880
4146
  ClipPointerHandler,
3881
4147
  DawClipElement,
3882
4148
  DawEditorElement,
4149
+ DawKeyboardShortcutsElement,
3883
4150
  DawPauseButtonElement,
3884
4151
  DawPlayButtonElement,
3885
4152
  DawPlayheadElement,