@viniaraujo68/plinth 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -19
- package/dist/components/ErrorDisplay.svelte +30 -16
- package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
- package/dist/components/JsonView.svelte +43 -0
- package/dist/components/JsonView.svelte.d.ts +4 -0
- package/dist/components/JsonViewFrame.svelte +654 -0
- package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
- package/dist/components/Paginator.svelte +34 -8
- package/dist/components/Paginator.svelte.d.ts +10 -0
- package/dist/components/RequestErrorDisplay.svelte +43 -0
- package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +204 -0
- package/dist/components/Tabs.svelte.d.ts +76 -0
- package/dist/components/VirtualJsonView.svelte +67 -0
- package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +11 -5
- package/dist/components/json-view.d.ts +70 -0
- package/dist/components/json-view.js +302 -0
- package/dist/components/request-error.d.ts +8 -0
- package/dist/components/request-error.js +55 -0
- package/dist/components/tabs.d.ts +5 -0
- package/dist/components/tabs.js +1 -0
- package/dist/http.d.ts +10 -5
- package/dist/http.js +34 -6
- package/dist/i18n/LocaleToggle.svelte +45 -0
- package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
- package/dist/i18n/i18n.svelte.d.ts +56 -0
- package/dist/i18n/i18n.svelte.js +94 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +7 -0
- package/dist/i18n/translate.d.ts +30 -0
- package/dist/i18n/translate.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/redirect.d.ts +40 -0
- package/dist/redirect.js +59 -0
- package/dist/routing.svelte.d.ts +1 -0
- package/dist/routing.svelte.js +3 -0
- package/dist/shell/AppShell.svelte +269 -95
- package/dist/shell/AppShell.svelte.d.ts +3 -2
- package/dist/shell/NavigationProgress.svelte +85 -0
- package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
- package/dist/shell/TopBar.svelte +191 -0
- package/dist/shell/TopBar.svelte.d.ts +41 -0
- package/dist/shell/UserAvatar.svelte +50 -0
- package/dist/shell/UserAvatar.svelte.d.ts +14 -0
- package/dist/shell/index.d.ts +2 -0
- package/dist/shell/index.js +7 -3
- package/dist/shell/initials.d.ts +6 -0
- package/dist/shell/initials.js +13 -0
- package/dist/table/DataTable.svelte +199 -9
- package/dist/table/DataTable.svelte.d.ts +28 -5
- package/dist/table/FilterBar.svelte +417 -0
- package/dist/table/FilterBar.svelte.d.ts +34 -0
- package/dist/table/FilterRuleEditor.svelte +290 -0
- package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
- package/dist/table/filter-labels.d.ts +23 -0
- package/dist/table/filter-labels.js +30 -0
- package/dist/table/filter.d.ts +27 -0
- package/dist/table/filter.js +279 -0
- package/dist/table/index.d.ts +4 -1
- package/dist/table/index.js +5 -2
- package/dist/table/types.d.ts +49 -0
- package/dist/toast/Toaster.svelte +7 -3
- package/dist/toast/index.d.ts +1 -1
- package/dist/toast/toast-manager.svelte.d.ts +29 -1
- package/dist/toast/toast-manager.svelte.js +61 -1
- package/dist/url-state/codec.d.ts +15 -0
- package/dist/url-state/codec.js +1 -0
- package/dist/url-state/index.d.ts +5 -0
- package/dist/url-state/index.js +4 -0
- package/dist/url-state/params-codec.d.ts +73 -0
- package/dist/url-state/params-codec.js +116 -0
- package/dist/url-state/search-params.d.ts +4 -0
- package/dist/url-state/search-params.js +28 -0
- package/dist/url-state/synced-state.svelte.d.ts +20 -0
- package/dist/url-state/synced-state.svelte.js +88 -0
- package/dist/url-state/url-store.svelte.d.ts +12 -0
- package/dist/url-state/url-store.svelte.js +56 -0
- package/dist/virtual/DataList.svelte +215 -0
- package/dist/virtual/DataList.svelte.d.ts +52 -0
- package/dist/virtual/VirtualList.svelte +150 -0
- package/dist/virtual/VirtualList.svelte.d.ts +42 -0
- package/dist/virtual/index.d.ts +7 -0
- package/dist/virtual/index.js +6 -0
- package/dist/virtual/keyed-cache.d.ts +7 -0
- package/dist/virtual/keyed-cache.js +20 -0
- package/dist/virtual/labels.d.ts +12 -0
- package/dist/virtual/labels.js +6 -0
- package/dist/virtual/paging.d.ts +5 -0
- package/dist/virtual/paging.js +11 -0
- package/dist/virtual/range.d.ts +9 -0
- package/dist/virtual/range.js +31 -0
- package/dist/virtual/size-tree.d.ts +10 -0
- package/dist/virtual/size-tree.js +56 -0
- package/dist/virtual/virtualizer.svelte.d.ts +38 -0
- package/dist/virtual/virtualizer.svelte.js +295 -0
- package/dist/virtual/windowed-data.svelte.d.ts +38 -0
- package/dist/virtual/windowed-data.svelte.js +139 -0
- package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
- package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
- package/package.json +13 -1
package/dist/table/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { default as DataTable } from "./DataTable.svelte";
|
|
2
|
-
export
|
|
2
|
+
export { default as FilterBar } from "./FilterBar.svelte";
|
|
3
|
+
export { DEFAULT_FILTER_LABELS, type FilterLabels } from "./filter-labels.js";
|
|
4
|
+
export type { ChoiceFilter, ChoiceOperator, Column, ColumnFilter, ColumnState, FilterCombinator, FilterGroup, FilterNode, FilterOperator, FilterOption, FilterRule, NumberFilter, NumberOperator, SortDirection, SortState, SortValue, TextFilter, TextOperator, } from "./types.js";
|
|
5
|
+
export { addNode, emptyFilter, filterableColumns, filterFromParams, filterRows, filterRules, filterToParams, filterValue, findRule, isFilterEmpty, isGroup, isRuleActive, nodeAt, normalizeFilter, NUMBER_OPERATORS, operatorsOf, removeNode, removeRule, replaceNode, setCombinator, setRule, TEXT_OPERATORS, type FilterableColumn, type NodePath, } from "./filter.js";
|
|
3
6
|
export { ariaSort, cellText, cellValue, compareSortValues, competitionRanks, nextSort, sortFromParams, sortRows, sortToParams, sortValue, type SortParamNames, } from "./sort.js";
|
|
4
7
|
export { canHideColumn, columnWidth, emptyColumnState, isColumnStateEmpty, isColumnVisible, moveColumn, orderColumns, parseColumnState, readColumnState, setColumnVisible, setColumnWidth, visibleColumns, writeColumnState, } from "./columns.js";
|
package/dist/table/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// A sortable table that turns into cards on a narrow screen, whose columns the reader can show,
|
|
2
|
-
// hide, reorder and
|
|
3
|
-
// virtualization, no saved views. `@viniaraujo68/plinth/table`.
|
|
2
|
+
// hide, reorder, resize and filter, optionally paged, and the pure functions behind it. Not a data
|
|
3
|
+
// grid: no virtualization, no saved views. `@viniaraujo68/plinth/table`.
|
|
4
4
|
export { default as DataTable } from "./DataTable.svelte";
|
|
5
|
+
export { default as FilterBar } from "./FilterBar.svelte";
|
|
6
|
+
export { DEFAULT_FILTER_LABELS } from "./filter-labels.js";
|
|
7
|
+
export { addNode, emptyFilter, filterableColumns, filterFromParams, filterRows, filterRules, filterToParams, filterValue, findRule, isFilterEmpty, isGroup, isRuleActive, nodeAt, normalizeFilter, NUMBER_OPERATORS, operatorsOf, removeNode, removeRule, replaceNode, setCombinator, setRule, TEXT_OPERATORS, } from "./filter.js";
|
|
5
8
|
export { ariaSort, cellText, cellValue, compareSortValues, competitionRanks, nextSort, sortFromParams, sortRows, sortToParams, sortValue, } from "./sort.js";
|
|
6
9
|
// The layout functions the table runs on, for a caller that arranges columns outside the menu —
|
|
7
10
|
// a preset layout, a "show everything" shortcut, or a state kept somewhere other than storage.
|
package/dist/table/types.d.ts
CHANGED
|
@@ -73,7 +73,55 @@ export interface Column<T> {
|
|
|
73
73
|
* `$derived` and the distinction stops mattering.
|
|
74
74
|
*/
|
|
75
75
|
cell?: Snippet<[T, number]>;
|
|
76
|
+
filter?: ColumnFilter<T>;
|
|
76
77
|
}
|
|
78
|
+
export type TextOperator = "contains" | "eq";
|
|
79
|
+
export type NumberOperator = "gte" | "gt" | "lte" | "lt" | "eq";
|
|
80
|
+
export type ChoiceOperator = "in";
|
|
81
|
+
export type FilterOperator = TextOperator | NumberOperator | ChoiceOperator;
|
|
82
|
+
export interface FilterOption {
|
|
83
|
+
value: string;
|
|
84
|
+
label: string;
|
|
85
|
+
}
|
|
86
|
+
interface FilterBase<T> {
|
|
87
|
+
value?: (row: T) => unknown;
|
|
88
|
+
}
|
|
89
|
+
export interface TextFilter<T> extends FilterBase<T> {
|
|
90
|
+
kind: "text";
|
|
91
|
+
operators?: readonly TextOperator[];
|
|
92
|
+
}
|
|
93
|
+
export interface NumberFilter<T> extends FilterBase<T> {
|
|
94
|
+
kind: "number";
|
|
95
|
+
operators?: readonly NumberOperator[];
|
|
96
|
+
min?: number;
|
|
97
|
+
max?: number;
|
|
98
|
+
step?: number;
|
|
99
|
+
format?: (value: number) => string;
|
|
100
|
+
}
|
|
101
|
+
export interface ChoiceFilter<T> extends FilterBase<T> {
|
|
102
|
+
kind: "choice";
|
|
103
|
+
options: readonly FilterOption[];
|
|
104
|
+
}
|
|
105
|
+
export type ColumnFilter<T> = TextFilter<T> | NumberFilter<T> | ChoiceFilter<T>;
|
|
106
|
+
export type FilterRule = {
|
|
107
|
+
key: string;
|
|
108
|
+
operator: TextOperator;
|
|
109
|
+
value: string;
|
|
110
|
+
} | {
|
|
111
|
+
key: string;
|
|
112
|
+
operator: NumberOperator;
|
|
113
|
+
value: number;
|
|
114
|
+
} | {
|
|
115
|
+
key: string;
|
|
116
|
+
operator: ChoiceOperator;
|
|
117
|
+
value: string[];
|
|
118
|
+
};
|
|
119
|
+
export type FilterCombinator = "and" | "or";
|
|
120
|
+
export interface FilterGroup {
|
|
121
|
+
combinator: FilterCombinator;
|
|
122
|
+
children: FilterNode[];
|
|
123
|
+
}
|
|
124
|
+
export type FilterNode = FilterRule | FilterGroup;
|
|
77
125
|
/**
|
|
78
126
|
* The part of a table the reader arranges: which columns show, in what order, and how wide the
|
|
79
127
|
* ones they dragged are. Everything is an override of what the columns declare, so a table whose
|
|
@@ -91,3 +139,4 @@ export interface ColumnState {
|
|
|
91
139
|
/** Widths the reader dragged, in pixels. A column with no entry sizes to its content. */
|
|
92
140
|
widths: Record<string, number>;
|
|
93
141
|
}
|
|
142
|
+
export {};
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
success: "alert-success",
|
|
53
53
|
warning: "alert-warning",
|
|
54
54
|
error: "alert-error",
|
|
55
|
+
loading: "",
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
// Inline paths rather than an icon font or an iconify plugin: the library must render correctly
|
|
58
59
|
// in a consumer that installed nothing but the package. Variant is never signalled by color
|
|
59
|
-
// alone, which is what makes the
|
|
60
|
+
// alone, which is what makes the five alerts distinguishable to a colorblind reader.
|
|
60
61
|
const VARIANT_ICONS: Record<ToastVariant, string[]> = {
|
|
61
62
|
info: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18", "M12 16v-5", "M12 8h.01"],
|
|
62
63
|
success: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18", "m8.5 12.4 2.4 2.4 4.6-5.2"],
|
|
@@ -66,8 +67,11 @@
|
|
|
66
67
|
"M12 17h.01",
|
|
67
68
|
],
|
|
68
69
|
error: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18", "m14.5 9.5-5 5", "m9.5 9.5 5 5"],
|
|
70
|
+
loading: ["M21 12a9 9 0 1 1-6.22-8.56"],
|
|
69
71
|
};
|
|
70
72
|
|
|
73
|
+
const POLITE_VARIANTS: ToastVariant[] = ["info", "success", "loading"];
|
|
74
|
+
|
|
71
75
|
const edgeOf = (value: ToastPosition) => {
|
|
72
76
|
if (value.startsWith("top")) return "top";
|
|
73
77
|
if (value.startsWith("middle")) return "middle";
|
|
@@ -171,7 +175,7 @@ It is `pointer-events: none` for that reason — an empty region must never swal
|
|
|
171
175
|
]}
|
|
172
176
|
data-testid="toast"
|
|
173
177
|
data-variant={toast.variant}
|
|
174
|
-
role={
|
|
178
|
+
role={POLITE_VARIANTS.includes(toast.variant) ? "status" : "alert"}
|
|
175
179
|
aria-atomic="true"
|
|
176
180
|
onmouseenter={() => resolvedManager.pause(toast.id)}
|
|
177
181
|
onmouseleave={(event) => resumeUnlessStillHeld(toast.id, event)}
|
|
@@ -181,7 +185,7 @@ It is `pointer-events: none` for that reason — an empty region must never swal
|
|
|
181
185
|
out:scale|global={{ start: 0.96, duration: 160 }}
|
|
182
186
|
>
|
|
183
187
|
<svg
|
|
184
|
-
class="size-5 shrink-0"
|
|
188
|
+
class={["size-5 shrink-0", toast.variant === "loading" && "animate-spin"]}
|
|
185
189
|
viewBox="0 0 24 24"
|
|
186
190
|
fill="none"
|
|
187
191
|
stroke="currentColor"
|
package/dist/toast/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createToastManager, getDefaultToastManager, toast, ToastManager, type Toast, type ToastApi, type ToastManagerOptions, type ToastOptions, type ToastPosition, type ToastVariant, } from "./toast-manager.svelte.js";
|
|
1
|
+
export { createToastManager, getDefaultToastManager, toast, ToastManager, type Toast, type ToastApi, type ToastManagerOptions, type ToastOptions, type ToastPosition, type ToastPromiseContent, type ToastPromiseMessages, type ToastPromiseOptions, type ToastUpdate, type ToastVariant, } from "./toast-manager.svelte.js";
|
|
2
2
|
export { default as Toaster } from "./Toaster.svelte";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Severity a toast carries. Maps to a daisyUI alert modifier and an icon in `Toaster`. */
|
|
2
|
-
export type ToastVariant = "info" | "success" | "warning" | "error";
|
|
2
|
+
export type ToastVariant = "info" | "success" | "warning" | "error" | "loading";
|
|
3
3
|
/**
|
|
4
4
|
* Where a `Toaster` anchors its stack. Mirrors daisyUI's two-axis toast placement, so the value
|
|
5
5
|
* is a rendering concern only — the manager never sees it.
|
|
@@ -29,6 +29,27 @@ export interface ToastOptions {
|
|
|
29
29
|
description?: string;
|
|
30
30
|
dismissible?: boolean;
|
|
31
31
|
}
|
|
32
|
+
export interface ToastUpdate {
|
|
33
|
+
message?: string;
|
|
34
|
+
variant?: ToastVariant;
|
|
35
|
+
description?: string;
|
|
36
|
+
duration?: number;
|
|
37
|
+
dismissible?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type ToastPromiseContent<T> = string | ((input: T) => string);
|
|
40
|
+
export interface ToastPromiseMessages<T> {
|
|
41
|
+
loading: string;
|
|
42
|
+
success: ToastPromiseContent<T>;
|
|
43
|
+
error: ToastPromiseContent<unknown>;
|
|
44
|
+
}
|
|
45
|
+
type ToastPhaseOptions = Omit<ToastOptions, "id">;
|
|
46
|
+
export interface ToastPromiseOptions {
|
|
47
|
+
id?: string;
|
|
48
|
+
loading?: ToastPhaseOptions;
|
|
49
|
+
success?: ToastPhaseOptions;
|
|
50
|
+
error?: ToastPhaseOptions;
|
|
51
|
+
onerror?: (error: unknown) => void;
|
|
52
|
+
}
|
|
32
53
|
export interface ToastManagerOptions {
|
|
33
54
|
/**
|
|
34
55
|
* How many toasts may be on screen at once; the oldest is dropped to make room. A cap exists
|
|
@@ -57,6 +78,9 @@ export declare class ToastManager {
|
|
|
57
78
|
success: (message: string, options?: ToastOptions) => string;
|
|
58
79
|
warning: (message: string, options?: ToastOptions) => string;
|
|
59
80
|
error: (message: string, options?: ToastOptions) => string;
|
|
81
|
+
loading: (message: string, options?: ToastOptions) => string;
|
|
82
|
+
update: (id: string, patch: ToastUpdate) => boolean;
|
|
83
|
+
promise: <T>(input: Promise<T> | (() => Promise<T>), messages: ToastPromiseMessages<T>, options?: ToastPromiseOptions) => Promise<T | undefined>;
|
|
60
84
|
/** Remove one toast. Unknown ids are ignored, so a caller may dismiss twice. */
|
|
61
85
|
dismiss: (id: string) => void;
|
|
62
86
|
/** Empty the queue. Useful on sign-out or on a navigation that invalidates every message. */
|
|
@@ -84,6 +108,9 @@ export interface ToastApi {
|
|
|
84
108
|
success: (message: string, options?: ToastOptions) => string;
|
|
85
109
|
warning: (message: string, options?: ToastOptions) => string;
|
|
86
110
|
error: (message: string, options?: ToastOptions) => string;
|
|
111
|
+
loading: (message: string, options?: ToastOptions) => string;
|
|
112
|
+
update: (id: string, patch: ToastUpdate) => boolean;
|
|
113
|
+
promise: <T>(input: Promise<T> | (() => Promise<T>), messages: ToastPromiseMessages<T>, options?: ToastPromiseOptions) => Promise<T | undefined>;
|
|
87
114
|
dismiss: (id: string) => void;
|
|
88
115
|
clear: () => void;
|
|
89
116
|
pause: (id: string) => void;
|
|
@@ -97,3 +124,4 @@ export interface ToastApi {
|
|
|
97
124
|
* the server never creates the browser singleton.
|
|
98
125
|
*/
|
|
99
126
|
export declare const toast: ToastApi;
|
|
127
|
+
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { reportError } from "../error.js";
|
|
1
2
|
/** Errors and warnings linger: they carry something the reader has to act on. */
|
|
2
3
|
const DEFAULT_DURATIONS = {
|
|
3
4
|
info: 4000,
|
|
4
5
|
success: 3000,
|
|
5
6
|
warning: 5000,
|
|
6
7
|
error: 5000,
|
|
8
|
+
loading: Number.POSITIVE_INFINITY,
|
|
7
9
|
};
|
|
8
10
|
const DEFAULT_LIMIT = 5;
|
|
9
11
|
let nextSequence = 0;
|
|
@@ -41,18 +43,58 @@ export class ToastManager {
|
|
|
41
43
|
message,
|
|
42
44
|
description: options.description,
|
|
43
45
|
duration: options.duration ?? this.#durations[variant],
|
|
44
|
-
dismissible: options.dismissible ??
|
|
46
|
+
dismissible: options.dismissible ?? variant !== "loading",
|
|
45
47
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
46
48
|
};
|
|
47
49
|
this.#upsert(next);
|
|
48
50
|
this.#enforceLimit();
|
|
49
51
|
this.#restartClock(next);
|
|
52
|
+
const clock = this.#clocks.get(id);
|
|
53
|
+
if (clock)
|
|
54
|
+
clock.run = undefined;
|
|
50
55
|
return id;
|
|
51
56
|
};
|
|
52
57
|
info = (message, options) => this.show(message, options, "info");
|
|
53
58
|
success = (message, options) => this.show(message, options, "success");
|
|
54
59
|
warning = (message, options) => this.show(message, options, "warning");
|
|
55
60
|
error = (message, options) => this.show(message, options, "error");
|
|
61
|
+
loading = (message, options) => this.show(message, options, "loading");
|
|
62
|
+
update = (id, patch) => {
|
|
63
|
+
const existing = this.#toasts.find((toast) => toast.id === id);
|
|
64
|
+
if (!existing)
|
|
65
|
+
return false;
|
|
66
|
+
const variant = patch.variant ?? existing.variant;
|
|
67
|
+
const variantChanged = variant !== existing.variant;
|
|
68
|
+
const next = {
|
|
69
|
+
...existing,
|
|
70
|
+
variant,
|
|
71
|
+
message: patch.message ?? existing.message,
|
|
72
|
+
description: "description" in patch ? patch.description : existing.description,
|
|
73
|
+
duration: patch.duration ?? (variantChanged ? this.#durations[variant] : existing.duration),
|
|
74
|
+
dismissible: patch.dismissible ?? (variantChanged ? variant !== "loading" : existing.dismissible),
|
|
75
|
+
};
|
|
76
|
+
this.#upsert(next);
|
|
77
|
+
this.#restartClock(next);
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
promise = async (input, messages, options = {}) => {
|
|
81
|
+
const id = this.show(messages.loading, { ...options.loading, id: options.id }, "loading");
|
|
82
|
+
const run = {};
|
|
83
|
+
const clock = this.#clocks.get(id);
|
|
84
|
+
if (clock)
|
|
85
|
+
clock.run = run;
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = await (typeof input === "function" ? input() : input);
|
|
89
|
+
}
|
|
90
|
+
catch (failure) {
|
|
91
|
+
(options.onerror ?? reportError)(failure);
|
|
92
|
+
this.#settle(id, run, "error", messages.error, failure, options.error);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
this.#settle(id, run, "success", messages.success, value, options.success);
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
56
98
|
/** Remove one toast. Unknown ids are ignored, so a caller may dismiss twice. */
|
|
57
99
|
dismiss = (id) => {
|
|
58
100
|
this.#remove(id);
|
|
@@ -89,6 +131,20 @@ export class ToastManager {
|
|
|
89
131
|
clock.paused = false;
|
|
90
132
|
this.#start(id);
|
|
91
133
|
};
|
|
134
|
+
#settle(id, run, variant, content, input, options) {
|
|
135
|
+
if (this.#clocks.get(id)?.run !== run)
|
|
136
|
+
return;
|
|
137
|
+
let message;
|
|
138
|
+
try {
|
|
139
|
+
message = typeof content === "function" ? content(input) : content;
|
|
140
|
+
}
|
|
141
|
+
catch (failure) {
|
|
142
|
+
reportError(failure);
|
|
143
|
+
this.#remove(id);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.show(message, { ...options, id }, variant);
|
|
147
|
+
}
|
|
92
148
|
#upsert(toast) {
|
|
93
149
|
const index = this.#toasts.findIndex((current) => current.id === toast.id);
|
|
94
150
|
if (index === -1)
|
|
@@ -122,6 +178,7 @@ export class ToastManager {
|
|
|
122
178
|
// a success) must stay frozen; starting a fresh countdown under the cursor would take the
|
|
123
179
|
// message away without the reader ever leaving it.
|
|
124
180
|
paused: previous?.paused ?? false,
|
|
181
|
+
run: previous?.run,
|
|
125
182
|
});
|
|
126
183
|
this.#start(toast.id);
|
|
127
184
|
}
|
|
@@ -169,6 +226,9 @@ export const toast = {
|
|
|
169
226
|
success: (message, options) => getDefaultToastManager().success(message, options),
|
|
170
227
|
warning: (message, options) => getDefaultToastManager().warning(message, options),
|
|
171
228
|
error: (message, options) => getDefaultToastManager().error(message, options),
|
|
229
|
+
loading: (message, options) => getDefaultToastManager().loading(message, options),
|
|
230
|
+
update: (id, patch) => getDefaultToastManager().update(id, patch),
|
|
231
|
+
promise: (input, messages, options) => getDefaultToastManager().promise(input, messages, options),
|
|
172
232
|
dismiss: (id) => getDefaultToastManager().dismiss(id),
|
|
173
233
|
clear: () => getDefaultToastManager().clear(),
|
|
174
234
|
pause: (id) => getDefaultToastManager().pause(id),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface StateCodec<State, Serialized> {
|
|
2
|
+
createDefault: () => State;
|
|
3
|
+
serialize: (state: State) => Serialized;
|
|
4
|
+
parse: (serialized: Serialized) => State;
|
|
5
|
+
key: (state: State) => string;
|
|
6
|
+
normalize?: (state: State) => State;
|
|
7
|
+
}
|
|
8
|
+
export interface UrlStateCodec<State> extends StateCodec<State, URLSearchParams> {
|
|
9
|
+
ownedParams: () => string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ExternalStore<Serialized> {
|
|
12
|
+
read: () => Serialized;
|
|
13
|
+
write: (serialized: Serialized) => void;
|
|
14
|
+
subscribe: (onChange: () => void) => void;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ExternalStore, StateCodec, UrlStateCodec } from "./codec.js";
|
|
2
|
+
export { createParamsCodec, param, type NumberParamOptions, type ParamHandler, type ParamsCodec, type ParamsDefinition, type ScalarParamHandler, } from "./params-codec.js";
|
|
3
|
+
export { changedParams } from "./search-params.js";
|
|
4
|
+
export { SyncedState, type SyncedStateOptions } from "./synced-state.svelte.js";
|
|
5
|
+
export { createUrlState, urlStore, type ReplaceHistory, type UrlStateOptions, type UrlStoreOptions, } from "./url-store.svelte.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { UrlStateCodec } from "./codec.js";
|
|
2
|
+
export interface ParamHandler<T> {
|
|
3
|
+
readonly defaultValue: T;
|
|
4
|
+
read: (values: readonly string[]) => T;
|
|
5
|
+
write: (value: T) => string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ScalarParamHandler<Value, T extends Value | undefined = Value | undefined> extends ParamHandler<T> {
|
|
8
|
+
decode: (raw: string) => Value | undefined;
|
|
9
|
+
encode: (value: Value) => string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface NumberParamOptions {
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
integer?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare function string<const T extends string>(options: {
|
|
17
|
+
oneOf: readonly T[];
|
|
18
|
+
default: NoInfer<T>;
|
|
19
|
+
}): ScalarParamHandler<T, T>;
|
|
20
|
+
declare function string<const T extends string>(options: {
|
|
21
|
+
oneOf: readonly T[];
|
|
22
|
+
default?: undefined;
|
|
23
|
+
}): ScalarParamHandler<T, T | undefined>;
|
|
24
|
+
declare function string(options: {
|
|
25
|
+
default: string;
|
|
26
|
+
}): ScalarParamHandler<string, string>;
|
|
27
|
+
declare function string(options?: {
|
|
28
|
+
default?: undefined;
|
|
29
|
+
}): ScalarParamHandler<string, string | undefined>;
|
|
30
|
+
declare function number(options: NumberParamOptions & {
|
|
31
|
+
default: number;
|
|
32
|
+
}): ScalarParamHandler<number, number>;
|
|
33
|
+
declare function number(options?: NumberParamOptions & {
|
|
34
|
+
default?: undefined;
|
|
35
|
+
}): ScalarParamHandler<number, number | undefined>;
|
|
36
|
+
declare function boolean(options: {
|
|
37
|
+
default: boolean;
|
|
38
|
+
}): ScalarParamHandler<boolean, boolean>;
|
|
39
|
+
declare function boolean(options?: {
|
|
40
|
+
default?: undefined;
|
|
41
|
+
}): ScalarParamHandler<boolean, boolean | undefined>;
|
|
42
|
+
declare function date(options: {
|
|
43
|
+
default: Date;
|
|
44
|
+
}): ScalarParamHandler<Date, Date>;
|
|
45
|
+
declare function date(options?: {
|
|
46
|
+
default?: undefined;
|
|
47
|
+
}): ScalarParamHandler<Date, Date | undefined>;
|
|
48
|
+
declare function datetime(options: {
|
|
49
|
+
default: Date;
|
|
50
|
+
}): ScalarParamHandler<Date, Date>;
|
|
51
|
+
declare function datetime(options?: {
|
|
52
|
+
default?: undefined;
|
|
53
|
+
}): ScalarParamHandler<Date, Date | undefined>;
|
|
54
|
+
export declare const param: {
|
|
55
|
+
string: typeof string;
|
|
56
|
+
number: typeof number;
|
|
57
|
+
boolean: typeof boolean;
|
|
58
|
+
date: typeof date;
|
|
59
|
+
datetime: typeof datetime;
|
|
60
|
+
list: <Value>(item: {
|
|
61
|
+
decode: (raw: string) => Value | undefined;
|
|
62
|
+
encode: (value: Value) => string | undefined;
|
|
63
|
+
}) => ParamHandler<Value[]>;
|
|
64
|
+
};
|
|
65
|
+
export type ParamsDefinition<State> = {
|
|
66
|
+
[Field in keyof State]: ParamHandler<State[Field]>;
|
|
67
|
+
};
|
|
68
|
+
export interface ParamsCodec<State> extends UrlStateCodec<State> {
|
|
69
|
+
readonly definition: ParamsDefinition<State>;
|
|
70
|
+
normalize: (state: State) => State;
|
|
71
|
+
}
|
|
72
|
+
export declare const createParamsCodec: <State extends object>(definition: ParamsDefinition<State>) => ParamsCodec<State>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const scalar = (decode, encode, defaultValue) => {
|
|
2
|
+
const encodePresent = (value) => {
|
|
3
|
+
if (value === null || value === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
const encoded = encode(value);
|
|
6
|
+
return encoded === "" ? undefined : encoded;
|
|
7
|
+
};
|
|
8
|
+
const encodedDefault = encodePresent(defaultValue);
|
|
9
|
+
return {
|
|
10
|
+
defaultValue,
|
|
11
|
+
decode,
|
|
12
|
+
encode,
|
|
13
|
+
read: (values) => {
|
|
14
|
+
if (values.length === 0)
|
|
15
|
+
return defaultValue;
|
|
16
|
+
return (decode(values[0]) ?? defaultValue);
|
|
17
|
+
},
|
|
18
|
+
write: (value) => {
|
|
19
|
+
const encoded = encodePresent(value);
|
|
20
|
+
return encoded === undefined || encoded === encodedDefault ? [] : [encoded];
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
function string(options = {}) {
|
|
25
|
+
const { oneOf } = options;
|
|
26
|
+
const accepts = (value) => oneOf === undefined || oneOf.includes(value);
|
|
27
|
+
return scalar((raw) => (accepts(raw) ? raw : undefined), (value) => (typeof value === "string" && accepts(value) ? value : undefined), options.default);
|
|
28
|
+
}
|
|
29
|
+
function number(options = {}) {
|
|
30
|
+
const { min = -Infinity, max = Infinity, integer = false } = options;
|
|
31
|
+
const accepts = (value) => typeof value === "number" &&
|
|
32
|
+
Number.isFinite(value) &&
|
|
33
|
+
(!integer || Number.isInteger(value)) &&
|
|
34
|
+
value >= min &&
|
|
35
|
+
value <= max;
|
|
36
|
+
return scalar((raw) => {
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
if (trimmed === "")
|
|
39
|
+
return undefined;
|
|
40
|
+
const value = Number(trimmed);
|
|
41
|
+
return accepts(value) ? value : undefined;
|
|
42
|
+
}, (value) => (accepts(value) ? String(value) : undefined), options.default);
|
|
43
|
+
}
|
|
44
|
+
const decodeBoolean = (raw) => {
|
|
45
|
+
if (raw === "true")
|
|
46
|
+
return true;
|
|
47
|
+
if (raw === "false")
|
|
48
|
+
return false;
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
function boolean(options = {}) {
|
|
52
|
+
return scalar(decodeBoolean, (value) => (typeof value === "boolean" ? String(value) : undefined), options.default);
|
|
53
|
+
}
|
|
54
|
+
const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
|
|
55
|
+
const CALENDAR_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
56
|
+
const decodeCalendarDate = (raw) => {
|
|
57
|
+
const match = CALENDAR_DATE.exec(raw);
|
|
58
|
+
if (match === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
61
|
+
const date = new Date(0);
|
|
62
|
+
date.setFullYear(year, month - 1, day);
|
|
63
|
+
date.setHours(0, 0, 0, 0);
|
|
64
|
+
const sameDay = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
|
|
65
|
+
return sameDay ? date : undefined;
|
|
66
|
+
};
|
|
67
|
+
const pad = (value, width) => String(value).padStart(width, "0");
|
|
68
|
+
const encodeCalendarDate = (value) => {
|
|
69
|
+
if (!isValidDate(value))
|
|
70
|
+
return undefined;
|
|
71
|
+
const year = value.getFullYear();
|
|
72
|
+
if (year < 0 || year > 9999)
|
|
73
|
+
return undefined;
|
|
74
|
+
return `${pad(year, 4)}-${pad(value.getMonth() + 1, 2)}-${pad(value.getDate(), 2)}`;
|
|
75
|
+
};
|
|
76
|
+
function date(options = {}) {
|
|
77
|
+
return scalar(decodeCalendarDate, encodeCalendarDate, options.default);
|
|
78
|
+
}
|
|
79
|
+
const decodeInstant = (raw) => {
|
|
80
|
+
const time = Date.parse(raw);
|
|
81
|
+
return Number.isNaN(time) ? undefined : new Date(time);
|
|
82
|
+
};
|
|
83
|
+
const encodeInstant = (value) => isValidDate(value) ? value.toISOString() : undefined;
|
|
84
|
+
function datetime(options = {}) {
|
|
85
|
+
return scalar(decodeInstant, encodeInstant, options.default);
|
|
86
|
+
}
|
|
87
|
+
const list = (item) => ({
|
|
88
|
+
defaultValue: [],
|
|
89
|
+
read: (values) => values.map((raw) => item.decode(raw)).filter((value) => value !== undefined),
|
|
90
|
+
write: (value) => (Array.isArray(value) ? value : [])
|
|
91
|
+
.filter((entry) => entry !== null && entry !== undefined)
|
|
92
|
+
.map((entry) => item.encode(entry))
|
|
93
|
+
.filter((encoded) => encoded !== undefined && encoded !== ""),
|
|
94
|
+
});
|
|
95
|
+
export const param = { string, number, boolean, date, datetime, list };
|
|
96
|
+
export const createParamsCodec = (definition) => {
|
|
97
|
+
const fields = Object.keys(definition);
|
|
98
|
+
const build = (valueOf) => structuredClone(Object.fromEntries(fields.map((field) => [field, valueOf(field)])));
|
|
99
|
+
const serialize = (state) => {
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
for (const field of fields)
|
|
102
|
+
for (const value of definition[field].write(state[field]))
|
|
103
|
+
params.append(field, value);
|
|
104
|
+
return params;
|
|
105
|
+
};
|
|
106
|
+
const parse = (params) => build((field) => definition[field].read(params.getAll(field)));
|
|
107
|
+
return {
|
|
108
|
+
definition,
|
|
109
|
+
ownedParams: () => [...fields],
|
|
110
|
+
createDefault: () => build((field) => definition[field].defaultValue),
|
|
111
|
+
serialize,
|
|
112
|
+
parse,
|
|
113
|
+
normalize: (state) => parse(serialize(state)),
|
|
114
|
+
key: (state) => serialize(state).toString(),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const pickParams: (params: URLSearchParams, names: readonly string[]) => URLSearchParams;
|
|
2
|
+
export declare const withOwnedParams: (url: URL, owned: readonly string[], params: URLSearchParams) => URL;
|
|
3
|
+
export declare const sameLocation: (a: URL, b: URL) => boolean;
|
|
4
|
+
export declare const changedParams: (previous: URLSearchParams, next: URLSearchParams) => string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const pickParams = (params, names) => {
|
|
2
|
+
const picked = new URLSearchParams();
|
|
3
|
+
for (const [name, value] of params)
|
|
4
|
+
if (names.includes(name))
|
|
5
|
+
picked.append(name, value);
|
|
6
|
+
return picked;
|
|
7
|
+
};
|
|
8
|
+
export const withOwnedParams = (url, owned, params) => {
|
|
9
|
+
const next = new URL(url);
|
|
10
|
+
for (const name of owned)
|
|
11
|
+
next.searchParams.delete(name);
|
|
12
|
+
for (const [name, value] of params)
|
|
13
|
+
next.searchParams.append(name, value);
|
|
14
|
+
return next;
|
|
15
|
+
};
|
|
16
|
+
const sortedSearch = (params) => {
|
|
17
|
+
const sorted = new URLSearchParams(params);
|
|
18
|
+
sorted.sort();
|
|
19
|
+
return sorted.toString();
|
|
20
|
+
};
|
|
21
|
+
export const sameLocation = (a, b) => a.origin === b.origin &&
|
|
22
|
+
a.pathname === b.pathname &&
|
|
23
|
+
a.hash === b.hash &&
|
|
24
|
+
sortedSearch(a.searchParams) === sortedSearch(b.searchParams);
|
|
25
|
+
export const changedParams = (previous, next) => {
|
|
26
|
+
const names = new Set([...previous.keys(), ...next.keys()]);
|
|
27
|
+
return [...names].filter((name) => JSON.stringify(previous.getAll(name)) !== JSON.stringify(next.getAll(name)));
|
|
28
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExternalStore, StateCodec } from "./codec.js";
|
|
2
|
+
export interface SyncedStateOptions<State, Serialized = never> {
|
|
3
|
+
codec: StateCodec<State, Serialized>;
|
|
4
|
+
store?: ExternalStore<Serialized>;
|
|
5
|
+
onSynced?: () => void;
|
|
6
|
+
resetOnCommit?: readonly (keyof State)[];
|
|
7
|
+
}
|
|
8
|
+
export declare class SyncedState<State, Serialized = never> {
|
|
9
|
+
#private;
|
|
10
|
+
draft: State;
|
|
11
|
+
committed: State;
|
|
12
|
+
constructor(options: SyncedStateOptions<State, Serialized>);
|
|
13
|
+
get isDirty(): boolean;
|
|
14
|
+
commit: () => void;
|
|
15
|
+
update: (change: (state: State) => State) => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
revert: () => void;
|
|
18
|
+
normalized: () => State;
|
|
19
|
+
key: () => string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
export class SyncedState {
|
|
3
|
+
draft;
|
|
4
|
+
committed;
|
|
5
|
+
#codec;
|
|
6
|
+
#store;
|
|
7
|
+
#onSynced;
|
|
8
|
+
#resetOnCommit;
|
|
9
|
+
#hasSynced = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.#codec = options.codec;
|
|
12
|
+
this.#store = options.store;
|
|
13
|
+
this.#onSynced = options.onSynced;
|
|
14
|
+
this.#resetOnCommit = options.resetOnCommit ?? [];
|
|
15
|
+
const initial = this.#codec.createDefault();
|
|
16
|
+
this.draft = $state(structuredClone(initial));
|
|
17
|
+
this.committed = $state(structuredClone(initial));
|
|
18
|
+
this.#store?.subscribe(this.#syncFromStore);
|
|
19
|
+
}
|
|
20
|
+
get isDirty() {
|
|
21
|
+
return this.#identity(this.#snapshot(this.draft)) !== this.key();
|
|
22
|
+
}
|
|
23
|
+
commit = () => {
|
|
24
|
+
const previous = this.#snapshot(this.committed);
|
|
25
|
+
const drafted = this.#snapshot(this.draft);
|
|
26
|
+
const next = this.#normalize(this.#movedBeyondResetFields(previous, drafted) ? this.#withResetFields(drafted) : drafted);
|
|
27
|
+
this.committed = next;
|
|
28
|
+
this.draft = structuredClone(next);
|
|
29
|
+
this.#writeStore();
|
|
30
|
+
};
|
|
31
|
+
update = (change) => {
|
|
32
|
+
const previous = this.#snapshot(this.committed);
|
|
33
|
+
const committed = this.#snapshot(change(this.#snapshot(this.committed)));
|
|
34
|
+
const draft = this.#snapshot(change(this.#snapshot(this.draft)));
|
|
35
|
+
const moved = this.#movedBeyondResetFields(previous, committed);
|
|
36
|
+
this.committed = this.#normalize(moved ? this.#withResetFields(committed) : committed);
|
|
37
|
+
this.draft = moved ? this.#withResetFields(draft) : draft;
|
|
38
|
+
this.#writeStore();
|
|
39
|
+
};
|
|
40
|
+
reset = () => {
|
|
41
|
+
const defaults = this.#codec.createDefault();
|
|
42
|
+
this.committed = structuredClone(defaults);
|
|
43
|
+
this.draft = structuredClone(defaults);
|
|
44
|
+
this.#writeStore();
|
|
45
|
+
};
|
|
46
|
+
revert = () => {
|
|
47
|
+
this.draft = this.#snapshot(this.committed);
|
|
48
|
+
};
|
|
49
|
+
normalized = () => this.#normalize(this.#snapshot(this.committed));
|
|
50
|
+
key = () => this.#codec.key(this.normalized());
|
|
51
|
+
#syncFromStore = () => {
|
|
52
|
+
if (this.#store === undefined)
|
|
53
|
+
return;
|
|
54
|
+
const incoming = this.#normalize(this.#codec.parse(this.#store.read()));
|
|
55
|
+
if (this.#hasSynced && this.#identity(incoming) === this.key())
|
|
56
|
+
return;
|
|
57
|
+
this.#hasSynced = true;
|
|
58
|
+
this.committed = incoming;
|
|
59
|
+
this.draft = structuredClone(incoming);
|
|
60
|
+
if (this.#onSynced)
|
|
61
|
+
untrack(this.#onSynced);
|
|
62
|
+
};
|
|
63
|
+
#writeStore() {
|
|
64
|
+
this.#store?.write(this.#codec.serialize(this.normalized()));
|
|
65
|
+
}
|
|
66
|
+
#movedBeyondResetFields(previous, next) {
|
|
67
|
+
if (this.#resetOnCommit.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
return (this.#identity(this.#withResetFields(previous)) !==
|
|
70
|
+
this.#identity(this.#withResetFields(next)));
|
|
71
|
+
}
|
|
72
|
+
#withResetFields(state) {
|
|
73
|
+
const defaults = this.#codec.createDefault();
|
|
74
|
+
const result = structuredClone(state);
|
|
75
|
+
for (const field of this.#resetOnCommit)
|
|
76
|
+
result[field] = defaults[field];
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
#identity(state) {
|
|
80
|
+
return this.#codec.key(this.#normalize(state));
|
|
81
|
+
}
|
|
82
|
+
#normalize(state) {
|
|
83
|
+
return this.#codec.normalize ? this.#codec.normalize(state) : state;
|
|
84
|
+
}
|
|
85
|
+
#snapshot(value) {
|
|
86
|
+
return $state.snapshot(value);
|
|
87
|
+
}
|
|
88
|
+
}
|