@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.
- package/README.md +1 -1
- package/dist/GridFooter.svelte +12 -11
- package/dist/SvAnchor.svelte +65 -0
- package/dist/SvAnchor.svelte.d.ts +24 -0
- package/dist/SvAspectRatio.svelte +38 -0
- package/dist/SvAspectRatio.svelte.d.ts +16 -0
- package/dist/SvAutoComplete.svelte +52 -7
- package/dist/SvAutoComplete.svelte.d.ts +4 -1
- package/dist/SvBlockquote.svelte +43 -0
- package/dist/SvBlockquote.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +33 -2
- package/dist/SvCalendar.svelte.d.ts +8 -1
- package/dist/SvCheckBox.svelte +6 -2
- package/dist/SvCode.svelte +52 -0
- package/dist/SvCode.svelte.d.ts +19 -0
- package/dist/SvCollapsible.svelte +75 -0
- package/dist/SvCollapsible.svelte.d.ts +27 -0
- package/dist/SvColorInput.svelte +11 -17
- package/dist/SvColorInput.svelte.d.ts +2 -0
- package/dist/SvComboBox.svelte +85 -8
- package/dist/SvComboBox.svelte.d.ts +14 -0
- package/dist/SvCountryInput.svelte +2 -2
- package/dist/SvCountryInput.svelte.d.ts +1 -1
- package/dist/SvDropDownList.svelte +154 -51
- package/dist/SvDropDownList.svelte.d.ts +17 -3
- package/dist/SvDurationInput.svelte +59 -41
- package/dist/SvDurationInput.svelte.d.ts +18 -0
- package/dist/SvField.svelte +203 -12
- package/dist/SvField.svelte.d.ts +45 -8
- package/dist/SvForm.svelte +195 -21
- package/dist/SvForm.svelte.d.ts +23 -0
- package/dist/SvGrid.controller.svelte.d.ts +12 -1
- package/dist/SvGrid.controller.svelte.js +87 -7
- package/dist/SvGrid.svelte +383 -181
- package/dist/SvGrid.types.d.ts +123 -2
- package/dist/SvGridChartView.svelte +70 -0
- package/dist/SvGridChartView.svelte.d.ts +11 -0
- package/dist/SvGridDropdown.svelte +9 -2
- package/dist/SvGridSelect.svelte +5 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroup.svelte +41 -0
- package/dist/SvGroup.svelte.d.ts +27 -0
- package/dist/SvHoverCard.svelte +70 -0
- package/dist/SvHoverCard.svelte.d.ts +41 -0
- package/dist/SvKbd.svelte +51 -0
- package/dist/SvKbd.svelte.d.ts +18 -0
- package/dist/SvList.svelte +55 -0
- package/dist/SvList.svelte.d.ts +23 -0
- package/dist/SvListBox.svelte +166 -77
- package/dist/SvListBox.svelte.d.ts +17 -4
- package/dist/SvLoadingOverlay.svelte +53 -0
- package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
- package/dist/SvMark.svelte +36 -0
- package/dist/SvMark.svelte.d.ts +14 -0
- package/dist/SvMaskedInput.svelte +50 -35
- package/dist/SvMaskedInput.svelte.d.ts +17 -2
- package/dist/SvMenuList.svelte +32 -3
- package/dist/SvMenubar.svelte +200 -0
- package/dist/SvMenubar.svelte.d.ts +18 -0
- package/dist/SvMultiSelect.svelte +7 -4
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +79 -42
- package/dist/SvNumberInput.svelte.d.ts +25 -0
- package/dist/SvOtpInput.svelte +2 -2
- package/dist/SvPasswordInput.svelte +51 -31
- package/dist/SvPasswordInput.svelte.d.ts +17 -0
- package/dist/SvPhoneInput.svelte +39 -28
- package/dist/SvPhoneInput.svelte.d.ts +4 -0
- package/dist/SvPopconfirm.svelte +79 -0
- package/dist/SvPopconfirm.svelte.d.ts +42 -0
- package/dist/SvPopover.svelte +87 -29
- package/dist/SvPopover.svelte.d.ts +18 -4
- package/dist/SvRadioGroup.svelte +5 -1
- package/dist/SvResult.svelte +73 -0
- package/dist/SvResult.svelte.d.ts +28 -0
- package/dist/SvSegmented.svelte +114 -0
- package/dist/SvSegmented.svelte.d.ts +29 -0
- package/dist/SvSimpleGrid.svelte +41 -0
- package/dist/SvSimpleGrid.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +23 -10
- package/dist/SvSpinner.svelte +43 -0
- package/dist/SvSpinner.svelte.d.ts +20 -0
- package/dist/SvStack.svelte +37 -0
- package/dist/SvStack.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +6 -1
- package/dist/SvTagsInput.svelte +9 -5
- package/dist/SvText.svelte +76 -0
- package/dist/SvText.svelte.d.ts +31 -0
- package/dist/SvTextInput.svelte +75 -43
- package/dist/SvTextInput.svelte.d.ts +28 -1
- package/dist/SvTimePicker.svelte +32 -2
- package/dist/SvTimePicker.svelte.d.ts +8 -9
- package/dist/SvTitle.svelte +57 -0
- package/dist/SvTitle.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +41 -6
- package/dist/SvToggleButton.svelte +4 -1
- package/dist/SvTooltip.svelte +68 -34
- package/dist/SvTooltip.svelte.d.ts +14 -2
- package/dist/SvTree.svelte +81 -21
- package/dist/SvTree.svelte.d.ts +3 -2
- package/dist/SvTreeSelect.svelte +5 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/SvVisuallyHidden.svelte +49 -0
- package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
- package/dist/ai.d.ts +289 -0
- package/dist/ai.js +1013 -0
- package/dist/board-view.svelte.d.ts +22 -0
- package/dist/board-view.svelte.js +13 -0
- package/dist/build-api.js +36 -6
- package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
- package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
- package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
- package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
- package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
- package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
- package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
- package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
- package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
- package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
- package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
- package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
- package/dist/cdn/chart-T1usDN0o.js +1212 -0
- package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
- package/dist/cdn/export-format-JT5pWVT8.js +561 -0
- package/dist/cdn/popover-P-9P2YYn.js +69 -0
- package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/cdn/src-BLhdLmoN.js +25707 -0
- package/dist/cdn/src-BwnUVj3T.js +25623 -0
- package/dist/cdn/svgrid.js +9 -32897
- package/dist/cdn/svgrid.svelte-external.js +9 -30052
- package/dist/cell-formatting.d.ts +2 -2
- package/dist/cell-render.js +4 -4
- package/dist/chart-view.svelte.d.ts +25 -0
- package/dist/chart-view.svelte.js +14 -0
- package/dist/createCombobox.svelte.d.ts +5 -0
- package/dist/createCombobox.svelte.js +21 -5
- package/dist/createDropdownList.svelte.d.ts +3 -0
- package/dist/createDropdownList.svelte.js +8 -2
- package/dist/createForm.svelte.d.ts +18 -0
- package/dist/createForm.svelte.js +51 -6
- package/dist/createMenu.svelte.js +27 -0
- package/dist/createPopoverSelect.svelte.d.ts +3 -0
- package/dist/createPopoverSelect.svelte.js +9 -1
- package/dist/createSlider.svelte.d.ts +8 -21
- package/dist/createSlider.svelte.js +34 -0
- package/dist/createTooltip.svelte.d.ts +12 -0
- package/dist/createTooltip.svelte.js +72 -9
- package/dist/editor-contract.d.ts +15 -0
- package/dist/export-provider.d.ts +27 -0
- package/dist/export-provider.js +9 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +33 -0
- package/dist/form-field.d.ts +45 -2
- package/dist/grid-messages.d.ts +61 -0
- package/dist/grid-messages.js +72 -0
- package/dist/group-display.d.ts +62 -0
- package/dist/group-display.js +78 -0
- package/dist/index.d.ts +36 -4
- package/dist/index.js +43 -2
- package/dist/js-scroller.svelte.d.ts +35 -0
- package/dist/js-scroller.svelte.js +154 -0
- package/dist/list-option.d.ts +46 -0
- package/dist/list-option.js +53 -0
- package/dist/menus.js +25 -11
- package/dist/pivot-view.svelte.d.ts +72 -0
- package/dist/pivot-view.svelte.js +13 -0
- package/dist/positioning.d.ts +111 -0
- package/dist/positioning.js +191 -0
- package/dist/scheduler-model.d.ts +3 -0
- package/dist/scheduler-model.js +22 -5
- package/dist/svgrid-wrapper.types.d.ts +18 -0
- package/dist/toast-store.svelte.d.ts +54 -0
- package/dist/toast-store.svelte.js +65 -15
- package/package.json +9 -1
- package/src/GridFooter.svelte +12 -11
- package/src/SvAnchor.svelte +65 -0
- package/src/SvAspectRatio.svelte +38 -0
- package/src/SvAutoComplete.svelte +52 -7
- package/src/SvBlockquote.svelte +43 -0
- package/src/SvCalendar.svelte +33 -2
- package/src/SvCalendar.test.ts +15 -0
- package/src/SvCheckBox.svelte +6 -2
- package/src/SvCode.svelte +52 -0
- package/src/SvCollapsible.svelte +75 -0
- package/src/SvColorInput.svelte +11 -17
- package/src/SvComboBox.svelte +85 -8
- package/src/SvCountryInput.svelte +2 -2
- package/src/SvDropDownList.svelte +154 -51
- package/src/SvDurationInput.svelte +59 -41
- package/src/SvField.svelte +203 -12
- package/src/SvForm.svelte +195 -21
- package/src/SvForm.test.ts +112 -0
- package/src/SvGrid.controller.svelte.ts +88 -7
- package/src/SvGrid.svelte +383 -181
- package/src/SvGrid.types.ts +125 -1
- package/src/SvGridChartView.svelte +70 -0
- package/src/SvGridDropdown.svelte +9 -2
- package/src/SvGridSelect.svelte +5 -2
- package/src/SvGroup.svelte +41 -0
- package/src/SvHoverCard.svelte +70 -0
- package/src/SvKbd.svelte +51 -0
- package/src/SvList.svelte +55 -0
- package/src/SvListBox.svelte +166 -77
- package/src/SvLoadingOverlay.svelte +53 -0
- package/src/SvMark.svelte +36 -0
- package/src/SvMaskedInput.svelte +50 -35
- package/src/SvMenuList.svelte +32 -3
- package/src/SvMenubar.svelte +200 -0
- package/src/SvMultiSelect.svelte +7 -4
- package/src/SvNumberInput.svelte +79 -42
- package/src/SvOtpInput.svelte +2 -2
- package/src/SvPasswordInput.svelte +51 -31
- package/src/SvPhoneInput.svelte +39 -28
- package/src/SvPopconfirm.svelte +79 -0
- package/src/SvPopover.svelte +87 -29
- package/src/SvRadioGroup.svelte +5 -1
- package/src/SvResult.svelte +73 -0
- package/src/SvSegmented.svelte +114 -0
- package/src/SvSimpleGrid.svelte +41 -0
- package/src/SvSlider.svelte +23 -10
- package/src/SvSpinner.svelte +43 -0
- package/src/SvStack.svelte +37 -0
- package/src/SvSwitchButton.svelte +6 -1
- package/src/SvTagsInput.svelte +9 -5
- package/src/SvText.svelte +76 -0
- package/src/SvTextInput.svelte +75 -43
- package/src/SvTimePicker.svelte +32 -2
- package/src/SvTitle.svelte +57 -0
- package/src/SvToaster.svelte +41 -6
- package/src/SvToaster.test.ts +32 -0
- package/src/SvToggleButton.svelte +4 -1
- package/src/SvTooltip.svelte +68 -34
- package/src/SvTree.svelte +81 -21
- package/src/SvTreeSelect.svelte +5 -2
- package/src/SvVisuallyHidden.svelte +49 -0
- package/src/ai.test.ts +502 -0
- package/src/ai.ts +1391 -0
- package/src/board-view.svelte.ts +33 -0
- package/src/build-api.coverage.test.ts +101 -0
- package/src/build-api.ts +43 -18
- package/src/builtin-editors.grid.test.ts +1 -1
- package/src/cell-formatting.ts +5 -5
- package/src/cell-render.ts +4 -4
- package/src/chart-view.svelte.ts +36 -0
- package/src/createCombobox.svelte.ts +17 -5
- package/src/createDropdownList.svelte.ts +8 -2
- package/src/createForm.svelte.ts +59 -6
- package/src/createMenu.svelte.ts +23 -0
- package/src/createPopoverSelect.svelte.ts +8 -1
- package/src/createSlider.svelte.ts +20 -0
- package/src/createTooltip.svelte.ts +67 -9
- package/src/editor-contract.ts +16 -0
- package/src/export-provider.ts +40 -0
- package/src/filter-operators.ts +37 -0
- package/src/form-field.ts +42 -2
- package/src/grid-messages.test.ts +24 -0
- package/src/grid-messages.ts +137 -0
- package/src/group-display.test.ts +77 -0
- package/src/group-display.ts +117 -0
- package/src/i18n.grid.test.ts +73 -0
- package/src/index.ts +91 -2
- package/src/js-scroller.svelte.ts +173 -0
- package/src/list-option.test.ts +56 -0
- package/src/list-option.ts +81 -0
- package/src/menus.test.ts +32 -0
- package/src/menus.ts +25 -12
- package/src/pivot-view.svelte.ts +90 -0
- package/src/pivot-view.test.ts +38 -0
- package/src/pivot.grid.test.ts +102 -0
- package/src/positioning.test.ts +141 -0
- package/src/positioning.ts +266 -0
- package/src/scheduler-model.test.ts +16 -0
- package/src/scheduler-model.ts +24 -5
- package/src/svgrid-wrapper.types.ts +23 -0
- package/src/svgrid.charting.test.ts +8 -1
- package/src/svgrid.comments-autocomplete.test.ts +7 -2
- package/src/svgrid.context-menu.test.ts +8 -3
- package/src/toast-store.svelte.ts +98 -0
- package/src/toast-store.test.ts +62 -1
- package/src/ui-breadth.test.ts +138 -0
- package/src/ui-buttons.test.ts +39 -0
- package/src/ui-inputs.test.ts +2 -2
- package/src/ui-range.test.ts +9 -0
- package/src/ui-selection.test.ts +36 -0
- package/src/ui-tier1.test.ts +1 -1
- package/src/virtual.test.ts +20 -6
- package/dist/SvGridBoard.svelte +0 -2339
- package/dist/SvGridBoard.svelte.d.ts +0 -31
- package/src/SvGridBoard.svelte +0 -2339
- 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' },
|
package/src/scheduler-model.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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). */
|