@svgrid/grid 2.2.27 → 2.2.29
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 +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +39 -7
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* toast-store - the headless model behind <SvToaster>: a singleton reactive
|
|
3
|
-
* queue of transient notifications with auto-dismiss timers, plus the `toast()`
|
|
4
|
-
* API you call from anywhere. Every toast is also pushed to the shared ARIA
|
|
5
|
-
* live region via `announce()`, so screen-reader users hear it even though the
|
|
6
|
-
* visual toast is off to the side.
|
|
7
|
-
*
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { toast } from '@svgrid/grid'
|
|
10
|
-
* toast.success('Saved')
|
|
11
|
-
* toast.error('Could not save', { duration: 0 }) // sticky
|
|
12
|
-
* ```
|
|
13
|
-
*
|
|
14
|
-
* Mount a single <SvToaster /> once near the app root to render the queue.
|
|
15
|
-
*/
|
|
16
|
-
import type { Snippet } from 'svelte'
|
|
17
|
-
import { announce } from './a11y/live-region'
|
|
18
|
-
|
|
19
|
-
export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
|
|
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
|
-
|
|
30
|
-
export type ToastOptions = {
|
|
31
|
-
variant?: ToastVariant
|
|
32
|
-
/** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
|
|
33
|
-
duration?: number
|
|
34
|
-
/** Show the dismiss (x) button. Default true. */
|
|
35
|
-
dismissible?: boolean
|
|
36
|
-
/** Optional bold title above the message. */
|
|
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]>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type Toast = {
|
|
47
|
-
id: number
|
|
48
|
-
message: string
|
|
49
|
-
variant: ToastVariant
|
|
50
|
-
duration: number
|
|
51
|
-
dismissible: boolean
|
|
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)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type VariantOptions = Omit<ToastOptions, 'variant'>
|
|
69
|
-
|
|
70
|
-
export type ToastFn = {
|
|
71
|
-
(message: string, options?: ToastOptions): number
|
|
72
|
-
info: (message: string, options?: VariantOptions) => number
|
|
73
|
-
success: (message: string, options?: VariantOptions) => number
|
|
74
|
-
warning: (message: string, options?: VariantOptions) => number
|
|
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
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function createToastStore() {
|
|
90
|
-
const toasts = $state<Toast[]>([])
|
|
91
|
-
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
92
|
-
// Remaining ms + when the current run started, so hovering can pause/resume
|
|
93
|
-
// the auto-dismiss countdown instead of losing it.
|
|
94
|
-
const meta = new Map<number, { remaining: number; startedAt: number }>()
|
|
95
|
-
let seq = 0
|
|
96
|
-
|
|
97
|
-
function schedule(id: number, ms: number) {
|
|
98
|
-
meta.set(id, { remaining: ms, startedAt: Date.now() })
|
|
99
|
-
timers.set(id, setTimeout(() => dismiss(id), ms))
|
|
100
|
-
}
|
|
101
|
-
/** Pause a toast's auto-dismiss (e.g. on hover), banking the time left. */
|
|
102
|
-
function pause(id: number): void {
|
|
103
|
-
const timer = timers.get(id)
|
|
104
|
-
const m = meta.get(id)
|
|
105
|
-
if (!timer || !m) return
|
|
106
|
-
clearTimeout(timer)
|
|
107
|
-
timers.delete(id)
|
|
108
|
-
m.remaining = Math.max(0, m.remaining - (Date.now() - m.startedAt))
|
|
109
|
-
}
|
|
110
|
-
/** Resume a paused toast with the banked time remaining. */
|
|
111
|
-
function resume(id: number): void {
|
|
112
|
-
const m = meta.get(id)
|
|
113
|
-
if (!m || timers.has(id) || m.remaining <= 0) return
|
|
114
|
-
m.startedAt = Date.now()
|
|
115
|
-
timers.set(id, setTimeout(() => dismiss(id), m.remaining))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function dismiss(id: number): void {
|
|
119
|
-
const i = toasts.findIndex((t) => t.id === id)
|
|
120
|
-
if (i >= 0) toasts.splice(i, 1)
|
|
121
|
-
const timer = timers.get(id)
|
|
122
|
-
if (timer) {
|
|
123
|
-
clearTimeout(timer)
|
|
124
|
-
timers.delete(id)
|
|
125
|
-
}
|
|
126
|
-
meta.delete(id)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function clear(): void {
|
|
130
|
-
for (const timer of timers.values()) clearTimeout(timer)
|
|
131
|
-
timers.clear()
|
|
132
|
-
meta.clear()
|
|
133
|
-
toasts.splice(0, toasts.length)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function push(message: string, options: ToastOptions = {}): number {
|
|
137
|
-
const id = ++seq
|
|
138
|
-
const variant = options.variant ?? 'info'
|
|
139
|
-
const duration = options.duration ?? 4000
|
|
140
|
-
const entry: Toast = {
|
|
141
|
-
id,
|
|
142
|
-
message,
|
|
143
|
-
variant,
|
|
144
|
-
duration,
|
|
145
|
-
dismissible: options.dismissible ?? true,
|
|
146
|
-
title: options.title,
|
|
147
|
-
action: options.action,
|
|
148
|
-
cancel: options.cancel,
|
|
149
|
-
render: options.render,
|
|
150
|
-
}
|
|
151
|
-
toasts.push(entry)
|
|
152
|
-
// Errors/warnings interrupt (assertive); info/success are polite.
|
|
153
|
-
announce(entry.title ? `${entry.title}: ${message}` : message, {
|
|
154
|
-
assertive: variant === 'error' || variant === 'warning',
|
|
155
|
-
})
|
|
156
|
-
if (duration > 0) schedule(id, duration)
|
|
157
|
-
return id
|
|
158
|
-
}
|
|
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
|
-
|
|
208
|
-
const toast = push as ToastFn
|
|
209
|
-
toast.info = (m, o = {}) => push(m, { ...o, variant: 'info' })
|
|
210
|
-
toast.success = (m, o = {}) => push(m, { ...o, variant: 'success' })
|
|
211
|
-
toast.warning = (m, o = {}) => push(m, { ...o, variant: 'warning' })
|
|
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
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
get toasts() {
|
|
220
|
-
return toasts
|
|
221
|
-
},
|
|
222
|
-
toast,
|
|
223
|
-
update,
|
|
224
|
-
dismiss,
|
|
225
|
-
clear,
|
|
226
|
-
pause,
|
|
227
|
-
resume,
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const store = createToastStore()
|
|
232
|
-
|
|
233
|
-
/** Live reactive list of active toasts (read inside a component/effect). */
|
|
234
|
-
export const toastStore = { get toasts() { return store.toasts } }
|
|
235
|
-
/** Show a toast. Returns its id. Has `.info/.success/.warning/.error` helpers. */
|
|
236
|
-
export const toast: ToastFn = store.toast
|
|
237
|
-
/** Dismiss a toast by id. */
|
|
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)
|
|
241
|
-
/** Remove every toast (and cancel their timers). */
|
|
242
|
-
export const clearToasts = (): void => store.clear()
|
|
243
|
-
/** Pause a toast's auto-dismiss countdown (e.g. while hovered). */
|
|
244
|
-
export const pauseToast = (id: number): void => store.pause(id)
|
|
245
|
-
/** Resume a paused toast's countdown with the time it had left. */
|
|
246
|
-
export const resumeToast = (id: number): void => store.resume(id)
|
|
1
|
+
/**
|
|
2
|
+
* toast-store - the headless model behind <SvToaster>: a singleton reactive
|
|
3
|
+
* queue of transient notifications with auto-dismiss timers, plus the `toast()`
|
|
4
|
+
* API you call from anywhere. Every toast is also pushed to the shared ARIA
|
|
5
|
+
* live region via `announce()`, so screen-reader users hear it even though the
|
|
6
|
+
* visual toast is off to the side.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { toast } from '@svgrid/grid'
|
|
10
|
+
* toast.success('Saved')
|
|
11
|
+
* toast.error('Could not save', { duration: 0 }) // sticky
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Mount a single <SvToaster /> once near the app root to render the queue.
|
|
15
|
+
*/
|
|
16
|
+
import type { Snippet } from 'svelte'
|
|
17
|
+
import { announce } from './a11y/live-region'
|
|
18
|
+
|
|
19
|
+
export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
|
|
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
|
+
|
|
30
|
+
export type ToastOptions = {
|
|
31
|
+
variant?: ToastVariant
|
|
32
|
+
/** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
|
|
33
|
+
duration?: number
|
|
34
|
+
/** Show the dismiss (x) button. Default true. */
|
|
35
|
+
dismissible?: boolean
|
|
36
|
+
/** Optional bold title above the message. */
|
|
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]>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Toast = {
|
|
47
|
+
id: number
|
|
48
|
+
message: string
|
|
49
|
+
variant: ToastVariant
|
|
50
|
+
duration: number
|
|
51
|
+
dismissible: boolean
|
|
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)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type VariantOptions = Omit<ToastOptions, 'variant'>
|
|
69
|
+
|
|
70
|
+
export type ToastFn = {
|
|
71
|
+
(message: string, options?: ToastOptions): number
|
|
72
|
+
info: (message: string, options?: VariantOptions) => number
|
|
73
|
+
success: (message: string, options?: VariantOptions) => number
|
|
74
|
+
warning: (message: string, options?: VariantOptions) => number
|
|
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
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createToastStore() {
|
|
90
|
+
const toasts = $state<Toast[]>([])
|
|
91
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
92
|
+
// Remaining ms + when the current run started, so hovering can pause/resume
|
|
93
|
+
// the auto-dismiss countdown instead of losing it.
|
|
94
|
+
const meta = new Map<number, { remaining: number; startedAt: number }>()
|
|
95
|
+
let seq = 0
|
|
96
|
+
|
|
97
|
+
function schedule(id: number, ms: number) {
|
|
98
|
+
meta.set(id, { remaining: ms, startedAt: Date.now() })
|
|
99
|
+
timers.set(id, setTimeout(() => dismiss(id), ms))
|
|
100
|
+
}
|
|
101
|
+
/** Pause a toast's auto-dismiss (e.g. on hover), banking the time left. */
|
|
102
|
+
function pause(id: number): void {
|
|
103
|
+
const timer = timers.get(id)
|
|
104
|
+
const m = meta.get(id)
|
|
105
|
+
if (!timer || !m) return
|
|
106
|
+
clearTimeout(timer)
|
|
107
|
+
timers.delete(id)
|
|
108
|
+
m.remaining = Math.max(0, m.remaining - (Date.now() - m.startedAt))
|
|
109
|
+
}
|
|
110
|
+
/** Resume a paused toast with the banked time remaining. */
|
|
111
|
+
function resume(id: number): void {
|
|
112
|
+
const m = meta.get(id)
|
|
113
|
+
if (!m || timers.has(id) || m.remaining <= 0) return
|
|
114
|
+
m.startedAt = Date.now()
|
|
115
|
+
timers.set(id, setTimeout(() => dismiss(id), m.remaining))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dismiss(id: number): void {
|
|
119
|
+
const i = toasts.findIndex((t) => t.id === id)
|
|
120
|
+
if (i >= 0) toasts.splice(i, 1)
|
|
121
|
+
const timer = timers.get(id)
|
|
122
|
+
if (timer) {
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
timers.delete(id)
|
|
125
|
+
}
|
|
126
|
+
meta.delete(id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function clear(): void {
|
|
130
|
+
for (const timer of timers.values()) clearTimeout(timer)
|
|
131
|
+
timers.clear()
|
|
132
|
+
meta.clear()
|
|
133
|
+
toasts.splice(0, toasts.length)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function push(message: string, options: ToastOptions = {}): number {
|
|
137
|
+
const id = ++seq
|
|
138
|
+
const variant = options.variant ?? 'info'
|
|
139
|
+
const duration = options.duration ?? 4000
|
|
140
|
+
const entry: Toast = {
|
|
141
|
+
id,
|
|
142
|
+
message,
|
|
143
|
+
variant,
|
|
144
|
+
duration,
|
|
145
|
+
dismissible: options.dismissible ?? true,
|
|
146
|
+
title: options.title,
|
|
147
|
+
action: options.action,
|
|
148
|
+
cancel: options.cancel,
|
|
149
|
+
render: options.render,
|
|
150
|
+
}
|
|
151
|
+
toasts.push(entry)
|
|
152
|
+
// Errors/warnings interrupt (assertive); info/success are polite.
|
|
153
|
+
announce(entry.title ? `${entry.title}: ${message}` : message, {
|
|
154
|
+
assertive: variant === 'error' || variant === 'warning',
|
|
155
|
+
})
|
|
156
|
+
if (duration > 0) schedule(id, duration)
|
|
157
|
+
return id
|
|
158
|
+
}
|
|
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
|
+
|
|
208
|
+
const toast = push as ToastFn
|
|
209
|
+
toast.info = (m, o = {}) => push(m, { ...o, variant: 'info' })
|
|
210
|
+
toast.success = (m, o = {}) => push(m, { ...o, variant: 'success' })
|
|
211
|
+
toast.warning = (m, o = {}) => push(m, { ...o, variant: 'warning' })
|
|
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
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
get toasts() {
|
|
220
|
+
return toasts
|
|
221
|
+
},
|
|
222
|
+
toast,
|
|
223
|
+
update,
|
|
224
|
+
dismiss,
|
|
225
|
+
clear,
|
|
226
|
+
pause,
|
|
227
|
+
resume,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const store = createToastStore()
|
|
232
|
+
|
|
233
|
+
/** Live reactive list of active toasts (read inside a component/effect). */
|
|
234
|
+
export const toastStore = { get toasts() { return store.toasts } }
|
|
235
|
+
/** Show a toast. Returns its id. Has `.info/.success/.warning/.error` helpers. */
|
|
236
|
+
export const toast: ToastFn = store.toast
|
|
237
|
+
/** Dismiss a toast by id. */
|
|
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)
|
|
241
|
+
/** Remove every toast (and cancel their timers). */
|
|
242
|
+
export const clearToasts = (): void => store.clear()
|
|
243
|
+
/** Pause a toast's auto-dismiss countdown (e.g. while hovered). */
|
|
244
|
+
export const pauseToast = (id: number): void => store.pause(id)
|
|
245
|
+
/** Resume a paused toast's countdown with the time it had left. */
|
|
246
|
+
export const resumeToast = (id: number): void => store.resume(id)
|