@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -14,6 +14,7 @@
14
14
  import SvListBox from "./SvListBox.svelte";
15
15
  import type {
16
16
  FilterOperator,
17
+ MenuPosition,
17
18
  } from "./SvGrid.types";
18
19
  import {
19
20
  getEditorInputType,
@@ -175,6 +176,29 @@
175
176
  import("./SvDateRangeInput.svelte").then((m) => (MenuRangePicker = m.default));
176
177
  });
177
178
 
179
+ /**
180
+ * The context menu is placed on a guessed height (280px, flipped above the
181
+ * click when that would not fit) before it exists. A consumer's menu can be
182
+ * far taller - the spreadsheet shell's has thirteen entries - and then the
183
+ * guess leaves its tail below the viewport. Once the menu is in the DOM its
184
+ * real box is measured and the position pulled back inside: up when it runs
185
+ * off the bottom, left when it runs off the right. The style reads
186
+ * `contextMenuPos`, so writing it moves the menu; the action's `update`
187
+ * runs on every position change and settles in one pass.
188
+ */
189
+ function keepInViewport(node: HTMLElement, _pos: MenuPosition) {
190
+ const fit = () => {
191
+ const pos = ctrl.contextMenuPos;
192
+ const box = node.getBoundingClientRect();
193
+ if (!box.width && !box.height) return;
194
+ const x = Math.max(8, Math.min(pos.x, window.innerWidth - box.width - 8));
195
+ const y = Math.max(8, Math.min(pos.y, window.innerHeight - box.height - 8));
196
+ if (x !== pos.x || y !== pos.y) ctrl.contextMenuPos = { x, y };
197
+ };
198
+ fit();
199
+ return { update: fit };
200
+ }
201
+
178
202
  /**
179
203
  * Opening the filter popover used to leave focus on the funnel button
180
204
  * outside it, so a keyboard user landed nowhere and had to tab in past the
@@ -832,6 +856,8 @@
832
856
  {/if}
833
857
 
834
858
  {#if contextMenuFor}
859
+ {@const items = contextMenuItems()}
860
+ {@const gutter = items.some((it) => it.icon)}
835
861
  <div
836
862
  class="sv-grid-menu-backdrop"
837
863
  role="presentation"
@@ -843,8 +869,9 @@
843
869
  data-svgrid-menu={ctrl.gridDomId}
844
870
  role="menu"
845
871
  style={`left: ${contextMenuPos.x}px; top: ${contextMenuPos.y}px;`}
872
+ use:keepInViewport={contextMenuPos}
846
873
  >
847
- {#each contextMenuItems() as item (item.key)}
874
+ {#each items as item (item.key)}
848
875
  {#if item.separator}
849
876
  <div class="sv-grid-menu-sep"></div>
850
877
  {:else}
@@ -854,7 +881,22 @@
854
881
  role="menuitem"
855
882
  disabled={item.disabled}
856
883
  onclick={() => { item.run?.(); closeContextMenu(); }}
857
- >{item.label}</button>
884
+ >
885
+ <!-- Every item keeps the gutter once one has an icon, so the labels
886
+ line up. The label is its own span with no whitespace text
887
+ around it, so the button reads exactly its label. -->
888
+ {#if gutter}<span class="sv-grid-menu-icon" aria-hidden="true">
889
+ {#if typeof item.icon === "string"}
890
+ {@render icon(item.icon)}
891
+ {:else if item.icon}
892
+ <svg class="sv-grid-menu-icon-own" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" focusable="false">
893
+ {#each item.icon.paths as path, i (i)}
894
+ <path d={path.d} fill={path.fill ? "currentColor" : "none"} stroke-width={path.width} stroke-dasharray={path.dash} />
895
+ {/each}
896
+ </svg>
897
+ {/if}
898
+ </span>{/if}<span class="sv-grid-menu-label">{item.label}</span>
899
+ </button>
858
900
  {/if}
859
901
  {/each}
860
902
  </div>
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The chart's right-click menu: the same portalled, focus-trapped,
4
+ * dismissable panel `SvContextMenu` opens, without the zone `<div>` that
5
+ * component wraps its children in. The chart owns its own `contextmenu`
6
+ * handler (the SVG is the zone) and hands this the items and a position.
7
+ *
8
+ * Lazy-loaded by `SvGridChart` on the first right-click, so a chart that is
9
+ * never right-clicked never pays for the menu machinery.
10
+ */
11
+ import { portalToBody, popIn } from './popover'
12
+ import { createDismissableLayer, onScrollOutside } from './a11y/dismissable'
13
+ import { createFocusTrap } from './a11y/focus-trap'
14
+ import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
15
+
16
+ type Props = {
17
+ items: ReadonlyArray<MenuItem>
18
+ x: number
19
+ y: number
20
+ onClose: () => void
21
+ ariaLabel?: string
22
+ }
23
+ let { items, x, y, onClose, ariaLabel = 'Chart menu' }: Props = $props()
24
+
25
+ let panelEl = $state<HTMLDivElement | null>(null)
26
+ const MENU_W = 220
27
+ const pos = $derived.by(() => {
28
+ if (typeof window === 'undefined') return { x, y }
29
+ const estH = Math.min(items.length, 12) * 34 + 8
30
+ return {
31
+ x: Math.max(6, Math.min(x, window.innerWidth - MENU_W - 6)),
32
+ y: Math.max(6, Math.min(y, window.innerHeight - estH - 6)),
33
+ }
34
+ })
35
+
36
+ $effect(() => {
37
+ if (!panelEl) return
38
+ const trap = createFocusTrap(panelEl, {
39
+ initialFocus: () => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])') ?? null,
40
+ })
41
+ trap.activate()
42
+ const layer = createDismissableLayer({ element: () => panelEl, onDismiss: onClose })
43
+ layer.activate()
44
+ const offScroll = onScrollOutside(() => panelEl, onClose)
45
+ return () => { layer.release(); trap.release(); offScroll() }
46
+ })
47
+ </script>
48
+
49
+ <div
50
+ bind:this={panelEl}
51
+ class="sv-ctx sv-grid-chart-menu"
52
+ use:portalToBody
53
+ use:popIn={{}}
54
+ style:position="fixed"
55
+ style:top={`${pos.y}px`}
56
+ style:left={`${pos.x}px`}
57
+ aria-label={ariaLabel}
58
+ >
59
+ <SvMenuList {items} onclose={onClose} onselect={() => onClose()} />
60
+ </div>
61
+
62
+ <style>
63
+ :global(.sv-grid-chart-menu) {
64
+ z-index: 2147483646; min-width: 220px;
65
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
66
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
67
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
68
+ }
69
+ </style>
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvChartPanes - a price chart with indicator panes stacked under it, the
4
+ * way a trading terminal lays them out: one shared x axis (labelled on the
5
+ * last pane only), one value-axis gutter width so the plots line up, and
6
+ * one crosshair / zoom window across all of them.
7
+ *
8
+ * Composition, not a new layout: every pane is a full `SvChart` in a sync
9
+ * group of its own, so tooltips, keyboard navigation, the screen-reader
10
+ * table, export and the context menu all come for free, and the geometry
11
+ * stays single-plot. The main chart takes every `SvChart` prop.
12
+ */
13
+ import SvGridChart from './SvGridChart.svelte'
14
+ import { indicatorPane, type ChartIndicatorSpec } from './chart-indicators'
15
+ import type { ChartSpec, ChartZoomWindow } from './chart-types'
16
+ import type { SvChartProps } from './SvGridChart.types'
17
+
18
+ type Props = Omit<SvChartProps, 'spec' | 'syncGroup' | 'zoom'> & {
19
+ /** The price spec: a candlestick / OHLC / line chart with the series the
20
+ * indicators read (`ohlc` and `volumes` on it for the volume-based ones). */
21
+ spec: ChartSpec
22
+ /** The panes under the price, top to bottom. */
23
+ indicators?: ChartIndicatorSpec[]
24
+ /** A sync group to share with charts outside the stack. Default: private. */
25
+ syncGroup?: string
26
+ /** Width of the value-axis gutter every pane uses, so the plots line up.
27
+ * Default 56. */
28
+ axisWidth?: number
29
+ /** The shared zoom window, bindable. */
30
+ zoom?: ChartZoomWindow | null
31
+ }
32
+ let {
33
+ spec,
34
+ indicators = [],
35
+ syncGroup,
36
+ axisWidth = 56,
37
+ zoom = $bindable(null),
38
+ onZoom,
39
+ zoomable,
40
+ legend,
41
+ toolbar,
42
+ ...rest
43
+ }: Props = $props()
44
+
45
+ const uid = `svcp-${Math.random().toString(36).slice(2, 8)}`
46
+ const group = $derived(syncGroup ?? uid)
47
+ // Every pane is laid out at the stack's pixel width, so the panes line up
48
+ // and none of them is a 520px viewBox stretched across a 900px column.
49
+ // Heights stay what the specs say; only the width follows the container.
50
+ let hostW = $state(0)
51
+ const widthOf = (s: ChartSpec) => (hostW > 0 && !s.width ? { width: Math.round(hostW) } : {})
52
+ /** The price chart hides its x labels when a pane below will show them. */
53
+ const mainSpec = $derived<ChartSpec>({
54
+ ...spec,
55
+ ...widthOf(spec),
56
+ xAxis: { ...spec.xAxis, ...(indicators.length ? { labels: false } : {}) },
57
+ yAxis: { ...spec.yAxis, width: axisWidth },
58
+ ...(spec.y2Axis || spec.series.some((s) => s.axis === 'right') ? { y2Axis: { ...spec.y2Axis, width: axisWidth } } : {}),
59
+ })
60
+ // The pane's title is drawn by this component as a small corner label
61
+ // rather than by the chart frame, which would take a third of a 100px
62
+ // pane for it.
63
+ const panes = $derived(
64
+ indicators.map((ind, i) => {
65
+ const { title, ...pane } = indicatorPane(spec, ind)
66
+ return {
67
+ title: title ?? ind.kind,
68
+ spec: {
69
+ ...pane,
70
+ ...widthOf(pane),
71
+ xAxis: { ...pane.xAxis, labels: i === indicators.length - 1 },
72
+ yAxis: { ...pane.yAxis, width: axisWidth },
73
+ } satisfies ChartSpec,
74
+ }
75
+ }),
76
+ )
77
+ </script>
78
+
79
+ <div class="sv-chart-panes" bind:clientWidth={hostW}>
80
+ <div class="sv-chart-pane is-main">
81
+ <SvGridChart spec={mainSpec} syncGroup={group} bind:zoom {onZoom} {zoomable} {legend} {toolbar} {...rest} />
82
+ </div>
83
+ {#each panes as pane, i (`${indicators[i]?.kind}-${i}`)}
84
+ <div class="sv-chart-pane is-indicator" data-indicator={indicators[i]?.kind}>
85
+ <span class="sv-chart-pane-title" style:left={`${axisWidth + 6}px`}>{pane.title}</span>
86
+ <SvGridChart spec={pane.spec} syncGroup={group} zoomable={zoomable ? { drag: false, wheel: typeof zoomable === 'object' ? zoomable.wheel : false, pan: typeof zoomable === 'object' ? zoomable.pan : false } : false} legend={false} toolbar={false} interactive localeText={rest.localeText} />
87
+ </div>
88
+ {/each}
89
+ </div>
90
+
91
+ <style>
92
+ .sv-chart-panes {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 2px;
96
+ min-width: 0;
97
+ }
98
+ .sv-chart-pane {
99
+ position: relative;
100
+ min-width: 0;
101
+ }
102
+ /* The title of a pane sits in its top-left corner, small, like a terminal. */
103
+ /* Placed just right of the value-axis gutter, clear of the tick labels. */
104
+ .sv-chart-pane-title {
105
+ position: absolute;
106
+ top: 2px;
107
+ z-index: 1;
108
+ font-size: 10px;
109
+ font-weight: 600;
110
+ color: var(--sg-muted, #64748b);
111
+ pointer-events: none;
112
+ }
113
+ </style>
@@ -0,0 +1,66 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { flushSync, mount, unmount } from 'svelte'
3
+ import SvChartPanes from './SvChartPanes.svelte'
4
+ import type { ChartSpec, OhlcBar } from './chart-types'
5
+
6
+ let app: ReturnType<typeof mount> | null = null
7
+ let target: HTMLElement | null = null
8
+ afterEach(() => {
9
+ if (app) unmount(app)
10
+ app = null
11
+ target?.remove()
12
+ target = null
13
+ })
14
+
15
+ const closes = [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64]
16
+ const bars: OhlcBar[] = closes.map((c, i) => ({ o: i ? closes[i - 1]! : c, h: c + 1, l: c - 1, c }))
17
+ const price: ChartSpec = {
18
+ type: 'candlestick',
19
+ categories: closes.map((_, i) => `2026-01-${String(i + 1).padStart(2, '0')}`),
20
+ series: [{ label: 'ACME', values: closes, ohlc: bars, volumes: closes.map((_, i) => 100 + i) }],
21
+ xType: 'ordinal-time',
22
+ width: 600,
23
+ height: 240,
24
+ }
25
+
26
+ function render(props: Record<string, unknown>) {
27
+ target = document.createElement('div')
28
+ document.body.appendChild(target)
29
+ app = mount(SvChartPanes, { target, props: props as never })
30
+ flushSync()
31
+ return target
32
+ }
33
+
34
+ describe('SvChartPanes', () => {
35
+ it('stacks the price chart over one pane per indicator, x labels on the last pane only', () => {
36
+ const el = render({ spec: price, indicators: [{ kind: 'volume' }, { kind: 'rsi', params: { period: 5 } }] })
37
+ expect(el.querySelectorAll('.sv-grid-chart-svg')).toHaveLength(3)
38
+ const panes = [...el.querySelectorAll('.sv-chart-pane.is-indicator')]
39
+ expect(panes.map((p) => p.getAttribute('data-indicator'))).toEqual(['volume', 'rsi'])
40
+ expect(panes.map((p) => p.querySelector('.sv-chart-pane-title')!.textContent)).toEqual(['Volume', 'RSI 5'])
41
+ // Category (x) labels: none on the main chart and the volume pane, some on the RSI pane.
42
+ const xLabels = (root: Element) => root.querySelectorAll('text.sv-grid-chart-axis.is-x, .sv-grid-chart-xlabel').length
43
+ const [main, vol, rsi] = [el.querySelector('.sv-chart-pane.is-main')!, panes[0]!, panes[1]!]
44
+ expect(xLabels(rsi)).toBeGreaterThan(0)
45
+ expect(xLabels(main)).toBe(0)
46
+ expect(xLabels(vol)).toBe(0)
47
+ // The volume pane draws bars, the RSI pane a line with its 30 / 70 lines.
48
+ expect(vol.querySelectorAll('.sv-grid-chart-bar').length).toBe(20)
49
+ expect(rsi.querySelectorAll('.sv-grid-chart-refline').length).toBe(2)
50
+ })
51
+
52
+ it('shares the crosshair across the stack through a private sync group', () => {
53
+ const el = render({ spec: price, indicators: [{ kind: 'volume' }] })
54
+ const main = el.querySelector('.sv-chart-pane.is-main')!
55
+ ;(main.querySelectorAll('.sv-grid-chart-cat-hit')[3] as SVGElement).dispatchEvent(new FocusEvent('focus'))
56
+ flushSync()
57
+ expect(main.querySelector('.sv-grid-chart-crosshair')).toBeTruthy()
58
+ expect(el.querySelector('.sv-chart-pane.is-indicator .sv-grid-chart-crosshair')).toBeTruthy()
59
+ })
60
+
61
+ it('with no indicators it is one chart with its own x labels', () => {
62
+ const el = render({ spec: price })
63
+ expect(el.querySelectorAll('.sv-grid-chart-svg')).toHaveLength(1)
64
+ expect(el.querySelectorAll('text.sv-grid-chart-axis.is-x, .sv-grid-chart-xlabel').length).toBeGreaterThan(0)
65
+ })
66
+ })