@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.
Files changed (216) hide show
  1. package/bundle/index.d.ts +365 -143
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2330 -826
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +2 -3
  6. package/dist/ac-text.d.ts.map +1 -1
  7. package/dist/ac-text.js +5 -5
  8. package/dist/ac-text.js.map +1 -1
  9. package/dist/{button-DgKUzE5o.js → button-DMt5XWUK.js} +6 -6
  10. package/dist/button-DMt5XWUK.js.map +1 -0
  11. package/dist/button.d.ts +3 -4
  12. package/dist/button.d.ts.map +1 -1
  13. package/dist/button.js +1 -1
  14. package/dist/checkbox.d.ts +3 -5
  15. package/dist/checkbox.d.ts.map +1 -1
  16. package/dist/checkbox.js +3 -3
  17. package/dist/checkbox.js.map +1 -1
  18. package/dist/{colour-C3MQIjFJ-BJV-QC-3.js → colour-C3MQIjFJ-BLXg8W3U.js} +2 -2
  19. package/dist/{colour-C3MQIjFJ-BJV-QC-3.js.map → colour-C3MQIjFJ-BLXg8W3U.js.map} +1 -1
  20. package/dist/{colour-picker-Bw-AZoWx.js → colour-picker-K6zu82lm.js} +8 -8
  21. package/dist/colour-picker-K6zu82lm.js.map +1 -0
  22. package/dist/colour-picker.d.ts +2 -2
  23. package/dist/colour-picker.js +1 -1
  24. package/dist/crumbs.d.ts +4 -6
  25. package/dist/crumbs.d.ts.map +1 -1
  26. package/dist/crumbs.js +12 -12
  27. package/dist/crumbs.js.map +1 -1
  28. package/dist/data-display.d.ts +1 -2
  29. package/dist/data-display.d.ts.map +1 -1
  30. package/dist/data-display.js +4 -5
  31. package/dist/data-display.js.map +1 -1
  32. package/dist/decorate-D7rC1gLP.js +27 -0
  33. package/dist/decorate-D7rC1gLP.js.map +1 -0
  34. package/dist/{defaults-FEZaGiYZ.js → defaults-C7elhuyJ.js} +2 -2
  35. package/dist/{defaults-FEZaGiYZ.js.map → defaults-C7elhuyJ.js.map} +1 -1
  36. package/dist/{dist-By5URG0Y.js → dist-BX0OVIm7.js} +2 -2
  37. package/dist/{dist-By5URG0Y.js.map → dist-BX0OVIm7.js.map} +1 -1
  38. package/dist/{dist-D7kJgnSE.js → dist-TAGYwaju.js} +1 -1
  39. package/dist/{dist-D7kJgnSE.js.map → dist-TAGYwaju.js.map} +1 -1
  40. package/dist/editable-label.d.ts +6 -1
  41. package/dist/editable-label.d.ts.map +1 -1
  42. package/dist/editable-label.js +51 -4
  43. package/dist/editable-label.js.map +1 -1
  44. package/dist/{hex-editor-Wd6ZtZA2.js → hex-editor-CsN_ySkH.js} +87 -87
  45. package/dist/hex-editor-CsN_ySkH.js.map +1 -0
  46. package/dist/{hex-editor-Gib1Grg2.d.ts → hex-editor-DlfoA65y.d.ts} +7 -8
  47. package/dist/hex-editor-DlfoA65y.d.ts.map +1 -0
  48. package/dist/hex.d.ts +1 -1
  49. package/dist/hex.js +1 -1
  50. package/dist/{highlight-CAjwQ4GE.js → highlight-TPbSHruI.js} +3 -3
  51. package/dist/highlight-TPbSHruI.js.map +1 -0
  52. package/dist/{icon-BR5aTIiP.js → icon-9NWTXtE1.js} +6 -7
  53. package/dist/icon-9NWTXtE1.js.map +1 -0
  54. package/dist/{icon-DQKdfR7U.d.ts → icon-CeTDJhC7.d.ts} +3 -4
  55. package/dist/icon-CeTDJhC7.d.ts.map +1 -0
  56. package/dist/icons.d.ts +2 -2
  57. package/dist/icons.js +3 -3
  58. package/dist/icons.js.map +1 -1
  59. package/dist/{incr-search-BjCNClnC.js → incr-search-DSKKsk6w.js} +119 -117
  60. package/dist/incr-search-DSKKsk6w.js.map +1 -0
  61. package/dist/incr-search.d.ts +2 -2
  62. package/dist/incr-search.js +2 -2
  63. package/dist/{index-CPVcTTXo.d.ts → index--hrxPBkQ.d.ts} +73 -74
  64. package/dist/index--hrxPBkQ.d.ts.map +1 -0
  65. package/dist/index-B1hNR7Z9.d.ts.map +1 -1
  66. package/dist/{index-CpbBI3n7.d.ts → index-CnL9krsH.d.ts} +3 -4
  67. package/dist/index-CnL9krsH.d.ts.map +1 -0
  68. package/dist/{index-CQxanqXF.d.ts → index-DSlPeWLK.d.ts} +28 -29
  69. package/dist/index-DSlPeWLK.d.ts.map +1 -0
  70. package/dist/{index-C5dK8RZa.d.ts → index-DrQjA8Gs.d.ts} +3 -4
  71. package/dist/index-DrQjA8Gs.d.ts.map +1 -0
  72. package/dist/{index-C9iBN908.d.ts → index-hXFn14tM.d.ts} +32 -32
  73. package/dist/index-hXFn14tM.d.ts.map +1 -0
  74. package/dist/index.d.ts +246 -32
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1464 -32
  77. package/dist/index.js.map +1 -1
  78. package/dist/keyboard-De_vOHLg.js.map +1 -1
  79. package/dist/{labelled-input-base-DS6-6C0r.js → labelled-input-base-CqPL5rKm.js} +2 -2
  80. package/dist/labelled-input-base-CqPL5rKm.js.map +1 -0
  81. package/dist/{labelled-input-base-DnRh4reU.d.ts → labelled-input-base-D-Y7agky.d.ts} +2 -3
  82. package/dist/labelled-input-base-D-Y7agky.d.ts.map +1 -0
  83. package/dist/labelled-radial-input.d.ts +4 -5
  84. package/dist/labelled-radial-input.d.ts.map +1 -1
  85. package/dist/labelled-radial-input.js +5 -5
  86. package/dist/labelled-radial-input.js.map +1 -1
  87. package/dist/labelled-range-input.d.ts +1 -1
  88. package/dist/labelled-range-input.js +4 -4
  89. package/dist/labelled-range-input.js.map +1 -1
  90. package/dist/led.d.ts +2 -3
  91. package/dist/led.d.ts.map +1 -1
  92. package/dist/led.js +3 -3
  93. package/dist/led.js.map +1 -1
  94. package/dist/{menu-BV6UFx_z.js → menu-DJJuCm-5.js} +55 -55
  95. package/dist/menu-DJJuCm-5.js.map +1 -0
  96. package/dist/{menu-item-DEzL_Yjk.js → menu-item-CKRfP0pN.js} +14 -14
  97. package/dist/menu-item-CKRfP0pN.js.map +1 -0
  98. package/dist/{menu-item-Cikacp4F.d.ts → menu-item-ClQF2DKN.d.ts} +7 -8
  99. package/dist/menu-item-ClQF2DKN.d.ts.map +1 -0
  100. package/dist/menu.d.ts +3 -3
  101. package/dist/menu.js +2 -2
  102. package/dist/miller.d.ts +7 -10
  103. package/dist/miller.d.ts.map +1 -1
  104. package/dist/miller.js +17 -17
  105. package/dist/miller.js.map +1 -1
  106. package/dist/narrowed-text.d.ts +2 -3
  107. package/dist/narrowed-text.d.ts.map +1 -1
  108. package/dist/narrowed-text.js +6 -6
  109. package/dist/narrowed-text.js.map +1 -1
  110. package/dist/panel.d.ts +2 -3
  111. package/dist/panel.d.ts.map +1 -1
  112. package/dist/panel.js +4 -4
  113. package/dist/panel.js.map +1 -1
  114. package/dist/plots.d.ts +1 -1
  115. package/dist/plots.js +1 -1
  116. package/dist/polar-pad.d.ts +1 -2
  117. package/dist/polar-pad.d.ts.map +1 -1
  118. package/dist/polar-pad.js +3 -3
  119. package/dist/polar-pad.js.map +1 -1
  120. package/dist/{popup-Ddx9LCzY.js → popup-ImtS9KnS.js} +1 -1
  121. package/dist/{popup-Ddx9LCzY.js.map → popup-ImtS9KnS.js.map} +1 -1
  122. package/dist/{prominence-CreDBdCs.js → prominence--ic9uWBl.js} +1 -1
  123. package/dist/{prominence-CreDBdCs.js.map → prominence--ic9uWBl.js.map} +1 -1
  124. package/dist/{radial-input-CNJ8asY2.js → radial-input-DZA6WaFu.js} +16 -16
  125. package/dist/radial-input-DZA6WaFu.js.map +1 -0
  126. package/dist/{radial-input-xSTd4kEz.d.ts → radial-input-LUSBEycd.d.ts} +2 -3
  127. package/dist/radial-input-LUSBEycd.d.ts.map +1 -0
  128. package/dist/radial-input.d.ts +1 -1
  129. package/dist/radial-input.js +1 -1
  130. package/dist/range-input.d.ts +2 -3
  131. package/dist/range-input.d.ts.map +1 -1
  132. package/dist/range-input.js +3 -3
  133. package/dist/range-input.js.map +1 -1
  134. package/dist/range.d.ts +2 -3
  135. package/dist/range.d.ts.map +1 -1
  136. package/dist/range.js +4 -4
  137. package/dist/range.js.map +1 -1
  138. package/dist/{registry-CZEB1YI9.js → registry-Q2gYQCHM.js} +1 -1
  139. package/dist/{registry-CZEB1YI9.js.map → registry-Q2gYQCHM.js.map} +1 -1
  140. package/dist/selecthorizontal.d.ts +2 -3
  141. package/dist/selecthorizontal.d.ts.map +1 -1
  142. package/dist/selecthorizontal.js +3 -3
  143. package/dist/selecthorizontal.js.map +1 -1
  144. package/dist/snap-container.d.ts +1 -2
  145. package/dist/snap-container.d.ts.map +1 -1
  146. package/dist/snap-container.js +3 -3
  147. package/dist/snap-container.js.map +1 -1
  148. package/dist/split-layout.d.ts +2 -3
  149. package/dist/split-layout.d.ts.map +1 -1
  150. package/dist/split-layout.js +6 -6
  151. package/dist/split-layout.js.map +1 -1
  152. package/dist/swipe.d.ts +1 -2
  153. package/dist/swipe.d.ts.map +1 -1
  154. package/dist/swipe.js +4 -4
  155. package/dist/swipe.js.map +1 -1
  156. package/dist/{tab-list-CFF7uSYH.d.ts → tab-list-BnqISNvO.d.ts} +5 -6
  157. package/dist/tab-list-BnqISNvO.d.ts.map +1 -0
  158. package/dist/tabs.d.ts +1 -1
  159. package/dist/tabs.js +4 -4
  160. package/dist/tabs.js.map +1 -1
  161. package/dist/tickled-controller-Cq1Va0rR.d.ts.map +1 -1
  162. package/dist/tickled-styles-Bg3QbcrD.js.map +1 -1
  163. package/dist/{timeline-DBWJsSjd.js → timeline-Cv75Th2U.js} +123 -119
  164. package/dist/timeline-Cv75Th2U.js.map +1 -0
  165. package/dist/timeline.d.ts +2 -2
  166. package/dist/timeline.js +1 -1
  167. package/dist/{tooltip-kfUBWYVB.d.ts → tooltip-CwGfb4lp.d.ts} +2 -3
  168. package/dist/tooltip-CwGfb4lp.d.ts.map +1 -0
  169. package/dist/tree-CvvKbO-f.js +1330 -0
  170. package/dist/tree-CvvKbO-f.js.map +1 -0
  171. package/dist/{tree-component-CQpoPo4s.d.ts → tree-component-D8Bg48tt.d.ts} +12 -12
  172. package/dist/tree-component-D8Bg48tt.d.ts.map +1 -0
  173. package/dist/tree.d.ts +4 -5
  174. package/dist/tree.d.ts.map +1 -1
  175. package/dist/tree.js +1 -1143
  176. package/dist/types-BKaqnpwx.d.ts.map +1 -1
  177. package/dist/{xy-axis-CIFok7MF.d.ts → xy-axis-D9uwpGTT.d.ts} +5 -6
  178. package/dist/xy-axis-D9uwpGTT.d.ts.map +1 -0
  179. package/dist/{xy-axis-BANbgrI5.js → xy-axis-DL0tfJDr.js} +13 -13
  180. package/dist/xy-axis-DL0tfJDr.js.map +1 -0
  181. package/dist/xy-pad.d.ts +1 -2
  182. package/dist/xy-pad.d.ts.map +1 -1
  183. package/dist/xy-pad.js +4 -4
  184. package/dist/xy-pad.js.map +1 -1
  185. package/package.json +17 -13
  186. package/dist/button-DgKUzE5o.js.map +0 -1
  187. package/dist/colour-picker-Bw-AZoWx.js.map +0 -1
  188. package/dist/decorate-BZbkqIhA.js +0 -9
  189. package/dist/hex-editor-Gib1Grg2.d.ts.map +0 -1
  190. package/dist/hex-editor-Wd6ZtZA2.js.map +0 -1
  191. package/dist/highlight-CAjwQ4GE.js.map +0 -1
  192. package/dist/icon-BR5aTIiP.js.map +0 -1
  193. package/dist/icon-DQKdfR7U.d.ts.map +0 -1
  194. package/dist/incr-search-BjCNClnC.js.map +0 -1
  195. package/dist/index-C5dK8RZa.d.ts.map +0 -1
  196. package/dist/index-C9iBN908.d.ts.map +0 -1
  197. package/dist/index-CPVcTTXo.d.ts.map +0 -1
  198. package/dist/index-CQxanqXF.d.ts.map +0 -1
  199. package/dist/index-CpbBI3n7.d.ts.map +0 -1
  200. package/dist/labelled-input-base-DS6-6C0r.js.map +0 -1
  201. package/dist/labelled-input-base-DnRh4reU.d.ts.map +0 -1
  202. package/dist/menu-BV6UFx_z.js.map +0 -1
  203. package/dist/menu-item-Cikacp4F.d.ts.map +0 -1
  204. package/dist/menu-item-DEzL_Yjk.js.map +0 -1
  205. package/dist/radial-input-CNJ8asY2.js.map +0 -1
  206. package/dist/radial-input-xSTd4kEz.d.ts.map +0 -1
  207. package/dist/tab-list-CFF7uSYH.d.ts.map +0 -1
  208. package/dist/timeline-DBWJsSjd.js.map +0 -1
  209. package/dist/tooltip-kfUBWYVB.d.ts.map +0 -1
  210. package/dist/tree-component-CQpoPo4s.d.ts.map +0 -1
  211. package/dist/tree-data-model-DGvRVOFY.js +0 -190
  212. package/dist/tree-data-model-DGvRVOFY.js.map +0 -1
  213. package/dist/tree.js.map +0 -1
  214. package/dist/xy-axis-BANbgrI5.js.map +0 -1
  215. package/dist/xy-axis-CIFok7MF.d.ts.map +0 -1
  216. /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-BZbkqIhA.js";
