@r2digisolutions/components 0.1.6 → 0.2.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 (389) hide show
  1. package/README.md +1 -1
  2. package/dist/components/atoms/Checkbox/Checkbox.svelte +10 -3
  3. package/dist/components/atoms/Checkbox/Checkbox.svelte.d.ts +3 -0
  4. package/dist/components/atoms/IconButton/IconButton.svelte +1 -1
  5. package/dist/components/atoms/Input/Input.svelte +17 -2
  6. package/dist/components/atoms/Input/Input.svelte.d.ts +6 -1
  7. package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte +21 -0
  8. package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte.d.ts +8 -0
  9. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.stories.js +8 -0
  10. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.svelte +122 -0
  11. package/dist/components/atoms/MediaKindIcon/MediaKindIcon.svelte.d.ts +11 -0
  12. package/dist/components/atoms/MediaKindIcon/MediaKindIconStory.svelte +12 -0
  13. package/dist/components/atoms/MediaKindIcon/MediaKindIconStory.svelte.d.ts +18 -0
  14. package/dist/components/atoms/PanKnob/PanKnob.stories.d.ts +9 -0
  15. package/dist/components/atoms/PanKnob/PanKnob.stories.js +8 -0
  16. package/dist/components/atoms/PanKnob/PanKnob.svelte +218 -0
  17. package/dist/components/atoms/PanKnob/PanKnob.svelte.d.ts +19 -0
  18. package/dist/components/atoms/PanKnob/PanKnobStory.svelte +15 -0
  19. package/dist/components/atoms/PanKnob/PanKnobStory.svelte.d.ts +3 -0
  20. package/dist/components/atoms/Playhead/Playhead.stories.js +8 -0
  21. package/dist/components/atoms/Playhead/Playhead.svelte +59 -0
  22. package/dist/components/atoms/Playhead/Playhead.svelte.d.ts +13 -0
  23. package/dist/components/atoms/Playhead/PlayheadStory.svelte +9 -0
  24. package/dist/components/atoms/Playhead/PlayheadStory.svelte.d.ts +18 -0
  25. package/dist/components/atoms/Textarea/Textarea.svelte +4 -1
  26. package/dist/components/atoms/Textarea/Textarea.svelte.d.ts +1 -0
  27. package/dist/components/atoms/TimeRuler/TimeRuler.stories.js +8 -0
  28. package/dist/components/atoms/TimeRuler/TimeRuler.svelte +58 -0
  29. package/dist/components/atoms/TimeRuler/TimeRuler.svelte.d.ts +10 -0
  30. package/dist/components/atoms/TimeRuler/TimeRulerStory.svelte +7 -0
  31. package/dist/components/atoms/TimeRuler/TimeRulerStory.svelte.d.ts +18 -0
  32. package/dist/components/atoms/Toggle/Toggle.svelte +1 -1
  33. package/dist/components/atoms/Tooltip/Tooltip.svelte +1 -1
  34. package/dist/components/atoms/TrimHandle/TrimHandle.stories.js +8 -0
  35. package/dist/components/atoms/TrimHandle/TrimHandle.svelte +63 -0
  36. package/dist/components/atoms/TrimHandle/TrimHandle.svelte.d.ts +12 -0
  37. package/dist/components/atoms/TrimHandle/TrimHandleStory.svelte +11 -0
  38. package/dist/components/atoms/TrimHandle/TrimHandleStory.svelte.d.ts +18 -0
  39. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.stories.d.ts +9 -0
  40. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.stories.js +8 -0
  41. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.svelte +102 -0
  42. package/dist/components/molecules/AiAssistPanel/AiAssistPanel.svelte.d.ts +20 -0
  43. package/dist/components/molecules/AiAssistPanel/AiAssistPanelStory.svelte +34 -0
  44. package/dist/components/molecules/AiAssistPanel/AiAssistPanelStory.svelte.d.ts +3 -0
  45. package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte +112 -62
  46. package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte.d.ts +2 -0
  47. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.stories.js +8 -0
  48. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.svelte +117 -0
  49. package/dist/components/molecules/BulkEditPanel/BulkEditPanel.svelte.d.ts +23 -0
  50. package/dist/components/molecules/BulkEditPanel/BulkEditPanelStory.svelte +18 -0
  51. package/dist/components/molecules/BulkEditPanel/BulkEditPanelStory.svelte.d.ts +18 -0
  52. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.stories.js +8 -0
  53. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.svelte +858 -0
  54. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanel.svelte.d.ts +43 -0
  55. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanelStory.svelte +7 -0
  56. package/dist/components/molecules/CanvasElementsPanel/CanvasElementsPanelStory.svelte.d.ts +18 -0
  57. package/dist/components/molecules/CanvasInspector/CanvasInspector.stories.d.ts +9 -0
  58. package/dist/components/molecules/CanvasInspector/CanvasInspector.stories.js +8 -0
  59. package/dist/components/molecules/CanvasInspector/CanvasInspector.svelte +1140 -0
  60. package/dist/components/molecules/CanvasInspector/CanvasInspector.svelte.d.ts +28 -0
  61. package/dist/components/molecules/CanvasInspector/CanvasInspectorStory.svelte +20 -0
  62. package/dist/components/molecules/CanvasInspector/CanvasInspectorStory.svelte.d.ts +3 -0
  63. package/dist/components/molecules/CodeEditor/CodeEditor.svelte +13 -10
  64. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +167 -97
  65. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.stories.d.ts +9 -0
  66. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.stories.js +8 -0
  67. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.svelte +108 -0
  68. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenu.svelte.d.ts +18 -0
  69. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenuStory.svelte +15 -0
  70. package/dist/components/molecules/ColumnVisibilityMenu/ColumnVisibilityMenuStory.svelte.d.ts +3 -0
  71. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.d.ts +9 -0
  72. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.js +8 -0
  73. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte +412 -0
  74. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte.d.ts +14 -0
  75. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte +27 -0
  76. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte.d.ts +3 -0
  77. package/dist/components/molecules/ConflictResolver/ConflictResolver.stories.js +8 -0
  78. package/dist/components/molecules/ConflictResolver/ConflictResolver.svelte +73 -0
  79. package/dist/components/molecules/ConflictResolver/ConflictResolver.svelte.d.ts +16 -0
  80. package/dist/components/molecules/ConflictResolver/ConflictResolverStory.svelte +11 -0
  81. package/dist/components/molecules/ConflictResolver/ConflictResolverStory.svelte.d.ts +18 -0
  82. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +237 -41
  83. package/dist/components/molecules/ContextMenu/ContextMenu.svelte.d.ts +17 -1
  84. package/dist/components/molecules/DateTimePicker/DateTimePicker.svelte.d.ts +1 -1
  85. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.stories.d.ts +9 -0
  86. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.stories.js +8 -0
  87. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.svelte +56 -0
  88. package/dist/components/molecules/ExpressionEditor/ExpressionEditor.svelte.d.ts +12 -0
  89. package/dist/components/molecules/ExpressionEditor/ExpressionEditorStory.svelte +12 -0
  90. package/dist/components/molecules/ExpressionEditor/ExpressionEditorStory.svelte.d.ts +3 -0
  91. package/dist/components/molecules/FilterList/FilterList.stories.d.ts +9 -0
  92. package/dist/components/molecules/FilterList/FilterList.stories.js +8 -0
  93. package/dist/components/molecules/FilterList/FilterList.svelte +101 -0
  94. package/dist/components/molecules/FilterList/FilterList.svelte.d.ts +18 -0
  95. package/dist/components/molecules/FilterList/FilterListStory.svelte +31 -0
  96. package/dist/components/molecules/FilterList/FilterListStory.svelte.d.ts +3 -0
  97. package/dist/components/molecules/FormDateTimePicker/FormDateTimePicker.svelte.d.ts +1 -1
  98. package/dist/components/molecules/Gauge/Gauge.stories.d.ts +1 -1
  99. package/dist/components/molecules/InfiniteScroll/InfiniteScroll.stories.d.ts +1 -1
  100. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.stories.d.ts +9 -0
  101. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.stories.js +8 -0
  102. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte +94 -0
  103. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowser.svelte.d.ts +15 -0
  104. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowserStory.svelte +18 -0
  105. package/dist/components/molecules/MediaAssetBrowser/MediaAssetBrowserStory.svelte.d.ts +3 -0
  106. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.stories.js +8 -0
  107. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.svelte +50 -0
  108. package/dist/components/molecules/MediaAssetCard/MediaAssetCard.svelte.d.ts +11 -0
  109. package/dist/components/molecules/MediaAssetCard/MediaAssetCardStory.svelte +13 -0
  110. package/dist/components/molecules/MediaAssetCard/MediaAssetCardStory.svelte.d.ts +18 -0
  111. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.stories.js +8 -0
  112. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.svelte +380 -0
  113. package/dist/components/molecules/MediaLayerItem/MediaLayerItem.svelte.d.ts +33 -0
  114. package/dist/components/molecules/MediaLayerItem/MediaLayerItemStory.svelte +14 -0
  115. package/dist/components/molecules/MediaLayerItem/MediaLayerItemStory.svelte.d.ts +18 -0
  116. package/dist/components/molecules/MediaLayerList/MediaLayerList.stories.d.ts +9 -0
  117. package/dist/components/molecules/MediaLayerList/MediaLayerList.stories.js +8 -0
  118. package/dist/components/molecules/MediaLayerList/MediaLayerList.svelte +399 -0
  119. package/dist/components/molecules/MediaLayerList/MediaLayerList.svelte.d.ts +37 -0
  120. package/dist/components/molecules/MediaLayerList/MediaLayerListStory.svelte +24 -0
  121. package/dist/components/molecules/MediaLayerList/MediaLayerListStory.svelte.d.ts +3 -0
  122. package/dist/components/molecules/MediaStage/MediaStage.stories.d.ts +9 -0
  123. package/dist/components/molecules/MediaStage/MediaStage.stories.js +8 -0
  124. package/dist/components/molecules/MediaStage/MediaStage.svelte +1498 -0
  125. package/dist/components/molecules/MediaStage/MediaStage.svelte.d.ts +45 -0
  126. package/dist/components/molecules/MediaStage/MediaStageStory.svelte +35 -0
  127. package/dist/components/molecules/MediaStage/MediaStageStory.svelte.d.ts +3 -0
  128. package/dist/components/molecules/MediaTimeline/MediaTimeline.stories.d.ts +9 -0
  129. package/dist/components/molecules/MediaTimeline/MediaTimeline.stories.js +8 -0
  130. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte +131 -0
  131. package/dist/components/molecules/MediaTimeline/MediaTimeline.svelte.d.ts +19 -0
  132. package/dist/components/molecules/MediaTimeline/MediaTimelineStory.svelte +20 -0
  133. package/dist/components/molecules/MediaTimeline/MediaTimelineStory.svelte.d.ts +3 -0
  134. package/dist/components/molecules/MixerChannel/MixerChannel.stories.d.ts +9 -0
  135. package/dist/components/molecules/MixerChannel/MixerChannel.stories.js +8 -0
  136. package/dist/components/molecules/MixerChannel/MixerChannel.svelte +92 -0
  137. package/dist/components/molecules/MixerChannel/MixerChannel.svelte.d.ts +11 -0
  138. package/dist/components/molecules/MixerChannel/MixerChannelStory.svelte +13 -0
  139. package/dist/components/molecules/MixerChannel/MixerChannelStory.svelte.d.ts +3 -0
  140. package/dist/components/molecules/NodeGraph/GraphBlueprintNode.svelte +454 -0
  141. package/dist/components/molecules/NodeGraph/GraphBlueprintNode.svelte.d.ts +8 -0
  142. package/dist/components/molecules/NodeGraph/GraphFlowBridge.svelte +26 -0
  143. package/dist/components/molecules/NodeGraph/GraphFlowBridge.svelte.d.ts +22 -0
  144. package/dist/components/molecules/NodeGraph/NodeGraph.stories.d.ts +12 -0
  145. package/dist/components/molecules/NodeGraph/NodeGraph.stories.js +9 -0
  146. package/dist/components/molecules/NodeGraph/NodeGraph.svelte +1409 -0
  147. package/dist/components/molecules/NodeGraph/NodeGraph.svelte.d.ts +33 -0
  148. package/dist/components/molecules/NodeGraph/NodeGraphStory.svelte +90 -0
  149. package/dist/components/molecules/NodeGraph/NodeGraphStory.svelte.d.ts +3 -0
  150. package/dist/components/molecules/NodePalette/NodePalette.stories.js +8 -0
  151. package/dist/components/molecules/NodePalette/NodePalette.svelte +86 -0
  152. package/dist/components/molecules/NodePalette/NodePalette.svelte.d.ts +11 -0
  153. package/dist/components/molecules/NodePalette/NodePaletteStory.svelte +11 -0
  154. package/dist/components/molecules/NodePalette/NodePaletteStory.svelte.d.ts +18 -0
  155. package/dist/components/molecules/Poll/Poll.stories.d.ts +1 -1
  156. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.stories.js +8 -0
  157. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte +72 -0
  158. package/dist/components/molecules/ProgramMonitor/ProgramMonitor.svelte.d.ts +12 -0
  159. package/dist/components/molecules/ProgramMonitor/ProgramMonitorStory.svelte +16 -0
  160. package/dist/components/molecules/ProgramMonitor/ProgramMonitorStory.svelte.d.ts +18 -0
  161. package/dist/components/molecules/PropertyField/PropertyField.svelte +57 -3
  162. package/dist/components/molecules/PropertyField/PropertyField.svelte.d.ts +10 -0
  163. package/dist/components/molecules/PropertyField/PropertyFieldStory.svelte +21 -0
  164. package/dist/components/molecules/PropertyGroup/PropertyGroup.svelte +1 -1
  165. package/dist/components/molecules/QueryBuilder/QueryBuilder.stories.d.ts +9 -0
  166. package/dist/components/molecules/QueryBuilder/QueryBuilder.stories.js +8 -0
  167. package/dist/components/molecules/QueryBuilder/QueryBuilder.svelte +52 -0
  168. package/dist/components/molecules/QueryBuilder/QueryBuilder.svelte.d.ts +18 -0
  169. package/dist/components/molecules/QueryBuilder/QueryBuilderStory.svelte +62 -0
  170. package/dist/components/molecules/QueryBuilder/QueryBuilderStory.svelte.d.ts +3 -0
  171. package/dist/components/molecules/RelationPicker/RelationPicker.stories.d.ts +9 -0
  172. package/dist/components/molecules/RelationPicker/RelationPicker.stories.js +8 -0
  173. package/dist/components/molecules/RelationPicker/RelationPicker.svelte +180 -0
  174. package/dist/components/molecules/RelationPicker/RelationPicker.svelte.d.ts +24 -0
  175. package/dist/components/molecules/RelationPicker/RelationPickerStory.svelte +16 -0
  176. package/dist/components/molecules/RelationPicker/RelationPickerStory.svelte.d.ts +3 -0
  177. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.stories.d.ts +9 -0
  178. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.stories.js +8 -0
  179. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.svelte +154 -0
  180. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilder.svelte.d.ts +12 -0
  181. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilderStory.svelte +16 -0
  182. package/dist/components/molecules/SchemaFieldBuilder/SchemaFieldBuilderStory.svelte.d.ts +3 -0
  183. package/dist/components/molecules/SecretField/SecretField.stories.d.ts +9 -0
  184. package/dist/components/molecules/SecretField/SecretField.stories.js +8 -0
  185. package/dist/components/molecules/SecretField/SecretField.svelte +58 -0
  186. package/dist/components/molecules/SecretField/SecretField.svelte.d.ts +15 -0
  187. package/dist/components/molecules/SecretField/SecretFieldStory.svelte +8 -0
  188. package/dist/components/molecules/SecretField/SecretFieldStory.svelte.d.ts +3 -0
  189. package/dist/components/molecules/SplitPane/SplitPane.svelte +96 -37
  190. package/dist/components/molecules/SplitPane/SplitPane.svelte.d.ts +10 -2
  191. package/dist/components/molecules/TableToolbar/TableToolbar.svelte.d.ts +1 -1
  192. package/dist/components/molecules/Terminal/Terminal.stories.js +20 -0
  193. package/dist/components/molecules/Terminal/Terminal.svelte +238 -0
  194. package/dist/components/molecules/Terminal/Terminal.svelte.d.ts +19 -0
  195. package/dist/components/molecules/Terminal/TerminalStory.svelte +47 -0
  196. package/dist/components/molecules/Terminal/TerminalStory.svelte.d.ts +6 -0
  197. package/dist/components/molecules/TimelineClip/TimelineClip.stories.js +8 -0
  198. package/dist/components/molecules/TimelineClip/TimelineClip.svelte +116 -0
  199. package/dist/components/molecules/TimelineClip/TimelineClip.svelte.d.ts +15 -0
  200. package/dist/components/molecules/TimelineClip/TimelineClipStory.svelte +18 -0
  201. package/dist/components/molecules/TimelineClip/TimelineClipStory.svelte.d.ts +18 -0
  202. package/dist/components/molecules/TimelineTrack/TimelineTrack.stories.js +8 -0
  203. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte +56 -0
  204. package/dist/components/molecules/TimelineTrack/TimelineTrack.svelte.d.ts +16 -0
  205. package/dist/components/molecules/TimelineTrack/TimelineTrackStory.svelte +35 -0
  206. package/dist/components/molecules/TimelineTrack/TimelineTrackStory.svelte.d.ts +18 -0
  207. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.stories.js +8 -0
  208. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.svelte +158 -0
  209. package/dist/components/molecules/TraceWaterfall/TraceWaterfall.svelte.d.ts +18 -0
  210. package/dist/components/molecules/TraceWaterfall/TraceWaterfallStory.svelte +55 -0
  211. package/dist/components/molecules/TraceWaterfall/TraceWaterfallStory.svelte.d.ts +18 -0
  212. package/dist/components/molecules/TrackHeader/TrackHeader.stories.d.ts +9 -0
  213. package/dist/components/molecules/TrackHeader/TrackHeader.stories.js +8 -0
  214. package/dist/components/molecules/TrackHeader/TrackHeader.svelte +54 -0
  215. package/dist/components/molecules/TrackHeader/TrackHeader.svelte.d.ts +9 -0
  216. package/dist/components/molecules/TrackHeader/TrackHeaderStory.svelte +10 -0
  217. package/dist/components/molecules/TrackHeader/TrackHeaderStory.svelte.d.ts +3 -0
  218. package/dist/components/molecules/TransportControls/TransportControls.stories.d.ts +9 -0
  219. package/dist/components/molecules/TransportControls/TransportControls.stories.js +8 -0
  220. package/dist/components/molecules/TransportControls/TransportControls.svelte +53 -0
  221. package/dist/components/molecules/TransportControls/TransportControls.svelte.d.ts +12 -0
  222. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte +18 -0
  223. package/dist/components/molecules/TransportControls/TransportControlsStory.svelte.d.ts +3 -0
  224. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.stories.js +8 -0
  225. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.svelte +117 -0
  226. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanel.svelte.d.ts +20 -0
  227. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanelStory.svelte +36 -0
  228. package/dist/components/molecules/VersionHistoryPanel/VersionHistoryPanelStory.svelte.d.ts +18 -0
  229. package/dist/components/molecules/VideoPlayer/VideoPlayer.stories.d.ts +1 -1
  230. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.stories.d.ts +9 -0
  231. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.stories.js +8 -0
  232. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte +59 -0
  233. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeader.svelte.d.ts +9 -0
  234. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeaderStory.svelte +10 -0
  235. package/dist/components/molecules/VideoTrackHeader/VideoTrackHeaderStory.svelte.d.ts +3 -0
  236. package/dist/components/molecules/WaveformRegion/WaveformRegion.stories.js +8 -0
  237. package/dist/components/molecules/WaveformRegion/WaveformRegion.svelte +82 -0
  238. package/dist/components/molecules/WaveformRegion/WaveformRegion.svelte.d.ts +13 -0
  239. package/dist/components/molecules/WaveformRegion/WaveformRegionStory.svelte +17 -0
  240. package/dist/components/molecules/WaveformRegion/WaveformRegionStory.svelte.d.ts +18 -0
  241. package/dist/components/molecules/WidgetCanvas/WidgetCanvas.svelte +13 -4
  242. package/dist/components/molecules/WidgetCanvas/WidgetCanvas.svelte.d.ts +3 -1
  243. package/dist/components/molecules/WidgetCanvas/widgetCanvasContext.d.ts +2 -0
  244. package/dist/components/molecules/WidgetFrame/WidgetFrame.svelte +206 -37
  245. package/dist/components/molecules/WidgetFrame/WidgetFrame.svelte.d.ts +31 -0
  246. package/dist/components/organisms/AssetPickerPage/AssetPickerPage.svelte.d.ts +1 -1
  247. package/dist/components/organisms/AudioEditor/AudioEditor.stories.d.ts +12 -0
  248. package/dist/components/organisms/AudioEditor/AudioEditor.stories.js +9 -0
  249. package/dist/components/organisms/AudioEditor/AudioEditor.svelte +273 -0
  250. package/dist/components/organisms/AudioEditor/AudioEditor.svelte.d.ts +13 -0
  251. package/dist/components/organisms/AudioEditor/AudioEditorStory.svelte +10 -0
  252. package/dist/components/organisms/AudioEditor/AudioEditorStory.svelte.d.ts +3 -0
  253. package/dist/components/organisms/BacklogPage/BacklogPage.svelte.d.ts +1 -1
  254. package/dist/components/organisms/BlogEditor/BlogEditor.stories.d.ts +12 -0
  255. package/dist/components/organisms/BlogEditor/BlogEditor.stories.js +9 -0
  256. package/dist/components/organisms/BlogEditor/BlogEditor.svelte +1208 -0
  257. package/dist/components/organisms/BlogEditor/BlogEditor.svelte.d.ts +16 -0
  258. package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte +144 -0
  259. package/dist/components/organisms/BlogEditor/BlogEditorStory.svelte.d.ts +3 -0
  260. package/dist/components/organisms/BlogEditor/DemoBlogCard.svelte +28 -0
  261. package/dist/components/organisms/BlogEditor/DemoBlogCard.svelte.d.ts +10 -0
  262. package/dist/components/organisms/BlogEditor/DemoBlogStats.svelte +34 -0
  263. package/dist/components/organisms/BlogEditor/DemoBlogStats.svelte.d.ts +11 -0
  264. package/dist/components/organisms/BlogEditor/DemoBlogWidget.svelte +13 -0
  265. package/dist/components/organisms/BlogEditor/DemoBlogWidget.svelte.d.ts +8 -0
  266. package/dist/components/organisms/BlogRenderer/BlogRenderer.stories.js +8 -0
  267. package/dist/components/organisms/BlogRenderer/BlogRenderer.svelte +469 -0
  268. package/dist/components/organisms/BlogRenderer/BlogRenderer.svelte.d.ts +14 -0
  269. package/dist/components/organisms/BlogRenderer/BlogRendererStory.svelte +20 -0
  270. package/dist/components/organisms/BlogRenderer/BlogRendererStory.svelte.d.ts +18 -0
  271. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.stories.d.ts +12 -0
  272. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.stories.js +9 -0
  273. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.svelte +1012 -0
  274. package/dist/components/organisms/BlueprintEditor/BlueprintEditor.svelte.d.ts +16 -0
  275. package/dist/components/organisms/BlueprintEditor/BlueprintEditorStory.svelte +76 -0
  276. package/dist/components/organisms/BlueprintEditor/BlueprintEditorStory.svelte.d.ts +3 -0
  277. package/dist/components/organisms/CalendarApp/CalendarApp.svelte.d.ts +1 -1
  278. package/dist/components/organisms/CanvasEditor/CanvasEditor.stories.d.ts +12 -0
  279. package/dist/components/organisms/CanvasEditor/CanvasEditor.stories.js +9 -0
  280. package/dist/components/organisms/CanvasEditor/CanvasEditor.svelte +1680 -0
  281. package/dist/components/organisms/CanvasEditor/CanvasEditor.svelte.d.ts +18 -0
  282. package/dist/components/organisms/CanvasEditor/CanvasEditorStory.svelte +329 -0
  283. package/dist/components/organisms/CanvasEditor/CanvasEditorStory.svelte.d.ts +3 -0
  284. package/dist/components/organisms/DashboardBuilderPage/DashboardBuilderPage.svelte.d.ts +1 -1
  285. package/dist/components/organisms/DataGrid/DataGrid.stories.d.ts +65 -0
  286. package/dist/components/organisms/DataGrid/DataGrid.stories.js +95 -0
  287. package/dist/components/organisms/DataGrid/DataGrid.svelte +2264 -0
  288. package/dist/components/organisms/DataGrid/DataGrid.svelte.d.ts +99 -0
  289. package/dist/components/organisms/DataGrid/DataGridStory.svelte +659 -0
  290. package/dist/components/organisms/DataGrid/DataGridStory.svelte.d.ts +11 -0
  291. package/dist/components/organisms/DataGrid/breakpoints.d.ts +10 -0
  292. package/dist/components/organisms/DataGrid/breakpoints.js +20 -0
  293. package/dist/components/organisms/DataGrid/cellEditors.d.ts +8 -0
  294. package/dist/components/organisms/DataGrid/cellEditors.js +59 -0
  295. package/dist/components/organisms/DataGrid/conditionalFormat.d.ts +11 -0
  296. package/dist/components/organisms/DataGrid/conditionalFormat.js +98 -0
  297. package/dist/components/organisms/DataGrid/filterRows.d.ts +5 -0
  298. package/dist/components/organisms/DataGrid/filterRows.js +60 -0
  299. package/dist/components/organisms/DataGrid/marks.d.ts +9 -0
  300. package/dist/components/organisms/DataGrid/marks.js +91 -0
  301. package/dist/components/organisms/DataGrid/types.d.ts +126 -0
  302. package/dist/components/organisms/DataGrid/types.js +21 -0
  303. package/dist/components/organisms/DesktopShell/DesktopShell.svelte.d.ts +1 -1
  304. package/dist/components/organisms/EditorShell/EditorShell.svelte +123 -28
  305. package/dist/components/organisms/EditorShell/EditorShell.svelte.d.ts +21 -1
  306. package/dist/components/organisms/FileUploader/FileUploader.svelte +77 -15
  307. package/dist/components/organisms/FileUploader/FileUploader.svelte.d.ts +11 -0
  308. package/dist/components/organisms/ImportWizard/ImportWizard.stories.js +8 -0
  309. package/dist/components/organisms/ImportWizard/ImportWizard.svelte +209 -0
  310. package/dist/components/organisms/ImportWizard/ImportWizard.svelte.d.ts +22 -0
  311. package/dist/components/organisms/ImportWizard/ImportWizardStory.svelte +16 -0
  312. package/dist/components/organisms/ImportWizard/ImportWizardStory.svelte.d.ts +18 -0
  313. package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.d.ts +33 -0
  314. package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.js +56 -0
  315. package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte +830 -0
  316. package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte.d.ts +68 -0
  317. package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte +600 -0
  318. package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte.d.ts +7 -0
  319. package/dist/components/organisms/ResourceStudio/fieldsToColumns.d.ts +4 -0
  320. package/dist/components/organisms/ResourceStudio/fieldsToColumns.js +20 -0
  321. package/dist/components/organisms/ResourceStudio/fieldsToSchema.d.ts +6 -0
  322. package/dist/components/organisms/ResourceStudio/fieldsToSchema.js +53 -0
  323. package/dist/components/organisms/ResourceStudio/types.d.ts +69 -0
  324. package/dist/components/organisms/ResourceStudio/types.js +15 -0
  325. package/dist/components/organisms/Scheduler/Scheduler.svelte.d.ts +1 -1
  326. package/dist/components/organisms/SchemaForm/SchemaForm.svelte +10 -3
  327. package/dist/components/organisms/SchemaForm/SchemaForm.svelte.d.ts +1 -0
  328. package/dist/components/organisms/VideoEditor/VideoEditor.stories.d.ts +12 -0
  329. package/dist/components/organisms/VideoEditor/VideoEditor.stories.js +9 -0
  330. package/dist/components/organisms/VideoEditor/VideoEditor.svelte +308 -0
  331. package/dist/components/organisms/VideoEditor/VideoEditor.svelte.d.ts +13 -0
  332. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte +10 -0
  333. package/dist/components/organisms/VideoEditor/VideoEditorStory.svelte.d.ts +3 -0
  334. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.stories.d.ts +12 -0
  335. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.stories.js +9 -0
  336. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte +22 -0
  337. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplate.svelte.d.ts +13 -0
  338. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplateStory.svelte +8 -0
  339. package/dist/components/templates/AudioEditorTemplate/AudioEditorTemplateStory.svelte.d.ts +3 -0
  340. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.stories.d.ts +12 -0
  341. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.stories.js +9 -0
  342. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.svelte +32 -0
  343. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplate.svelte.d.ts +17 -0
  344. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplateStory.svelte +21 -0
  345. package/dist/components/templates/CanvasEditorTemplate/CanvasEditorTemplateStory.svelte.d.ts +3 -0
  346. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.d.ts +25 -0
  347. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.js +18 -0
  348. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte +192 -0
  349. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte.d.ts +62 -0
  350. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte +129 -0
  351. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte.d.ts +7 -0
  352. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.stories.d.ts +12 -0
  353. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.stories.js +9 -0
  354. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.svelte +22 -0
  355. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplate.svelte.d.ts +13 -0
  356. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplateStory.svelte +8 -0
  357. package/dist/components/templates/VideoEditorTemplate/VideoEditorTemplateStory.svelte.d.ts +3 -0
  358. package/dist/index.d.ts +94 -1
  359. package/dist/index.js +68 -0
  360. package/dist/types/xyflow-svelte.d.ts +62 -0
  361. package/dist/utils/audioProject.d.ts +19 -0
  362. package/dist/utils/audioProject.js +70 -0
  363. package/dist/utils/blogDocument.d.ts +175 -0
  364. package/dist/utils/blogDocument.js +220 -0
  365. package/dist/utils/blueprintRuntime.d.ts +34 -0
  366. package/dist/utils/blueprintRuntime.js +289 -0
  367. package/dist/utils/canvasDocument.d.ts +239 -0
  368. package/dist/utils/canvasDocument.js +443 -0
  369. package/dist/utils/canvasExport.d.ts +10 -0
  370. package/dist/utils/canvasExport.js +469 -0
  371. package/dist/utils/canvasHierarchy.d.ts +189 -0
  372. package/dist/utils/canvasHierarchy.js +674 -0
  373. package/dist/utils/canvasHistory.d.ts +19 -0
  374. package/dist/utils/canvasHistory.js +48 -0
  375. package/dist/utils/canvasWidget.d.ts +81 -0
  376. package/dist/utils/canvasWidget.js +597 -0
  377. package/dist/utils/marqueeSelect.d.ts +48 -0
  378. package/dist/utils/marqueeSelect.js +189 -0
  379. package/dist/utils/mediaTime.d.ts +16 -0
  380. package/dist/utils/mediaTime.js +57 -0
  381. package/dist/utils/mediaTracks.d.ts +61 -0
  382. package/dist/utils/mediaTracks.js +102 -0
  383. package/dist/utils/nodeGraph.d.ts +354 -0
  384. package/dist/utils/nodeGraph.js +1453 -0
  385. package/dist/utils/queryBridge.d.ts +18 -0
  386. package/dist/utils/queryBridge.js +118 -0
  387. package/dist/utils/videoSequence.d.ts +13 -0
  388. package/dist/utils/videoSequence.js +63 -0
  389. package/package.json +5 -2
