@svgrid/grid 2.2.16 → 2.2.18

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 (290) hide show
  1. package/README.md +1 -1
  2. package/dist/GridFooter.svelte +12 -11
  3. package/dist/SvAnchor.svelte +65 -0
  4. package/dist/SvAnchor.svelte.d.ts +24 -0
  5. package/dist/SvAspectRatio.svelte +38 -0
  6. package/dist/SvAspectRatio.svelte.d.ts +16 -0
  7. package/dist/SvAutoComplete.svelte +52 -7
  8. package/dist/SvAutoComplete.svelte.d.ts +4 -1
  9. package/dist/SvBlockquote.svelte +43 -0
  10. package/dist/SvBlockquote.svelte.d.ts +16 -0
  11. package/dist/SvCalendar.svelte +33 -2
  12. package/dist/SvCalendar.svelte.d.ts +8 -1
  13. package/dist/SvCheckBox.svelte +6 -2
  14. package/dist/SvCode.svelte +52 -0
  15. package/dist/SvCode.svelte.d.ts +19 -0
  16. package/dist/SvCollapsible.svelte +75 -0
  17. package/dist/SvCollapsible.svelte.d.ts +27 -0
  18. package/dist/SvColorInput.svelte +11 -17
  19. package/dist/SvColorInput.svelte.d.ts +2 -0
  20. package/dist/SvComboBox.svelte +85 -8
  21. package/dist/SvComboBox.svelte.d.ts +14 -0
  22. package/dist/SvCountryInput.svelte +2 -2
  23. package/dist/SvCountryInput.svelte.d.ts +1 -1
  24. package/dist/SvDropDownList.svelte +154 -51
  25. package/dist/SvDropDownList.svelte.d.ts +17 -3
  26. package/dist/SvDurationInput.svelte +59 -41
  27. package/dist/SvDurationInput.svelte.d.ts +18 -0
  28. package/dist/SvField.svelte +203 -12
  29. package/dist/SvField.svelte.d.ts +45 -8
  30. package/dist/SvForm.svelte +195 -21
  31. package/dist/SvForm.svelte.d.ts +23 -0
  32. package/dist/SvGrid.controller.svelte.d.ts +12 -1
  33. package/dist/SvGrid.controller.svelte.js +87 -7
  34. package/dist/SvGrid.svelte +383 -181
  35. package/dist/SvGrid.types.d.ts +123 -2
  36. package/dist/SvGridChartView.svelte +70 -0
  37. package/dist/SvGridChartView.svelte.d.ts +11 -0
  38. package/dist/SvGridDropdown.svelte +9 -2
  39. package/dist/SvGridSelect.svelte +5 -2
  40. package/dist/SvGridSelect.svelte.d.ts +1 -1
  41. package/dist/SvGroup.svelte +41 -0
  42. package/dist/SvGroup.svelte.d.ts +27 -0
  43. package/dist/SvHoverCard.svelte +70 -0
  44. package/dist/SvHoverCard.svelte.d.ts +41 -0
  45. package/dist/SvKbd.svelte +51 -0
  46. package/dist/SvKbd.svelte.d.ts +18 -0
  47. package/dist/SvList.svelte +55 -0
  48. package/dist/SvList.svelte.d.ts +23 -0
  49. package/dist/SvListBox.svelte +166 -77
  50. package/dist/SvListBox.svelte.d.ts +17 -4
  51. package/dist/SvLoadingOverlay.svelte +53 -0
  52. package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
  53. package/dist/SvMark.svelte +36 -0
  54. package/dist/SvMark.svelte.d.ts +14 -0
  55. package/dist/SvMaskedInput.svelte +50 -35
  56. package/dist/SvMaskedInput.svelte.d.ts +17 -2
  57. package/dist/SvMenuList.svelte +32 -3
  58. package/dist/SvMenubar.svelte +200 -0
  59. package/dist/SvMenubar.svelte.d.ts +18 -0
  60. package/dist/SvMultiSelect.svelte +7 -4
  61. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  62. package/dist/SvNumberInput.svelte +79 -42
  63. package/dist/SvNumberInput.svelte.d.ts +25 -0
  64. package/dist/SvOtpInput.svelte +2 -2
  65. package/dist/SvPasswordInput.svelte +51 -31
  66. package/dist/SvPasswordInput.svelte.d.ts +17 -0
  67. package/dist/SvPhoneInput.svelte +39 -28
  68. package/dist/SvPhoneInput.svelte.d.ts +4 -0
  69. package/dist/SvPopconfirm.svelte +79 -0
  70. package/dist/SvPopconfirm.svelte.d.ts +42 -0
  71. package/dist/SvPopover.svelte +87 -29
  72. package/dist/SvPopover.svelte.d.ts +18 -4
  73. package/dist/SvRadioGroup.svelte +5 -1
  74. package/dist/SvResult.svelte +73 -0
  75. package/dist/SvResult.svelte.d.ts +28 -0
  76. package/dist/SvSegmented.svelte +114 -0
  77. package/dist/SvSegmented.svelte.d.ts +29 -0
  78. package/dist/SvSimpleGrid.svelte +41 -0
  79. package/dist/SvSimpleGrid.svelte.d.ts +25 -0
  80. package/dist/SvSlider.svelte +23 -10
  81. package/dist/SvSpinner.svelte +43 -0
  82. package/dist/SvSpinner.svelte.d.ts +20 -0
  83. package/dist/SvStack.svelte +37 -0
  84. package/dist/SvStack.svelte.d.ts +24 -0
  85. package/dist/SvSwitchButton.svelte +6 -1
  86. package/dist/SvTagsInput.svelte +9 -5
  87. package/dist/SvText.svelte +76 -0
  88. package/dist/SvText.svelte.d.ts +31 -0
  89. package/dist/SvTextInput.svelte +75 -43
  90. package/dist/SvTextInput.svelte.d.ts +28 -1
  91. package/dist/SvTimePicker.svelte +32 -2
  92. package/dist/SvTimePicker.svelte.d.ts +8 -9
  93. package/dist/SvTitle.svelte +57 -0
  94. package/dist/SvTitle.svelte.d.ts +24 -0
  95. package/dist/SvToaster.svelte +41 -6
  96. package/dist/SvToggleButton.svelte +4 -1
  97. package/dist/SvTooltip.svelte +68 -34
  98. package/dist/SvTooltip.svelte.d.ts +14 -2
  99. package/dist/SvTree.svelte +81 -21
  100. package/dist/SvTree.svelte.d.ts +3 -2
  101. package/dist/SvTreeSelect.svelte +5 -2
  102. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  103. package/dist/SvVisuallyHidden.svelte +49 -0
  104. package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
  105. package/dist/ai.d.ts +289 -0
  106. package/dist/ai.js +1013 -0
  107. package/dist/board-view.svelte.d.ts +22 -0
  108. package/dist/board-view.svelte.js +13 -0
  109. package/dist/build-api.js +36 -6
  110. package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
  111. package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
  112. package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
  113. package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
  114. package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
  115. package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
  116. package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
  117. package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
  118. package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
  119. package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
  120. package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
  121. package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
  122. package/dist/cdn/chart-T1usDN0o.js +1212 -0
  123. package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
  124. package/dist/cdn/export-format-JT5pWVT8.js +561 -0
  125. package/dist/cdn/popover-P-9P2YYn.js +69 -0
  126. package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
  127. package/dist/cdn/src-BLhdLmoN.js +25707 -0
  128. package/dist/cdn/src-BwnUVj3T.js +25623 -0
  129. package/dist/cdn/svgrid.js +9 -32897
  130. package/dist/cdn/svgrid.svelte-external.js +9 -30052
  131. package/dist/cell-formatting.d.ts +2 -2
  132. package/dist/cell-render.js +4 -4
  133. package/dist/chart-view.svelte.d.ts +25 -0
  134. package/dist/chart-view.svelte.js +14 -0
  135. package/dist/createCombobox.svelte.d.ts +5 -0
  136. package/dist/createCombobox.svelte.js +21 -5
  137. package/dist/createDropdownList.svelte.d.ts +3 -0
  138. package/dist/createDropdownList.svelte.js +8 -2
  139. package/dist/createForm.svelte.d.ts +18 -0
  140. package/dist/createForm.svelte.js +51 -6
  141. package/dist/createMenu.svelte.js +27 -0
  142. package/dist/createPopoverSelect.svelte.d.ts +3 -0
  143. package/dist/createPopoverSelect.svelte.js +9 -1
  144. package/dist/createSlider.svelte.d.ts +8 -21
  145. package/dist/createSlider.svelte.js +34 -0
  146. package/dist/createTooltip.svelte.d.ts +12 -0
  147. package/dist/createTooltip.svelte.js +72 -9
  148. package/dist/editor-contract.d.ts +15 -0
  149. package/dist/export-provider.d.ts +27 -0
  150. package/dist/export-provider.js +9 -0
  151. package/dist/filter-operators.d.ts +6 -0
  152. package/dist/filter-operators.js +33 -0
  153. package/dist/form-field.d.ts +45 -2
  154. package/dist/grid-messages.d.ts +61 -0
  155. package/dist/grid-messages.js +72 -0
  156. package/dist/group-display.d.ts +62 -0
  157. package/dist/group-display.js +78 -0
  158. package/dist/index.d.ts +36 -4
  159. package/dist/index.js +43 -2
  160. package/dist/js-scroller.svelte.d.ts +35 -0
  161. package/dist/js-scroller.svelte.js +154 -0
  162. package/dist/list-option.d.ts +46 -0
  163. package/dist/list-option.js +53 -0
  164. package/dist/menus.js +25 -11
  165. package/dist/pivot-view.svelte.d.ts +72 -0
  166. package/dist/pivot-view.svelte.js +13 -0
  167. package/dist/positioning.d.ts +111 -0
  168. package/dist/positioning.js +191 -0
  169. package/dist/scheduler-model.d.ts +3 -0
  170. package/dist/scheduler-model.js +22 -5
  171. package/dist/svgrid-wrapper.types.d.ts +18 -0
  172. package/dist/toast-store.svelte.d.ts +54 -0
  173. package/dist/toast-store.svelte.js +65 -15
  174. package/package.json +9 -1
  175. package/src/GridFooter.svelte +12 -11
  176. package/src/SvAnchor.svelte +65 -0
  177. package/src/SvAspectRatio.svelte +38 -0
  178. package/src/SvAutoComplete.svelte +52 -7
  179. package/src/SvBlockquote.svelte +43 -0
  180. package/src/SvCalendar.svelte +33 -2
  181. package/src/SvCalendar.test.ts +15 -0
  182. package/src/SvCheckBox.svelte +6 -2
  183. package/src/SvCode.svelte +52 -0
  184. package/src/SvCollapsible.svelte +75 -0
  185. package/src/SvColorInput.svelte +11 -17
  186. package/src/SvComboBox.svelte +85 -8
  187. package/src/SvCountryInput.svelte +2 -2
  188. package/src/SvDropDownList.svelte +154 -51
  189. package/src/SvDurationInput.svelte +59 -41
  190. package/src/SvField.svelte +203 -12
  191. package/src/SvForm.svelte +195 -21
  192. package/src/SvForm.test.ts +112 -0
  193. package/src/SvGrid.controller.svelte.ts +88 -7
  194. package/src/SvGrid.svelte +383 -181
  195. package/src/SvGrid.types.ts +125 -1
  196. package/src/SvGridChartView.svelte +70 -0
  197. package/src/SvGridDropdown.svelte +9 -2
  198. package/src/SvGridSelect.svelte +5 -2
  199. package/src/SvGroup.svelte +41 -0
  200. package/src/SvHoverCard.svelte +70 -0
  201. package/src/SvKbd.svelte +51 -0
  202. package/src/SvList.svelte +55 -0
  203. package/src/SvListBox.svelte +166 -77
  204. package/src/SvLoadingOverlay.svelte +53 -0
  205. package/src/SvMark.svelte +36 -0
  206. package/src/SvMaskedInput.svelte +50 -35
  207. package/src/SvMenuList.svelte +32 -3
  208. package/src/SvMenubar.svelte +200 -0
  209. package/src/SvMultiSelect.svelte +7 -4
  210. package/src/SvNumberInput.svelte +79 -42
  211. package/src/SvOtpInput.svelte +2 -2
  212. package/src/SvPasswordInput.svelte +51 -31
  213. package/src/SvPhoneInput.svelte +39 -28
  214. package/src/SvPopconfirm.svelte +79 -0
  215. package/src/SvPopover.svelte +87 -29
  216. package/src/SvRadioGroup.svelte +5 -1
  217. package/src/SvResult.svelte +73 -0
  218. package/src/SvSegmented.svelte +114 -0
  219. package/src/SvSimpleGrid.svelte +41 -0
  220. package/src/SvSlider.svelte +23 -10
  221. package/src/SvSpinner.svelte +43 -0
  222. package/src/SvStack.svelte +37 -0
  223. package/src/SvSwitchButton.svelte +6 -1
  224. package/src/SvTagsInput.svelte +9 -5
  225. package/src/SvText.svelte +76 -0
  226. package/src/SvTextInput.svelte +75 -43
  227. package/src/SvTimePicker.svelte +32 -2
  228. package/src/SvTitle.svelte +57 -0
  229. package/src/SvToaster.svelte +41 -6
  230. package/src/SvToaster.test.ts +32 -0
  231. package/src/SvToggleButton.svelte +4 -1
  232. package/src/SvTooltip.svelte +68 -34
  233. package/src/SvTree.svelte +81 -21
  234. package/src/SvTreeSelect.svelte +5 -2
  235. package/src/SvVisuallyHidden.svelte +49 -0
  236. package/src/ai.test.ts +502 -0
  237. package/src/ai.ts +1391 -0
  238. package/src/board-view.svelte.ts +33 -0
  239. package/src/build-api.coverage.test.ts +101 -0
  240. package/src/build-api.ts +43 -18
  241. package/src/builtin-editors.grid.test.ts +1 -1
  242. package/src/cell-formatting.ts +5 -5
  243. package/src/cell-render.ts +4 -4
  244. package/src/chart-view.svelte.ts +36 -0
  245. package/src/createCombobox.svelte.ts +17 -5
  246. package/src/createDropdownList.svelte.ts +8 -2
  247. package/src/createForm.svelte.ts +59 -6
  248. package/src/createMenu.svelte.ts +23 -0
  249. package/src/createPopoverSelect.svelte.ts +8 -1
  250. package/src/createSlider.svelte.ts +20 -0
  251. package/src/createTooltip.svelte.ts +67 -9
  252. package/src/editor-contract.ts +16 -0
  253. package/src/export-provider.ts +40 -0
  254. package/src/filter-operators.ts +37 -0
  255. package/src/form-field.ts +42 -2
  256. package/src/grid-messages.test.ts +24 -0
  257. package/src/grid-messages.ts +137 -0
  258. package/src/group-display.test.ts +77 -0
  259. package/src/group-display.ts +117 -0
  260. package/src/i18n.grid.test.ts +73 -0
  261. package/src/index.ts +91 -2
  262. package/src/js-scroller.svelte.ts +173 -0
  263. package/src/list-option.test.ts +56 -0
  264. package/src/list-option.ts +81 -0
  265. package/src/menus.test.ts +32 -0
  266. package/src/menus.ts +25 -12
  267. package/src/pivot-view.svelte.ts +90 -0
  268. package/src/pivot-view.test.ts +38 -0
  269. package/src/pivot.grid.test.ts +102 -0
  270. package/src/positioning.test.ts +141 -0
  271. package/src/positioning.ts +266 -0
  272. package/src/scheduler-model.test.ts +16 -0
  273. package/src/scheduler-model.ts +24 -5
  274. package/src/svgrid-wrapper.types.ts +23 -0
  275. package/src/svgrid.charting.test.ts +8 -1
  276. package/src/svgrid.comments-autocomplete.test.ts +7 -2
  277. package/src/svgrid.context-menu.test.ts +8 -3
  278. package/src/toast-store.svelte.ts +98 -0
  279. package/src/toast-store.test.ts +62 -1
  280. package/src/ui-breadth.test.ts +138 -0
  281. package/src/ui-buttons.test.ts +39 -0
  282. package/src/ui-inputs.test.ts +2 -2
  283. package/src/ui-range.test.ts +9 -0
  284. package/src/ui-selection.test.ts +36 -0
  285. package/src/ui-tier1.test.ts +1 -1
  286. package/src/virtual.test.ts +20 -6
  287. package/dist/SvGridBoard.svelte +0 -2339
  288. package/dist/SvGridBoard.svelte.d.ts +0 -31
  289. package/src/SvGridBoard.svelte +0 -2339
  290. package/src/board.test.ts +0 -927