1
+ import { n as safeCustomElement, t as __decorate } from "./decorate-D7rC1gLP.js";
2
2
  import { LitElement, css, html, nothing } from "lit";
3
- import { customElement, property, state } from "lit/decorators.js";
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
- const resolveGrid = (viewRange, userScale, viewportWidth, mode = `adaptive`, fixedMinorStep, ppu) => {
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
- const resolveAdaptiveGrid = (viewRange, userScale, viewportWidth) => {
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 = Math.pow(10, Math.floor(Math.log10(rawStep)));
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
- const resolveFixedGrid = (baseMinorStep, ppu) => {
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
- const scaleMultiplier = (scale) => {
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
- const drawMajorMinorGrid = (ctx, region, resolution, altMajorBg, vars, ppu) => {
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
- const drawAxis = (ctx, region, resolution, axisHeight, formatLabel, vars, ppu, scrollTop = 0) => {
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
- const drawTrackBackgrounds = (ctx, region, trackLayout, tracks, vars, ppu) => {
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 + `22` : i % 2 === 0 ? bgDefault : bgAlt;
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
- const drawEventRects = (ctx, region, events, trackLayout, selectedIds, tickledId, drag, vars, ppu) => {
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
- const drawMarquee = (ctx, drag, ppu) => {
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
- const drawRoundRect = (ctx, x, y, w, h, r) => {
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
- const resolveCssVars = (host) => {
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
- const canvasToTimeline = (canvasX, ppu, min) => canvasX / ppu + min;
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
- const pxToLogicalUnits = (ppu) => ppu > 0 ? 1 / ppu : 1;
291
- const hitTest = (logicalX, y, events, trackLayout, ppu) => {
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
- const trackAtY = (y, trackLayout) => {
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
- const applyDeadzone = (value, deadzone) => Math.abs(value) > deadzone ? value - Math.sign(value) * deadzone : 0;
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
- const computeAxisZoom = (startViewMin, startPpu, anchorUnit, dx, dy) => {
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
- const computeVerticalAxisZoom = (startVMin, startVMax, startVScale, anchorBaseY, dx, dy) => {
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
- const attachTimelineInteractions = (host, vcanvasEl) => {
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
- const handleHover = (host, vcanvasEl, e) => {
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
- const toggleSelection = (host, eventId) => {
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
- const fireSelectionChange = (host) => {
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([customElement(`ixfx-timeline-track-tab`)], TimelineTrackTabElement);
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([customElement(`ixfx-timeline`)], IxfxTimelineElement);
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 { attachTimelineInteractions as a, timelineStyles as i, IxfxTimelineElement as n, resolveCssVars as o, TimelineTrackTabElement as r, resolveGrid as s, TimelineController as t };
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-DBWJsSjd.js.map
1752
+ //# sourceMappingURL=timeline-Cv75Th2U.js.map