@ixfx/components 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.d.ts +365 -143
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2330 -826
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +2 -3
- package/dist/ac-text.d.ts.map +1 -1
- package/dist/ac-text.js +5 -5
- package/dist/ac-text.js.map +1 -1
- package/dist/{button-DgKUzE5o.js → button-DMt5XWUK.js} +6 -6
- package/dist/button-DMt5XWUK.js.map +1 -0
- package/dist/button.d.ts +3 -4
- package/dist/button.d.ts.map +1 -1
- package/dist/button.js +1 -1
- package/dist/checkbox.d.ts +3 -5
- package/dist/checkbox.d.ts.map +1 -1
- package/dist/checkbox.js +3 -3
- package/dist/checkbox.js.map +1 -1
- package/dist/{colour-C3MQIjFJ-BJV-QC-3.js → colour-C3MQIjFJ-BLXg8W3U.js} +2 -2
- package/dist/{colour-C3MQIjFJ-BJV-QC-3.js.map → colour-C3MQIjFJ-BLXg8W3U.js.map} +1 -1
- package/dist/{colour-picker-Bw-AZoWx.js → colour-picker-K6zu82lm.js} +8 -8
- package/dist/colour-picker-K6zu82lm.js.map +1 -0
- package/dist/colour-picker.d.ts +2 -2
- package/dist/colour-picker.js +1 -1
- package/dist/crumbs.d.ts +4 -6
- package/dist/crumbs.d.ts.map +1 -1
- package/dist/crumbs.js +12 -12
- package/dist/crumbs.js.map +1 -1
- package/dist/data-display.d.ts +1 -2
- package/dist/data-display.d.ts.map +1 -1
- package/dist/data-display.js +4 -5
- package/dist/data-display.js.map +1 -1
- package/dist/decorate-D7rC1gLP.js +27 -0
- package/dist/decorate-D7rC1gLP.js.map +1 -0
- package/dist/{defaults-FEZaGiYZ.js → defaults-C7elhuyJ.js} +2 -2
- package/dist/{defaults-FEZaGiYZ.js.map → defaults-C7elhuyJ.js.map} +1 -1
- package/dist/{dist-By5URG0Y.js → dist-BX0OVIm7.js} +2 -2
- package/dist/{dist-By5URG0Y.js.map → dist-BX0OVIm7.js.map} +1 -1
- package/dist/{dist-D7kJgnSE.js → dist-TAGYwaju.js} +1 -1
- package/dist/{dist-D7kJgnSE.js.map → dist-TAGYwaju.js.map} +1 -1
- package/dist/editable-label.d.ts +6 -1
- package/dist/editable-label.d.ts.map +1 -1
- package/dist/editable-label.js +51 -4
- package/dist/editable-label.js.map +1 -1
- package/dist/{hex-editor-Wd6ZtZA2.js → hex-editor-CsN_ySkH.js} +87 -87
- package/dist/hex-editor-CsN_ySkH.js.map +1 -0
- package/dist/{hex-editor-Gib1Grg2.d.ts → hex-editor-DlfoA65y.d.ts} +7 -8
- package/dist/hex-editor-DlfoA65y.d.ts.map +1 -0
- package/dist/hex.d.ts +1 -1
- package/dist/hex.js +1 -1
- package/dist/{highlight-CAjwQ4GE.js → highlight-TPbSHruI.js} +3 -3
- package/dist/highlight-TPbSHruI.js.map +1 -0
- package/dist/{icon-BR5aTIiP.js → icon-9NWTXtE1.js} +6 -7
- package/dist/icon-9NWTXtE1.js.map +1 -0
- package/dist/{icon-DQKdfR7U.d.ts → icon-CeTDJhC7.d.ts} +3 -4
- package/dist/icon-CeTDJhC7.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +3 -3
- package/dist/icons.js.map +1 -1
- package/dist/{incr-search-BjCNClnC.js → incr-search-DSKKsk6w.js} +119 -117
- package/dist/incr-search-DSKKsk6w.js.map +1 -0
- package/dist/incr-search.d.ts +2 -2
- package/dist/incr-search.js +2 -2
- package/dist/{index-CPVcTTXo.d.ts → index--hrxPBkQ.d.ts} +73 -74
- package/dist/index--hrxPBkQ.d.ts.map +1 -0
- package/dist/index-B1hNR7Z9.d.ts.map +1 -1
- package/dist/{index-CpbBI3n7.d.ts → index-CnL9krsH.d.ts} +3 -4
- package/dist/index-CnL9krsH.d.ts.map +1 -0
- package/dist/{index-CQxanqXF.d.ts → index-DSlPeWLK.d.ts} +28 -29
- package/dist/index-DSlPeWLK.d.ts.map +1 -0
- package/dist/{index-C5dK8RZa.d.ts → index-DrQjA8Gs.d.ts} +3 -4
- package/dist/index-DrQjA8Gs.d.ts.map +1 -0
- package/dist/{index-C9iBN908.d.ts → index-hXFn14tM.d.ts} +32 -32
- package/dist/index-hXFn14tM.d.ts.map +1 -0
- package/dist/index.d.ts +246 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1464 -32
- package/dist/index.js.map +1 -1
- package/dist/keyboard-De_vOHLg.js.map +1 -1
- package/dist/{labelled-input-base-DS6-6C0r.js → labelled-input-base-CqPL5rKm.js} +2 -2
- package/dist/labelled-input-base-CqPL5rKm.js.map +1 -0
- package/dist/{labelled-input-base-DnRh4reU.d.ts → labelled-input-base-D-Y7agky.d.ts} +2 -3
- package/dist/labelled-input-base-D-Y7agky.d.ts.map +1 -0
- package/dist/labelled-radial-input.d.ts +4 -5
- package/dist/labelled-radial-input.d.ts.map +1 -1
- package/dist/labelled-radial-input.js +5 -5
- package/dist/labelled-radial-input.js.map +1 -1
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-range-input.js +4 -4
- package/dist/labelled-range-input.js.map +1 -1
- package/dist/led.d.ts +2 -3
- package/dist/led.d.ts.map +1 -1
- package/dist/led.js +3 -3
- package/dist/led.js.map +1 -1
- package/dist/{menu-BV6UFx_z.js → menu-DJJuCm-5.js} +55 -55
- package/dist/menu-DJJuCm-5.js.map +1 -0
- package/dist/{menu-item-DEzL_Yjk.js → menu-item-CKRfP0pN.js} +14 -14
- package/dist/menu-item-CKRfP0pN.js.map +1 -0
- package/dist/{menu-item-Cikacp4F.d.ts → menu-item-ClQF2DKN.d.ts} +7 -8
- package/dist/menu-item-ClQF2DKN.d.ts.map +1 -0
- package/dist/menu.d.ts +3 -3
- package/dist/menu.js +2 -2
- package/dist/miller.d.ts +7 -10
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +17 -17
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.d.ts +2 -3
- package/dist/narrowed-text.d.ts.map +1 -1
- package/dist/narrowed-text.js +6 -6
- package/dist/narrowed-text.js.map +1 -1
- package/dist/panel.d.ts +2 -3
- package/dist/panel.d.ts.map +1 -1
- package/dist/panel.js +4 -4
- package/dist/panel.js.map +1 -1
- package/dist/plots.d.ts +1 -1
- package/dist/plots.js +1 -1
- package/dist/polar-pad.d.ts +1 -2
- package/dist/polar-pad.d.ts.map +1 -1
- package/dist/polar-pad.js +3 -3
- package/dist/polar-pad.js.map +1 -1
- package/dist/{popup-Ddx9LCzY.js → popup-ImtS9KnS.js} +1 -1
- package/dist/{popup-Ddx9LCzY.js.map → popup-ImtS9KnS.js.map} +1 -1
- package/dist/{prominence-CreDBdCs.js → prominence--ic9uWBl.js} +1 -1
- package/dist/{prominence-CreDBdCs.js.map → prominence--ic9uWBl.js.map} +1 -1
- package/dist/{radial-input-CNJ8asY2.js → radial-input-DZA6WaFu.js} +16 -16
- package/dist/radial-input-DZA6WaFu.js.map +1 -0
- package/dist/{radial-input-xSTd4kEz.d.ts → radial-input-LUSBEycd.d.ts} +2 -3
- package/dist/radial-input-LUSBEycd.d.ts.map +1 -0
- package/dist/radial-input.d.ts +1 -1
- package/dist/radial-input.js +1 -1
- package/dist/range-input.d.ts +2 -3
- package/dist/range-input.d.ts.map +1 -1
- package/dist/range-input.js +3 -3
- package/dist/range-input.js.map +1 -1
- package/dist/range.d.ts +2 -3
- package/dist/range.d.ts.map +1 -1
- package/dist/range.js +4 -4
- package/dist/range.js.map +1 -1
- package/dist/{registry-CZEB1YI9.js → registry-Q2gYQCHM.js} +1 -1
- package/dist/{registry-CZEB1YI9.js.map → registry-Q2gYQCHM.js.map} +1 -1
- package/dist/selecthorizontal.d.ts +2 -3
- package/dist/selecthorizontal.d.ts.map +1 -1
- package/dist/selecthorizontal.js +3 -3
- package/dist/selecthorizontal.js.map +1 -1
- package/dist/snap-container.d.ts +1 -2
- package/dist/snap-container.d.ts.map +1 -1
- package/dist/snap-container.js +3 -3
- package/dist/snap-container.js.map +1 -1
- package/dist/split-layout.d.ts +2 -3
- package/dist/split-layout.d.ts.map +1 -1
- package/dist/split-layout.js +6 -6
- package/dist/split-layout.js.map +1 -1
- package/dist/swipe.d.ts +1 -2
- package/dist/swipe.d.ts.map +1 -1
- package/dist/swipe.js +4 -4
- package/dist/swipe.js.map +1 -1
- package/dist/{tab-list-CFF7uSYH.d.ts → tab-list-BnqISNvO.d.ts} +5 -6
- package/dist/tab-list-BnqISNvO.d.ts.map +1 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +4 -4
- package/dist/tabs.js.map +1 -1
- package/dist/tickled-controller-Cq1Va0rR.d.ts.map +1 -1
- package/dist/tickled-styles-Bg3QbcrD.js.map +1 -1
- package/dist/{timeline-DBWJsSjd.js → timeline-Cv75Th2U.js} +123 -119
- package/dist/timeline-Cv75Th2U.js.map +1 -0
- package/dist/timeline.d.ts +2 -2
- package/dist/timeline.js +1 -1
- package/dist/{tooltip-kfUBWYVB.d.ts → tooltip-CwGfb4lp.d.ts} +2 -3
- package/dist/tooltip-CwGfb4lp.d.ts.map +1 -0
- package/dist/tree-CvvKbO-f.js +1330 -0
- package/dist/tree-CvvKbO-f.js.map +1 -0
- package/dist/{tree-component-CQpoPo4s.d.ts → tree-component-D8Bg48tt.d.ts} +12 -12
- package/dist/tree-component-D8Bg48tt.d.ts.map +1 -0
- package/dist/tree.d.ts +4 -5
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1143
- package/dist/types-BKaqnpwx.d.ts.map +1 -1
- package/dist/{xy-axis-CIFok7MF.d.ts → xy-axis-D9uwpGTT.d.ts} +5 -6
- package/dist/xy-axis-D9uwpGTT.d.ts.map +1 -0
- package/dist/{xy-axis-BANbgrI5.js → xy-axis-DL0tfJDr.js} +13 -13
- package/dist/xy-axis-DL0tfJDr.js.map +1 -0
- package/dist/xy-pad.d.ts +1 -2
- package/dist/xy-pad.d.ts.map +1 -1
- package/dist/xy-pad.js +4 -4
- package/dist/xy-pad.js.map +1 -1
- package/package.json +17 -13
- package/dist/button-DgKUzE5o.js.map +0 -1
- package/dist/colour-picker-Bw-AZoWx.js.map +0 -1
- package/dist/decorate-BZbkqIhA.js +0 -9
- package/dist/hex-editor-Gib1Grg2.d.ts.map +0 -1
- package/dist/hex-editor-Wd6ZtZA2.js.map +0 -1
- package/dist/highlight-CAjwQ4GE.js.map +0 -1
- package/dist/icon-BR5aTIiP.js.map +0 -1
- package/dist/icon-DQKdfR7U.d.ts.map +0 -1
- package/dist/incr-search-BjCNClnC.js.map +0 -1
- package/dist/index-C5dK8RZa.d.ts.map +0 -1
- package/dist/index-C9iBN908.d.ts.map +0 -1
- package/dist/index-CPVcTTXo.d.ts.map +0 -1
- package/dist/index-CQxanqXF.d.ts.map +0 -1
- package/dist/index-CpbBI3n7.d.ts.map +0 -1
- package/dist/labelled-input-base-DS6-6C0r.js.map +0 -1
- package/dist/labelled-input-base-DnRh4reU.d.ts.map +0 -1
- package/dist/menu-BV6UFx_z.js.map +0 -1
- package/dist/menu-item-Cikacp4F.d.ts.map +0 -1
- package/dist/menu-item-DEzL_Yjk.js.map +0 -1
- package/dist/radial-input-CNJ8asY2.js.map +0 -1
- package/dist/radial-input-xSTd4kEz.d.ts.map +0 -1
- package/dist/tab-list-CFF7uSYH.d.ts.map +0 -1
- package/dist/timeline-DBWJsSjd.js.map +0 -1
- package/dist/tooltip-kfUBWYVB.d.ts.map +0 -1
- package/dist/tree-component-CQpoPo4s.d.ts.map +0 -1
- package/dist/tree-data-model-DGvRVOFY.js +0 -190
- package/dist/tree-data-model-DGvRVOFY.js.map +0 -1
- package/dist/tree.js.map +0 -1
- package/dist/xy-axis-BANbgrI5.js.map +0 -1
- package/dist/xy-axis-CIFok7MF.d.ts.map +0 -1
- /package/dist/{interaction-CQDQJf2Q.js → interaction-DG7X1rhI.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tickled-styles-Bg3QbcrD.js","names":[],"sources":["../src/interaction/tickled-controller.ts","../src/interaction/tickled-styles.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\n\n/**\n * Represents an item that can be tickled (highlighted via hover or keyboard navigation)\n */\nexport type TickledItem<T = unknown> = {\n /**\n * Unique identifier for the item (e.g., index, id, path)\n */\n readonly id: T;\n\n /**\n * Whether the item is currently enabled/interactive\n */\n readonly enabled?: boolean;\n};\n\n/**\n * Input mode determines which mechanism controls the tickled state\n */\nexport type InputMode = `keyboard` | `mouse`;\n\n/**\n * Configuration for TickledController\n */\nexport type TickledControllerConfig<T> = {\n /**\n * Function to retrieve all items that can be tickled\n */\n getItems: () => ReadonlyArray<TickledItem<T>>;\n\n /**\n * Optional callback when an item becomes tickled\n * Use this for side effects like opening submenus\n * @param id - The id of the newly tickled item, or undefined if nothing is tickled\n */\n onTickle?: (id: T | undefined) => void;\n\n /**\n * Optional callback fired after any state change (tickled id or input mode)\n * Use this to sync DOM attributes for slotted content that can't use reactive rendering\n *\n * For components that render items directly, this is usually not needed as\n * Lit's reactive rendering will handle updates automatically via requestUpdate()\n *\n * @example\n * ```ts\n * onStateChange: () => {\n * // Sync data-tickled attributes for slotted items\n * const items = this.getItems();\n * items.forEach((item, i) => {\n * if (this.tickled.isTickled(i)) {\n * item.setAttribute('data-tickled', '');\n * } else {\n * item.removeAttribute('data-tickled');\n * }\n * });\n * }\n * ```\n */\n onStateChange?: () => void;\n\n /**\n * Optional initial input mode (defaults to 'mouse')\n */\n initialInputMode?: InputMode;\n};\n\n/**\n * ReactiveController that manages \"tickled\" state for interactive lists.\n *\n * A \"tickled\" item is one that is under active attention via hover (mouse mode)\n * or keyboard focus (keyboard mode). Only one item can be tickled at a time.\n *\n * The input mode determines which mechanism controls tickled state:\n * - Mouse mode: hover controls tickled, keyboard navigation ignored\n * - Keyboard mode: keyboard navigation controls tickled, hover ignored\n *\n * Last interaction wins - any mouse hover switches to mouse mode, any keyboard\n * navigation switches to keyboard mode.\n *\n * ## Global Mouse Tracking\n *\n * This controller uses a singleton global `pointermove` listener to track actual\n * mouse movement. This prevents `pointerenter` events caused by content scrolling\n * from incorrectly switching to mouse mode when the user is actually using keyboard\n * navigation.\n *\n * @example\n * ```ts\n * class MyList extends LitElement {\n * private tickled = new TickledController(this, {\n * getItems: () => this.items.map((item, i) => ({ id: i, enabled: !item.disabled })),\n * onTickle: (id) => {\n * // Optional: perform side effect when item is tickled\n * console.log('Tickled item:', id);\n * }\n * });\n *\n * render() {\n * return html`\n * <ul @pointerleave=${(e) => this.tickled.handlePointerLeave(e)}>\n * ${this.items.map((item, i) => html`\n * <li\n * class=\"item ${this.tickled.isTickled(i) ? 'tickled' : ''}\"\n * @pointerenter=${() => this.tickled.handlePointerEnter(i)}\n * >${item.label}</li>\n * `)}\n * </ul>\n * `;\n * }\n *\n * private handleKeyDown(e: KeyboardEvent) {\n * if (e.key === 'ArrowDown') {\n * this.tickled.handleKeyboardNavigation('next');\n * } else if (e.key === 'ArrowUp') {\n * this.tickled.handleKeyboardNavigation('previous');\n * }\n * }\n * }\n * ```\n */\nexport class TickledController<T = number> implements ReactiveController {\n private host: ReactiveControllerHost;\n private config: TickledControllerConfig<T>;\n private _tickledId: T | undefined = undefined;\n private _inputMode: InputMode;\n\n private static _lastMouseClientX: number = Number.NaN;\n private static _lastMouseClientY: number = Number.NaN;\n private static _mouseMovedSinceKeyboard: boolean = true;\n private static _globalListenerAttached: boolean = false;\n\n constructor(host: ReactiveControllerHost, config: TickledControllerConfig<T>) {\n this.host = host;\n this.config = config;\n this._inputMode = config.initialInputMode ?? `mouse`;\n host.addController(this);\n }\n\n hostConnected(): void {\n if (!TickledController._globalListenerAttached) {\n document.addEventListener(`pointermove`, (e: PointerEvent) => {\n TickledController.handleGlobalPointerMove(e.clientX, e.clientY);\n }, { passive: true });\n TickledController._globalListenerAttached = true;\n }\n }\n\n hostDisconnected(): void {\n // No-op: global listener persists for the lifetime of the page\n }\n\n /**\n * Get the current input mode\n */\n get inputMode(): InputMode {\n return this._inputMode;\n }\n\n /**\n * Get the currently tickled item id\n */\n get tickledId(): T | undefined {\n return this._tickledId;\n }\n\n /**\n * Check if a specific item is tickled\n */\n isTickled(id: T): boolean {\n return this._tickledId === id;\n }\n\n /**\n * Handle global pointer move event.\n * Called automatically by the global listener.\n * Tracks mouse movement to distinguish real hover from content scrolling.\n */\n private static handleGlobalPointerMove(clientX: number, clientY: number): void {\n if (!Number.isNaN(TickledController._lastMouseClientX)) {\n if (\n clientX !== TickledController._lastMouseClientX\n || clientY !== TickledController._lastMouseClientY\n ) {\n TickledController._mouseMovedSinceKeyboard = true;\n }\n }\n TickledController._lastMouseClientX = clientX;\n TickledController._lastMouseClientY = clientY;\n }\n\n /**\n * Check if actual mouse movement has occurred since last keyboard interaction.\n * Used to distinguish real hover/leave from content-scrolling-induced events.\n */\n static get hasMouseMovedSinceKeyboard(): boolean {\n return TickledController._mouseMovedSinceKeyboard;\n }\n\n /**\n * Handle pointer enter event on an item (mouse hover).\n * Only processes if actual mouse movement is detected (not content scrolling).\n * Switches to mouse mode and tickles the item.\n */\n handlePointerEnter(id: T): void {\n if (!TickledController._mouseMovedSinceKeyboard) {\n return;\n }\n\n const items = this.config.getItems();\n const item = items.find(item => item.id === id);\n if (item && item.enabled === false) {\n return;\n }\n\n const modeChanged = this._inputMode !== `mouse`;\n this._inputMode = `mouse`;\n\n const previousId = this._tickledId;\n this._tickledId = id;\n\n this.host.requestUpdate();\n\n if (previousId !== id && this.config.onTickle) {\n this.config.onTickle(id);\n }\n\n if ((previousId !== id || modeChanged) && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n\n /**\n * Handle pointer leave event on an item.\n * Only clears tickled state if actual mouse movement is detected.\n * @param _e The pointer event (not used, but kept for API compatibility)\n */\n handlePointerLeave(_e: PointerEvent): void {\n if (TickledController._mouseMovedSinceKeyboard) {\n this.clearTickled();\n }\n }\n\n /**\n * Handle keyboard navigation.\n * Switches to keyboard mode and moves tickled state.\n *\n * @param direction - 'next', 'previous', 'first', 'last', or specific id\n */\n handleKeyboardNavigation(direction: `next` | `previous` | `first` | `last` | T): void {\n const modeChanged = this._inputMode !== `keyboard`;\n this._inputMode = `keyboard`;\n\n const items = this.config.getItems();\n const enabledItems = items.filter(item => item.enabled !== false);\n\n if (enabledItems.length === 0) {\n return;\n }\n\n let newId: T | undefined;\n\n if (direction === `first`) {\n newId = enabledItems[0].id;\n } else if (direction === `last`) {\n newId = enabledItems.at(-1)?.id;\n } else if (direction === `next` || direction === `previous`) {\n const currentIndex = enabledItems.findIndex(item => item.id === this._tickledId);\n\n if (currentIndex === -1) {\n newId = direction === `next` ? enabledItems[0].id : enabledItems.at(-1)?.id;\n } else {\n const delta = direction === `next` ? 1 : -1;\n const newIndex = currentIndex + delta;\n\n if (newIndex >= 0 && newIndex < enabledItems.length) {\n newId = enabledItems[newIndex].id;\n }\n // If newIndex is out of bounds, keep current tickled item\n }\n } else {\n const item = enabledItems.find(item => item.id === direction);\n if (item) {\n newId = item.id;\n }\n }\n\n if (newId !== undefined) {\n const previousId = this._tickledId;\n this._tickledId = newId;\n this.host.requestUpdate();\n\n if (previousId !== newId && this.config.onTickle) {\n this.config.onTickle(newId);\n }\n\n if ((previousId !== newId || modeChanged) && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n\n /**\n * Manually set the tickled item (useful for programmatic control).\n * Does NOT change input mode.\n */\n setTickled(id: T | undefined): void {\n const previousId = this._tickledId;\n this._tickledId = id;\n this.host.requestUpdate();\n\n if (previousId !== id && this.config.onTickle) {\n this.config.onTickle(id);\n }\n\n if (previousId !== id && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n\n /**\n * Clear the tickled state (e.g., on pointer leave).\n * Note: For pointer leave events, prefer using handlePointerLeave() which\n * checks for actual mouse movement.\n */\n clearTickled(): void {\n if (this._tickledId !== undefined) {\n this._tickledId = undefined;\n this.host.requestUpdate();\n\n if (this.config.onTickle) {\n this.config.onTickle(undefined);\n }\n\n if (this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n\n /**\n * Manually set the input mode.\n * When switching to keyboard mode, resets the mouse movement tracking.\n */\n setInputMode(mode: InputMode): void {\n if (this._inputMode !== mode) {\n this._inputMode = mode;\n this.host.requestUpdate();\n\n if (mode === `keyboard`) {\n TickledController._mouseMovedSinceKeyboard = false;\n }\n\n if (this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n}\n","import { css } from 'lit';\n\n/**\n * Shared CSS styles for tickled interaction pattern.\n *\n * Tickled state is controlled via the `.tickled` CSS class (applied by TickledController).\n * The `tickledItemStyles` provides base styling that all tickled components should use.\n *\n * ## CSS Variable Contract\n *\n * **Style variables (default values defined in `tickledItemStyles`):**\n * - `--item-bg`: Default background (default: transparent)\n * - `--item-bg-tickled`: Background when tickled (default: rgba(0, 0, 0, 0.06))\n * - `--item-bg-selected`: Background when selected (default: rgba(25, 118, 210, 1))\n * - `--item-bg-selected-tickled`: Background when selected AND tickled (default: rgba(21, 101, 192, 1))\n * - `--item-border`: Border style (default: 1px solid transparent)\n * - `--item-border-tickled`: Border when tickled (default: 1px solid var(--accent))\n * - `--item-border-selected`: Border when selected (default: 1px solid rgba(25, 118, 210, 1))\n * - `--item-text-tickled`: Text color when tickled (default: inherit)\n * - `--item-text-selected`: Text color when selected (default: #fff)\n * - `--item-padding`: Item padding (default: 4px 8px)\n * - `--item-border-radius`: Border radius (default: 4px)\n * - `--item-icon-spacing`: Gap between icon and text (default: 4px)\n *\n * ## Usage\n *\n * Components should import both `tickledStyles` and `tickledItemStyles`:\n *\n * ```ts\n * import { tickledStyles, tickledItemStyles } from '../interaction/index.js';\n *\n * class MyComponent extends LitElement {\n * static styles = [\n * tickledStyles,\n * tickledItemStyles,\n * css`\n * :host {\n * --item-bg-tickled: #e0e0e0;\n * --item-bg-selected: #1976d2;\n * }\n * `\n * ];\n * }\n * ```\n */\n\nexport const tickledStyles = css`\n /**\n * No suppression needed - tickled styling is always active.\n */\n`;\n\n/**\n * Shared item styling for tickled components.\n *\n * Provides base styles for interactive items with tickled and selected states.\n * Components should import this in addition to `tickledStyles` and can override\n * CSS variables to customize appearance.\n */\nexport const tickledItemStyles = css`\n :host {\n --item-bg: transparent;\n --item-bg-tickled: rgba(0, 0, 0, 0.06);\n --item-bg-selected: rgba(25, 118, 210, 1);\n --item-bg-selected-tickled: rgba(21, 101, 192, 1);\n --item-border: 1px solid transparent;\n --item-border-tickled: 1px solid var(--accent);\n --item-border-selected: 1px solid rgba(25, 118, 210, 1);\n --item-text: inherit;\n --item-text-tickled: inherit;\n --item-text-selected: #fff;\n --item-padding: 4px 8px;\n --item-border-radius: 4px;\n --item-icon-spacing: 4px;\n }\n\n .item {\n display: inline-flex;\n align-items: center;\n gap: var(--item-icon-spacing);\n padding: var(--item-padding);\n background: var(--item-bg);\n border: var(--item-border);\n border-radius: var(--item-border-radius);\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n transition: background var(--transition, 0.1s ease-out), border-color var(--transition, 0.1s ease-out);\n }\n\n .item.tickled {\n background: var(--item-bg-tickled);\n border-color: var(--item-border-tickled);\n color: var(--item-text-tickled);\n }\n\n .item.selected {\n background: var(--item-bg-selected);\n border-color: var(--item-border-selected);\n color: var(--item-text-selected);\n }\n\n .item.selected.tickled {\n background: var(--item-bg-selected-tickled);\n outline: 2px dashed var(--surface-3);\n outline-offset: -2px;\n }\n`;"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tickled-styles-Bg3QbcrD.js","names":[],"sources":["../src/interaction/tickled-controller.ts","../src/interaction/tickled-styles.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\n\n/**\n * Represents an item that can be tickled (highlighted via hover or keyboard navigation)\n */\nexport type TickledItem<T = unknown> = {\n /**\n * Unique identifier for the item (e.g., index, id, path)\n */\n readonly id: T;\n\n /**\n * Whether the item is currently enabled/interactive\n */\n readonly enabled?: boolean;\n};\n\n/**\n * Input mode determines which mechanism controls the tickled state\n */\nexport type InputMode = `keyboard` | `mouse`;\n\n/**\n * Configuration for TickledController\n */\nexport type TickledControllerConfig<T> = {\n /**\n * Function to retrieve all items that can be tickled\n */\n getItems: () => ReadonlyArray<TickledItem<T>>;\n\n /**\n * Optional callback when an item becomes tickled\n * Use this for side effects like opening submenus\n * @param id - The id of the newly tickled item, or undefined if nothing is tickled\n */\n onTickle?: (id: T | undefined) => void;\n\n /**\n * Optional callback fired after any state change (tickled id or input mode)\n * Use this to sync DOM attributes for slotted content that can't use reactive rendering\n *\n * For components that render items directly, this is usually not needed as\n * Lit's reactive rendering will handle updates automatically via requestUpdate()\n *\n * @example\n * ```ts\n * onStateChange: () => {\n * // Sync data-tickled attributes for slotted items\n * const items = this.getItems();\n * items.forEach((item, i) => {\n * if (this.tickled.isTickled(i)) {\n * item.setAttribute('data-tickled', '');\n * } else {\n * item.removeAttribute('data-tickled');\n * }\n * });\n * }\n * ```\n */\n onStateChange?: () => void;\n\n /**\n * Optional initial input mode (defaults to 'mouse')\n */\n initialInputMode?: InputMode;\n};\n\n/**\n * ReactiveController that manages \"tickled\" state for interactive lists.\n *\n * A \"tickled\" item is one that is under active attention via hover (mouse mode)\n * or keyboard focus (keyboard mode). Only one item can be tickled at a time.\n *\n * The input mode determines which mechanism controls tickled state:\n * - Mouse mode: hover controls tickled, keyboard navigation ignored\n * - Keyboard mode: keyboard navigation controls tickled, hover ignored\n *\n * Last interaction wins - any mouse hover switches to mouse mode, any keyboard\n * navigation switches to keyboard mode.\n *\n * ## Global Mouse Tracking\n *\n * This controller uses a singleton global `pointermove` listener to track actual\n * mouse movement. This prevents `pointerenter` events caused by content scrolling\n * from incorrectly switching to mouse mode when the user is actually using keyboard\n * navigation.\n *\n * @example\n * ```ts\n * class MyList extends LitElement {\n * private tickled = new TickledController(this, {\n * getItems: () => this.items.map((item, i) => ({ id: i, enabled: !item.disabled })),\n * onTickle: (id) => {\n * // Optional: perform side effect when item is tickled\n * console.log('Tickled item:', id);\n * }\n * });\n *\n * render() {\n * return html`\n * <ul @pointerleave=${(e) => this.tickled.handlePointerLeave(e)}>\n * ${this.items.map((item, i) => html`\n * <li\n * class=\"item ${this.tickled.isTickled(i) ? 'tickled' : ''}\"\n * @pointerenter=${() => this.tickled.handlePointerEnter(i)}\n * >${item.label}</li>\n * `)}\n * </ul>\n * `;\n * }\n *\n * private handleKeyDown(e: KeyboardEvent) {\n * if (e.key === 'ArrowDown') {\n * this.tickled.handleKeyboardNavigation('next');\n * } else if (e.key === 'ArrowUp') {\n * this.tickled.handleKeyboardNavigation('previous');\n * }\n * }\n * }\n * ```\n */\nexport class TickledController<T = number> implements ReactiveController {\n private host: ReactiveControllerHost;\n private config: TickledControllerConfig<T>;\n private _tickledId: T | undefined = undefined;\n private _inputMode: InputMode;\n\n private static _lastMouseClientX: number = Number.NaN;\n private static _lastMouseClientY: number = Number.NaN;\n private static _mouseMovedSinceKeyboard: boolean = true;\n private static _globalListenerAttached: boolean = false;\n\n constructor(host: ReactiveControllerHost, config: TickledControllerConfig<T>) {\n this.host = host;\n this.config = config;\n this._inputMode = config.initialInputMode ?? `mouse`;\n host.addController(this);\n }\n\n hostConnected(): void {\n if (!TickledController._globalListenerAttached) {\n document.addEventListener(`pointermove`, (e: PointerEvent) => {\n TickledController.handleGlobalPointerMove(e.clientX, e.clientY);\n }, { passive: true });\n TickledController._globalListenerAttached = true;\n }\n }\n\n hostDisconnected(): void {\n // No-op: global listener persists for the lifetime of the page\n }\n\n /**\n * Get the current input mode\n */\n get inputMode(): InputMode {\n return this._inputMode;\n }\n\n /**\n * Get the currently tickled item id\n */\n get tickledId(): T | undefined {\n return this._tickledId;\n }\n\n /**\n * Check if a specific item is tickled\n */\n isTickled(id: T): boolean {\n return this._tickledId === id;\n }\n\n /**\n * Handle global pointer move event.\n * Called automatically by the global listener.\n * Tracks mouse movement to distinguish real hover from content scrolling.\n */\n private static handleGlobalPointerMove(clientX: number, clientY: number): void {\n if (!Number.isNaN(TickledController._lastMouseClientX)) {\n if (\n clientX !== TickledController._lastMouseClientX\n || clientY !== TickledController._lastMouseClientY\n ) {\n TickledController._mouseMovedSinceKeyboard = true;\n }\n }\n TickledController._lastMouseClientX = clientX;\n TickledController._lastMouseClientY = clientY;\n }\n\n /**\n * Check if actual mouse movement has occurred since last keyboard interaction.\n * Used to distinguish real hover/leave from content-scrolling-induced events.\n */\n static get hasMouseMovedSinceKeyboard(): boolean {\n return TickledController._mouseMovedSinceKeyboard;\n }\n\n /**\n * Handle pointer enter event on an item (mouse hover).\n * Only processes if actual mouse movement is detected (not content scrolling).\n * Switches to mouse mode and tickles the item.\n */\n handlePointerEnter(id: T): void {\n if (!TickledController._mouseMovedSinceKeyboard) {\n return;\n }\n\n const items = this.config.getItems();\n const item = items.find(item => item.id === id);\n if (item && item.enabled === false) {\n return;\n }\n\n const modeChanged = this._inputMode !== `mouse`;\n this._inputMode = `mouse`;\n\n const previousId = this._tickledId;\n this._tickledId = id;\n\n this.host.requestUpdate();\n\n if (previousId !== id && this.config.onTickle) {\n this.config.onTickle(id);\n }\n\n if ((previousId !== id || modeChanged) && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n\n /**\n * Handle pointer leave event on an item.\n * Only clears tickled state if actual mouse movement is detected.\n * @param _e The pointer event (not used, but kept for API compatibility)\n */\n handlePointerLeave(_e: PointerEvent): void {\n if (TickledController._mouseMovedSinceKeyboard) {\n this.clearTickled();\n }\n }\n\n /**\n * Handle keyboard navigation.\n * Switches to keyboard mode and moves tickled state.\n *\n * @param direction - 'next', 'previous', 'first', 'last', or specific id\n */\n handleKeyboardNavigation(direction: `next` | `previous` | `first` | `last` | T): void {\n const modeChanged = this._inputMode !== `keyboard`;\n this._inputMode = `keyboard`;\n\n const items = this.config.getItems();\n const enabledItems = items.filter(item => item.enabled !== false);\n\n if (enabledItems.length === 0) {\n return;\n }\n\n let newId: T | undefined;\n\n if (direction === `first`) {\n newId = enabledItems[0].id;\n } else if (direction === `last`) {\n newId = enabledItems.at(-1)?.id;\n } else if (direction === `next` || direction === `previous`) {\n const currentIndex = enabledItems.findIndex(item => item.id === this._tickledId);\n\n if (currentIndex === -1) {\n newId = direction === `next` ? enabledItems[0].id : enabledItems.at(-1)?.id;\n } else {\n const delta = direction === `next` ? 1 : -1;\n const newIndex = currentIndex + delta;\n\n if (newIndex >= 0 && newIndex < enabledItems.length) {\n newId = enabledItems[newIndex].id;\n }\n // If newIndex is out of bounds, keep current tickled item\n }\n } else {\n const item = enabledItems.find(item => item.id === direction);\n if (item) {\n newId = item.id;\n }\n }\n\n if (newId !== undefined) {\n const previousId = this._tickledId;\n this._tickledId = newId;\n this.host.requestUpdate();\n\n if (previousId !== newId && this.config.onTickle) {\n this.config.onTickle(newId);\n }\n\n if ((previousId !== newId || modeChanged) && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n\n /**\n * Manually set the tickled item (useful for programmatic control).\n * Does NOT change input mode.\n */\n setTickled(id: T | undefined): void {\n const previousId = this._tickledId;\n this._tickledId = id;\n this.host.requestUpdate();\n\n if (previousId !== id && this.config.onTickle) {\n this.config.onTickle(id);\n }\n\n if (previousId !== id && this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n\n /**\n * Clear the tickled state (e.g., on pointer leave).\n * Note: For pointer leave events, prefer using handlePointerLeave() which\n * checks for actual mouse movement.\n */\n clearTickled(): void {\n if (this._tickledId !== undefined) {\n this._tickledId = undefined;\n this.host.requestUpdate();\n\n if (this.config.onTickle) {\n this.config.onTickle(undefined);\n }\n\n if (this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n\n /**\n * Manually set the input mode.\n * When switching to keyboard mode, resets the mouse movement tracking.\n */\n setInputMode(mode: InputMode): void {\n if (this._inputMode !== mode) {\n this._inputMode = mode;\n this.host.requestUpdate();\n\n if (mode === `keyboard`) {\n TickledController._mouseMovedSinceKeyboard = false;\n }\n\n if (this.config.onStateChange) {\n this.config.onStateChange();\n }\n }\n }\n}\n","import { css } from 'lit';\n\n/**\n * Shared CSS styles for tickled interaction pattern.\n *\n * Tickled state is controlled via the `.tickled` CSS class (applied by TickledController).\n * The `tickledItemStyles` provides base styling that all tickled components should use.\n *\n * ## CSS Variable Contract\n *\n * **Style variables (default values defined in `tickledItemStyles`):**\n * - `--item-bg`: Default background (default: transparent)\n * - `--item-bg-tickled`: Background when tickled (default: rgba(0, 0, 0, 0.06))\n * - `--item-bg-selected`: Background when selected (default: rgba(25, 118, 210, 1))\n * - `--item-bg-selected-tickled`: Background when selected AND tickled (default: rgba(21, 101, 192, 1))\n * - `--item-border`: Border style (default: 1px solid transparent)\n * - `--item-border-tickled`: Border when tickled (default: 1px solid var(--accent))\n * - `--item-border-selected`: Border when selected (default: 1px solid rgba(25, 118, 210, 1))\n * - `--item-text-tickled`: Text color when tickled (default: inherit)\n * - `--item-text-selected`: Text color when selected (default: #fff)\n * - `--item-padding`: Item padding (default: 4px 8px)\n * - `--item-border-radius`: Border radius (default: 4px)\n * - `--item-icon-spacing`: Gap between icon and text (default: 4px)\n *\n * ## Usage\n *\n * Components should import both `tickledStyles` and `tickledItemStyles`:\n *\n * ```ts\n * import { tickledStyles, tickledItemStyles } from '../interaction/index.js';\n *\n * class MyComponent extends LitElement {\n * static styles = [\n * tickledStyles,\n * tickledItemStyles,\n * css`\n * :host {\n * --item-bg-tickled: #e0e0e0;\n * --item-bg-selected: #1976d2;\n * }\n * `\n * ];\n * }\n * ```\n */\n\nexport const tickledStyles = css`\n /**\n * No suppression needed - tickled styling is always active.\n */\n`;\n\n/**\n * Shared item styling for tickled components.\n *\n * Provides base styles for interactive items with tickled and selected states.\n * Components should import this in addition to `tickledStyles` and can override\n * CSS variables to customize appearance.\n */\nexport const tickledItemStyles = css`\n :host {\n --item-bg: transparent;\n --item-bg-tickled: rgba(0, 0, 0, 0.06);\n --item-bg-selected: rgba(25, 118, 210, 1);\n --item-bg-selected-tickled: rgba(21, 101, 192, 1);\n --item-border: 1px solid transparent;\n --item-border-tickled: 1px solid var(--accent);\n --item-border-selected: 1px solid rgba(25, 118, 210, 1);\n --item-text: inherit;\n --item-text-tickled: inherit;\n --item-text-selected: #fff;\n --item-padding: 4px 8px;\n --item-border-radius: 4px;\n --item-icon-spacing: 4px;\n }\n\n .item {\n display: inline-flex;\n align-items: center;\n gap: var(--item-icon-spacing);\n padding: var(--item-padding);\n background: var(--item-bg);\n border: var(--item-border);\n border-radius: var(--item-border-radius);\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n transition: background var(--transition, 0.1s ease-out), border-color var(--transition, 0.1s ease-out);\n }\n\n .item.tickled {\n background: var(--item-bg-tickled);\n border-color: var(--item-border-tickled);\n color: var(--item-text-tickled);\n }\n\n .item.selected {\n background: var(--item-bg-selected);\n border-color: var(--item-border-selected);\n color: var(--item-text-selected);\n }\n\n .item.selected.tickled {\n background: var(--item-bg-selected-tickled);\n outline: 2px dashed var(--surface-3);\n outline-offset: -2px;\n }\n`;"],"mappings":"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"}
|
|
@@ -1,7 +1,71 @@
|
|
|
1
|
-
import { t as __decorate } from "./decorate-
|
|
1
|
+
import { n as safeCustomElement, t as __decorate } from "./decorate-D7rC1gLP.js";
|
|
2
2
|
import { LitElement, css, html, nothing } from "lit";
|
|
3
|
-
import {
|
|
3
|
+
import { property, state } from "lit/decorators.js";
|
|
4
4
|
import "@clinth/vcanvas";
|
|
5
|
+
//#region src/timeline/timeline-controller.ts
|
|
6
|
+
/**
|
|
7
|
+
* External adapter class that can configure a timeline element
|
|
8
|
+
* with musical time, custom label formatting, etc.
|
|
9
|
+
*
|
|
10
|
+
* The controller has no import of any timeline element type.
|
|
11
|
+
* Users create a controller, pass it the component instance,
|
|
12
|
+
* and the controller calls the component's public API.
|
|
13
|
+
*/
|
|
14
|
+
var TimelineController = class {
|
|
15
|
+
#host;
|
|
16
|
+
#config;
|
|
17
|
+
#bpm = 120;
|
|
18
|
+
#timeSignatureNumerator = 4;
|
|
19
|
+
#timeSignatureDenominator = 4;
|
|
20
|
+
constructor(timeline, config) {
|
|
21
|
+
this.#host = timeline;
|
|
22
|
+
this.#config = config;
|
|
23
|
+
timeline.min = config.logicalMin;
|
|
24
|
+
timeline.max = config.logicalMax;
|
|
25
|
+
if (config.labelFormatter) timeline.formatLabel = config.labelFormatter;
|
|
26
|
+
timeline.addEventListener(`track-select`, this.#onTrackSelect);
|
|
27
|
+
}
|
|
28
|
+
/** Update the tempo (BPM) for musical time calculations. */
|
|
29
|
+
setTempo(bpm) {
|
|
30
|
+
this.#bpm = bpm;
|
|
31
|
+
this.#updateFormatLabel();
|
|
32
|
+
}
|
|
33
|
+
/** Update the time signature for musical time calculations. */
|
|
34
|
+
setTimeSignature(numerator, denominator) {
|
|
35
|
+
this.#timeSignatureNumerator = numerator;
|
|
36
|
+
this.#timeSignatureDenominator = denominator;
|
|
37
|
+
this.#updateFormatLabel();
|
|
38
|
+
}
|
|
39
|
+
/** Set the visible view range on the timeline. */
|
|
40
|
+
setViewRange(range) {
|
|
41
|
+
this.#host.setViewRange(range);
|
|
42
|
+
}
|
|
43
|
+
/** Clean up any resources held by the controller. */
|
|
44
|
+
dispose() {
|
|
45
|
+
this.#host.removeEventListener(`track-select`, this.#onTrackSelect);
|
|
46
|
+
}
|
|
47
|
+
#onTrackSelect = (e) => {
|
|
48
|
+
const detail = e.detail;
|
|
49
|
+
const trackEventIds = this.#host.getTrackEvents(detail.trackKey).map((e) => e.id);
|
|
50
|
+
if (detail.shiftKey) {
|
|
51
|
+
const current = new Set(this.#host.selectedIds);
|
|
52
|
+
for (const id of trackEventIds) if (current.has(id)) current.delete(id);
|
|
53
|
+
else current.add(id);
|
|
54
|
+
this.#host.selectedIds = [...current];
|
|
55
|
+
} else this.#host.selectedIds = trackEventIds;
|
|
56
|
+
};
|
|
57
|
+
#updateFormatLabel() {
|
|
58
|
+
if (this.#config.labelFormatter) return;
|
|
59
|
+
this.#bpm;
|
|
60
|
+
const num = this.#timeSignatureNumerator;
|
|
61
|
+
this.#host.formatLabel = (v) => {
|
|
62
|
+
const beatsPerBar = num;
|
|
63
|
+
const totalBeats = v;
|
|
64
|
+
return `${Math.floor(totalBeats / beatsPerBar) + 1}.${Math.floor(totalBeats % beatsPerBar) + 1}`;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
5
69
|
//#region src/timeline/timeline-draw.ts
|
|
6
70
|
const NICE_STEPS = [
|
|
7
71
|
1,
|
|
@@ -12,15 +76,15 @@ const NICE_STEPS = [
|
|
|
12
76
|
/**
|
|
13
77
|
* Dispatch to adaptive or fixed grid mode.
|
|
14
78
|
*/
|
|
15
|
-
|
|
79
|
+
function resolveGrid(viewRange, userScale, viewportWidth, mode = `adaptive`, fixedMinorStep, ppu) {
|
|
16
80
|
if (mode === `fixed` && fixedMinorStep !== void 0 && ppu !== void 0) return resolveFixedGrid(fixedMinorStep, ppu);
|
|
17
81
|
return resolveAdaptiveGrid(viewRange, userScale, viewportWidth);
|
|
18
|
-
}
|
|
82
|
+
}
|
|
19
83
|
/**
|
|
20
84
|
* Compute a nice major/minor grid resolution for the current view.
|
|
21
85
|
* Uses a 1-2-5-10 "nice number" algorithm.
|
|
22
86
|
*/
|
|
23
|
-
|
|
87
|
+
function resolveAdaptiveGrid(viewRange, userScale, viewportWidth) {
|
|
24
88
|
const span = viewRange.max - viewRange.min;
|
|
25
89
|
if (span <= 0 || viewportWidth <= 0) return {
|
|
26
90
|
majorStep: 100,
|
|
@@ -29,7 +93,7 @@ const resolveAdaptiveGrid = (viewRange, userScale, viewportWidth) => {
|
|
|
29
93
|
};
|
|
30
94
|
const targetPx = 80 * scaleMultiplier(userScale);
|
|
31
95
|
const rawStep = span / Math.max(2, Math.floor(viewportWidth / targetPx));
|
|
32
|
-
const magnitude =
|
|
96
|
+
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
|
|
33
97
|
let bestStep = magnitude;
|
|
34
98
|
for (const n of NICE_STEPS) {
|
|
35
99
|
const candidate = n * magnitude;
|
|
@@ -46,14 +110,14 @@ const resolveAdaptiveGrid = (viewRange, userScale, viewportWidth) => {
|
|
|
46
110
|
minorDivisions,
|
|
47
111
|
minorStep
|
|
48
112
|
};
|
|
49
|
-
}
|
|
113
|
+
}
|
|
50
114
|
/**
|
|
51
115
|
* Fixed grid: user specifies a base minor step in logical units.
|
|
52
116
|
* If the minor step at current zoom would produce lines < 5px apart,
|
|
53
117
|
* double the effective step until they're >= 5px.
|
|
54
118
|
* Major = 4× effective minor.
|
|
55
119
|
*/
|
|
56
|
-
|
|
120
|
+
function resolveFixedGrid(baseMinorStep, ppu) {
|
|
57
121
|
const MIN_PX_BETWEEN = 5;
|
|
58
122
|
let effectiveMinor = baseMinorStep;
|
|
59
123
|
while (effectiveMinor * ppu < MIN_PX_BETWEEN && effectiveMinor < 0xe8d4a51000) effectiveMinor *= 2;
|
|
@@ -63,8 +127,8 @@ const resolveFixedGrid = (baseMinorStep, ppu) => {
|
|
|
63
127
|
minorDivisions,
|
|
64
128
|
minorStep: effectiveMinor
|
|
65
129
|
};
|
|
66
|
-
}
|
|
67
|
-
|
|
130
|
+
}
|
|
131
|
+
function scaleMultiplier(scale) {
|
|
68
132
|
switch (scale) {
|
|
69
133
|
case `x-wide`: return 2;
|
|
70
134
|
case `wide`: return 1.5;
|
|
@@ -73,8 +137,8 @@ const scaleMultiplier = (scale) => {
|
|
|
73
137
|
case `x-slim`: return .5;
|
|
74
138
|
default: return 1;
|
|
75
139
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
140
|
+
}
|
|
141
|
+
function drawMajorMinorGrid(ctx, region, resolution, altMajorBg, vars, ppu) {
|
|
78
142
|
const { majorStep, minorStep } = resolution;
|
|
79
143
|
if (majorStep <= 0) return;
|
|
80
144
|
const regionEnd = region.x + region.width;
|
|
@@ -88,7 +152,7 @@ const drawMajorMinorGrid = (ctx, region, resolution, altMajorBg, vars, ppu) => {
|
|
|
88
152
|
}
|
|
89
153
|
if (minorStep > 0) {
|
|
90
154
|
ctx.strokeStyle = vars[`--timeline-grid-minor-color`] ?? `rgba(255,255,255,0.05)`;
|
|
91
|
-
ctx.lineWidth = parseFloat(vars[`--timeline-grid-minor-width`] ?? `0.5`);
|
|
155
|
+
ctx.lineWidth = Number.parseFloat(vars[`--timeline-grid-minor-width`] ?? `0.5`);
|
|
92
156
|
ctx.beginPath();
|
|
93
157
|
const firstMinor = Math.floor(region.x / minorStep) * minorStep;
|
|
94
158
|
for (let val = firstMinor; val < regionEnd; val += minorStep) {
|
|
@@ -100,7 +164,7 @@ const drawMajorMinorGrid = (ctx, region, resolution, altMajorBg, vars, ppu) => {
|
|
|
100
164
|
ctx.stroke();
|
|
101
165
|
}
|
|
102
166
|
ctx.strokeStyle = vars[`--timeline-grid-major-color`] ?? `rgba(255,255,255,0.12)`;
|
|
103
|
-
ctx.lineWidth = parseFloat(vars[`--timeline-grid-major-width`] ?? `1`);
|
|
167
|
+
ctx.lineWidth = Number.parseFloat(vars[`--timeline-grid-major-width`] ?? `1`);
|
|
104
168
|
ctx.beginPath();
|
|
105
169
|
const firstMajor = Math.floor(region.x / majorStep) * majorStep;
|
|
106
170
|
for (let val = firstMajor; val < regionEnd; val += majorStep) {
|
|
@@ -109,13 +173,13 @@ const drawMajorMinorGrid = (ctx, region, resolution, altMajorBg, vars, ppu) => {
|
|
|
109
173
|
ctx.lineTo(px, region.y + region.height);
|
|
110
174
|
}
|
|
111
175
|
ctx.stroke();
|
|
112
|
-
}
|
|
176
|
+
}
|
|
113
177
|
/**
|
|
114
178
|
* Draw the axis header band. With 2D virtualization the axis is "sticky" —
|
|
115
179
|
* it is always drawn at the current vertical scroll position (`scrollTop`)
|
|
116
180
|
* so it appears fixed at the viewport top.
|
|
117
181
|
*/
|
|
118
|
-
|
|
182
|
+
function drawAxis(ctx, region, resolution, axisHeight, formatLabel, vars, ppu, scrollTop = 0) {
|
|
119
183
|
const { majorStep } = resolution;
|
|
120
184
|
if (majorStep <= 0 || axisHeight <= 0) return;
|
|
121
185
|
const axisTop = scrollTop;
|
|
@@ -127,7 +191,7 @@ const drawAxis = (ctx, region, resolution, axisHeight, formatLabel, vars, ppu, s
|
|
|
127
191
|
const bgLeft = region.x * ppu;
|
|
128
192
|
ctx.fillRect(bgLeft, axisTop, region.width * ppu, bgHeight);
|
|
129
193
|
ctx.strokeStyle = vars[`--timeline-grid-major-color`] ?? `rgba(255,255,255,0.12)`;
|
|
130
|
-
ctx.lineWidth = parseFloat(vars[`--timeline-grid-major-width`] ?? `1`);
|
|
194
|
+
ctx.lineWidth = Number.parseFloat(vars[`--timeline-grid-major-width`] ?? `1`);
|
|
131
195
|
ctx.beginPath();
|
|
132
196
|
const firstMajor = Math.ceil(region.x / majorStep) * majorStep;
|
|
133
197
|
for (let val = firstMajor; val < regionEnd; val += majorStep) {
|
|
@@ -152,8 +216,8 @@ const drawAxis = (ctx, region, resolution, axisHeight, formatLabel, vars, ppu, s
|
|
|
152
216
|
ctx.fillText(formatLabel(val), px + labelPadding, axisTop + bgHeight / 2);
|
|
153
217
|
}
|
|
154
218
|
ctx.textAlign = `start`;
|
|
155
|
-
}
|
|
156
|
-
|
|
219
|
+
}
|
|
220
|
+
function drawTrackBackgrounds(ctx, region, trackLayout, tracks, vars, ppu) {
|
|
157
221
|
const bgDefault = vars[`--timeline-track-bg`] ?? `#1a1a1a`;
|
|
158
222
|
const bgAlt = vars[`--timeline-track-bg-alt`] ?? `#1e1e1e`;
|
|
159
223
|
for (let i = 0; i < trackLayout.length; i++) {
|
|
@@ -161,7 +225,7 @@ const drawTrackBackgrounds = (ctx, region, trackLayout, tracks, vars, ppu) => {
|
|
|
161
225
|
if (layout.y + layout.height < region.y) continue;
|
|
162
226
|
if (layout.y > region.y + region.height) break;
|
|
163
227
|
const track = tracks.find((t) => t.key === layout.trackKey);
|
|
164
|
-
ctx.fillStyle = track?.color ? track.color
|
|
228
|
+
ctx.fillStyle = track?.color ? `${track.color}22` : i % 2 === 0 ? bgDefault : bgAlt;
|
|
165
229
|
ctx.fillRect(region.x * ppu, layout.y, region.width * ppu, layout.height);
|
|
166
230
|
ctx.strokeStyle = vars[`--timeline-grid-major-color`] ?? `rgba(255,255,255,0.12)`;
|
|
167
231
|
ctx.lineWidth = .5;
|
|
@@ -170,13 +234,13 @@ const drawTrackBackgrounds = (ctx, region, trackLayout, tracks, vars, ppu) => {
|
|
|
170
234
|
ctx.lineTo((region.x + region.width) * ppu, layout.y + layout.height);
|
|
171
235
|
ctx.stroke();
|
|
172
236
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
237
|
+
}
|
|
238
|
+
function drawEventRects(ctx, region, events, trackLayout, selectedIds, tickledId, drag, vars, ppu) {
|
|
175
239
|
const eventBg = vars[`--timeline-event-bg`] ?? `#3498db`;
|
|
176
240
|
const eventSelectedBg = vars[`--timeline-event-selected-bg`] ?? `#5dade2`;
|
|
177
241
|
const eventTickledBg = vars[`--timeline-event-tickled-bg`] ?? `#444`;
|
|
178
242
|
const eventLabelColor = vars[`--timeline-event-label-color`] ?? `#fff`;
|
|
179
|
-
const radius = parseFloat(vars[`--timeline-event-radius`] ?? `4`);
|
|
243
|
+
const radius = Number.parseFloat(vars[`--timeline-event-radius`] ?? `4`);
|
|
180
244
|
const regionEnd = region.x + region.width;
|
|
181
245
|
const padding = 3;
|
|
182
246
|
for (const evt of events) {
|
|
@@ -230,8 +294,8 @@ const drawEventRects = (ctx, region, events, trackLayout, selectedIds, tickledId
|
|
|
230
294
|
ctx.restore();
|
|
231
295
|
}
|
|
232
296
|
}
|
|
233
|
-
}
|
|
234
|
-
|
|
297
|
+
}
|
|
298
|
+
function drawMarquee(ctx, drag, ppu) {
|
|
235
299
|
if (drag.kind !== `marquee`) return;
|
|
236
300
|
const x1 = Math.min(drag.startLogicalX, drag.currentLogicalX) * ppu;
|
|
237
301
|
const x2 = Math.max(drag.startLogicalX, drag.currentLogicalX) * ppu;
|
|
@@ -242,8 +306,8 @@ const drawMarquee = (ctx, drag, ppu) => {
|
|
|
242
306
|
ctx.strokeStyle = `rgba(100, 150, 255, 0.6)`;
|
|
243
307
|
ctx.lineWidth = 1;
|
|
244
308
|
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
|
245
|
-
}
|
|
246
|
-
|
|
309
|
+
}
|
|
310
|
+
function drawRoundRect(ctx, x, y, w, h, r) {
|
|
247
311
|
r = Math.max(0, r);
|
|
248
312
|
ctx.moveTo(x + r, y);
|
|
249
313
|
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
@@ -251,7 +315,7 @@ const drawRoundRect = (ctx, x, y, w, h, r) => {
|
|
|
251
315
|
ctx.arcTo(x, y + h, x, y, r);
|
|
252
316
|
ctx.arcTo(x, y, x + w, y, r);
|
|
253
317
|
ctx.closePath();
|
|
254
|
-
}
|
|
318
|
+
}
|
|
255
319
|
const TIMELINE_CSS_VARS = [
|
|
256
320
|
`--timeline-bg`,
|
|
257
321
|
`--timeline-track-bg`,
|
|
@@ -268,12 +332,12 @@ const TIMELINE_CSS_VARS = [
|
|
|
268
332
|
`--timeline-event-radius`,
|
|
269
333
|
`--timeline-axis-label-color`
|
|
270
334
|
];
|
|
271
|
-
|
|
335
|
+
function resolveCssVars(host) {
|
|
272
336
|
const computed = getComputedStyle(host);
|
|
273
337
|
const vars = {};
|
|
274
338
|
for (const v of TIMELINE_CSS_VARS) vars[v] = computed.getPropertyValue(v).trim();
|
|
275
339
|
return vars;
|
|
276
|
-
}
|
|
340
|
+
}
|
|
277
341
|
//#endregion
|
|
278
342
|
//#region src/timeline/timeline-interactions.ts
|
|
279
343
|
/** Hit zone near the edge of an event (px in screen space) */
|
|
@@ -282,13 +346,17 @@ const EDGE_HIT_PX = 8;
|
|
|
282
346
|
* Convert a vcanvas logical (pixel) coordinate to timeline logical units.
|
|
283
347
|
* vcanvas pixel coordinates are in the 1:1 pixel space; divide by ppu then add min.
|
|
284
348
|
*/
|
|
285
|
-
|
|
349
|
+
function canvasToTimeline(canvasX, ppu, min) {
|
|
350
|
+
return canvasX / ppu + min;
|
|
351
|
+
}
|
|
286
352
|
/**
|
|
287
353
|
* Convert screen-space pixel distance to timeline logical units.
|
|
288
354
|
* Used for edge hit threshold: how many logical units does EDGE_HIT_PX correspond to.
|
|
289
355
|
*/
|
|
290
|
-
|
|
291
|
-
|
|
356
|
+
function pxToLogicalUnits(ppu) {
|
|
357
|
+
return ppu > 0 ? 1 / ppu : 1;
|
|
358
|
+
}
|
|
359
|
+
function hitTest(logicalX, y, events, trackLayout, ppu) {
|
|
292
360
|
const track = trackLayout.find((tl) => y >= tl.y && y < tl.y + tl.height);
|
|
293
361
|
if (!track) return {
|
|
294
362
|
kind: `empty`,
|
|
@@ -321,17 +389,19 @@ const hitTest = (logicalX, y, events, trackLayout, ppu) => {
|
|
|
321
389
|
y,
|
|
322
390
|
trackKey: track.trackKey
|
|
323
391
|
};
|
|
324
|
-
}
|
|
325
|
-
|
|
392
|
+
}
|
|
393
|
+
function trackAtY(y, trackLayout) {
|
|
326
394
|
return trackLayout.find((tl) => y >= tl.y && y < tl.y + tl.height)?.trackKey;
|
|
327
|
-
}
|
|
395
|
+
}
|
|
328
396
|
/** Sensitivity: how many pixels of vertical drag = 1 e-fold of zoom */
|
|
329
397
|
const ZOOM_SENSITIVITY_PX = 100;
|
|
330
398
|
/**
|
|
331
399
|
* Subtract a deadzone from a value.
|
|
332
400
|
* Returns 0 if |value| <= deadzone, otherwise value shifted toward zero by deadzone.
|
|
333
401
|
*/
|
|
334
|
-
|
|
402
|
+
function applyDeadzone(value, deadzone) {
|
|
403
|
+
return Math.abs(value) > deadzone ? value - Math.sign(value) * deadzone : 0;
|
|
404
|
+
}
|
|
335
405
|
/**
|
|
336
406
|
* Compute new ppu + viewMin from an axis-zoom drag.
|
|
337
407
|
* Vertical delta (dy) controls zoom (up = zoom in, down = zoom out), anchored at `anchorUnit`.
|
|
@@ -339,7 +409,7 @@ const applyDeadzone = (value, deadzone) => Math.abs(value) > deadzone ? value -
|
|
|
339
409
|
*
|
|
340
410
|
* Returns { ppu, viewMin } — the caller uses host.setZoomPan() to apply.
|
|
341
411
|
*/
|
|
342
|
-
|
|
412
|
+
function computeAxisZoom(startViewMin, startPpu, anchorUnit, dx, dy) {
|
|
343
413
|
const newPpu = startPpu * Math.exp(-dy / ZOOM_SENSITIVITY_PX);
|
|
344
414
|
let newMin = anchorUnit - (anchorUnit - startViewMin) * startPpu / newPpu;
|
|
345
415
|
if (newPpu > 0) newMin += -dx / newPpu;
|
|
@@ -347,7 +417,7 @@ const computeAxisZoom = (startViewMin, startPpu, anchorUnit, dx, dy) => {
|
|
|
347
417
|
ppu: newPpu,
|
|
348
418
|
viewMin: newMin
|
|
349
419
|
};
|
|
350
|
-
}
|
|
420
|
+
}
|
|
351
421
|
/**
|
|
352
422
|
* Compute new vertical view range from a v-axis-zoom drag.
|
|
353
423
|
* Horizontal delta (dx) controls zoom (right = zoom in, left = zoom out),
|
|
@@ -356,7 +426,7 @@ const computeAxisZoom = (startViewMin, startPpu, anchorUnit, dx, dy) => {
|
|
|
356
426
|
*
|
|
357
427
|
* Returns { vMin, vMax } — the caller uses host.setVerticalZoomPan() to apply.
|
|
358
428
|
*/
|
|
359
|
-
|
|
429
|
+
function computeVerticalAxisZoom(startVMin, startVMax, startVScale, anchorBaseY, dx, dy) {
|
|
360
430
|
const startSpan = startVMax - startVMin;
|
|
361
431
|
const newSpan = startSpan * Math.exp(-dx / ZOOM_SENSITIVITY_PX);
|
|
362
432
|
let newMin = anchorBaseY - (startSpan > 0 ? (anchorBaseY - startVMin) / startSpan : .5) * newSpan;
|
|
@@ -365,12 +435,12 @@ const computeVerticalAxisZoom = (startVMin, startVMax, startVScale, anchorBaseY,
|
|
|
365
435
|
vMin: newMin,
|
|
366
436
|
vMax: newMin + newSpan
|
|
367
437
|
};
|
|
368
|
-
}
|
|
438
|
+
}
|
|
369
439
|
/**
|
|
370
440
|
* Attaches pointer and keyboard interaction handlers to the timeline.
|
|
371
441
|
* Returns a cleanup function.
|
|
372
442
|
*/
|
|
373
|
-
|
|
443
|
+
function attachTimelineInteractions(host, vcanvasEl) {
|
|
374
444
|
const ac = new AbortController();
|
|
375
445
|
const signal = ac.signal;
|
|
376
446
|
vcanvasEl.addEventListener(`pointerdown`, (e) => {
|
|
@@ -431,7 +501,6 @@ const attachTimelineInteractions = (host, vcanvasEl) => {
|
|
|
431
501
|
currentLogicalX: lx,
|
|
432
502
|
currentY: ly
|
|
433
503
|
};
|
|
434
|
-
return;
|
|
435
504
|
}
|
|
436
505
|
}, { signal });
|
|
437
506
|
vcanvasEl.addEventListener(`pointermove`, (e) => {
|
|
@@ -504,7 +573,6 @@ const attachTimelineInteractions = (host, vcanvasEl) => {
|
|
|
504
573
|
end: newEnd
|
|
505
574
|
} : ev);
|
|
506
575
|
vcanvasEl.invalidateAll();
|
|
507
|
-
return;
|
|
508
576
|
}
|
|
509
577
|
}, { signal });
|
|
510
578
|
vcanvasEl.addEventListener(`pointerup`, (e) => {
|
|
@@ -577,8 +645,8 @@ const attachTimelineInteractions = (host, vcanvasEl) => {
|
|
|
577
645
|
}
|
|
578
646
|
}, { signal });
|
|
579
647
|
return () => ac.abort();
|
|
580
|
-
}
|
|
581
|
-
|
|
648
|
+
}
|
|
649
|
+
function handleHover(host, vcanvasEl, e) {
|
|
582
650
|
const ppu = host.pxPerUnit;
|
|
583
651
|
const canvasCoord = vcanvasEl.convertFromViewportToCanvas(e.offsetX, e.offsetY);
|
|
584
652
|
const lx = canvasToTimeline(canvasCoord.x, ppu, host.min);
|
|
@@ -602,22 +670,22 @@ const handleHover = (host, vcanvasEl, e) => {
|
|
|
602
670
|
host._tickledId = newTickledId;
|
|
603
671
|
vcanvasEl.invalidateAll();
|
|
604
672
|
}
|
|
605
|
-
}
|
|
606
|
-
|
|
673
|
+
}
|
|
674
|
+
function toggleSelection(host, eventId) {
|
|
607
675
|
const next = new Set(host._selectedIds);
|
|
608
676
|
if (next.has(eventId)) next.delete(eventId);
|
|
609
677
|
else next.add(eventId);
|
|
610
678
|
host._selectedIds = next;
|
|
611
679
|
fireSelectionChange(host);
|
|
612
680
|
host.getVCanvas()?.invalidateAll();
|
|
613
|
-
}
|
|
614
|
-
|
|
681
|
+
}
|
|
682
|
+
function fireSelectionChange(host) {
|
|
615
683
|
host.dispatchEvent(new CustomEvent(`selection-change`, {
|
|
616
684
|
detail: { selectedIds: [...host._selectedIds] },
|
|
617
685
|
bubbles: true,
|
|
618
686
|
composed: true
|
|
619
687
|
}));
|
|
620
|
-
}
|
|
688
|
+
}
|
|
621
689
|
//#endregion
|
|
622
690
|
//#region src/timeline/timeline-styles.ts
|
|
623
691
|
const timelineStyles = css`
|
|
@@ -941,7 +1009,7 @@ __decorate([property({
|
|
|
941
1009
|
type: Boolean,
|
|
942
1010
|
reflect: true
|
|
943
1011
|
})], TimelineTrackTabElement.prototype, "resizable", void 0);
|
|
944
|
-
TimelineTrackTabElement = __decorate([
|
|
1012
|
+
TimelineTrackTabElement = __decorate([safeCustomElement(`ixfx-timeline-track-tab`)], TimelineTrackTabElement);
|
|
945
1013
|
//#endregion
|
|
946
1014
|
//#region src/timeline/timeline-element.ts
|
|
947
1015
|
/** Reasonable maximum ppu to prevent infinite zoom-in */
|
|
@@ -1677,72 +1745,8 @@ __decorate([state()], IxfxTimelineElement.prototype, "_trackHeightOverrides", vo
|
|
|
1677
1745
|
__decorate([state()], IxfxTimelineElement.prototype, "_trackLayout", void 0);
|
|
1678
1746
|
__decorate([state()], IxfxTimelineElement.prototype, "_internalEvents", void 0);
|
|
1679
1747
|
__decorate([state()], IxfxTimelineElement.prototype, "_internalTracks", void 0);
|
|
1680
|
-
IxfxTimelineElement = __decorate([
|
|
1681
|
-
//#endregion
|
|
1682
|
-
//#region src/timeline/timeline-controller.ts
|
|
1683
|
-
/**
|
|
1684
|
-
* External adapter class that can configure a timeline element
|
|
1685
|
-
* with musical time, custom label formatting, etc.
|
|
1686
|
-
*
|
|
1687
|
-
* The controller has no import of any timeline element type.
|
|
1688
|
-
* Users create a controller, pass it the component instance,
|
|
1689
|
-
* and the controller calls the component's public API.
|
|
1690
|
-
*/
|
|
1691
|
-
var TimelineController = class {
|
|
1692
|
-
#host;
|
|
1693
|
-
#config;
|
|
1694
|
-
#bpm = 120;
|
|
1695
|
-
#timeSignatureNumerator = 4;
|
|
1696
|
-
#timeSignatureDenominator = 4;
|
|
1697
|
-
constructor(timeline, config) {
|
|
1698
|
-
this.#host = timeline;
|
|
1699
|
-
this.#config = config;
|
|
1700
|
-
timeline.min = config.logicalMin;
|
|
1701
|
-
timeline.max = config.logicalMax;
|
|
1702
|
-
if (config.labelFormatter) timeline.formatLabel = config.labelFormatter;
|
|
1703
|
-
timeline.addEventListener(`track-select`, this.#onTrackSelect);
|
|
1704
|
-
}
|
|
1705
|
-
/** Update the tempo (BPM) for musical time calculations. */
|
|
1706
|
-
setTempo(bpm) {
|
|
1707
|
-
this.#bpm = bpm;
|
|
1708
|
-
this.#updateFormatLabel();
|
|
1709
|
-
}
|
|
1710
|
-
/** Update the time signature for musical time calculations. */
|
|
1711
|
-
setTimeSignature(numerator, denominator) {
|
|
1712
|
-
this.#timeSignatureNumerator = numerator;
|
|
1713
|
-
this.#timeSignatureDenominator = denominator;
|
|
1714
|
-
this.#updateFormatLabel();
|
|
1715
|
-
}
|
|
1716
|
-
/** Set the visible view range on the timeline. */
|
|
1717
|
-
setViewRange(range) {
|
|
1718
|
-
this.#host.setViewRange(range);
|
|
1719
|
-
}
|
|
1720
|
-
/** Clean up any resources held by the controller. */
|
|
1721
|
-
dispose() {
|
|
1722
|
-
this.#host.removeEventListener(`track-select`, this.#onTrackSelect);
|
|
1723
|
-
}
|
|
1724
|
-
#onTrackSelect = (e) => {
|
|
1725
|
-
const detail = e.detail;
|
|
1726
|
-
const trackEventIds = this.#host.getTrackEvents(detail.trackKey).map((e) => e.id);
|
|
1727
|
-
if (detail.shiftKey) {
|
|
1728
|
-
const current = new Set(this.#host.selectedIds);
|
|
1729
|
-
for (const id of trackEventIds) if (current.has(id)) current.delete(id);
|
|
1730
|
-
else current.add(id);
|
|
1731
|
-
this.#host.selectedIds = [...current];
|
|
1732
|
-
} else this.#host.selectedIds = trackEventIds;
|
|
1733
|
-
};
|
|
1734
|
-
#updateFormatLabel() {
|
|
1735
|
-
if (this.#config.labelFormatter) return;
|
|
1736
|
-
this.#bpm;
|
|
1737
|
-
const num = this.#timeSignatureNumerator;
|
|
1738
|
-
this.#host.formatLabel = (v) => {
|
|
1739
|
-
const beatsPerBar = num;
|
|
1740
|
-
const totalBeats = v;
|
|
1741
|
-
return `${Math.floor(totalBeats / beatsPerBar) + 1}.${Math.floor(totalBeats % beatsPerBar) + 1}`;
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
};
|
|
1748
|
+
IxfxTimelineElement = __decorate([safeCustomElement(`ixfx-timeline`)], IxfxTimelineElement);
|
|
1745
1749
|
//#endregion
|
|
1746
|
-
export {
|
|
1750
|
+
export { resolveCssVars as a, attachTimelineInteractions as i, TimelineTrackTabElement as n, resolveGrid as o, timelineStyles as r, TimelineController as s, IxfxTimelineElement as t };
|
|
1747
1751
|
|
|
1748
|
-
//# sourceMappingURL=timeline-
|
|
1752
|
+
//# sourceMappingURL=timeline-Cv75Th2U.js.map
|