@@ -0,0 +1,266 @@
1
+ /**
2
+ * positioning - a small, dependency-free anchor-positioning engine for the
3
+ * overlay layer (popover, tooltip, menu, dropdowns). It replaces the vertical-
4
+ * flip-only `anchoredRect` for components that need the full placement matrix.
5
+ *
6
+ * Two halves:
7
+ *
8
+ * 1. `computePosition(reference, floating, options)` - PURE geometry. Given the
9
+ * reference rect and the floating size (plus a viewport), it returns the
10
+ * floating `x`/`y`, the resolved `placement` (after flip), the arrow offset,
11
+ * and the available `maxWidth`/`maxHeight` on the chosen side. No DOM, so it
12
+ * is fully unit-tested by feeding rects and asserting placement.
13
+ *
14
+ * 2. `autoUpdate(referenceEl, floatingEl, update)` - the browser half. Re-runs
15
+ * `update` whenever the reference/floating resize, an ancestor scrolls, or the
16
+ * window resizes (via `ResizeObserver` + scroll/resize listeners). SSR-safe
17
+ * (a no-op without `window`).
18
+ *
19
+ * The middleware (flip / shift / size / arrow) is implemented inline - no
20
+ * Floating-UI dependency - matching the kit's zero-runtime-dep posture.
21
+ */
22
+
23
+ /** A physical side the floating element is placed on, relative to the reference. */
24
+ export type Side = 'top' | 'bottom' | 'left' | 'right'
25
+
26
+ /** Cross-axis alignment. `center` is the bare side (e.g. `'bottom'`). */
27
+ export type Align = 'start' | 'center' | 'end'
28
+
29
+ /** `'bottom'` = bottom-center; `'bottom-start'` / `'bottom-end'` align the edges. */
30
+ export type Placement = Side | `${Side}-start` | `${Side}-end`
31
+
32
+ /** A viewport-relative rectangle (CSS `getBoundingClientRect` shape suffices). */
33
+ export type Rect = { x: number; y: number; width: number; height: number }
34
+
35
+ /** The available viewport (defaults to `window` in the browser). */
36
+ export type Viewport = { width: number; height: number }
37
+
38
+ export type ComputePositionOptions = {
39
+ /** Preferred placement. Default `'bottom-start'`. */
40
+ placement?: Placement
41
+ /** Main-axis gap between reference and floating. Default 6. */
42
+ offset?: number
43
+ /** Viewport edge kept clear on every side. Default 8. */
44
+ padding?: number
45
+ /** Flip to the opposite side when the preferred side overflows. Default true. */
46
+ flip?: boolean
47
+ /** Shift along the cross axis to stay in view (without flipping). Default true. */
48
+ shift?: boolean
49
+ /** Compute `maxWidth`/`maxHeight` for the chosen side. Default true. */
50
+ size?: boolean
51
+ /** When set, compute the arrow offset for an arrow element of this size (px). */
52
+ arrow?: { size: number }
53
+ /** Extra placements to try (after the automatic opposite) before giving up. */
54
+ fallbackPlacements?: Placement[]
55
+ /** Viewport to fit within. Defaults to the window; pure callers pass it. */
56
+ viewport?: Viewport
57
+ /** Floor for the available main-axis size returned by `size`. Default 0. */
58
+ minMainAxis?: number
59
+ }
60
+
61
+ export type ComputePositionResult = {
62
+ /** Floating left, in viewport (fixed-position) coordinates. */
63
+ x: number
64
+ /** Floating top, in viewport (fixed-position) coordinates. */
65
+ y: number
66
+ /** The resolved placement after flip. */
67
+ placement: Placement
68
+ /** The resolved side after flip. */
69
+ side: Side
70
+ /** The resolved cross-axis alignment. */
71
+ align: Align
72
+ /** Space available for the floating on the chosen side's main axis (+cross). */
73
+ maxWidth: number
74
+ maxHeight: number
75
+ /**
76
+ * Arrow offset within the floating box: `x` for top/bottom placements, `y` for
77
+ * left/right. Only present when `options.arrow` is set. Clamped so the arrow
78
+ * stays inside the floating (minus padding).
79
+ */
80
+ arrow?: { x?: number; y?: number }
81
+ }
82
+
83
+ const SIDES: Record<Side, Side> = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }
84
+
85
+ /** Split a placement into its side + alignment (`center` when no suffix). */
86
+ export function parsePlacement(p: Placement): { side: Side; align: Align } {
87
+ const [side, align] = p.split('-') as [Side, 'start' | 'end' | undefined]
88
+ return { side, align: align ?? 'center' }
89
+ }
90
+
91
+ function opposite(p: Placement): Placement {
92
+ const { side, align } = parsePlacement(p)
93
+ return (align === 'center' ? SIDES[side] : `${SIDES[side]}-${align}`) as Placement
94
+ }
95
+
96
+ function clamp(v: number, min: number, max: number): number {
97
+ if (max < min) return min
98
+ return v < min ? min : v > max ? max : v
99
+ }
100
+
101
+ /** Floating x/y for a given side+align (before flip/shift). */
102
+ function coordsFor(side: Side, align: Align, r: Rect, f: Viewport, offset: number): { x: number; y: number } {
103
+ if (side === 'top' || side === 'bottom') {
104
+ const y = side === 'top' ? r.y - f.height - offset : r.y + r.height + offset
105
+ const x =
106
+ align === 'start' ? r.x : align === 'end' ? r.x + r.width - f.width : r.x + r.width / 2 - f.width / 2
107
+ return { x, y }
108
+ }
109
+ const x = side === 'left' ? r.x - f.width - offset : r.x + r.width + offset
110
+ const y =
111
+ align === 'start' ? r.y : align === 'end' ? r.y + r.height - f.height : r.y + r.height / 2 - f.height / 2
112
+ return { x, y }
113
+ }
114
+
115
+ /** How far the floating exceeds the viewport on the placement's MAIN axis. >0 = overflow. */
116
+ function mainAxisOverflow(side: Side, x: number, y: number, f: Viewport, v: Viewport, pad: number): number {
117
+ switch (side) {
118
+ case 'top':
119
+ return pad - y
120
+ case 'bottom':
121
+ return y + f.height - (v.height - pad)
122
+ case 'left':
123
+ return pad - x
124
+ default:
125
+ return x + f.width - (v.width - pad)
126
+ }
127
+ }
128
+
129
+ function resolveViewport(v?: Viewport): Viewport {
130
+ if (v) return v
131
+ if (typeof window !== 'undefined') return { width: window.innerWidth, height: window.innerHeight }
132
+ return { width: 0, height: 0 }
133
+ }
134
+
135
+ /**
136
+ * Compute the floating position anchored to `reference`, applying flip (opposite
137
+ * side when the preferred side overflows), shift (cross-axis clamp to stay in
138
+ * view), size (available max width/height on the chosen side) and an optional
139
+ * arrow offset. Pure - pass an explicit `viewport` in tests.
140
+ */
141
+ export function computePosition(
142
+ reference: Rect,
143
+ floating: Viewport,
144
+ options: ComputePositionOptions = {},
145
+ ): ComputePositionResult {
146
+ const offset = options.offset ?? 6
147
+ const pad = options.padding ?? 8
148
+ const doFlip = options.flip !== false
149
+ const doShift = options.shift !== false
150
+ const v = resolveViewport(options.viewport)
151
+ const preferred = options.placement ?? 'bottom-start'
152
+
153
+ // ---- flip: pick the placement whose main-axis overflow is smallest ----
154
+ const candidates: Placement[] = [preferred]
155
+ if (doFlip) {
156
+ candidates.push(opposite(preferred))
157
+ for (const fb of options.fallbackPlacements ?? []) candidates.push(fb)
158
+ }
159
+ let chosen = preferred
160
+ let best = Infinity
161
+ for (const cand of candidates) {
162
+ const { side, align } = parsePlacement(cand)
163
+ const { x, y } = coordsFor(side, align, reference, floating, offset)
164
+ const overflow = mainAxisOverflow(side, x, y, floating, v, pad)
165
+ if (overflow <= 0) {
166
+ chosen = cand
167
+ best = overflow
168
+ break
169
+ }
170
+ if (overflow < best) {
171
+ best = overflow
172
+ chosen = cand
173
+ }
174
+ }
175
+
176
+ const { side, align } = parsePlacement(chosen)
177
+ let { x, y } = coordsFor(side, align, reference, floating, offset)
178
+
179
+ // ---- shift: clamp along the CROSS axis so the floating stays in view ----
180
+ if (doShift) {
181
+ if (side === 'top' || side === 'bottom') {
182
+ x = clamp(x, pad, v.width - pad - floating.width)
183
+ } else {
184
+ y = clamp(y, pad, v.height - pad - floating.height)
185
+ }
186
+ }
187
+
188
+ // ---- size: available room on the chosen side ----
189
+ const minMain = options.minMainAxis ?? 0
190
+ let maxWidth: number
191
+ let maxHeight: number
192
+ if (side === 'top' || side === 'bottom') {
193
+ const avail = side === 'top' ? reference.y - offset - pad : v.height - pad - (reference.y + reference.height + offset)
194
+ maxHeight = Math.max(minMain, Math.floor(avail))
195
+ maxWidth = Math.max(0, v.width - 2 * pad)
196
+ } else {
197
+ const avail = side === 'left' ? reference.x - offset - pad : v.width - pad - (reference.x + reference.width + offset)
198
+ maxWidth = Math.max(minMain, Math.floor(avail))
199
+ maxHeight = Math.max(0, v.height - 2 * pad)
200
+ }
201
+
202
+ const result: ComputePositionResult = { x, y, placement: chosen, side, align, maxWidth, maxHeight }
203
+
204
+ // ---- arrow: point at the reference center, clamped inside the floating ----
205
+ if (options.arrow) {
206
+ const a = options.arrow.size
207
+ if (side === 'top' || side === 'bottom') {
208
+ const center = reference.x + reference.width / 2
209
+ const ax = clamp(center - x - a / 2, pad, floating.width - a - pad)
210
+ result.arrow = { x: ax }
211
+ } else {
212
+ const center = reference.y + reference.height / 2
213
+ const ay = clamp(center - y - a / 2, pad, floating.height - a - pad)
214
+ result.arrow = { y: ay }
215
+ }
216
+ }
217
+
218
+ return result
219
+ }
220
+
221
+ /** Collect scrollable ancestors so `autoUpdate` can listen to their scroll. */
222
+ function scrollParents(node: Element): Element[] {
223
+ const out: Element[] = []
224
+ let el: Element | null = node.parentElement
225
+ while (el) {
226
+ const style = getComputedStyle(el)
227
+ if (/(auto|scroll|overlay|hidden)/.test(style.overflow + style.overflowY + style.overflowX)) out.push(el)
228
+ el = el.parentElement
229
+ }
230
+ return out
231
+ }
232
+
233
+ /**
234
+ * Re-run `update` whenever the anchored position could change: the reference or
235
+ * floating element resizes, an ancestor scrolls, or the window scrolls/resizes.
236
+ * Returns a cleanup function. SSR-safe (a no-op without `window`).
237
+ *
238
+ * ```ts
239
+ * const stop = autoUpdate(triggerEl, panelEl, () => reposition())
240
+ * // ...later
241
+ * stop()
242
+ * ```
243
+ */
244
+ export function autoUpdate(reference: Element, floating: HTMLElement, update: () => void): () => void {
245
+ if (typeof window === 'undefined') return () => {}
246
+ const ancestors = scrollParents(reference)
247
+ for (const a of ancestors) a.addEventListener('scroll', update, { passive: true })
248
+ window.addEventListener('scroll', update, { passive: true })
249
+ window.addEventListener('resize', update)
250
+
251
+ let ro: ResizeObserver | undefined
252
+ if (typeof ResizeObserver === 'function') {
253
+ ro = new ResizeObserver(update)
254
+ ro.observe(reference)
255
+ ro.observe(floating)
256
+ }
257
+
258
+ update()
259
+
260
+ return () => {
261
+ for (const a of ancestors) a.removeEventListener('scroll', update)
262
+ window.removeEventListener('scroll', update)
263
+ window.removeEventListener('resize', update)
264
+ ro?.disconnect()
265
+ }
266
+ }
@@ -151,6 +151,22 @@ describe('resolveEvents', () => {
151
151
  expect(other.isException).toBeFalsy()
152
152
  })