@@ -0,0 +1,2264 @@
1
+ <script module lang="ts">
2
+ export type {
3
+ DataGridColumn,
4
+ DataGridEditType,
5
+ DataGridEditConfig,
6
+ DataGridEditOption,
7
+ DataGridViewMode,
8
+ DataGridFilter,
9
+ Breakpoint,
10
+ CellRef,
11
+ GridSelection,
12
+ CellNote,
13
+ GridMark,
14
+ MarkColor,
15
+ MarkPreset,
16
+ ConditionalFormatRule,
17
+ ConditionalFormatStyle,
18
+ FormatCondition,
19
+ FormatTarget,
20
+ SortDir
21
+ } from './types.js';
22
+ export {
23
+ EMPTY_SELECTION,
24
+ noteKey,
25
+ markKey,
26
+ selectionCount,
27
+ selectionLabel
28
+ } from './types.js';
29
+ export { evaluateFormatRules, evaluateCondition, styleToCss } from './conditionalFormat.js';
30
+ export {
31
+ inferCellEditor,
32
+ resolveCellEditor,
33
+ coerceEditValue,
34
+ draftFromValue
35
+ } from './cellEditors.js';
36
+ export { MARK_COLORS, MARK_TINT, MARK_SWATCH, markTint, markSwatch, isMarkPreset } from './marks.js';
37
+ export {
38
+ BREAKPOINT_MIN,
39
+ matchesMinBreakpoint,
40
+ isColumnVisibleAtWidth
41
+ } from './breakpoints.js';
42
+ export { filterRows, cellText, uniqueColumnValues } from './filterRows.js';
43
+ </script>
44
+
45
+ <script lang="ts" generics="T extends Record<string, unknown> = Record<string, unknown>">
46
+ import type { Snippet } from 'svelte';
47
+ import Skeleton from '../../atoms/Skeleton/Skeleton.svelte';
48
+ import Button from '../../atoms/Button/Button.svelte';
49
+ import Checkbox from '../../atoms/Checkbox/Checkbox.svelte';
50
+ import Input from '../../atoms/Input/Input.svelte';
51
+ import Textarea from '../../atoms/Textarea/Textarea.svelte';
52
+ import Toggle from '../../atoms/Toggle/Toggle.svelte';
53
+ import MarqueeRect from '../../atoms/MarqueeRect/MarqueeRect.svelte';
54
+ import Tooltip from '../../atoms/Tooltip/Tooltip.svelte';
55
+ import BulkActionBar from '../../molecules/BulkActionBar/BulkActionBar.svelte';
56
+ import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
57
+ import type {
58
+ ContextMenuItem,
59
+ ContextMenuAnchor
60
+ } from '../../molecules/ContextMenu/ContextMenu.svelte';
61
+ import DropdownMenu from '../../molecules/DropdownMenu/DropdownMenu.svelte';
62
+ import type { DropdownItem } from '../../molecules/DropdownMenu/DropdownMenu.svelte';
63
+ import Select from '../../molecules/Select/Select.svelte';
64
+ import ColorPicker from '../../molecules/ColorPicker/ColorPicker.svelte';
65
+ import FilterBar from '../../molecules/FilterBar/FilterBar.svelte';
66
+ import type { FilterChip } from '../../molecules/FilterBar/FilterBar.svelte';
67
+ import Card from '../../molecules/Card/Card.svelte';
68
+ import MoreHorizontal from '@lucide/svelte/icons/more-horizontal';
69
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
70
+ import Pencil from '@lucide/svelte/icons/pencil';
71
+ import LayoutList from '@lucide/svelte/icons/layout-list';
72
+ import Table2 from '@lucide/svelte/icons/table-2';
73
+ import LayoutGrid from '@lucide/svelte/icons/layout-grid';
74
+ import Rows3 from '@lucide/svelte/icons/rows-3';
75
+ import {
76
+ attachMarqueeSelect,
77
+ collectMarqueeItems,
78
+ defaultMarqueeIgnore,
79
+ resolveMarqueeSelection,
80
+ type MarqueeRect as MarqueeRectBox
81
+ } from '../../../utils/marqueeSelect.js';
82
+ import {
83
+ type DataGridColumn,
84
+ type DataGridEditConfig,
85
+ type DataGridViewMode,
86
+ type DataGridFilter,
87
+ type GridSelection,
88
+ type CellNote,
89
+ type GridMark,
90
+ type MarkColor,
91
+ type CellRef,
92
+ type ConditionalFormatRule,
93
+ type SortDir,
94
+ EMPTY_SELECTION,
95
+ noteKey,
96
+ markKey,
97
+ selectionCount,
98
+ selectionLabel
99
+ } from './types.js';
100
+ import { evaluateFormatRules, styleToCss } from './conditionalFormat.js';
101
+ import { coerceEditValue, draftFromValue, resolveCellEditor } from './cellEditors.js';
102
+ import { MARK_COLORS, markTint, markSwatch } from './marks.js';
103
+ import { isColumnVisibleAtWidth } from './breakpoints.js';
104
+ import { filterRows, cellText, uniqueColumnValues } from './filterRows.js';
105
+
106
+ function portalToBody(node: HTMLElement) {
107
+ document.body.appendChild(node);
108
+ return {
109
+ destroy() {
110
+ node.remove();
111
+ }
112
+ };
113
+ }
114
+
115
+ interface DataGridProps {
116
+ columns?: DataGridColumn<T>[];
117
+ rows?: T[];
118
+ rowKey?: keyof T | ((row: T, index: number) => string);
119
+ selection?: GridSelection;
120
+ notes?: CellNote[];
121
+ marks?: GridMark[];
122
+ formatRules?: ConditionalFormatRule[];
123
+ /** Client-side search string (FilterBar). */
124
+ filterQuery?: string;
125
+ /** Active column filters (FilterBar chips). */
126
+ columnFilters?: DataGridFilter[];
127
+ /** Expanded row ids when collapsible. */
128
+ expandedRows?: string[];
129
+ viewMode?: DataGridViewMode;
130
+ sortable?: boolean;
131
+ selectable?: boolean;
132
+ selectColumns?: boolean;
133
+ selectCells?: boolean;
134
+ /** When false, row/cell click only fires onrowclick — selection via checkbox/marquee/context menu. */
135
+ selectOnClick?: boolean;
136
+ marqueeSelect?: boolean;
137
+ contextMenu?: boolean;
138
+ editable?: boolean;
139
+ /** Show expand control + detail panel (auto-hidden cols + custom snippet). */
140
+ collapsible?: boolean;
141
+ /** Show FilterBar above the grid. */
142
+ filterable?: boolean;
143
+ /** Show table/list/card/grid toggle. */
144
+ showViewModes?: boolean;
145
+ /** Column id used as title in list/card/grid modes. */
146
+ titleColumnId?: string;
147
+ striped?: boolean;
148
+ hoverable?: boolean;
149
+ compact?: boolean;
150
+ stickyHeader?: boolean;
151
+ loading?: boolean;
152
+ empty?: string;
153
+ showDock?: boolean;
154
+ actions?: BulkAction[];
155
+ contextActions?: BulkAction[];
156
+ rowActions?: BulkAction[];
157
+ showRowActions?: boolean;
158
+ class?: string;
159
+ cell?: Snippet<[T, DataGridColumn<T>]>;
160
+ /** Custom expanded-row content. Receives row + auto-hidden columns. */
161
+ expandedRow?: Snippet<[T, DataGridColumn<T>[]]>;
162
+ /** Custom card/list body; falls back to title + fields. */
163
+ itemCard?: Snippet<[T]>;
164
+ dockExtra?: Snippet;
165
+ leading?: Snippet;
166
+ onselectionchange?: (selection: GridSelection) => void;
167
+ onnoteschange?: (notes: CellNote[]) => void;
168
+ onmarkschange?: (marks: GridMark[]) => void;
169
+ onformatruleschange?: (rules: ConditionalFormatRule[]) => void;
170
+ onfilterchange?: (query: string, filters: DataGridFilter[]) => void;
171
+ onviewmodechange?: (mode: DataGridViewMode) => void;
172
+ onexpandedchange?: (ids: string[]) => void;
173
+ oncolumnresize?: (columnId: string, width: number) => void;
174
+ oncelledit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
175
+ onrowpatch?: (rowId: string, patch: Record<string, unknown>, row: T) => void;
176
+ onaction?: (id: string, selection: GridSelection) => void;
177
+ onsort?: (columnId: string, direction: Exclude<SortDir, null>) => void;
178
+ onrowclick?: (row: T) => void;
179
+ onrowdblclick?: (row: T) => void;
180
+ }
181
+
182
+ let {
183
+ columns = [] as DataGridColumn<T>[],
184
+ rows = [] as T[],
185
+ rowKey,
186
+ selection = $bindable<GridSelection>(EMPTY_SELECTION),
187
+ notes = $bindable<CellNote[]>([]),
188
+ marks = $bindable<GridMark[]>([]),
189
+ formatRules = $bindable<ConditionalFormatRule[]>([]),
190
+ filterQuery = $bindable(''),
191
+ columnFilters = $bindable<DataGridFilter[]>([]),
192
+ expandedRows = $bindable<string[]>([]),
193
+ viewMode = $bindable<DataGridViewMode>('table'),
194
+ sortable = true,
195
+ selectable = true,
196
+ selectColumns = true,
197
+ selectCells = true,
198
+ selectOnClick = true,
199
+ marqueeSelect = true,
200
+ contextMenu = true,
201
+ editable = false,
202
+ collapsible = false,
203
+ filterable = false,
204
+ showViewModes = false,
205
+ titleColumnId,
206
+ striped = false,
207
+ hoverable = true,
208
+ compact = false,
209
+ stickyHeader = true,
210
+ loading = false,
211
+ empty = 'No results',
212
+ showDock = true,
213
+ actions,
214
+ contextActions,
215
+ rowActions,
216
+ showRowActions,
217
+ class: className = '',
218
+ cell,
219
+ expandedRow,
220
+ itemCard,
221
+ dockExtra,
222
+ leading,
223
+ onselectionchange,
224
+ onnoteschange,
225
+ onmarkschange,
226
+ onformatruleschange,
227
+ onfilterchange,
228
+ onviewmodechange,
229
+ onexpandedchange,
230
+ oncolumnresize,
231
+ oncelledit,
232
+ onrowpatch,
233
+ onaction,
234
+ onsort,
235
+ onrowclick,
236
+ onrowdblclick
237
+ }: DataGridProps = $props();
238
+
239
+ let sortId = $state<string | null>(null);
240
+ let sortDir = $state<SortDir>(null);
241
+ let columnWidths = $state<Record<string, number>>({});
242
+ let resizing = $state<{ id: string; startX: number; startW: number } | null>(null);
243
+ let editing = $state<CellRef | null>(null);
244
+ let editValue = $state('');
245
+ let editBool = $state(false);
246
+ let activeEditor = $state<DataGridEditConfig | null>(null);
247
+ let noteTargets = $state<CellRef[] | null>(null);
248
+ let noteDraft = $state('');
249
+ let markPickerOpen = $state(false);
250
+ let markTargets = $state<GridMark[] | null>(null);
251
+ let markCustomColor = $state('#f59e0b');
252
+ let anchorCell = $state<CellRef | null>(null);
253
+ let lastClickedRow = $state<string | null>(null);
254
+ let gridScrollEl = $state<HTMLElement | null>(null);
255
+ let marqueeRect = $state<MarqueeRectBox | null>(null);
256
+ let contextTarget = $state<CellRef | null>(null);
257
+ let contextOpen = $state(false);
258
+ let contextAnchor = $state<ContextMenuAnchor | null>(null);
259
+ let viewportWidth = $state(
260
+ typeof window !== 'undefined' ? window.innerWidth : 1280
261
+ );
262
+ let filterColumnDraft = $state('');
263
+ let filterValueDraft = $state('');
264
+
265
+ $effect(() => {
266
+ const onResize = () => {
267
+ viewportWidth = window.innerWidth;
268
+ };
269
+ window.addEventListener('resize', onResize);
270
+ onResize();
271
+ return () => window.removeEventListener('resize', onResize);
272
+ });
273
+
274
+ const autoHiddenColumns = $derived(
275
+ columns.filter(
276
+ (c) =>
277
+ !c.hidden &&
278
+ c.hideBelow &&
279
+ !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)
280
+ )
281
+ );
282
+
283
+ /** Expand controls only when there is content to reveal. */
284
+ const collapsibleEnabled = $derived(
285
+ autoHiddenColumns.length > 0 || (collapsible && Boolean(expandedRow))
286
+ );
287
+
288
+ $effect(() => {
289
+ if (collapsibleEnabled || expandedRows.length === 0) return;
290
+ expandedRows = [];
291
+ onexpandedchange?.([]);
292
+ });
293
+
294
+ const visibleColumns = $derived(
295
+ columns.filter((c) => {
296
+ if (c.hidden) return false;
297
+ if (c.hideBelow && !isColumnVisibleAtWidth(c.hideBelow, viewportWidth)) return false;
298
+ return true;
299
+ })
300
+ );
301
+ const pinnedColumns = $derived(visibleColumns.filter((c) => c.pinned));
302
+ const scrollColumns = $derived(visibleColumns.filter((c) => !c.pinned));
303
+ const orderedColumns = $derived([...pinnedColumns, ...scrollColumns]);
304
+
305
+ const filterableColumns = $derived(columns.filter((c) => c.filterable !== false && !c.hidden));
306
+ const titleCol = $derived(
307
+ columns.find((c) => c.id === titleColumnId) ??
308
+ orderedColumns[0] ??
309
+ columns.find((c) => !c.hidden) ??
310
+ null
311
+ );
312
+
313
+ const filterBarChips = $derived.by((): FilterChip[] =>
314
+ columnFilters.map((f) => ({
315
+ id: f.id,
316
+ label: f.label ?? `${columns.find((c) => c.id === f.columnId)?.header ?? f.columnId}: ${f.value}`
317
+ }))
318
+ );
319
+
320
+ const notesMap = $derived.by(() => {
321
+ const map = new Map<string, CellNote>();
322
+ for (const n of notes) map.set(noteKey(n.rowId, n.columnId), n);
323
+ return map;
324
+ });
325
+
326
+ const marksMap = $derived.by(() => {
327
+ const map = new Map<string, GridMark>();
328
+ for (const m of marks) map.set(markKey(m.rowId, m.columnId), m);
329
+ return map;
330
+ });
331
+
332
+ const defaultActions = $derived.by((): BulkAction[] => {
333
+ if (actions) return actions;
334
+ const list: BulkAction[] = [
335
+ { id: 'note', label: 'Note', variant: 'secondary' },
336
+ { id: 'mark', label: 'Mark', variant: 'secondary' },
337
+ { id: 'copy', label: 'Copy', variant: 'secondary' }
338
+ ];
339
+ if (formatRules.length > 0) {
340
+ list.push({ id: 'format', label: 'Format', variant: 'secondary' });
341
+ }
342
+ return list;
343
+ });
344
+
345
+ const resolvedContextActions = $derived(contextActions ?? actions ?? []);
346
+ const resolvedRowActions = $derived(rowActions ?? contextActions ?? actions ?? []);
347
+ const rowActionsEnabled = $derived(showRowActions ?? selectable);
348
+ const contextMenuEnabled = $derived(contextMenu && (selectable || editable) && !loading);
349
+ const dockCount = $derived(selectionCount(selection));
350
+ const dockLabels = $derived(selectionLabel(selection));
351
+ const colCount = $derived(
352
+ orderedColumns.length +
353
+ (selectable ? 1 : 0) +
354
+ (rowActionsEnabled ? 1 : 0) +
355
+ (collapsibleEnabled && viewMode === 'table' ? 1 : 0)
356
+ );
357
+
358
+ const isTableMode = $derived(viewMode === 'table');
359
+ const marqueeSelectActive = $derived(marqueeSelect && isTableMode);
360
+
361
+ function actionToContextItem(action: BulkAction): ContextMenuItem {
362
+ return {
363
+ id: action.id,
364
+ label: action.label,
365
+ disabled: action.disabled || action.loading,
366
+ destructive: action.variant === 'destructive'
367
+ };
368
+ }
369
+
370
+ function actionToDropdownItem(action: BulkAction): DropdownItem {
371
+ return {
372
+ id: action.id,
373
+ label: action.label,
374
+ disabled: action.disabled || action.loading,
375
+ destructive: action.variant === 'destructive',
376
+ icon: action.icon
377
+ };
378
+ }
379
+
380
+ const rowActionMenuItems = $derived(
381
+ resolvedRowActions.length > 0
382
+ ? resolvedRowActions.map(actionToDropdownItem)
383
+ : [
384
+ { id: 'copy', label: 'Copy' },
385
+ { id: 'note', label: 'Add note' },
386
+ { id: 'mark', label: 'Mark…' },
387
+ ...(editable ? [{ id: 'edit', label: 'Edit cell' }] : [])
388
+ ]
389
+ );
390
+
391
+ function getKey(row: T, index: number): string {
392
+ if (typeof rowKey === 'function') return rowKey(row, index);
393
+ if (rowKey && row[rowKey] != null) return String(row[rowKey]);
394
+ if ('id' in row && row.id != null) return String(row.id);
395
+ return String(index);
396
+ }
397
+
398
+ function getValue(row: T, column: DataGridColumn<T>): unknown {
399
+ const key = (column.accessor ?? column.id) as string;
400
+ return key.split('.').reduce<unknown>((acc, part) => {
401
+ if (acc && typeof acc === 'object' && part in (acc as object)) {
402
+ return (acc as Record<string, unknown>)[part];
403
+ }
404
+ return undefined;
405
+ }, row);
406
+ }
407
+
408
+ function formatCell(value: unknown): string {
409
+ if (value == null) return '';
410
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
411
+ return String(value);
412
+ }
413
+
414
+ function setSelection(next: GridSelection) {
415
+ selection = next;
416
+ onselectionchange?.(next);
417
+ }
418
+
419
+ function clearSelection() {
420
+ setSelection(EMPTY_SELECTION);
421
+ anchorCell = null;
422
+ lastClickedRow = null;
423
+ }
424
+
425
+ const filteredRows = $derived(
426
+ filterable || filterQuery || columnFilters.length
427
+ ? filterRows(
428
+ rows as Record<string, unknown>[],
429
+ columns as DataGridColumn[],
430
+ filterQuery,
431
+ columnFilters
432
+ )
433
+ : rows
434
+ );
435
+
436
+ const sortedRows = $derived.by(() => {
437
+ const base = filteredRows as T[];
438
+ if (!sortId || !sortDir) return base;
439
+ const col = columns.find((c) => c.id === sortId);
440
+ if (!col) return base;
441
+ const dir = sortDir === 'asc' ? 1 : -1;
442
+ return [...base].sort((a, b) => {
443
+ const av = getValue(a, col);
444
+ const bv = getValue(b, col);
445
+ if (av == null && bv == null) return 0;
446
+ if (av == null) return 1;
447
+ if (bv == null) return -1;
448
+ if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
449
+ return String(av).localeCompare(String(bv), undefined, { numeric: true }) * dir;
450
+ });
451
+ });
452
+
453
+ const rowIds = $derived(sortedRows.map((r, i) => getKey(r, i)));
454
+
455
+ const contextMenuItems = $derived.by((): ContextMenuItem[] => {
456
+ const target = contextTarget;
457
+ const targetCol = target ? columns.find((c) => c.id === target.columnId) : undefined;
458
+ const canEditTarget = Boolean(target && targetCol && canEditColumn(targetCol));
459
+ const items: ContextMenuItem[] = [];
460
+ if (canEditTarget) {
461
+ items.push({ id: 'edit', label: 'Edit cell', shortcut: 'F2' });
462
+ }
463
+ items.push({ id: 'copy', label: 'Copy', shortcut: '⌘C' });
464
+ if (target) {
465
+ const hasNote = notesMap.has(noteKey(target.rowId, target.columnId));
466
+ items.push({ id: 'note', label: hasNote ? 'Edit note' : 'Add note' });
467
+ } else {
468
+ items.push({ id: 'note', label: 'Add note', disabled: true });
469
+ }
470
+ const markable = Boolean(target) || selection.type !== 'none';
471
+ items.push({ id: 'mark', label: 'Mark…', disabled: !markable });
472
+ items.push({
473
+ id: 'clear-mark',
474
+ label: 'Clear mark',
475
+ disabled: !markable || !targetsHaveMarks(markTargetsFromSelection(selectionOrContext()))
476
+ });
477
+ items.push({ id: 'sep-select', label: '', separator: true });
478
+ items.push({ id: 'select-row', label: 'Select row', disabled: !target || !selectable });
479
+ items.push({
480
+ id: 'select-all',
481
+ label: 'Select all',
482
+ disabled: !selectable || rowIds.length === 0
483
+ });
484
+ items.push({
485
+ id: 'clear',
486
+ label: 'Clear selection',
487
+ disabled: !selectable || dockCount === 0
488
+ });
489
+ if (resolvedContextActions.length > 0) {
490
+ const used = new Set(items.map((i) => i.id));
491
+ const extras = resolvedContextActions.filter((a) => !used.has(a.id));
492
+ if (extras.length > 0) {
493
+ items.push({ id: 'sep-actions', label: '', separator: true });
494
+ for (const action of extras) items.push(actionToContextItem(action));
495
+ }
496
+ }
497
+ return items;
498
+ });
499
+
500
+ function rowSelectionIds(sel: GridSelection): string[] {
501
+ return sel.type === 'rows' ? sel.ids : [];
502
+ }
503
+ function columnSelectionIds(sel: GridSelection): string[] {
504
+ return sel.type === 'columns' ? sel.ids : [];
505
+ }
506
+ function cellSelection(sel: GridSelection): CellRef[] {
507
+ return sel.type === 'cells' ? sel.cells : [];
508
+ }
509
+
510
+ const allRowsSelected = $derived.by(() => {
511
+ const ids = rowSelectionIds(selection);
512
+ return rowIds.length > 0 && ids.length === rowIds.length && rowIds.every((id) => ids.includes(id));
513
+ });
514
+ const someRowsSelected = $derived(rowSelectionIds(selection).length > 0 && !allRowsSelected);
515
+
516
+ const rowFormats = $derived.by(() => {
517
+ const map = new Map<string, ReturnType<typeof evaluateFormatRules>>();
518
+ if (!formatRules.length) return map;
519
+ for (let i = 0; i < sortedRows.length; i++) {
520
+ const row = sortedRows[i];
521
+ map.set(getKey(row, i), evaluateFormatRules(row as Record<string, unknown>, formatRules));
522
+ }
523
+ return map;
524
+ });
525
+
526
+ function canSort(column: DataGridColumn<T>) {
527
+ if (column.sortable === false) return false;
528
+ return sortable || column.sortable === true;
529
+ }
530
+
531
+ function toggleSort(column: DataGridColumn<T>) {
532
+ if (!canSort(column)) return;
533
+ if (sortId !== column.id) {
534
+ sortId = column.id;
535
+ sortDir = 'asc';
536
+ } else if (sortDir === 'asc') {
537
+ sortDir = 'desc';
538
+ } else {
539
+ sortId = null;
540
+ sortDir = null;
541
+ return;
542
+ }
543
+ if (sortDir) onsort?.(column.id, sortDir);
544
+ }
545
+
546
+ function toggleAllRows() {
547
+ if (allRowsSelected) {
548
+ clearSelection();
549
+ return;
550
+ }
551
+ setSelection({ type: 'rows', ids: [...rowIds] });
552
+ }
553
+
554
+ function isRowExpanded(rowId: string) {
555
+ return expandedRows.includes(rowId);
556
+ }
557
+
558
+ function toggleExpand(rowId: string) {
559
+ const next = isRowExpanded(rowId)
560
+ ? expandedRows.filter((id) => id !== rowId)
561
+ : [...expandedRows, rowId];
562
+ expandedRows = next;
563
+ onexpandedchange?.(next);
564
+ }
565
+
566
+ function setViewMode(mode: DataGridViewMode) {
567
+ viewMode = mode;
568
+ onviewmodechange?.(mode);
569
+ }
570
+
571
+ function syncFilterQuery(q: string) {
572
+ filterQuery = q;
573
+ onfilterchange?.(q, columnFilters);
574
+ }
575
+
576
+ function removeFilterChip(id: string) {
577
+ columnFilters = columnFilters.filter((f) => f.id !== id);
578
+ onfilterchange?.(filterQuery, columnFilters);
579
+ }
580
+
581
+ function clearAllFilters() {
582
+ columnFilters = [];
583
+ filterQuery = '';
584
+ onfilterchange?.('', []);
585
+ }
586
+
587
+ function addColumnFilter(columnId: string, value: string) {
588
+ const col = columns.find((c) => c.id === columnId);
589
+ if (!col || !value) return;
590
+ const id = `${columnId}::${value}`;
591
+ if (columnFilters.some((f) => f.id === id)) return;
592
+ columnFilters = [
593
+ ...columnFilters,
594
+ {
595
+ id,
596
+ columnId,
597
+ value,
598
+ op: 'eq',
599
+ label: `${col.header}: ${value}`
600
+ }
601
+ ];
602
+ onfilterchange?.(filterQuery, columnFilters);
603
+ }
604
+
605
+ function secondaryFields(row: T): Array<{ column: DataGridColumn<T>; text: string }> {
606
+ const skip = new Set([titleCol?.id, ...orderedColumns.slice(0, 2).map((c) => c.id)]);
607
+ const pool = [
608
+ ...autoHiddenColumns,
609
+ ...columns.filter((c) => !c.hidden && !orderedColumns.some((o) => o.id === c.id))
610
+ ];
611
+ const seen = new Set<string>();
612
+ const out: Array<{ column: DataGridColumn<T>; text: string }> = [];
613
+ for (const column of [...orderedColumns.slice(1), ...pool]) {
614
+ if (skip.has(column.id) || seen.has(column.id)) continue;
615
+ seen.add(column.id);
616
+ out.push({
617
+ column,
618
+ text: cellText(row as Record<string, unknown>, column as DataGridColumn)
619
+ });
620
+ if (out.length >= 6) break;
621
+ }
622
+ return out;
623
+ }
624
+
625
+ function toggleRow(rowId: string, event: MouseEvent) {
626
+ event.stopPropagation();
627
+ clearNativeTextSelection();
628
+ if (event.shiftKey && lastClickedRow) {
629
+ const a = rowIds.indexOf(lastClickedRow);
630
+ const b = rowIds.indexOf(rowId);
631
+ if (a >= 0 && b >= 0) {
632
+ const [lo, hi] = a < b ? [a, b] : [b, a];
633
+ const range = rowIds.slice(lo, hi + 1);
634
+ const existing = selection.type === 'rows' ? selection.ids : [];
635
+ setSelection({ type: 'rows', ids: [...new Set([...existing, ...range])] });
636
+ lastClickedRow = rowId;
637
+ return;
638
+ }
639
+ }
640
+ lastClickedRow = rowId;
641
+ if (selection.type === 'rows') {
642
+ const has = selection.ids.includes(rowId);
643
+ const ids = has ? selection.ids.filter((id) => id !== rowId) : [...selection.ids, rowId];
644
+ setSelection(ids.length ? { type: 'rows', ids } : EMPTY_SELECTION);
645
+ return;
646
+ }
647
+ setSelection({ type: 'rows', ids: [rowId] });
648
+ }
649
+
650
+ function toggleColumn(columnId: string, event: MouseEvent) {
651
+ if (!selectColumns) return;
652
+ event.stopPropagation();
653
+ if (selection.type === 'columns') {
654
+ const has = selection.ids.includes(columnId);
655
+ const ids = has
656
+ ? selection.ids.filter((id) => id !== columnId)
657
+ : [...selection.ids, columnId];
658
+ setSelection(ids.length ? { type: 'columns', ids } : EMPTY_SELECTION);
659
+ return;
660
+ }
661
+ setSelection({ type: 'columns', ids: [columnId] });
662
+ }
663
+
664
+ function cellsEqual(a: CellRef, b: CellRef) {
665
+ return a.rowId === b.rowId && a.columnId === b.columnId;
666
+ }
667
+
668
+ function expandRange(a: CellRef, b: CellRef): CellRef[] {
669
+ const rowA = rowIds.indexOf(a.rowId);
670
+ const rowB = rowIds.indexOf(b.rowId);
671
+ const colIds = orderedColumns.map((c) => c.id);
672
+ const colA = colIds.indexOf(a.columnId);
673
+ const colB = colIds.indexOf(b.columnId);
674
+ if (rowA < 0 || rowB < 0 || colA < 0 || colB < 0) return [b];
675
+ const [r0, r1] = rowA < rowB ? [rowA, rowB] : [rowB, rowA];
676
+ const [c0, c1] = colA < colB ? [colA, colB] : [colB, colA];
677
+ const cells: CellRef[] = [];
678
+ for (let r = r0; r <= r1; r++) {
679
+ for (let c = c0; c <= c1; c++) {
680
+ cells.push({ rowId: rowIds[r], columnId: colIds[c] });
681
+ }
682
+ }
683
+ return cells;
684
+ }
685
+
686
+ function clearNativeTextSelection() {
687
+ const sel = typeof window !== 'undefined' ? window.getSelection?.() : null;
688
+ if (sel && sel.rangeCount > 0) sel.removeAllRanges();
689
+ }
690
+
691
+ function selectCell(rowId: string, columnId: string, event: MouseEvent) {
692
+ if (!selectCells) return;
693
+ clearNativeTextSelection();
694
+ const ref = { rowId, columnId };
695
+ if (event.shiftKey && anchorCell) {
696
+ setSelection({ type: 'cells', cells: expandRange(anchorCell, ref) });
697
+ return;
698
+ }
699
+ if (event.metaKey || event.ctrlKey) {
700
+ if (selection.type === 'cells') {
701
+ const has = selection.cells.some((c) => cellsEqual(c, ref));
702
+ const cells = has
703
+ ? selection.cells.filter((c) => !cellsEqual(c, ref))
704
+ : [...selection.cells, ref];
705
+ setSelection(cells.length ? { type: 'cells', cells } : EMPTY_SELECTION);
706
+ } else {
707
+ setSelection({ type: 'cells', cells: [ref] });
708
+ }
709
+ anchorCell = ref;
710
+ return;
711
+ }
712
+ anchorCell = ref;
713
+ setSelection({ type: 'cells', cells: [ref] });
714
+ }
715
+
716
+ function isRowSelected(rowId: string) {
717
+ return rowSelectionIds(selection).includes(rowId);
718
+ }
719
+ function isColumnSelected(columnId: string) {
720
+ return columnSelectionIds(selection).includes(columnId);
721
+ }
722
+ function isCellSelected(rowId: string, columnId: string) {
723
+ if (rowSelectionIds(selection).includes(rowId)) return true;
724
+ if (columnSelectionIds(selection).includes(columnId)) return true;
725
+ return cellSelection(selection).some((c) => c.rowId === rowId && c.columnId === columnId);
726
+ }
727
+
728
+ function canEditColumn(column: DataGridColumn<T>): boolean {
729
+ return editable && column.editable !== false;
730
+ }
731
+
732
+ function startEdit(row: T, column: DataGridColumn<T>, index: number) {
733
+ if (!canEditColumn(column)) return;
734
+ const rowId = getKey(row, index);
735
+ const value = getValue(row, column);
736
+ const editor = resolveCellEditor(column, value);
737
+ activeEditor = editor;
738
+ editing = { rowId, columnId: column.id };
739
+ if (editor.type === 'boolean') {
740
+ editBool = Boolean(value);
741
+ editValue = '';
742
+ } else {
743
+ editBool = false;
744
+ editValue = String(draftFromValue(value, editor));
745
+ }
746
+ }
747
+
748
+ function commitEdit(row: T) {
749
+ if (!editing || !activeEditor) return;
750
+ const { rowId, columnId } = editing;
751
+ const col = columns.find((c) => c.id === columnId);
752
+ const accessor = (col?.accessor ?? columnId) as string;
753
+ const draft = activeEditor.type === 'boolean' ? editBool : editValue;
754
+ const nextValue = coerceEditValue(activeEditor, draft);
755
+ editing = null;
756
+ activeEditor = null;
757
+ oncelledit?.(rowId, columnId, nextValue, row);
758
+ onrowpatch?.(rowId, { [accessor]: nextValue }, row);
759
+ }
760
+
761
+ function cancelEdit() {
762
+ editing = null;
763
+ activeEditor = null;
764
+ }
765
+
766
+ function onEditKeydown(e: KeyboardEvent, row: T) {
767
+ if (e.key === 'Enter' && activeEditor?.type !== 'textarea') {
768
+ e.preventDefault();
769
+ commitEdit(row);
770
+ } else if (e.key === 'Escape') {
771
+ e.preventDefault();
772
+ cancelEdit();
773
+ }
774
+ }
775
+
776
+ function cellsForNotes(sel: GridSelection = selectionOrContext()): CellRef[] {
777
+ if (sel.type === 'cells') {
778
+ const seen = new Set<string>();
779
+ const out: CellRef[] = [];
780
+ for (const c of sel.cells) {
781
+ const k = noteKey(c.rowId, c.columnId);
782
+ if (seen.has(k)) continue;
783
+ seen.add(k);
784
+ out.push(c);
785
+ }
786
+ return out;
787
+ }
788
+ if (sel.type === 'rows') {
789
+ const out: CellRef[] = [];
790
+ for (const rowId of sel.ids) {
791
+ for (const col of orderedColumns) {
792
+ out.push({ rowId, columnId: col.id });
793
+ }
794
+ }
795
+ return out;
796
+ }
797
+ if (sel.type === 'columns') {
798
+ const out: CellRef[] = [];
799
+ for (let i = 0; i < sortedRows.length; i++) {
800
+ const rowId = getKey(sortedRows[i], i);
801
+ for (const columnId of sel.ids) {
802
+ out.push({ rowId, columnId });
803
+ }
804
+ }
805
+ return out;
806
+ }
807
+ if (contextTarget) return [contextTarget];
808
+ return [];
809
+ }
810
+
811
+ function openNoteEditor(targets: CellRef[]) {
812
+ if (!targets.length) return;
813
+ noteTargets = targets;
814
+ markPickerOpen = false;
815
+ markTargets = null;
816
+ const texts = targets
817
+ .map((t) => notesMap.get(noteKey(t.rowId, t.columnId))?.text?.trim())
818
+ .filter((t): t is string => Boolean(t));
819
+ const unique = [...new Set(texts)];
820
+ noteDraft = unique.length === 1 ? unique[0] : '';
821
+ }
822
+
823
+ function openNoteForSelection() {
824
+ openNoteEditor(cellsForNotes(selectionOrContext()));
825
+ }
826
+
827
+ function saveNotes() {
828
+ if (!noteTargets?.length) return;
829
+ const text = noteDraft.trim();
830
+ const keys = new Set(noteTargets.map((t) => noteKey(t.rowId, t.columnId)));
831
+ let next = notes.filter((n) => !keys.has(noteKey(n.rowId, n.columnId)));
832
+ if (text) {
833
+ const updatedAt = new Date().toISOString();
834
+ for (const t of noteTargets) {
835
+ next.push({
836
+ rowId: t.rowId,
837
+ columnId: t.columnId,
838
+ text,
839
+ updatedAt
840
+ });
841
+ }
842
+ }
843
+ notes = next;
844
+ onnoteschange?.(next);
845
+ noteTargets = null;
846
+ }
847
+
848
+ function closeNoteEditor() {
849
+ noteTargets = null;
850
+ }
851
+
852
+ /** Resolve mark targets from selection: rows → row marks; cells → cell marks. */
853
+ function markTargetsFromSelection(sel: GridSelection): Array<{ rowId: string; columnId?: string }> {
854
+ if (sel.type === 'rows') {
855
+ return sel.ids.map((rowId) => ({ rowId }));
856
+ }
857
+ if (sel.type === 'cells') {
858
+ const seen = new Set<string>();
859
+ const out: Array<{ rowId: string; columnId?: string }> = [];
860
+ for (const c of sel.cells) {
861
+ const k = markKey(c.rowId, c.columnId);
862
+ if (seen.has(k)) continue;
863
+ seen.add(k);
864
+ out.push({ rowId: c.rowId, columnId: c.columnId });
865
+ }
866
+ return out;
867
+ }
868
+ if (sel.type === 'columns') {
869
+ // Plan: column marks out of scope — fall back to cells in those columns.
870
+ const out: Array<{ rowId: string; columnId?: string }> = [];
871
+ for (let i = 0; i < sortedRows.length; i++) {
872
+ const rowId = getKey(sortedRows[i], i);
873
+ for (const columnId of sel.ids) {
874
+ out.push({ rowId, columnId });
875
+ }
876
+ }
877
+ return out;
878
+ }
879
+ if (contextTarget) {
880
+ if (selectCells) return [{ rowId: contextTarget.rowId, columnId: contextTarget.columnId }];
881
+ return [{ rowId: contextTarget.rowId }];
882
+ }
883
+ return [];
884
+ }
885
+
886
+ function targetsHaveMarks(targets: Array<{ rowId: string; columnId?: string }>): boolean {
887
+ return targets.some((t) => marksMap.has(markKey(t.rowId, t.columnId)));
888
+ }
889
+
890
+ function openMarkPicker(targets?: Array<{ rowId: string; columnId?: string }>) {
891
+ const next = targets ?? markTargetsFromSelection(selectionOrContext());
892
+ if (!next.length) return;
893
+ const firstColor =
894
+ marksMap.get(markKey(next[0].rowId, next[0].columnId))?.color ?? 'amber';
895
+ markTargets = next.map((t) => ({
896
+ rowId: t.rowId,
897
+ columnId: t.columnId,
898
+ color: (marksMap.get(markKey(t.rowId, t.columnId))?.color ?? firstColor) as MarkColor
899
+ }));
900
+ markCustomColor = markSwatch(firstColor);
901
+ markPickerOpen = true;
902
+ noteTargets = null;
903
+ }
904
+
905
+ function closeMarkPicker() {
906
+ markPickerOpen = false;
907
+ markTargets = null;
908
+ }
909
+
910
+ function applyMarkColor(color: MarkColor) {
911
+ if (!markTargets?.length) return;
912
+ const keys = new Set(markTargets.map((t) => markKey(t.rowId, t.columnId)));
913
+ const next = marks.filter((m) => !keys.has(markKey(m.rowId, m.columnId)));
914
+ for (const t of markTargets) {
915
+ next.push({
916
+ rowId: t.rowId,
917
+ ...(t.columnId ? { columnId: t.columnId } : {}),
918
+ color
919
+ });
920
+ }
921
+ marks = next;
922
+ onmarkschange?.(next);
923
+ closeMarkPicker();
924
+ }
925
+
926
+ function applyCustomMarkColor() {
927
+ const value = markCustomColor.trim();
928
+ if (!value) return;
929
+ applyMarkColor(value);
930
+ }
931
+
932
+ function clearMarksForTargets(targets?: Array<{ rowId: string; columnId?: string }>) {
933
+ const list = targets ?? markTargetsFromSelection(selectionOrContext());
934
+ if (!list.length) return;
935
+ const keys = new Set(list.map((t) => markKey(t.rowId, t.columnId)));
936
+ const next = marks.filter((m) => !keys.has(markKey(m.rowId, m.columnId)));
937
+ marks = next;
938
+ onmarkschange?.(next);
939
+ closeMarkPicker();
940
+ }
941
+
942
+ function rowMarkColor(rowId: string): MarkColor | null {
943
+ return marksMap.get(markKey(rowId))?.color ?? null;
944
+ }
945
+
946
+ function cellMarkColor(rowId: string, columnId: string): MarkColor | null {
947
+ return marksMap.get(markKey(rowId, columnId))?.color ?? null;
948
+ }
949
+
950
+ function handleDockAction(id: string) {
951
+ if (id === 'note') {
952
+ openNoteForSelection();
953
+ return;
954
+ }
955
+ if (id === 'mark') {
956
+ openMarkPicker();
957
+ return;
958
+ }
959
+ if (id === 'copy') {
960
+ copySelection();
961
+ return;
962
+ }
963
+ onaction?.(id, selection);
964
+ }
965
+
966
+ /** Right-click only pins the menu target — it does not change selection. */
967
+ function prepareContextTarget(rowId: string, columnId: string) {
968
+ contextTarget = { rowId, columnId };
969
+ }
970
+
971
+ function openContextMenuAt(e: MouseEvent, rowId: string, columnId: string) {
972
+ if (!contextMenuEnabled) return;
973
+ e.preventDefault();
974
+ e.stopPropagation();
975
+ prepareContextTarget(rowId, columnId);
976
+ contextAnchor = { x: e.clientX, y: e.clientY };
977
+ contextOpen = true;
978
+ }
979
+
980
+ function closeContextMenu() {
981
+ contextOpen = false;
982
+ contextAnchor = null;
983
+ }
984
+
985
+ function onContextMenuPick(id: string) {
986
+ runGridAction(id, contextTarget);
987
+ closeContextMenu();
988
+ }
989
+
990
+ function selectRowFromCell(rowId: string, event: MouseEvent) {
991
+ if (event.shiftKey || event.metaKey || event.ctrlKey) {
992
+ toggleRow(rowId, event);
993
+ return;
994
+ }
995
+ lastClickedRow = rowId;
996
+ setSelection({ type: 'rows', ids: [rowId] });
997
+ }
998
+
999
+ function rowIdFromEvent(e: Event): string | null {
1000
+ const t = e.target;
1001
+ if (!(t instanceof Element)) return null;
1002
+ return t.closest('[data-row-id]')?.getAttribute('data-row-id') ?? null;
1003
+ }
1004
+
1005
+ function columnIdFromEvent(e: Event): string | null {
1006
+ const t = e.target;
1007
+ if (!(t instanceof Element)) return null;
1008
+ return t.closest('[data-column-id]')?.getAttribute('data-column-id') ?? null;
1009
+ }
1010
+
1011
+ function rowById(rowId: string): T | undefined {
1012
+ const idx = rowIds.indexOf(rowId);
1013
+ return idx >= 0 ? sortedRows[idx] : undefined;
1014
+ }
1015
+
1016
+ function emitRowClick(rowId: string) {
1017
+ const row = rowById(rowId);
1018
+ if (row) onrowclick?.(row);
1019
+ }
1020
+
1021
+ function emitRowDblClick(rowId: string) {
1022
+ const row = rowById(rowId);
1023
+ if (row) onrowdblclick?.(row);
1024
+ }
1025
+
1026
+ /** Single delegated handlers — avoids broken per-row listeners inside {#each}. */
1027
+ function onGridClick(e: MouseEvent) {
1028
+ // Ignore the synthetic click that precedes dblclick (detail === 2).
1029
+ if (e.detail > 1) return;
1030
+ const t = e.target;
1031
+ if (!(t instanceof Element)) return;
1032
+ if (t.closest('thead, button, input, textarea, select, a, label, [data-marquee-ignore]')) return;
1033
+ const cell = t.closest('[role="gridcell"][data-row-id]');
1034
+ if (!(cell instanceof HTMLElement)) return;
1035
+ const id = cell.getAttribute('data-row-id');
1036
+ const colId = cell.getAttribute('data-column-id') ?? '';
1037
+ if (!id) return;
1038
+ if (selectCells) {
1039
+ selectCell(id, colId, e);
1040
+ } else if (selectable && selectOnClick) {
1041
+ selectRowFromCell(id, e);
1042
+ }
1043
+ emitRowClick(id);
1044
+ }
1045
+
1046
+ function onGridDblClick(e: MouseEvent) {
1047
+ const t = e.target;
1048
+ if (!(t instanceof Element)) return;
1049
+ if (t.closest('thead, button, input, textarea, select, a, label, [data-marquee-ignore]')) return;
1050
+ const cell = t.closest('[role="gridcell"][data-row-id]');
1051
+ if (!(cell instanceof HTMLElement)) return;
1052
+ const id = cell.getAttribute('data-row-id');
1053
+ const colId = cell.getAttribute('data-column-id') ?? '';
1054
+ if (!id) return;
1055
+ // Prefer consumer dblclick; otherwise fall through to edit.
1056
+ if (onrowdblclick) {
1057
+ emitRowClick(id);
1058
+ emitRowDblClick(id);
1059
+ return;
1060
+ }
1061
+ if (editable) {
1062
+ const col = columns.find((c) => c.id === colId);
1063
+ const current = rowById(id);
1064
+ const idx = rowIds.indexOf(id);
1065
+ if (col && current && canEditColumn(col)) startEdit(current, col, idx);
1066
+ }
1067
+ }
1068
+
1069
+ function selectionOrContext(): GridSelection {
1070
+ if (selection.type !== 'none') return selection;
1071
+ if (!contextTarget) return EMPTY_SELECTION;
1072
+ if (selectCells) {
1073
+ return { type: 'cells', cells: [contextTarget] };
1074
+ }
1075
+ return { type: 'rows', ids: [contextTarget.rowId] };
1076
+ }
1077
+
1078
+ function copySelection(sel: GridSelection = selection) {
1079
+ const active = sel.type === 'none' ? selectionOrContext() : sel;
1080
+ const lines: string[] = [];
1081
+ if (active.type === 'rows') {
1082
+ for (const id of active.ids) {
1083
+ const idx = rowIds.indexOf(id);
1084
+ if (idx < 0) continue;
1085
+ const row = rows[idx];
1086
+ lines.push(orderedColumns.map((c) => formatCell(getValue(row, c))).join('\t'));
1087
+ }
1088
+ } else if (active.type === 'columns') {
1089
+ for (const row of rows) {
1090
+ lines.push(
1091
+ active.ids
1092
+ .map((id) => {
1093
+ const col = columns.find((c) => c.id === id);
1094
+ return col ? formatCell(getValue(row, col)) : '';
1095
+ })
1096
+ .join('\t')
1097
+ );
1098
+ }
1099
+ } else if (active.type === 'cells') {
1100
+ const byRow = new Map<string, Map<string, string>>();
1101
+ for (const cellRef of active.cells) {
1102
+ const idx = rowIds.indexOf(cellRef.rowId);
1103
+ if (idx < 0) continue;
1104
+ const col = columns.find((c) => c.id === cellRef.columnId);
1105
+ if (!col) continue;
1106
+ if (!byRow.has(cellRef.rowId)) byRow.set(cellRef.rowId, new Map());
1107
+ byRow
1108
+ .get(cellRef.rowId)!
1109
+ .set(cellRef.columnId, formatCell(getValue(rows[idx], col)));
1110
+ }
1111
+ for (const cols of byRow.values()) {
1112
+ lines.push([...cols.values()].join('\t'));
1113
+ }
1114
+ }
1115
+ const text = lines.join('\n');
1116
+ if (text) navigator.clipboard?.writeText(text).catch(() => {});
1117
+ }
1118
+
1119
+ function runGridAction(id: string, focus?: CellRef | null) {
1120
+ const target = focus ?? contextTarget;
1121
+ if (id === 'copy') {
1122
+ copySelection(selectionOrContext());
1123
+ return;
1124
+ }
1125
+ if (id === 'note') {
1126
+ const sel = selectionOrContext();
1127
+ const targets = cellsForNotes(sel);
1128
+ if (targets.length) {
1129
+ openNoteEditor(targets);
1130
+ return;
1131
+ }
1132
+ if (target) {
1133
+ openNoteEditor([target]);
1134
+ return;
1135
+ }
1136
+ openNoteForSelection();
1137
+ return;
1138
+ }
1139
+ if (id === 'mark') {
1140
+ openMarkPicker();
1141
+ return;
1142
+ }
1143
+ if (id === 'clear-mark') {
1144
+ clearMarksForTargets();
1145
+ return;
1146
+ }
1147
+ if (id === 'edit' && target && editable) {
1148
+ const col = columns.find((c) => c.id === target.columnId);
1149
+ if (!col || !canEditColumn(col)) return;
1150
+ for (let i = 0; i < sortedRows.length; i++) {
1151
+ const row = sortedRows[i];
1152
+ if (getKey(row, i) === target.rowId) {
1153
+ startEdit(row, col, i);
1154
+ return;
1155
+ }
1156
+ }
1157
+ return;
1158
+ }
1159
+ if (id === 'select-row' && target) {
1160
+ setSelection({ type: 'rows', ids: [target.rowId] });
1161
+ return;
1162
+ }
1163
+ if (id === 'select-all') {
1164
+ setSelection({ type: 'rows', ids: [...rowIds] });
1165
+ return;
1166
+ }
1167
+ if (id === 'clear') {
1168
+ clearSelection();
1169
+ return;
1170
+ }
1171
+ onaction?.(id, selectionOrContext());
1172
+ }
1173
+
1174
+ function onContextSelect(id: string) {
1175
+ const target = contextTarget;
1176
+ runGridAction(id, target);
1177
+ }
1178
+
1179
+ function onRowMenuAction(id: string, rowId: string) {
1180
+ const columnId = orderedColumns[0]?.id ?? 'id';
1181
+ prepareContextTarget(rowId, columnId);
1182
+ runGridAction(id, { rowId, columnId });
1183
+ }
1184
+
1185
+ function onKeydown(e: KeyboardEvent) {
1186
+ if (e.key === 'F2' && editable && !editing) {
1187
+ const target =
1188
+ contextTarget ??
1189
+ (selection.type === 'cells' && selection.cells[0]
1190
+ ? selection.cells[0]
1191
+ : selection.type === 'rows' && selection.ids[0] && orderedColumns[0]
1192
+ ? { rowId: selection.ids[0], columnId: orderedColumns[0].id }
1193
+ : null);
1194
+ if (target) {
1195
+ e.preventDefault();
1196
+ runGridAction('edit', target);
1197
+ return;
1198
+ }
1199
+ }
1200
+ if (e.key === 'Escape') {
1201
+ if (editing) {
1202
+ cancelEdit();
1203
+ return;
1204
+ }
1205
+ if (noteTargets) {
1206
+ closeNoteEditor();
1207
+ return;
1208
+ }
1209
+ if (markPickerOpen) {
1210
+ closeMarkPicker();
1211
+ return;
1212
+ }
1213
+ if (contextOpen) {
1214
+ closeContextMenu();
1215
+ return;
1216
+ }
1217
+ if (dockCount > 0) clearSelection();
1218
+ }
1219
+ }
1220
+
1221
+ function startResize(columnId: string, e: PointerEvent) {
1222
+ e.preventDefault();
1223
+ e.stopPropagation();
1224
+ const el = (e.currentTarget as HTMLElement).parentElement;
1225
+ const startW = columnWidths[columnId] ?? el?.getBoundingClientRect().width ?? 120;
1226
+ resizing = { id: columnId, startX: e.clientX, startW };
1227
+ const onMove = (ev: PointerEvent) => {
1228
+ if (!resizing) return;
1229
+ const delta = ev.clientX - resizing.startX;
1230
+ columnWidths = {
1231
+ ...columnWidths,
1232
+ [resizing.id]: Math.max(72, resizing.startW + delta)
1233
+ };
1234
+ };
1235
+ const onUp = () => {
1236
+ if (resizing) oncolumnresize?.(resizing.id, columnWidths[resizing.id] ?? resizing.startW);
1237
+ resizing = null;
1238
+ window.removeEventListener('pointermove', onMove);
1239
+ window.removeEventListener('pointerup', onUp);
1240
+ };
1241
+ window.addEventListener('pointermove', onMove);
1242
+ window.addEventListener('pointerup', onUp);
1243
+ }
1244
+
1245
+ function colWidth(column: DataGridColumn<T>): string | undefined {
1246
+ if (columnWidths[column.id]) return `${columnWidths[column.id]}px`;
1247
+ return column.width;
1248
+ }
1249
+
1250
+ function pinnedOffset(columnId: string): string | undefined {
1251
+ const idx = pinnedColumns.findIndex((c) => c.id === columnId);
1252
+ if (idx < 0) return undefined;
1253
+ let left = selectable ? 40 : 0;
1254
+ for (let i = 0; i < idx; i++) {
1255
+ const c = pinnedColumns[i];
1256
+ left += columnWidths[c.id] ?? (c.width ? parseInt(c.width, 10) || 120 : 120);
1257
+ }
1258
+ return `${left}px`;
1259
+ }
1260
+
1261
+ function marqueeIdForRow(rowId: string) {
1262
+ return `row:${rowId}`;
1263
+ }
1264
+ function marqueeIdForCell(rowId: string, columnId: string) {
1265
+ return `cell:${noteKey(rowId, columnId)}`;
1266
+ }
1267
+
1268
+ function snapshotMarqueeIds(): string[] {
1269
+ if (selection.type === 'rows') return selection.ids.map(marqueeIdForRow);
1270
+ if (selection.type === 'cells') {
1271
+ return selection.cells.map((c) => marqueeIdForCell(c.rowId, c.columnId));
1272
+ }
1273
+ return [];
1274
+ }
1275
+
1276
+ function applyMarqueeIds(ids: string[]) {
1277
+ if (selectCells) {
1278
+ const cells: CellRef[] = [];
1279
+ for (const id of ids) {
1280
+ if (!id.startsWith('cell:')) continue;
1281
+ const rest = id.slice(5);
1282
+ const sep = rest.indexOf('::');
1283
+ if (sep < 0) continue;
1284
+ cells.push({ rowId: rest.slice(0, sep), columnId: rest.slice(sep + 2) });
1285
+ }
1286
+ setSelection(cells.length ? { type: 'cells', cells } : EMPTY_SELECTION);
1287
+ return;
1288
+ }
1289
+ const idsOnly = ids.filter((id) => id.startsWith('row:')).map((id) => id.slice(4));
1290
+ setSelection(idsOnly.length ? { type: 'rows', ids: idsOnly } : EMPTY_SELECTION);
1291
+ }
1292
+
1293
+ const marqueeEnabled = $derived(selectable && marqueeSelectActive && !loading);
1294
+
1295
+ $effect(() => {
1296
+ const el = gridScrollEl;
1297
+ if (!el || !marqueeEnabled) return;
1298
+
1299
+ let baseline: string[] = [];
1300
+ let dragStarted = false;
1301
+
1302
+ const dispose = attachMarqueeSelect(el, {
1303
+ getItems: () =>
1304
+ collectMarqueeItems(
1305
+ el,
1306
+ selectCells ? '[data-marquee-id^="cell:"]' : '[data-marquee-id^="row:"]'
1307
+ ),
1308
+ shouldIgnore: (target) => {
1309
+ if (editing || noteTargets || markPickerOpen) return true;
1310
+ if (defaultMarqueeIgnore(target)) return true;
1311
+ if (target instanceof Element && target.closest('thead')) return true;
1312
+ return false;
1313
+ },
1314
+ onRect: (rect) => {
1315
+ marqueeRect = rect;
1316
+ if (!rect) dragStarted = false;
1317
+ },
1318
+ onDrag: ({ ids, modifier }) => {
1319
+ if (!dragStarted) {
1320
+ dragStarted = true;
1321
+ baseline = snapshotMarqueeIds();
1322
+ }
1323
+ clearNativeTextSelection();
1324
+ applyMarqueeIds(resolveMarqueeSelection(ids, baseline, modifier));
1325
+ },
1326
+ onSelect: ({ ids, modifier }) => {
1327
+ clearNativeTextSelection();
1328
+ applyMarqueeIds(resolveMarqueeSelection(ids, baseline, modifier));
1329
+ dragStarted = false;
1330
+ baseline = [];
1331
+ }
1332
+ });
1333
+
1334
+ return dispose;
1335
+ });
1336
+
1337
+ const alignClass = {
1338
+ left: 'text-left',
1339
+ center: 'text-center',
1340
+ right: 'text-right'
1341
+ } as const;
1342
+
1343
+ </script>
1344
+
1345
+ <svelte:window
1346
+ onkeydown={onKeydown}
1347
+ onpointerdown={(e) => {
1348
+ if (!contextOpen || e.button === 2) return;
1349
+ const t = e.target as Node | null;
1350
+ if (t && typeof (t as Element).closest === 'function' && (t as Element).closest('[aria-label="Context menu"]')) {
1351
+ return;
1352
+ }
1353
+ closeContextMenu();
1354
+ }}
1355
+ />
1356
+
1357
+ {#snippet gridBody()}
1358
+ <div class={['relative flex h-full min-h-0 w-full flex-col gap-3', className]}>
1359
+ {#if filterable || showViewModes}
1360
+ <div class="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
1361
+ {#if filterable}
1362
+ <div class="min-w-0 flex-1">
1363
+ <FilterBar
1364
+ bind:query={filterQuery}
1365
+ filters={filterBarChips}
1366
+ placeholder="Search rows…"
1367
+ onquerychange={syncFilterQuery}
1368
+ onremovefilter={removeFilterChip}
1369
+ onclear={clearAllFilters}
1370
+ >
1371
+ {#snippet actions()}
1372
+ {#if filterableColumns.length}
1373
+ {@const columnOptions = filterableColumns.map((c) => ({
1374
+ value: c.id,
1375
+ label: c.header
1376
+ }))}
1377
+ {@const valueOptions = filterColumnDraft
1378
+ ? uniqueColumnValues(
1379
+ rows as Record<string, unknown>[],
1380
+ columns.find((c) => c.id === filterColumnDraft)! as DataGridColumn
1381
+ ).map((v) => ({ value: v, label: v }))
1382
+ : []}
1383
+ <div class="flex items-center gap-1.5">
1384
+ <Select
1385
+ size="sm"
1386
+ class="w-[9.5rem] shrink-0"
1387
+ placeholder="Filter column…"
1388
+ options={columnOptions}
1389
+ bind:value={filterColumnDraft}
1390
+ onchange={() => {
1391
+ filterValueDraft = '';
1392
+ }}
1393
+ />
1394
+ {#if filterColumnDraft}
1395
+ <Select
1396
+ size="sm"
1397
+ class="w-[9.5rem] shrink-0"
1398
+ placeholder="Value…"
1399
+ options={valueOptions}
1400
+ bind:value={filterValueDraft}
1401
+ onchange={(v) => {
1402
+ if (!v) return;
1403
+ addColumnFilter(filterColumnDraft, v);
1404
+ filterValueDraft = '';
1405
+ }}
1406
+ />
1407
+ {/if}
1408
+ </div>
1409
+ {/if}
1410
+ {/snippet}
1411
+ </FilterBar>
1412
+ </div>
1413
+ {/if}
1414
+ {#if showViewModes}
1415
+ <div
1416
+ class="inline-flex h-12 shrink-0 items-center gap-0.5 rounded-2xl border border-border bg-surface-elevated p-2 shadow-sm"
1417
+ role="group"
1418
+ aria-label="View mode"
1419
+ >
1420
+ {#each [
1421
+ { id: 'table' as const, label: 'Table', Icon: Table2 },
1422
+ { id: 'list' as const, label: 'List', Icon: LayoutList },
1423
+ { id: 'card' as const, label: 'Cards', Icon: Rows3 },
1424
+ { id: 'grid' as const, label: 'Grid', Icon: LayoutGrid }
1425
+ ] as mode (mode.id)}
1426
+ <button
1427
+ type="button"
1428
+ class={[
1429
+ 'inline-flex h-8 w-8 items-center justify-center rounded-lg text-secondary transition',
1430
+ viewMode === mode.id && 'bg-brand-500/15 text-brand-700 dark:text-brand-300'
1431
+ ]}
1432
+ aria-label={mode.label}
1433
+ aria-pressed={viewMode === mode.id}
1434
+ onclick={() => setViewMode(mode.id)}
1435
+ >
1436
+ <mode.Icon class="h-4 w-4" strokeWidth={2} />
1437
+ </button>
1438
+ {/each}
1439
+ </div>
1440
+ {/if}
1441
+ </div>
1442
+ {/if}
1443
+
1444
+ {#if isTableMode}
1445
+ <div
1446
+ bind:this={gridScrollEl}
1447
+ class={[
1448
+ 'relative min-h-0 w-full flex-1 overflow-auto rounded-xl border border-border bg-surface-elevated',
1449
+ selectable && 'select-none'
1450
+ ]}
1451
+ role="grid"
1452
+ aria-rowcount={sortedRows.length}
1453
+ aria-colcount={colCount}
1454
+ onclick={onGridClick}
1455
+ ondblclick={onGridDblClick}
1456
+ >
1457
+ <table class="w-full border-collapse text-sm">
1458
+ <thead
1459
+ class={[
1460
+ 'border-b border-border bg-surface-overlay/80 text-xs font-semibold tracking-wide text-secondary uppercase',
1461
+ stickyHeader && 'sticky top-0 z-20'
1462
+ ]}
1463
+ oncontextmenu={(e) => e.stopPropagation()}
1464
+ >
1465
+ <tr role="row">
1466
+ {#if selectable}
1467
+ <th
1468
+ scope="col"
1469
+ class={['w-10 px-2', compact ? 'py-2' : 'py-2.5', stickyHeader && 'sticky left-0 z-30 bg-surface-overlay']}
1470
+ style={stickyHeader ? 'left:0' : undefined}
1471
+ data-marquee-ignore
1472
+ >
1473
+ <Checkbox
1474
+ size="sm"
1475
+ class="gap-0"
1476
+ checked={allRowsSelected}
1477
+ indeterminate={someRowsSelected}
1478
+ aria-label="Select all rows"
1479
+ onclick={(e) => e.stopPropagation()}
1480
+ onchange={(checked) => {
1481
+ if (checked) setSelection({ type: 'rows', ids: [...rowIds] });
1482
+ else clearSelection();
1483
+ }}
1484
+ />
1485
+ </th>
1486
+ {/if}
1487
+ {#if collapsibleEnabled}
1488
+ <th
1489
+ scope="col"
1490
+ class={['w-8 px-1', compact ? 'py-2' : 'py-2.5']}
1491
+ data-marquee-ignore
1492
+ aria-label="Expand"
1493
+ ></th>
1494
+ {/if}
1495
+ {#each orderedColumns as column (column.id)}
1496
+ {@const selected = isColumnSelected(column.id)}
1497
+ <th
1498
+ scope="col"
1499
+ class={[
1500
+ 'relative px-3 font-semibold',
1501
+ compact ? 'py-2' : 'py-2.5',
1502
+ alignClass[column.align ?? 'left'],
1503
+ canSort(column) && 'cursor-pointer select-none hover:text-primary',
1504
+ selected && 'bg-brand-500/10 text-brand-700 dark:text-brand-300',
1505
+ column.pinned && stickyHeader && 'sticky z-30 bg-surface-overlay',
1506
+ column.class
1507
+ ]}
1508
+ style={[
1509
+ colWidth(column) ? `width:${colWidth(column)};min-width:${colWidth(column)}` : column.minWidth ? `min-width:${column.minWidth}` : '',
1510
+ column.pinned ? `left:${pinnedOffset(column.id)}` : ''
1511
+ ]
1512
+ .filter(Boolean)
1513
+ .join(';')}
1514
+ aria-sort={
1515
+ sortId === column.id
1516
+ ? sortDir === 'asc'
1517
+ ? 'ascending'
1518
+ : 'descending'
1519
+ : canSort(column)
1520
+ ? 'none'
1521
+ : undefined
1522
+ }
1523
+ onclick={(e) => {
1524
+ if (e.altKey && selectColumns) {
1525
+ toggleColumn(column.id, e);
1526
+ return;
1527
+ }
1528
+ if (canSort(column)) toggleSort(column);
1529
+ }}
1530
+ oncontextmenu={(e) => {
1531
+ if (!selectColumns) return;
1532
+ e.preventDefault();
1533
+ e.stopPropagation();
1534
+ toggleColumn(column.id, e as unknown as MouseEvent);
1535
+ }}
1536
+ >
1537
+ <span class="inline-flex items-center gap-1">
1538
+ {column.header}
1539
+ {#if canSort(column)}
1540
+ <span class="text-muted" aria-hidden="true">
1541
+ {#if sortId === column.id && sortDir === 'asc'}↑
1542
+ {:else if sortId === column.id && sortDir === 'desc'}↓
1543
+ {:else}↕{/if}
1544
+ </span>
1545
+ {/if}
1546
+ </span>
1547
+ <span
1548
+ class="absolute top-0 right-0 h-full w-1.5 cursor-col-resize hover:bg-brand-500/40"
1549
+ role="separator"
1550
+ aria-orientation="vertical"
1551
+ aria-label={`Resize ${column.header}`}
1552
+ data-marquee-ignore
1553
+ onpointerdown={(e) => startResize(column.id, e)}
1554
+ ></span>
1555
+ </th>
1556
+ {/each}
1557
+ {#if rowActionsEnabled}
1558
+ <th
1559
+ scope="col"
1560
+ class={['w-12 px-2', compact ? 'py-2' : 'py-2.5']}
1561
+ data-marquee-ignore
1562
+ >
1563
+ <span class="sr-only">Actions</span>
1564
+ </th>
1565
+ {/if}
1566
+ </tr>
1567
+ </thead>
1568
+ <tbody>
1569
+ {#if loading}
1570
+ {#each Array.from({ length: 5 }) as _, i (i)}
1571
+ <tr>
1572
+ {#if selectable}<td class="px-2 py-2"><Skeleton class="h-4 w-4" /></td>{/if}
1573
+ {#if collapsibleEnabled}<td class="px-1 py-2"><Skeleton class="h-4 w-4" /></td>{/if}
1574
+ {#each orderedColumns as column (column.id)}
1575
+ <td class="px-3 py-2"><Skeleton class="h-4 w-full" /></td>
1576
+ {/each}
1577
+ {#if rowActionsEnabled}<td class="px-2 py-2"><Skeleton class="h-4 w-6" /></td>{/if}
1578
+ </tr>
1579
+ {/each}
1580
+ {:else if sortedRows.length === 0}
1581
+ <tr>
1582
+ <td
1583
+ class="px-3 py-10 text-center text-sm text-muted"
1584
+ colspan={Math.max(colCount, 1)}
1585
+ >
1586
+ {empty}
1587
+ </td>
1588
+ </tr>
1589
+ {:else}
1590
+ {#each sortedRows as row, index (getKey(row, index))}
1591
+ {@const rid = getKey(row, index)}
1592
+ {@const rowSelected = isRowSelected(rid)}
1593
+ {@const rowExpanded = isRowExpanded(rid)}
1594
+ {@const formats = rowFormats.get(rid) ?? { rowStyle: {}, cellStyles: {} }}
1595
+ {@const rowMark = rowMarkColor(rid)}
1596
+ <tr
1597
+ role="row"
1598
+ class={[
1599
+ 'border-b border-border',
1600
+ !rowExpanded && 'last:border-b-0',
1601
+ striped && index % 2 === 1 && !formats.rowStyle.background && !rowMark && 'bg-surface-overlay/40',
1602
+ hoverable && 'hover:bg-surface-overlay/70',
1603
+ rowSelected && 'bg-brand-500/12 ring-1 ring-inset ring-brand-500/30',
1604
+ onrowclick && 'cursor-pointer'
1605
+ ]}
1606
+ style={[
1607
+ rowMark && !rowSelected && !formats.rowStyle.background
1608
+ ? `background:${markTint(rowMark)}`
1609
+ : '',
1610
+ styleToCss(formats.rowStyle)
1611
+ ]
1612
+ .filter(Boolean)
1613
+ .join(';')}
1614
+ data-row-id={rid}
1615
+ data-marquee-id={!selectCells && marqueeEnabled ? marqueeIdForRow(rid) : undefined}
1616
+ oncontextmenu={(e) => {
1617
+ const el = e.target as HTMLElement | null;
1618
+ if (el?.closest?.('[role="gridcell"]')) return;
1619
+ const id = rowIdFromEvent(e) ?? rid;
1620
+ const colId = orderedColumns[0]?.id;
1621
+ if (colId) openContextMenuAt(e, id, colId);
1622
+ }}
1623
+ >
1624
+ {#if selectable}
1625
+ <td
1626
+ class={['px-2', compact ? 'py-1.5' : 'py-2.5', stickyHeader && 'sticky left-0 z-10 bg-inherit']}
1627
+ data-marquee-ignore
1628
+ onclick={(e) => e.stopPropagation()}
1629
+ >
1630
+ <Checkbox
1631
+ size="sm"
1632
+ class="gap-0"
1633
+ checked={rowSelected}
1634
+ aria-label={`Select row ${rid}`}
1635
+ onclick={(e) => {
1636
+ e.stopPropagation();
1637
+ toggleRow(rid, e);
1638
+ }}
1639
+ />
1640
+ </td>
1641
+ {/if}
1642
+ {#if collapsibleEnabled}
1643
+ <td
1644
+ class={['px-1', compact ? 'py-1.5' : 'py-2.5']}
1645
+ data-marquee-ignore
1646
+ onclick={(e) => e.stopPropagation()}
1647
+ >
1648
+ <button
1649
+ type="button"
1650
+ class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-secondary hover:bg-surface-overlay hover:text-primary"
1651
+ aria-expanded={rowExpanded}
1652
+ aria-label={rowExpanded ? 'Collapse row' : 'Expand row'}
1653
+ onclick={() => toggleExpand(rid)}
1654
+ >
1655
+ <ChevronRight
1656
+ class={['h-4 w-4 transition-transform', rowExpanded && 'rotate-90']}
1657
+ strokeWidth={2}
1658
+ />
1659
+ </button>
1660
+ </td>
1661
+ {/if}
1662
+ {#each orderedColumns as column (column.id)}
1663
+ {@const selected = isCellSelected(rid, column.id)}
1664
+ {@const cellMark = cellMarkColor(rid, column.id)}
1665
+ {@const cellStyle = {
1666
+ ...formats.rowStyle,
1667
+ ...(formats.cellStyles[column.id] ?? {}),
1668
+ ...(cellMark &&
1669
+ !selected &&
1670
+ !formats.cellStyles[column.id]?.background
1671
+ ? { background: markTint(cellMark) }
1672
+ : {})
1673
+ }}
1674
+ {@const note = notesMap.get(noteKey(rid, column.id))}
1675
+ {@const isEditing =
1676
+ editing?.rowId === rid && editing?.columnId === column.id}
1677
+ <td
1678
+ role="gridcell"
1679
+ class={[
1680
+ 'group relative px-3 text-primary',
1681
+ compact ? 'py-1.5' : 'py-2.5',
1682
+ alignClass[column.align ?? 'left'],
1683
+ selected && 'ring-1 ring-inset ring-brand-500/70 bg-brand-500/15',
1684
+ column.pinned && 'sticky z-10 bg-inherit',
1685
+ column.class
1686
+ ]}
1687
+ style={[
1688
+ styleToCss(cellStyle),
1689
+ colWidth(column) ? `width:${colWidth(column)};min-width:${colWidth(column)}` : '',
1690
+ column.pinned ? `left:${pinnedOffset(column.id)}` : ''
1691
+ ]
1692
+ .filter(Boolean)
1693
+ .join(';')}
1694
+ data-row-id={rid}
1695
+ data-column-id={column.id}
1696
+ data-marquee-id={selectCells && marqueeEnabled
1697
+ ? marqueeIdForCell(rid, column.id)
1698
+ : undefined}
1699
+ onmousedown={(e) => {
1700
+ // Avoid native text highlight while selecting cells.
1701
+ if (selectCells && e.button === 0 && !isEditing) {
1702
+ e.preventDefault();
1703
+ }
1704
+ }}
1705
+ oncontextmenu={(e) => {
1706
+ const id = rowIdFromEvent(e) ?? rid;
1707
+ const colId = columnIdFromEvent(e) ?? column.id;
1708
+ openContextMenuAt(e, id, colId);
1709
+ }}
1710
+ >
1711
+ {#if isEditing && activeEditor}
1712
+ <div
1713
+ data-marquee-ignore
1714
+ class="select-text min-w-[7rem]"
1715
+ onclick={(e) => e.stopPropagation()}
1716
+ onmousedown={(e) => e.stopPropagation()}
1717
+ >
1718
+ {#if activeEditor.type === 'boolean'}
1719
+ <Toggle
1720
+ size="sm"
1721
+ checked={editBool}
1722
+ onchange={(v) => {
1723
+ editBool = v;
1724
+ commitEdit(row);
1725
+ }}
1726
+ />
1727
+ {:else if activeEditor.type === 'select'}
1728
+ <Select
1729
+ size="sm"
1730
+ class="w-full"
1731
+ placeholder={activeEditor.placeholder ?? 'Select…'}
1732
+ options={activeEditor.options ?? []}
1733
+ bind:value={editValue}
1734
+ onchange={() => commitEdit(row)}
1735
+ />
1736
+ {:else if activeEditor.type === 'textarea'}
1737
+ <Textarea
1738
+ rows={2}
1739
+ class="w-full"
1740
+ placeholder={activeEditor.placeholder}
1741
+ bind:value={editValue}
1742
+ onkeydown={(e) => {
1743
+ if (e.key === 'Escape') {
1744
+ e.preventDefault();
1745
+ cancelEdit();
1746
+ } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1747
+ e.preventDefault();
1748
+ commitEdit(row);
1749
+ }
1750
+ }}
1751
+ onblur={() => commitEdit(row)}
1752
+ />
1753
+ {:else if activeEditor.type === 'number'}
1754
+ <Input
1755
+ type="number"
1756
+ size="sm"
1757
+ class="gap-0! [&>div]:h-7! [&>div]:rounded-md! [&>div]:px-1.5!"
1758
+ placeholder={activeEditor.placeholder}
1759
+ min={activeEditor.min}
1760
+ max={activeEditor.max}
1761
+ step={activeEditor.step}
1762
+ bind:value={editValue}
1763
+ autofocus
1764
+ onkeydown={(e) => onEditKeydown(e, row)}
1765
+ onblur={() => commitEdit(row)}
1766
+ />
1767
+ {:else if activeEditor.type === 'date'}
1768
+ <Input
1769
+ type="date"
1770
+ size="sm"
1771
+ class="gap-0! [&>div]:h-7! [&>div]:rounded-md! [&>div]:px-1.5!"
1772
+ bind:value={editValue}
1773
+ autofocus
1774
+ onkeydown={(e) => onEditKeydown(e, row)}
1775
+ onblur={() => commitEdit(row)}
1776
+ />
1777
+ {:else}
1778
+ <Input
1779
+ type="text"
1780
+ size="sm"
1781
+ class="gap-0! [&>div]:h-7! [&>div]:rounded-md! [&>div]:px-1.5!"
1782
+ placeholder={activeEditor.placeholder}
1783
+ bind:value={editValue}
1784
+ autofocus
1785
+ onkeydown={(e) => onEditKeydown(e, row)}
1786
+ onblur={() => commitEdit(row)}
1787
+ />
1788
+ {/if}
1789
+ </div>
1790
+ {:else if cell}
1791
+ {@render cell(row, column)}
1792
+ {:else}
1793
+ {formatCell(getValue(row, column))}
1794
+ {/if}
1795
+
1796
+ {#if canEditColumn(column) && !isEditing}
1797
+ <button
1798
+ type="button"
1799
+ class="absolute right-1 bottom-1 z-10 inline-flex h-6 w-6 items-center justify-center rounded-md bg-surface-elevated/95 text-secondary opacity-0 shadow-sm ring-1 ring-border/60 transition hover:bg-surface-overlay hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 group-hover:opacity-100"
1800
+ aria-label={`Edit ${column.header}`}
1801
+ data-marquee-ignore
1802
+ onclick={(e) => {
1803
+ e.stopPropagation();
1804
+ startEdit(row, column, index);
1805
+ }}
1806
+ >
1807
+ <Pencil class="h-3.5 w-3.5" strokeWidth={2} />
1808
+ </button>
1809
+ {/if}
1810
+
1811
+ {#if note}
1812
+ <Tooltip
1813
+ content={note.text}
1814
+ side="top"
1815
+ delay={180}
1816
+ class="absolute top-0 right-0 z-10"
1817
+ >
1818
+ <button
1819
+ type="button"
1820
+ class="relative block h-3.5 w-3.5"
1821
+ aria-label="View note"
1822
+ data-marquee-ignore
1823
+ onclick={(e) => {
1824
+ e.stopPropagation();
1825
+ openNoteEditor([{ rowId: rid, columnId: column.id }]);
1826
+ }}
1827
+ >
1828
+ <span
1829
+ class="absolute top-0 right-0 h-0 w-0 border-t-8 border-l-8 border-t-amber-500 border-l-transparent"
1830
+ aria-hidden="true"
1831
+ ></span>
1832
+ </button>
1833
+ </Tooltip>
1834
+ {/if}
1835
+ </td>
1836
+ {/each}
1837
+ {#if rowActionsEnabled}
1838
+ <td
1839
+ class={['px-1', compact ? 'py-1' : 'py-1.5']}
1840
+ data-marquee-ignore
1841
+ onclick={(e) => e.stopPropagation()}
1842
+ oncontextmenu={(e) => e.stopPropagation()}
1843
+ >
1844
+ <DropdownMenu
1845
+ size="sm"
1846
+ align="end"
1847
+ label="Row actions"
1848
+ items={rowActionMenuItems}
1849
+ onselect={(id) => onRowMenuAction(id, rid)}
1850
+ >
1851
+ {#snippet trigger()}
1852
+ <button
1853
+ type="button"
1854
+ class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-secondary hover:bg-surface-overlay hover:text-primary"
1855
+ aria-label={`Actions for row ${rid}`}
1856
+ >
1857
+ <MoreHorizontal class="h-4 w-4" strokeWidth={2} />
1858
+ </button>
1859
+ {/snippet}
1860
+ </DropdownMenu>
1861
+ </td>
1862
+ {/if}
1863
+ </tr>
1864
+ {#if collapsibleEnabled && rowExpanded}
1865
+ <tr class="border-b border-border bg-surface-overlay/40">
1866
+ <td colspan={Math.max(colCount, 1)} class="px-4 py-3">
1867
+ {#if expandedRow}
1868
+ {@render expandedRow(row, autoHiddenColumns)}
1869
+ {:else}
1870
+ <dl class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
1871
+ {#each autoHiddenColumns.length ? autoHiddenColumns : columns.filter((c) => !orderedColumns.some((o) => o.id === c.id) && !c.hidden) as col (col.id)}
1872
+ <div class="min-w-0">
1873
+ <dt class="text-[11px] font-semibold tracking-wide text-muted uppercase">
1874
+ {col.header}
1875
+ </dt>
1876
+ <dd class="mt-0.5 text-sm text-primary">
1877
+ {#if cell}
1878
+ {@render cell(row, col)}
1879
+ {:else}
1880
+ {formatCell(getValue(row, col))}
1881
+ {/if}
1882
+ </dd>
1883
+ </div>
1884
+ {:else}
1885
+ <p class="text-xs text-muted">No extra fields for this row.</p>
1886
+ {/each}
1887
+ </dl>
1888
+ {/if}
1889
+ </td>
1890
+ </tr>
1891
+ {/if}
1892
+ {/each}
1893
+ {/if}
1894
+ </tbody>
1895
+ </table>
1896
+ <MarqueeRect
1897
+ rect={marqueeRect && gridScrollEl
1898
+ ? {
1899
+ x: marqueeRect.x - gridScrollEl.scrollLeft,
1900
+ y: marqueeRect.y - gridScrollEl.scrollTop,
1901
+ width: marqueeRect.width,
1902
+ height: marqueeRect.height
1903
+ }
1904
+ : null}
1905
+ />
1906
+ </div>
1907
+ {:else}
1908
+ <!-- list / card / grid modes -->
1909
+ <div
1910
+ class={[
1911
+ 'min-h-0 w-full flex-1 overflow-auto rounded-xl border border-border bg-surface-elevated p-3',
1912
+ viewMode === 'list' && 'flex flex-col gap-2',
1913
+ viewMode === 'card' && 'flex flex-col gap-3',
1914
+ viewMode === 'grid' &&
1915
+ 'grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
1916
+ ]}
1917
+ >
1918
+ {#if loading}
1919
+ {#each Array.from({ length: 4 }) as _, i (i)}
1920
+ <Skeleton class="h-24 w-full rounded-xl" />
1921
+ {/each}
1922
+ {:else if sortedRows.length === 0}
1923
+ <p class="px-2 py-8 text-center text-sm text-muted">{empty}</p>
1924
+ {:else}
1925
+ {#each sortedRows as row, index (getKey(row, index))}
1926
+ {@const rid = getKey(row, index)}
1927
+ {@const rowSelected = isRowSelected(rid)}
1928
+ {@const rowExpanded = isRowExpanded(rid)}
1929
+ {@const title =
1930
+ titleCol
1931
+ ? cellText(row as Record<string, unknown>, titleCol as DataGridColumn)
1932
+ : rid}
1933
+ {#if itemCard}
1934
+ <div
1935
+ class={rowSelected ? 'rounded-xl ring-2 ring-brand-500/50' : undefined}
1936
+ onclick={() => {
1937
+ if (selectable && selectOnClick) setSelection({ type: 'rows', ids: [rid] });
1938
+ emitRowClick(rid);
1939
+ }}
1940
+ >
1941
+ {@render itemCard(row)}
1942
+ </div>
1943
+ {:else if viewMode === 'list'}
1944
+ <div
1945
+ class={[
1946
+ 'flex items-start gap-3 rounded-xl border border-border px-3 py-2.5 transition',
1947
+ rowSelected && 'border-brand-500/40 bg-brand-500/10',
1948
+ hoverable && 'hover:bg-surface-overlay/70'
1949
+ ]}
1950
+ >
1951
+ {#if selectable}
1952
+ <Checkbox
1953
+ size="sm"
1954
+ class="mt-0.5 gap-0"
1955
+ checked={rowSelected}
1956
+ aria-label={`Select ${title}`}
1957
+ onchange={(checked) => {
1958
+ if (checked) {
1959
+ const ids =
1960
+ selection.type === 'rows'
1961
+ ? [...new Set([...selection.ids, rid])]
1962
+ : [rid];
1963
+ setSelection({ type: 'rows', ids });
1964
+ } else if (selection.type === 'rows') {
1965
+ const ids = selection.ids.filter((id) => id !== rid);
1966
+ setSelection(ids.length ? { type: 'rows', ids } : EMPTY_SELECTION);
1967
+ }
1968
+ }}
1969
+ />
1970
+ {/if}
1971
+ {#if collapsibleEnabled}
1972
+ <button
1973
+ type="button"
1974
+ class="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-secondary hover:bg-surface-overlay"
1975
+ aria-expanded={rowExpanded}
1976
+ onclick={() => toggleExpand(rid)}
1977
+ >
1978
+ <ChevronRight
1979
+ class={['h-4 w-4 transition-transform', rowExpanded && 'rotate-90']}
1980
+ strokeWidth={2}
1981
+ />
1982
+ </button>
1983
+ {/if}
1984
+ <button
1985
+ type="button"
1986
+ class="min-w-0 flex-1 text-left"
1987
+ onclick={() => {
1988
+ emitRowClick(rid);
1989
+ if (selectable && selectOnClick && !rowSelected) {
1990
+ setSelection({ type: 'rows', ids: [rid] });
1991
+ }
1992
+ }}
1993
+ >
1994
+ <p class="truncate text-sm font-semibold text-primary">{title}</p>
1995
+ <p class="mt-0.5 truncate text-xs text-muted">
1996
+ {secondaryFields(row)
1997
+ .slice(0, 3)
1998
+ .map((f) => f.text)
1999
+ .filter(Boolean)
2000
+ .join(' · ')}
2001
+ </p>
2002
+ {#if rowExpanded}
2003
+ <div class="mt-2 grid gap-2 border-t border-border pt-2 sm:grid-cols-2">
2004
+ {#if expandedRow}
2005
+ {@render expandedRow(row, autoHiddenColumns)}
2006
+ {:else}
2007
+ {#each secondaryFields(row) as field (field.column.id)}
2008
+ <div>
2009
+ <p class="text-[11px] font-semibold text-muted uppercase">
2010
+ {field.column.header}
2011
+ </p>
2012
+ <p class="text-sm text-primary">{field.text}</p>
2013
+ </div>
2014
+ {/each}
2015
+ {/if}
2016
+ </div>
2017
+ {/if}
2018
+ </button>
2019
+ </div>
2020
+ {:else}
2021
+ <Card
2022
+ variant="bordered"
2023
+ padding="sm"
2024
+ hoverable
2025
+ class={rowSelected ? 'ring-2 ring-brand-500/50' : ''}
2026
+ onclick={() => {
2027
+ if (selectable && selectOnClick) setSelection({ type: 'rows', ids: [rid] });
2028
+ emitRowClick(rid);
2029
+ }}
2030
+ >
2031
+ {#snippet header()}
2032
+ <div class="flex items-start justify-between gap-2">
2033
+ <p class="truncate text-sm font-semibold text-primary">{title}</p>
2034
+ {#if collapsibleEnabled}
2035
+ <button
2036
+ type="button"
2037
+ class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-secondary hover:bg-surface-overlay"
2038
+ aria-expanded={rowExpanded}
2039
+ onclick={(e) => {
2040
+ e.stopPropagation();
2041
+ toggleExpand(rid);
2042
+ }}
2043
+ >
2044
+ <ChevronRight
2045
+ class={['h-4 w-4 transition-transform', rowExpanded && 'rotate-90']}
2046
+ strokeWidth={2}
2047
+ />
2048
+ </button>
2049
+ {/if}
2050
+ </div>
2051
+ {/snippet}
2052
+ <div class="space-y-1.5">
2053
+ {#each secondaryFields(row).slice(0, viewMode === 'grid' ? 3 : 5) as field (field.column.id)}
2054
+ <div class="flex items-baseline justify-between gap-2 text-xs">
2055
+ <span class="text-muted">{field.column.header}</span>
2056
+ <span class="truncate font-medium text-primary">{field.text}</span>
2057
+ </div>
2058
+ {/each}
2059
+ {#if rowExpanded}
2060
+ <div class="border-t border-border pt-2">
2061
+ {#if expandedRow}
2062
+ {@render expandedRow(row, autoHiddenColumns)}
2063
+ {:else}
2064
+ {#each autoHiddenColumns as col (col.id)}
2065
+ <div class="flex justify-between gap-2 py-0.5 text-xs">
2066
+ <span class="text-muted">{col.header}</span>
2067
+ <span class="text-primary">{formatCell(getValue(row, col))}</span>
2068
+ </div>
2069
+ {/each}
2070
+ {/if}
2071
+ </div>
2072
+ {/if}
2073
+ </div>
2074
+ </Card>
2075
+ {/if}
2076
+ {/each}
2077
+ {/if}
2078
+ </div>
2079
+ {/if}
2080
+
2081
+ {#if noteTargets || markPickerOpen || (showDock && dockCount > 0)}
2082
+ <!-- Fixed to the viewport (portaled) so parent relative/overflow don't shift the dock. -->
2083
+ <div
2084
+ use:portalToBody
2085
+ class="pointer-events-none fixed inset-x-0 bottom-3 z-[60] flex justify-center px-3"
2086
+ >
2087
+ <div class="relative w-full max-w-2xl">
2088
+ {#if noteTargets}
2089
+ {@const count = noteTargets.length}
2090
+ {@const existingCount = noteTargets.filter((t) =>
2091
+ notesMap.has(noteKey(t.rowId, t.columnId))
2092
+ ).length}
2093
+ <div
2094
+ class="pointer-events-auto absolute bottom-full left-1/2 z-50 mb-2 w-72 -translate-x-1/2 rounded-xl border border-border bg-surface-elevated p-3 shadow-xl"
2095
+ role="dialog"
2096
+ aria-label={existingCount ? 'Edit note' : 'Add note'}
2097
+ >
2098
+ <p class="text-xs font-semibold text-primary">
2099
+ {existingCount ? 'Edit note' : 'Add note'}
2100
+ </p>
2101
+ <p class="mt-0.5 text-[11px] text-muted">
2102
+ {#if count === 1}
2103
+ {noteTargets[0].rowId} / {noteTargets[0].columnId}
2104
+ {:else}
2105
+ {count} cells
2106
+ {/if}
2107
+ </p>
2108
+ <textarea
2109
+ class="mt-2 h-24 w-full rounded-lg border border-border bg-surface px-2 py-1.5 text-xs"
2110
+ bind:value={noteDraft}
2111
+ placeholder="Write a note…"
2112
+ ></textarea>
2113
+ <div class="mt-2 flex justify-end gap-1.5">
2114
+ <Button size="xs" variant="ghost" onclick={closeNoteEditor}>Cancel</Button>
2115
+ {#if existingCount}
2116
+ <Button
2117
+ size="xs"
2118
+ variant="ghost"
2119
+ onclick={() => {
2120
+ noteDraft = '';
2121
+ saveNotes();
2122
+ }}>Delete</Button
2123
+ >
2124
+ {/if}
2125
+ <Button size="xs" variant="primary" onclick={saveNotes}>Save</Button>
2126
+ </div>
2127
+ </div>
2128
+ {:else if markPickerOpen && markTargets}
2129
+ {@const count = markTargets.length}
2130
+ {@const existingCount = markTargets.filter((t) =>
2131
+ marksMap.has(markKey(t.rowId, t.columnId))
2132
+ ).length}
2133
+ <div
2134
+ class="pointer-events-auto absolute bottom-full left-1/2 z-50 mb-2 w-72 -translate-x-1/2 rounded-xl border border-border bg-surface-elevated p-3 shadow-xl"
2135
+ role="dialog"
2136
+ aria-label="Mark color"
2137
+ >
2138
+ <p class="text-xs font-semibold text-primary">Mark</p>
2139
+ <p class="mt-0.5 text-[11px] text-muted">
2140
+ {#if count === 1}
2141
+ {#if markTargets[0].columnId}
2142
+ {markTargets[0].rowId} / {markTargets[0].columnId}
2143
+ {:else}
2144
+ Row {markTargets[0].rowId}
2145
+ {/if}
2146
+ {:else}
2147
+ {count} targets
2148
+ {/if}
2149
+ </p>
2150
+ <div class="mt-3 flex flex-wrap items-center justify-center gap-2">
2151
+ {#each MARK_COLORS as color (color)}
2152
+ <button
2153
+ type="button"
2154
+ class="h-7 w-7 rounded-full border-2 border-white shadow ring-1 ring-black/10 transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
2155
+ style="background:{markSwatch(color)}"
2156
+ aria-label={`Mark ${color}`}
2157
+ onclick={() => applyMarkColor(color)}
2158
+ ></button>
2159
+ {/each}
2160
+ </div>
2161
+ <div class="mt-3 flex items-end gap-2">
2162
+ <div class="min-w-0 flex-1">
2163
+ <ColorPicker
2164
+ label="Custom"
2165
+ bind:value={markCustomColor}
2166
+ format="hex"
2167
+ showSwatches={false}
2168
+ />
2169
+ </div>
2170
+ <Button size="xs" variant="secondary" onclick={applyCustomMarkColor}
2171
+ >Apply</Button
2172
+ >
2173
+ </div>
2174
+ <div class="mt-3 flex justify-end gap-1.5">
2175
+ <Button size="xs" variant="ghost" onclick={closeMarkPicker}>Cancel</Button>
2176
+ {#if existingCount}
2177
+ <Button size="xs" variant="ghost" onclick={() => clearMarksForTargets(markTargets ?? undefined)}
2178
+ >Clear</Button
2179
+ >
2180
+ {/if}
2181
+ </div>
2182
+ </div>
2183
+ {/if}
2184
+
2185
+ {#if showDock && dockCount > 0}
2186
+ <div class="pointer-events-auto">
2187
+ <BulkActionBar
2188
+ count={dockCount}
2189
+ total={selection.type === 'rows' ? rowIds.length : undefined}
2190
+ itemLabel={dockLabels.itemLabel}
2191
+ itemLabelPlural={dockLabels.itemLabelPlural}
2192
+ placement="dock"
2193
+ sticky={false}
2194
+ actions={defaultActions}
2195
+ {leading}
2196
+ extra={dockExtra}
2197
+ onaction={handleDockAction}
2198
+ onclear={clearSelection}
2199
+ onselectall={selection.type === 'rows'
2200
+ ? () => setSelection({ type: 'rows', ids: [...rowIds] })
2201
+ : undefined}
2202
+ />
2203
+ </div>
2204
+ {:else}
2205
+ <!-- Anchor so the note/mark popover sits above the dock baseline. -->
2206
+ <div class="h-0 w-full" aria-hidden="true"></div>
2207
+ {/if}
2208
+ </div>
2209
+ </div>
2210
+ {/if}
2211
+ </div>
2212
+ {/snippet}
2213
+
2214
+ {#if contextMenuEnabled}
2215
+ {@render gridBody()}
2216
+ {#if contextOpen && contextAnchor}
2217
+ {@const ax = contextAnchor.x}
2218
+ {@const ay = contextAnchor.y}
2219
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
2220
+ <div
2221
+ use:portalToBody
2222
+ role="menu"
2223
+ tabindex={-1}
2224
+ aria-label="Context menu"
2225
+ style={`position:fixed;top:${ay}px;left:${ax}px;z-index:9999;`}
2226
+ class="min-w-44 rounded-xl border border-border bg-surface-elevated p-1.5 shadow-xl outline-none"
2227
+ onpointerdown={(e) => e.stopPropagation()}
2228
+ >
2229
+ {#each contextMenuItems as item (item.id)}
2230
+ {#if item.separator}
2231
+ <div class="my-1 h-px bg-border" role="separator"></div>
2232
+ {:else}
2233
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
2234
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
2235
+ <div
2236
+ role="menuitem"
2237
+ tabindex={-1}
2238
+ aria-disabled={item.disabled || undefined}
2239
+ onclick={() => {
2240
+ if (item.disabled) return;
2241
+ onContextMenuPick(item.id);
2242
+ }}
2243
+ class={[
2244
+ 'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors hover:bg-surface-overlay',
2245
+ item.disabled && 'cursor-not-allowed opacity-40',
2246
+ item.destructive ? 'text-red-600 dark:text-red-400' : 'text-primary'
2247
+ ]}
2248
+ >
2249
+ <span class="truncate">{item.label}</span>
2250
+ {#if item.shortcut}
2251
+ <kbd
2252
+ class="shrink-0 rounded border border-border bg-surface-overlay px-1.5 py-0.5 font-mono text-[10px] text-secondary"
2253
+ >
2254
+ {item.shortcut}
2255
+ </kbd>
2256
+ {/if}
2257
+ </div>
2258
+ {/if}
2259
+ {/each}
2260
+ </div>
2261
+ {/if}
2262
+ {:else}
2263
+ {@render gridBody()}
2264
+ {/if}