153
153
 
154
+ it('applies per-occurrence `fields` overrides to one instance (merged row + resource)', () => {
155
+ const rows: Row[] = [
156
+ { ...weekly, who: 'room1', exceptions: [{ occurrenceStart: '2026-07-13T09:30', fields: { who: 'room9', title: 'Standup (offsite)' } }] },
157
+ ]
158
+ const out = resolveEvents(rows, spec(), at(2026, 7, 1), at(2026, 8, 1))
159
+ const overridden = out.find((e) => e.start.getDate() === 13)!
160
+ expect(overridden.resourceId).toBe('room9') // re-derived from the merged row
161
+ expect((overridden.row as Row).who).toBe('room9') // the instance row carries the override
162
+ expect(overridden.title).toBe('Standup (offsite)') // title from fields when not set explicitly
163
+ expect(overridden.isException).toBe(true)
164
+ // Siblings keep the base resource + row.
165
+ const sibling = out.find((e) => e.start.getDate() === 20)!
166
+ expect(sibling.resourceId).toBe('room1')
167
+ expect((sibling.row as Row).who).toBe('room1')
168
+ })
169
+
154
170
  it('sorts all-day before timed at the same start', () => {
155
171
  const rows: Row[] = [
156
172
  { id: 'timed', title: 't', start: '2026-07-15T00:00', end: '2026-07-15T01:00' },
@@ -65,6 +65,9 @@ export type RecurrenceException = {
65
65
  end?: DateLike | null
66
66
  title?: string
67
67
  allDay?: boolean
68
+ /** Per-occurrence overrides for any other field, merged over the row for just
69
+ * this occurrence (attendees, calendar, color, resource, ...). */
70
+ fields?: Record<string, unknown>
68
71
  }
69
72
 
70
73
  /** Field accessors the model uses to read an event off a row. The component
@@ -251,6 +254,14 @@ export function resolveEvents<TData>(
251
254
  let iEnd = new Date(occStart.getTime() + durationMs)
252
255
  let iTitle = title
253
256
  let iAllDay = allDay
257
+ // Per-occurrence overrides of ANY other field: merge onto a copy of the
258
+ // row for this instance, then re-derive the surfaced props from it so a
259
+ // "This event" change to attendees / calendar / color / resource shows.
260
+ let iRow = row
261
+ let iColor = color
262
+ let iColor2 = color2
263
+ let iStatus = status
264
+ let iResource = resourceId
254
265
  if (ex) {
255
266
  const exS = toDate(ex.start)
256
267
  const exE = toDate(ex.end)
@@ -262,21 +273,29 @@ export function resolveEvents<TData>(
262
273
  }
263
274
  if (ex.title != null) iTitle = ex.title
264
275
  if (ex.allDay != null) iAllDay = ex.allDay
276
+ if (ex.fields) {
277
+ iRow = { ...(row as Record<string, unknown>), ...ex.fields } as TData
278
+ iColor = spec.getColor?.(iRow) ?? color
279
+ iColor2 = spec.getSecondaryColor?.(iRow) ?? color2
280
+ iStatus = spec.getStatus?.(iRow) ?? status
281
+ iResource = spec.getResource?.(iRow) ?? resourceId
282
+ if (ex.title == null) iTitle = spec.getTitle?.(iRow) ?? iTitle
283
+ }
265
284
  }
266
285
  if (!rangesOverlap(iStart, iEnd, rangeStart, rangeEnd)) continue
267
286
  out.push({
268
287
  // Key on the CANONICAL occurrence start so a moved occurrence keeps its identity.
269
288
  key: `${rowKey}#${occStart.getFullYear()}-${occStart.getMonth() + 1}-${occStart.getDate()}T${occStart.getHours()}:${occStart.getMinutes()}`,
270
289
  rowKey,
271
- row,
290
+ row: iRow,
272
291
  title: iTitle,
273
292
  start: iStart,
274
293
  end: iEnd,
275
294
  allDay: iAllDay,
276
- color,
277
- color2,
278
- status,
279
- resourceId,
295
+ color: iColor,
296
+ color2: iColor2,
297
+ status: iStatus,
298
+ resourceId: iResource,
280
299
  recurring: true,
281
300
  occurrenceStart: occStart,
282
301
  isException: !!ex,
@@ -1,4 +1,7 @@
1
1
  import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core'
2
+ // Type-only import (the reverse of SvGrid.types importing SvGridApi); TS resolves the
3
+ // type-level cycle. Backs the generic key/value typing of `setOption` / `getOption`.
4
+ import type { Props } from './SvGrid.types'
2
5
  import type { GridExportOptions, GridClipboardOptions } from './export-format'
3
6
  import type { ChartSpec, ChartType } from './chart'
4
7
 
@@ -194,6 +197,26 @@ export type SvGridApi<
194
197
  setSort(columnId: string, direction: 'asc' | 'desc' | null): void
195
198
  clearSort(): void
196
199
  setGroupBy(columnIds: ReadonlyArray<string>): void
200
+
201
+ // ----- Options (runtime prop overrides) -----
202
+ /**
203
+ * Change a grid prop at runtime, e.g. `api.setOption('sortable', true)`. The
204
+ * override is merged over the incoming prop and the grid re-renders reactively -
205
+ * identical to the parent passing a new prop value. Pass `undefined` to clear the
206
+ * override and fall back to the prop.
207
+ *
208
+ * Note: seed-once props are NOT retroactive - `initialSorting`,
209
+ * `initialColumnPinning`, `initialHiddenColumns`, `columnOrder`, the initial
210
+ * `pageSize`, and the one-shot `externalSort` / `externalFilter` are read at mount,
211
+ * so overriding them later has no effect. Prefer the dedicated data path for
212
+ * `data` / `columns` (this works, but the sync effects are the idiomatic route).
213
+ */
214
+ setOption<K extends keyof Props<TFeatures, TData>>(key: K, value: Props<TFeatures, TData>[K] | undefined): void
215
+ /** Read a prop's effective value: the runtime override if set, else the incoming prop. */
216
+ getOption<K extends keyof Props<TFeatures, TData>>(key: K): Props<TFeatures, TData>[K]
217
+ /** Clear every runtime override set via `setOption`, reverting to the incoming props. */
218
+ resetOptions(): void
219
+
197
220
  /** Set the operator filter for a column. Pass `null` to clear. */
198
221
  setFilter(
199
222
  columnId: string,
@@ -73,11 +73,15 @@ function mountGrid(overrides: Record<string, unknown> = {}): Promise<MountResult
73
73
  })
74
74
  }
75
75
 
76
- const tick = () => Promise.resolve()
76
+ // A macrotask boundary: drains all pending microtasks (Svelte effects, the chart
77
+ // panel's lazy import() + its .then, and the follow-up flush) so the docked panel
78
+ // is mounted before assertions. The panel is loaded lazily as its own chunk.
79
+ const tick = () => new Promise((r) => setTimeout(r))
77
80
 
78
81
  describe('SvGrid built-in charting', () => {
79
82
  it('derives a chart spec from all displayed rows (default dimension + measure)', async () => {
80
83
  const { api, destroy } = await mountGrid({ charting: true })
84
+ await tick() // the chart engine loads lazily; ready a tick after mount
81
85
  try {
82
86
  const spec = api.getChartSpec()
83
87
  expect(spec).not.toBeNull()
@@ -398,6 +402,7 @@ describe('SvGrid built-in charting', () => {
398
402
 
399
403
  it('splits into one series per distinct value of the `series` field', async () => {
400
404
  const { api, destroy } = await mountGrid({ charting: { dimension: 'team', series: 'name', measures: 'age' } })
405
+ await tick() // the chart engine loads lazily; ready a tick after mount
401
406
  try {
402
407
  const spec = api.getChartSpec()
403
408
  // categories = distinct teams (4); one series per distinct name (6).
@@ -409,6 +414,7 @@ describe('SvGrid built-in charting', () => {
409
414
 
410
415
  it('plots one series per measure with `measures[]`', async () => {
411
416
  const { api, destroy } = await mountGrid({ charting: { dimension: 'team', measures: ['age', 'salary'] } })
417
+ await tick() // the chart engine loads lazily; ready a tick after mount
412
418
  try {
413
419
  const spec = api.getChartSpec()
414
420
  expect(spec!.series.length).toBe(2)
@@ -419,6 +425,7 @@ describe('SvGrid built-in charting', () => {
419
425
 
420
426
  it('marks the spec stacked when config.stacked is set', async () => {
421
427
  const { api, destroy } = await mountGrid({ charting: { dimension: 'team', series: 'name', measures: 'age', stacked: true } })
428
+ await tick() // the chart engine loads lazily; ready a tick after mount
422
429
  try {
423
430
  expect(api.getChartSpec()!.stacked).toBe(true)
424
431
  } finally {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Track C: free-text autocomplete cell type + editable cell comments.
3
3
  */
4
- import { describe, expect, it } from 'vitest'
4
+ import { describe, expect, it, vi } from 'vitest'
5
5
  import { mount, unmount } from 'svelte'
6
6
  import SvGrid from './SvGrid.svelte'
7
7
  import { createCoreRowModel, tableFeatures } from './index'
@@ -13,7 +13,10 @@ const rows: Row[] = [
13
13
  { id: 1, name: 'Ann', city: 'Paris' },
14
14
  { id: 2, name: 'Bo', city: 'Berlin' },
15
15
  ]
16
- const tick = () => new Promise<void>((r) => queueMicrotask(r))
16
+ // Macrotask boundary: drains pending microtasks incl. the menu overlay's lazy
17
+ // import() + flush, so the context menu is mounted before assertions (GridMenus
18
+ // loads lazily as its own chunk the first time a menu/tooltip opens).
19
+ const tick = () => new Promise<void>((r) => setTimeout(r))
17
20
 
18
21
  function mountGrid(extraCols: ColumnDef<typeof features, Row>[], props: Record<string, unknown> = {}) {
19
22
  return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
@@ -90,6 +93,8 @@ describe('editable comments', () => {
90
93
  await tick()
91
94
  const cell = dataCell(target, 0)
92
95
  cell.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))
96
+ // First menu-open triggers GridMenus' lazy import; poll until it mounts.
97
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull())
93
98
  await tick()
94
99
  const editItem = [...target.querySelectorAll('.sv-grid-context-menu .sv-grid-menu-item')].find(
95
100
  (b) => b.textContent?.trim() === 'Edit comment',
@@ -3,7 +3,7 @@
3
3
  * fires a synthetic `contextmenu` event on a cell, and asserts the menu
4
4
  * opens with the expected items and that actions/close work.
5
5
  */
6
- import { describe, expect, it } from 'vitest'
6
+ import { describe, expect, it, vi } from 'vitest'
7
7
  import { mount, unmount } from 'svelte'
8
8
  import SvGrid from './SvGrid.svelte'
9
9
  import {
@@ -29,7 +29,10 @@ const cols: ColumnDef<typeof features, Row>[] = [
29
29
  { field: 'name', header: 'Name', width: 200, editorType: 'text' },
30
30
  { field: 'team', header: 'Team', width: 160, editorType: 'text' },
31
31
  ]
32
- const tick = () => new Promise<void>((r) => queueMicrotask(r))
32
+ // Macrotask boundary: drains pending microtasks incl. the menu overlay's lazy
33
+ // import() + flush, so the context menu is mounted before assertions (GridMenus
34
+ // loads lazily as its own chunk the first time a menu/tooltip opens).
35
+ const tick = () => new Promise<void>((r) => setTimeout(r))
33
36
 
34
37
  function mountGrid(contextMenu: unknown) {
35
38
  return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
@@ -76,7 +79,9 @@ describe('SvGrid context menu', () => {
76
79
  const { target, destroy } = await mountGrid(true)
77
80
  await tick()
78
81
  rightClickFirstCell(target)
79
- await tick()
82
+ // First menu-open triggers GridMenus' lazy import (transforming the module on
83
+ // first load can exceed a single tick); poll until the overlay mounts.
84
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull())
80
85
  const menu = target.querySelector('.sv-grid-context-menu')
81
86
  expect(menu).not.toBeNull()
82
87
  const labels = [...menu!.querySelectorAll('.sv-grid-menu-item')].map((b) => b.textContent?.trim())
@@ -13,10 +13,20 @@
13
13
  *
14
14
  * Mount a single <SvToaster /> once near the app root to render the queue.
15
15
  */
16
+ import type { Snippet } from 'svelte'
16
17
  import { announce } from './a11y/live-region'
17
18
 
18
19
  export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
19
20
 
21
+ /** A button rendered inside a toast (a primary `action` or a secondary `cancel`). */
22
+ export type ToastAction = {
23
+ label: string
24
+ /** Invoked with the toast id when pressed. */
25
+ onClick?: (id: number) => void
26
+ /** Keep the toast open after the click (default: dismiss it). */
27
+ keepOpen?: boolean
28
+ }
29
+
20
30
  export type ToastOptions = {
21
31
  variant?: ToastVariant
22
32
  /** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
@@ -25,6 +35,12 @@ export type ToastOptions = {
25
35
  dismissible?: boolean
26
36
  /** Optional bold title above the message. */
27
37
  title?: string
38
+ /** Primary action button. */
39
+ action?: ToastAction
40
+ /** Secondary (cancel) action button. */
41
+ cancel?: ToastAction
42
+ /** Render a fully custom toast body instead of the icon + title + message. */
43
+ render?: Snippet<[Toast]>
28
44
  }
29
45
 
30
46
  export type Toast = {
@@ -34,6 +50,19 @@ export type Toast = {
34
50
  duration: number
35
51
  dismissible: boolean
36
52
  title?: string
53
+ action?: ToastAction
54
+ cancel?: ToastAction
55
+ render?: Snippet<[Toast]>
56
+ }
57
+
58
+ /** Patch applied by `toast.update(id, patch)`; every field is optional. */
59
+ export type UpdateToast = Partial<Omit<Toast, 'id'>>
60
+
61
+ /** Messages for `toast.promise`; success/error may derive from the value/error. */
62
+ export type PromiseMessages<T> = {
63
+ loading: string
64
+ success: string | ((value: T) => string)
65
+ error: string | ((error: unknown) => string)
37
66
  }
38
67
 
39
68
  type VariantOptions = Omit<ToastOptions, 'variant'>
@@ -44,6 +73,17 @@ export type ToastFn = {
44
73
  success: (message: string, options?: VariantOptions) => number
45
74
  warning: (message: string, options?: VariantOptions) => number
46
75
  error: (message: string, options?: VariantOptions) => number
76
+ /**
77
+ * Show a sticky loading toast, then update it IN PLACE to success/error when
78
+ * the promise settles. Returns the original promise so callers can await it.
79
+ */
80
+ promise: <T>(promise: Promise<T>, messages: PromiseMessages<T>, options?: ToastOptions) => Promise<T>
81
+ /** Patch an existing toast (message/variant/title/action/duration/render). */
82
+ update: (id: number, patch: UpdateToast) => void
83
+ /** Push a toast with a fully custom body snippet. Returns its id. */
84
+ custom: (render: Snippet<[Toast]>, options?: ToastOptions) => number
85
+ /** Dismiss a toast by id. */
86
+ dismiss: (id: number) => void
47
87
  }
48
88
 
49
89
  function createToastStore() {
@@ -104,6 +144,9 @@ function createToastStore() {
104
144
  duration,
105
145
  dismissible: options.dismissible ?? true,
106
146
  title: options.title,
147
+ action: options.action,
148
+ cancel: options.cancel,
149
+ render: options.render,
107
150
  }
108
151
  toasts.push(entry)
109
152
  // Errors/warnings interrupt (assertive); info/success are polite.
@@ -114,17 +157,70 @@ function createToastStore() {
114
157
  return id
115
158
  }
116
159
 
160
+ /** Patch an existing toast in place (used by `promise`, and callable directly). */
161
+ function update(id: number, patch: UpdateToast): void {
162
+ const t = toasts.find((x) => x.id === id)
163
+ if (!t) return
164
+ if (patch.message != null) t.message = patch.message
165
+ if (patch.title !== undefined) t.title = patch.title
166
+ if (patch.variant) t.variant = patch.variant
167
+ if (patch.dismissible !== undefined) t.dismissible = patch.dismissible
168
+ if (patch.action !== undefined) t.action = patch.action
169
+ if (patch.cancel !== undefined) t.cancel = patch.cancel
170
+ if (patch.render !== undefined) t.render = patch.render
171
+ // A new duration restarts the auto-dismiss timer (0 = make it sticky again).
172
+ if (patch.duration !== undefined) {
173
+ const timer = timers.get(id)
174
+ if (timer) { clearTimeout(timer); timers.delete(id) }
175
+ meta.delete(id)
176
+ t.duration = patch.duration
177
+ if (patch.duration > 0) schedule(id, patch.duration)
178
+ }
179
+ announce(t.title ? `${t.title}: ${t.message}` : t.message, {
180
+ assertive: t.variant === 'error' || t.variant === 'warning',
181
+ })
182
+ }
183
+
184
+ function promise<T>(p: Promise<T>, messages: PromiseMessages<T>, options: ToastOptions = {}): Promise<T> {
185
+ const id = push(messages.loading, {
186
+ ...options,
187
+ variant: 'info',
188
+ duration: 0,
189
+ dismissible: options.dismissible ?? false,
190
+ })
191
+ p.then(
192
+ (value) => update(id, {
193
+ message: typeof messages.success === 'function' ? messages.success(value) : messages.success,
194
+ variant: 'success',
195
+ duration: options.duration ?? 4000,
196
+ dismissible: true,
197
+ }),
198
+ (error) => update(id, {
199
+ message: typeof messages.error === 'function' ? messages.error(error) : messages.error,
200
+ variant: 'error',
201
+ duration: options.duration ?? 6000,
202
+ dismissible: true,
203
+ }),
204
+ )
205
+ return p
206
+ }
207
+
117
208
  const toast = push as ToastFn
118
209
  toast.info = (m, o = {}) => push(m, { ...o, variant: 'info' })
119
210
  toast.success = (m, o = {}) => push(m, { ...o, variant: 'success' })
120
211
  toast.warning = (m, o = {}) => push(m, { ...o, variant: 'warning' })
121
212
  toast.error = (m, o = {}) => push(m, { ...o, variant: 'error' })
213
+ toast.promise = promise
214
+ toast.update = update
215
+ toast.custom = (render, o = {}) => push('', { ...o, render })
216
+ toast.dismiss = dismiss
122
217
 
123
218
  return {
124
219
  get toasts() {
125
220
  return toasts
126
221
  },
127
222
  toast,
223
+ update,
128
224
  dismiss,
129
225
  clear,
130
226
  pause,
@@ -140,6 +236,8 @@ export const toastStore = { get toasts() { return store.toasts } }
140
236
  export const toast: ToastFn = store.toast
141
237
  /** Dismiss a toast by id. */
142
238
  export const dismissToast = (id: number): void => store.dismiss(id)
239
+ /** Patch an existing toast in place (message/variant/title/action/duration/render). */
240
+ export const updateToast = (id: number, patch: UpdateToast): void => store.update(id, patch)
143
241
  /** Remove every toast (and cancel their timers). */
144
242
  export const clearToasts = (): void => store.clear()
145
243
  /** Pause a toast's auto-dismiss countdown (e.g. while hovered). */